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(ai-chat): headStart handover for custom agents + triggerConfig (#3964)
## Summary
`chat.headStart` now works with the `chat.customAgent` and
`chat.createSession` backends (not just `chat.agent`), and takes a
`triggerConfig` option. These docs cover both.
The Fast starts guide gets a "Handover with custom agents" section
showing how each backend consumes the handover (`consumeHandover`
returning `{ isFinal, skipped }` for custom agents, `turn.handover` for
createSession), including threading `originalMessages` so a resumed tool
round merges into the handed-over assistant. The `chat.headStart` API
section documents `triggerConfig` (tags, queue, machine, and the rest)
on the auto-triggered run.
The reference picks up `ChatTurn.handover`, `turn.complete()` with no
source, `chat.waitForHandover`, and a new `HeadStartHandlerOptions`
table.
Docs for the SDK changes in
[#3963](#3963).
|`turn.complete(source)`| Pipe stream, capture response, accumulate, and signal turn-complete|
121
+
|`turn.complete(source?)`| Pipe stream, capture response, accumulate, and signal turn-complete. Call with no source on a final head-start handover (`turn.handover.isFinal`), where the warm step-1 partial is already the response|
121
122
|`turn.done()`| Signal turn-complete only (when you have piped manually) |
122
123
|`turn.addResponse(response)`| Add a response to the accumulator manually |
123
124
|`turn.setMessages(uiMessages)`| Replace the accumulated messages — continuation seeding and on-demand compaction |
Copy file name to clipboardExpand all lines: docs/ai-chat/fast-starts.mdx
+73-2Lines changed: 73 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -108,7 +108,7 @@ if (payload.trigger === "preload") {
108
108
109
109
## Head Start
110
110
111
-
Head Start runs step 1's LLM call in your warm server process while the chat.agent run boots in parallel. The user sees one continuous turn: text first from your server, then a clean handover to the agent for tool execution and any further steps.
111
+
Head Start runs step 1's LLM call in your warm server process while the agent run boots in parallel. The user sees one continuous turn: text first from your server, then a clean handover to the agent for tool execution and any further steps. The agent you hand off to can be a `chat.agent`, a `chat.customAgent`, or a `chat.createSession` loop (see [Handover with custom agents](#handover-with-custom-agents)).
112
112
113
113
`chat.headStart` returns a standard [Web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) handler — `(req: Request) => Promise<Response>` — so it slots into any runtime that speaks Web Fetch.
114
114
@@ -545,16 +545,86 @@ Head Start composes with [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemes
545
545
546
546
Your hydrate hook shapes **model context**, not the transcript — dropping reasoning-only entries or unresolved tool rows from the returned chain is fine and does not affect what `onTurnComplete` persists or what the UI renders.
547
547
548
+
### Handover with custom agents
549
+
550
+
The route handler is backend-agnostic: `agentId` can point at a `chat.agent`, a [`chat.customAgent`](/ai-chat/custom-agents), or a [`chat.createSession`](/ai-chat/custom-agents#managed-loop-chatcreatesession) loop. With `chat.agent` the handover is consumed for you (the steps above). The two hand-rolled backends consume it explicitly on turn 0.
551
+
552
+
#### chat.createSession
553
+
554
+
The turn iterator surfaces the handover as `turn.handover`. On a final (pure-text) handover, call `turn.complete()` with no source to finalize the warm partial without streaming; otherwise stream as usual. The iterator threads the spliced partial as `originalMessages` for you, so a resumed tool round merges into the handed-over assistant.
555
+
556
+
```ts trigger/chat.ts
557
+
forawait (const turn ofsession) {
558
+
// Pure-text handover (isFinal): step 1 already IS the response.
559
+
const result =turn.handover?.isFinal
560
+
?undefined
561
+
:streamText({
562
+
model: anthropic("claude-sonnet-4-6"),
563
+
messages: turn.messages,
564
+
abortSignal: turn.signal,
565
+
stopWhen: stepCountIs(10),
566
+
});
567
+
568
+
awaitturn.complete(result); // no source on a final handover
569
+
}
570
+
```
571
+
572
+
#### chat.customAgent
573
+
574
+
In a hand-rolled loop, call `conversation.consumeHandover({ payload })` at the top of turn 0. It waits for the handover signal, seeds prior history from `payload.headStartMessages`, splices the warm step-1 partial into the accumulator, and returns `{ isFinal, skipped }`.
575
+
576
+
```ts trigger/chat.ts
577
+
// Turn 0, gated on a head-start run:
578
+
if (turn===0&&payload.trigger==="handover-prepare") {
if (response) awaitconversation.addResponse(response);
594
+
}
595
+
awaitchat.writeTurnComplete(); // on isFinal the warm partial is already the response
596
+
return;
597
+
}
598
+
```
599
+
600
+
Gate the call on `trigger === "handover-prepare"` — `consumeHandover` consumes the warm handover, not a normal first message. See [Custom agents](/ai-chat/custom-agents) for the full loop (continuation seeding, stop handling, persistence). The lower-level `chat.waitForHandover({ payload })` and `accumulator.applyHandover(signal)` are exported if you need to wait and splice in separate steps.
601
+
602
+
<Note>
603
+
Always pass `originalMessages: conversation.uiMessages` to `pipeAndCapture` in a custom loop. It keeps assistant message IDs stable across turns and lets a tool-approval or handover resume merge into the trailing assistant — the same threading `chat.agent` does internally.
604
+
</Note>
605
+
548
606
### The `chat.headStart` API
549
607
550
608
```ts
551
609
chat.headStart<TTools>({
552
-
agentId: string, // The chat.agent({ id }) you're handing off to
610
+
agentId: string, // The chat.agent / chat.customAgent id you're handing off to
idleTimeoutInSeconds?: number, // How long the agent waits for the handover signal. Default: 60
613
+
triggerConfig?: Partial<SessionTriggerConfig>, // Run options for the handover-prepare run
555
614
}): (req:Request) =>Promise<Response>
556
615
```
557
616
617
+
`triggerConfig` sets run options on the auto-triggered handover-prepare run: `tags`, `queue`, `machine`, `maxAttempts`, `maxDuration`, `region`, and `lockToVersion`. The `chat:{chatId}` tag is prepended automatically. Because the session is created once on the first head-start turn (idempotent on the chat id), this is the only place to set those options for a head-start chat's lifetime, mirroring what [`chat.createStartSessionAction`](/ai-chat/sessions) sets for the direct-trigger path.
streamText({ ...helper.toStreamTextOptions({ tools: headStartTools }), model, system }),
625
+
});
626
+
```
627
+
558
628
The `run` callback receives:
559
629
560
630
-`messages: UIMessage[]` — user messages parsed from the request body.
@@ -599,3 +669,4 @@ This is **not** a stock `useChat` `endpoint` — it's not the canonical request
599
669
-[`chat.headStart` factory and types](/ai-chat/reference) — full signatures for `HeadStartRunArgs`, `HeadStartChatHelper`, `HeadStartSession`, `HeadStartHandlerOptions`.
600
670
-[`headStart` transport option](/ai-chat/reference#triggerchattransport-options) — alongside `accessToken`, `startSession`, etc.
601
671
-[`onPreload` hook](/ai-chat/lifecycle-hooks#onpreload) — the backend hook that fires when a run is preloaded.
672
+
-[Custom agents](/ai-chat/custom-agents) — the `chat.customAgent` and `chat.createSession` loops that `consumeHandover` / `turn.handover` plug into.
|`complete(source)`|`Promise<UIMessage \| undefined>`| Pipe, capture, accumulate, cleanup, and signal turn-complete|
482
+
|`complete(source?)`|`Promise<UIMessage \| undefined>`| Pipe, capture, accumulate, cleanup, and signal turn-complete. Call with no source on a final head-start handover (`handover.isFinal`), where the warm partial is already the response|
482
483
|`done()`|`Promise<void>`| Signal turn-complete (when you've piped manually) |
483
484
|`addResponse(response)`|`Promise<void>`| Add response to accumulator manually |
484
485
|`setMessages(uiMessages)`|`Promise<void>`| Replace the accumulated messages (continuation seeding, compaction) |
485
486
|`prepareStep()`|`function \| undefined`|`prepareStep` callback wiring compaction + injection — pass to `streamText` when not using `chat.toStreamTextOptions()`|
486
487
488
+
## HeadStartHandlerOptions
489
+
490
+
Options for [`chat.headStart()`](/ai-chat/fast-starts#head-start), the warm-server first-turn handler (`@trigger.dev/sdk/chat-server`).
|`idleTimeoutInSeconds`|`number`|`60`| How long the agent waits for the handover signal |
497
+
|`triggerConfig`|`Partial<SessionTriggerConfig>`|`undefined`| Run options (tags, queue, machine, maxAttempts, maxDuration, region, lockToVersion) for the auto-triggered handover-prepare run. The `chat:{chatId}` tag is prepended automatically |
498
+
499
+
`chat.headStart(options)` returns the handler `(req: Request) => Promise<Response>`. The `run` callback receives `HeadStartRunArgs`: `{ messages: UIMessage[], signal: AbortSignal, chat: HeadStartChatHelper }`, where the helper exposes `chat.toStreamTextOptions({ tools })` and a `chat.session` escape hatch. See [Head Start](/ai-chat/fast-starts#head-start) for the full guide.
500
+
487
501
## chat namespace
488
502
489
503
All methods available on the `chat` object from `@trigger.dev/sdk/ai`.
@@ -499,6 +513,7 @@ All methods available on the `chat` object from `@trigger.dev/sdk/ai`.
499
513
|`chat.messages`| Input stream for incoming messages — use `.waitWithIdleTimeout()`|
500
514
|`chat.local<T>({ id })`| Create a per-run typed local (see [`chat.local`](/ai-chat/chat-local)) |
501
515
|`chat.createStartSessionAction(taskId, options?)`| Returns a server action that creates a chat Session + triggers the first run + returns a session-scoped PAT. Idempotent on `(env, externalId)`. |
516
+
|`chat.waitForHandover(options)`| Wait for a [`chat.headStart`](/ai-chat/fast-starts#handover-with-custom-agents) handover signal in a custom loop. Returns the signal or `null`. `chat.MessageAccumulator` wraps this as `consumeHandover()` / `applyHandover()`|
502
517
|`chat.requestUpgrade()`| End the current run after this turn so the next message starts on the latest agent version. Server-orchestrated handoff. |
503
518
|`chat.setTurnTimeout(duration)`| Override turn timeout at runtime (e.g. `"2h"`) |
504
519
|`chat.setTurnTimeoutInSeconds(seconds)`| Override turn timeout at runtime (in seconds) |
0 commit comments