diff --git a/docs/ai-chat/client-protocol.mdx b/docs/ai-chat/client-protocol.mdx index 0a94327b78..e679623394 100644 --- a/docs/ai-chat/client-protocol.mdx +++ b/docs/ai-chat/client-protocol.mdx @@ -281,7 +281,7 @@ Re-calling `POST /api/v1/sessions` with the same `(taskIdentifier, externalId)` The `publicAccessToken` returned by `POST /api/v1/sessions` is valid for 60 minutes. Two ways to keep going past that: -1. **Take refreshed tokens from the stream.** Every `turn-complete` control record on `.out` carries a `public-access-token` header with a refreshed JWT (see [`turn-complete` control record](#turn-complete-control-record)). For active conversations this just rolls — replace your stored token whenever the header is present. +1. **Take refreshed tokens from the stream.** Most `turn-complete` control records on `.out` carry a `public-access-token` header with a refreshed JWT (see [`turn-complete` control record](#turn-complete-control-record)). The header is optional and may be absent on some turns (for example an errored turn), so replace your stored token whenever the header is present rather than expecting it every turn. For active conversations it rolls on its own. 2. **Re-call `POST /api/v1/sessions`.** Idempotent, returns `isCached: true` and a brand-new 60-minute token. Use this if a chat goes idle long enough that the SSE stream has closed and you need to resume. @@ -1048,7 +1048,7 @@ Yes. `.in` records are processed in arrival order — the agent's stop handler a -Any opaque ASCII string up to ~64 characters. The built-in clients pass a `nanoid(7)` (e.g. `"V1StGXR"`) generated per request. The server uses it as a per-record idempotency key — re-POSTing the same body with the same `X-Part-Id` produces a single S2 record. If you don't send the header, the server generates one for you and idempotency is per-request only. +Any opaque ASCII string up to ~64 characters. The built-in clients generate a high-entropy id per logical send (a UUID in the browser, a `nanoid` server-side) and reuse it across auth retries of that send. The server uses it as a per-record idempotency key — re-POSTing the same body with the same `X-Part-Id` produces a single S2 record. If you don't send the header, the server generates one for you and idempotency is per-request only. diff --git a/docs/ai-chat/error-handling.mdx b/docs/ai-chat/error-handling.mdx index 9eb6ec1f0e..e93f3c8185 100644 --- a/docs/ai-chat/error-handling.mdx +++ b/docs/ai-chat/error-handling.mdx @@ -131,17 +131,17 @@ To persist errors for debugging or undo, use `onTurnComplete` (which fires even ### Using `onTurnComplete` -`onTurnComplete` fires after every turn — successful **or** errored. The `responseMessage` will be undefined or partial on errors. Use this to mark the turn as failed: +`onTurnComplete` fires after every turn — successful **or** errored. On an errored turn `responseMessage` is undefined or partial and `error` carries the thrown value (with `finishReason` set to `"error"`). Use this to mark the turn as failed: ```ts -onTurnComplete: async ({ chatId, uiMessages, responseMessage, stopped }) => { +onTurnComplete: async ({ chatId, uiMessages, responseMessage, stopped, error }) => { // Persist the messages regardless of error state await db.chat.update({ where: { id: chatId }, data: { messages: uiMessages, - // Mark the chat as errored if no response message - lastTurnStatus: responseMessage ? "ok" : stopped ? "stopped" : "errored", + // `error` is set when the turn threw + lastTurnStatus: error ? "errored" : stopped ? "stopped" : "ok", }, }); }, diff --git a/docs/ai-chat/how-it-works.mdx b/docs/ai-chat/how-it-works.mdx index ecde885f4a..2a9f42e028 100644 --- a/docs/ai-chat/how-it-works.mdx +++ b/docs/ai-chat/how-it-works.mdx @@ -40,7 +40,7 @@ The agent task is running. It reads the new message off `.in`, fires `onTurnStar ### Idle (awaiting next message) -The turn is over. The task is alive but not doing work — it is parked in a waitpoint on `.in`, waiting for the next user message. If one arrives, it goes back to **Streaming** for the next turn. If `idleTimeoutInSeconds` (defaulting to a few minutes) passes with no new message, it moves to **Suspended**. +The turn is over. The task is alive but not doing work — it is parked in a waitpoint on `.in`, waiting for the next user message. If one arrives, it goes back to **Streaming** for the next turn. If `idleTimeoutInSeconds` (30 seconds by default) passes with no new message, it moves to **Suspended**. ### Suspended diff --git a/docs/ai-chat/lifecycle-hooks.mdx b/docs/ai-chat/lifecycle-hooks.mdx index c327374e05..17d62663c6 100644 --- a/docs/ai-chat/lifecycle-hooks.mdx +++ b/docs/ai-chat/lifecycle-hooks.mdx @@ -223,7 +223,7 @@ export const myChat = chat.agent({ ## onValidateMessages -Validate or transform incoming `UIMessage[]` before they are converted to model messages. Fires once per turn with the raw messages from the wire payload (after cleanup of aborted tool parts), **before** accumulation and `toModelMessages()`. +Validate or transform incoming `UIMessage[]` before they are converted to model messages. Fires on turns that carry incoming messages, with the raw messages from the wire payload (after cleanup of aborted tool parts), **before** accumulation and `toModelMessages()`. Turns with no incoming messages — preload, close, and regenerate with nothing re-sent — skip it. Return the validated messages array. Throw to abort the turn with an error. diff --git a/docs/ai-chat/reference.mdx b/docs/ai-chat/reference.mdx index 147f396bd5..6e8a916728 100644 --- a/docs/ai-chat/reference.mdx +++ b/docs/ai-chat/reference.mdx @@ -286,6 +286,8 @@ Passed to the `onTurnComplete` callback. | `continuation` | `boolean` | Whether this run is continuing an existing chat | | `usage` | `LanguageModelUsage \| undefined` | Token usage for this turn | | `totalUsage` | `LanguageModelUsage` | Cumulative token usage across all turns | +| `finishReason` | `FinishReason \| undefined` | Why the LLM stopped (`"stop"`, `"tool-calls"`, `"error"`, …) | +| `error` | `unknown` | Set when the turn threw; `responseMessage` is then undefined or partial | ## BeforeTurnCompleteEvent @@ -761,10 +763,10 @@ See [Actions](/ai-chat/actions) for backend setup and [Sending actions](/ai-chat Eagerly trigger a run before the first message. ```ts -transport.preload(chatId, { idleTimeoutInSeconds?: number }): Promise +transport.preload(chatId): Promise ``` -No-op if a session already exists for this chatId. See [Preload](/ai-chat/fast-starts#preload) for full details. +No-op if a session already exists for this chatId. The preload idle window is set by `preloadIdleTimeoutInSeconds` on the agent, not by this call. See [Preload](/ai-chat/fast-starts#preload) for full details. ## useTriggerChatTransport