@trigger.dev/sdk 4.5.0-rc.4 → 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/imports/ai-runtime-cjs.cjs.map +1 -0
- package/dist/commonjs/imports/ai-runtime.d.ts +1 -0
- package/dist/commonjs/imports/ai-runtime.js +27 -0
- package/dist/commonjs/v3/ai.d.ts +17 -2
- package/dist/commonjs/v3/ai.js +349 -139
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/aiAutoTelemetry.d.ts +2 -0
- package/dist/commonjs/v3/aiAutoTelemetry.js +81 -0
- package/dist/commonjs/v3/aiAutoTelemetry.js.map +1 -0
- package/dist/commonjs/v3/chat-client.js +8 -3
- 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-server.d.ts +29 -6
- package/dist/commonjs/v3/chat-server.js +6 -4
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +11 -0
- package/dist/commonjs/v3/chat.js +95 -7
- 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/shared.js +17 -9
- package/dist/commonjs/v3/shared.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/imports/ai-runtime.d.ts +2 -0
- package/dist/esm/imports/ai-runtime.js +16 -0
- package/dist/esm/imports/ai-runtime.js.map +1 -0
- package/dist/esm/v3/ai.d.ts +17 -2
- package/dist/esm/v3/ai.js +319 -110
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/aiAutoTelemetry.d.ts +2 -0
- package/dist/esm/v3/aiAutoTelemetry.js +78 -0
- package/dist/esm/v3/aiAutoTelemetry.js.map +1 -0
- package/dist/esm/v3/chat-client.js +6 -1
- 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-server.d.ts +29 -6
- package/dist/esm/v3/chat-server.js +3 -1
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +11 -0
- package/dist/esm/v3/chat.js +95 -7
- 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/shared.js +18 -10
- package/dist/esm/v3/shared.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 +11 -5
package/dist/esm/v3/ai.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { accessoryAttributes, apiClientManager, getSchemaParseFn, headerValue, InputStreamOncePromise, isSchemaZodEsque, logger, ManualWaitpointPromise, OutOfMemoryError, sessionStreams, SemanticInternalAttributes, taskContext, SESSION_IN_EVENT_ID_HEADER, TRIGGER_CONTROL_SUBTYPE, generateJWT, } from "@trigger.dev/core/v3";
|
|
2
|
-
|
|
1
|
+
import { accessoryAttributes, apiClientManager, controlSubtype, getSchemaParseFn, headerValue, InputStreamOncePromise, isSchemaZodEsque, logger, ManualWaitpointPromise, OutOfMemoryError, sessionStreams, SemanticInternalAttributes, taskContext, SESSION_IN_EVENT_ID_HEADER, TRIGGER_CONTROL_SUBTYPE, generateJWT, } from "@trigger.dev/core/v3";
|
|
2
|
+
// Runtime VALUES go through the ESM/CJS shim so the CJS build can `require`
|
|
3
|
+
// ESM-only `ai@7` (see ../imports/ai-runtime.ts).
|
|
4
|
+
import { convertToModelMessages, dynamicTool, generateId as generateMessageId, getToolName, isToolUIPart, jsonSchema, readUIMessageStream, tool as aiTool, zodSchema, } from "../imports/ai-runtime.js";
|
|
3
5
|
import { trace } from "@opentelemetry/api";
|
|
4
6
|
import { auth } from "./auth.js";
|
|
5
7
|
import { locals } from "./locals.js";
|
|
@@ -16,6 +18,7 @@ import { runBashInSkill, readFileInSkill } from "./agentSkillsRuntime.js";
|
|
|
16
18
|
import { markChatAgentRunForStreamsWarning } from "./streams.js";
|
|
17
19
|
import { sessions, } from "./sessions.js";
|
|
18
20
|
import { createTask } from "./shared.js";
|
|
21
|
+
import { ensureAiSdkTelemetry } from "./aiAutoTelemetry.js";
|
|
19
22
|
import { resourceCatalog } from "@trigger.dev/core/v3";
|
|
20
23
|
import { tracer } from "./tracer.js";
|
|
21
24
|
const METADATA_KEY = "tool.execute.options";
|
|
@@ -65,51 +68,41 @@ const lastTurnCompleteSeqNumKey = locals.create("chat.lastTurnCompleteSeqNum");
|
|
|
65
68
|
* the `.in` subscription so already-processed user messages don't get
|
|
66
69
|
* replayed from S2.
|
|
67
70
|
*
|
|
68
|
-
* Implementation
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
71
|
+
* Implementation is a non-blocking records read (`wait=0`) — the
|
|
72
|
+
* endpoint returns everything currently stored (including pre-trim
|
|
73
|
+
* records, since S2 trims are eventually consistent) in one shot, and
|
|
74
|
+
* we keep the LAST matching header. The previous SSE-based scan had to
|
|
75
|
+
* idle-wait a full 5s window to know it reached the tail, which put a
|
|
76
|
+
* constant ~6s tax on every continuation boot.
|
|
74
77
|
*
|
|
75
78
|
* Returns `undefined` if no `turn-complete` carrying the header has been
|
|
76
79
|
* written yet — first-turn-ever, first turn post-OOM-with-no-prior-runs,
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* messages.
|
|
80
|
+
* a `turn-complete` written before this header existed, or a server old
|
|
81
|
+
* enough that the records endpoint doesn't serialize headers. Callers
|
|
82
|
+
* fall back to subscribing `.in` from seq 0 in that case; the slim-wire
|
|
83
|
+
* merge handles any dedup against snapshot-restored messages.
|
|
81
84
|
* @internal
|
|
82
85
|
*/
|
|
83
86
|
async function findLatestSessionInCursor(chatId) {
|
|
84
87
|
const apiClient = apiClientManager.clientOrThrow();
|
|
88
|
+
const response = await apiClient.readSessionStreamRecords(chatId, "out");
|
|
85
89
|
let latestCursor;
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (event.subtype !== TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
96
|
-
return;
|
|
97
|
-
const raw = headerValue(event.headers, SESSION_IN_EVENT_ID_HEADER);
|
|
98
|
-
if (!raw)
|
|
99
|
-
return;
|
|
100
|
-
const parsed = Number.parseInt(raw, 10);
|
|
101
|
-
if (Number.isFinite(parsed))
|
|
102
|
-
latestCursor = parsed;
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
// Drain the stream so the underlying SSE reader runs to completion. We
|
|
106
|
-
// don't accumulate chunks; `onControl` fires inline as turn-complete
|
|
107
|
-
// records arrive.
|
|
108
|
-
for await (const _ of stream) {
|
|
109
|
-
// intentionally empty
|
|
90
|
+
for (const record of response.records) {
|
|
91
|
+
if (controlSubtype(record.headers) !== TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
92
|
+
continue;
|
|
93
|
+
const raw = headerValue(record.headers, SESSION_IN_EVENT_ID_HEADER);
|
|
94
|
+
if (!raw)
|
|
95
|
+
continue;
|
|
96
|
+
const parsed = Number.parseInt(raw, 10);
|
|
97
|
+
if (Number.isFinite(parsed))
|
|
98
|
+
latestCursor = parsed;
|
|
110
99
|
}
|
|
111
100
|
return latestCursor;
|
|
112
101
|
}
|
|
102
|
+
/** Test-only entry point for the records-based cursor scan. @internal */
|
|
103
|
+
export async function __findLatestSessionInCursorForTests(chatId) {
|
|
104
|
+
return findLatestSessionInCursor(chatId);
|
|
105
|
+
}
|
|
113
106
|
let readChatSnapshotImpl;
|
|
114
107
|
export function __setReadChatSnapshotImplForTests(impl) {
|
|
115
108
|
readChatSnapshotImpl = impl;
|
|
@@ -641,9 +634,14 @@ function createTaskToolExecuteHandler(task) {
|
|
|
641
634
|
const toolMeta = {
|
|
642
635
|
toolCallId: toolOpts?.toolCallId ?? "",
|
|
643
636
|
};
|
|
644
|
-
|
|
637
|
+
// v6 passes user context as `experimental_context`, v7 as `context`. Read
|
|
638
|
+
// whichever is set and stamp both so subtasks reading either name work.
|
|
639
|
+
const toolContext = toolOpts?.context ?? toolOpts?.experimental_context;
|
|
640
|
+
if (toolContext !== undefined) {
|
|
645
641
|
try {
|
|
646
|
-
|
|
642
|
+
const serialized = JSON.parse(JSON.stringify(toolContext));
|
|
643
|
+
toolMeta.experimental_context = serialized;
|
|
644
|
+
toolMeta.context = serialized;
|
|
647
645
|
}
|
|
648
646
|
catch {
|
|
649
647
|
/* non-serializable */
|
|
@@ -972,8 +970,15 @@ const messagesInput = {
|
|
|
972
970
|
on(handler) {
|
|
973
971
|
return getChatSession().in.on((chunk) => {
|
|
974
972
|
if (chunk.kind === "message") {
|
|
975
|
-
|
|
973
|
+
// Returning `true` marks the record CONSUMED at the manager level:
|
|
974
|
+
// it is neither buffered for a later `once()` nor re-delivered by
|
|
975
|
+
// the buffer drain when the next turn re-attaches its handler.
|
|
976
|
+
// Without this, a message arriving mid-stream was delivered twice
|
|
977
|
+
// and ran a duplicate turn.
|
|
978
|
+
void Promise.resolve(handler(chunk.payload)).catch(() => { });
|
|
979
|
+
return true;
|
|
976
980
|
}
|
|
981
|
+
return undefined;
|
|
977
982
|
});
|
|
978
983
|
},
|
|
979
984
|
once(options) {
|
|
@@ -1067,8 +1072,13 @@ const stopInput = {
|
|
|
1067
1072
|
on(handler) {
|
|
1068
1073
|
return getChatSession().in.on((chunk) => {
|
|
1069
1074
|
if (chunk.kind === "stop") {
|
|
1070
|
-
|
|
1075
|
+
// Consume stop records (see the messages facade above). A stop is
|
|
1076
|
+
// only meaningful to the turn it interrupts — buffering it would
|
|
1077
|
+
// let a stale stop abort a future turn.
|
|
1078
|
+
void Promise.resolve(handler({ stop: true, message: chunk.message })).catch(() => { });
|
|
1079
|
+
return true;
|
|
1071
1080
|
}
|
|
1081
|
+
return undefined;
|
|
1072
1082
|
});
|
|
1073
1083
|
},
|
|
1074
1084
|
once(options) {
|
|
@@ -1222,6 +1232,9 @@ const chatHandoverIsFinalKey = locals.create("chat.handoverIsFinal");
|
|
|
1222
1232
|
* `tool-approval-response` rows are AI-SDK-internal and don't need a
|
|
1223
1233
|
* UIMessage representation. We map:
|
|
1224
1234
|
* - `text` parts → `{ type: "text", text }`
|
|
1235
|
+
* - `reasoning` parts → `{ type: "reasoning", text, state: "done" }`
|
|
1236
|
+
* (provider metadata carried so an Anthropic thinking signature
|
|
1237
|
+
* survives a UIMessage → ModelMessage round trip)
|
|
1225
1238
|
* - `tool-call` parts → `{ type: "tool-${name}", toolCallId,
|
|
1226
1239
|
* state: "input-available", input }`
|
|
1227
1240
|
* - `tool-approval-request` parts → skipped (AI SDK derives the
|
|
@@ -1238,6 +1251,14 @@ function synthesizeHandoverUIMessage(partial, messageId) {
|
|
|
1238
1251
|
if (part.type === "text" && typeof part.text === "string") {
|
|
1239
1252
|
parts.push({ type: "text", text: part.text });
|
|
1240
1253
|
}
|
|
1254
|
+
else if (part.type === "reasoning" && typeof part.text === "string") {
|
|
1255
|
+
parts.push({
|
|
1256
|
+
type: "reasoning",
|
|
1257
|
+
text: part.text,
|
|
1258
|
+
state: "done",
|
|
1259
|
+
...(part.providerOptions ? { providerMetadata: part.providerOptions } : {}),
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1241
1262
|
else if (part.type === "tool-call" && part.toolCallId && part.toolName) {
|
|
1242
1263
|
parts.push({
|
|
1243
1264
|
type: `tool-${part.toolName}`,
|
|
@@ -2582,6 +2603,11 @@ function chatCustomAgent(options) {
|
|
|
2582
2603
|
// No client-side upsert needed.
|
|
2583
2604
|
locals.set(chatSessionHandleKey, sessions.open(payload.chatId));
|
|
2584
2605
|
locals.set(chatAgentRunContextKey, runOptions.ctx);
|
|
2606
|
+
// Initialize the turn-complete trim slot so `chat.writeTurnComplete`
|
|
2607
|
+
// trims `session.out` back to the previous turn boundary. Without
|
|
2608
|
+
// this the slot is undefined and the trim never runs, so `.out`
|
|
2609
|
+
// grows without bound for the whole custom-agent surface.
|
|
2610
|
+
locals.set(lastTurnCompleteSeqNumKey, { value: undefined });
|
|
2585
2611
|
markChatAgentRunForStreamsWarning();
|
|
2586
2612
|
taskContext.setConversationId(payload.chatId);
|
|
2587
2613
|
stampConversationIdOnActiveSpan(payload.chatId);
|
|
@@ -2615,6 +2641,11 @@ function chatAgent(options) {
|
|
|
2615
2641
|
agentConfig: { type: "ai-sdk-chat" },
|
|
2616
2642
|
run: async (payload, { signal: runSignal, ctx }) => {
|
|
2617
2643
|
locals.set(chatAgentRunContextKey, ctx);
|
|
2644
|
+
// On AI SDK 7, register the `@ai-sdk/otel` integration (once per process)
|
|
2645
|
+
// so `experimental_telemetry` spans flow into the run trace. Awaited here
|
|
2646
|
+
// at run boot — before any `streamText` — and a no-op on v5/v6 or when the
|
|
2647
|
+
// optional `@ai-sdk/otel` peer isn't installed. See ./aiAutoTelemetry.ts.
|
|
2648
|
+
await ensureAiSdkTelemetry();
|
|
2618
2649
|
// Bind the run to its backing Session so every module-level helper
|
|
2619
2650
|
// (chat.stream, chat.messages, chat.stopSignal) resolves to this
|
|
2620
2651
|
// chat's `.in` / `.out` channels.
|
|
@@ -2702,6 +2733,11 @@ function chatAgent(options) {
|
|
|
2702
2733
|
// `messagesInput.waitWithIdleTimeout` so recovered turns fire first.
|
|
2703
2734
|
const bootInjectedQueue = [];
|
|
2704
2735
|
const couldHavePriorState = payload.continuation === true || ctx.attempt.number > 1;
|
|
2736
|
+
// `.in` resume cursor, computed at most once per boot. The boot
|
|
2737
|
+
// block below resolves it (snapshot field or records scan) and the
|
|
2738
|
+
// resume-cursor block reuses it instead of re-scanning.
|
|
2739
|
+
let bootInCursor;
|
|
2740
|
+
let bootInCursorResolved = false;
|
|
2705
2741
|
if (!hydrateMessages && couldHavePriorState) {
|
|
2706
2742
|
// Single parent span for the whole boot read phase — snapshot
|
|
2707
2743
|
// read, session.out replay, session.in replay. Per-phase timing
|
|
@@ -2737,23 +2773,28 @@ function chatAgent(options) {
|
|
|
2737
2773
|
slot.value = seeded;
|
|
2738
2774
|
}
|
|
2739
2775
|
}
|
|
2740
|
-
//
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2776
|
+
// The `.out` replay and the `.in` cursor + tail read are
|
|
2777
|
+
// independent (both depend only on the snapshot) — run them
|
|
2778
|
+
// concurrently. Each phase keeps its own catch + duration
|
|
2779
|
+
// attribute.
|
|
2780
|
+
const replayOutPhase = async () => {
|
|
2781
|
+
const replayOutStart = Date.now();
|
|
2782
|
+
try {
|
|
2783
|
+
const replayResult = await replaySessionOutTail(sessionIdForSnapshot, { lastEventId: bootSnapshot?.lastOutEventId });
|
|
2784
|
+
replayedSettled = replayResult.settled;
|
|
2785
|
+
replayedPartial = replayResult.partial;
|
|
2786
|
+
replayedPartialRaw = replayResult.partialRaw;
|
|
2787
|
+
}
|
|
2788
|
+
catch (error) {
|
|
2789
|
+
logger.warn("chat.agent: session.out replay failed; using snapshot only", {
|
|
2790
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2791
|
+
sessionId: sessionIdForSnapshot,
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
bootSpan.setAttribute("chat.boot.replay.out.durationMs", Date.now() - replayOutStart);
|
|
2795
|
+
bootSpan.setAttribute("chat.boot.replay.out.settledCount", replayedSettled.length);
|
|
2796
|
+
bootSpan.setAttribute("chat.boot.replay.out.partialPresent", replayedPartial !== undefined);
|
|
2797
|
+
};
|
|
2757
2798
|
// session.in tail read
|
|
2758
2799
|
//
|
|
2759
2800
|
// session.in carries the user-side of the conversation
|
|
@@ -2764,20 +2805,43 @@ function chatAgent(options) {
|
|
|
2764
2805
|
// visible via the live SSE subscription — by which point they
|
|
2765
2806
|
// would arrive AFTER the partial-assistant orphan and look like
|
|
2766
2807
|
// brand-new turns to the model, producing inverted chains.
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2808
|
+
//
|
|
2809
|
+
// The cursor comes from the snapshot when present (written
|
|
2810
|
+
// there since `lastInEventId` was added) — otherwise from a
|
|
2811
|
+
// records scan of `.out`'s latest turn-complete header.
|
|
2812
|
+
const replayInPhase = async () => {
|
|
2813
|
+
const replayInStart = Date.now();
|
|
2814
|
+
const snapshotInCursor = bootSnapshot?.lastInEventId !== undefined
|
|
2815
|
+
? Number.parseInt(bootSnapshot.lastInEventId, 10)
|
|
2816
|
+
: undefined;
|
|
2817
|
+
if (snapshotInCursor !== undefined && Number.isFinite(snapshotInCursor)) {
|
|
2818
|
+
bootInCursor = snapshotInCursor;
|
|
2819
|
+
bootInCursorResolved = true;
|
|
2820
|
+
}
|
|
2821
|
+
else {
|
|
2822
|
+
try {
|
|
2823
|
+
bootInCursor = await findLatestSessionInCursor(payload.chatId);
|
|
2824
|
+
bootInCursorResolved = true;
|
|
2825
|
+
}
|
|
2826
|
+
catch {
|
|
2827
|
+
// Transient scan failure: leave unresolved so the
|
|
2828
|
+
// resume-cursor block below retries the lookup.
|
|
2829
|
+
bootInCursor = undefined;
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
bootSpan.setAttribute("chat.boot.replay.in.cursorFromSnapshot", snapshotInCursor !== undefined);
|
|
2833
|
+
try {
|
|
2834
|
+
replayedInTail = await replaySessionInTail(payload.chatId, {
|
|
2835
|
+
lastEventId: bootInCursor !== undefined ? String(bootInCursor) : undefined,
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
catch (error) {
|
|
2839
|
+
logger.warn("chat.agent: session.in replay failed; in-flight users may not be recovered", { error: error instanceof Error ? error.message : String(error) });
|
|
2840
|
+
}
|
|
2841
|
+
bootSpan.setAttribute("chat.boot.replay.in.durationMs", Date.now() - replayInStart);
|
|
2842
|
+
bootSpan.setAttribute("chat.boot.replay.in.userCount", replayedInTail.length);
|
|
2843
|
+
};
|
|
2844
|
+
await Promise.all([replayOutPhase(), replayInPhase()]);
|
|
2781
2845
|
}, {
|
|
2782
2846
|
attributes: {
|
|
2783
2847
|
[SemanticInternalAttributes.STYLE_ICON]: "tabler-rotate-clockwise",
|
|
@@ -2818,7 +2882,12 @@ function chatAgent(options) {
|
|
|
2818
2882
|
bootSnapshot !== undefined;
|
|
2819
2883
|
if (needsResumeCursor) {
|
|
2820
2884
|
try {
|
|
2821
|
-
|
|
2885
|
+
// Reuse the cursor the boot block already resolved (snapshot
|
|
2886
|
+
// field or records scan) — only scan here when the boot block
|
|
2887
|
+
// was skipped (hydrateMessages, or snapshot-only signals).
|
|
2888
|
+
const cursor = bootInCursorResolved
|
|
2889
|
+
? bootInCursor
|
|
2890
|
+
: await findLatestSessionInCursor(payload.chatId);
|
|
2822
2891
|
if (cursor !== undefined) {
|
|
2823
2892
|
sessionStreams.setLastSeqNum(payload.chatId, "in", cursor);
|
|
2824
2893
|
sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor);
|
|
@@ -3582,6 +3651,18 @@ function chatAgent(options) {
|
|
|
3582
3651
|
// therefore a delta merge, not a full-history reset.
|
|
3583
3652
|
if (currentWirePayload.trigger !== "action") {
|
|
3584
3653
|
let cleanedUIMessages = cleanedIncomingMessages;
|
|
3654
|
+
// Turn-0 head-start with hydrateMessages: the boot seeding from
|
|
3655
|
+
// `payload.headStartMessages` is non-hydrate-only, so ship the
|
|
3656
|
+
// route handler's first-turn history to the hydrate hook as
|
|
3657
|
+
// incoming messages instead (gated on the pending handover).
|
|
3658
|
+
if (turn === 0 &&
|
|
3659
|
+
hydrateMessages &&
|
|
3660
|
+
cleanedUIMessages.length === 0 &&
|
|
3661
|
+
(locals.get(chatHandoverPartialKey)?.length ?? 0) > 0 &&
|
|
3662
|
+
Array.isArray(payload.headStartMessages) &&
|
|
3663
|
+
payload.headStartMessages.length > 0) {
|
|
3664
|
+
cleanedUIMessages = payload.headStartMessages;
|
|
3665
|
+
}
|
|
3585
3666
|
// Validate/transform UIMessages before conversion — catches malformed
|
|
3586
3667
|
// messages from storage or untrusted input before they reach the model.
|
|
3587
3668
|
// Slim wire: triggers like `regenerate-message` carry no incoming
|
|
@@ -3760,40 +3841,47 @@ function chatAgent(options) {
|
|
|
3760
3841
|
// `preload` / `close` / `handover-prepare` and submits
|
|
3761
3842
|
// with no incoming message fall through with the boot-
|
|
3762
3843
|
// seeded accumulator unchanged.
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3844
|
+
}
|
|
3845
|
+
if (turn === 0) {
|
|
3846
|
+
// Head-start handover splice (turn 0 only, BOTH
|
|
3847
|
+
// accumulation branches — hydrate and default): the
|
|
3848
|
+
// `chat.handover` route handler signalled a mid-turn
|
|
3849
|
+
// handover, so splice its partial assistant response
|
|
3850
|
+
// (text + pending tool-calls + the synthesized
|
|
3851
|
+
// tool-approval round) onto the accumulator.
|
|
3852
|
+
// `streamText` then hits AI SDK's initial-tool-
|
|
3853
|
+
// execution branch, runs the agent-side tool executes,
|
|
3854
|
+
// and resumes from step 2 — skipping the first model
|
|
3855
|
+
// call (already done by the handler).
|
|
3856
|
+
//
|
|
3857
|
+
// We also synthesize a UIMessage form of the partial
|
|
3858
|
+
// assistant and push it to `accumulatedUIMessages` so
|
|
3859
|
+
// AI SDK's `processUIMessageStream` (invoked when the
|
|
3860
|
+
// run loop calls `runResult.toUIMessageStream({
|
|
3861
|
+
// onFinish })`) can initialize `state.message` from
|
|
3862
|
+
// the trailing assistant in `originalMessages`. Without
|
|
3863
|
+
// that, the `tool-output-available` chunks emitted by
|
|
3864
|
+
// the initial-tool-execution branch can't find their
|
|
3865
|
+
// matching tool-call in state and AI SDK throws
|
|
3866
|
+
// `UIMessageStreamError: No tool invocation found`.
|
|
3867
|
+
const pendingHandoverPartial = locals.get(chatHandoverPartialKey);
|
|
3868
|
+
if (pendingHandoverPartial && pendingHandoverPartial.length > 0) {
|
|
3869
|
+
const handoverMessageId = locals.get(chatHandoverMessageIdKey);
|
|
3870
|
+
// Skip if the hydrated chain already persisted the
|
|
3871
|
+
// partial under the handover messageId.
|
|
3872
|
+
const alreadyInChain = handoverMessageId !== undefined &&
|
|
3873
|
+
accumulatedUIMessages.some((m) => m.id === handoverMessageId);
|
|
3874
|
+
if (!alreadyInChain) {
|
|
3786
3875
|
accumulatedMessages.push(...pendingHandoverPartial);
|
|
3787
|
-
const handoverMessageId = locals.get(chatHandoverMessageIdKey);
|
|
3788
3876
|
const partialUI = synthesizeHandoverUIMessage(pendingHandoverPartial, handoverMessageId);
|
|
3789
3877
|
if (partialUI) {
|
|
3790
3878
|
accumulatedUIMessages.push(partialUI);
|
|
3791
3879
|
}
|
|
3792
|
-
locals.set(chatHandoverPartialKey, []); // consume once
|
|
3793
3880
|
}
|
|
3881
|
+
locals.set(chatHandoverPartialKey, []); // consume once
|
|
3794
3882
|
}
|
|
3795
|
-
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
3796
3883
|
}
|
|
3884
|
+
locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
3797
3885
|
} // end if (trigger !== "action")
|
|
3798
3886
|
// ── Action result handling ──────────────────────────────
|
|
3799
3887
|
// For action turns, skip the turn machinery entirely.
|
|
@@ -4490,11 +4578,15 @@ function chatAgent(options) {
|
|
|
4490
4578
|
if (!hydrateMessages) {
|
|
4491
4579
|
try {
|
|
4492
4580
|
await tracer.startActiveSpan("snapshot.write", async () => {
|
|
4581
|
+
const snapshotInCursor = getChatSession().in.lastDispatchedSeqNum();
|
|
4493
4582
|
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
4494
4583
|
version: 1,
|
|
4495
4584
|
savedAt: Date.now(),
|
|
4496
4585
|
messages: accumulatedUIMessages,
|
|
4497
4586
|
lastOutEventId: turnCompleteResult?.lastEventId,
|
|
4587
|
+
lastInEventId: snapshotInCursor !== undefined
|
|
4588
|
+
? String(snapshotInCursor)
|
|
4589
|
+
: undefined,
|
|
4498
4590
|
});
|
|
4499
4591
|
}, {
|
|
4500
4592
|
attributes: {
|
|
@@ -4631,17 +4723,100 @@ function chatAgent(options) {
|
|
|
4631
4723
|
if (turnError instanceof OutOfMemoryError) {
|
|
4632
4724
|
throw turnError;
|
|
4633
4725
|
}
|
|
4726
|
+
let errorTurnCompleteResult;
|
|
4634
4727
|
try {
|
|
4635
4728
|
await withChatWriter(async (writer) => {
|
|
4636
4729
|
const errorText = turnError instanceof Error ? turnError.message : "An unexpected error occurred";
|
|
4637
4730
|
writer.write({ type: "error", errorText });
|
|
4638
4731
|
});
|
|
4639
4732
|
// Signal turn complete so the client knows this turn is done
|
|
4640
|
-
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4733
|
+
errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4641
4734
|
}
|
|
4642
4735
|
catch {
|
|
4643
4736
|
// Best-effort — if stream write fails, let the run continue anyway
|
|
4644
4737
|
}
|
|
4738
|
+
// The submit-message merge into the accumulator may not have run
|
|
4739
|
+
// yet (a pre-run hook threw), so fold the wire message in for the
|
|
4740
|
+
// error event + snapshot — the cursor has already advanced past it,
|
|
4741
|
+
// so otherwise it survives in neither the snapshot nor the `.in` tail.
|
|
4742
|
+
const erroredWireMessage = currentWirePayload.message;
|
|
4743
|
+
const erroredUIMessages = erroredWireMessage &&
|
|
4744
|
+
!accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id)
|
|
4745
|
+
? [...accumulatedUIMessages, erroredWireMessage]
|
|
4746
|
+
: accumulatedUIMessages;
|
|
4747
|
+
// Fire onTurnComplete on the error path too — the docs promise it
|
|
4748
|
+
// runs "after every turn, successful or errored" so customers can
|
|
4749
|
+
// mark the turn failed. `responseMessage` is undefined/partial and
|
|
4750
|
+
// `error` carries the thrown value.
|
|
4751
|
+
if (onTurnComplete) {
|
|
4752
|
+
try {
|
|
4753
|
+
await tracer.startActiveSpan("onTurnComplete()", async () => {
|
|
4754
|
+
await onTurnComplete({
|
|
4755
|
+
ctx,
|
|
4756
|
+
chatId: currentWirePayload.chatId,
|
|
4757
|
+
messages: accumulatedMessages,
|
|
4758
|
+
uiMessages: erroredUIMessages,
|
|
4759
|
+
newMessages: [],
|
|
4760
|
+
newUIMessages: erroredWireMessage ? [erroredWireMessage] : [],
|
|
4761
|
+
responseMessage: undefined,
|
|
4762
|
+
rawResponseMessage: undefined,
|
|
4763
|
+
turn,
|
|
4764
|
+
runId: ctx.run.id,
|
|
4765
|
+
chatAccessToken: "",
|
|
4766
|
+
// Parsed `clientData` isn't reliably in scope here (parsing
|
|
4767
|
+
// may itself be the failure), and the raw metadata is the
|
|
4768
|
+
// wrong shape — leave it undefined on the error path.
|
|
4769
|
+
clientData: undefined,
|
|
4770
|
+
stopped: false,
|
|
4771
|
+
continuation,
|
|
4772
|
+
previousRunId,
|
|
4773
|
+
preloaded,
|
|
4774
|
+
totalUsage: cumulativeUsage,
|
|
4775
|
+
finishReason: "error",
|
|
4776
|
+
error: turnError,
|
|
4777
|
+
lastEventId: errorTurnCompleteResult?.lastEventId,
|
|
4778
|
+
});
|
|
4779
|
+
}, {
|
|
4780
|
+
attributes: {
|
|
4781
|
+
[SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete",
|
|
4782
|
+
[SemanticInternalAttributes.COLLAPSED]: true,
|
|
4783
|
+
"chat.id": currentWirePayload.chatId,
|
|
4784
|
+
"chat.turn": turn + 1,
|
|
4785
|
+
"chat.errored": true,
|
|
4786
|
+
},
|
|
4787
|
+
});
|
|
4788
|
+
}
|
|
4789
|
+
catch {
|
|
4790
|
+
// A throwing onTurnComplete on the error path must not crash
|
|
4791
|
+
// the run — keep the conversation alive for the next message.
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
// Persist a snapshot so the failed turn's user message isn't
|
|
4795
|
+
// stranded. `writeTurnCompleteChunk` already advanced the `.in`
|
|
4796
|
+
// cursor past it (via the session-in-event-id header), and the
|
|
4797
|
+
// success-path snapshot write is skipped on error — without this
|
|
4798
|
+
// the next boot would resume past a message that exists in
|
|
4799
|
+
// neither the snapshot nor the replayable `.in` tail.
|
|
4800
|
+
if (!hydrateMessages) {
|
|
4801
|
+
try {
|
|
4802
|
+
const errorSnapshotInCursor = getChatSession().in.lastDispatchedSeqNum();
|
|
4803
|
+
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
4804
|
+
version: 1,
|
|
4805
|
+
savedAt: Date.now(),
|
|
4806
|
+
messages: erroredUIMessages,
|
|
4807
|
+
lastOutEventId: errorTurnCompleteResult?.lastEventId,
|
|
4808
|
+
lastInEventId: errorSnapshotInCursor !== undefined
|
|
4809
|
+
? String(errorSnapshotInCursor)
|
|
4810
|
+
: undefined,
|
|
4811
|
+
});
|
|
4812
|
+
}
|
|
4813
|
+
catch (error) {
|
|
4814
|
+
logger.warn("chat.agent: error-path snapshot write failed", {
|
|
4815
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4816
|
+
sessionId: sessionIdForSnapshot,
|
|
4817
|
+
});
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4645
4820
|
// chat.requestUpgrade() / chat.endRun() — exit after error turn too
|
|
4646
4821
|
if (locals.get(chatUpgradeRequestedKey) ||
|
|
4647
4822
|
locals.get(chatEndRunRequestedKey)) {
|
|
@@ -5507,18 +5682,32 @@ function createChatSession(payload, options) {
|
|
|
5507
5682
|
return {
|
|
5508
5683
|
async next() {
|
|
5509
5684
|
turn++;
|
|
5510
|
-
// First turn:
|
|
5511
|
-
|
|
5685
|
+
// First turn: wait when the boot payload carries no message.
|
|
5686
|
+
// Preload boots wait for the first real message; continuation
|
|
5687
|
+
// boots (fresh run via `ensureRunForSession` / end-and-continue)
|
|
5688
|
+
// arrive with the sticky boot-payload fields stripped, so running
|
|
5689
|
+
// a turn immediately would invoke the model with no user input.
|
|
5690
|
+
const isMessagelessContinuationBoot = currentPayload.continuation === true && !currentPayload.message;
|
|
5691
|
+
if (turn === 0 && (currentPayload.trigger === "preload" || isMessagelessContinuationBoot)) {
|
|
5512
5692
|
const result = await messagesInput.waitWithIdleTimeout({
|
|
5513
5693
|
idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30,
|
|
5514
5694
|
timeout,
|
|
5515
|
-
spanName:
|
|
5695
|
+
spanName: currentPayload.trigger === "preload"
|
|
5696
|
+
? "waiting for first message"
|
|
5697
|
+
: "waiting for first message (continuation)",
|
|
5516
5698
|
});
|
|
5517
5699
|
if (!result.ok || runSignal.aborted) {
|
|
5518
5700
|
stop.cleanup();
|
|
5519
5701
|
return { done: true, value: undefined };
|
|
5520
5702
|
}
|
|
5703
|
+
const continuationBoot = isMessagelessContinuationBoot;
|
|
5521
5704
|
currentPayload = result.output;
|
|
5705
|
+
// Preserve the continuation flag — the wire payload of the next
|
|
5706
|
+
// message doesn't carry it, and `turn.continuation` is how the
|
|
5707
|
+
// user knows to seed history (e.g. `turn.setMessages(stored)`).
|
|
5708
|
+
if (continuationBoot && currentPayload.continuation === undefined) {
|
|
5709
|
+
currentPayload = { ...currentPayload, continuation: true };
|
|
5710
|
+
}
|
|
5522
5711
|
}
|
|
5523
5712
|
// Subsequent turns: wait for the next message
|
|
5524
5713
|
if (turn > 0) {
|
|
@@ -5671,14 +5860,22 @@ function createChatSession(payload, options) {
|
|
|
5671
5860
|
locals.set(chatResponsePartsKey, []);
|
|
5672
5861
|
}
|
|
5673
5862
|
}
|
|
5674
|
-
// Capture token usage from the streamText result
|
|
5863
|
+
// Capture token usage from the streamText result. Race with a 2s
|
|
5864
|
+
// timeout — on stop-abort the AI SDK's totalUsage promise can hang
|
|
5865
|
+
// indefinitely, which would wedge the turn loop (same guard as
|
|
5866
|
+
// chat.agent's turn loop).
|
|
5675
5867
|
let turnUsage;
|
|
5676
5868
|
if (typeof source.totalUsage?.then === "function") {
|
|
5677
5869
|
try {
|
|
5678
|
-
const usage = await
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5870
|
+
const usage = (await Promise.race([
|
|
5871
|
+
source.totalUsage,
|
|
5872
|
+
new Promise((r) => setTimeout(() => r(undefined), 2_000)),
|
|
5873
|
+
]));
|
|
5874
|
+
if (usage) {
|
|
5875
|
+
turnUsage = usage;
|
|
5876
|
+
previousTurnUsage = usage;
|
|
5877
|
+
cumulativeUsage = addUsage(cumulativeUsage, usage);
|
|
5878
|
+
}
|
|
5682
5879
|
}
|
|
5683
5880
|
catch {
|
|
5684
5881
|
/* non-fatal */
|
|
@@ -6021,7 +6218,8 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
6021
6218
|
// run-list filter by chat works without the customer having to wire it
|
|
6022
6219
|
// up. Mirrors the browser-mediated `TriggerChatTransport.doStart` path.
|
|
6023
6220
|
const userTags = params.triggerConfig?.tags ?? options?.triggerConfig?.tags ?? [];
|
|
6024
|
-
|
|
6221
|
+
// Platform cap is 10 tags per run; the auto chat tag takes one slot.
|
|
6222
|
+
const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 10);
|
|
6025
6223
|
const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
|
|
6026
6224
|
const triggerConfig = {
|
|
6027
6225
|
basePayload: {
|
|
@@ -6319,8 +6517,19 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
6319
6517
|
// 2. Trim back to the previous turn-complete, if we have one. Skipping on
|
|
6320
6518
|
// first-turn-ever (or first turn post-OOM without a snapshot seed) is
|
|
6321
6519
|
// fine — the chain catches up next turn.
|
|
6322
|
-
|
|
6323
|
-
|
|
6520
|
+
//
|
|
6521
|
+
// Lazily create the slot if a caller reached here without one (a plain
|
|
6522
|
+
// `task()` driving `chat.createSession` / `chat.writeTurnComplete`, vs.
|
|
6523
|
+
// chatAgent/chatCustomAgent which seed it at boot). The first call then
|
|
6524
|
+
// does no trim (nothing before it) and records its seq; later calls trim
|
|
6525
|
+
// — so `.out` is bounded for every writeTurnComplete caller, not just the
|
|
6526
|
+
// built-in agents.
|
|
6527
|
+
let slot = locals.get(lastTurnCompleteSeqNumKey);
|
|
6528
|
+
if (!slot) {
|
|
6529
|
+
slot = { value: undefined };
|
|
6530
|
+
locals.set(lastTurnCompleteSeqNumKey, slot);
|
|
6531
|
+
}
|
|
6532
|
+
const prev = slot.value;
|
|
6324
6533
|
if (slot && prev !== undefined) {
|
|
6325
6534
|
try {
|
|
6326
6535
|
await session.out.trimTo(prev);
|