Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/ai-chat/client-protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Note>
Expand Down Expand Up @@ -1048,7 +1048,7 @@ Yes. `.in` records are processed in arrival order — the agent's stop handler a
</Expandable>

<Expandable title="What's the format of the optional `X-Part-Id` header?">
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.
</Expandable>

<Expandable title="What happens on rate-limit (429)?">
Expand Down
8 changes: 4 additions & 4 deletions docs/ai-chat/error-handling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment thread
ericallam marked this conversation as resolved.
Comment thread
ericallam marked this conversation as resolved.
},
});
},
Expand Down
2 changes: 1 addition & 1 deletion docs/ai-chat/how-it-works.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/ai-chat/lifecycle-hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 4 additions & 2 deletions docs/ai-chat/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Comment thread
ericallam marked this conversation as resolved.
Comment thread
ericallam marked this conversation as resolved.

## BeforeTurnCompleteEvent

Expand Down Expand Up @@ -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<void>
transport.preload(chatId): Promise<void>
```

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

Expand Down
Loading