Skip to content

Commit c62b5e2

Browse files
committed
document the writer stuff
1 parent a78bc05 commit c62b5e2

File tree

3 files changed

+79
-3
lines changed

3 files changed

+79
-3
lines changed

docs/ai-chat/backend.mdx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ export const myChat = chat.task({
101101
| `runId` | `string` | The Trigger.dev run ID |
102102
| `chatAccessToken` | `string` | Scoped access token for this run |
103103
| `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
104+
| `writer` | [`ChatWriter`](/ai-chat/reference#chatwriter) | Stream writer for custom chunks |
105+
106+
Every lifecycle callback receives a `writer` — a lazy stream writer that lets you send custom `UIMessageChunk` parts (like `data-*` parts) to the frontend without the ceremony of `chat.stream.writer()`. See [ChatWriter](/ai-chat/reference#chatwriter).
104107

105108
#### onChatStart
106109

@@ -145,6 +148,7 @@ Fires at the start of every turn, after message accumulation and `onChatStart` (
145148
| `continuation` | `boolean` | Whether this run is continuing an existing chat |
146149
| `preloaded` | `boolean` | Whether this run was preloaded |
147150
| `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
151+
| `writer` | [`ChatWriter`](/ai-chat/reference#chatwriter) | Stream writer for custom chunks |
148152

149153
```ts
150154
export const myChat = chat.task({
@@ -170,9 +174,41 @@ export const myChat = chat.task({
170174
By persisting in `onTurnStart`, the user's message is saved to your database before the AI starts streaming. If the user refreshes mid-stream, the message is already there.
171175
</Tip>
172176

177+
#### onBeforeTurnComplete
178+
179+
Fires after the response is captured but **before** the stream closes. The `writer` can send custom chunks that appear in the current turn — use this for post-processing indicators, compaction progress, or any data the user should see before the turn ends.
180+
181+
```ts
182+
export const myChat = chat.task({
183+
id: "my-chat",
184+
onBeforeTurnComplete: async ({ writer, usage, uiMessages }) => {
185+
// Write a custom data part while the stream is still open
186+
writer.write({
187+
type: "data-usage-summary",
188+
data: {
189+
tokens: usage?.totalTokens,
190+
messageCount: uiMessages.length,
191+
},
192+
});
193+
194+
// You can also compact messages here and write progress
195+
if (usage?.totalTokens && usage.totalTokens > 50_000) {
196+
writer.write({ type: "data-compaction", data: { status: "compacting" } });
197+
chat.setMessages(compactedMessages);
198+
writer.write({ type: "data-compaction", data: { status: "complete" } });
199+
}
200+
},
201+
run: async ({ messages, signal }) => {
202+
return streamText({ model: openai("gpt-4o"), messages, abortSignal: signal });
203+
},
204+
});
205+
```
206+
207+
Receives the same fields as [`TurnCompleteEvent`](/ai-chat/reference#turncompleteevent), plus a [`writer`](/ai-chat/reference#chatwriter).
208+
173209
#### onTurnComplete
174210

175-
Fires after each turn completes — after the response is captured, before waiting for the next message. This is the primary hook for persisting the assistant's response.
211+
Fires after each turn completes — after the response is captured and the stream is closed. This is the primary hook for persisting the assistant's response. Does not include a `writer` since the stream is already closed.
176212

177213
| Field | Type | Description |
178214
|-------|------|-------------|

docs/ai-chat/features.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ export const myChat = chat.task({
207207
Use `data-*` chunk types (e.g. `data-status`, `data-progress`) for custom data. The AI SDK processes these into `DataUIPart` objects in `message.parts` on the frontend. Writing the same `type` + `id` again updates the existing part instead of creating a new one — useful for live progress.
208208
</Tip>
209209

210+
<Tip>
211+
Inside lifecycle callbacks (`onPreload`, `onChatStart`, `onTurnStart`, `onBeforeTurnComplete`, `onCompacted`), you can use the `writer` parameter instead of `chat.stream.writer()` — it's simpler and avoids the `execute` + `waitUntilComplete` boilerplate. See [ChatWriter](/ai-chat/reference#chatwriter).
212+
</Tip>
213+
210214
`chat.stream` exposes the full stream API:
211215

212216
| Method | Description |

docs/ai-chat/reference.mdx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ Options for `chat.task()`.
1616
| `onPreload` | `(event: PreloadEvent) => Promise<void> \| void` || Fires on preloaded runs before the first message |
1717
| `onChatStart` | `(event: ChatStartEvent) => Promise<void> \| void` || Fires on turn 0 before `run()` |
1818
| `onTurnStart` | `(event: TurnStartEvent) => Promise<void> \| void` || Fires every turn before `run()` |
19-
| `onTurnComplete` | `(event: TurnCompleteEvent) => Promise<void> \| void` || Fires after each turn completes |
20-
| `onCompacted` | `(event: CompactedEvent) => Promise<void> \| void` || Fires when compaction occurs. See [Compaction](/ai-chat/compaction) |
19+
| `onBeforeTurnComplete` | `(event: BeforeTurnCompleteEvent) => Promise<void> \| void` || Fires after response but before stream closes. Includes `writer`. |
20+
| `onTurnComplete` | `(event: TurnCompleteEvent) => Promise<void> \| void` || Fires after each turn completes (stream closed) |
21+
| `onCompacted` | `(event: CompactedEvent) => Promise<void> \| void` || Fires when compaction occurs. Includes `writer`. See [Compaction](/ai-chat/compaction) |
2122
| `compaction` | `ChatTaskCompactionOptions` || Automatic context compaction. See [Compaction](/ai-chat/compaction) |
2223
| `pendingMessages` | `PendingMessagesOptions` || Mid-execution message injection. See [Pending Messages](/ai-chat/pending-messages) |
2324
| `prepareMessages` | `(event: PrepareMessagesEvent) => ModelMessage[]` || Transform model messages before use (cache breaks, context injection, etc.) |
@@ -57,6 +58,7 @@ Passed to the `onPreload` callback.
5758
| `runId` | `string` | The Trigger.dev run ID |
5859
| `chatAccessToken` | `string` | Scoped access token for this run |
5960
| `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
61+
| `writer` | [`ChatWriter`](#chatwriter) | Stream writer for custom chunks. Lazy — no overhead if unused. |
6062

6163
## ChatStartEvent
6264

@@ -72,6 +74,7 @@ Passed to the `onChatStart` callback.
7274
| `continuation` | `boolean` | Whether this run is continuing an existing chat |
7375
| `previousRunId` | `string \| undefined` | Previous run ID (only when `continuation` is true) |
7476
| `preloaded` | `boolean` | Whether this run was preloaded before the first message |
77+
| `writer` | [`ChatWriter`](#chatwriter) | Stream writer for custom chunks. Lazy — no overhead if unused. |
7578

7679
## TurnStartEvent
7780

@@ -89,6 +92,7 @@ Passed to the `onTurnStart` callback.
8992
| `continuation` | `boolean` | Whether this run is continuing an existing chat |
9093
| `previousRunId` | `string \| undefined` | Previous run ID (only when `continuation` is true) |
9194
| `preloaded` | `boolean` | Whether this run was preloaded |
95+
| `writer` | [`ChatWriter`](#chatwriter) | Stream writer for custom chunks. Lazy — no overhead if unused. |
9296

9397
## TurnCompleteEvent
9498

@@ -112,6 +116,37 @@ Passed to the `onTurnComplete` callback.
112116
| `usage` | `LanguageModelUsage \| undefined` | Token usage for this turn |
113117
| `totalUsage` | `LanguageModelUsage` | Cumulative token usage across all turns |
114118

119+
## BeforeTurnCompleteEvent
120+
121+
Passed to the `onBeforeTurnComplete` callback. Same fields as `TurnCompleteEvent` plus a `writer`.
122+
123+
| Field | Type | Description |
124+
|-------|------|-------------|
125+
| _(all TurnCompleteEvent fields)_ | | See [TurnCompleteEvent](#turncompleteevent) |
126+
| `writer` | [`ChatWriter`](#chatwriter) | Stream writer — the stream is still open so chunks appear in the current turn |
127+
128+
## ChatWriter
129+
130+
A stream writer passed to lifecycle callbacks. Write custom `UIMessageChunk` parts (e.g. `data-*` parts) to the chat stream.
131+
132+
The writer is lazy — no stream is opened unless you call `write()` or `merge()`, so there's zero overhead for callbacks that don't use it.
133+
134+
| Method | Type | Description |
135+
|--------|------|-------------|
136+
| `write(part)` | `(part: UIMessageChunk) => void` | Write a single chunk to the chat stream |
137+
| `merge(stream)` | `(stream: ReadableStream<UIMessageChunk>) => void` | Merge another stream's chunks into the chat stream |
138+
139+
```ts
140+
onTurnStart: async ({ writer }) => {
141+
// Write a custom data part — render it on the frontend
142+
writer.write({ type: "data-status", data: { loading: true } });
143+
},
144+
onBeforeTurnComplete: async ({ writer, usage }) => {
145+
// Stream is still open — these chunks arrive before the turn ends
146+
writer.write({ type: "data-usage", data: { tokens: usage?.totalTokens } });
147+
},
148+
```
149+
115150
## ChatTaskCompactionOptions
116151

117152
Options for the `compaction` field on `chat.task()`. See [Compaction](/ai-chat/compaction) for usage guide.
@@ -153,6 +188,7 @@ Passed to the `onCompacted` callback.
153188
| `stepNumber` | `number` | Step number (-1 for outer loop) |
154189
| `chatId` | `string \| undefined` | Chat session ID |
155190
| `turn` | `number \| undefined` | Current turn |
191+
| `writer` | [`ChatWriter`](#chatwriter) | Stream writer for custom chunks during compaction |
156192

157193
## PendingMessagesOptions
158194

0 commit comments

Comments
 (0)