Skip to content

Commit bf6df3d

Browse files
committed
fix(chat): publish the agent options for hydrateMessages agents, and align the Head Start docs
chat.toStreamTextOptions() read the agent-level system, registry, cacheControl and systemProviderOptions from a key set only inside the snapshot boot block, which a hydrateMessages agent skips, so the spread form dropped them there. The key is set on every boot now, with a test for the hydrateMessages case. Docs: the actions gating example returns chat.turn() instead of a streamText call onAction no longer receives; the extracted-loop examples import ChatStreamText, stepCountIs and ModelMessage; the Head Start pages and the chat-server docstrings list prompt among the owned options and describe tools as caller-supplied.
1 parent 871f86c commit bf6df3d

9 files changed

Lines changed: 57 additions & 31 deletions

File tree

docs/ai-chat/actions.mdx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,11 @@ onTurnComplete: async ({ chatId, newUIMessages }) => {
107107
If you have a [human-in-the-loop](/ai-chat/patterns/human-in-the-loop) tool waiting on `addToolOutput`, you usually want to refuse competing actions like `regenerate` until the answer arrives. [`chat.history.getPendingToolCalls()`](/ai-chat/backend#chat-history) gives you exactly that signal:
108108

109109
```ts
110-
onAction: async ({ action, signal, streamText }) => {
110+
onAction: async ({ action }) => {
111111
if (action.type === "regenerate") {
112112
if (chat.history.getPendingToolCalls().length > 0) return; // gated
113113
chat.history.slice(0, -1);
114-
return streamText({
115-
model: anthropic("claude-sonnet-4-5"),
116-
messages: await convertToModelMessages(chat.history.all()),
117-
abortSignal: signal,
118-
});
114+
return chat.turn();
119115
}
120116
},
121117
```

docs/ai-chat/backend.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ If the managed prompt names a model, pass a registry on the agent so the runtime
9090
For complex agent flows where `streamText` is called deep inside your code, use `chat.pipe()`. It works from **anywhere inside a task** — even nested function calls.
9191

9292
```ts trigger/agent-chat.ts
93-
import { chat } from "@trigger.dev/sdk/ai";
93+
import { chat, type ChatStreamText } from "@trigger.dev/sdk/ai";
9494
import { anthropic } from "@ai-sdk/anthropic";
95-
import type { ModelMessage } from "ai";
95+
import { stepCountIs, type ModelMessage } from "ai";
9696

9797
export const agentChat = chat.agent({
9898
id: "agent-chat",

docs/ai-chat/fast-starts.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,9 @@ This is an **import-chain** problem, not a runtime one. A "we'll strip the execu
272272

273273
<Note>
274274
That `streamText` is the SDK's, not the one from `ai`. It pins `messages`,
275-
`stopWhen: stepCountIs(1)` and `abortSignal`, which the handover depends on:
275+
`prompt`, `stopWhen: stepCountIs(1)` and `abortSignal`, which the handover depends on:
276276
running past step 1 would splice a stream the agent is supposed to own. Setting
277-
any of the three at the call site is a type error, and a throw if you get past
277+
any of the four at the call site is a type error, and a throw if you get past
278278
the types, rather than breaking the handover quietly. `chat.toStreamTextOptions()` is still there if you want to build the
279279
options yourself.
280280
</Note>
@@ -646,11 +646,11 @@ The SDK owns these keys. Passing one to the callback's `streamText` is a type er
646646
| Key | What the SDK sets | Why |
647647
| --- | --- | --- |
648648
| `messages` | `convertToModelMessages(uiMessages)` | First-turn user history |
649-
| `tools` | What you pass | Schema-only tools for step 1 |
649+
| `prompt` | Nothing, and rejects yours | `messages` already carries the history |
650650
| `stopWhen` | `stepCountIs(1)` | Step 1 only, the agent picks up step 2 onward |
651651
| `abortSignal` | Combined request + idle timeout | Safe cleanup on disconnect |
652652

653-
You bring `model`, `system`, `providerOptions`, `prepareStep`, anything else `streamText` accepts. Note that `tools` is the one owned key you do pass, since the SDK cannot know your schema-only set.
653+
You bring `model`, `system`, `providerOptions`, `prepareStep`, anything else `streamText` accepts. `tools` is yours too: pass your schema-only set to the callback's `streamText` (or to `chat.toStreamTextOptions({ tools })`), since the SDK cannot know it.
654654

655655
#### The transport option
656656

docs/ai-chat/migrating-from-a-route-handler.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ Head Start brings the route handler back for exactly that first turn. It runs st
478478
```
479479

480480
<Note>
481-
That `streamText` owns `messages`, `abortSignal` and `stopWhen`. Passing one is
481+
That `streamText` owns `messages`, `prompt`, `abortSignal` and `stopWhen`. Passing one is
482482
a type error, and a runtime throw if you get past the types. Unlike the agent side, re-setting one breaks the
483483
handover rather than degrading it: `stopWhen` is pinned to `stepCountIs(1)`
484484
because the agent, not the handler, runs step 2 onward.

packages/trigger-sdk/skills/trigger-authoring-chat-agent/SKILL.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ inside nested helpers, call `await chat.pipe(result)` from anywhere in the task
120120
`run` resolve `void`.
121121

122122
```ts
123+
import { chat, type ChatStreamText } from "@trigger.dev/sdk/ai";
124+
import { anthropic } from "@ai-sdk/anthropic";
125+
import type { ModelMessage } from "ai";
126+
123127
export const agentChat = chat.agent({
124128
id: "agent-chat",
125129
run: async ({ messages, streamText }) => {
@@ -262,7 +266,7 @@ There is no API route in this model. The transport replaces the route round-trip
262266
model, and telemetry. The imported one carries none of it, with no error.
263267
`...chat.toStreamTextOptions()` does the same job by hand, and is what a custom agent has to use,
264268
since it has no `run` argument. A `chat.headStart` route gets a bound `streamText` too, and there it
265-
also owns `messages`, `stopWhen` and `abortSignal`. Spreading it and then re-setting
269+
also owns `messages`, `prompt`, `stopWhen` and `abortSignal`. Spreading it and then re-setting
266270
`tools` or `prepareStep` replaces the managed ones; the run argument's `streamText` merges `tools`
267271
and composes `prepareStep` instead.
268272

packages/trigger-sdk/skills/trigger-chat-agent-advanced/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,8 @@ import { headStartTools } from "@/lib/chat-tools/schemas";
231231

232232
export const chatHandler = chat.headStart({
233233
agentId: "my-chat",
234-
// `streamText` from the run argument owns `messages`, `stopWhen` and
235-
// `abortSignal`: the handover needs `stopWhen: stepCountIs(1)` so the agent,
234+
// `streamText` from the run argument owns `messages`, `prompt`, `stopWhen`
235+
// and `abortSignal`: the handover needs `stopWhen: stepCountIs(1)` so the agent,
236236
// not this handler, runs step 2 onward. Passing any of them is a type error.
237237
run: async ({ streamText }) =>
238238
streamText({

packages/trigger-sdk/src/v3/ai.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7499,18 +7499,18 @@ function chatAgent<
74997499
}
75007500
}
75017501

7502-
locals.set(chatAgentManagedConfigKey, {
7503-
registry: promptRegistry,
7504-
system: agentSystem,
7505-
cacheControl: agentCacheControl,
7506-
systemProviderOptions: agentSystemProviderOptions,
7507-
});
7508-
75097502
// Make the seeded UI accumulator visible to `chat.history.*`
75107503
// before any hook (`onChatStart`, `onTurnStart`, etc.) fires.
75117504
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
75127505
}
75137506

7507+
locals.set(chatAgentManagedConfigKey, {
7508+
registry: promptRegistry,
7509+
system: agentSystem,
7510+
cacheControl: agentCacheControl,
7511+
systemProviderOptions: agentSystemProviderOptions,
7512+
});
7513+
75147514
// Token usage tracking across turns
75157515
let previousTurnUsage: LanguageModelUsage | undefined;
75167516
let cumulativeUsage: LanguageModelUsage = emptyUsage();

packages/trigger-sdk/src/v3/chat-server.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,16 @@ export type HeadStartRunArgs<TTools extends Record<string, Tool>> = {
172172
/** Helper exposing `toStreamTextOptions(...)` and a session escape hatch. */
173173
chat: HeadStartChatHelper<TTools>;
174174
/**
175-
* `streamText` with the four options the handover protocol depends on already
176-
* applied: the converted `messages`, your `tools`, `stopWhen: stepCountIs(1)`
177-
* and the combined `abortSignal`.
175+
* `streamText` with the options the handover protocol depends on already
176+
* applied: the converted `messages`, `stopWhen: stepCountIs(1)` and the
177+
* combined `abortSignal`, plus the `tools` you pass to it.
178178
*
179179
* Prefer it over importing `streamText` from `ai`. Spreading
180180
* `chat.toStreamTextOptions()` into the imported one is equivalent, but
181-
* setting `messages`, `stopWhen` or `abortSignal` after the spread breaks the
182-
* handover, and nothing catches that. Passing any of them here throws
183-
* instead, and note that the four it owns are a type error, not just a
184-
* runtime one.
181+
* setting `messages`, `prompt`, `stopWhen` or `abortSignal` after the spread
182+
* breaks the handover, and nothing catches that. Passing any of those four
183+
* here is a type error, and a throw if you get past the types. `tools` is
184+
* yours to supply.
185185
*/
186186
streamText: HeadStartStreamTextFn;
187187
};
@@ -203,7 +203,7 @@ export type HeadStartChatHelper<TTools extends Record<string, Tool>> = {
203203
* this helper just hands back the options the SDK needs to own.
204204
*
205205
* The customer COULD override any of these by re-setting them after
206-
* the spread, but doing so for `stopWhen` / `messages` /
206+
* the spread, but doing so for `stopWhen` / `messages` / `prompt` /
207207
* `abortSignal` will break the handover protocol. The intent is
208208
* that customers spread first, then add only their own keys.
209209
*/

packages/trigger-sdk/test/spread-form-agent-options.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,30 @@ describe("chat.toStreamTextOptions with agent-level options", () => {
4747
await harness.close();
4848
}
4949
});
50+
51+
it("carries chat.agent({ system }) in the spread form for a hydrateMessages agent", async () => {
52+
const prompts: string[] = [];
53+
const model = new MockLanguageModelV3({
54+
doStream: async ({ prompt }) => {
55+
prompts.push(JSON.stringify(prompt));
56+
return {
57+
stream: simulateReadableStream({ chunks: textChunks("ok"), initialDelayInMs: 5 }),
58+
};
59+
},
60+
});
61+
const agent = chat.agent({
62+
id: "spread-form-agent-system-hydrate",
63+
system: "AGENT-SYSTEM-VIA-SPREAD-HYDRATE",
64+
hydrateMessages: async ({ incomingMessages }) => incomingMessages,
65+
run: async ({ messages, signal }) =>
66+
streamText({ model, messages, abortSignal: signal, ...chat.toStreamTextOptions() }),
67+
});
68+
const harness = mockChatAgent(agent, { chatId: "spread-form-agent-system-hydrate" });
69+
try {
70+
await harness.sendMessage({ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] });
71+
expect(prompts.at(-1)!).toContain("AGENT-SYSTEM-VIA-SPREAD-HYDRATE");
72+
} finally {
73+
await harness.close();
74+
}
75+
});
5076
});

0 commit comments

Comments
 (0)