@trigger.dev/sdk 4.4.6 → 4.5.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/dist/commonjs/v3/agentSkillsRuntime.d.ts +28 -0
  2. package/dist/commonjs/v3/agentSkillsRuntime.js +163 -0
  3. package/dist/commonjs/v3/agentSkillsRuntime.js.map +1 -0
  4. package/dist/commonjs/v3/ai-shared.d.ts +173 -0
  5. package/dist/commonjs/v3/ai-shared.js +25 -0
  6. package/dist/commonjs/v3/ai-shared.js.map +1 -0
  7. package/dist/commonjs/v3/ai.d.ts +2823 -5
  8. package/dist/commonjs/v3/ai.js +6197 -13
  9. package/dist/commonjs/v3/ai.js.map +1 -1
  10. package/dist/commonjs/v3/auth.d.ts +9 -0
  11. package/dist/commonjs/v3/auth.js.map +1 -1
  12. package/dist/commonjs/v3/chat-client.d.ts +301 -0
  13. package/dist/commonjs/v3/chat-client.js +624 -0
  14. package/dist/commonjs/v3/chat-client.js.map +1 -0
  15. package/dist/commonjs/v3/chat-react.d.ts +155 -0
  16. package/dist/commonjs/v3/chat-react.js +330 -0
  17. package/dist/commonjs/v3/chat-react.js.map +1 -0
  18. package/dist/commonjs/v3/chat-server.d.ts +206 -0
  19. package/dist/commonjs/v3/chat-server.js +737 -0
  20. package/dist/commonjs/v3/chat-server.js.map +1 -0
  21. package/dist/commonjs/v3/chat-server.test.d.ts +1 -0
  22. package/dist/commonjs/v3/chat-server.test.js +518 -0
  23. package/dist/commonjs/v3/chat-server.test.js.map +1 -0
  24. package/dist/commonjs/v3/chat-tab-coordinator.d.ts +65 -0
  25. package/dist/commonjs/v3/chat-tab-coordinator.js +235 -0
  26. package/dist/commonjs/v3/chat-tab-coordinator.js.map +1 -0
  27. package/dist/commonjs/v3/chat-tab-coordinator.test.d.ts +1 -0
  28. package/dist/commonjs/v3/chat-tab-coordinator.test.js +140 -0
  29. package/dist/commonjs/v3/chat-tab-coordinator.test.js.map +1 -0
  30. package/dist/commonjs/v3/chat.d.ts +437 -0
  31. package/dist/commonjs/v3/chat.js +968 -0
  32. package/dist/commonjs/v3/chat.js.map +1 -0
  33. package/dist/commonjs/v3/chat.test.d.ts +1 -0
  34. package/dist/commonjs/v3/chat.test.js +1180 -0
  35. package/dist/commonjs/v3/chat.test.js.map +1 -0
  36. package/dist/commonjs/v3/createStartSessionAction.test.d.ts +1 -0
  37. package/dist/commonjs/v3/createStartSessionAction.test.js +113 -0
  38. package/dist/commonjs/v3/createStartSessionAction.test.js.map +1 -0
  39. package/dist/commonjs/v3/deployments.d.ts +26 -0
  40. package/dist/commonjs/v3/deployments.js +37 -0
  41. package/dist/commonjs/v3/deployments.js.map +1 -0
  42. package/dist/commonjs/v3/index.d.ts +6 -3
  43. package/dist/commonjs/v3/index.js +7 -1
  44. package/dist/commonjs/v3/index.js.map +1 -1
  45. package/dist/commonjs/v3/runs.d.ts +22 -7
  46. package/dist/commonjs/v3/runs.js +1 -0
  47. package/dist/commonjs/v3/runs.js.map +1 -1
  48. package/dist/commonjs/v3/sessions.d.ts +228 -0
  49. package/dist/commonjs/v3/sessions.js +664 -0
  50. package/dist/commonjs/v3/sessions.js.map +1 -0
  51. package/dist/commonjs/v3/sessions.test.d.ts +1 -0
  52. package/dist/commonjs/v3/sessions.test.js +154 -0
  53. package/dist/commonjs/v3/sessions.test.js.map +1 -0
  54. package/dist/commonjs/v3/shared.d.ts +24 -2
  55. package/dist/commonjs/v3/shared.js +189 -1
  56. package/dist/commonjs/v3/shared.js.map +1 -1
  57. package/dist/commonjs/v3/skill.d.ts +99 -0
  58. package/dist/commonjs/v3/skill.js +155 -0
  59. package/dist/commonjs/v3/skill.js.map +1 -0
  60. package/dist/commonjs/v3/skills.d.ts +2 -0
  61. package/dist/commonjs/v3/skills.js +6 -0
  62. package/dist/commonjs/v3/skills.js.map +1 -0
  63. package/dist/commonjs/v3/streams.js +127 -19
  64. package/dist/commonjs/v3/streams.js.map +1 -1
  65. package/dist/commonjs/v3/tasks.d.ts +2 -1
  66. package/dist/commonjs/v3/tasks.js +1 -0
  67. package/dist/commonjs/v3/tasks.js.map +1 -1
  68. package/dist/commonjs/v3/test/index.d.ts +3 -0
  69. package/dist/commonjs/v3/test/index.js +18 -0
  70. package/dist/commonjs/v3/test/index.js.map +1 -0
  71. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +259 -0
  72. package/dist/commonjs/v3/test/mock-chat-agent.js +468 -0
  73. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -0
  74. package/dist/commonjs/v3/test/setup-catalog.d.ts +1 -0
  75. package/dist/commonjs/v3/test/setup-catalog.js +18 -0
  76. package/dist/commonjs/v3/test/setup-catalog.js.map +1 -0
  77. package/dist/commonjs/v3/test/test-session-handle.d.ts +53 -0
  78. package/dist/commonjs/v3/test/test-session-handle.js +256 -0
  79. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -0
  80. package/dist/commonjs/version.js +1 -1
  81. package/dist/esm/v3/agentSkillsRuntime.d.ts +28 -0
  82. package/dist/esm/v3/agentSkillsRuntime.js +136 -0
  83. package/dist/esm/v3/agentSkillsRuntime.js.map +1 -0
  84. package/dist/esm/v3/ai-shared.d.ts +173 -0
  85. package/dist/esm/v3/ai-shared.js +22 -0
  86. package/dist/esm/v3/ai-shared.js.map +1 -0
  87. package/dist/esm/v3/ai.d.ts +2823 -5
  88. package/dist/esm/v3/ai.js +6187 -14
  89. package/dist/esm/v3/ai.js.map +1 -1
  90. package/dist/esm/v3/auth.d.ts +9 -0
  91. package/dist/esm/v3/auth.js.map +1 -1
  92. package/dist/esm/v3/chat-client.d.ts +301 -0
  93. package/dist/esm/v3/chat-client.js +619 -0
  94. package/dist/esm/v3/chat-client.js.map +1 -0
  95. package/dist/esm/v3/chat-react.d.ts +155 -0
  96. package/dist/esm/v3/chat-react.js +325 -0
  97. package/dist/esm/v3/chat-react.js.map +1 -0
  98. package/dist/esm/v3/chat-server.d.ts +206 -0
  99. package/dist/esm/v3/chat-server.js +734 -0
  100. package/dist/esm/v3/chat-server.js.map +1 -0
  101. package/dist/esm/v3/chat-server.test.d.ts +1 -0
  102. package/dist/esm/v3/chat-server.test.js +516 -0
  103. package/dist/esm/v3/chat-server.test.js.map +1 -0
  104. package/dist/esm/v3/chat-tab-coordinator.d.ts +65 -0
  105. package/dist/esm/v3/chat-tab-coordinator.js +231 -0
  106. package/dist/esm/v3/chat-tab-coordinator.js.map +1 -0
  107. package/dist/esm/v3/chat-tab-coordinator.test.d.ts +1 -0
  108. package/dist/esm/v3/chat-tab-coordinator.test.js +138 -0
  109. package/dist/esm/v3/chat-tab-coordinator.test.js.map +1 -0
  110. package/dist/esm/v3/chat.d.ts +437 -0
  111. package/dist/esm/v3/chat.js +961 -0
  112. package/dist/esm/v3/chat.js.map +1 -0
  113. package/dist/esm/v3/chat.test.d.ts +1 -0
  114. package/dist/esm/v3/chat.test.js +1178 -0
  115. package/dist/esm/v3/chat.test.js.map +1 -0
  116. package/dist/esm/v3/createStartSessionAction.test.d.ts +1 -0
  117. package/dist/esm/v3/createStartSessionAction.test.js +111 -0
  118. package/dist/esm/v3/createStartSessionAction.test.js.map +1 -0
  119. package/dist/esm/v3/deployments.d.ts +26 -0
  120. package/dist/esm/v3/deployments.js +34 -0
  121. package/dist/esm/v3/deployments.js.map +1 -0
  122. package/dist/esm/v3/index.d.ts +6 -3
  123. package/dist/esm/v3/index.js +4 -1
  124. package/dist/esm/v3/index.js.map +1 -1
  125. package/dist/esm/v3/runs.d.ts +15 -0
  126. package/dist/esm/v3/runs.js +1 -0
  127. package/dist/esm/v3/runs.js.map +1 -1
  128. package/dist/esm/v3/sessions.d.ts +228 -0
  129. package/dist/esm/v3/sessions.js +656 -0
  130. package/dist/esm/v3/sessions.js.map +1 -0
  131. package/dist/esm/v3/sessions.test.d.ts +1 -0
  132. package/dist/esm/v3/sessions.test.js +152 -0
  133. package/dist/esm/v3/sessions.test.js.map +1 -0
  134. package/dist/esm/v3/shared.d.ts +24 -2
  135. package/dist/esm/v3/shared.js +188 -1
  136. package/dist/esm/v3/shared.js.map +1 -1
  137. package/dist/esm/v3/skill.d.ts +99 -0
  138. package/dist/esm/v3/skill.js +128 -0
  139. package/dist/esm/v3/skill.js.map +1 -0
  140. package/dist/esm/v3/skills.d.ts +2 -0
  141. package/dist/esm/v3/skills.js +2 -0
  142. package/dist/esm/v3/skills.js.map +1 -0
  143. package/dist/esm/v3/streams.js +127 -20
  144. package/dist/esm/v3/streams.js.map +1 -1
  145. package/dist/esm/v3/tasks.d.ts +2 -1
  146. package/dist/esm/v3/tasks.js +2 -1
  147. package/dist/esm/v3/tasks.js.map +1 -1
  148. package/dist/esm/v3/test/index.d.ts +3 -0
  149. package/dist/esm/v3/test/index.js +13 -0
  150. package/dist/esm/v3/test/index.js.map +1 -0
  151. package/dist/esm/v3/test/mock-chat-agent.d.ts +259 -0
  152. package/dist/esm/v3/test/mock-chat-agent.js +465 -0
  153. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -0
  154. package/dist/esm/v3/test/setup-catalog.d.ts +1 -0
  155. package/dist/esm/v3/test/setup-catalog.js +16 -0
  156. package/dist/esm/v3/test/setup-catalog.js.map +1 -0
  157. package/dist/esm/v3/test/test-session-handle.d.ts +53 -0
  158. package/dist/esm/v3/test/test-session-handle.js +251 -0
  159. package/dist/esm/v3/test/test-session-handle.js.map +1 -0
  160. package/dist/esm/version.js +1 -1
  161. package/package.json +87 -6
@@ -0,0 +1,737 @@
1
+ "use strict";
2
+ /**
3
+ * Server-side helpers for the `chat.agent` head-start flow — a
4
+ * customer's warm process (Next.js route handler, Express, etc.)
5
+ * gets the conversation moving while the heavy chat.agent run boots
6
+ * in parallel. Mid-turn, ownership of the durable stream hands over
7
+ * to the agent.
8
+ *
9
+ * The `chat.headStart({ agentId, run })` entry point returns a
10
+ * Next.js-style POST handler. Inside the customer's `run` callback
11
+ * they call `streamText` themselves, spreading
12
+ * `chat.toStreamTextOptions({ tools })` to inherit handover wiring.
13
+ * The handler runs `streamText` step 1 in the customer's process
14
+ * while the chat.agent run boots in parallel; on `tool-calls` the
15
+ * agent run picks up tool execution and continues, on pure-text the
16
+ * agent run exits clean without an LLM call.
17
+ *
18
+ * Two-layer naming: customer-facing surface is "head start"
19
+ * (describes the *benefit* — fast first-turn TTFC). The internal
20
+ * protocol still uses "handover" (describes the *mechanism* — the
21
+ * conversation hands off mid-turn from the warm process to the
22
+ * agent). Customers see `chat.headStart`, `HeadStartSession`, etc.
23
+ * The wire format and run-loop locals stay on `handover` /
24
+ * `handover-prepare` / `handover-skip`.
25
+ *
26
+ * Cooperative ordering only — handler stops writing to `session.out`
27
+ * before sending the `handover` chunk on `session.in`. No S2 fencing.
28
+ *
29
+ * ⚠️ HARD CONSTRAINT — bundle isolation
30
+ *
31
+ * This module is the customer-facing boundary for the route handler.
32
+ * The whole TTFC win comes from the customer's process being
33
+ * lightweight while the heavy agent run boots in parallel. **The
34
+ * route-handler bundle must not include heavy tool execute deps**:
35
+ * E2B, puppeteer/playwright, native bindings, the trigger SDK
36
+ * runtime, turndown, image processing libs, anything that pulls
37
+ * weight or pulls `node:` builtins.
38
+ *
39
+ * "Schema-only" tools must live in a module that imports only `ai`
40
+ * (for `tool()`) and `zod`. The agent task module imports those
41
+ * schemas and adds execute fns elsewhere — that's where the heavy
42
+ * deps live, and it's never reached by the route handler bundle.
43
+ *
44
+ * Runtime "strip executes" helpers (anything that takes a tool
45
+ * catalog with executes and removes them) DO NOT solve this. The
46
+ * import chain is resolved at bundle/build time, so importing the
47
+ * full catalog drags every dep in regardless of what the SDK does
48
+ * with the value at runtime.
49
+ *
50
+ * IMPORTANT (internal): this module must NOT import from `./ai.ts`.
51
+ * `ai.ts` statically imports `agentSkillsRuntime` (which uses `node:`
52
+ * builtins unfit for some serverless runtimes) and the heavy task
53
+ * runtime. Allowed imports: `./ai-shared.js`, `./chat-client.js`,
54
+ * `@trigger.dev/core/v3` (api client), `ai` (types + lightweight
55
+ * helpers like `stepCountIs` / `convertToModelMessages`).
56
+ */
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.chat = void 0;
59
+ const v3_1 = require("@trigger.dev/core/v3");
60
+ const ai_1 = require("ai");
61
+ // ---------------------------------------------------------------------------
62
+ // Public API
63
+ // ---------------------------------------------------------------------------
64
+ exports.chat = {
65
+ /**
66
+ * Returns a Next.js-style POST handler for the chat.agent
67
+ * head-start flow. Customer mounts it as
68
+ * `export const { POST } = chat.headStart({...})` (or
69
+ * `export const POST = chat.headStart({...})`).
70
+ *
71
+ * Pair with the browser transport's `headStart: "/api/chat"`
72
+ * option so the first message of a brand-new chat lands here
73
+ * before the agent run boots.
74
+ */
75
+ headStart(opts) {
76
+ return async (req) => {
77
+ const session = await openHandoverSession({
78
+ req,
79
+ agentId: opts.agentId,
80
+ idleTimeoutInSeconds: opts.idleTimeoutInSeconds,
81
+ });
82
+ const helper = {
83
+ toStreamTextOptions(spreadOpts) {
84
+ return session.buildStreamTextOptions(spreadOpts);
85
+ },
86
+ session: session.handle,
87
+ };
88
+ const result = await opts.run({
89
+ messages: session.uiMessages,
90
+ signal: session.combinedSignal,
91
+ chat: helper,
92
+ });
93
+ return session.handle.handoverResponse(result);
94
+ };
95
+ },
96
+ /**
97
+ * Lower-level primitive for power users who want to call
98
+ * `streamText` themselves outside the `run` callback shape — custom
99
+ * transforms, non-AI-SDK code paths, or manual control over the
100
+ * response. Same wiring `chat.headStart` builds on internally.
101
+ */
102
+ openSession(opts) {
103
+ return openHandoverSession(opts).then((s) => s.handle);
104
+ },
105
+ /**
106
+ * Wrap a Web Fetch handler — `(req: Request) => Promise<Response>` —
107
+ * as a Node `http` listener — `(req: IncomingMessage, res: ServerResponse) => Promise<void>`.
108
+ *
109
+ * Use this to mount `chat.headStart` (or any other Web Fetch
110
+ * handler) inside Node-only frameworks like Express, Fastify, Koa,
111
+ * or raw `node:http`. Web-native frameworks (Next.js App Router,
112
+ * Hono, SvelteKit, Remix, Workers, Bun, Deno, etc.) don't need
113
+ * this — they pass `Request` objects directly.
114
+ *
115
+ * Streams the response body chunk-by-chunk to the Node response,
116
+ * so the `chat.headStart` SSE chunks reach the browser as they
117
+ * arrive (no buffering). Aborts the underlying handler if the
118
+ * client closes the connection.
119
+ *
120
+ * Type-only import of `node:http` types — no runtime dep on `node:http`,
121
+ * so this stays safe to bundle into edge / Workers builds (the
122
+ * function just won't be called there).
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * import express from "express";
127
+ * import { chat } from "@trigger.dev/sdk/chat-server";
128
+ *
129
+ * const handler = chat.headStart({
130
+ * agentId: "my-chat",
131
+ * run: async ({ chat: helper }) => streamText({ ... }),
132
+ * });
133
+ *
134
+ * const app = express();
135
+ * app.post("/api/chat", chat.toNodeListener(handler));
136
+ * ```
137
+ */
138
+ toNodeListener,
139
+ };
140
+ async function openHandoverSession(opts) {
141
+ const wirePayload = (await opts.req.json());
142
+ const chatId = wirePayload.chatId;
143
+ if (!chatId) {
144
+ throw new Error("[chat.handover] request body missing `chatId`");
145
+ }
146
+ // Slim wire — head-start ships full history via `headStartMessages` (not
147
+ // `message`/`messages`) because the route handler runs on the customer's
148
+ // own HTTP endpoint and isn't subject to the 512 KiB `/in/append` cap.
149
+ // The full UIMessage[] flows through `wirePayload` into the auto-trigger
150
+ // `basePayload` below, where the agent run boot consumes it on first turn.
151
+ const uiMessages = (wirePayload.headStartMessages ?? []);
152
+ // `convertToModelMessages` is async — resolve once up front so the
153
+ // synchronous `toStreamTextOptions` builder can hand back a fully
154
+ // formed object. AI SDK's `streamText` validates `messages` as a
155
+ // `ModelMessage[]` synchronously and rejects a Promise.
156
+ const modelMessages = await (0, ai_1.convertToModelMessages)(uiMessages);
157
+ const apiClient = resolveApiClient();
158
+ const idleTimeoutInSeconds = opts.idleTimeoutInSeconds ?? 60;
159
+ // Create the session and trigger the chat.agent's `handover-prepare`
160
+ // run atomically. `createSession` is idempotent on `(env, externalId
161
+ // = chatId)` and the auto-triggered run uses `triggerConfig.
162
+ // basePayload` as the wire payload — so a single round-trip both
163
+ // ensures the session exists and starts the agent booting with the
164
+ // right trigger.
165
+ //
166
+ // Awaited intentionally: subsequent writes to `session.out` (the
167
+ // tee from the customer's `streamText` to S2) need the session to
168
+ // exist, and the handover signal at end-of-step-1 needs the agent
169
+ // run to be there to consume it. The added latency (~one round trip
170
+ // to the control plane) is bounded; the agent's compute boot still
171
+ // overlaps with LLM TTFB.
172
+ const created = await apiClient.createSession({
173
+ type: "chat.agent",
174
+ externalId: chatId,
175
+ taskIdentifier: opts.agentId,
176
+ triggerConfig: {
177
+ basePayload: {
178
+ ...wirePayload,
179
+ chatId,
180
+ trigger: "handover-prepare",
181
+ idleTimeoutInSeconds,
182
+ },
183
+ idleTimeoutInSeconds,
184
+ },
185
+ });
186
+ const sessionPublicAccessToken = created.publicAccessToken;
187
+ // Combined abort signal: request lifecycle OR an internal timeout
188
+ // mirroring the agent's idle wait so a hung handler doesn't sit
189
+ // forever.
190
+ const abortController = new AbortController();
191
+ const requestAbort = opts.req.signal;
192
+ if (requestAbort) {
193
+ if (requestAbort.aborted)
194
+ abortController.abort();
195
+ else
196
+ requestAbort.addEventListener("abort", () => abortController.abort(), { once: true });
197
+ }
198
+ const idleTimer = setTimeout(() => abortController.abort(new Error("chat.handover: idle timeout")), idleTimeoutInSeconds * 1000);
199
+ const buildStreamTextOptions = (spreadOpts) => {
200
+ // The customer spreads this object into their `streamText` call
201
+ // and then adds `model`, `system`, etc. on top. We set the four
202
+ // keys handover correctness depends on:
203
+ //
204
+ // - `messages`: the wire payload's UIMessages, converted
205
+ // (Promise resolved upfront so the spread is synchronous)
206
+ // - `tools`: customer's schema-only tool set
207
+ // - `stopWhen`: `stepCountIs(1)` — step 1 only. Agent run picks
208
+ // up tool execution and step 2+ after the handover signal.
209
+ // - `abortSignal`: combined request-lifecycle + idle timeout
210
+ //
211
+ // The customer's `StreamTextResult` exposes `finishReason` and
212
+ // `response.messages` directly, so we don't need to install an
213
+ // `onStepFinish` capture hook — we read those off the result in
214
+ // `handoverWhenDone`.
215
+ return {
216
+ messages: modelMessages,
217
+ tools: spreadOpts?.tools,
218
+ stopWhen: (0, ai_1.stepCountIs)(1),
219
+ abortSignal: abortController.signal,
220
+ };
221
+ };
222
+ // Tee a UIMessage stream into session.out via S2 direct-write,
223
+ // batched. `SessionStreamInstance` calls `initializeSessionStream`
224
+ // once to fetch S2 credentials, then pipes via `StreamsWriterV2`'s
225
+ // `BatchTransform` — one S2 append per ~200ms of chunks instead of
226
+ // one HTTP round-trip per UIMessageChunk.
227
+ let sessionWriter = null;
228
+ const tee = (stream) => {
229
+ const [a, b] = stream.tee();
230
+ sessionWriter = new v3_1.SessionStreamInstance({
231
+ apiClient,
232
+ baseUrl: apiClient.baseUrl,
233
+ sessionId: chatId, // Sessions are addressable by externalId (chatId).
234
+ io: "out",
235
+ source: b,
236
+ signal: abortController.signal,
237
+ });
238
+ return a;
239
+ };
240
+ /** Wait for the teed S2 writer to drain. Called before signaling handover. */
241
+ const flushSessionWriter = async () => {
242
+ if (!sessionWriter)
243
+ return;
244
+ try {
245
+ await sessionWriter.wait();
246
+ }
247
+ catch {
248
+ // Drop write errors — the customer's response stream is the
249
+ // source of truth for what the user sees. Durability/resume
250
+ // best-effort.
251
+ }
252
+ };
253
+ const handover = async (args) => {
254
+ const chunk = {
255
+ kind: "handover",
256
+ partialAssistantMessage: args.partialAssistantMessage,
257
+ messageId: args.messageId,
258
+ isFinal: args.isFinal,
259
+ };
260
+ await apiClient.appendToSessionStream(chatId, "in", JSON.stringify(chunk));
261
+ };
262
+ /**
263
+ * Sent only on dispatch error (handler aborted before producing a
264
+ * `finishReason`). Normal pure-text and tool-call finishes go
265
+ * through `handover()` with the appropriate `isFinal` flag.
266
+ */
267
+ const handoverSkip = async () => {
268
+ const chunk = { kind: "handover-skip" };
269
+ await apiClient.appendToSessionStream(chatId, "in", JSON.stringify(chunk));
270
+ };
271
+ // A stable assistant messageId for this turn. The customer's
272
+ // `toUIMessageStream` is configured to emit its `start` chunk with
273
+ // this id, the handover signal carries it to the agent, and the
274
+ // agent's post-handover `toUIMessageStream` reuses it — so all
275
+ // chunks (customer's step 1 + agent's step 2) merge into one
276
+ // assistant message on the browser side.
277
+ const turnMessageId = (0, ai_1.generateId)();
278
+ let resolveDecision;
279
+ const decisionPromise = new Promise((resolve) => {
280
+ resolveDecision = resolve;
281
+ });
282
+ const handoverWhenDone = async (result) => {
283
+ // Owns idle-timer cleanup via the finally below, so both the
284
+ // sugar (`handoverResponse`) and the escape-hatch
285
+ // (`chat.openSession()` → `handle.handoverWhenDone(...)`) clean up
286
+ // the timer the same way.
287
+ try {
288
+ // `result.finishReason` is a Promise<FinishReason> on the AI SDK
289
+ // result. Wait for the stream to settle, then dispatch.
290
+ const finishReason = await result.finishReason;
291
+ // Drain the S2 tee so any in-flight handler writes (last
292
+ // `tool-input-available` parts, the synthetic `finish-step` for
293
+ // pure-text) are visible before the agent reads from session.out
294
+ // / session.in. Cooperative ordering — agent doesn't read past
295
+ // these unless we've finished writing them.
296
+ await flushSessionWriter();
297
+ const responseMessages = (await result.response).messages;
298
+ if (finishReason === "tool-calls") {
299
+ // Reshape pending tool-calls into AI SDK's tool-approval round
300
+ // so the agent's `streamText` resumes by executing them
301
+ // before the step-2 LLM call.
302
+ const reshaped = reshapeForHandoverResume(responseMessages);
303
+ await handover({
304
+ partialAssistantMessage: reshaped,
305
+ messageId: turnMessageId,
306
+ isFinal: false,
307
+ });
308
+ }
309
+ else {
310
+ // Pure-text (or any non-tool-calls) finish — customer's step 1
311
+ // IS the final response. The agent runs the turn-loop hooks
312
+ // (`onChatStart`, `onTurnStart`, `onTurnComplete`, etc.) using
313
+ // this partial as the response, but skips the LLM call. That
314
+ // way persistence (`onTurnComplete` writing to DB), self-
315
+ // review, and any post-turn work all fire normally.
316
+ await handover({
317
+ partialAssistantMessage: responseMessages,
318
+ messageId: turnMessageId,
319
+ isFinal: true,
320
+ });
321
+ }
322
+ resolveDecision({ kind: "handover" });
323
+ }
324
+ catch (err) {
325
+ // Dispatch failed before we could send the handover signal.
326
+ // Tell the agent to exit clean (no hooks fire) and close the
327
+ // response stream so it doesn't hang waiting for agent chunks.
328
+ resolveDecision({ kind: "handover-skip" });
329
+ try {
330
+ await handoverSkip();
331
+ }
332
+ catch {
333
+ // best-effort
334
+ }
335
+ throw err;
336
+ }
337
+ finally {
338
+ clearTimeout(idleTimer);
339
+ }
340
+ };
341
+ /**
342
+ * Build a single ReadableStream that:
343
+ * 1. Forwards the customer's `streamText` chunks (step 1) directly
344
+ * to the response — same low-latency path as before.
345
+ * 2. After step 1 ends and the dispatch decision lands:
346
+ * - `handover-skip`: closes the response immediately. The agent
347
+ * run exits without writing more chunks.
348
+ * - `handover`: subscribes to `session.out` from the sequence
349
+ * ID where the customer's tee left off, forwarding the agent
350
+ * run's chunks (tool-output-available, step 2 LLM text,
351
+ * `finish-step`, etc.) until `trigger:turn-complete`.
352
+ *
353
+ * The browser sees one continuous SSE response per first turn, just
354
+ * like a normal `streamText` would produce.
355
+ */
356
+ const stitchHandoverStream = (customerBranch) => {
357
+ return new ReadableStream({
358
+ async start(controller) {
359
+ try {
360
+ // Phase 1: forward customer's chunks.
361
+ const reader = customerBranch.getReader();
362
+ try {
363
+ while (true) {
364
+ const { done, value } = await reader.read();
365
+ if (done)
366
+ break;
367
+ controller.enqueue(value);
368
+ }
369
+ }
370
+ finally {
371
+ reader.releaseLock();
372
+ }
373
+ // Phase 2a: wait for handoverWhenDone to decide.
374
+ const decision = await decisionPromise;
375
+ if (decision.kind === "handover-skip") {
376
+ controller.close();
377
+ return;
378
+ }
379
+ // Phase 2b: agent is taking over. Resume from session.out
380
+ // starting AFTER the customer tee's last write, so we don't
381
+ // re-emit chunks the browser already saw.
382
+ const writeResult = sessionWriter
383
+ ? await sessionWriter.wait().catch(() => undefined)
384
+ : undefined;
385
+ const customerLastEventId = writeResult?.lastEventId;
386
+ // Capture the latest S2 event id seen on session.out via
387
+ // `onPart`. After the stream closes we emit it to the
388
+ // browser as a `trigger:session-state` control chunk so the
389
+ // transport can hydrate `state.lastEventId` for turn 2's
390
+ // subscribe — without it, turn 2 reads session.out from the
391
+ // start and replays turn 1 to the user.
392
+ //
393
+ // The agent's `turn-complete` control record is now header-
394
+ // form on S2 (see `client-protocol.mdx`), so the
395
+ // `for await (const chunk of agentStream)` loop below NEVER
396
+ // sees it as a data chunk — `subscribeToSessionStream` routes
397
+ // it to `onControl`. Use that to know when to stop and
398
+ // synthesise the data-chunk shape the browser bridge still
399
+ // expects (this HTTP response stream is NOT S2 and keeps the
400
+ // legacy chunk shape for the customer-server-to-browser hop).
401
+ let latestEventId;
402
+ let turnComplete = false;
403
+ // Dedicated abort signal for this agent subscription. Aborted
404
+ // from `onControl` the moment turn-complete fires so the
405
+ // `for await` loop below exits immediately instead of blocking
406
+ // until S2's long-poll closes (~60s). Combined with the outer
407
+ // `abortController.signal` via `AbortSignal.any` so a request-
408
+ // wide abort still tears the subscription down.
409
+ const subscriptionAbort = new AbortController();
410
+ const agentStream = await apiClient.subscribeToSessionStream(chatId, "out", {
411
+ ...(customerLastEventId != null
412
+ ? { lastEventId: customerLastEventId }
413
+ : {}),
414
+ signal: AbortSignal.any([abortController.signal, subscriptionAbort.signal]),
415
+ onPart: (part) => {
416
+ if (part.id)
417
+ latestEventId = part.id;
418
+ },
419
+ onControl: (event) => {
420
+ if (event.subtype === v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE) {
421
+ turnComplete = true;
422
+ // Synthesise the data-chunk shape for the browser
423
+ // bridge. The customer-server-to-browser response is
424
+ // not S2; it keeps the legacy chunk shape so the
425
+ // browser's transport can recognise turn-complete the
426
+ // same way it always has.
427
+ controller.enqueue({
428
+ type: "trigger:turn-complete",
429
+ });
430
+ // Stop the SSE read now. Without this the `for await`
431
+ // can't see the control event (control records are
432
+ // never enqueued into the data stream) and would idle
433
+ // until S2's long-poll timeout closes the connection.
434
+ subscriptionAbort.abort();
435
+ }
436
+ },
437
+ });
438
+ try {
439
+ for await (const chunk of agentStream) {
440
+ // Data records only — control records are routed via
441
+ // `onControl` above and trigger the subscription abort.
442
+ controller.enqueue(chunk);
443
+ if (turnComplete)
444
+ break;
445
+ }
446
+ }
447
+ catch (err) {
448
+ // AbortError from `subscriptionAbort` is the expected exit
449
+ // path once turn-complete fires; surface anything else.
450
+ const isAbort = err instanceof Error && err.name === "AbortError";
451
+ if (!isAbort || !turnComplete)
452
+ throw err;
453
+ }
454
+ // Final control chunk: hand the browser transport the
455
+ // `lastEventId` it should use for the next turn's
456
+ // session.out subscribe. Filtered out before reaching the
457
+ // AI SDK on the browser side.
458
+ if (latestEventId != null) {
459
+ controller.enqueue({
460
+ type: "trigger:session-state",
461
+ lastEventId: latestEventId,
462
+ });
463
+ }
464
+ controller.close();
465
+ }
466
+ catch (err) {
467
+ controller.error(err);
468
+ }
469
+ },
470
+ cancel() {
471
+ // Browser closed the connection. Trigger the abort so any
472
+ // pending session.out subscription stops too.
473
+ abortController.abort();
474
+ },
475
+ });
476
+ };
477
+ const handoverResponse = (result) => {
478
+ // `generateMessageId` makes the customer's `start` chunk carry
479
+ // `turnMessageId`, so the browser-side AI SDK keys the assistant
480
+ // message by it. The agent's post-handover stream emits chunks
481
+ // with the same id (passed via the handover signal) — both sides
482
+ // merge into one message on the browser.
483
+ const teed = tee(result.toUIMessageStream({
484
+ generateMessageId: () => turnMessageId,
485
+ }));
486
+ // `handoverWhenDone` re-throws on dispatch failure for visibility,
487
+ // but the recovery (resolveDecision + handoverSkip) has already run
488
+ // by then and `stitchHandoverStream` closes the response cleanly via
489
+ // `decisionPromise`. The user-facing path is fine; we only suppress
490
+ // the unhandled-rejection so processes started with
491
+ // `--unhandled-rejections=throw` don't crash on what is effectively
492
+ // a logged failure with no further action to take.
493
+ // (Idle-timer cleanup lives inside `handoverWhenDone` itself.)
494
+ void handoverWhenDone(result).catch(() => { });
495
+ const stitched = stitchHandoverStream(teed);
496
+ // Encode UIMessageChunks as SSE for the AI SDK transport on the
497
+ // browser. AI SDK's `toUIMessageStreamResponse()` does this same
498
+ // thing internally; replicate the format here so we don't have
499
+ // to bridge through the SDK's response helper.
500
+ const encoder = new TextEncoder();
501
+ const sseStream = stitched.pipeThrough(new TransformStream({
502
+ transform(chunk, controller) {
503
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
504
+ },
505
+ }));
506
+ return new Response(sseStream, {
507
+ headers: {
508
+ "Content-Type": "text/event-stream",
509
+ "X-Vercel-AI-UI-Message-Stream": "v1",
510
+ "Cache-Control": "no-cache, no-transform",
511
+ Connection: "keep-alive",
512
+ // Browser transport reads these to hydrate session state
513
+ // for subsequent (non-handover) turns. Once the browser has
514
+ // the PAT it talks directly to `session.in` / `session.out`
515
+ // without going back through the handler.
516
+ "X-Trigger-Chat-Id": chatId,
517
+ "X-Trigger-Chat-Access-Token": sessionPublicAccessToken,
518
+ },
519
+ });
520
+ };
521
+ const handle = {
522
+ chatId,
523
+ tee,
524
+ handoverWhenDone,
525
+ handoverResponse,
526
+ handover,
527
+ handoverSkip,
528
+ };
529
+ return {
530
+ uiMessages,
531
+ combinedSignal: abortController.signal,
532
+ handle,
533
+ buildStreamTextOptions,
534
+ };
535
+ }
536
+ function resolveApiClient() {
537
+ // Reuse the SDK's standard apiClientManager so customers configure
538
+ // base URL + secret key the same way as for `tasks.trigger(...)`.
539
+ const client = v3_1.apiClientManager.clientOrThrow();
540
+ return client;
541
+ }
542
+ /** @internal — exposed via `chat.toNodeListener`. */
543
+ function toNodeListener(webHandler) {
544
+ return async function nodeListener(req, res) {
545
+ const abort = new AbortController();
546
+ res.on("close", () => abort.abort());
547
+ try {
548
+ const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
549
+ const method = req.method ?? "GET";
550
+ const hasBody = method !== "GET" && method !== "HEAD";
551
+ // Read full body upfront. Chat wire payloads are small (sub-KB
552
+ // typically) so accumulating avoids the duplex-stream ceremony
553
+ // some Node versions need for streaming request bodies into
554
+ // a Web Request.
555
+ let body;
556
+ if (hasBody) {
557
+ const chunks = [];
558
+ for await (const chunk of req) {
559
+ chunks.push(chunk);
560
+ }
561
+ if (chunks.length > 0) {
562
+ let total = 0;
563
+ for (const c of chunks)
564
+ total += c.length;
565
+ const merged = new Uint8Array(total);
566
+ let offset = 0;
567
+ for (const c of chunks) {
568
+ merged.set(c, offset);
569
+ offset += c.length;
570
+ }
571
+ body = merged.buffer.slice(merged.byteOffset, merged.byteOffset + merged.byteLength);
572
+ }
573
+ }
574
+ // Flatten Node header values: arrays → comma-joined (per RFC 7230 §3.2.2).
575
+ const webHeaders = new Headers();
576
+ for (const [name, value] of Object.entries(req.headers)) {
577
+ if (value == null)
578
+ continue;
579
+ if (Array.isArray(value)) {
580
+ for (const v of value)
581
+ webHeaders.append(name, v);
582
+ }
583
+ else {
584
+ webHeaders.set(name, value);
585
+ }
586
+ }
587
+ const webReq = new Request(url, {
588
+ method,
589
+ headers: webHeaders,
590
+ body,
591
+ signal: abort.signal,
592
+ });
593
+ const webRes = await webHandler(webReq);
594
+ res.statusCode = webRes.status;
595
+ // `Headers.forEach` exposes the value comma-joined for multi-valued
596
+ // headers, which `setHeader` accepts. Set-Cookie is handled separately
597
+ // via `getSetCookie()` to preserve multiple values.
598
+ webRes.headers.forEach((value, key) => {
599
+ if (key.toLowerCase() === "set-cookie")
600
+ return;
601
+ res.setHeader(key, value);
602
+ });
603
+ const setCookies = typeof webRes.headers.getSetCookie === "function"
604
+ ? webRes.headers.getSetCookie()
605
+ : [];
606
+ if (setCookies.length > 0) {
607
+ res.setHeader("set-cookie", setCookies);
608
+ }
609
+ if (!webRes.body) {
610
+ res.end();
611
+ return;
612
+ }
613
+ // Pipe the Web Response body to the Node response. On client
614
+ // disconnect (`abort.signal`), cancel the reader so a pending
615
+ // `read()` rejects and we exit the loop instead of blocking on
616
+ // a stream that will never produce more chunks.
617
+ const reader = webRes.body.getReader();
618
+ const onAbort = () => {
619
+ reader.cancel(abort.signal.reason).catch(() => { });
620
+ };
621
+ if (abort.signal.aborted)
622
+ onAbort();
623
+ else
624
+ abort.signal.addEventListener("abort", onAbort, { once: true });
625
+ try {
626
+ while (true) {
627
+ const { done, value } = await reader.read();
628
+ if (done)
629
+ break;
630
+ res.write(value);
631
+ }
632
+ }
633
+ catch {
634
+ // Reader was cancelled (client disconnect). Silently end.
635
+ }
636
+ finally {
637
+ abort.signal.removeEventListener("abort", onAbort);
638
+ }
639
+ res.end();
640
+ }
641
+ catch (err) {
642
+ if (!res.headersSent) {
643
+ res.statusCode = 500;
644
+ res.setHeader("content-type", "text/plain; charset=utf-8");
645
+ res.end(err instanceof Error ? err.message : "Internal error");
646
+ }
647
+ else {
648
+ res.end();
649
+ }
650
+ }
651
+ };
652
+ }
653
+ /**
654
+ * Reshape a step-1 partial so the agent's `streamText` resumes by
655
+ * executing pending tool-calls before the next LLM call.
656
+ *
657
+ * When the customer's handler runs `streamText` with schema-only tools
658
+ * (no `execute` fns) and `stopWhen: stepCountIs(1)`, the LLM emits
659
+ * tool-calls but AI SDK can't execute them — the partial we ship is
660
+ * `[{ assistant: text + tool-call }]`. Splicing that as-is onto the
661
+ * agent's accumulator and calling `streamText` throws
662
+ * `MissingToolResultsError` synchronously inside
663
+ * `convertToLanguageModelPrompt`.
664
+ *
665
+ * AI SDK's documented escape hatch for "external party decides what
666
+ * to do with a tool-call, then SDK executes" is the tool-approval
667
+ * round. By appending a `tool-approval-request` part to the assistant
668
+ * message and a trailing `tool` message with a matching
669
+ * `tool-approval-response { approved: true }`, AI SDK:
670
+ * 1. Suppresses `MissingToolResultsError` for approved tool-calls
671
+ * (`convert-to-language-model-prompt.ts:135-144`).
672
+ * 2. Hits its initial-tool-execution branch
673
+ * (`stream-text.ts:1342-1486`) on the next `streamText` call,
674
+ * runs the agent-side `execute` fns, and synthesizes
675
+ * `tool-result` parts before the step-2 LLM call.
676
+ *
677
+ * If the customer's tools already had `execute` fns (rare for the
678
+ * handover use case but valid), the partial already contains a
679
+ * `tool-result` per tool-call — we leave those alone and only inject
680
+ * approvals for genuinely-pending calls.
681
+ *
682
+ * `collectToolApprovals` only scans the LAST message
683
+ * (`collect-tool-approvals.ts:30-37`), so the synthesized tool message
684
+ * must end up at the tail of the partial. The agent's run-loop
685
+ * splices the partial onto the end of the accumulator, which keeps
686
+ * this invariant.
687
+ */
688
+ function reshapeForHandoverResume(responseMessages) {
689
+ // First pass: gather the set of tool-call IDs that already have a
690
+ // matching tool-result. Those are "complete" — leave them alone.
691
+ const completedToolCallIds = new Set();
692
+ for (const message of responseMessages) {
693
+ if (message.role !== "tool" || typeof message.content === "string")
694
+ continue;
695
+ for (const part of message.content) {
696
+ if (part.type === "tool-result" && part.toolCallId) {
697
+ completedToolCallIds.add(part.toolCallId);
698
+ }
699
+ }
700
+ }
701
+ // Second pass: clone the messages, appending a tool-approval-request
702
+ // alongside each pending tool-call. Collect the matching responses.
703
+ const approvalResponses = [];
704
+ let approvalCounter = 0;
705
+ const reshaped = responseMessages.map((message) => {
706
+ if (message.role !== "assistant" || typeof message.content === "string") {
707
+ return message;
708
+ }
709
+ const newContent = [...message.content];
710
+ for (const part of message.content) {
711
+ if (part.type === "tool-call" &&
712
+ part.toolCallId &&
713
+ !completedToolCallIds.has(part.toolCallId)) {
714
+ const approvalId = `handover-approval-${++approvalCounter}`;
715
+ newContent.push({
716
+ type: "tool-approval-request",
717
+ approvalId,
718
+ toolCallId: part.toolCallId,
719
+ });
720
+ approvalResponses.push({
721
+ type: "tool-approval-response",
722
+ approvalId,
723
+ approved: true,
724
+ });
725
+ }
726
+ }
727
+ return { ...message, content: newContent };
728
+ });
729
+ if (approvalResponses.length > 0) {
730
+ reshaped.push({
731
+ role: "tool",
732
+ content: approvalResponses,
733
+ });
734
+ }
735
+ return reshaped;
736
+ }
737
+ //# sourceMappingURL=chat-server.js.map