cloudflare / cloudflare/agents
Think: no server-side path to write assistant-message metadata (messageMetadata dropped through toUIMessageStream)
- Dominant language
- TypeScript
- Stars
- 5.6k
- Forks
- 711
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 53
Description
## Summary
There's no supported server-side path to write **per-message metadata onto an assistant message when running on `Think`**. Base `AIChatAgent` + `streamText` already supports the AI SDK `messageMetadata` callback (confirmed in #466), but `Think` wraps the stream and only forwards `{ sendReasoning, onError }`, so the `messageMetadata` callback is dropped on the way through. The result: an app on `Think` can attach metadata to user messages but not to assistant messages.
## Motivating use case
Show a per-message `createdAt` timestamp in the chat UI.
- **User messages** — works today. `Think` preserves incoming `message.metadata` (it spreads `...message.metadata` when stamping `channel`), so a client can attach `metadata: { createdAt }` at `sendMessage` time and it survives persistence + round-trips.
- **Assistant messages** — no supported write hook. The only per-message time that exists is the internal `cf_ai_chat_agent_messages.created_at` column, which is used purely for `order by` and is **not** exposed in the `UIMessage` objects the client receives.
- **Historical messages** — even if a write hook is added going forward, already-stored messages have no `metadata`, only the `created_at` column, so they can't show a timestamp without that column being surfaced.
## Where the gap is
`Think`'s stream contract exposes only:
```ts
// StreamableResult (think.ts)
toUIMessageStream(options?: {
sendReasoning?: boolean;
onError?: (error: unknown) => string;
}): AsyncIterable;
```
and the default wrapper calls `result.toUIMessageStream({ sendReasoning, onError })` — the AI SDK `messageMetadata` callback that base `AIChatAgent` supports never gets a chance to run. So from a `Think` turn (`onChatMessage` / `runTurn`) there's no supported way to stamp metadata onto the assistant message the framework persists.
Today the only escape hatch is to mutate the final assistant message and call `persistMessages()` / `saveMessages()` manually — essentially the same class of workaround people landed on in #466 (appending a `[METADATA]:` marker to text and parsing it client-side).
## Requests
1. **Forward `messageMetadata` through `Think`'s `toUIMessageStream`** (or expose an assistant-turn metadata write hook), so a `Think` turn can attach structured metadata to the assistant message it persists — parity with what base `AIChatAgent` + `streamText` already allow.
2. **(Optional) Surface the existing `cf_ai_chat_agent_messages.created_at`** on messages exposed to the client, so historical assistant messages (which have no `metadata`) can also display a timestamp.
## Related
- #1676 — First-class events, render-only messages, and typed metadata in Think. That issue covers *typing* the opaque `metadata` and preserving it across persistence; this issue is about the missing *write path* for assistant-message metadata on `Think` specifically. Complementary — a typed-metadata story is most useful once there's a supported way to write it from a turn.
- #466 — Appending metadata from the server in `onChatMessage`. Closed as not-priority, and scoped to `generateText` on base `AIChatAgent`; the maintainer confirmed there that `streamText`'s `messageMetadata` callback works on base `AIChatAgent`. This issue is the `Think`-layer regression of that same capability.
## Environment
- `@cloudflare/think` + `@cloudflare/ai-chat` (AIChatAgent), AI SDK v5, `streamText`.
Contributor guide
Assessment
This issue has not been assessed yet.