You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(chat): correct the mailbox docs and describe the stop guarantee
The mailbox section described head-of-line behaviour that no longer happens: it
said a control record arriving before a message keeps `hasPending()` false and
makes `next()` wait, when a message behind a stop or a handover is now reported
and returned normally. Docs that state the opposite of what ships are worse than
none, so those claims are replaced with what the delivery guarantee actually is.
Also documents two things that had no coverage. A stop applies only to the turn
that was live when it arrived, so a recovered message's turn is not aborted by a
stop from before the crash. And `sessionInEventId` is a lower bound rather than
the sequence of the record a turn answered, which is the mistake a client makes
when it tries to match a turn boundary to its own send.
Copy file name to clipboardExpand all lines: docs/ai-chat/custom-agents.mdx
+20-17Lines changed: 20 additions & 17 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -179,6 +179,12 @@ for await (const turn of session) {
179
179
180
180
The frontend stops a turn with [`transport.stopGeneration(chatId)`](/ai-chat/frontend#stop-generation), which writes a stop signal to the session's input stream. It aborts the current turn's generation but keeps the run alive, so the next message continues on the same session.
181
181
182
+
A stop only applies to the turn that was live when it arrived. If the run crashes
183
+
and a later run recovers a message that had not been answered yet, a stop that
184
+
was already applied before the crash is not applied again, so the turn answering
185
+
the recovered message runs to completion. A stop sent after the recovery is live
186
+
and aborts that turn as normal.
187
+
182
188
`turn.signal` is a combined stop-and-cancel `AbortSignal`, fresh each turn. Pass it to `streamText` so the stop reaches the model, then let `turn.complete()` finish the turn:
183
189
184
190
```ts trigger/my-chat.ts
@@ -227,16 +233,16 @@ For full control, skip `createSession` and compose the primitives directly:
227
233
228
234
| Method | Behavior |
229
235
| --- | --- |
230
-
|`peek()`| Return the buffer head when it is a message, without consuming it; otherwise return `undefined`|
231
-
|`hasPending()`| Resolve `true` when the buffer head is a message; does not consume it |
236
+
|`peek()`| Return the next queued message without consuming it, or `undefined` when none is queued|
237
+
|`hasPending()`| Resolve `true` when a message is queued; does not consume it |
232
238
|`next({ timeoutInSeconds? })`| Consume exactly one message record in channel order, or resolve `undefined` when the optional timeout elapses |
233
239
|`on(handler)`| Consume messages as they arrive and invoke the handler |
234
240
|`waitWithIdleTimeout(options)`| Wait warm, then suspend the run until the next message arrives |
235
241
236
-
`hasPending()` checks whether the local, already-delivered buffer head is a
237
-
message that`next()`can consume immediately. It does not query the remote
238
-
Session channel or start a subscription. Use `waitWithIdleTimeout()` when the
239
-
loop needs to idle until future input arrives.
242
+
`hasPending()` checks whether a message has already been delivered locally and is
243
+
waiting for`next()`to take it. It does not query the remote Session channel or
244
+
start a subscription. Use `waitWithIdleTimeout()` when the loop needs to idle
245
+
until future input arrives.
240
246
241
247
`next({ timeoutInSeconds: 0 })` is also a local, non-blocking read. Call
242
248
`next()` without a timeout, or with a positive timeout, to subscribe for future
@@ -262,18 +268,15 @@ owns its own turn sequencing never advances past input it has not taken. By
262
268
contrast, `on()` commits a record as soon as it dispatches the handler; avoid
263
269
mixing `on()` and `next()` when a single loop owns mailbox consumption.
264
270
265
-
The Session `.in` channel also carries control records such as handovers. If one
266
-
comes before a message, `hasPending()` stays `false` and `next()` leaves the
267
-
control record for its own consumer. After that record is handled, the message
268
-
becomes pending.
269
-
270
-
A control record that nothing on the run consumes is discarded rather than left
271
-
at the head of the channel. `hasPending()` and `next()` only look at the head, so
272
-
a record parked there would make every message behind it undeliverable.
271
+
The Session `.in` channel also carries control records such as stops and
272
+
handovers. Those are routed to their own consumers and never block messages: a
273
+
message that arrived behind one is still reported by `hasPending()` and still
274
+
returned by `next()`, in channel order. The same holds for a record kind this
275
+
version of the SDK does not recognise, which is discarded rather than left where
276
+
it would make every message behind it undeliverable.
273
277
274
-
`next()` still returns `undefined` whenever no message became consumable before
275
-
the timeout, including while a control record that does have its own consumer
276
-
sits at the head.
278
+
`next()` returns `undefined` when no message became consumable before the
|`chat.writeTurnComplete(options?)`| Signal turn complete; returns `{ lastEventId, sessionInEventId }` resume cursors. `sessionInEventId` is a lower bound, not the sequence of the record the turn answered|
508
508
|`chat.createStopSignal()`| Create a managed stop signal wired to the stop input stream |
|`chat.local<T>({ id })`| Create a per-run typed local (see [`chat.local`](/ai-chat/chat-local)) |
@@ -645,7 +645,7 @@ The `onEvent` callback receives a `ChatTransportEvent` (exported from `@trigger.
645
645
|`message-send-failed`|`messageId?`, `source`, `error`, `status?`, `durationMs`, `partId?`, `bodyBytes?`| A send definitively failed after internal retries. Fires in addition to `useChat`'s `onError`. |
646
646
|`stream-connected`|`resumed`, `lastEventId?`, `messageId?`| The SSE subscription to the session's output stream started delivering. `resumed: true` when reconnecting from a stored cursor (page reload) rather than following a fresh send. `lastEventId` is the cursor it connected from. |
647
647
|`first-chunk`|`chunkType?`, `lastEventId?`, `messageId?`, `sinceSendMs?`| The first response chunk of a turn arrived. `sinceSendMs` is the delta from the last turn-producing send — time to first token without any bookkeeping. |
648
-
|`turn-completed`|`lastEventId?`, `sessionInEventId?`, `messageId?`, `sinceSendMs?`| The agent's turn-complete control record arrived — the "finished answering" signal. `sinceSendMs` is the full turn latency; `sessionInEventId` is the agent's committed input-stream cursor. |
648
+
|`turn-completed`|`lastEventId?`, `sessionInEventId?`, `messageId?`, `sinceSendMs?`| The agent's turn-complete control record arrived — the "finished answering" signal. `sinceSendMs` is the full turn latency; `sessionInEventId` is the cursor the agent can safely resume its inputstream from. Treat it as a lower bound: it is held back behind any message still waiting to be handled, so it can be below the sequence of the record this turn answered. Do not use it to decide whether a turn boundary belongs to your own send. |
649
649
|`stream-error`|`error`, `status?`| The output stream failed unrecoverably. |
650
650
651
651
`source` identifies the send path: `"submit-message"`, `"regenerate-message"`, `"steer"` (`sendPendingMessage`), `"action"` (`sendAction`), `"stop"` (`stopGeneration`), or `"head-start"`.
0 commit comments