@trigger.dev/sdk 0.0.0-prerelease-20260909071038 → 0.0.0-prerelease-streamfix-20260909094302
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.
- package/dist/commonjs/imports/ai-runtime-cjs.cjs.map +1 -1
- package/dist/commonjs/imports/ai-runtime.js +0 -2
- package/dist/commonjs/v3/ai-shared.d.ts +0 -15
- package/dist/commonjs/v3/ai-shared.js +1 -35
- package/dist/commonjs/v3/ai-shared.js.map +1 -1
- package/dist/commonjs/v3/ai.d.ts +25 -275
- package/dist/commonjs/v3/ai.js +308 -1283
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-client.d.ts +2 -11
- package/dist/commonjs/v3/chat-client.js +111 -227
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.d.ts +0 -88
- package/dist/commonjs/v3/chat-react.js +1 -130
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +6 -42
- package/dist/commonjs/v3/chat-server.js +7 -52
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +10 -92
- package/dist/commonjs/v3/chat.js +73 -328
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +2 -15
- package/dist/commonjs/v3/sessions.js +1 -12
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/shared.js +36 -30
- package/dist/commonjs/v3/shared.js.map +1 -1
- package/dist/commonjs/v3/test/index.d.ts +0 -1
- package/dist/commonjs/v3/test/index.js +1 -3
- package/dist/commonjs/v3/test/index.js.map +1 -1
- package/dist/commonjs/v3/test/mock-chat-agent.d.ts +4 -47
- package/dist/commonjs/v3/test/mock-chat-agent.js +4 -115
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/v3/test/test-session-handle.js +0 -6
- package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/imports/ai-runtime.d.ts +2 -2
- package/dist/esm/imports/ai-runtime.js +2 -2
- package/dist/esm/imports/ai-runtime.js.map +1 -1
- package/dist/esm/v3/ai-shared.d.ts +0 -15
- package/dist/esm/v3/ai-shared.js +0 -33
- package/dist/esm/v3/ai-shared.js.map +1 -1
- package/dist/esm/v3/ai.d.ts +25 -275
- package/dist/esm/v3/ai.js +305 -1275
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-client.d.ts +2 -11
- package/dist/esm/v3/chat-client.js +112 -228
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.d.ts +0 -88
- package/dist/esm/v3/chat-react.js +1 -127
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +6 -42
- package/dist/esm/v3/chat-server.js +8 -53
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +10 -92
- package/dist/esm/v3/chat.js +74 -329
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +2 -15
- package/dist/esm/v3/sessions.js +1 -11
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/shared.js +23 -17
- package/dist/esm/v3/shared.js.map +1 -1
- package/dist/esm/v3/test/index.d.ts +0 -1
- package/dist/esm/v3/test/index.js +0 -1
- package/dist/esm/v3/test/index.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.d.ts +4 -47
- package/dist/esm/v3/test/mock-chat-agent.js +6 -117
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/v3/test/test-session-handle.js +0 -6
- package/dist/esm/v3/test/test-session-handle.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/actions.mdx +23 -55
- package/docs/ai-chat/anatomy.mdx +3 -3
- package/docs/ai-chat/backend.mdx +48 -125
- package/docs/ai-chat/background-injection.mdx +19 -69
- package/docs/ai-chat/client-protocol.mdx +4 -5
- package/docs/ai-chat/compaction.mdx +7 -13
- package/docs/ai-chat/custom-agents.mdx +0 -23
- package/docs/ai-chat/fast-starts.mdx +20 -27
- package/docs/ai-chat/frontend.mdx +14 -19
- package/docs/ai-chat/lifecycle-hooks.mdx +2 -6
- package/docs/ai-chat/migrating-from-a-route-handler.mdx +14 -16
- package/docs/ai-chat/patterns/database-persistence.mdx +2 -6
- package/docs/ai-chat/patterns/persistence-and-replay.mdx +23 -34
- package/docs/ai-chat/patterns/skills.mdx +10 -7
- package/docs/ai-chat/patterns/version-upgrades.mdx +6 -79
- package/docs/ai-chat/pending-messages.mdx +3 -3
- package/docs/ai-chat/prompt-caching.mdx +25 -23
- package/docs/ai-chat/quick-start.mdx +11 -11
- package/docs/ai-chat/reference.mdx +9 -67
- package/docs/ai-chat/sessions.mdx +1 -6
- package/docs/ai-chat/testing.mdx +1 -2
- package/docs/ai-chat/tools.mdx +13 -18
- package/docs/ai-chat/upgrade-guide.mdx +2 -2
- package/docs/apikeys.mdx +45 -27
- package/docs/config/extensions/syncEnvVars.mdx +0 -6
- package/docs/deploy-environment-variables.mdx +2 -23
- package/docs/deployment/overview.mdx +8 -4
- package/docs/deployment/preview-branches.mdx +4 -4
- package/docs/deployment/version-skew-protection.mdx +0 -62
- package/docs/manual-setup.mdx +7 -7
- package/docs/mcp-tools.mdx +0 -9
- package/docs/quick-start.mdx +3 -3
- package/docs/realtime/auth.mdx +1 -1
- package/docs/self-hosting/security.mdx +0 -5
- package/docs/tasks/scheduled.mdx +0 -24
- package/docs/triggering.mdx +1 -1
- package/package.json +4 -4
- package/skills/trigger-authoring-chat-agent/SKILL.md +29 -41
- package/skills/trigger-chat-agent-advanced/SKILL.md +12 -31
- package/dist/commonjs/v3/chatSnapshotIo.d.ts +0 -2
- package/dist/commonjs/v3/chatSnapshotIo.js +0 -174
- package/dist/commonjs/v3/chatSnapshotIo.js.map +0 -1
- package/dist/commonjs/v3/chatVersionSkew.d.ts +0 -12
- package/dist/commonjs/v3/chatVersionSkew.js +0 -30
- package/dist/commonjs/v3/chatVersionSkew.js.map +0 -1
- package/dist/commonjs/v3/externalDeploymentId.d.ts +0 -23
- package/dist/commonjs/v3/externalDeploymentId.js +0 -43
- package/dist/commonjs/v3/externalDeploymentId.js.map +0 -1
- package/dist/commonjs/v3/test/transcript-storage-tests.d.ts +0 -40
- package/dist/commonjs/v3/test/transcript-storage-tests.js +0 -258
- package/dist/commonjs/v3/test/transcript-storage-tests.js.map +0 -1
- package/dist/commonjs/v3/transcriptStorage.d.ts +0 -229
- package/dist/commonjs/v3/transcriptStorage.js +0 -321
- package/dist/commonjs/v3/transcriptStorage.js.map +0 -1
- package/dist/esm/v3/chatSnapshotIo.d.ts +0 -2
- package/dist/esm/v3/chatSnapshotIo.js +0 -166
- package/dist/esm/v3/chatSnapshotIo.js.map +0 -1
- package/dist/esm/v3/chatVersionSkew.d.ts +0 -12
- package/dist/esm/v3/chatVersionSkew.js +0 -27
- package/dist/esm/v3/chatVersionSkew.js.map +0 -1
- package/dist/esm/v3/externalDeploymentId.d.ts +0 -23
- package/dist/esm/v3/externalDeploymentId.js +0 -38
- package/dist/esm/v3/externalDeploymentId.js.map +0 -1
- package/dist/esm/v3/test/transcript-storage-tests.d.ts +0 -40
- package/dist/esm/v3/test/transcript-storage-tests.js +0 -255
- package/dist/esm/v3/test/transcript-storage-tests.js.map +0 -1
- package/dist/esm/v3/transcriptStorage.d.ts +0 -229
- package/dist/esm/v3/transcriptStorage.js +0 -309
- package/dist/esm/v3/transcriptStorage.js.map +0 -1
- package/docs/ai-chat/patterns/native-compaction.mdx +0 -310
- package/docs/ai-chat/transcript-storage.mdx +0 -233
- package/docs/reports.mdx +0 -157
- package/docs/troubleshooting-zod.mdx +0 -158
package/dist/commonjs/v3/chat.js
CHANGED
|
@@ -44,32 +44,6 @@ function isAuthError(error) {
|
|
|
44
44
|
const e = error;
|
|
45
45
|
return e.name === "TriggerApiError" && (e.status === 401 || e.status === 403);
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Detect the closed-session refusal from `.in/append`: HTTP 409 with the
|
|
49
|
-
* stable `session_closed` code. Terminal — never retried.
|
|
50
|
-
*/
|
|
51
|
-
function isSessionClosedError(error) {
|
|
52
|
-
if (error === null || typeof error !== "object")
|
|
53
|
-
return false;
|
|
54
|
-
const e = error;
|
|
55
|
-
return e.name === "TriggerApiError" && e.status === 409 && e.code === "session_closed";
|
|
56
|
-
}
|
|
57
|
-
function sessionClosedReasonOf(error) {
|
|
58
|
-
if (error === null || typeof error !== "object")
|
|
59
|
-
return undefined;
|
|
60
|
-
const reason = error.closedReason;
|
|
61
|
-
return typeof reason === "string" ? reason : undefined;
|
|
62
|
-
}
|
|
63
|
-
/** The local form of the refusal, for sends the transport rejects itself. */
|
|
64
|
-
function sessionClosedError(chatId, reason) {
|
|
65
|
-
const err = new Error(reason ? `Chat session ${chatId} is closed: ${reason}` : `Chat session ${chatId} is closed`);
|
|
66
|
-
err.name = "TriggerApiError";
|
|
67
|
-
err.status = 409;
|
|
68
|
-
err.code = "session_closed";
|
|
69
|
-
if (reason)
|
|
70
|
-
err.closedReason = reason;
|
|
71
|
-
return err;
|
|
72
|
-
}
|
|
73
47
|
/**
|
|
74
48
|
* Detect a 404 from a session-PAT-authed call — i.e. the session doesn't
|
|
75
49
|
* exist in the current environment. Happens when hydrated session state
|
|
@@ -175,7 +149,6 @@ class TriggerChatTransport {
|
|
|
175
149
|
_onSessionChange;
|
|
176
150
|
_onEvent;
|
|
177
151
|
sessions = new Map();
|
|
178
|
-
pendingResumeCursors = new Map();
|
|
179
152
|
activeStreams = new Map();
|
|
180
153
|
pendingStarts = new Map();
|
|
181
154
|
// Last turn-producing send per chat — attribution source for the
|
|
@@ -215,8 +188,6 @@ class TriggerChatTransport {
|
|
|
215
188
|
lastEventId: session.lastEventId,
|
|
216
189
|
activeInputSeq: session.activeInputSeq,
|
|
217
190
|
isStreaming: session.isStreaming,
|
|
218
|
-
closed: session.closed,
|
|
219
|
-
closedReason: session.closedReason,
|
|
220
191
|
});
|
|
221
192
|
}
|
|
222
193
|
}
|
|
@@ -266,10 +237,6 @@ class TriggerChatTransport {
|
|
|
266
237
|
*/
|
|
267
238
|
sendMessages = async (options) => {
|
|
268
239
|
const { trigger, chatId, messageId, messages, abortSignal, body, metadata } = options;
|
|
269
|
-
const closedState = this.sessions.get(chatId);
|
|
270
|
-
if (closedState?.closed) {
|
|
271
|
-
throw sessionClosedError(chatId, closedState.closedReason);
|
|
272
|
-
}
|
|
273
240
|
if (this.coordinator) {
|
|
274
241
|
if (this.coordinator.isReadOnly(chatId)) {
|
|
275
242
|
throw new Error("This chat is active in another tab");
|
|
@@ -279,14 +246,6 @@ class TriggerChatTransport {
|
|
|
279
246
|
const mergedMetadata = this.defaultMetadata || metadata
|
|
280
247
|
? { ...(this.defaultMetadata ?? {}), ...(metadata ?? {}) }
|
|
281
248
|
: undefined;
|
|
282
|
-
// An action sent through `useChat`. `useChatActions` (or any caller) puts
|
|
283
|
-
// it in `body.action`; sending it here rather than through
|
|
284
|
-
// `transport.sendAction` means `useChat` owns the response stream, so an
|
|
285
|
-
// action that becomes a turn renders the way a message turn does.
|
|
286
|
-
const actionInBody = body?.action;
|
|
287
|
-
if (actionInBody !== undefined) {
|
|
288
|
-
return this.sendAction(chatId, actionInBody, { abortSignal, metadata: mergedMetadata });
|
|
289
|
-
}
|
|
290
249
|
// First-turn handover routing — when `headStart` is set AND no
|
|
291
250
|
// session state exists yet for this chatId, POST the wire payload
|
|
292
251
|
// to the customer's `chat.handover` route handler. The handler
|
|
@@ -349,9 +308,11 @@ class TriggerChatTransport {
|
|
|
349
308
|
activeStream.abort();
|
|
350
309
|
this.activeStreams.delete(chatId);
|
|
351
310
|
}
|
|
311
|
+
// A stop that never saw its TURN_COMPLETE leaves the flag set, and the new
|
|
312
|
+
// turn would be skipped record by record.
|
|
313
|
+
state.skipToTurnComplete = false;
|
|
352
314
|
state.activeInputSeq = inSeq;
|
|
353
315
|
state.isStreaming = true;
|
|
354
|
-
state.outstandingTurnAbandoned = false;
|
|
355
316
|
this.notifySessionChange(chatId, state);
|
|
356
317
|
// Owning turn: aborting this live send stops the turn the user drives.
|
|
357
318
|
return this.subscribeToSessionStream(state, abortSignal, chatId, {
|
|
@@ -423,16 +384,6 @@ class TriggerChatTransport {
|
|
|
423
384
|
};
|
|
424
385
|
this.sessions.set(chatId, state);
|
|
425
386
|
this.notifySessionChange(chatId, state);
|
|
426
|
-
// Step 1 streams from the warm server either way; this says the agent run that owes step 2
|
|
427
|
-
// is parked on an undeployed external deployment id.
|
|
428
|
-
if (response.headers.get("X-Trigger-Chat-Pending-Version") === "1") {
|
|
429
|
-
this.emitEvent({
|
|
430
|
-
type: "run-pending-version",
|
|
431
|
-
chatId,
|
|
432
|
-
timestamp: Date.now(),
|
|
433
|
-
source: "head-start",
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
387
|
// Filter the parsed UIMessage stream:
|
|
437
388
|
// - Drop control chunks (`trigger:turn-complete`,
|
|
438
389
|
// `trigger:session-state`) before they reach AI SDK — they
|
|
@@ -599,9 +550,6 @@ class TriggerChatTransport {
|
|
|
599
550
|
const state = this.sessions.get(options.chatId);
|
|
600
551
|
if (!state)
|
|
601
552
|
return null;
|
|
602
|
-
// A closed session has no further turns to resume.
|
|
603
|
-
if (state.closed)
|
|
604
|
-
return null;
|
|
605
553
|
// Watch is a standing subscription: a settled session is exactly the
|
|
606
554
|
// state it waits in, so a completed last turn must not block the resume.
|
|
607
555
|
if (state.isStreaming === false && !this.watchMode)
|
|
@@ -646,13 +594,7 @@ class TriggerChatTransport {
|
|
|
646
594
|
catch {
|
|
647
595
|
return false;
|
|
648
596
|
}
|
|
649
|
-
|
|
650
|
-
// nothing to supersede, and gating it would swallow the next turn.
|
|
651
|
-
if (!state.outstandingTurnAbandoned &&
|
|
652
|
-
(state.isStreaming || state.activeInputSeq !== undefined)) {
|
|
653
|
-
state.skipToTurnComplete = true;
|
|
654
|
-
state.supersededInputSeq = state.activeInputSeq;
|
|
655
|
-
}
|
|
597
|
+
state.skipToTurnComplete = true;
|
|
656
598
|
const activeStream = this.activeStreams.get(chatId);
|
|
657
599
|
if (activeStream) {
|
|
658
600
|
activeStream.abort();
|
|
@@ -671,28 +613,15 @@ class TriggerChatTransport {
|
|
|
671
613
|
this.notifySessionChange(chatId, state);
|
|
672
614
|
return true;
|
|
673
615
|
};
|
|
674
|
-
/**
|
|
675
|
-
* Clear the supersede gate armed by a stop. Call this when the stopped
|
|
676
|
-
* turn died without ever writing its `trigger:turn-complete` boundary,
|
|
677
|
-
* otherwise the next turn stays gated forever.
|
|
678
|
-
*/
|
|
679
|
-
clearSupersedeGate = (chatId) => {
|
|
680
|
-
const state = this.sessions.get(chatId);
|
|
681
|
-
if (!state)
|
|
682
|
-
return;
|
|
683
|
-
state.skipToTurnComplete = false;
|
|
684
|
-
state.supersededInputSeq = undefined;
|
|
685
|
-
state.outstandingTurnAbandoned = true;
|
|
686
|
-
};
|
|
687
616
|
/**
|
|
688
617
|
* Send a custom action chunk (for `chat.agent`'s `actionSchema` /
|
|
689
|
-
* `onAction` hook).
|
|
690
|
-
* `onAction` fire on the agent side
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
618
|
+
* `onAction` hook). Actions are not turns — only `hydrateMessages`
|
|
619
|
+
* and `onAction` fire on the agent side. The returned stream
|
|
620
|
+
* carries any model response `onAction` produced (when it returns a
|
|
621
|
+
* `StreamTextResult`); for `void`-returning side-effect-only actions
|
|
622
|
+
* the stream completes immediately with `trigger:turn-complete`.
|
|
694
623
|
*/
|
|
695
|
-
sendAction = async (chatId, action
|
|
624
|
+
sendAction = async (chatId, action) => {
|
|
696
625
|
if (this.coordinator) {
|
|
697
626
|
if (this.coordinator.isReadOnly(chatId)) {
|
|
698
627
|
throw new Error("This chat is active in another tab");
|
|
@@ -704,9 +633,7 @@ class TriggerChatTransport {
|
|
|
704
633
|
chatId,
|
|
705
634
|
trigger: "action",
|
|
706
635
|
action,
|
|
707
|
-
metadata: this.defaultMetadata
|
|
708
|
-
? { ...(this.defaultMetadata ?? {}), ...(options?.metadata ?? {}) }
|
|
709
|
-
: undefined,
|
|
636
|
+
metadata: this.defaultMetadata ?? undefined,
|
|
710
637
|
};
|
|
711
638
|
const body = this.serializeInputChunk({ kind: "message", payload: wirePayload });
|
|
712
639
|
const partId = crypto.randomUUID();
|
|
@@ -721,14 +648,16 @@ class TriggerChatTransport {
|
|
|
721
648
|
activeStream.abort();
|
|
722
649
|
this.activeStreams.delete(chatId);
|
|
723
650
|
}
|
|
651
|
+
// A stop that never saw its TURN_COMPLETE leaves the flag set, and the new
|
|
652
|
+
// turn would be skipped record by record.
|
|
653
|
+
state.skipToTurnComplete = false;
|
|
724
654
|
// Mark streaming + persist so a reload mid-action resumes (reconnectToStream
|
|
725
655
|
// no-ops when the persisted session says isStreaming: false).
|
|
726
656
|
state.activeInputSeq = inSeq;
|
|
727
657
|
state.isStreaming = true;
|
|
728
|
-
state.outstandingTurnAbandoned = false;
|
|
729
658
|
this.notifySessionChange(chatId, state);
|
|
730
659
|
// Owning action: aborting this send stops the turn the user drives.
|
|
731
|
-
return this.subscribeToSessionStream(state,
|
|
660
|
+
return this.subscribeToSessionStream(state, undefined, chatId, {
|
|
732
661
|
sinceInSeq: inSeq,
|
|
733
662
|
sendStopOnAbort: true,
|
|
734
663
|
});
|
|
@@ -743,42 +672,14 @@ class TriggerChatTransport {
|
|
|
743
672
|
return this.toPersisted(state);
|
|
744
673
|
};
|
|
745
674
|
setSession(chatId, session) {
|
|
746
|
-
this.sessions.set(chatId,
|
|
675
|
+
this.sessions.set(chatId, {
|
|
747
676
|
publicAccessToken: session.publicAccessToken,
|
|
748
677
|
lastEventId: session.lastEventId,
|
|
749
678
|
activeInputSeq: session.activeInputSeq,
|
|
750
679
|
isStreaming: session.isStreaming,
|
|
751
|
-
})
|
|
680
|
+
});
|
|
752
681
|
this.notifySessionChange(chatId, this.toPersisted(this.sessions.get(chatId)));
|
|
753
682
|
}
|
|
754
|
-
/**
|
|
755
|
-
* Seed the `.out` resume cursor from a loaded transcript. Applied to the
|
|
756
|
-
* session now if it exists, otherwise held until the session is created so
|
|
757
|
-
* the first live subscription opens past the persisted history instead of
|
|
758
|
-
* replaying it. Never moves an existing cursor backward: the transcript load
|
|
759
|
-
* is async, so a live `.out` record can already have advanced the session
|
|
760
|
-
* past the snapshot, and overwriting it would replay those records.
|
|
761
|
-
*/
|
|
762
|
-
seedResumeCursor = (chatId, lastEventId) => {
|
|
763
|
-
const existing = this.sessions.get(chatId);
|
|
764
|
-
if (existing?.publicAccessToken) {
|
|
765
|
-
if (existing.lastEventId === undefined) {
|
|
766
|
-
existing.lastEventId = lastEventId;
|
|
767
|
-
this.notifySessionChange(chatId, this.toPersisted(existing));
|
|
768
|
-
}
|
|
769
|
-
this.pendingResumeCursors.delete(chatId);
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
this.pendingResumeCursors.set(chatId, lastEventId);
|
|
773
|
-
};
|
|
774
|
-
applyPendingResumeCursor(chatId, state) {
|
|
775
|
-
const pending = this.pendingResumeCursors.get(chatId);
|
|
776
|
-
if (pending !== undefined && state.lastEventId === undefined) {
|
|
777
|
-
state.lastEventId = pending;
|
|
778
|
-
}
|
|
779
|
-
this.pendingResumeCursors.delete(chatId);
|
|
780
|
-
return state;
|
|
781
|
-
}
|
|
782
683
|
setOnSessionChange(callback) {
|
|
783
684
|
this._onSessionChange = callback;
|
|
784
685
|
}
|
|
@@ -849,22 +750,6 @@ class TriggerChatTransport {
|
|
|
849
750
|
setClientData(clientData) {
|
|
850
751
|
this.defaultMetadata = clientData;
|
|
851
752
|
}
|
|
852
|
-
/**
|
|
853
|
-
* Update the request-time callbacks. Same reason as `setClientData`:
|
|
854
|
-
* a host that rebuilds these per render (a dashboard navigating between
|
|
855
|
-
* projects) would otherwise keep hitting the endpoint captured at
|
|
856
|
-
* construction. Every read goes through the live field, so the next
|
|
857
|
-
* request uses the latest callback.
|
|
858
|
-
*/
|
|
859
|
-
setAccessToken(accessToken) {
|
|
860
|
-
this.resolveAccessToken = accessToken;
|
|
861
|
-
}
|
|
862
|
-
setStartSession(startSession) {
|
|
863
|
-
this.resolveStartSession = startSession;
|
|
864
|
-
}
|
|
865
|
-
setFetch(fetchOverride) {
|
|
866
|
-
this.fetchOverride = fetchOverride;
|
|
867
|
-
}
|
|
868
753
|
// -------------------------------------------------------------------------
|
|
869
754
|
// Multi-tab coordination passthrough
|
|
870
755
|
// -------------------------------------------------------------------------
|
|
@@ -912,42 +797,7 @@ class TriggerChatTransport {
|
|
|
912
797
|
lastEventId: state.lastEventId,
|
|
913
798
|
activeInputSeq: state.activeInputSeq,
|
|
914
799
|
isStreaming: state.isStreaming,
|
|
915
|
-
closed: state.closed,
|
|
916
|
-
closedReason: state.closedReason,
|
|
917
800
|
});
|
|
918
|
-
/**
|
|
919
|
-
* Flip a session to closed and tell listeners once. Terminal: sends and
|
|
920
|
-
* reconnects stop from here on, and there is no reopening.
|
|
921
|
-
*/
|
|
922
|
-
markSessionClosed(chatId, state, reason, source) {
|
|
923
|
-
if (state.closed)
|
|
924
|
-
return;
|
|
925
|
-
state.closed = true;
|
|
926
|
-
if (reason)
|
|
927
|
-
state.closedReason = reason;
|
|
928
|
-
state.isStreaming = false;
|
|
929
|
-
state.activeInputSeq = undefined;
|
|
930
|
-
state.skipToTurnComplete = false;
|
|
931
|
-
state.supersededInputSeq = undefined;
|
|
932
|
-
this.emitEvent({
|
|
933
|
-
type: "session-closed",
|
|
934
|
-
chatId,
|
|
935
|
-
timestamp: Date.now(),
|
|
936
|
-
...(reason ? { reason } : {}),
|
|
937
|
-
source,
|
|
938
|
-
});
|
|
939
|
-
this.notifySessionChange(chatId, state);
|
|
940
|
-
this.coordinator?.release(chatId);
|
|
941
|
-
}
|
|
942
|
-
/**
|
|
943
|
-
* Whether the conversation has been closed — by the agent via
|
|
944
|
-
* `chat.close()`, or from outside via `sessions.close()`. Closed is
|
|
945
|
-
* one-way: the transport refuses further sends and reconnects, and the
|
|
946
|
-
* transcript stays readable.
|
|
947
|
-
*/
|
|
948
|
-
sessionStatus = (chatId) => this.sessions.get(chatId)?.closed ? "closed" : "open";
|
|
949
|
-
/** The reason the session was closed, when one was given. */
|
|
950
|
-
sessionClosedReason = (chatId) => this.sessions.get(chatId)?.closedReason;
|
|
951
801
|
notifySessionChange(chatId, session) {
|
|
952
802
|
if (!this._onSessionChange)
|
|
953
803
|
return;
|
|
@@ -982,7 +832,7 @@ class TriggerChatTransport {
|
|
|
982
832
|
// `sessions: { ... }` already, or the very first `accessToken` call
|
|
983
833
|
// returns a PAT for an out-of-band-created session.
|
|
984
834
|
const token = await this.resolveAccessToken({ chatId });
|
|
985
|
-
const state =
|
|
835
|
+
const state = { publicAccessToken: token };
|
|
986
836
|
this.sessions.set(chatId, state);
|
|
987
837
|
this.notifySessionChange(chatId, state);
|
|
988
838
|
return state;
|
|
@@ -991,23 +841,15 @@ class TriggerChatTransport {
|
|
|
991
841
|
if (!this.resolveStartSession) {
|
|
992
842
|
throw new Error("TriggerChatTransport: `startSession` is required to call `start()` / `preload()`. Either provide it or pre-hydrate the session via `sessions: { ... }`.");
|
|
993
843
|
}
|
|
994
|
-
const { publicAccessToken
|
|
844
|
+
const { publicAccessToken } = await this.resolveStartSession({
|
|
995
845
|
taskId: this.taskId,
|
|
996
846
|
chatId,
|
|
997
847
|
clientData: (this.defaultMetadata ?? {}),
|
|
998
848
|
});
|
|
999
|
-
|
|
1000
|
-
this.emitEvent({
|
|
1001
|
-
type: "run-pending-version",
|
|
1002
|
-
chatId,
|
|
1003
|
-
timestamp: Date.now(),
|
|
1004
|
-
source: "start",
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
|
-
const state = this.applyPendingResumeCursor(chatId, {
|
|
849
|
+
const state = {
|
|
1008
850
|
publicAccessToken,
|
|
1009
851
|
isStreaming: false,
|
|
1010
|
-
}
|
|
852
|
+
};
|
|
1011
853
|
this.sessions.set(chatId, state);
|
|
1012
854
|
this.notifySessionChange(chatId, state);
|
|
1013
855
|
return state;
|
|
@@ -1020,33 +862,6 @@ class TriggerChatTransport {
|
|
|
1020
862
|
const raw = this.resolveBaseURLFn(ctx);
|
|
1021
863
|
return raw.replace(/\/$/, "");
|
|
1022
864
|
}
|
|
1023
|
-
/**
|
|
1024
|
-
* `fetchClient` for an `out` subscription. Reads `this.fetchOverride` on
|
|
1025
|
-
* every call, not once per subscription: a watch-mode subscription
|
|
1026
|
-
* reconnects for the lifetime of the page, and `setFetch` may have swapped
|
|
1027
|
-
* the override in between.
|
|
1028
|
-
*/
|
|
1029
|
-
sseFetchClient(ctx) {
|
|
1030
|
-
return ((input, init) => {
|
|
1031
|
-
const override = this.fetchOverride;
|
|
1032
|
-
if (!override)
|
|
1033
|
-
return fetch(input, init);
|
|
1034
|
-
if (typeof input === "string") {
|
|
1035
|
-
return override(input, init ?? {}, ctx);
|
|
1036
|
-
}
|
|
1037
|
-
if (input instanceof URL) {
|
|
1038
|
-
return override(input.toString(), init ?? {}, ctx);
|
|
1039
|
-
}
|
|
1040
|
-
// Request — preserve its url + intrinsic init, let any provided init
|
|
1041
|
-
// override on top (matches fetch(Request, init) semantics).
|
|
1042
|
-
return override(input.url, {
|
|
1043
|
-
method: input.method,
|
|
1044
|
-
headers: input.headers,
|
|
1045
|
-
signal: input.signal,
|
|
1046
|
-
...(init ?? {}),
|
|
1047
|
-
}, ctx);
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
865
|
async doFetch(ctx, url, init) {
|
|
1051
866
|
return this.fetchOverride ? this.fetchOverride(url, init, ctx) : fetch(url, init);
|
|
1052
867
|
}
|
|
@@ -1068,65 +883,24 @@ class TriggerChatTransport {
|
|
|
1068
883
|
const err = new Error(`appendToSessionStream failed: ${response.status} ${text}`);
|
|
1069
884
|
err.name = "TriggerApiError";
|
|
1070
885
|
err.status = response.status;
|
|
1071
|
-
let parsed;
|
|
1072
|
-
try {
|
|
1073
|
-
parsed = JSON.parse(text);
|
|
1074
|
-
}
|
|
1075
|
-
catch {
|
|
1076
|
-
parsed = undefined;
|
|
1077
|
-
}
|
|
1078
|
-
if (typeof parsed?.code === "string")
|
|
1079
|
-
err.code = parsed.code;
|
|
1080
|
-
if (typeof parsed?.closedReason === "string")
|
|
1081
|
-
err.closedReason = parsed.closedReason;
|
|
1082
886
|
throw err;
|
|
1083
887
|
}
|
|
1084
888
|
// The appended record's `.in` seq, for correlating the response stream to
|
|
1085
889
|
// this send. Omitted by older webapps / a lost idempotency claim.
|
|
1086
890
|
const data = (await response.json().catch(() => undefined));
|
|
1087
|
-
if (data?.pendingVersion === true) {
|
|
1088
|
-
this.emitEvent({
|
|
1089
|
-
type: "run-pending-version",
|
|
1090
|
-
chatId,
|
|
1091
|
-
timestamp: Date.now(),
|
|
1092
|
-
source: "send",
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1095
891
|
return typeof data?.seq === "number" ? data.seq : undefined;
|
|
1096
892
|
}
|
|
1097
893
|
async callWithAuthRetry(chatId, state, op) {
|
|
1098
|
-
if (state.closed) {
|
|
1099
|
-
throw sessionClosedError(chatId, state.closedReason);
|
|
1100
|
-
}
|
|
1101
|
-
// Every attempt goes through here, so a closed-session refusal is terminal
|
|
1102
|
-
// on whichever one hits it. Reaching a 409 from a later attempt and not
|
|
1103
|
-
// marking it would leave local status open on a conversation the server
|
|
1104
|
-
// has already ended.
|
|
1105
|
-
const attempt = async (token) => {
|
|
1106
|
-
try {
|
|
1107
|
-
return await op(token);
|
|
1108
|
-
}
|
|
1109
|
-
catch (err) {
|
|
1110
|
-
if (isSessionClosedError(err)) {
|
|
1111
|
-
// Terminal. Retrying, refreshing the PAT, or recreating the session
|
|
1112
|
-
// would all be wrong: the conversation is over.
|
|
1113
|
-
this.markSessionClosed(chatId, state, sessionClosedReasonOf(err), "append");
|
|
1114
|
-
}
|
|
1115
|
-
throw err;
|
|
1116
|
-
}
|
|
1117
|
-
};
|
|
1118
894
|
// 1) Try with the current PAT.
|
|
1119
895
|
try {
|
|
1120
|
-
return await
|
|
896
|
+
return await op(state.publicAccessToken);
|
|
1121
897
|
}
|
|
1122
898
|
catch (err) {
|
|
1123
|
-
if (isSessionClosedError(err))
|
|
1124
|
-
throw err;
|
|
1125
899
|
if (isSessionNotFoundError(err)) {
|
|
1126
900
|
// The cached PAT authenticated but the session doesn't exist here —
|
|
1127
901
|
// recreate it and retry.
|
|
1128
902
|
await this.recreateSession(chatId, state);
|
|
1129
|
-
return await
|
|
903
|
+
return await op(state.publicAccessToken);
|
|
1130
904
|
}
|
|
1131
905
|
if (!isAuthError(err))
|
|
1132
906
|
throw err;
|
|
@@ -1137,7 +911,7 @@ class TriggerChatTransport {
|
|
|
1137
911
|
state.publicAccessToken = fresh;
|
|
1138
912
|
this.notifySessionChange(chatId, state);
|
|
1139
913
|
try {
|
|
1140
|
-
return await
|
|
914
|
+
return await op(fresh);
|
|
1141
915
|
}
|
|
1142
916
|
catch (err) {
|
|
1143
917
|
if (!isSessionNotFoundError(err))
|
|
@@ -1147,7 +921,7 @@ class TriggerChatTransport {
|
|
|
1147
921
|
// state is stale (created in a different environment, or before the
|
|
1148
922
|
// sessions upgrade). Recreate the session and retry once.
|
|
1149
923
|
await this.recreateSession(chatId, state);
|
|
1150
|
-
return await
|
|
924
|
+
return await op(state.publicAccessToken);
|
|
1151
925
|
}
|
|
1152
926
|
/**
|
|
1153
927
|
* Repair a stale/phantom hydrated session. The cached state pointed at a
|
|
@@ -1163,19 +937,11 @@ class TriggerChatTransport {
|
|
|
1163
937
|
if (!this.resolveStartSession) {
|
|
1164
938
|
throw new Error("TriggerChatTransport: session not found and no `startSession` configured to recreate it. The stored session state for this chat may be stale (e.g. created in a different environment) — provide `startSession` or clear the stored session so a fresh one can be created.");
|
|
1165
939
|
}
|
|
1166
|
-
const { publicAccessToken
|
|
940
|
+
const { publicAccessToken } = await this.resolveStartSession({
|
|
1167
941
|
taskId: this.taskId,
|
|
1168
942
|
chatId,
|
|
1169
943
|
clientData: (this.defaultMetadata ?? {}),
|
|
1170
944
|
});
|
|
1171
|
-
if (pendingVersion) {
|
|
1172
|
-
this.emitEvent({
|
|
1173
|
-
type: "run-pending-version",
|
|
1174
|
-
chatId,
|
|
1175
|
-
timestamp: Date.now(),
|
|
1176
|
-
source: "start",
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
945
|
state.publicAccessToken = publicAccessToken;
|
|
1180
946
|
state.lastEventId = undefined;
|
|
1181
947
|
state.isStreaming = false;
|
|
@@ -1199,13 +965,8 @@ class TriggerChatTransport {
|
|
|
1199
965
|
: internalAbort.signal;
|
|
1200
966
|
if (abortSignal) {
|
|
1201
967
|
abortSignal.addEventListener("abort", () => {
|
|
1202
|
-
|
|
1203
|
-
// has no turn to stop: don't gate the next one, don't write a stop.
|
|
1204
|
-
const outstanding = !state.outstandingTurnAbandoned &&
|
|
1205
|
-
(state.isStreaming || state.activeInputSeq !== undefined);
|
|
1206
|
-
if (options?.sendStopOnAbort !== false && outstanding && !internalAbort.signal.aborted) {
|
|
968
|
+
if (options?.sendStopOnAbort !== false) {
|
|
1207
969
|
state.skipToTurnComplete = true;
|
|
1208
|
-
state.supersededInputSeq = state.activeInputSeq;
|
|
1209
970
|
this.appendInputChunk(chatId, state.publicAccessToken, this.serializeInputChunk({ kind: "stop" })).catch(() => { });
|
|
1210
971
|
}
|
|
1211
972
|
internalAbort.abort();
|
|
@@ -1265,7 +1026,26 @@ class TriggerChatTransport {
|
|
|
1265
1026
|
})()
|
|
1266
1027
|
: () => { };
|
|
1267
1028
|
const sseCtx = { endpoint: "out", chatId };
|
|
1268
|
-
const
|
|
1029
|
+
const fetchOverride = this.fetchOverride;
|
|
1030
|
+
const sseFetchClient = fetchOverride
|
|
1031
|
+
? ((input, init) => {
|
|
1032
|
+
if (typeof input === "string") {
|
|
1033
|
+
return fetchOverride(input, init ?? {}, sseCtx);
|
|
1034
|
+
}
|
|
1035
|
+
if (input instanceof URL) {
|
|
1036
|
+
return fetchOverride(input.toString(), init ?? {}, sseCtx);
|
|
1037
|
+
}
|
|
1038
|
+
// Request — preserve its url + intrinsic init, let any
|
|
1039
|
+
// provided init override on top (matches fetch(Request, init)
|
|
1040
|
+
// semantics).
|
|
1041
|
+
return fetchOverride(input.url, {
|
|
1042
|
+
method: input.method,
|
|
1043
|
+
headers: input.headers,
|
|
1044
|
+
signal: input.signal,
|
|
1045
|
+
...(init ?? {}),
|
|
1046
|
+
}, sseCtx);
|
|
1047
|
+
})
|
|
1048
|
+
: undefined;
|
|
1269
1049
|
let sawFirstChunk = false;
|
|
1270
1050
|
let sinceInSeq = options?.sinceInSeq;
|
|
1271
1051
|
const connectSseOnce = async (token) => {
|
|
@@ -1313,16 +1093,36 @@ class TriggerChatTransport {
|
|
|
1313
1093
|
return await connectSseOnce(fresh);
|
|
1314
1094
|
}
|
|
1315
1095
|
};
|
|
1096
|
+
// A body that ends without a turn-complete is only terminal when the
|
|
1097
|
+
// server says the session settled — otherwise the turn is still
|
|
1098
|
+
// running and we lost the connection (long-poll window closed, proxy
|
|
1099
|
+
// restarted). Resubscribe from `state.lastEventId`, bounded so a
|
|
1100
|
+
// permanently empty stream can't spin.
|
|
1101
|
+
const MAX_EOF_RESUBSCRIBES = 5;
|
|
1316
1102
|
let eofResubscribes = 0;
|
|
1317
1103
|
const resumeAfterEof = async () => {
|
|
1318
1104
|
// Watch mode is a standing subscription: it outlives turn-complete
|
|
1319
1105
|
// (which clears `isStreaming`) and idle windows EOF by design, so the
|
|
1320
1106
|
// give-up budget doesn't apply. Only abort or a settled session ends it.
|
|
1321
|
-
while ((this.watchMode || (state.isStreaming && eofResubscribes <
|
|
1107
|
+
while ((this.watchMode || (state.isStreaming && eofResubscribes < MAX_EOF_RESUBSCRIBES)) &&
|
|
1322
1108
|
!currentSubscription?.sessionSettled &&
|
|
1323
1109
|
!combinedSignal.aborted) {
|
|
1324
1110
|
eofResubscribes++;
|
|
1325
|
-
|
|
1111
|
+
// Sleep, but wake immediately on abort — otherwise a stop lands
|
|
1112
|
+
// mid-backoff and the stream stays open for the rest of it.
|
|
1113
|
+
await new Promise((resolve) => {
|
|
1114
|
+
let timer;
|
|
1115
|
+
const done = () => {
|
|
1116
|
+
clearTimeout(timer);
|
|
1117
|
+
combinedSignal.removeEventListener("abort", done);
|
|
1118
|
+
resolve();
|
|
1119
|
+
};
|
|
1120
|
+
// Jitter the backoff so many clients reconnecting after the same
|
|
1121
|
+
// dropped window don't resubscribe in lockstep.
|
|
1122
|
+
const backoff = Math.min(100 * 2 ** (eofResubscribes - 1), 5_000);
|
|
1123
|
+
timer = setTimeout(done, backoff * (0.5 + Math.random() * 0.5));
|
|
1124
|
+
combinedSignal.addEventListener("abort", done);
|
|
1125
|
+
});
|
|
1326
1126
|
if (combinedSignal.aborted)
|
|
1327
1127
|
break;
|
|
1328
1128
|
const opened = await openWithAuthRetry();
|
|
@@ -1446,41 +1246,11 @@ class TriggerChatTransport {
|
|
|
1446
1246
|
continue;
|
|
1447
1247
|
}
|
|
1448
1248
|
}
|
|
1449
|
-
if (state.skipToTurnComplete
|
|
1450
|
-
controlValue
|
|
1451
|
-
|
|
1452
|
-
// Fall through on a boundary so the `sinceInSeq` check and the
|
|
1453
|
-
// turn-completed emit below still run; content records are skipped.
|
|
1454
|
-
// A close is terminal and a pending version is the gated turn's own
|
|
1455
|
-
// handover status, so neither is ever gated away.
|
|
1456
|
-
if (controlValue !== v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
1457
|
-
continue;
|
|
1458
|
-
// A boundary older than the superseded turn is not its boundary —
|
|
1459
|
-
// keep gating. A header-less boundary (legacy wire) still clears.
|
|
1460
|
-
const gateCursorRaw = (0, v3_1.headerValue)(value.headers, v3_1.SESSION_IN_EVENT_ID_HEADER);
|
|
1461
|
-
const gateCursor = gateCursorRaw !== undefined ? Number.parseInt(gateCursorRaw, 10) : NaN;
|
|
1462
|
-
if (state.supersededInputSeq !== undefined &&
|
|
1463
|
-
!Number.isNaN(gateCursor) &&
|
|
1464
|
-
gateCursor < state.supersededInputSeq) {
|
|
1465
|
-
continue;
|
|
1466
|
-
}
|
|
1467
|
-
state.skipToTurnComplete = false;
|
|
1468
|
-
state.supersededInputSeq = undefined;
|
|
1469
|
-
// This boundary is the new turn's own, so the gate swallowed its
|
|
1470
|
-
// output: fail the turn instead of completing an empty answer, and
|
|
1471
|
-
// leave nothing armed for the retry.
|
|
1472
|
-
if (sinceInSeq !== undefined &&
|
|
1473
|
-
!Number.isNaN(gateCursor) &&
|
|
1474
|
-
gateCursor >= sinceInSeq) {
|
|
1475
|
-
// The lost boundary may also carry the session close.
|
|
1476
|
-
if ((0, v3_1.headerValue)(value.headers, v3_1.SESSION_CLOSED_HEADER) === "true") {
|
|
1477
|
-
this.markSessionClosed(chatId, state, (0, v3_1.headerValue)(value.headers, v3_1.SESSION_CLOSED_REASON_HEADER), "stream");
|
|
1478
|
-
}
|
|
1479
|
-
state.activeInputSeq = undefined;
|
|
1480
|
-
state.isStreaming = false;
|
|
1481
|
-
this.notifySessionChange(chatId, state);
|
|
1482
|
-
throw new Error("The previous turn's output was lost. Try again.");
|
|
1249
|
+
if (state.skipToTurnComplete) {
|
|
1250
|
+
if (controlValue === v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE) {
|
|
1251
|
+
state.skipToTurnComplete = false;
|
|
1483
1252
|
}
|
|
1253
|
+
continue;
|
|
1484
1254
|
}
|
|
1485
1255
|
if (controlValue === v3_1.TRIGGER_CONTROL_SUBTYPE.UPGRADE_REQUIRED) {
|
|
1486
1256
|
// Server has already triggered the new run via
|
|
@@ -1489,26 +1259,6 @@ class TriggerChatTransport {
|
|
|
1489
1259
|
// and keep reading.
|
|
1490
1260
|
continue;
|
|
1491
1261
|
}
|
|
1492
|
-
if (controlValue === v3_1.TRIGGER_CONTROL_SUBTYPE.SESSION_CLOSED) {
|
|
1493
|
-
this.markSessionClosed(chatId, state, (0, v3_1.headerValue)(value.headers, v3_1.SESSION_CLOSED_REASON_HEADER), "stream");
|
|
1494
|
-
internalAbort.abort();
|
|
1495
|
-
try {
|
|
1496
|
-
controller.close();
|
|
1497
|
-
}
|
|
1498
|
-
catch {
|
|
1499
|
-
/* already closed */
|
|
1500
|
-
}
|
|
1501
|
-
return;
|
|
1502
|
-
}
|
|
1503
|
-
if (controlValue === v3_1.TRIGGER_CONTROL_SUBTYPE.PENDING_VERSION) {
|
|
1504
|
-
this.emitEvent({
|
|
1505
|
-
type: "run-pending-version",
|
|
1506
|
-
chatId,
|
|
1507
|
-
timestamp: Date.now(),
|
|
1508
|
-
source: "upgrade",
|
|
1509
|
-
});
|
|
1510
|
-
continue;
|
|
1511
|
-
}
|
|
1512
1262
|
if (controlValue === v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE) {
|
|
1513
1263
|
// Skip a turn-complete from an earlier turn (committed `.in` cursor
|
|
1514
1264
|
// below this send's seq), e.g. an undo action that raced this send.
|
|
@@ -1519,11 +1269,6 @@ class TriggerChatTransport {
|
|
|
1519
1269
|
continue;
|
|
1520
1270
|
}
|
|
1521
1271
|
}
|
|
1522
|
-
// A close decided during this turn rides out on turn-complete —
|
|
1523
|
-
// the last record a reader sees before it ends the stream.
|
|
1524
|
-
if ((0, v3_1.headerValue)(value.headers, v3_1.SESSION_CLOSED_HEADER) === "true") {
|
|
1525
|
-
this.markSessionClosed(chatId, state, (0, v3_1.headerValue)(value.headers, v3_1.SESSION_CLOSED_REASON_HEADER), "stream");
|
|
1526
|
-
}
|
|
1527
1272
|
const refreshedToken = (0, v3_1.headerValue)(value.headers, v3_1.PUBLIC_ACCESS_TOKEN_HEADER) ??
|
|
1528
1273
|
legacyChunk?.publicAccessToken;
|
|
1529
1274
|
if (refreshedToken) {
|