@trigger.dev/sdk 4.5.0-rc.5 → 4.5.0-rc.6
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/v3/ai.d.ts +7 -0
- package/dist/commonjs/v3/ai.js +303 -106
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-client.js +3 -0
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.js +10 -7
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat.js +34 -6
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/chat.test.js +53 -0
- package/dist/commonjs/v3/chat.test.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +8 -4
- package/dist/commonjs/v3/sessions.js +7 -3
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/test/mock-chat-agent.d.ts +6 -0
- package/dist/commonjs/v3/test/mock-chat-agent.js +1 -0
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +7 -0
- package/dist/esm/v3/ai.js +303 -107
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-client.js +3 -0
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.js +10 -7
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat.js +34 -6
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/chat.test.js +53 -0
- package/dist/esm/v3/chat.test.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +8 -4
- package/dist/esm/v3/sessions.js +7 -3
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.d.ts +6 -0
- package/dist/esm/v3/test/mock-chat-agent.js +1 -0
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +4 -3
package/dist/commonjs/v3/ai.d.ts
CHANGED
|
@@ -1263,6 +1263,13 @@ export type TurnCompleteEvent<TClientData = unknown, TUIM extends UIMessage = UI
|
|
|
1263
1263
|
* manual `pipeChat()` or an aborted stream).
|
|
1264
1264
|
*/
|
|
1265
1265
|
finishReason?: FinishReason;
|
|
1266
|
+
/**
|
|
1267
|
+
* Set when the turn failed (the `run()` body or a lifecycle hook threw).
|
|
1268
|
+
* On an errored turn `responseMessage` is undefined or partial and
|
|
1269
|
+
* `finishReason` is `"error"`. Use this to mark the turn failed in your
|
|
1270
|
+
* persistence. Undefined on a successful turn.
|
|
1271
|
+
*/
|
|
1272
|
+
error?: unknown;
|
|
1266
1273
|
};
|
|
1267
1274
|
/**
|
|
1268
1275
|
* Event passed to the `onBeforeTurnComplete` callback.
|
package/dist/commonjs/v3/ai.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.chat = exports.upsertIncomingMessage = exports.PENDING_MESSAGE_INJECTED_TYPE = exports.ai = void 0;
|
|
4
|
+
exports.__findLatestSessionInCursorForTests = __findLatestSessionInCursorForTests;
|
|
4
5
|
exports.__setReadChatSnapshotImplForTests = __setReadChatSnapshotImplForTests;
|
|
5
6
|
exports.__setWriteChatSnapshotImplForTests = __setWriteChatSnapshotImplForTests;
|
|
6
7
|
exports.__readChatSnapshotProductionPathForTests = __readChatSnapshotProductionPathForTests;
|
|
@@ -81,51 +82,41 @@ const lastTurnCompleteSeqNumKey = locals_js_1.locals.create("chat.lastTurnComple
|
|
|
81
82
|
* the `.in` subscription so already-processed user messages don't get
|
|
82
83
|
* replayed from S2.
|
|
83
84
|
*
|
|
84
|
-
* Implementation
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
85
|
+
* Implementation is a non-blocking records read (`wait=0`) — the
|
|
86
|
+
* endpoint returns everything currently stored (including pre-trim
|
|
87
|
+
* records, since S2 trims are eventually consistent) in one shot, and
|
|
88
|
+
* we keep the LAST matching header. The previous SSE-based scan had to
|
|
89
|
+
* idle-wait a full 5s window to know it reached the tail, which put a
|
|
90
|
+
* constant ~6s tax on every continuation boot.
|
|
90
91
|
*
|
|
91
92
|
* Returns `undefined` if no `turn-complete` carrying the header has been
|
|
92
93
|
* written yet — first-turn-ever, first turn post-OOM-with-no-prior-runs,
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* messages.
|
|
94
|
+
* a `turn-complete` written before this header existed, or a server old
|
|
95
|
+
* enough that the records endpoint doesn't serialize headers. Callers
|
|
96
|
+
* fall back to subscribing `.in` from seq 0 in that case; the slim-wire
|
|
97
|
+
* merge handles any dedup against snapshot-restored messages.
|
|
97
98
|
* @internal
|
|
98
99
|
*/
|
|
99
100
|
async function findLatestSessionInCursor(chatId) {
|
|
100
101
|
const apiClient = v3_1.apiClientManager.clientOrThrow();
|
|
102
|
+
const response = await apiClient.readSessionStreamRecords(chatId, "out");
|
|
101
103
|
let latestCursor;
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (event.subtype !== v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
112
|
-
return;
|
|
113
|
-
const raw = (0, v3_1.headerValue)(event.headers, v3_1.SESSION_IN_EVENT_ID_HEADER);
|
|
114
|
-
if (!raw)
|
|
115
|
-
return;
|
|
116
|
-
const parsed = Number.parseInt(raw, 10);
|
|
117
|
-
if (Number.isFinite(parsed))
|
|
118
|
-
latestCursor = parsed;
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
// Drain the stream so the underlying SSE reader runs to completion. We
|
|
122
|
-
// don't accumulate chunks; `onControl` fires inline as turn-complete
|
|
123
|
-
// records arrive.
|
|
124
|
-
for await (const _ of stream) {
|
|
125
|
-
// intentionally empty
|
|
104
|
+
for (const record of response.records) {
|
|
105
|
+
if ((0, v3_1.controlSubtype)(record.headers) !== v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
106
|
+
continue;
|
|
107
|
+
const raw = (0, v3_1.headerValue)(record.headers, v3_1.SESSION_IN_EVENT_ID_HEADER);
|
|
108
|
+
if (!raw)
|
|
109
|
+
continue;
|
|
110
|
+
const parsed = Number.parseInt(raw, 10);
|
|
111
|
+
if (Number.isFinite(parsed))
|
|
112
|
+
latestCursor = parsed;
|
|
126
113
|
}
|
|
127
114
|
return latestCursor;
|
|
128
115
|
}
|
|
116
|
+
/** Test-only entry point for the records-based cursor scan. @internal */
|
|
117
|
+
async function __findLatestSessionInCursorForTests(chatId) {
|
|
118
|
+
return findLatestSessionInCursor(chatId);
|
|
119
|
+
}
|
|
129
120
|
let readChatSnapshotImpl;
|
|
130
121
|
function __setReadChatSnapshotImplForTests(impl) {
|
|
131
122
|
readChatSnapshotImpl = impl;
|
|
@@ -993,8 +984,15 @@ const messagesInput = {
|
|
|
993
984
|
on(handler) {
|
|
994
985
|
return getChatSession().in.on((chunk) => {
|
|
995
986
|
if (chunk.kind === "message") {
|
|
996
|
-
|
|
987
|
+
// Returning `true` marks the record CONSUMED at the manager level:
|
|
988
|
+
// it is neither buffered for a later `once()` nor re-delivered by
|
|
989
|
+
// the buffer drain when the next turn re-attaches its handler.
|
|
990
|
+
// Without this, a message arriving mid-stream was delivered twice
|
|
991
|
+
// and ran a duplicate turn.
|
|
992
|
+
void Promise.resolve(handler(chunk.payload)).catch(() => { });
|
|
993
|
+
return true;
|
|
997
994
|
}
|
|
995
|
+
return undefined;
|
|
998
996
|
});
|
|
999
997
|
},
|
|
1000
998
|
once(options) {
|
|
@@ -1088,8 +1086,13 @@ const stopInput = {
|
|
|
1088
1086
|
on(handler) {
|
|
1089
1087
|
return getChatSession().in.on((chunk) => {
|
|
1090
1088
|
if (chunk.kind === "stop") {
|
|
1091
|
-
|
|
1089
|
+
// Consume stop records (see the messages facade above). A stop is
|
|
1090
|
+
// only meaningful to the turn it interrupts — buffering it would
|
|
1091
|
+
// let a stale stop abort a future turn.
|
|
1092
|
+
void Promise.resolve(handler({ stop: true, message: chunk.message })).catch(() => { });
|
|
1093
|
+
return true;
|
|
1092
1094
|
}
|
|
1095
|
+
return undefined;
|
|
1093
1096
|
});
|
|
1094
1097
|
},
|
|
1095
1098
|
once(options) {
|
|
@@ -1243,6 +1246,9 @@ const chatHandoverIsFinalKey = locals_js_1.locals.create("chat.handoverIsFinal")
|
|
|
1243
1246
|
* `tool-approval-response` rows are AI-SDK-internal and don't need a
|
|
1244
1247
|
* UIMessage representation. We map:
|
|
1245
1248
|
* - `text` parts → `{ type: "text", text }`
|
|
1249
|
+
* - `reasoning` parts → `{ type: "reasoning", text, state: "done" }`
|
|
1250
|
+
* (provider metadata carried so an Anthropic thinking signature
|
|
1251
|
+
* survives a UIMessage → ModelMessage round trip)
|
|
1246
1252
|
* - `tool-call` parts → `{ type: "tool-${name}", toolCallId,
|
|
1247
1253
|
* state: "input-available", input }`
|
|
1248
1254
|
* - `tool-approval-request` parts → skipped (AI SDK derives the
|
|
@@ -1259,6 +1265,14 @@ function synthesizeHandoverUIMessage(partial, messageId) {
|
|
|
1259
1265
|
if (part.type === "text" && typeof part.text === "string") {
|
|
1260
1266
|
parts.push({ type: "text", text: part.text });
|
|
1261
1267
|
}
|
|
1268
|
+
else if (part.type === "reasoning" && typeof part.text === "string") {
|
|
1269
|
+
parts.push({
|
|
1270
|
+
type: "reasoning",
|
|
1271
|
+
text: part.text,
|
|
1272
|
+
state: "done",
|
|
1273
|
+
...(part.providerOptions ? { providerMetadata: part.providerOptions } : {}),
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1262
1276
|
else if (part.type === "tool-call" && part.toolCallId && part.toolName) {
|
|
1263
1277
|
parts.push({
|
|
1264
1278
|
type: `tool-${part.toolName}`,
|
|
@@ -2605,6 +2619,11 @@ function chatCustomAgent(options) {
|
|
|
2605
2619
|
// No client-side upsert needed.
|
|
2606
2620
|
locals_js_1.locals.set(chatSessionHandleKey, sessions_js_1.sessions.open(payload.chatId));
|
|
2607
2621
|
locals_js_1.locals.set(chatAgentRunContextKey, runOptions.ctx);
|
|
2622
|
+
// Initialize the turn-complete trim slot so `chat.writeTurnComplete`
|
|
2623
|
+
// trims `session.out` back to the previous turn boundary. Without
|
|
2624
|
+
// this the slot is undefined and the trim never runs, so `.out`
|
|
2625
|
+
// grows without bound for the whole custom-agent surface.
|
|
2626
|
+
locals_js_1.locals.set(lastTurnCompleteSeqNumKey, { value: undefined });
|
|
2608
2627
|
(0, streams_js_1.markChatAgentRunForStreamsWarning)();
|
|
2609
2628
|
v3_1.taskContext.setConversationId(payload.chatId);
|
|
2610
2629
|
stampConversationIdOnActiveSpan(payload.chatId);
|
|
@@ -2730,6 +2749,11 @@ function chatAgent(options) {
|
|
|
2730
2749
|
// `messagesInput.waitWithIdleTimeout` so recovered turns fire first.
|
|
2731
2750
|
const bootInjectedQueue = [];
|
|
2732
2751
|
const couldHavePriorState = payload.continuation === true || ctx.attempt.number > 1;
|
|
2752
|
+
// `.in` resume cursor, computed at most once per boot. The boot
|
|
2753
|
+
// block below resolves it (snapshot field or records scan) and the
|
|
2754
|
+
// resume-cursor block reuses it instead of re-scanning.
|
|
2755
|
+
let bootInCursor;
|
|
2756
|
+
let bootInCursorResolved = false;
|
|
2733
2757
|
if (!hydrateMessages && couldHavePriorState) {
|
|
2734
2758
|
// Single parent span for the whole boot read phase — snapshot
|
|
2735
2759
|
// read, session.out replay, session.in replay. Per-phase timing
|
|
@@ -2765,23 +2789,28 @@ function chatAgent(options) {
|
|
|
2765
2789
|
slot.value = seeded;
|
|
2766
2790
|
}
|
|
2767
2791
|
}
|
|
2768
|
-
//
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2792
|
+
// The `.out` replay and the `.in` cursor + tail read are
|
|
2793
|
+
// independent (both depend only on the snapshot) — run them
|
|
2794
|
+
// concurrently. Each phase keeps its own catch + duration
|
|
2795
|
+
// attribute.
|
|
2796
|
+
const replayOutPhase = async () => {
|
|
2797
|
+
const replayOutStart = Date.now();
|
|
2798
|
+
try {
|
|
2799
|
+
const replayResult = await replaySessionOutTail(sessionIdForSnapshot, { lastEventId: bootSnapshot?.lastOutEventId });
|
|
2800
|
+
replayedSettled = replayResult.settled;
|
|
2801
|
+
replayedPartial = replayResult.partial;
|
|
2802
|
+
replayedPartialRaw = replayResult.partialRaw;
|
|
2803
|
+
}
|
|
2804
|
+
catch (error) {
|
|
2805
|
+
v3_1.logger.warn("chat.agent: session.out replay failed; using snapshot only", {
|
|
2806
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2807
|
+
sessionId: sessionIdForSnapshot,
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
bootSpan.setAttribute("chat.boot.replay.out.durationMs", Date.now() - replayOutStart);
|
|
2811
|
+
bootSpan.setAttribute("chat.boot.replay.out.settledCount", replayedSettled.length);
|
|
2812
|
+
bootSpan.setAttribute("chat.boot.replay.out.partialPresent", replayedPartial !== undefined);
|
|
2813
|
+
};
|
|
2785
2814
|
// session.in tail read
|
|
2786
2815
|
//
|
|
2787
2816
|
// session.in carries the user-side of the conversation
|
|
@@ -2792,20 +2821,43 @@ function chatAgent(options) {
|
|
|
2792
2821
|
// visible via the live SSE subscription — by which point they
|
|
2793
2822
|
// would arrive AFTER the partial-assistant orphan and look like
|
|
2794
2823
|
// brand-new turns to the model, producing inverted chains.
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2824
|
+
//
|
|
2825
|
+
// The cursor comes from the snapshot when present (written
|
|
2826
|
+
// there since `lastInEventId` was added) — otherwise from a
|
|
2827
|
+
// records scan of `.out`'s latest turn-complete header.
|
|
2828
|
+
const replayInPhase = async () => {
|
|
2829
|
+
const replayInStart = Date.now();
|
|
2830
|
+
const snapshotInCursor = bootSnapshot?.lastInEventId !== undefined
|
|
2831
|
+
? Number.parseInt(bootSnapshot.lastInEventId, 10)
|
|
2832
|
+
: undefined;
|
|
2833
|
+
if (snapshotInCursor !== undefined && Number.isFinite(snapshotInCursor)) {
|
|
2834
|
+
bootInCursor = snapshotInCursor;
|
|
2835
|
+
bootInCursorResolved = true;
|
|
2836
|
+
}
|
|
2837
|
+
else {
|
|
2838
|
+
try {
|
|
2839
|
+
bootInCursor = await findLatestSessionInCursor(payload.chatId);
|
|
2840
|
+
bootInCursorResolved = true;
|
|
2841
|
+
}
|
|
2842
|
+
catch {
|
|
2843
|
+
// Transient scan failure: leave unresolved so the
|
|
2844
|
+
// resume-cursor block below retries the lookup.
|
|
2845
|
+
bootInCursor = undefined;
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
bootSpan.setAttribute("chat.boot.replay.in.cursorFromSnapshot", snapshotInCursor !== undefined);
|
|
2849
|
+
try {
|
|
2850
|
+
replayedInTail = await replaySessionInTail(payload.chatId, {
|
|
2851
|
+
lastEventId: bootInCursor !== undefined ? String(bootInCursor) : undefined,
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
catch (error) {
|
|
2855
|
+
v3_1.logger.warn("chat.agent: session.in replay failed; in-flight users may not be recovered", { error: error instanceof Error ? error.message : String(error) });
|
|
2856
|
+
}
|
|
2857
|
+
bootSpan.setAttribute("chat.boot.replay.in.durationMs", Date.now() - replayInStart);
|
|
2858
|
+
bootSpan.setAttribute("chat.boot.replay.in.userCount", replayedInTail.length);
|
|
2859
|
+
};
|
|
2860
|
+
await Promise.all([replayOutPhase(), replayInPhase()]);
|
|
2809
2861
|
}, {
|
|
2810
2862
|
attributes: {
|
|
2811
2863
|
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "tabler-rotate-clockwise",
|
|
@@ -2846,7 +2898,12 @@ function chatAgent(options) {
|
|
|
2846
2898
|
bootSnapshot !== undefined;
|
|
2847
2899
|
if (needsResumeCursor) {
|
|
2848
2900
|
try {
|
|
2849
|
-
|
|
2901
|
+
// Reuse the cursor the boot block already resolved (snapshot
|
|
2902
|
+
// field or records scan) — only scan here when the boot block
|
|
2903
|
+
// was skipped (hydrateMessages, or snapshot-only signals).
|
|
2904
|
+
const cursor = bootInCursorResolved
|
|
2905
|
+
? bootInCursor
|
|
2906
|
+
: await findLatestSessionInCursor(payload.chatId);
|
|
2850
2907
|
if (cursor !== undefined) {
|
|
2851
2908
|
v3_1.sessionStreams.setLastSeqNum(payload.chatId, "in", cursor);
|
|
2852
2909
|
v3_1.sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor);
|
|
@@ -3610,6 +3667,18 @@ function chatAgent(options) {
|
|
|
3610
3667
|
// therefore a delta merge, not a full-history reset.
|
|
3611
3668
|
if (currentWirePayload.trigger !== "action") {
|
|
3612
3669
|
let cleanedUIMessages = cleanedIncomingMessages;
|
|
3670
|
+
// Turn-0 head-start with hydrateMessages: the boot seeding from
|
|
3671
|
+
// `payload.headStartMessages` is non-hydrate-only, so ship the
|
|
3672
|
+
// route handler's first-turn history to the hydrate hook as
|
|
3673
|
+
// incoming messages instead (gated on the pending handover).
|
|
3674
|
+
if (turn === 0 &&
|
|
3675
|
+
hydrateMessages &&
|
|
3676
|
+
cleanedUIMessages.length === 0 &&
|
|
3677
|
+
(locals_js_1.locals.get(chatHandoverPartialKey)?.length ?? 0) > 0 &&
|
|
3678
|
+
Array.isArray(payload.headStartMessages) &&
|
|
3679
|
+
payload.headStartMessages.length > 0) {
|
|
3680
|
+
cleanedUIMessages = payload.headStartMessages;
|
|
3681
|
+
}
|
|
3613
3682
|
// Validate/transform UIMessages before conversion — catches malformed
|
|
3614
3683
|
// messages from storage or untrusted input before they reach the model.
|
|
3615
3684
|
// Slim wire: triggers like `regenerate-message` carry no incoming
|
|
@@ -3788,40 +3857,47 @@ function chatAgent(options) {
|
|
|
3788
3857
|
// `preload` / `close` / `handover-prepare` and submits
|
|
3789
3858
|
// with no incoming message fall through with the boot-
|
|
3790
3859
|
// seeded accumulator unchanged.
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3860
|
+
}
|
|
3861
|
+
if (turn === 0) {
|
|
3862
|
+
// Head-start handover splice (turn 0 only, BOTH
|
|
3863
|
+
// accumulation branches — hydrate and default): the
|
|
3864
|
+
// `chat.handover` route handler signalled a mid-turn
|
|
3865
|
+
// handover, so splice its partial assistant response
|
|
3866
|
+
// (text + pending tool-calls + the synthesized
|
|
3867
|
+
// tool-approval round) onto the accumulator.
|
|
3868
|
+
// `streamText` then hits AI SDK's initial-tool-
|
|
3869
|
+
// execution branch, runs the agent-side tool executes,
|
|
3870
|
+
// and resumes from step 2 — skipping the first model
|
|
3871
|
+
// call (already done by the handler).
|
|
3872
|
+
//
|
|
3873
|
+
// We also synthesize a UIMessage form of the partial
|
|
3874
|
+
// assistant and push it to `accumulatedUIMessages` so
|
|
3875
|
+
// AI SDK's `processUIMessageStream` (invoked when the
|
|
3876
|
+
// run loop calls `runResult.toUIMessageStream({
|
|
3877
|
+
// onFinish })`) can initialize `state.message` from
|
|
3878
|
+
// the trailing assistant in `originalMessages`. Without
|
|
3879
|
+
// that, the `tool-output-available` chunks emitted by
|
|
3880
|
+
// the initial-tool-execution branch can't find their
|
|
3881
|
+
// matching tool-call in state and AI SDK throws
|
|
3882
|
+
// `UIMessageStreamError: No tool invocation found`.
|
|
3883
|
+
const pendingHandoverPartial = locals_js_1.locals.get(chatHandoverPartialKey);
|
|
3884
|
+
if (pendingHandoverPartial && pendingHandoverPartial.length > 0) {
|
|
3885
|
+
const handoverMessageId = locals_js_1.locals.get(chatHandoverMessageIdKey);
|
|
3886
|
+
// Skip if the hydrated chain already persisted the
|
|
3887
|
+
// partial under the handover messageId.
|
|
3888
|
+
const alreadyInChain = handoverMessageId !== undefined &&
|
|
3889
|
+
accumulatedUIMessages.some((m) => m.id === handoverMessageId);
|
|
3890
|
+
if (!alreadyInChain) {
|
|
3814
3891
|
accumulatedMessages.push(...pendingHandoverPartial);
|
|
3815
|
-
const handoverMessageId = locals_js_1.locals.get(chatHandoverMessageIdKey);
|
|
3816
3892
|
const partialUI = synthesizeHandoverUIMessage(pendingHandoverPartial, handoverMessageId);
|
|
3817
3893
|
if (partialUI) {
|
|
3818
3894
|
accumulatedUIMessages.push(partialUI);
|
|
3819
3895
|
}
|
|
3820
|
-
locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
|
|
3821
3896
|
}
|
|
3897
|
+
locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
|
|
3822
3898
|
}
|
|
3823
|
-
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
3824
3899
|
}
|
|
3900
|
+
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
3825
3901
|
} // end if (trigger !== "action")
|
|
3826
3902
|
// ── Action result handling ──────────────────────────────
|
|
3827
3903
|
// For action turns, skip the turn machinery entirely.
|
|
@@ -4518,11 +4594,15 @@ function chatAgent(options) {
|
|
|
4518
4594
|
if (!hydrateMessages) {
|
|
4519
4595
|
try {
|
|
4520
4596
|
await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
|
|
4597
|
+
const snapshotInCursor = getChatSession().in.lastDispatchedSeqNum();
|
|
4521
4598
|
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
4522
4599
|
version: 1,
|
|
4523
4600
|
savedAt: Date.now(),
|
|
4524
4601
|
messages: accumulatedUIMessages,
|
|
4525
4602
|
lastOutEventId: turnCompleteResult?.lastEventId,
|
|
4603
|
+
lastInEventId: snapshotInCursor !== undefined
|
|
4604
|
+
? String(snapshotInCursor)
|
|
4605
|
+
: undefined,
|
|
4526
4606
|
});
|
|
4527
4607
|
}, {
|
|
4528
4608
|
attributes: {
|
|
@@ -4659,17 +4739,100 @@ function chatAgent(options) {
|
|
|
4659
4739
|
if (turnError instanceof v3_1.OutOfMemoryError) {
|
|
4660
4740
|
throw turnError;
|
|
4661
4741
|
}
|
|
4742
|
+
let errorTurnCompleteResult;
|
|
4662
4743
|
try {
|
|
4663
4744
|
await withChatWriter(async (writer) => {
|
|
4664
4745
|
const errorText = turnError instanceof Error ? turnError.message : "An unexpected error occurred";
|
|
4665
4746
|
writer.write({ type: "error", errorText });
|
|
4666
4747
|
});
|
|
4667
4748
|
// Signal turn complete so the client knows this turn is done
|
|
4668
|
-
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4749
|
+
errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4669
4750
|
}
|
|
4670
4751
|
catch {
|
|
4671
4752
|
// Best-effort — if stream write fails, let the run continue anyway
|
|
4672
4753
|
}
|
|
4754
|
+
// The submit-message merge into the accumulator may not have run
|
|
4755
|
+
// yet (a pre-run hook threw), so fold the wire message in for the
|
|
4756
|
+
// error event + snapshot — the cursor has already advanced past it,
|
|
4757
|
+
// so otherwise it survives in neither the snapshot nor the `.in` tail.
|
|
4758
|
+
const erroredWireMessage = currentWirePayload.message;
|
|
4759
|
+
const erroredUIMessages = erroredWireMessage &&
|
|
4760
|
+
!accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id)
|
|
4761
|
+
? [...accumulatedUIMessages, erroredWireMessage]
|
|
4762
|
+
: accumulatedUIMessages;
|
|
4763
|
+
// Fire onTurnComplete on the error path too — the docs promise it
|
|
4764
|
+
// runs "after every turn, successful or errored" so customers can
|
|
4765
|
+
// mark the turn failed. `responseMessage` is undefined/partial and
|
|
4766
|
+
// `error` carries the thrown value.
|
|
4767
|
+
if (onTurnComplete) {
|
|
4768
|
+
try {
|
|
4769
|
+
await tracer_js_1.tracer.startActiveSpan("onTurnComplete()", async () => {
|
|
4770
|
+
await onTurnComplete({
|
|
4771
|
+
ctx,
|
|
4772
|
+
chatId: currentWirePayload.chatId,
|
|
4773
|
+
messages: accumulatedMessages,
|
|
4774
|
+
uiMessages: erroredUIMessages,
|
|
4775
|
+
newMessages: [],
|
|
4776
|
+
newUIMessages: erroredWireMessage ? [erroredWireMessage] : [],
|
|
4777
|
+
responseMessage: undefined,
|
|
4778
|
+
rawResponseMessage: undefined,
|
|
4779
|
+
turn,
|
|
4780
|
+
runId: ctx.run.id,
|
|
4781
|
+
chatAccessToken: "",
|
|
4782
|
+
// Parsed `clientData` isn't reliably in scope here (parsing
|
|
4783
|
+
// may itself be the failure), and the raw metadata is the
|
|
4784
|
+
// wrong shape — leave it undefined on the error path.
|
|
4785
|
+
clientData: undefined,
|
|
4786
|
+
stopped: false,
|
|
4787
|
+
continuation,
|
|
4788
|
+
previousRunId,
|
|
4789
|
+
preloaded,
|
|
4790
|
+
totalUsage: cumulativeUsage,
|
|
4791
|
+
finishReason: "error",
|
|
4792
|
+
error: turnError,
|
|
4793
|
+
lastEventId: errorTurnCompleteResult?.lastEventId,
|
|
4794
|
+
});
|
|
4795
|
+
}, {
|
|
4796
|
+
attributes: {
|
|
4797
|
+
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete",
|
|
4798
|
+
[v3_1.SemanticInternalAttributes.COLLAPSED]: true,
|
|
4799
|
+
"chat.id": currentWirePayload.chatId,
|
|
4800
|
+
"chat.turn": turn + 1,
|
|
4801
|
+
"chat.errored": true,
|
|
4802
|
+
},
|
|
4803
|
+
});
|
|
4804
|
+
}
|
|
4805
|
+
catch {
|
|
4806
|
+
// A throwing onTurnComplete on the error path must not crash
|
|
4807
|
+
// the run — keep the conversation alive for the next message.
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4810
|
+
// Persist a snapshot so the failed turn's user message isn't
|
|
4811
|
+
// stranded. `writeTurnCompleteChunk` already advanced the `.in`
|
|
4812
|
+
// cursor past it (via the session-in-event-id header), and the
|
|
4813
|
+
// success-path snapshot write is skipped on error — without this
|
|
4814
|
+
// the next boot would resume past a message that exists in
|
|
4815
|
+
// neither the snapshot nor the replayable `.in` tail.
|
|
4816
|
+
if (!hydrateMessages) {
|
|
4817
|
+
try {
|
|
4818
|
+
const errorSnapshotInCursor = getChatSession().in.lastDispatchedSeqNum();
|
|
4819
|
+
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
4820
|
+
version: 1,
|
|
4821
|
+
savedAt: Date.now(),
|
|
4822
|
+
messages: erroredUIMessages,
|
|
4823
|
+
lastOutEventId: errorTurnCompleteResult?.lastEventId,
|
|
4824
|
+
lastInEventId: errorSnapshotInCursor !== undefined
|
|
4825
|
+
? String(errorSnapshotInCursor)
|
|
4826
|
+
: undefined,
|
|
4827
|
+
});
|
|
4828
|
+
}
|
|
4829
|
+
catch (error) {
|
|
4830
|
+
v3_1.logger.warn("chat.agent: error-path snapshot write failed", {
|
|
4831
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4832
|
+
sessionId: sessionIdForSnapshot,
|
|
4833
|
+
});
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4673
4836
|
// chat.requestUpgrade() / chat.endRun() — exit after error turn too
|
|
4674
4837
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey) ||
|
|
4675
4838
|
locals_js_1.locals.get(chatEndRunRequestedKey)) {
|
|
@@ -5535,18 +5698,32 @@ function createChatSession(payload, options) {
|
|
|
5535
5698
|
return {
|
|
5536
5699
|
async next() {
|
|
5537
5700
|
turn++;
|
|
5538
|
-
// First turn:
|
|
5539
|
-
|
|
5701
|
+
// First turn: wait when the boot payload carries no message.
|
|
5702
|
+
// Preload boots wait for the first real message; continuation
|
|
5703
|
+
// boots (fresh run via `ensureRunForSession` / end-and-continue)
|
|
5704
|
+
// arrive with the sticky boot-payload fields stripped, so running
|
|
5705
|
+
// a turn immediately would invoke the model with no user input.
|
|
5706
|
+
const isMessagelessContinuationBoot = currentPayload.continuation === true && !currentPayload.message;
|
|
5707
|
+
if (turn === 0 && (currentPayload.trigger === "preload" || isMessagelessContinuationBoot)) {
|
|
5540
5708
|
const result = await messagesInput.waitWithIdleTimeout({
|
|
5541
5709
|
idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30,
|
|
5542
5710
|
timeout,
|
|
5543
|
-
spanName:
|
|
5711
|
+
spanName: currentPayload.trigger === "preload"
|
|
5712
|
+
? "waiting for first message"
|
|
5713
|
+
: "waiting for first message (continuation)",
|
|
5544
5714
|
});
|
|
5545
5715
|
if (!result.ok || runSignal.aborted) {
|
|
5546
5716
|
stop.cleanup();
|
|
5547
5717
|
return { done: true, value: undefined };
|
|
5548
5718
|
}
|
|
5719
|
+
const continuationBoot = isMessagelessContinuationBoot;
|
|
5549
5720
|
currentPayload = result.output;
|
|
5721
|
+
// Preserve the continuation flag — the wire payload of the next
|
|
5722
|
+
// message doesn't carry it, and `turn.continuation` is how the
|
|
5723
|
+
// user knows to seed history (e.g. `turn.setMessages(stored)`).
|
|
5724
|
+
if (continuationBoot && currentPayload.continuation === undefined) {
|
|
5725
|
+
currentPayload = { ...currentPayload, continuation: true };
|
|
5726
|
+
}
|
|
5550
5727
|
}
|
|
5551
5728
|
// Subsequent turns: wait for the next message
|
|
5552
5729
|
if (turn > 0) {
|
|
@@ -5699,14 +5876,22 @@ function createChatSession(payload, options) {
|
|
|
5699
5876
|
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
5700
5877
|
}
|
|
5701
5878
|
}
|
|
5702
|
-
// Capture token usage from the streamText result
|
|
5879
|
+
// Capture token usage from the streamText result. Race with a 2s
|
|
5880
|
+
// timeout — on stop-abort the AI SDK's totalUsage promise can hang
|
|
5881
|
+
// indefinitely, which would wedge the turn loop (same guard as
|
|
5882
|
+
// chat.agent's turn loop).
|
|
5703
5883
|
let turnUsage;
|
|
5704
5884
|
if (typeof source.totalUsage?.then === "function") {
|
|
5705
5885
|
try {
|
|
5706
|
-
const usage = await
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5886
|
+
const usage = (await Promise.race([
|
|
5887
|
+
source.totalUsage,
|
|
5888
|
+
new Promise((r) => setTimeout(() => r(undefined), 2_000)),
|
|
5889
|
+
]));
|
|
5890
|
+
if (usage) {
|
|
5891
|
+
turnUsage = usage;
|
|
5892
|
+
previousTurnUsage = usage;
|
|
5893
|
+
cumulativeUsage = addUsage(cumulativeUsage, usage);
|
|
5894
|
+
}
|
|
5710
5895
|
}
|
|
5711
5896
|
catch {
|
|
5712
5897
|
/* non-fatal */
|
|
@@ -6049,7 +6234,8 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
6049
6234
|
// run-list filter by chat works without the customer having to wire it
|
|
6050
6235
|
// up. Mirrors the browser-mediated `TriggerChatTransport.doStart` path.
|
|
6051
6236
|
const userTags = params.triggerConfig?.tags ?? options?.triggerConfig?.tags ?? [];
|
|
6052
|
-
|
|
6237
|
+
// Platform cap is 10 tags per run; the auto chat tag takes one slot.
|
|
6238
|
+
const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 10);
|
|
6053
6239
|
const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
|
|
6054
6240
|
const triggerConfig = {
|
|
6055
6241
|
basePayload: {
|
|
@@ -6347,8 +6533,19 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
6347
6533
|
// 2. Trim back to the previous turn-complete, if we have one. Skipping on
|
|
6348
6534
|
// first-turn-ever (or first turn post-OOM without a snapshot seed) is
|
|
6349
6535
|
// fine — the chain catches up next turn.
|
|
6350
|
-
|
|
6351
|
-
|
|
6536
|
+
//
|
|
6537
|
+
// Lazily create the slot if a caller reached here without one (a plain
|
|
6538
|
+
// `task()` driving `chat.createSession` / `chat.writeTurnComplete`, vs.
|
|
6539
|
+
// chatAgent/chatCustomAgent which seed it at boot). The first call then
|
|
6540
|
+
// does no trim (nothing before it) and records its seq; later calls trim
|
|
6541
|
+
// — so `.out` is bounded for every writeTurnComplete caller, not just the
|
|
6542
|
+
// built-in agents.
|
|
6543
|
+
let slot = locals_js_1.locals.get(lastTurnCompleteSeqNumKey);
|
|
6544
|
+
if (!slot) {
|
|
6545
|
+
slot = { value: undefined };
|
|
6546
|
+
locals_js_1.locals.set(lastTurnCompleteSeqNumKey, slot);
|
|
6547
|
+
}
|
|
6548
|
+
const prev = slot.value;
|
|
6352
6549
|
if (slot && prev !== undefined) {
|
|
6353
6550
|
try {
|
|
6354
6551
|
await session.out.trimTo(prev);
|