Repository navigation
think: write assistant-message metadata via forwarded messageMetadata (#1873) - #2321
Merged
Merged
Conversation
…tadata (#1873) Think wrapped the AI SDK stream and forwarded only { sendReasoning, onError } through toUIMessageStream, dropping the messageMetadata callback that base AIChatAgent + streamText accept. A turn could stamp metadata on user messages but not on the assistant message Think persists. Add an instance-level `messageMetadata` property and a per-turn `TurnConfig.messageMetadata` override, resolved as `config.messageMetadata ?? this.messageMetadata` and forwarded through the wrapped toUIMessageStream. The new `MessageMetadataCallback<Metadata = Record<string, unknown>>` type carries a defaulted generic so the typed-metadata story in #1676 can narrow the return type without a breaking change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion tests - Keep messageMetadata off the public StreamableResult options; the wrapper forwards the resolved per-turn/instance writer directly. - Test the live start/finish chunks, the sub-agent chat() RPC path, and that an auto-continuation resolves the writer again and persists its own assistant message. - Correct docs: the callback runs for every part; drop the unverified stall/error survival claim; document continuation behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: 3472376 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
⚪ agents import sizesMeasured 343 runtime imports as minified bundles. The primary size is gzip; raw minified size is included for diagnosis. An existing import growing by more than 10% is marked red. This report is informational.
Compared No import sizes changed. All 343 current runtime imports
Reported by agent-think[bot]. |
agents
@cloudflare/ai-chat
@cloudflare/codemode
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
Merged
This was referenced Sep 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1873.
Problem
Thinkwraps the AI SDKstreamTextresult and callsresult.toUIMessageStream({ sendReasoning, onError })itself. That drops the AI SDKmessageMetadatacallback, which baseAIChatAgent+streamTextalready support. So onThink, a turn can stamp metadata on user messages (the client sends it and Think keeps it), but there is no supported way to write metadata onto the assistant message Think persists. The motivating case is a per-messagecreatedAttimestamp in the chat UI.Fix
The downstream plumbing already worked. The shared
StreamAccumulator(agents/chat) mergesmessageMetadatafromstart,finishandmessage-metadatachunks into the persisted message, and the AI SDK client applies them live. The only missing piece was passing the callback intotoUIMessageStream.Think#messageMetadatafor metadata that applies to every turn.TurnConfig.messageMetadatareturned frombeforeTurnfor per-turn metadata. It is resolved asconfig.messageMetadata ?? this.messageMetadata, the same pattern assendReasoning.MessageMetadataCallback<Metadata = Record<string, unknown>>. It matches the AI SDK signature({ part }) => metadata | undefined. The defaulted generic leaves room for the typed-metadata work in First-class events, render-only messages, and typed metadata in Think #1676 without a breaking change.streamTextwrapper, so it applies to both the WebSocket chat path (_streamResult) and the sub-agentchat()RPC path (_streamResultToRpcCallback).Behavior worth knowing
start/finishride on those chunks; results for other parts are sent asmessage-metadatachunks. Every non-undefinedreturn is shallow-merged into the message's metadata.beforeTurnruns again withctx.continuation: true, and the continuation persists as a separate assistant message with its own metadata. The test below locks this in._pipelineExtensionBeforeTurnonly merges an allow-list of serializable fields, so an extension can't set this field, and amessageMetadataset by the subclass is kept.Credit
The first commit is @abhagsain's branch from the issue thread (compare), cherry-picked with authorship kept. It builds on the minimal patch @O4epegb posted. The second commit tightens it:
messageMetadatafrom the publicStreamableResult.toUIMessageStreamoptions. Nothing in Think passes it, so the "caller option wins" branch was dead code, and it widened a public interface that customonChatMessageimplementations satisfy.chat()RPC path, and the continuation behavior. The original tests only checked persistence over WebSocket.start/finish. I dropped the claim that astartstamp "survives a turn that stalls or errors", which no test covers, and documented the continuation behavior.Tests
packages/think/src/tests/assistant-message-metadata.test.ts(7 tests):start+finishmetadata are merged onto the persisted assistant message.start/finishchunks broadcast to the WebSocket client carrymessageMetadata.ctx.continuation), and each assistant message has its own metadata.chat()RPC path: the streamedstartchunk and the persisted message both carry the metadata.With the forwarding line removed, 5 of the 6 tests that expect metadata fail; the opt-in test passes either way, as it should. The full Think workers suite passes (943 tests), and
pnpm run checkis clean.Out of scope
Request 2 in the issue, exposing
cf_ai_chat_agent_messages.created_aton messages sent to the client for historical messages, is left out. It changes the message shape for every client, and it's cleaner as its own decision, alongside #1676's typed-metadata story.