bug(e2e): multi-turn structured output regresses for Anthropic on native combined path (#605 follow-up)
@AlemTuzlak is already working on this.
Since Sep 17, 2026.
- Dominant language
- TypeScript
- Stars
- 3.1k
- Forks
- 331
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 160
Description
Symptom
When the engine takes the #605 native combined path for Anthropic (Claude 4.5+), the multi-turn structured output E2E test fails on the second turn:
```
Locator: getByTestId('structured-output-part').nth(1)
Expected substring: "Vegan"
Received string: "{"title":"Classic Spaghetti Pomodoro",...}"
```
The second turn's rendered structured-output part shows the first turn's content (Pomodoro) instead of the new content (Vegan). Both `structured-output-part` elements exist on the page (so `toHaveCount(2)` passes), but both contain identical turn-1 data.
The same test passes for every other provider in the matrix:
- ✅ openai (also native combined path)
- ✅ gemini, ollama, groq, grok, openrouter (legacy finalization path)
Test: `testing/e2e/tests/multi-turn-structured.spec.ts:28` — "anthropic — multi-turn-structured › every assistant turn keeps its own typed structured-output part".
Anthropic is currently excluded from `feature-support.ts`'s `'multi-turn-structured'` set as a temporary workaround so CI stays green while this is investigated.
Hypotheses (none verified)
The failure is specific to Anthropic on the native combined path — that's the only thing that changed for this provider in #605. Suspects:
-
aimock-matching desync. The assistant message in turn 2's history shape changed from a `tool_use` block (pre-#605 forced-tool workaround) to plain text content (post-#605 schema-constrained text). aimock's matcher uses the last user message text from the converted completion-request shape, which should still resolve correctly — but maybe the conversion differs in a subtle way that causes the wrong fixture to win.
-
Engine state bleed. `harvestCombinedStructuredOutput` reads `this.accumulatedContent` per engine instance. Per-call instances should isolate this, but worth verifying with a unit test that exercises two sequential `chat({ outputSchema, messages: [u1, a1, u2] })` calls against a mock adapter.
-
Client-side StreamProcessor binding. The structured-output.start event carries `messageId` derived from the adapter's `TEXT_MESSAGE_START.messageId`. If both turns somehow ended up using the same id (deterministic generator?), the client would attach turn 2's part to turn 1's message. The adapter uses `generateId(this.name)` per call — should be unique, but worth checking.
-
structured-output part rendering in useChat. The `structured-output` part type may be matched / replaced by id rather than appended per message. If the messageId-binding is wrong (per hypothesis 3) the part could overwrite incorrectly.
Reproducer
`pnpm --filter @tanstack/ai-e2e test:e2e -- --grep "anthropic — multi-turn-structured"`
The test is currently gated out of the matrix; restore `'anthropic'` to the `'multi-turn-structured'` set in `testing/e2e/src/lib/feature-support.ts` to re-enable.
Suggested approach
- Write a vitest unit test in `packages/typescript/ai/tests` that simulates the multi-turn anthropic native-combined-mode flow against a mocked adapter (two sequential `chat()` calls with the previous JSON text in conversation history). If the structured-output.complete events carry the right per-turn data, the issue is downstream (client-side rendering or aimock). If they don't, narrow to the engine's harvest path.
- If the engine path looks correct, capture the actual aimock fixture-match log when the test runs locally (`LLMOCK_VERBOSE=1` if supported, or temporarily instrument the e2e test page to log the request body / response per turn).
Once root cause is identified, restore `'anthropic'` to the multi-turn-structured matrix.
Out of scope for this issue
- Other adapters or feature areas. Single-turn structured output for Anthropic continues to work via native combined mode (passes structured-output.spec).
- The OpenRouter per-request combined-mode lookup (tracked in #612).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.