@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,961 @@
1
+ /**
2
+ * @module @trigger.dev/sdk/chat
3
+ *
4
+ * Browser-safe module for AI SDK chat transport integration.
5
+ * Use this on the frontend with the AI SDK's `useChat` hook.
6
+ *
7
+ * For backend helpers (`chatAgent`, `pipeChat`), use `@trigger.dev/sdk/ai` instead.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * import { useChat } from "@ai-sdk/react";
12
+ * import { TriggerChatTransport } from "@trigger.dev/sdk/chat";
13
+ *
14
+ * function Chat() {
15
+ * const { messages, sendMessage, status } = useChat({
16
+ * transport: new TriggerChatTransport({
17
+ * task: "my-chat-task",
18
+ * accessToken: async ({ chatId }) => fetchSessionToken(chatId),
19
+ * startSession: async ({ chatId, taskId }) => createChatSession({ chatId, taskId }),
20
+ * }),
21
+ * });
22
+ * }
23
+ * ```
24
+ */
25
+ import { controlSubtype, headerValue, PUBLIC_ACCESS_TOKEN_HEADER, SSEStreamSubscription, TRIGGER_CONTROL_SUBTYPE, } from "@trigger.dev/core/v3";
26
+ import { ChatTabCoordinator } from "./chat-tab-coordinator.js";
27
+ const DEFAULT_BASE_URL = "https://api.trigger.dev";
28
+ const DEFAULT_STREAM_TIMEOUT_SECONDS = 120;
29
+ /**
30
+ * Detect 401/403 from realtime/input-stream calls without relying on `instanceof`
31
+ * (Vitest can load duplicate `@trigger.dev/core` copies, which breaks subclass checks).
32
+ */
33
+ function isAuthError(error) {
34
+ if (error === null || typeof error !== "object")
35
+ return false;
36
+ const e = error;
37
+ return e.name === "TriggerApiError" && (e.status === 401 || e.status === 403);
38
+ }
39
+ /**
40
+ * Parses an SSE byte/text stream of `data: <UIMessageChunk JSON>\n\n`
41
+ * frames back into `UIMessageChunk` objects. Used by the handover
42
+ * first-turn path to convert the customer's route handler response
43
+ * (which is AI-SDK-shaped SSE text) into the chunk form the AI SDK's
44
+ * `useChat` consumes from a transport.
45
+ *
46
+ * Spec-light parser — assumes well-formed `data:` events from our own
47
+ * `chat.handover` SSE writer. Lines starting with `:` (comments) and
48
+ * other event types are ignored.
49
+ */
50
+ function parseUIMessageSseTransform() {
51
+ let buffer = "";
52
+ return new TransformStream({
53
+ transform(chunk, controller) {
54
+ buffer += chunk;
55
+ // Frames are separated by blank lines.
56
+ let idx = buffer.indexOf("\n\n");
57
+ while (idx !== -1) {
58
+ const frame = buffer.slice(0, idx);
59
+ buffer = buffer.slice(idx + 2);
60
+ for (const line of frame.split("\n")) {
61
+ if (line.startsWith("data: ")) {
62
+ const data = line.slice(6).trim();
63
+ if (!data)
64
+ continue;
65
+ try {
66
+ controller.enqueue(JSON.parse(data));
67
+ }
68
+ catch {
69
+ /* drop malformed chunk; the response source is our own writer */
70
+ }
71
+ }
72
+ }
73
+ idx = buffer.indexOf("\n\n");
74
+ }
75
+ },
76
+ flush(controller) {
77
+ // Trailing data without a closing blank line — treat as a final frame.
78
+ if (buffer.trim().length === 0)
79
+ return;
80
+ for (const line of buffer.split("\n")) {
81
+ if (line.startsWith("data: ")) {
82
+ const data = line.slice(6).trim();
83
+ if (!data)
84
+ continue;
85
+ try {
86
+ controller.enqueue(JSON.parse(data));
87
+ }
88
+ catch {
89
+ /* drop */
90
+ }
91
+ }
92
+ }
93
+ buffer = "";
94
+ },
95
+ });
96
+ }
97
+ /**
98
+ * A custom AI SDK `ChatTransport` that runs chat completions as durable
99
+ * Trigger.dev tasks via the Sessions primitive.
100
+ *
101
+ * Lifecycle:
102
+ * 1. Customer pre-creates the session server-side OR calls
103
+ * `transport.start(chatId)` to mint a one-shot start token and
104
+ * `POST /api/v1/sessions` from the browser.
105
+ * 2. The server triggers the first run as part of session create and
106
+ * returns a session-scoped PAT.
107
+ * 3. `sendMessages` appends to `.in` and subscribes to `.out`. When a
108
+ * run dies (idle, cancel, end-and-continue), the server's
109
+ * append-time probe triggers a fresh run for the same session —
110
+ * transport keeps streaming.
111
+ * 4. `stop()` posts a `{kind:"stop"}` chunk; the agent's turn aborts
112
+ * but the run keeps reading `.in` for the next message.
113
+ * 5. PAT expiry: transport invokes `accessToken` to refresh and
114
+ * retries the failing request once.
115
+ */
116
+ export class TriggerChatTransport {
117
+ taskId;
118
+ resolveAccessToken;
119
+ resolveStartSession;
120
+ resolveBaseURLFn;
121
+ fetchOverride;
122
+ extraHeaders;
123
+ streamTimeoutSeconds;
124
+ defaultMetadata;
125
+ watchMode;
126
+ headStart;
127
+ coordinator = null;
128
+ _onSessionChange;
129
+ sessions = new Map();
130
+ activeStreams = new Map();
131
+ pendingStarts = new Map();
132
+ constructor(options) {
133
+ this.taskId = options.task;
134
+ this.resolveAccessToken = options.accessToken;
135
+ this.resolveStartSession = options.startSession;
136
+ const baseURLOption = options.baseURL ?? DEFAULT_BASE_URL;
137
+ const streamOverride = options.streamBaseURL;
138
+ this.resolveBaseURLFn = typeof baseURLOption === "function"
139
+ ? (ctx) => (ctx.endpoint === "out" && streamOverride ? streamOverride : baseURLOption(ctx))
140
+ : (ctx) => (ctx.endpoint === "out" && streamOverride ? streamOverride : baseURLOption);
141
+ this.fetchOverride = options.fetch;
142
+ this.extraHeaders = options.headers ?? {};
143
+ this.streamTimeoutSeconds = options.streamTimeoutSeconds ?? DEFAULT_STREAM_TIMEOUT_SECONDS;
144
+ this.defaultMetadata = options.clientData;
145
+ this._onSessionChange = options.onSessionChange;
146
+ this.watchMode = options.watch ?? false;
147
+ this.headStart = options.headStart;
148
+ if (options.multiTab && !this.watchMode) {
149
+ this.coordinator = new ChatTabCoordinator();
150
+ this.coordinator.addSessionListener((chatId, sessionUpdate) => {
151
+ const session = this.sessions.get(chatId);
152
+ if (session && sessionUpdate.lastEventId) {
153
+ session.lastEventId = sessionUpdate.lastEventId;
154
+ }
155
+ });
156
+ }
157
+ if (options.sessions) {
158
+ for (const [chatId, session] of Object.entries(options.sessions)) {
159
+ this.sessions.set(chatId, {
160
+ publicAccessToken: session.publicAccessToken,
161
+ lastEventId: session.lastEventId,
162
+ isStreaming: session.isStreaming,
163
+ });
164
+ }
165
+ }
166
+ }
167
+ // -------------------------------------------------------------------------
168
+ // Public lifecycle
169
+ // -------------------------------------------------------------------------
170
+ /**
171
+ * Eagerly create a Session and trigger its first run. Useful as a
172
+ * "the user might be about to send a message — boot the agent now"
173
+ * preload, or to take ownership of the session before any sendMessage.
174
+ *
175
+ * Idempotent: calling `start(chatId)` twice converges to the same
176
+ * session via the `(env, externalId)` upsert. Concurrent calls
177
+ * deduplicate via an in-flight promise.
178
+ *
179
+ * Requires `getStartToken` to be configured. Customers who pre-create
180
+ * sessions server-side don't need to call this.
181
+ */
182
+ async start(chatId) {
183
+ const existing = this.sessions.get(chatId);
184
+ if (existing?.publicAccessToken) {
185
+ return this.toPersisted(existing);
186
+ }
187
+ const inflight = this.pendingStarts.get(chatId);
188
+ if (inflight)
189
+ return inflight.then(this.toPersisted);
190
+ const promise = this.doStart(chatId).finally(() => {
191
+ this.pendingStarts.delete(chatId);
192
+ });
193
+ this.pendingStarts.set(chatId, promise);
194
+ return promise.then(this.toPersisted);
195
+ }
196
+ /**
197
+ * Eagerly create the session before the user types. Same semantics as
198
+ * {@link start} — kept as a separate name for the AI SDK Chat hook,
199
+ * which calls `preload` rather than `start`.
200
+ */
201
+ async preload(chatId) {
202
+ await this.start(chatId);
203
+ }
204
+ /**
205
+ * Send a user message via the session's `.in` channel. The server
206
+ * probes `currentRunId`; if terminal/null it triggers a fresh run on
207
+ * the same session before the append lands. The returned
208
+ * `ReadableStream` carries the agent's response chunks via `.out` SSE.
209
+ */
210
+ sendMessages = async (options) => {
211
+ const { trigger, chatId, messageId, messages, abortSignal, body, metadata } = options;
212
+ if (this.coordinator) {
213
+ if (this.coordinator.isReadOnly(chatId)) {
214
+ throw new Error("This chat is active in another tab");
215
+ }
216
+ this.coordinator.claim(chatId);
217
+ }
218
+ const mergedMetadata = this.defaultMetadata || metadata
219
+ ? { ...(this.defaultMetadata ?? {}), ...(metadata ?? {}) }
220
+ : undefined;
221
+ // First-turn handover routing — when `headStart` is set AND no
222
+ // session state exists yet for this chatId, POST the wire payload
223
+ // to the customer's `chat.handover` route handler. The handler
224
+ // creates the session, triggers the agent run with
225
+ // `handover-prepare`, runs `streamText` step 1 in its warm
226
+ // process, and tees the output back as the SSE response. We
227
+ // hydrate session state from the response headers so subsequent
228
+ // turns bypass the handler and use direct `session.in` writes.
229
+ if (this.headStart && !this.sessions.has(chatId)) {
230
+ return this.sendMessagesViaHandover({
231
+ trigger,
232
+ chatId,
233
+ messageId,
234
+ messages,
235
+ abortSignal,
236
+ body,
237
+ metadata: mergedMetadata,
238
+ });
239
+ }
240
+ // Slim wire — at most ONE message per record. The agent rebuilds prior
241
+ // history from its durable S3 snapshot + session.out replay at run boot
242
+ // (or `hydrateMessages`, if registered). See plan vivid-humming-bonbon.
243
+ //
244
+ // - "submit-message": ship the latest message (new user message OR a
245
+ // tool-approval-responded assistant message). Throw if absent.
246
+ // - "regenerate-message": omit `message`; the agent slices its own
247
+ // history (drops the trailing assistant) and re-runs.
248
+ if (trigger === "submit-message" && messages.length === 0) {
249
+ throw new Error("TriggerChatTransport.sendMessages: 'submit-message' trigger requires at least one message");
250
+ }
251
+ const wirePayload = {
252
+ ...(body ?? {}),
253
+ ...(trigger === "submit-message" ? { message: messages.at(-1) } : {}),
254
+ chatId,
255
+ trigger,
256
+ messageId,
257
+ metadata: mergedMetadata,
258
+ };
259
+ const state = await this.ensureSessionState(chatId);
260
+ const sendChatMessage = async (token) => {
261
+ await this.appendInputChunk(chatId, token, this.serializeInputChunk({ kind: "message", payload: wirePayload }));
262
+ };
263
+ await this.callWithAuthRetry(chatId, state, sendChatMessage);
264
+ // Cancel any in-flight stream for this chat — the new turn supersedes it.
265
+ const activeStream = this.activeStreams.get(chatId);
266
+ if (activeStream) {
267
+ activeStream.abort();
268
+ this.activeStreams.delete(chatId);
269
+ }
270
+ state.isStreaming = true;
271
+ this.notifySessionChange(chatId, state);
272
+ return this.subscribeToSessionStream(state, abortSignal, chatId);
273
+ };
274
+ /**
275
+ * First-turn-only path used when `headStart` is configured. POSTs the
276
+ * wire payload to the customer's `chat.handover` route handler and
277
+ * pipes its SSE response back as a UIMessageChunk stream. Hydrates
278
+ * session state from response headers so subsequent turns bypass
279
+ * the endpoint and use the direct `session.in` path.
280
+ */
281
+ async sendMessagesViaHandover(args) {
282
+ if (!this.headStart) {
283
+ throw new Error("sendMessagesViaHandover called without headStart configured");
284
+ }
285
+ // Head-start ships full UIMessage history via `headStartMessages`. The
286
+ // route handler runs on the customer's own HTTP endpoint (NOT
287
+ // `/realtime/v1/sessions/{id}/in/append`), so the 512 KiB body cap
288
+ // doesn't apply. The agent's run boot consumes `headStartMessages` ONLY
289
+ // when no snapshot exists yet (very first turn) — see plan section B.3.
290
+ const wirePayload = {
291
+ ...(args.body ?? {}),
292
+ headStartMessages: args.messages,
293
+ chatId: args.chatId,
294
+ trigger: args.trigger,
295
+ messageId: args.messageId,
296
+ metadata: args.metadata,
297
+ };
298
+ const response = await fetch(this.headStart, {
299
+ method: "POST",
300
+ headers: {
301
+ "Content-Type": "application/json",
302
+ ...this.extraHeaders,
303
+ },
304
+ body: JSON.stringify(wirePayload),
305
+ signal: args.abortSignal,
306
+ });
307
+ if (!response.ok) {
308
+ throw new Error(`chat.handover endpoint returned ${response.status} ${response.statusText}`);
309
+ }
310
+ if (!response.body) {
311
+ throw new Error("chat.handover endpoint returned no response body");
312
+ }
313
+ // Hydrate session state from response headers so subsequent turns
314
+ // skip the endpoint and write directly to session.in. Failing fast
315
+ // when the header is missing avoids a quiet degraded state where
316
+ // every later turn re-runs the handover route instead of taking
317
+ // the slim-wire path.
318
+ const accessToken = response.headers.get("X-Trigger-Chat-Access-Token");
319
+ const chatId = args.chatId;
320
+ if (!accessToken) {
321
+ throw new Error("chat.handover response is missing the X-Trigger-Chat-Access-Token header. chat.agent's handover endpoint must echo the session PAT so the transport can hydrate.");
322
+ }
323
+ const state = {
324
+ publicAccessToken: accessToken,
325
+ isStreaming: true,
326
+ };
327
+ this.sessions.set(chatId, state);
328
+ this.notifySessionChange(chatId, state);
329
+ // Filter the parsed UIMessage stream:
330
+ // - Drop control chunks (`trigger:turn-complete`,
331
+ // `trigger:session-state`) before they reach AI SDK — they
332
+ // aren't valid UIMessageChunks and the AI SDK chunk parser
333
+ // would reject them.
334
+ // - On `trigger:turn-complete`, clear `isStreaming` so the
335
+ // useChat resume / reconnectToStream path doesn't open a
336
+ // second `session.out` subscription on top of our stitched
337
+ // response.
338
+ // - On `trigger:session-state`, hydrate `state.lastEventId`
339
+ // with the agent's final S2 event id. Without this, turn 2's
340
+ // `session.out` subscribe reads from the start and replays
341
+ // turn 1's chunks back into the UI.
342
+ // - On stream end (handover-skip case — no
343
+ // `trigger:turn-complete` arrives, customer's stream just
344
+ // ends), also clear `isStreaming` for the same reason.
345
+ const sessions = this.sessions;
346
+ const notifyChange = (id, state) => this.notifySessionChange(id, state);
347
+ const TRIGGER_TURN_COMPLETE = "trigger:turn-complete";
348
+ const TRIGGER_SESSION_STATE = "trigger:session-state";
349
+ const clearStreaming = () => {
350
+ const state = sessions.get(chatId);
351
+ if (state && state.isStreaming) {
352
+ state.isStreaming = false;
353
+ notifyChange(chatId, state);
354
+ }
355
+ };
356
+ const setLastEventId = (lastEventId) => {
357
+ const state = sessions.get(chatId);
358
+ if (state) {
359
+ state.lastEventId = lastEventId;
360
+ notifyChange(chatId, state);
361
+ }
362
+ };
363
+ return response.body
364
+ .pipeThrough(new TextDecoderStream())
365
+ .pipeThrough(parseUIMessageSseTransform())
366
+ .pipeThrough(new TransformStream({
367
+ transform(chunk, controller) {
368
+ if (chunk && typeof chunk === "object") {
369
+ const type = chunk.type;
370
+ if (type === TRIGGER_TURN_COMPLETE) {
371
+ clearStreaming();
372
+ return; // drop — not a real UIMessageChunk
373
+ }
374
+ if (type === TRIGGER_SESSION_STATE) {
375
+ const lastEventId = chunk.lastEventId;
376
+ if (typeof lastEventId === "string") {
377
+ setLastEventId(lastEventId);
378
+ }
379
+ return; // drop
380
+ }
381
+ }
382
+ controller.enqueue(chunk);
383
+ },
384
+ flush() {
385
+ clearStreaming();
386
+ },
387
+ }));
388
+ }
389
+ /**
390
+ * Send a steering message during an active stream without disrupting
391
+ * it. The agent's `pendingMessages` config decides whether to inject
392
+ * between tool-call steps or buffer for the next turn.
393
+ */
394
+ sendPendingMessage = async (chatId, message, metadata) => {
395
+ const state = this.sessions.get(chatId);
396
+ if (!state)
397
+ return false;
398
+ const mergedMetadata = this.defaultMetadata || metadata
399
+ ? { ...(this.defaultMetadata ?? {}), ...(metadata ?? {}) }
400
+ : undefined;
401
+ const wirePayload = {
402
+ message,
403
+ chatId,
404
+ trigger: "submit-message",
405
+ metadata: mergedMetadata,
406
+ };
407
+ const send = async (token) => {
408
+ await this.appendInputChunk(chatId, token, this.serializeInputChunk({ kind: "message", payload: wirePayload }));
409
+ };
410
+ try {
411
+ await this.callWithAuthRetry(chatId, state, send);
412
+ return true;
413
+ }
414
+ catch {
415
+ return false;
416
+ }
417
+ };
418
+ /**
419
+ * Re-establish an SSE subscription to a known session. Used after a
420
+ * page refresh: the customer hydrates `sessions` in the constructor,
421
+ * the AI SDK calls `reconnectToStream` to resume the stream.
422
+ */
423
+ reconnectToStream = async (options) => {
424
+ const state = this.sessions.get(options.chatId);
425
+ if (!state)
426
+ return null;
427
+ if (state.isStreaming === false)
428
+ return null;
429
+ if (this.activeStreams.has(options.chatId))
430
+ return null;
431
+ const abortController = new AbortController();
432
+ this.activeStreams.set(options.chatId, abortController);
433
+ const abortSignal = options.abortSignal
434
+ ? AbortSignal.any([options.abortSignal, abortController.signal])
435
+ : abortController.signal;
436
+ return this.subscribeToSessionStream(state, abortSignal, options.chatId, {
437
+ sendStopOnAbort: !!options.abortSignal,
438
+ // Reconnect-on-reload opts into the server's settled-peek shortcut
439
+ // so the SSE doesn't hang for 60s when no turn is in flight. Active
440
+ // send-a-message paths must keep wait=60 to avoid racing the
441
+ // freshly-triggered turn's first chunk.
442
+ peekSettled: true,
443
+ });
444
+ };
445
+ /**
446
+ * Stop the current generation. Sends `{kind:"stop"}` on `.in`; the
447
+ * agent aborts its `streamText` call but stays alive for the next
448
+ * message.
449
+ */
450
+ stopGeneration = async (chatId) => {
451
+ const state = this.sessions.get(chatId);
452
+ if (!state)
453
+ return false;
454
+ const send = async (token) => {
455
+ await this.appendInputChunk(chatId, token, this.serializeInputChunk({ kind: "stop" }));
456
+ };
457
+ try {
458
+ await this.callWithAuthRetry(chatId, state, send);
459
+ }
460
+ catch {
461
+ return false;
462
+ }
463
+ state.skipToTurnComplete = true;
464
+ const activeStream = this.activeStreams.get(chatId);
465
+ if (activeStream) {
466
+ activeStream.abort();
467
+ this.activeStreams.delete(chatId);
468
+ }
469
+ return true;
470
+ };
471
+ /**
472
+ * Send a custom action chunk (for `chat.agent`'s `actionSchema` /
473
+ * `onAction` hook). Actions are not turns — only `hydrateMessages`
474
+ * and `onAction` fire on the agent side. The returned stream
475
+ * carries any model response `onAction` produced (when it returns a
476
+ * `StreamTextResult`); for `void`-returning side-effect-only actions
477
+ * the stream completes immediately with `trigger:turn-complete`.
478
+ */
479
+ sendAction = async (chatId, action) => {
480
+ if (this.coordinator) {
481
+ if (this.coordinator.isReadOnly(chatId)) {
482
+ throw new Error("This chat is active in another tab");
483
+ }
484
+ this.coordinator.claim(chatId);
485
+ }
486
+ const state = await this.ensureSessionState(chatId);
487
+ const wirePayload = {
488
+ chatId,
489
+ trigger: "action",
490
+ action,
491
+ metadata: this.defaultMetadata ?? undefined,
492
+ };
493
+ const body = this.serializeInputChunk({ kind: "message", payload: wirePayload });
494
+ const send = async (token) => {
495
+ await this.appendInputChunk(chatId, token, body);
496
+ };
497
+ await this.callWithAuthRetry(chatId, state, send);
498
+ return this.subscribeToSessionStream(state, undefined, chatId);
499
+ };
500
+ // -------------------------------------------------------------------------
501
+ // External-state surface
502
+ // -------------------------------------------------------------------------
503
+ getSession = (chatId) => {
504
+ const state = this.sessions.get(chatId);
505
+ if (!state)
506
+ return undefined;
507
+ return this.toPersisted(state);
508
+ };
509
+ setSession(chatId, session) {
510
+ this.sessions.set(chatId, {
511
+ publicAccessToken: session.publicAccessToken,
512
+ lastEventId: session.lastEventId,
513
+ isStreaming: session.isStreaming,
514
+ });
515
+ this.notifySessionChange(chatId, this.toPersisted(this.sessions.get(chatId)));
516
+ }
517
+ setOnSessionChange(callback) {
518
+ this._onSessionChange = callback;
519
+ }
520
+ /**
521
+ * Update the transport's `clientData`. Used by `useTriggerChatTransport`
522
+ * to keep the latest value reachable from inside `startSession` and
523
+ * the per-turn `metadata` merge without recreating the transport.
524
+ *
525
+ * Reads always go through the live field — closures around the
526
+ * transport see the latest value the next time they fire.
527
+ */
528
+ setClientData(clientData) {
529
+ this.defaultMetadata = clientData;
530
+ }
531
+ // -------------------------------------------------------------------------
532
+ // Multi-tab coordination passthrough
533
+ // -------------------------------------------------------------------------
534
+ isReadOnly(chatId) {
535
+ return this.coordinator?.isReadOnly(chatId) ?? false;
536
+ }
537
+ hasClaim(chatId) {
538
+ return this.coordinator?.hasClaim(chatId) ?? false;
539
+ }
540
+ addReadOnlyListener(fn) {
541
+ this.coordinator?.addListener(fn);
542
+ }
543
+ removeReadOnlyListener(fn) {
544
+ this.coordinator?.removeListener(fn);
545
+ }
546
+ broadcastMessages(chatId, messages) {
547
+ this.coordinator?.broadcastMessages(chatId, messages);
548
+ }
549
+ addMessagesListener(fn) {
550
+ this.coordinator?.addMessagesListener(fn);
551
+ }
552
+ removeMessagesListener(fn) {
553
+ this.coordinator?.removeMessagesListener(fn);
554
+ }
555
+ dispose() {
556
+ // Tear down any open session.out subscriptions before the coordinator
557
+ // goes away. Otherwise controllers in `activeStreams` keep reading
558
+ // until they time out, leaking network and memory on every
559
+ // unmount/navigation.
560
+ for (const controller of this.activeStreams.values()) {
561
+ controller.abort();
562
+ }
563
+ this.activeStreams.clear();
564
+ this.coordinator?.dispose();
565
+ this.coordinator = null;
566
+ }
567
+ // -------------------------------------------------------------------------
568
+ // Internal helpers
569
+ // -------------------------------------------------------------------------
570
+ serializeInputChunk(chunk) {
571
+ return JSON.stringify(chunk);
572
+ }
573
+ toPersisted = (state) => ({
574
+ publicAccessToken: state.publicAccessToken,
575
+ lastEventId: state.lastEventId,
576
+ isStreaming: state.isStreaming,
577
+ });
578
+ notifySessionChange(chatId, session) {
579
+ if (!this._onSessionChange)
580
+ return;
581
+ this._onSessionChange(chatId, session ? this.toPersisted(session) : null);
582
+ }
583
+ /**
584
+ * Resolves the session state for a chatId, starting the session if
585
+ * needed (and `getStartToken` is configured). Customers who provide
586
+ * `accessToken` but no `getStartToken` are expected to have created
587
+ * the session server-side; in that case the first `accessToken` call
588
+ * returns a fresh session PAT.
589
+ */
590
+ async ensureSessionState(chatId) {
591
+ const existing = this.sessions.get(chatId);
592
+ if (existing?.publicAccessToken)
593
+ return existing;
594
+ if (this.resolveStartSession) {
595
+ // Lazily start: customer's server action creates the session and
596
+ // returns a PAT. Idempotent on `(env, externalId)` so concurrent
597
+ // tabs / repeat calls converge to the same session.
598
+ const inflight = this.pendingStarts.get(chatId);
599
+ if (inflight)
600
+ return inflight;
601
+ const promise = this.doStart(chatId).finally(() => {
602
+ this.pendingStarts.delete(chatId);
603
+ });
604
+ this.pendingStarts.set(chatId, promise);
605
+ return promise;
606
+ }
607
+ // No `startSession` configured. Customer fully manages session
608
+ // lifecycle externally — they're expected to have hydrated
609
+ // `sessions: { ... }` already, or the very first `accessToken` call
610
+ // returns a PAT for an out-of-band-created session.
611
+ const token = await this.resolveAccessToken({ chatId });
612
+ const state = { publicAccessToken: token };
613
+ this.sessions.set(chatId, state);
614
+ this.notifySessionChange(chatId, state);
615
+ return state;
616
+ }
617
+ async doStart(chatId) {
618
+ if (!this.resolveStartSession) {
619
+ throw new Error("TriggerChatTransport: `startSession` is required to call `start()` / `preload()`. Either provide it or pre-hydrate the session via `sessions: { ... }`.");
620
+ }
621
+ const { publicAccessToken } = await this.resolveStartSession({
622
+ taskId: this.taskId,
623
+ chatId,
624
+ clientData: (this.defaultMetadata ?? {}),
625
+ });
626
+ const state = {
627
+ publicAccessToken,
628
+ isStreaming: false,
629
+ };
630
+ this.sessions.set(chatId, state);
631
+ this.notifySessionChange(chatId, state);
632
+ return state;
633
+ }
634
+ /**
635
+ * Run `op` with the session's stored PAT. On 401/403, refresh the PAT
636
+ * via `accessToken` and retry once. Surfaces non-auth errors as-is.
637
+ */
638
+ resolveBaseURL(ctx) {
639
+ const raw = this.resolveBaseURLFn(ctx);
640
+ return raw.replace(/\/$/, "");
641
+ }
642
+ async doFetch(ctx, url, init) {
643
+ return this.fetchOverride ? this.fetchOverride(url, init, ctx) : fetch(url, init);
644
+ }
645
+ async appendInputChunk(chatId, token, body) {
646
+ const ctx = { endpoint: "in", chatId };
647
+ const url = `${this.resolveBaseURL(ctx)}/realtime/v1/sessions/${encodeURIComponent(chatId)}/in/append`;
648
+ const headers = {
649
+ "Content-Type": "application/json",
650
+ Authorization: `Bearer ${token}`,
651
+ "x-trigger-source": "sdk",
652
+ ...this.extraHeaders,
653
+ };
654
+ const response = await this.doFetch(ctx, url, { method: "POST", headers, body });
655
+ if (!response.ok) {
656
+ const text = await response.text().catch(() => "");
657
+ const err = new Error(`appendToSessionStream failed: ${response.status} ${text}`);
658
+ err.name = "TriggerApiError";
659
+ err.status = response.status;
660
+ throw err;
661
+ }
662
+ }
663
+ async callWithAuthRetry(chatId, state, op) {
664
+ try {
665
+ await op(state.publicAccessToken);
666
+ return;
667
+ }
668
+ catch (err) {
669
+ if (!isAuthError(err))
670
+ throw err;
671
+ }
672
+ const fresh = await this.resolveAccessToken({ chatId });
673
+ state.publicAccessToken = fresh;
674
+ this.notifySessionChange(chatId, state);
675
+ await op(fresh);
676
+ }
677
+ /**
678
+ * Open an SSE subscription to the session's `.out` stream and pipe
679
+ * UIMessageChunks through to the AI SDK. Trigger control records
680
+ * (`turn-complete`, `upgrade-required` — see `trigger-control` header
681
+ * on `client-protocol.mdx#records-on-session-out`) are routed by
682
+ * header and never reach the consumer. `upgrade-required` is purely
683
+ * telemetry now since the server handles the run swap inline (see
684
+ * `end-and-continue`).
685
+ */
686
+ subscribeToSessionStream(state, abortSignal, chatId, options) {
687
+ const internalAbort = new AbortController();
688
+ this.activeStreams.set(chatId, internalAbort);
689
+ const combinedSignal = abortSignal
690
+ ? AbortSignal.any([abortSignal, internalAbort.signal])
691
+ : internalAbort.signal;
692
+ if (abortSignal) {
693
+ abortSignal.addEventListener("abort", () => {
694
+ if (options?.sendStopOnAbort !== false) {
695
+ state.skipToTurnComplete = true;
696
+ this.appendInputChunk(chatId, state.publicAccessToken, this.serializeInputChunk({ kind: "stop" })).catch(() => { });
697
+ }
698
+ internalAbort.abort();
699
+ }, { once: true });
700
+ }
701
+ const streamUrl = `${this.resolveBaseURL({ endpoint: "out", chatId })}/realtime/v1/sessions/${encodeURIComponent(chatId)}/out`;
702
+ return new ReadableStream({
703
+ start: async (controller) => {
704
+ // Track the live subscription so browser wake events can act
705
+ // on it. Three classes of wake:
706
+ // - `online`: network came back. Existing connection might
707
+ // be silently dead; force a fresh one.
708
+ // - `visibilitychange` → visible after long hidden: tab
709
+ // was backgrounded long enough that the OS likely killed
710
+ // the TCP socket. Force reconnect.
711
+ // - `visibilitychange` → visible after short hidden: cheap
712
+ // wake of any in-flight backoff.
713
+ // - `pageshow` with `event.persisted`: bfcache restore
714
+ // (mobile Safari back/forward, app-switcher resume). The
715
+ // socket is definitely dead. Force reconnect.
716
+ let currentSubscription = null;
717
+ let hiddenSince = null;
718
+ const FORCE_RECONNECT_AFTER_HIDDEN_MS = 30_000;
719
+ const onVisibilityChange = () => {
720
+ if (typeof document === "undefined")
721
+ return;
722
+ if (document.visibilityState === "hidden") {
723
+ hiddenSince = Date.now();
724
+ return;
725
+ }
726
+ const wasHiddenForMs = hiddenSince ? Date.now() - hiddenSince : 0;
727
+ hiddenSince = null;
728
+ if (wasHiddenForMs >= FORCE_RECONNECT_AFTER_HIDDEN_MS) {
729
+ currentSubscription?.forceReconnect();
730
+ }
731
+ else {
732
+ currentSubscription?.retryNow();
733
+ }
734
+ };
735
+ const onPageShow = (event) => {
736
+ // PageTransitionEvent in browsers; type guard via `persisted`.
737
+ if (event.persisted) {
738
+ currentSubscription?.forceReconnect();
739
+ }
740
+ };
741
+ const onOnline = () => currentSubscription?.forceReconnect();
742
+ const teardownWakeListeners = typeof document !== "undefined" && typeof window !== "undefined"
743
+ ? (() => {
744
+ document.addEventListener("visibilitychange", onVisibilityChange);
745
+ window.addEventListener("online", onOnline);
746
+ window.addEventListener("pageshow", onPageShow);
747
+ return () => {
748
+ document.removeEventListener("visibilitychange", onVisibilityChange);
749
+ window.removeEventListener("online", onOnline);
750
+ window.removeEventListener("pageshow", onPageShow);
751
+ };
752
+ })()
753
+ : () => { };
754
+ const sseCtx = { endpoint: "out", chatId };
755
+ const fetchOverride = this.fetchOverride;
756
+ const sseFetchClient = fetchOverride
757
+ ? ((input, init) => {
758
+ if (typeof input === "string") {
759
+ return fetchOverride(input, init ?? {}, sseCtx);
760
+ }
761
+ if (input instanceof URL) {
762
+ return fetchOverride(input.toString(), init ?? {}, sseCtx);
763
+ }
764
+ // Request — preserve its url + intrinsic init, let any
765
+ // provided init override on top (matches fetch(Request, init)
766
+ // semantics).
767
+ return fetchOverride(input.url, {
768
+ method: input.method,
769
+ headers: input.headers,
770
+ signal: input.signal,
771
+ ...(init ?? {}),
772
+ }, sseCtx);
773
+ })
774
+ : undefined;
775
+ const connectSseOnce = async (token) => {
776
+ const subscription = new SSEStreamSubscription(streamUrl, {
777
+ headers: {
778
+ Authorization: `Bearer ${token}`,
779
+ ...this.extraHeaders,
780
+ ...(options?.peekSettled ? { "X-Peek-Settled": "1" } : {}),
781
+ },
782
+ signal: combinedSignal,
783
+ timeoutInSeconds: this.streamTimeoutSeconds,
784
+ lastEventId: state.lastEventId,
785
+ // Catch silent-dead-socket: if no chunk (or server
786
+ // keepalive) arrives in 60s, force reconnect. Sized
787
+ // generously over typical agent thinking pauses.
788
+ stallTimeoutMs: 60_000,
789
+ fetchClient: sseFetchClient,
790
+ });
791
+ currentSubscription = subscription;
792
+ const sseStream = await subscription.subscribe();
793
+ const reader = sseStream.getReader();
794
+ try {
795
+ const first = await reader.read();
796
+ if (first.done) {
797
+ reader.releaseLock();
798
+ return null;
799
+ }
800
+ return { reader, primed: first.value };
801
+ }
802
+ catch (readErr) {
803
+ reader.releaseLock();
804
+ throw readErr;
805
+ }
806
+ };
807
+ try {
808
+ let reader;
809
+ let primed;
810
+ try {
811
+ const opened = await connectSseOnce(state.publicAccessToken);
812
+ if (opened === null) {
813
+ controller.close();
814
+ return;
815
+ }
816
+ reader = opened.reader;
817
+ primed = opened.primed;
818
+ }
819
+ catch (e) {
820
+ if (isAuthError(e)) {
821
+ const fresh = await this.resolveAccessToken({ chatId });
822
+ state.publicAccessToken = fresh;
823
+ this.notifySessionChange(chatId, state);
824
+ const opened = await connectSseOnce(fresh);
825
+ if (opened === null) {
826
+ controller.close();
827
+ return;
828
+ }
829
+ reader = opened.reader;
830
+ primed = opened.primed;
831
+ }
832
+ else {
833
+ throw e;
834
+ }
835
+ }
836
+ while (true) {
837
+ let value;
838
+ if (primed !== undefined) {
839
+ value = primed;
840
+ primed = undefined;
841
+ }
842
+ else {
843
+ const next = await reader.read();
844
+ if (next.done) {
845
+ controller.close();
846
+ return;
847
+ }
848
+ value = next.value;
849
+ }
850
+ if (combinedSignal.aborted) {
851
+ internalAbort.abort();
852
+ await reader.cancel();
853
+ controller.close();
854
+ return;
855
+ }
856
+ if (value.id)
857
+ state.lastEventId = value.id;
858
+ // Trigger control record (turn-complete, upgrade-required) —
859
+ // routed by header, body is empty. Detect via the
860
+ // `trigger-control` header on the SSE record. Data records
861
+ // (UIMessageChunks) fall through to the chunk path below.
862
+ //
863
+ // Cross-version bridge: a customer who redeploys their
864
+ // Next.js app (new browser SDK) before their next
865
+ // `trigger deploy` (old agent SDK still writing turn-complete
866
+ // / upgrade-required as `chunk.type` data records) would
867
+ // otherwise hang. Fall back to the legacy chunk-type form
868
+ // when no header is present so the deploy-skew window
869
+ // closes turns correctly.
870
+ let controlValue = controlSubtype(value.headers);
871
+ let legacyChunk;
872
+ if (!controlValue && value.chunk && typeof value.chunk === "object") {
873
+ const chunk = value.chunk;
874
+ if (chunk.type === "trigger:turn-complete") {
875
+ controlValue = TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE;
876
+ legacyChunk = chunk;
877
+ }
878
+ else if (chunk.type === "trigger:upgrade-required") {
879
+ controlValue = TRIGGER_CONTROL_SUBTYPE.UPGRADE_REQUIRED;
880
+ }
881
+ else if (typeof chunk.type === "string" && chunk.type.startsWith("trigger:")) {
882
+ // Future / unknown `trigger:*` legacy control type from
883
+ // a pre-upgrade agent — drop so it doesn't reach the AI
884
+ // SDK as an unrecognised UIMessageChunk.
885
+ continue;
886
+ }
887
+ }
888
+ if (state.skipToTurnComplete) {
889
+ if (controlValue === TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE) {
890
+ state.skipToTurnComplete = false;
891
+ }
892
+ continue;
893
+ }
894
+ if (controlValue === TRIGGER_CONTROL_SUBTYPE.UPGRADE_REQUIRED) {
895
+ // Server has already triggered the new run via
896
+ // `end-and-continue`; the next chunks on this same `.out`
897
+ // stream come from v2. Filter the marker for cleanliness
898
+ // and keep reading.
899
+ continue;
900
+ }
901
+ if (controlValue === TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE) {
902
+ const refreshedToken = headerValue(value.headers, PUBLIC_ACCESS_TOKEN_HEADER) ??
903
+ legacyChunk?.publicAccessToken;
904
+ if (refreshedToken) {
905
+ state.publicAccessToken = refreshedToken;
906
+ }
907
+ state.isStreaming = false;
908
+ this.notifySessionChange(chatId, state);
909
+ this.coordinator?.release(chatId);
910
+ this.coordinator?.broadcastSession(chatId, {
911
+ lastEventId: state.lastEventId,
912
+ });
913
+ if (this.watchMode)
914
+ continue;
915
+ internalAbort.abort();
916
+ try {
917
+ controller.close();
918
+ }
919
+ catch {
920
+ /* already closed */
921
+ }
922
+ return;
923
+ }
924
+ // Data record — `value.chunk` is the parsed UIMessageChunk
925
+ // unwrapped from the S2 record envelope (the parser does the
926
+ // JSON unwrap). Drop empty/malformed payloads defensively.
927
+ if (value.chunk == null)
928
+ continue;
929
+ controller.enqueue(value.chunk);
930
+ }
931
+ }
932
+ catch (error) {
933
+ if (error instanceof Error && error.name === "AbortError") {
934
+ try {
935
+ controller.close();
936
+ }
937
+ catch {
938
+ /* already closed */
939
+ }
940
+ return;
941
+ }
942
+ controller.error(error);
943
+ }
944
+ finally {
945
+ teardownWakeListeners();
946
+ this.activeStreams.delete(chatId);
947
+ this.coordinator?.release(chatId);
948
+ }
949
+ },
950
+ });
951
+ }
952
+ }
953
+ /**
954
+ * Convenience constructor matching {@link TriggerChatTransport}.
955
+ */
956
+ export function createChatTransport(options) {
957
+ return new TriggerChatTransport(options);
958
+ }
959
+ // Server-side agent chat re-exports.
960
+ export { AgentChat, ChatStream, } from "./chat-client.js";
961
+ //# sourceMappingURL=chat.js.map