Skip to content

withPersistence does not persist structured-output finalization results#1072

Description

@kolaworld

TanStack AI version

0.43.0

Framework/Library version

@tanstack/ai-client: 0.23.0 | @tanstack/ai-groq: 0.5.4 | @tanstack/ai-persistence: 0.1.0 | @tanstack/ai-svelte: 0.16.0

Describe the bug and the steps to reproduce it

Issue

TanStack AI has two structured-output modes:

  • Native combined mode: the adapter declares supportsCombinedToolsAndSchema(), schema-constrained JSON is produced as the agent loop's final text, and that text lives in accumulatedContent / FinishInfo.content.
  • Separate finalization mode: tools and reasoning run first, then a second structured-output request runs. This is the path required for Groq.

Groq's API rejects response_format: json_schema together with tools and stream, so @tanstack/ai-groq forces the separate finalization path in text.ts#L109-L117.

On that finalization path, the engine deliberately does not fold finalization TEXT_MESSAGE_CONTENT deltas into accumulatedContent. The source comment says doing so would pollute agent-loop state and leak raw JSON into FinishInfo.content on onFinish (index.ts#L2913-L2924). Middleware docs match that contract: info.content is the agent loop's text; the structured result is delivered via the structured-output.complete CUSTOM event observed in onChunk with ctx.phase === 'structuredOutput'.

withPersistence only appends the terminal assistant turn from FinishInfo.content in finishedTranscript() (middleware.ts#L426-L459, used from onFinish at L1603-L1616). FinishInfo itself has no structured-output field (types.ts#L383-L392).

So on Groq (and any other separate-finalization adapter), the structured result streams live to the client but is not written into the persisted ModelMessage[] transcript. Native-combined adapters often still "persist" the JSON only because it happens to be info.content.

This does not seem like a Groq emission bug. Keeping finalization JSON out of accumulatedContent is intentional phase separation. The gap is that persistence has no alternate path for the structured result after finalization.

Your Minimal, Reproducible Example - (Sandbox Highly Recommended)

https://stackblitz.com/edit/vitejs-vite-ibnwwemz?file=package.json,index.html,src%2Fmain.ts

Terms & Code of Conduct

  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions