CaseAgent refactor diagnosis

The draft is valid-looking. The handoff is not.

Why submit_case_draft repeatedly rejects proposals during the workflow-to-agent migration—and how to fix the protocol instead of teaching the model to guess object shapes.

Short version: the previous workflow validated structured proposal output at generation time. The new native-agent path delegates to a free-form subagent and forwards its answer as unknown. Strict validation happens only afterward, where every failure is collapsed into one generic error.

1. What Dane is seeing

A reasonable draft is generated

Routing and proposal-writing appear to complete. The answer can look perfectly usable to a human reviewer.

Submission rejects it

Proposal writer returned an invalid reply result.

The message does not say which field or structural boundary failed.

The coordinator guesses

It retries plain text, nested reply objects, textBody, wrappers, and other shapes. They all fail for the same hidden reason.

This is not caused by the missing public endpoint. Public routing affects whether the browser can observe the agent. The draft rejection happens entirely inside the agent's generation and persistence path.

2. The old workflow had a structured-output boundary

The proposal skill was invoked with the exact mode-specific schema. Flue returned validated data.

1

Build schema

buildProposalResultSchema(...)

2

Generate with schema

session.skill('proposal', { result: resultSchema })

3

Receive validated object

const { data } = ...

const resultSchema = buildProposalResultSchema(generationMode, options)

const { data } = await session.skill('proposal', {
  args: skillArgs,
  result: resultSchema,
  tools,
})

return data // already schema-valid

flue/src/proposal-core.ts:307-344

3. The native-agent path lost that boundary

1

Delegate

The coordinator sends prepared context to proposal-writer.

2

Free-form result

The subagent profile has instructions and tools, but no result schema.

3

Forward unknown

submit_case_draft accepts result: unknown and forwards it unchanged.

What the coordinator may receive

JSON stringfenced JSONprose{ data: ... }{ result: ... }incomplete object
"{\"generationMode\":\"reply\", ...}"

{ data: { generationMode: "reply", ... } }

```json
{ "generationMode": "reply", ... }
```

What validation expects

The raw proposal object itself, with all required and cross-validated fields.

{
  generationMode: "reply",
  body: "...",
  reasoning: "...",
  confidence: "medium",
  proposalKind: "reply",
  detectedLanguage: "en",
  supportAction: "...",
  isStaleClosureTemplate: false,
  surfaceId: "...",
  primaryJourneyId: "...",
  relatedJourneyIds: []
}

flue/src/case-agent-profiles.ts:34-39 · flue/src/case-agent-agent-tools.ts:25-28, 111-118

4. The first strict check happens too late

const parsed = v.safeParse(
  buildProposalResultSchema('reply', options),
  result,
)

if (!parsed.success) {
  throw new Error(
    'Proposal writer returned an invalid reply result.',
  )
}

flue/src/case-agent-reply-runtime.ts:261-280

Why repeated retries do not help

  • The useful Valibot issues are discarded.
  • Root type, wrapper, missing-field, enum, and routing failures all look identical.
  • The coordinator cannot know how to repair the result.
  • It guesses schemas that were never the contract.

What the error actually means

Only this:

The value passed as result did not satisfy the complete, mode-specific proposal schema.

It does not establish that the customer-facing draft text itself was bad.

5. How to confirm the diagnosis safely

One failed trace is enough. Capture structure only—no customer text, proposal values, tokens, or tool results.

CaptureWhy it matters
typeof resultA root string instead of object confirms the most likely mismatch.
Array.isArray(result)Rules out another root-shape mismatch.
Top-level keys onlyKeys such as data, result, or output reveal a wrapper.
Valibot issue paths/typesDistinguishes root shape, missing fields, invalid enum, and routing failures.
stage, workflowMode, generationModeDetects reply/engineering and proposal-kind mismatches.
Structural hash across retriesShows whether the coordinator resubmits the same invalid shape.
{
  resultType: typeof result,
  resultIsArray: Array.isArray(result),
  resultKeys: isRecord(result) ? Object.keys(result) : [],
  validationIssues: parsed.issues.map(issue => ({
    path: boundedIssuePath(issue),
    type: issue.type,
  })),
  stage: runtime.stage,
  workflowMode: runtime.workflowMode,
  generationMode: runtime.generationMode,
}
Do not log values. Keep customer text, draft bodies, context tokens, IDs, tool arguments, and tool results out of this diagnostic.

6. Best fix: restore trusted, schema-bound generation

Do not make the coordinator reverse-engineer an internal object contract. Move generation and submission behind one trusted tool boundary.

1

Trusted preparation

Load case context, mode, routing, and verified tool scope.

2

Schema-bound generation

Invoke the proposal skill with result: buildProposalResultSchema(...).

3

Trusted persistence

Validate, canonicalize, and persist the returned data directly.

Recommended coordinator tool

generate_and_submit_case_draft({
  draftAttemptId,
  routingSelection,
})
  1. Read trusted prepared context.
  2. Build the mode-specific schema.
  3. Invoke the proposal skill with that schema.
  4. Receive validated data.
  5. Canonicalize and persist it.
  6. Return only accepted or a bounded continue instruction.

Avoid these “fixes”

  • Do not ask the coordinator to try more object shapes.
  • Do not parse arbitrary prose as JSON.
  • Do not silently unwrap a growing list of guessed keys.
  • Do not weaken the proposal schema.
  • Do not expose customer values in validation telemetry.
Immediate diagnostic improvement: preserve bounded validation issue paths and structural categories in the error/Sentry event. This will make the hypothesis easy to confirm, but it is not a substitute for restoring schema-bound generation.

Bottom line

The customer-facing draft can be good while the machine-to-machine handoff is invalid.

The refactor changed the proposal boundary from “validated structured data” to “free-form subagent answer.” Restore that boundary in trusted code, and the repeated submit_case_draft failures should disappear without teaching the model any new schema-guessing behavior.