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

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