@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/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;
|
|
@@ -12,7 +13,9 @@ exports.__setReplaySessionInTailImplForTests = __setReplaySessionInTailImplForTe
|
|
|
12
13
|
exports.__replaySessionInTailProductionPathForTests = __replaySessionInTailProductionPathForTests;
|
|
13
14
|
exports.buildSkillTools = buildSkillTools;
|
|
14
15
|
const v3_1 = require("@trigger.dev/core/v3");
|
|
15
|
-
|
|
16
|
+
// Runtime VALUES go through the ESM/CJS shim so the CJS build can `require`
|
|
17
|
+
// ESM-only `ai@7` (see ../imports/ai-runtime.ts).
|
|
18
|
+
const ai_runtime_js_1 = require("../imports/ai-runtime.js");
|
|
16
19
|
const api_1 = require("@opentelemetry/api");
|
|
17
20
|
const auth_js_1 = require("./auth.js");
|
|
18
21
|
const locals_js_1 = require("./locals.js");
|
|
@@ -29,6 +32,7 @@ const agentSkillsRuntime_js_1 = require("./agentSkillsRuntime.js");
|
|
|
29
32
|
const streams_js_1 = require("./streams.js");
|
|
30
33
|
const sessions_js_1 = require("./sessions.js");
|
|
31
34
|
const shared_js_1 = require("./shared.js");
|
|
35
|
+
const aiAutoTelemetry_js_1 = require("./aiAutoTelemetry.js");
|
|
32
36
|
const v3_2 = require("@trigger.dev/core/v3");
|
|
33
37
|
const tracer_js_1 = require("./tracer.js");
|
|
34
38
|
const METADATA_KEY = "tool.execute.options";
|
|
@@ -44,7 +48,7 @@ function toModelMessages(messages) {
|
|
|
44
48
|
// conditional spread keeps the options object byte-identical to the no-tools
|
|
45
49
|
// path when nothing was declared.
|
|
46
50
|
const tools = locals_js_1.locals.get(chatResolvedToolsKey);
|
|
47
|
-
return (0,
|
|
51
|
+
return (0, ai_runtime_js_1.convertToModelMessages)(messages, {
|
|
48
52
|
ignoreIncompleteToolCalls: true,
|
|
49
53
|
...(tools ? { tools } : {}),
|
|
50
54
|
});
|
|
@@ -78,51 +82,41 @@ const lastTurnCompleteSeqNumKey = locals_js_1.locals.create("chat.lastTurnComple
|
|
|
78
82
|
* the `.in` subscription so already-processed user messages don't get
|
|
79
83
|
* replayed from S2.
|
|
80
84
|
*
|
|
81
|
-
* Implementation
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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.
|
|
87
91
|
*
|
|
88
92
|
* Returns `undefined` if no `turn-complete` carrying the header has been
|
|
89
93
|
* written yet — first-turn-ever, first turn post-OOM-with-no-prior-runs,
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* 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.
|
|
94
98
|
* @internal
|
|
95
99
|
*/
|
|
96
100
|
async function findLatestSessionInCursor(chatId) {
|
|
97
101
|
const apiClient = v3_1.apiClientManager.clientOrThrow();
|
|
102
|
+
const response = await apiClient.readSessionStreamRecords(chatId, "out");
|
|
98
103
|
let latestCursor;
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (event.subtype !== v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
109
|
-
return;
|
|
110
|
-
const raw = (0, v3_1.headerValue)(event.headers, v3_1.SESSION_IN_EVENT_ID_HEADER);
|
|
111
|
-
if (!raw)
|
|
112
|
-
return;
|
|
113
|
-
const parsed = Number.parseInt(raw, 10);
|
|
114
|
-
if (Number.isFinite(parsed))
|
|
115
|
-
latestCursor = parsed;
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
// Drain the stream so the underlying SSE reader runs to completion. We
|
|
119
|
-
// don't accumulate chunks; `onControl` fires inline as turn-complete
|
|
120
|
-
// records arrive.
|
|
121
|
-
for await (const _ of stream) {
|
|
122
|
-
// 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;
|
|
123
113
|
}
|
|
124
114
|
return latestCursor;
|
|
125
115
|
}
|
|
116
|
+
/** Test-only entry point for the records-based cursor scan. @internal */
|
|
117
|
+
async function __findLatestSessionInCursorForTests(chatId) {
|
|
118
|
+
return findLatestSessionInCursor(chatId);
|
|
119
|
+
}
|
|
126
120
|
let readChatSnapshotImpl;
|
|
127
121
|
function __setReadChatSnapshotImplForTests(impl) {
|
|
128
122
|
readChatSnapshotImpl = impl;
|
|
@@ -451,7 +445,7 @@ async function replaySessionOutTail(sessionId, options) {
|
|
|
451
445
|
});
|
|
452
446
|
let last;
|
|
453
447
|
try {
|
|
454
|
-
for await (const snapshot of (0,
|
|
448
|
+
for await (const snapshot of (0, ai_runtime_js_1.readUIMessageStream)({ stream: segmentStream })) {
|
|
455
449
|
last = snapshot;
|
|
456
450
|
}
|
|
457
451
|
}
|
|
@@ -654,9 +648,14 @@ function createTaskToolExecuteHandler(task) {
|
|
|
654
648
|
const toolMeta = {
|
|
655
649
|
toolCallId: toolOpts?.toolCallId ?? "",
|
|
656
650
|
};
|
|
657
|
-
|
|
651
|
+
// v6 passes user context as `experimental_context`, v7 as `context`. Read
|
|
652
|
+
// whichever is set and stamp both so subtasks reading either name work.
|
|
653
|
+
const toolContext = toolOpts?.context ?? toolOpts?.experimental_context;
|
|
654
|
+
if (toolContext !== undefined) {
|
|
658
655
|
try {
|
|
659
|
-
|
|
656
|
+
const serialized = JSON.parse(JSON.stringify(toolContext));
|
|
657
|
+
toolMeta.experimental_context = serialized;
|
|
658
|
+
toolMeta.context = serialized;
|
|
660
659
|
}
|
|
661
660
|
catch {
|
|
662
661
|
/* non-serializable */
|
|
@@ -699,9 +698,9 @@ function toolFromTask(task, options) {
|
|
|
699
698
|
// Zod-backed tasks: use static `tool()` so runtime shape matches `ToolSet`. Generic task context
|
|
700
699
|
// prevents `tool()` overloads from inferring input; `as any` is localized to this call only.
|
|
701
700
|
if ("schema" in task && task.schema && (0, v3_1.isSchemaZodEsque)(task.schema)) {
|
|
702
|
-
const staticTool = (0,
|
|
701
|
+
const staticTool = (0, ai_runtime_js_1.tool)({
|
|
703
702
|
description: task.description ?? "",
|
|
704
|
-
inputSchema: (0,
|
|
703
|
+
inputSchema: (0, ai_runtime_js_1.zodSchema)(task.schema),
|
|
705
704
|
execute: async (input, toolOpts) => executeFromTaskInput(input, toolOpts),
|
|
706
705
|
...(options?.experimental_toToolResultContent !== undefined
|
|
707
706
|
? { experimental_toToolResultContent: options.experimental_toToolResultContent }
|
|
@@ -709,7 +708,7 @@ function toolFromTask(task, options) {
|
|
|
709
708
|
});
|
|
710
709
|
return staticTool;
|
|
711
710
|
}
|
|
712
|
-
const toolDefinition = (0,
|
|
711
|
+
const toolDefinition = (0, ai_runtime_js_1.dynamicTool)({
|
|
713
712
|
description: task.description,
|
|
714
713
|
inputSchema: convertTaskSchemaToToolParameters(task),
|
|
715
714
|
...(options?.experimental_toToolResultContent !== undefined
|
|
@@ -777,15 +776,15 @@ function convertTaskSchemaToToolParameters(task) {
|
|
|
777
776
|
if ("schema" in task) {
|
|
778
777
|
// If TaskSchema is ArkTypeEsque, use ai.jsonSchema to convert it to a Schema
|
|
779
778
|
if ("toJsonSchema" in task.schema && typeof task.schema.toJsonSchema === "function") {
|
|
780
|
-
return (0,
|
|
779
|
+
return (0, ai_runtime_js_1.jsonSchema)(task.schema.toJsonSchema());
|
|
781
780
|
}
|
|
782
781
|
// If TaskSchema is ZodEsque, use ai.zodSchema to convert it to a Schema
|
|
783
782
|
if ((0, v3_1.isSchemaZodEsque)(task.schema)) {
|
|
784
|
-
return (0,
|
|
783
|
+
return (0, ai_runtime_js_1.zodSchema)(task.schema);
|
|
785
784
|
}
|
|
786
785
|
}
|
|
787
786
|
if ("jsonSchema" in task) {
|
|
788
|
-
return (0,
|
|
787
|
+
return (0, ai_runtime_js_1.jsonSchema)(task.jsonSchema);
|
|
789
788
|
}
|
|
790
789
|
throw new Error("Cannot convert task to a tool. Make sure to use a task with a schema or jsonSchema.");
|
|
791
790
|
}
|
|
@@ -985,8 +984,15 @@ const messagesInput = {
|
|
|
985
984
|
on(handler) {
|
|
986
985
|
return getChatSession().in.on((chunk) => {
|
|
987
986
|
if (chunk.kind === "message") {
|
|
988
|
-
|
|
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;
|
|
989
994
|
}
|
|
995
|
+
return undefined;
|
|
990
996
|
});
|
|
991
997
|
},
|
|
992
998
|
once(options) {
|
|
@@ -1080,8 +1086,13 @@ const stopInput = {
|
|
|
1080
1086
|
on(handler) {
|
|
1081
1087
|
return getChatSession().in.on((chunk) => {
|
|
1082
1088
|
if (chunk.kind === "stop") {
|
|
1083
|
-
|
|
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;
|
|
1084
1094
|
}
|
|
1095
|
+
return undefined;
|
|
1085
1096
|
});
|
|
1086
1097
|
},
|
|
1087
1098
|
once(options) {
|
|
@@ -1235,6 +1246,9 @@ const chatHandoverIsFinalKey = locals_js_1.locals.create("chat.handoverIsFinal")
|
|
|
1235
1246
|
* `tool-approval-response` rows are AI-SDK-internal and don't need a
|
|
1236
1247
|
* UIMessage representation. We map:
|
|
1237
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)
|
|
1238
1252
|
* - `tool-call` parts → `{ type: "tool-${name}", toolCallId,
|
|
1239
1253
|
* state: "input-available", input }`
|
|
1240
1254
|
* - `tool-approval-request` parts → skipped (AI SDK derives the
|
|
@@ -1251,6 +1265,14 @@ function synthesizeHandoverUIMessage(partial, messageId) {
|
|
|
1251
1265
|
if (part.type === "text" && typeof part.text === "string") {
|
|
1252
1266
|
parts.push({ type: "text", text: part.text });
|
|
1253
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
|
+
}
|
|
1254
1276
|
else if (part.type === "tool-call" && part.toolCallId && part.toolName) {
|
|
1255
1277
|
parts.push({
|
|
1256
1278
|
type: `tool-${part.toolName}`,
|
|
@@ -1269,7 +1291,7 @@ function synthesizeHandoverUIMessage(partial, messageId) {
|
|
|
1269
1291
|
// browser). Fall back to a fresh id only if the handover signal
|
|
1270
1292
|
// didn't carry one.
|
|
1271
1293
|
return {
|
|
1272
|
-
id: messageId ?? (0,
|
|
1294
|
+
id: messageId ?? (0, ai_runtime_js_1.generateId)(),
|
|
1273
1295
|
role: "assistant",
|
|
1274
1296
|
parts,
|
|
1275
1297
|
};
|
|
@@ -1425,7 +1447,7 @@ function* iterateToolParts(message) {
|
|
|
1425
1447
|
if (message.role !== "assistant")
|
|
1426
1448
|
return;
|
|
1427
1449
|
for (const part of (message.parts ?? [])) {
|
|
1428
|
-
if (!(0,
|
|
1450
|
+
if (!(0, ai_runtime_js_1.isToolUIPart)(part))
|
|
1429
1451
|
continue;
|
|
1430
1452
|
const toolCallId = part.toolCallId;
|
|
1431
1453
|
if (typeof toolCallId !== "string" || toolCallId.length === 0)
|
|
@@ -1433,7 +1455,7 @@ function* iterateToolParts(message) {
|
|
|
1433
1455
|
yield {
|
|
1434
1456
|
part,
|
|
1435
1457
|
toolCallId,
|
|
1436
|
-
toolName: (0,
|
|
1458
|
+
toolName: (0, ai_runtime_js_1.getToolName)(part),
|
|
1437
1459
|
state: part.state,
|
|
1438
1460
|
};
|
|
1439
1461
|
}
|
|
@@ -1457,7 +1479,7 @@ function extractPendingToolCallsFromPartial(partial) {
|
|
|
1457
1479
|
const parts = (partial.parts ?? []);
|
|
1458
1480
|
for (let i = 0; i < parts.length; i++) {
|
|
1459
1481
|
const part = parts[i];
|
|
1460
|
-
if (!(0,
|
|
1482
|
+
if (!(0, ai_runtime_js_1.isToolUIPart)(part))
|
|
1461
1483
|
continue;
|
|
1462
1484
|
if (!isPendingToolState(part.state))
|
|
1463
1485
|
continue;
|
|
@@ -1466,7 +1488,7 @@ function extractPendingToolCallsFromPartial(partial) {
|
|
|
1466
1488
|
continue;
|
|
1467
1489
|
out.push({
|
|
1468
1490
|
toolCallId,
|
|
1469
|
-
toolName: (0,
|
|
1491
|
+
toolName: (0, ai_runtime_js_1.getToolName)(part),
|
|
1470
1492
|
input: part.input,
|
|
1471
1493
|
partIndex: i,
|
|
1472
1494
|
});
|
|
@@ -1569,7 +1591,7 @@ function extractNewToolResultsFromHistory(message, messages) {
|
|
|
1569
1591
|
function mergeIncomingIntoHydrated(hydrated, incoming) {
|
|
1570
1592
|
const incomingAdvancedByCallId = new Map();
|
|
1571
1593
|
for (const part of (incoming.parts ?? [])) {
|
|
1572
|
-
if (!(0,
|
|
1594
|
+
if (!(0, ai_runtime_js_1.isToolUIPart)(part))
|
|
1573
1595
|
continue;
|
|
1574
1596
|
const toolCallId = part.toolCallId;
|
|
1575
1597
|
if (typeof toolCallId !== "string" || toolCallId.length === 0)
|
|
@@ -1583,7 +1605,7 @@ function mergeIncomingIntoHydrated(hydrated, incoming) {
|
|
|
1583
1605
|
let mutated = false;
|
|
1584
1606
|
const hydratedParts = (hydrated.parts ?? []);
|
|
1585
1607
|
const mergedParts = hydratedParts.map((part) => {
|
|
1586
|
-
if (!(0,
|
|
1608
|
+
if (!(0, ai_runtime_js_1.isToolUIPart)(part))
|
|
1587
1609
|
return part;
|
|
1588
1610
|
const toolCallId = part.toolCallId;
|
|
1589
1611
|
if (typeof toolCallId !== "string" || toolCallId.length === 0)
|
|
@@ -1973,7 +1995,7 @@ async function chatCompact(messages, steps, options) {
|
|
|
1973
1995
|
return { type: "skipped" };
|
|
1974
1996
|
}
|
|
1975
1997
|
const result = await tracer_js_1.tracer.startActiveSpan("context compaction", async (span) => {
|
|
1976
|
-
const compactionId = (0,
|
|
1998
|
+
const compactionId = (0, ai_runtime_js_1.generateId)();
|
|
1977
1999
|
let summary;
|
|
1978
2000
|
const { waitUntilComplete } = chatStream.writer({
|
|
1979
2001
|
spanName: "stream compaction chunks",
|
|
@@ -2148,7 +2170,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2148
2170
|
execute: ({ write }) => {
|
|
2149
2171
|
write({
|
|
2150
2172
|
type: ai_shared_js_2.PENDING_MESSAGE_INJECTED_TYPE,
|
|
2151
|
-
id: (0,
|
|
2173
|
+
id: (0, ai_runtime_js_1.generateId)(),
|
|
2152
2174
|
data: {
|
|
2153
2175
|
messageIds: uiMessages.map((m) => m.id),
|
|
2154
2176
|
messages: uiMessages.map((m, idx) => ({
|
|
@@ -2302,9 +2324,9 @@ function findSkillByName(skills, name) {
|
|
|
2302
2324
|
* (e.g. in a `chat.createSession` loop with custom streamText).
|
|
2303
2325
|
*/
|
|
2304
2326
|
function buildSkillTools(skills) {
|
|
2305
|
-
const loadSkill = (0,
|
|
2327
|
+
const loadSkill = (0, ai_runtime_js_1.tool)({
|
|
2306
2328
|
description: "Load the full instructions for a skill by its name. Call this first before using a skill.",
|
|
2307
|
-
inputSchema: (0,
|
|
2329
|
+
inputSchema: (0, ai_runtime_js_1.jsonSchema)({
|
|
2308
2330
|
type: "object",
|
|
2309
2331
|
properties: {
|
|
2310
2332
|
name: {
|
|
@@ -2332,9 +2354,9 @@ function buildSkillTools(skills) {
|
|
|
2332
2354
|
};
|
|
2333
2355
|
},
|
|
2334
2356
|
});
|
|
2335
|
-
const readFile = (0,
|
|
2357
|
+
const readFile = (0, ai_runtime_js_1.tool)({
|
|
2336
2358
|
description: "Read a file from a skill's bundled folder. Paths must be relative to the skill's root.",
|
|
2337
|
-
inputSchema: (0,
|
|
2359
|
+
inputSchema: (0, ai_runtime_js_1.jsonSchema)({
|
|
2338
2360
|
type: "object",
|
|
2339
2361
|
properties: {
|
|
2340
2362
|
skill: { type: "string", description: "The skill's name (from frontmatter)." },
|
|
@@ -2362,9 +2384,9 @@ function buildSkillTools(skills) {
|
|
|
2362
2384
|
}
|
|
2363
2385
|
},
|
|
2364
2386
|
});
|
|
2365
|
-
const bash = (0,
|
|
2387
|
+
const bash = (0, ai_runtime_js_1.tool)({
|
|
2366
2388
|
description: "Run a bash command inside a skill's bundled folder. Use this to invoke the skill's scripts. The working directory is the skill's root.",
|
|
2367
|
-
inputSchema: (0,
|
|
2389
|
+
inputSchema: (0, ai_runtime_js_1.jsonSchema)({
|
|
2368
2390
|
type: "object",
|
|
2369
2391
|
properties: {
|
|
2370
2392
|
skill: { type: "string", description: "The skill's name (from frontmatter)." },
|
|
@@ -2597,6 +2619,11 @@ function chatCustomAgent(options) {
|
|
|
2597
2619
|
// No client-side upsert needed.
|
|
2598
2620
|
locals_js_1.locals.set(chatSessionHandleKey, sessions_js_1.sessions.open(payload.chatId));
|
|
2599
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 });
|
|
2600
2627
|
(0, streams_js_1.markChatAgentRunForStreamsWarning)();
|
|
2601
2628
|
v3_1.taskContext.setConversationId(payload.chatId);
|
|
2602
2629
|
stampConversationIdOnActiveSpan(payload.chatId);
|
|
@@ -2630,6 +2657,11 @@ function chatAgent(options) {
|
|
|
2630
2657
|
agentConfig: { type: "ai-sdk-chat" },
|
|
2631
2658
|
run: async (payload, { signal: runSignal, ctx }) => {
|
|
2632
2659
|
locals_js_1.locals.set(chatAgentRunContextKey, ctx);
|
|
2660
|
+
// On AI SDK 7, register the `@ai-sdk/otel` integration (once per process)
|
|
2661
|
+
// so `experimental_telemetry` spans flow into the run trace. Awaited here
|
|
2662
|
+
// at run boot — before any `streamText` — and a no-op on v5/v6 or when the
|
|
2663
|
+
// optional `@ai-sdk/otel` peer isn't installed. See ./aiAutoTelemetry.ts.
|
|
2664
|
+
await (0, aiAutoTelemetry_js_1.ensureAiSdkTelemetry)();
|
|
2633
2665
|
// Bind the run to its backing Session so every module-level helper
|
|
2634
2666
|
// (chat.stream, chat.messages, chat.stopSignal) resolves to this
|
|
2635
2667
|
// chat's `.in` / `.out` channels.
|
|
@@ -2717,6 +2749,11 @@ function chatAgent(options) {
|
|
|
2717
2749
|
// `messagesInput.waitWithIdleTimeout` so recovered turns fire first.
|
|
2718
2750
|
const bootInjectedQueue = [];
|
|
2719
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;
|
|
2720
2757
|
if (!hydrateMessages && couldHavePriorState) {
|
|
2721
2758
|
// Single parent span for the whole boot read phase — snapshot
|
|
2722
2759
|
// read, session.out replay, session.in replay. Per-phase timing
|
|
@@ -2752,23 +2789,28 @@ function chatAgent(options) {
|
|
|
2752
2789
|
slot.value = seeded;
|
|
2753
2790
|
}
|
|
2754
2791
|
}
|
|
2755
|
-
//
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
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
|
+
};
|
|
2772
2814
|
// session.in tail read
|
|
2773
2815
|
//
|
|
2774
2816
|
// session.in carries the user-side of the conversation
|
|
@@ -2779,20 +2821,43 @@ function chatAgent(options) {
|
|
|
2779
2821
|
// visible via the live SSE subscription — by which point they
|
|
2780
2822
|
// would arrive AFTER the partial-assistant orphan and look like
|
|
2781
2823
|
// brand-new turns to the model, producing inverted chains.
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
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()]);
|
|
2796
2861
|
}, {
|
|
2797
2862
|
attributes: {
|
|
2798
2863
|
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "tabler-rotate-clockwise",
|
|
@@ -2833,7 +2898,12 @@ function chatAgent(options) {
|
|
|
2833
2898
|
bootSnapshot !== undefined;
|
|
2834
2899
|
if (needsResumeCursor) {
|
|
2835
2900
|
try {
|
|
2836
|
-
|
|
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);
|
|
2837
2907
|
if (cursor !== undefined) {
|
|
2838
2908
|
v3_1.sessionStreams.setLastSeqNum(payload.chatId, "in", cursor);
|
|
2839
2909
|
v3_1.sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor);
|
|
@@ -3597,6 +3667,18 @@ function chatAgent(options) {
|
|
|
3597
3667
|
// therefore a delta merge, not a full-history reset.
|
|
3598
3668
|
if (currentWirePayload.trigger !== "action") {
|
|
3599
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
|
+
}
|
|
3600
3682
|
// Validate/transform UIMessages before conversion — catches malformed
|
|
3601
3683
|
// messages from storage or untrusted input before they reach the model.
|
|
3602
3684
|
// Slim wire: triggers like `regenerate-message` carry no incoming
|
|
@@ -3775,40 +3857,47 @@ function chatAgent(options) {
|
|
|
3775
3857
|
// `preload` / `close` / `handover-prepare` and submits
|
|
3776
3858
|
// with no incoming message fall through with the boot-
|
|
3777
3859
|
// seeded accumulator unchanged.
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
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) {
|
|
3801
3891
|
accumulatedMessages.push(...pendingHandoverPartial);
|
|
3802
|
-
const handoverMessageId = locals_js_1.locals.get(chatHandoverMessageIdKey);
|
|
3803
3892
|
const partialUI = synthesizeHandoverUIMessage(pendingHandoverPartial, handoverMessageId);
|
|
3804
3893
|
if (partialUI) {
|
|
3805
3894
|
accumulatedUIMessages.push(partialUI);
|
|
3806
3895
|
}
|
|
3807
|
-
locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
|
|
3808
3896
|
}
|
|
3897
|
+
locals_js_1.locals.set(chatHandoverPartialKey, []); // consume once
|
|
3809
3898
|
}
|
|
3810
|
-
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
3811
3899
|
}
|
|
3900
|
+
locals_js_1.locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
|
|
3812
3901
|
} // end if (trigger !== "action")
|
|
3813
3902
|
// ── Action result handling ──────────────────────────────
|
|
3814
3903
|
// For action turns, skip the turn machinery entirely.
|
|
@@ -3826,7 +3915,7 @@ function chatAgent(options) {
|
|
|
3826
3915
|
const resolvedOptions = resolveUIMessageStreamOptions();
|
|
3827
3916
|
const uiStream = actionStreamResult.toUIMessageStream({
|
|
3828
3917
|
...resolvedOptions,
|
|
3829
|
-
generateMessageId: resolvedOptions.generateMessageId ??
|
|
3918
|
+
generateMessageId: resolvedOptions.generateMessageId ?? ai_runtime_js_1.generateId,
|
|
3830
3919
|
});
|
|
3831
3920
|
await pipeChat(uiStream, {
|
|
3832
3921
|
signal: combinedSignal,
|
|
@@ -4050,7 +4139,7 @@ function chatAgent(options) {
|
|
|
4050
4139
|
// Always provide generateMessageId so the start chunk carries a
|
|
4051
4140
|
// messageId. Without this, the frontend and backend generate IDs
|
|
4052
4141
|
// independently and they won't match for ID-based dedup.
|
|
4053
|
-
generateMessageId: resolvedOptions.generateMessageId ??
|
|
4142
|
+
generateMessageId: resolvedOptions.generateMessageId ?? ai_runtime_js_1.generateId,
|
|
4054
4143
|
onFinish: ({ responseMessage, finishReason, }) => {
|
|
4055
4144
|
capturedResponseMessage = responseMessage;
|
|
4056
4145
|
capturedFinishReason = finishReason;
|
|
@@ -4178,7 +4267,7 @@ function chatAgent(options) {
|
|
|
4178
4267
|
// may produce a message with an empty ID since IDs are normally
|
|
4179
4268
|
// assigned by the frontend's useChat).
|
|
4180
4269
|
if (!capturedResponseMessage.id) {
|
|
4181
|
-
capturedResponseMessage = { ...capturedResponseMessage, id: (0,
|
|
4270
|
+
capturedResponseMessage = { ...capturedResponseMessage, id: (0, ai_runtime_js_1.generateId)() };
|
|
4182
4271
|
}
|
|
4183
4272
|
// Append any non-transient data parts queued via chat.response or writer.write()
|
|
4184
4273
|
const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
|
|
@@ -4234,7 +4323,7 @@ function chatAgent(options) {
|
|
|
4234
4323
|
const remainingParts = locals_js_1.locals.get(chatResponsePartsKey);
|
|
4235
4324
|
if (remainingParts && remainingParts.length > 0) {
|
|
4236
4325
|
capturedResponseMessage = {
|
|
4237
|
-
id: (0,
|
|
4326
|
+
id: (0, ai_runtime_js_1.generateId)(),
|
|
4238
4327
|
role: "assistant",
|
|
4239
4328
|
parts: [...remainingParts],
|
|
4240
4329
|
};
|
|
@@ -4276,7 +4365,7 @@ function chatAgent(options) {
|
|
|
4276
4365
|
});
|
|
4277
4366
|
if (shouldTrigger) {
|
|
4278
4367
|
await tracer_js_1.tracer.startActiveSpan("context compaction (outer loop)", async (compactionSpan) => {
|
|
4279
|
-
const compactionId = (0,
|
|
4368
|
+
const compactionId = (0, ai_runtime_js_1.generateId)();
|
|
4280
4369
|
const { waitUntilComplete } = chatStream.writer({
|
|
4281
4370
|
spanName: "stream compaction chunks",
|
|
4282
4371
|
collapsed: true,
|
|
@@ -4505,11 +4594,15 @@ function chatAgent(options) {
|
|
|
4505
4594
|
if (!hydrateMessages) {
|
|
4506
4595
|
try {
|
|
4507
4596
|
await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
|
|
4597
|
+
const snapshotInCursor = getChatSession().in.lastDispatchedSeqNum();
|
|
4508
4598
|
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
4509
4599
|
version: 1,
|
|
4510
4600
|
savedAt: Date.now(),
|
|
4511
4601
|
messages: accumulatedUIMessages,
|
|
4512
4602
|
lastOutEventId: turnCompleteResult?.lastEventId,
|
|
4603
|
+
lastInEventId: snapshotInCursor !== undefined
|
|
4604
|
+
? String(snapshotInCursor)
|
|
4605
|
+
: undefined,
|
|
4513
4606
|
});
|
|
4514
4607
|
}, {
|
|
4515
4608
|
attributes: {
|
|
@@ -4646,17 +4739,100 @@ function chatAgent(options) {
|
|
|
4646
4739
|
if (turnError instanceof v3_1.OutOfMemoryError) {
|
|
4647
4740
|
throw turnError;
|
|
4648
4741
|
}
|
|
4742
|
+
let errorTurnCompleteResult;
|
|
4649
4743
|
try {
|
|
4650
4744
|
await withChatWriter(async (writer) => {
|
|
4651
4745
|
const errorText = turnError instanceof Error ? turnError.message : "An unexpected error occurred";
|
|
4652
4746
|
writer.write({ type: "error", errorText });
|
|
4653
4747
|
});
|
|
4654
4748
|
// Signal turn complete so the client knows this turn is done
|
|
4655
|
-
await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4749
|
+
errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId);
|
|
4656
4750
|
}
|
|
4657
4751
|
catch {
|
|
4658
4752
|
// Best-effort — if stream write fails, let the run continue anyway
|
|
4659
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
|
+
}
|
|
4660
4836
|
// chat.requestUpgrade() / chat.endRun() — exit after error turn too
|
|
4661
4837
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey) ||
|
|
4662
4838
|
locals_js_1.locals.get(chatEndRunRequestedKey)) {
|
|
@@ -5350,7 +5526,7 @@ class ChatMessageAccumulator {
|
|
|
5350
5526
|
}
|
|
5351
5527
|
async addResponse(response) {
|
|
5352
5528
|
if (!response.id) {
|
|
5353
|
-
response = { ...response, id: (0,
|
|
5529
|
+
response = { ...response, id: (0, ai_runtime_js_1.generateId)() };
|
|
5354
5530
|
}
|
|
5355
5531
|
this.uiMessages.push(response);
|
|
5356
5532
|
try {
|
|
@@ -5522,18 +5698,32 @@ function createChatSession(payload, options) {
|
|
|
5522
5698
|
return {
|
|
5523
5699
|
async next() {
|
|
5524
5700
|
turn++;
|
|
5525
|
-
// First turn:
|
|
5526
|
-
|
|
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)) {
|
|
5527
5708
|
const result = await messagesInput.waitWithIdleTimeout({
|
|
5528
5709
|
idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30,
|
|
5529
5710
|
timeout,
|
|
5530
|
-
spanName:
|
|
5711
|
+
spanName: currentPayload.trigger === "preload"
|
|
5712
|
+
? "waiting for first message"
|
|
5713
|
+
: "waiting for first message (continuation)",
|
|
5531
5714
|
});
|
|
5532
5715
|
if (!result.ok || runSignal.aborted) {
|
|
5533
5716
|
stop.cleanup();
|
|
5534
5717
|
return { done: true, value: undefined };
|
|
5535
5718
|
}
|
|
5719
|
+
const continuationBoot = isMessagelessContinuationBoot;
|
|
5536
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
|
+
}
|
|
5537
5727
|
}
|
|
5538
5728
|
// Subsequent turns: wait for the next message
|
|
5539
5729
|
if (turn > 0) {
|
|
@@ -5679,21 +5869,29 @@ function createChatSession(payload, options) {
|
|
|
5679
5869
|
const queuedParts = locals_js_1.locals.get(chatResponsePartsKey);
|
|
5680
5870
|
if (queuedParts && queuedParts.length > 0) {
|
|
5681
5871
|
await accumulator.addResponse({
|
|
5682
|
-
id: (0,
|
|
5872
|
+
id: (0, ai_runtime_js_1.generateId)(),
|
|
5683
5873
|
role: "assistant",
|
|
5684
5874
|
parts: queuedParts,
|
|
5685
5875
|
});
|
|
5686
5876
|
locals_js_1.locals.set(chatResponsePartsKey, []);
|
|
5687
5877
|
}
|
|
5688
5878
|
}
|
|
5689
|
-
// 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).
|
|
5690
5883
|
let turnUsage;
|
|
5691
5884
|
if (typeof source.totalUsage?.then === "function") {
|
|
5692
5885
|
try {
|
|
5693
|
-
const usage = await
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
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
|
+
}
|
|
5697
5895
|
}
|
|
5698
5896
|
catch {
|
|
5699
5897
|
/* non-fatal */
|
|
@@ -6036,7 +6234,8 @@ function createChatStartSessionAction(taskId, options) {
|
|
|
6036
6234
|
// run-list filter by chat works without the customer having to wire it
|
|
6037
6235
|
// up. Mirrors the browser-mediated `TriggerChatTransport.doStart` path.
|
|
6038
6236
|
const userTags = params.triggerConfig?.tags ?? options?.triggerConfig?.tags ?? [];
|
|
6039
|
-
|
|
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);
|
|
6040
6239
|
const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {};
|
|
6041
6240
|
const triggerConfig = {
|
|
6042
6241
|
basePayload: {
|
|
@@ -6334,8 +6533,19 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
6334
6533
|
// 2. Trim back to the previous turn-complete, if we have one. Skipping on
|
|
6335
6534
|
// first-turn-ever (or first turn post-OOM without a snapshot seed) is
|
|
6336
6535
|
// fine — the chain catches up next turn.
|
|
6337
|
-
|
|
6338
|
-
|
|
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;
|
|
6339
6549
|
if (slot && prev !== undefined) {
|
|
6340
6550
|
try {
|
|
6341
6551
|
await session.out.trimTo(prev);
|