cloudflare / cloudflare/agents

Think: no server-side path to write assistant-message metadata (messageMetadata dropped through toUIMessageStream)

Open
#1,873 4 comments 1 reaction 1 assignee Claimed by @threepointone View on GitHub
think
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.