TanStack AI version
0.48.0
Framework/Library version
@tanstack/ai-client: 0.26.0 | @tanstack/ai-persistence: 0.5.0
Describe the bug and the steps to reproduce it
This regression was introduced by PR #1174 (1c0415b), which moved AG-UI extras under metadata.tanstack and replaced message object spreads with spec-only wire projections. Two conversion paths are incomplete.
Inbound: createdAt is not restored on ModelMessage
uiMessagesToWire() serializes UIMessage.createdAt as an ISO-8601 string in metadata.tanstack.createdAt (ag-ui-wire.ts#L149-L178), and chatParamsFromRequestBody() preserves that metadata (chat-params.ts#L113-L127).
For a text-only AG-UI user message, convertMessagesToModelMessages() then reaches its default branch and casts the wire message to ModelMessage without restoring createdAt (messages.ts#L118-L226). The runtime object still has metadata, but ModelMessage defines createdAt and does not define message-level metadata (types.ts#L363-L395).
reconstructChat() converts stored model messages with modelMessagesToUIMessages() (reconstruct.ts#L118-L135). modelMessageToUIMessage() restores only top-level modelMessage.createdAt and does not copy the undeclared metadata property (messages.ts#L601-L608).
The client-to-server request therefore carries the original timestamp, but the canonical model message and the reconstructed UI message do not.
Outbound: tool ModelMessage fields are removed
The public connection adapter contract accepts UIMessage[] | ModelMessage[] (connection-adapters.md#L556-L590), but buildRunAgentInputBody() casts every input to UIMessage[] before calling uiMessagesToWire() (connection-adapters.ts#L1219-L1227).
uiMessagesToWire() handles system and user explicitly, then treats every other message as an assistant message. A tool ModelMessage has no parts, so that path produces no generated content or tool call, and toAnchor() then keeps only its allowlisted fields and drops content and toolCallId (ag-ui-wire.ts#L62-L168).
{ id: "result-1", role: "tool", toolCallId: "call-1", content: "[]" } is therefore serialized as { id: "result-1", role: "tool" }, and chatParamsFromRequestBody() rejects it because AG-UI tool messages require both content and toolCallId (chat-params.ts#L78-L98).
Expected behavior
convertMessagesToModelMessages() should restore a valid metadata.tanstack.createdAt ISO string as ModelMessage.createdAt, matching the existing snapshot normalization. Invalid timestamps should be ignored consistently.
The built-in connection adapters should serialize both input types in their public contract. A tool ModelMessage should produce a valid AG-UI tool message that retains id, content, and toolCallId.
Both directions should round-trip their canonical fields: a UIMessage creation time should survive wire conversion and persistence reconstruction, and a tool ModelMessage should survive wire conversion and server parsing.
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
https://stackblitz.com/edit/vitejs-vite-g7mtazna?file=package.json,index.html,src%2Fmain.ts
Do you intend to try to help solve this bug with your own PR?
Yes, I am also opening a PR that solves the problem along side this issue
Terms & Code of Conduct
TanStack AI version
0.48.0
Framework/Library version
@tanstack/ai-client: 0.26.0 | @tanstack/ai-persistence: 0.5.0
Describe the bug and the steps to reproduce it
This regression was introduced by PR #1174 (
1c0415b), which moved AG-UI extras undermetadata.tanstackand replaced message object spreads with spec-only wire projections. Two conversion paths are incomplete.Inbound:
createdAtis not restored onModelMessageuiMessagesToWire()serializesUIMessage.createdAtas an ISO-8601 string inmetadata.tanstack.createdAt(ag-ui-wire.ts#L149-L178), andchatParamsFromRequestBody()preserves that metadata (chat-params.ts#L113-L127).For a text-only AG-UI user message,
convertMessagesToModelMessages()then reaches its default branch and casts the wire message toModelMessagewithout restoringcreatedAt(messages.ts#L118-L226). The runtime object still hasmetadata, butModelMessagedefinescreatedAtand does not define message-level metadata (types.ts#L363-L395).reconstructChat()converts stored model messages withmodelMessagesToUIMessages()(reconstruct.ts#L118-L135).modelMessageToUIMessage()restores only top-levelmodelMessage.createdAtand does not copy the undeclared metadata property (messages.ts#L601-L608).The client-to-server request therefore carries the original timestamp, but the canonical model message and the reconstructed UI message do not.
Outbound: tool
ModelMessagefields are removedThe public connection adapter contract accepts
UIMessage[] | ModelMessage[](connection-adapters.md#L556-L590), butbuildRunAgentInputBody()casts every input toUIMessage[]before callinguiMessagesToWire()(connection-adapters.ts#L1219-L1227).uiMessagesToWire()handlessystemanduserexplicitly, then treats every other message as an assistant message. A toolModelMessagehas noparts, so that path produces no generated content or tool call, andtoAnchor()then keeps only its allowlisted fields and dropscontentandtoolCallId(ag-ui-wire.ts#L62-L168).{ id: "result-1", role: "tool", toolCallId: "call-1", content: "[]" }is therefore serialized as{ id: "result-1", role: "tool" }, andchatParamsFromRequestBody()rejects it because AG-UI tool messages require bothcontentandtoolCallId(chat-params.ts#L78-L98).Expected behavior
convertMessagesToModelMessages()should restore a validmetadata.tanstack.createdAtISO string asModelMessage.createdAt, matching the existing snapshot normalization. Invalid timestamps should be ignored consistently.The built-in connection adapters should serialize both input types in their public contract. A tool
ModelMessageshould produce a valid AG-UI tool message that retainsid,content, andtoolCallId.Both directions should round-trip their canonical fields: a
UIMessagecreation time should survive wire conversion and persistence reconstruction, and a toolModelMessageshould survive wire conversion and server parsing.Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
https://stackblitz.com/edit/vitejs-vite-g7mtazna?file=package.json,index.html,src%2Fmain.ts
Do you intend to try to help solve this bug with your own PR?
Yes, I am also opening a PR that solves the problem along side this issue
Terms & Code of Conduct