@trigger.dev/sdk 4.5.12 → 4.5.14
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 +154 -20
- package/dist/commonjs/v3/ai.js +1241 -387
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +7 -2
- package/dist/commonjs/v3/chat.js +22 -7
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/chat.test.js +13 -4
- package/dist/commonjs/v3/chat.test.js.map +1 -1
- package/dist/commonjs/v3/envvars.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +4 -10
- package/dist/commonjs/v3/sessions.js +73 -47
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/streams.js +1 -0
- package/dist/commonjs/v3/streams.js.map +1 -1
- 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/v3/test/test-session-handle.js +22 -23
- package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +154 -20
- package/dist/esm/v3/ai.js +1239 -387
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +7 -2
- package/dist/esm/v3/chat.js +22 -7
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/chat.test.js +13 -4
- package/dist/esm/v3/chat.test.js.map +1 -1
- package/dist/esm/v3/envvars.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +4 -10
- package/dist/esm/v3/sessions.js +73 -47
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/streams.js +1 -0
- package/dist/esm/v3/streams.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.js +2 -1
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/v3/test/test-session-handle.js +23 -24
- package/dist/esm/v3/test/test-session-handle.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/client-protocol.mdx +8 -3
- package/docs/ai-chat/custom-agents.mdx +181 -46
- package/docs/ai-chat/patterns/recovery-boot.mdx +9 -2
- package/docs/ai-chat/patterns/version-upgrades.mdx +26 -6
- package/docs/ai-chat/pending-messages.mdx +5 -3
- package/docs/ai-chat/reference.mdx +26 -10
- package/docs/ai-chat/types.mdx +5 -1
- package/docs/deployment/atomic-deployment.mdx +12 -0
- package/docs/deployment/overview.mdx +7 -1
- package/docs/deployment/version-skew-protection.mdx +430 -0
- package/docs/github-actions.mdx +33 -5
- package/docs/github-integration.mdx +12 -0
- package/docs/realtime/auth.mdx +18 -0
- package/docs/realtime/react-hooks/session-stream.mdx +109 -0
- package/docs/realtime/react-hooks/streams.mdx +71 -3
- package/docs/self-hosting/env/webapp.mdx +1 -0
- package/docs/self-hosting/security.mdx +1 -1
- package/docs/tasks/streams.mdx +3 -0
- package/docs/vercel-integration.mdx +43 -9
- package/docs/versioning.mdx +2 -0
- package/package.json +2 -2
package/dist/commonjs/v3/ai.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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;
|
|
5
4
|
exports.__setReadChatSnapshotImplForTests = __setReadChatSnapshotImplForTests;
|
|
6
5
|
exports.__setWriteChatSnapshotImplForTests = __setWriteChatSnapshotImplForTests;
|
|
7
6
|
exports.__readChatSnapshotProductionPathForTests = __readChatSnapshotProductionPathForTests;
|
|
@@ -11,6 +10,9 @@ exports.__setReplaySessionOutTailImplForTests = __setReplaySessionOutTailImplFor
|
|
|
11
10
|
exports.__replaySessionOutTailProductionPathForTests = __replaySessionOutTailProductionPathForTests;
|
|
12
11
|
exports.__setReplaySessionInTailImplForTests = __setReplaySessionInTailImplForTests;
|
|
13
12
|
exports.__replaySessionInTailProductionPathForTests = __replaySessionInTailProductionPathForTests;
|
|
13
|
+
exports.__resetChatInputRouterForTests = __resetChatInputRouterForTests;
|
|
14
|
+
exports.__chatInputCheckpointForTests = __chatInputCheckpointForTests;
|
|
15
|
+
exports.__findLatestSessionInCheckpointForTests = __findLatestSessionInCheckpointForTests;
|
|
14
16
|
exports.buildSkillTools = buildSkillTools;
|
|
15
17
|
const v3_1 = require("@trigger.dev/core/v3");
|
|
16
18
|
// Runtime VALUES go through the ESM/CJS shim so the CJS build can `require`
|
|
@@ -119,49 +121,6 @@ async function findLatestSessionInCursor(chatId) {
|
|
|
119
121
|
}
|
|
120
122
|
return latestCursor;
|
|
121
123
|
}
|
|
122
|
-
/** Test-only entry point for the records-based cursor scan. @internal */
|
|
123
|
-
async function __findLatestSessionInCursorForTests(chatId) {
|
|
124
|
-
return findLatestSessionInCursor(chatId);
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Seed the `.in` resume cursor for custom-agent loops (`chat.customAgent`
|
|
128
|
-
* raw loops and `chat.createSession`) the way `chat.agent`'s boot does.
|
|
129
|
-
*
|
|
130
|
-
* MUST run before anything attaches a `.in` listener (`createStopSignal`,
|
|
131
|
-
* `chat.messages.on`, the first wait): attaching opens the SSE tail with
|
|
132
|
-
* `Last-Event-ID` from the seeded cursor, so attach-then-seed replays
|
|
133
|
-
* every record from seq 0 — already-answered user messages get delivered
|
|
134
|
-
* into the new run's first wait and the loop re-answers them.
|
|
135
|
-
*
|
|
136
|
-
* Seeds both cursors: `setLastSeqNum` controls the SSE `Last-Event-ID`,
|
|
137
|
-
* `setLastDispatchedSeqNum` gates waiter dispatch — seeding only the
|
|
138
|
-
* former still re-delivers records the manager buffered before the seed.
|
|
139
|
-
*
|
|
140
|
-
* No-ops on fresh boots and when a cursor is already seeded (e.g. the
|
|
141
|
-
* `chatCustomAgent` wrapper ran before a nested `createChatSession`).
|
|
142
|
-
* @internal
|
|
143
|
-
*/
|
|
144
|
-
async function seedSessionInResumeCursorForCustomLoop(payload) {
|
|
145
|
-
if (v3_1.sessionStreams.lastSeqNum(payload.chatId, "in") !== undefined)
|
|
146
|
-
return;
|
|
147
|
-
// No continuation/attempt gate: the wire may omit `continuation` on a
|
|
148
|
-
// run that still has prior turns (chat.agent covers that case via its
|
|
149
|
-
// snapshot). The scan doubles as the prior-state probe — a fresh
|
|
150
|
-
// session has no turn-complete on `.out`, returns no cursor, and
|
|
151
|
-
// seeds nothing. Cost on fresh boots is one non-blocking records read.
|
|
152
|
-
try {
|
|
153
|
-
const cursor = await findLatestSessionInCursor(payload.chatId);
|
|
154
|
-
if (cursor !== undefined) {
|
|
155
|
-
v3_1.sessionStreams.setLastSeqNum(payload.chatId, "in", cursor);
|
|
156
|
-
v3_1.sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
v3_1.logger.warn("chat session: session.in resume cursor lookup failed; old messages may replay", {
|
|
161
|
-
error: error instanceof Error ? error.message : String(error),
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
124
|
let readChatSnapshotImpl;
|
|
166
125
|
function __setReadChatSnapshotImplForTests(impl) {
|
|
167
126
|
readChatSnapshotImpl = impl;
|
|
@@ -1025,90 +984,508 @@ async function withChatWriter(fn) {
|
|
|
1025
984
|
await flush();
|
|
1026
985
|
return result;
|
|
1027
986
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
987
|
+
const CHAT_CUSTOM_AGENT_CLIENT_DATA_ERROR_TEXT = "Invalid client data";
|
|
988
|
+
const chatCustomAgentClientDataParserKey = locals_js_1.locals.create("chat.customAgentClientDataParser");
|
|
989
|
+
const chatCustomAgentClientDataErrorHandlerKey = locals_js_1.locals.create("chat.customAgentClientDataErrorHandler");
|
|
990
|
+
function shouldValidateChatCustomAgentPayload(payload) {
|
|
991
|
+
return (payload.trigger !== "close" && locals_js_1.locals.get(chatCustomAgentClientDataParserKey) !== undefined);
|
|
992
|
+
}
|
|
993
|
+
function assertChatCustomAgentSyncParseResult(result) {
|
|
994
|
+
if (result && typeof result.then === "function") {
|
|
995
|
+
void Promise.resolve(result).catch(() => { });
|
|
996
|
+
throw new Error("chat.messages.peek() cannot validate clientData with an asynchronous schema. " +
|
|
997
|
+
"Use chat.messages.once(), chat.messages.wait(), or chat.messages.waitWithIdleTimeout().");
|
|
998
|
+
}
|
|
999
|
+
return result;
|
|
1000
|
+
}
|
|
1001
|
+
function getChatCustomAgentSyncSchemaParseFn(schema) {
|
|
1002
|
+
const parser = schema;
|
|
1003
|
+
if (typeof parser === "function" && typeof parser.assert === "function") {
|
|
1004
|
+
return parser.assert.bind(parser);
|
|
1005
|
+
}
|
|
1006
|
+
if (typeof parser === "function") {
|
|
1007
|
+
return (value) => assertChatCustomAgentSyncParseResult(parser(value));
|
|
1008
|
+
}
|
|
1009
|
+
if (typeof parser.parse === "function") {
|
|
1010
|
+
return (value) => assertChatCustomAgentSyncParseResult(parser.parse(value));
|
|
1011
|
+
}
|
|
1012
|
+
if (typeof parser.validateSync === "function") {
|
|
1013
|
+
return parser.validateSync.bind(parser);
|
|
1014
|
+
}
|
|
1015
|
+
if (typeof parser.create === "function") {
|
|
1016
|
+
return parser.create.bind(parser);
|
|
1017
|
+
}
|
|
1018
|
+
if (typeof parser.assert === "function") {
|
|
1019
|
+
return (value) => {
|
|
1020
|
+
parser.assert(value);
|
|
1021
|
+
return value;
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
return () => {
|
|
1025
|
+
throw new Error("chat.messages.peek() cannot validate clientData with this schema. " +
|
|
1026
|
+
"Use chat.messages.once(), chat.messages.wait(), or chat.messages.waitWithIdleTimeout().");
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
async function writeChatCustomAgentClientDataErrorToStream(payload) {
|
|
1030
|
+
try {
|
|
1031
|
+
await withChatWriter((writer) => {
|
|
1032
|
+
writer.write({
|
|
1033
|
+
type: "error",
|
|
1034
|
+
errorText: CHAT_CUSTOM_AGENT_CLIENT_DATA_ERROR_TEXT,
|
|
1035
|
+
});
|
|
1036
|
+
});
|
|
1037
|
+
await chatWriteTurnComplete();
|
|
1038
|
+
}
|
|
1039
|
+
catch (signalError) {
|
|
1040
|
+
v3_1.logger.warn("chat.customAgent: failed to report clientData validation error", {
|
|
1041
|
+
chatId: payload.chatId,
|
|
1042
|
+
trigger: payload.trigger,
|
|
1043
|
+
error: signalError instanceof Error ? signalError.message : String(signalError),
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
async function reportChatCustomAgentClientDataError(payload, error, options) {
|
|
1048
|
+
const errorText = error instanceof Error ? error.message : "An unexpected error occurred";
|
|
1049
|
+
v3_1.logger.warn("chat.customAgent: clientData validation failed", {
|
|
1050
|
+
chatId: payload.chatId,
|
|
1051
|
+
trigger: payload.trigger,
|
|
1052
|
+
error: errorText,
|
|
1053
|
+
});
|
|
1054
|
+
const errorHandler = options.callHandler === false
|
|
1055
|
+
? undefined
|
|
1056
|
+
: locals_js_1.locals.get(chatCustomAgentClientDataErrorHandlerKey);
|
|
1057
|
+
if (errorHandler) {
|
|
1058
|
+
try {
|
|
1059
|
+
await errorHandler({ error, payload });
|
|
1060
|
+
}
|
|
1061
|
+
catch (handlerError) {
|
|
1062
|
+
v3_1.logger.warn("chat.customAgent: clientData validation error handler failed", {
|
|
1063
|
+
chatId: payload.chatId,
|
|
1064
|
+
trigger: payload.trigger,
|
|
1065
|
+
error: handlerError instanceof Error ? handlerError.message : String(handlerError),
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (!options.writeToStream) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
await writeChatCustomAgentClientDataErrorToStream(payload);
|
|
1073
|
+
}
|
|
1074
|
+
async function parseChatCustomAgentPayload(payload) {
|
|
1075
|
+
const parser = locals_js_1.locals.get(chatCustomAgentClientDataParserKey);
|
|
1076
|
+
if (!parser || payload.trigger === "close") {
|
|
1077
|
+
return { ok: true, payload };
|
|
1078
|
+
}
|
|
1079
|
+
try {
|
|
1080
|
+
const metadata = await parser.parse(payload.metadata);
|
|
1081
|
+
return { ok: true, payload: { ...payload, metadata } };
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
return { ok: false, error };
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
async function validateChatCustomAgentPayload(payload, options = {}) {
|
|
1088
|
+
const result = await parseChatCustomAgentPayload(payload);
|
|
1089
|
+
if (!result.ok) {
|
|
1090
|
+
await reportChatCustomAgentClientDataError(payload, result.error, {
|
|
1091
|
+
writeToStream: options.writeErrorToStream ?? true,
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
return result;
|
|
1095
|
+
}
|
|
1096
|
+
function validateChatCustomAgentPayloadSync(payload) {
|
|
1097
|
+
const parser = locals_js_1.locals.get(chatCustomAgentClientDataParserKey);
|
|
1098
|
+
if (!parser || payload.trigger === "close") {
|
|
1099
|
+
return payload;
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
return { ...payload, metadata: parser.parseSync(payload.metadata) };
|
|
1103
|
+
}
|
|
1104
|
+
catch (error) {
|
|
1105
|
+
v3_1.logger.warn("chat.customAgent: clientData validation failed in chat.messages.peek()", {
|
|
1106
|
+
chatId: payload.chatId,
|
|
1107
|
+
trigger: payload.trigger,
|
|
1108
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1109
|
+
});
|
|
1110
|
+
throw error;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Read one record from a route, suspending the run if nothing is there yet.
|
|
1115
|
+
*
|
|
1116
|
+
* The wake and the read are separate steps: the channel wakes the run, then the
|
|
1117
|
+
* router hands over whatever it routed. Nothing else can take the record in
|
|
1118
|
+
* between, which is what keeps the published cursors and the delivered record
|
|
1119
|
+
* in agreement.
|
|
1120
|
+
* @internal
|
|
1121
|
+
*/
|
|
1122
|
+
async function waitOnChatRoute(route, options) {
|
|
1123
|
+
const router = chatInputRouter();
|
|
1124
|
+
const session = getChatSession();
|
|
1125
|
+
return tracer_js_1.tracer.startActiveSpan(options.spanName ?? `chat.${route}.wait()`, async (span) => {
|
|
1126
|
+
const idleMs = (options.idleTimeoutInSeconds ?? 0) * 1000;
|
|
1127
|
+
if (idleMs > 0) {
|
|
1128
|
+
const warm = await router.next(route, { timeoutMs: idleMs });
|
|
1129
|
+
if (warm) {
|
|
1130
|
+
span.setAttribute("wait.resolved", "idle");
|
|
1131
|
+
return { ok: true, output: warm.data, record: warm };
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
const buffered = await router.next(route, { timeoutMs: 0 });
|
|
1136
|
+
if (buffered) {
|
|
1137
|
+
span.setAttribute("wait.resolved", "buffered");
|
|
1138
|
+
return { ok: true, output: buffered.data, record: buffered };
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (options.skipSuspend) {
|
|
1142
|
+
span.setAttribute("wait.resolved", "skipped");
|
|
1143
|
+
return {
|
|
1144
|
+
ok: false,
|
|
1145
|
+
error: new Error("Idle timeout elapsed and skipSuspend is set"),
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
if (options.onSuspend)
|
|
1149
|
+
await options.onSuspend();
|
|
1150
|
+
span.setAttribute("wait.resolved", "suspended");
|
|
1151
|
+
while (true) {
|
|
1152
|
+
/**
|
|
1153
|
+
* The floor doubles as the wake cursor: the server completes the
|
|
1154
|
+
* waitpoint immediately if anything sits after this sequence, so a
|
|
1155
|
+
* floor that has advanced past an unread record parks a waitpoint
|
|
1156
|
+
* nothing will complete. Recorded on the span so a run that never woke
|
|
1157
|
+
* can be diagnosed from its trace alone.
|
|
1158
|
+
*/
|
|
1159
|
+
const wakeFrom = router.resumeFloor();
|
|
1160
|
+
span.setAttribute("wait.lastSeqNum", wakeFrom ?? -1);
|
|
1161
|
+
const wake = await session.in.awaitWake({
|
|
1162
|
+
timeout: options.timeout,
|
|
1163
|
+
lastSeqNum: wakeFrom,
|
|
1164
|
+
});
|
|
1165
|
+
if (!wake.ok) {
|
|
1166
|
+
span.recordException(wake.error);
|
|
1167
|
+
return { ok: false, error: wake.error };
|
|
1168
|
+
}
|
|
1169
|
+
const record = await router.next(route);
|
|
1170
|
+
if (!record)
|
|
1171
|
+
continue;
|
|
1172
|
+
if (options.onResume)
|
|
1173
|
+
await options.onResume();
|
|
1174
|
+
return { ok: true, output: record.data, record };
|
|
1175
|
+
}
|
|
1176
|
+
}, {
|
|
1177
|
+
attributes: {
|
|
1178
|
+
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "sessions",
|
|
1179
|
+
session: session.id,
|
|
1180
|
+
io: "in",
|
|
1181
|
+
route,
|
|
1182
|
+
...(0, v3_1.accessoryAttributes)({
|
|
1183
|
+
items: [{ text: `${session.id}.in:${route}`, variant: "normal" }],
|
|
1184
|
+
style: "codepath",
|
|
1185
|
+
}),
|
|
1186
|
+
},
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Raw `.in` message delivery, with no client-data validation.
|
|
1191
|
+
*
|
|
1192
|
+
* Sits directly on the router's `messages` route, so a record handed to the
|
|
1193
|
+
* handler is consumed and the resume floor is free to advance past it.
|
|
1194
|
+
*/
|
|
1195
|
+
function subscribeToRawChatMessages(handler) {
|
|
1196
|
+
return chatInputRouter().on(CHAT_ROUTE_MESSAGES, (record) => {
|
|
1197
|
+
const chunk = record.data;
|
|
1198
|
+
void Promise.resolve(handler(chunk.payload)).catch(() => { });
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Message delivery with client-data validation in front of it.
|
|
1203
|
+
*
|
|
1204
|
+
* Parses are chained so payloads are validated in wire order, which is the
|
|
1205
|
+
* point of the feature: a later message must not be validated against state a
|
|
1206
|
+
* earlier one has not established yet.
|
|
1207
|
+
*
|
|
1208
|
+
* The handler is dispatched but **not** awaited by that chain. Awaiting it
|
|
1209
|
+
* would serialise user code, so one slow or never-resolving handler would stall
|
|
1210
|
+
* delivery of every later message, and only when a schema is declared. Raw
|
|
1211
|
+
* delivery has always been fire-and-forget, so awaiting here would also make
|
|
1212
|
+
* handler concurrency differ between the validated and unvalidated paths for no
|
|
1213
|
+
* stated reason.
|
|
1214
|
+
*
|
|
1215
|
+
* `off()` detaches from the router but does not cancel work already chained:
|
|
1216
|
+
* the parse chain is a live promise chain and runs to completion on its own, so
|
|
1217
|
+
* a frame that arrives just before a turn closes is still parsed and a failure
|
|
1218
|
+
* is still reported (through the `!active` branch). Nothing therefore has to
|
|
1219
|
+
* wait on it, which is why no `drain()` hook is exposed. The one uncovered edge
|
|
1220
|
+
* is a parse still in flight when the task itself returns, where teardown can
|
|
1221
|
+
* cut the report short; give this a bounded wait at the run-end boundary rather
|
|
1222
|
+
* than an unbounded one, since the chain awaits a user-supplied schema.
|
|
1223
|
+
*/
|
|
1224
|
+
function subscribeToValidatedChatMessages(handler, options = {}) {
|
|
1225
|
+
let active = true;
|
|
1226
|
+
let delivery = Promise.resolve();
|
|
1227
|
+
const subscription = subscribeToRawChatMessages((payload) => {
|
|
1228
|
+
delivery = delivery
|
|
1229
|
+
.then(async () => {
|
|
1230
|
+
const result = await parseChatCustomAgentPayload(payload);
|
|
1231
|
+
if (!result.ok) {
|
|
1232
|
+
if (active) {
|
|
1233
|
+
// Completing the turn here could close an active response.
|
|
1234
|
+
await reportChatCustomAgentClientDataError(payload, result.error, {
|
|
1235
|
+
writeToStream: false,
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
else if (options.onInvalidAfterOff) {
|
|
1239
|
+
await options.onInvalidAfterOff(payload, result.error);
|
|
1240
|
+
}
|
|
1241
|
+
else {
|
|
1242
|
+
// The subscription was removed while parsing. Keep the failure
|
|
1243
|
+
// observable without invoking a user callback after off().
|
|
1244
|
+
await reportChatCustomAgentClientDataError(payload, result.error, {
|
|
1245
|
+
writeToStream: false,
|
|
1246
|
+
callHandler: false,
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (active) {
|
|
1252
|
+
void Promise.resolve(handler(result.payload, () => active)).catch(() => { });
|
|
1253
|
+
}
|
|
1254
|
+
else {
|
|
1255
|
+
void Promise.resolve(options.onAfterOff?.(result.payload)).catch(() => { });
|
|
1256
|
+
}
|
|
1257
|
+
})
|
|
1258
|
+
.catch(() => { });
|
|
1259
|
+
});
|
|
1260
|
+
return {
|
|
1261
|
+
off() {
|
|
1262
|
+
active = false;
|
|
1263
|
+
subscription.off();
|
|
1264
|
+
},
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Validated delivery that does **not** consume.
|
|
1269
|
+
*
|
|
1270
|
+
* The steering path must observe rather than take: a record it does not inject
|
|
1271
|
+
* has to stay queued so a later turn answers it. Validation still runs in wire
|
|
1272
|
+
* order in front of the handler, so `clientData` is parsed before steering code
|
|
1273
|
+
* sees it, and a frame whose parse finishes after the turn closed needs no
|
|
1274
|
+
* buffer of its own, because the record was never removed from the channel.
|
|
1275
|
+
*/
|
|
1276
|
+
function observeValidatedChatMessages(
|
|
1277
|
+
/**
|
|
1278
|
+
* Receives the observed record's own `seqNum`. It is passed per record rather
|
|
1279
|
+
* than tracked outside, because the handler runs after an await: with two
|
|
1280
|
+
* frames in flight a shared slot already holds the newer sequence by the time
|
|
1281
|
+
* the older frame's parse resolves, and the steering queue would then take
|
|
1282
|
+
* the wrong record off the channel.
|
|
1283
|
+
*/
|
|
1284
|
+
handler) {
|
|
1285
|
+
let active = true;
|
|
1286
|
+
let delivery = Promise.resolve();
|
|
1287
|
+
const claims = locals_js_1.locals.get(chatObserverClaimedSeqNumsKey) ??
|
|
1288
|
+
new Map();
|
|
1289
|
+
locals_js_1.locals.set(chatObserverClaimedSeqNumsKey, claims);
|
|
1290
|
+
const subscription = chatInputRouter().observe(CHAT_ROUTE_MESSAGES, (record) => {
|
|
1291
|
+
const payload = record.data.payload;
|
|
1292
|
+
const seqNum = record.seqNum;
|
|
1293
|
+
/**
|
|
1294
|
+
* Claimed synchronously, before any await, so a read cannot validate the
|
|
1295
|
+
* same record. The promise lets a read wait for the outcome rather than
|
|
1296
|
+
* spin on a record it is not allowed to take.
|
|
1297
|
+
*/
|
|
1298
|
+
let release;
|
|
1299
|
+
const released = new Promise((resolve) => {
|
|
1300
|
+
release = resolve;
|
|
1301
|
+
});
|
|
1302
|
+
claims.set(record.seqNum, { released, release });
|
|
1303
|
+
const releaseClaim = () => {
|
|
1304
|
+
claims.delete(record.seqNum);
|
|
1305
|
+
release();
|
|
1306
|
+
};
|
|
1307
|
+
delivery = delivery
|
|
1308
|
+
.then(async () => {
|
|
1309
|
+
const result = await parseChatCustomAgentPayload(payload);
|
|
1310
|
+
if (!result.ok) {
|
|
1311
|
+
/**
|
|
1312
|
+
* Reported once, then taken off the channel.
|
|
1313
|
+
*
|
|
1314
|
+
* Observing does not consume, so without this the record would be
|
|
1315
|
+
* read again by the next turn and parsed a second time, firing a
|
|
1316
|
+
* user-supplied schema and reporting the same failure twice. A frame
|
|
1317
|
+
* that fails validation is never going to be answered by this run or
|
|
1318
|
+
* a later one, so removing it loses nothing that was still owed.
|
|
1319
|
+
*/
|
|
1320
|
+
const timing = locals_js_1.locals.get(chatCustomAgentClientDataErrorTimingKey) ?? "turn-end";
|
|
1321
|
+
const writeNow = timing === "arrival" || !active;
|
|
1322
|
+
await reportChatCustomAgentClientDataError(payload, result.error, {
|
|
1323
|
+
writeToStream: writeNow,
|
|
1324
|
+
});
|
|
1325
|
+
if (!writeNow) {
|
|
1326
|
+
const deferred = locals_js_1.locals.get(chatCustomAgentDeferredClientDataErrorsKey) ?? [];
|
|
1327
|
+
deferred.push(payload);
|
|
1328
|
+
locals_js_1.locals.set(chatCustomAgentDeferredClientDataErrorsKey, deferred);
|
|
1329
|
+
}
|
|
1330
|
+
// Drop the record: an invalid frame is never answered, by this run or
|
|
1331
|
+
// a later one. Released so a waiting read stops waiting, finds it
|
|
1332
|
+
// gone, and goes back to waiting for the next message.
|
|
1333
|
+
chatInputRouter().take(CHAT_ROUTE_MESSAGES, record.seqNum);
|
|
1334
|
+
releaseClaim();
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
// Valid, so release. The record is still queued, and whichever comes
|
|
1338
|
+
// first, an injection or a later turn's read, now owns it.
|
|
1339
|
+
releaseClaim();
|
|
1340
|
+
void Promise.resolve(handler(result.payload, seqNum, () => active)).catch(() => { });
|
|
1341
|
+
})
|
|
1342
|
+
.catch(() => { });
|
|
1343
|
+
});
|
|
1344
|
+
return {
|
|
1345
|
+
off() {
|
|
1346
|
+
active = false;
|
|
1347
|
+
subscription.off();
|
|
1348
|
+
},
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Whether a record is currently owned by the validating observer.
|
|
1353
|
+
*
|
|
1354
|
+
* A read that pulls one puts it back and keeps waiting, so the observer's parse
|
|
1355
|
+
* stays the only one and the record is not consumed out from under it.
|
|
1356
|
+
*/
|
|
1357
|
+
function observerClaimFor(record) {
|
|
1358
|
+
return locals_js_1.locals.get(chatObserverClaimedSeqNumsKey)?.get(record.seqNum)?.released;
|
|
1359
|
+
}
|
|
1037
1360
|
const messagesInput = {
|
|
1038
1361
|
id: "chat-messages",
|
|
1039
1362
|
on(handler) {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
// Without this, a message arriving mid-stream was delivered twice
|
|
1046
|
-
// and ran a duplicate turn.
|
|
1047
|
-
void Promise.resolve(handler(chunk.payload)).catch(() => { });
|
|
1048
|
-
return true;
|
|
1049
|
-
}
|
|
1050
|
-
return undefined;
|
|
1051
|
-
});
|
|
1363
|
+
if (!locals_js_1.locals.get(chatCustomAgentClientDataParserKey)) {
|
|
1364
|
+
return subscribeToRawChatMessages(handler);
|
|
1365
|
+
}
|
|
1366
|
+
const deliver = (payload) => handler(payload);
|
|
1367
|
+
return subscribeToValidatedChatMessages(deliver, { onAfterOff: deliver });
|
|
1052
1368
|
},
|
|
1053
1369
|
once(options) {
|
|
1054
|
-
const ctx = v3_1.taskContext.ctx;
|
|
1055
|
-
const runId = ctx?.run.id;
|
|
1056
1370
|
return new v3_1.InputStreamOncePromise((resolve, reject) => {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1371
|
+
/**
|
|
1372
|
+
* Same skip-and-wait rule as `waitWithIdleTimeout`: a payload that fails
|
|
1373
|
+
* validation is reported and not surfaced. The timeout is a total budget
|
|
1374
|
+
* across retries, so skipping a frame cannot extend the wait forever.
|
|
1375
|
+
*/
|
|
1376
|
+
const deadline = options?.timeoutMs === undefined ? undefined : Date.now() + options.timeoutMs;
|
|
1377
|
+
const take = () => {
|
|
1378
|
+
chatInputRouter()
|
|
1379
|
+
.next(CHAT_ROUTE_MESSAGES, {
|
|
1380
|
+
timeoutMs: deadline === undefined ? undefined : Math.max(0, deadline - Date.now()),
|
|
1381
|
+
})
|
|
1382
|
+
.then(async (record) => {
|
|
1383
|
+
if (!record) {
|
|
1384
|
+
resolve({
|
|
1385
|
+
ok: false,
|
|
1386
|
+
error: new v3_1.InputStreamTimeoutError("chat-messages", options?.timeoutMs ?? 0),
|
|
1387
|
+
});
|
|
1063
1388
|
return;
|
|
1064
1389
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1390
|
+
const claim = observerClaimFor(record);
|
|
1391
|
+
if (claim) {
|
|
1392
|
+
chatInputRouter().untake(CHAT_ROUTE_MESSAGES, record);
|
|
1393
|
+
await claim;
|
|
1394
|
+
take();
|
|
1067
1395
|
return;
|
|
1068
1396
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
items: [{ text: "chat-messages", variant: "normal" }],
|
|
1084
|
-
style: "codepath",
|
|
1085
|
-
}),
|
|
1086
|
-
},
|
|
1087
|
-
})
|
|
1088
|
-
.catch(reject);
|
|
1397
|
+
const payload = record.data.payload;
|
|
1398
|
+
if (!shouldValidateChatCustomAgentPayload(payload)) {
|
|
1399
|
+
resolve({ ok: true, output: payload });
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
const validated = await validateChatCustomAgentPayload(payload);
|
|
1403
|
+
if (validated.ok) {
|
|
1404
|
+
resolve({ ok: true, output: validated.payload });
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
take();
|
|
1408
|
+
}, reject);
|
|
1409
|
+
};
|
|
1410
|
+
take();
|
|
1089
1411
|
});
|
|
1090
1412
|
},
|
|
1091
1413
|
peek() {
|
|
1092
|
-
const
|
|
1093
|
-
if (
|
|
1094
|
-
return
|
|
1095
|
-
|
|
1414
|
+
const record = chatInputRouter().peek(CHAT_ROUTE_MESSAGES);
|
|
1415
|
+
if (!record)
|
|
1416
|
+
return undefined;
|
|
1417
|
+
const payload = record.data.payload;
|
|
1418
|
+
return validateChatCustomAgentPayloadSync(payload);
|
|
1419
|
+
},
|
|
1420
|
+
async hasPending() {
|
|
1421
|
+
return chatInputRouter().hasPending(CHAT_ROUTE_MESSAGES);
|
|
1422
|
+
},
|
|
1423
|
+
async next(options) {
|
|
1424
|
+
const timeoutInSeconds = options?.timeoutInSeconds;
|
|
1425
|
+
if (timeoutInSeconds !== undefined &&
|
|
1426
|
+
(!Number.isFinite(timeoutInSeconds) || timeoutInSeconds < 0)) {
|
|
1427
|
+
throw new TypeError("chat.messages.next() timeoutInSeconds must be a finite non-negative number");
|
|
1428
|
+
}
|
|
1429
|
+
// Consuming read, so it takes the same claim-and-validate path as the other
|
|
1430
|
+
// reads: a record the observer still owns is put back and awaited, and an
|
|
1431
|
+
// invalid payload is reported and skipped rather than surfaced raw.
|
|
1432
|
+
const totalMs = timeoutInSeconds === undefined ? undefined : timeoutInSeconds * 1000;
|
|
1433
|
+
/**
|
|
1434
|
+
* The caller's timeout is a total budget, not a per-attempt one. Skipping an
|
|
1435
|
+
* invalid frame must not buy another full wait, or a client sending invalid
|
|
1436
|
+
* frames faster than the timeout would keep the read blocked indefinitely
|
|
1437
|
+
* and it would never return.
|
|
1438
|
+
*/
|
|
1439
|
+
const deadline = totalMs === undefined ? undefined : Date.now() + totalMs;
|
|
1440
|
+
while (true) {
|
|
1441
|
+
const record = await chatInputRouter().next(CHAT_ROUTE_MESSAGES, {
|
|
1442
|
+
timeoutMs: deadline === undefined ? undefined : Math.max(0, deadline - Date.now()),
|
|
1443
|
+
});
|
|
1444
|
+
if (!record)
|
|
1445
|
+
return undefined;
|
|
1446
|
+
const claim = observerClaimFor(record);
|
|
1447
|
+
if (claim) {
|
|
1448
|
+
chatInputRouter().untake(CHAT_ROUTE_MESSAGES, record);
|
|
1449
|
+
await claim;
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
const chunk = record.data;
|
|
1453
|
+
if (!shouldValidateChatCustomAgentPayload(chunk.payload)) {
|
|
1454
|
+
return { id: record.id, seqNum: record.seqNum, payload: chunk.payload };
|
|
1455
|
+
}
|
|
1456
|
+
const validated = await validateChatCustomAgentPayload(chunk.payload);
|
|
1457
|
+
if (validated.ok) {
|
|
1458
|
+
return { id: record.id, seqNum: record.seqNum, payload: validated.payload };
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1096
1461
|
},
|
|
1097
1462
|
wait(options) {
|
|
1098
1463
|
return new v3_1.ManualWaitpointPromise(async (resolve, reject) => {
|
|
1099
1464
|
try {
|
|
1465
|
+
// Same skip-and-wait rule as `waitWithIdleTimeout`, looping back into
|
|
1466
|
+
// the suspending wait rather than surfacing an unvalidated payload.
|
|
1100
1467
|
while (true) {
|
|
1101
|
-
const result = await
|
|
1468
|
+
const result = await waitOnChatRoute(CHAT_ROUTE_MESSAGES, { timeout: options?.timeout, spanName: options?.spanName });
|
|
1102
1469
|
if (!result.ok) {
|
|
1103
|
-
resolve(result);
|
|
1470
|
+
resolve({ ok: false, error: result.error ?? new Error("Timed out") });
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
const claim = observerClaimFor(result.record);
|
|
1474
|
+
if (claim) {
|
|
1475
|
+
chatInputRouter().untake(CHAT_ROUTE_MESSAGES, result.record);
|
|
1476
|
+
await claim;
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
const payload = result.output.payload;
|
|
1480
|
+
if (!shouldValidateChatCustomAgentPayload(payload)) {
|
|
1481
|
+
resolve({ ok: true, output: payload });
|
|
1104
1482
|
return;
|
|
1105
1483
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1484
|
+
const validated = await validateChatCustomAgentPayload(payload);
|
|
1485
|
+
if (validated.ok) {
|
|
1486
|
+
resolve({ ok: true, output: validated.payload });
|
|
1108
1487
|
return;
|
|
1109
1488
|
}
|
|
1110
|
-
// Stop chunks are handled by the stopInput facade's persistent
|
|
1111
|
-
// listener; loop back into the suspending wait.
|
|
1112
1489
|
}
|
|
1113
1490
|
}
|
|
1114
1491
|
catch (error) {
|
|
@@ -1117,102 +1494,75 @@ const messagesInput = {
|
|
|
1117
1494
|
});
|
|
1118
1495
|
},
|
|
1119
1496
|
async waitWithIdleTimeout(options) {
|
|
1497
|
+
/**
|
|
1498
|
+
* A payload that fails client-data validation is reported and skipped
|
|
1499
|
+
* rather than surfaced, so the caller never sees an unvalidated
|
|
1500
|
+
* `clientData`. The loop waits for the next message instead of returning an
|
|
1501
|
+
* error, which keeps a single bad client send from ending the turn.
|
|
1502
|
+
*/
|
|
1120
1503
|
while (true) {
|
|
1121
|
-
const result = await
|
|
1504
|
+
const result = await waitOnChatRoute(CHAT_ROUTE_MESSAGES, options);
|
|
1122
1505
|
if (!result.ok)
|
|
1123
|
-
return result;
|
|
1124
|
-
|
|
1125
|
-
|
|
1506
|
+
return { ok: false, error: result.error };
|
|
1507
|
+
const claim = observerClaimFor(result.record);
|
|
1508
|
+
if (claim) {
|
|
1509
|
+
chatInputRouter().untake(CHAT_ROUTE_MESSAGES, result.record);
|
|
1510
|
+
await claim;
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
const payload = result.output.payload;
|
|
1514
|
+
if (!shouldValidateChatCustomAgentPayload(payload)) {
|
|
1515
|
+
return { ok: true, output: payload };
|
|
1516
|
+
}
|
|
1517
|
+
const validated = await validateChatCustomAgentPayload(payload);
|
|
1518
|
+
if (validated.ok) {
|
|
1519
|
+
return { ok: true, output: validated.payload };
|
|
1126
1520
|
}
|
|
1127
|
-
// Swallow stop-kind chunks — persistent stop listener already handled
|
|
1128
|
-
// the abort; we just loop for the next message.
|
|
1129
1521
|
}
|
|
1130
1522
|
},
|
|
1131
1523
|
async send(_runId, data, options) {
|
|
1132
|
-
// The `runId` argument is kept for signature parity with
|
|
1133
|
-
// `RealtimeDefinedInputStream` but ignored — sessions are addressed
|
|
1134
|
-
// by sessionId, not runId. Callers producing messages from outside
|
|
1135
|
-
// the run should prefer the transport's `session.in.send(...)` path.
|
|
1136
1524
|
await getChatSession().in.send({ kind: "message", payload: data }, options?.requestOptions);
|
|
1137
1525
|
},
|
|
1138
1526
|
};
|
|
1139
1527
|
const stopInput = {
|
|
1140
1528
|
id: "chat-stop",
|
|
1141
1529
|
on(handler) {
|
|
1142
|
-
return
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
// only meaningful to the turn it interrupts — buffering it would
|
|
1146
|
-
// let a stale stop abort a future turn.
|
|
1147
|
-
void Promise.resolve(handler({ stop: true, message: chunk.message })).catch(() => { });
|
|
1148
|
-
return true;
|
|
1149
|
-
}
|
|
1150
|
-
return undefined;
|
|
1530
|
+
return chatInputRouter().on(CHAT_ROUTE_STOP, (record) => {
|
|
1531
|
+
const chunk = record.data;
|
|
1532
|
+
void Promise.resolve(handler({ stop: true, message: chunk.message })).catch(() => { });
|
|
1151
1533
|
});
|
|
1152
1534
|
},
|
|
1153
1535
|
once(options) {
|
|
1154
|
-
const ctx = v3_1.taskContext.ctx;
|
|
1155
|
-
const runId = ctx?.run.id;
|
|
1156
1536
|
return new v3_1.InputStreamOncePromise((resolve, reject) => {
|
|
1157
|
-
|
|
1158
|
-
.
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
resolve({
|
|
1167
|
-
ok: true,
|
|
1168
|
-
output: { stop: true, message: result.output.message },
|
|
1169
|
-
});
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1537
|
+
chatInputRouter()
|
|
1538
|
+
.next(CHAT_ROUTE_STOP, { timeoutMs: options?.timeoutMs })
|
|
1539
|
+
.then((record) => {
|
|
1540
|
+
if (!record) {
|
|
1541
|
+
resolve({
|
|
1542
|
+
ok: false,
|
|
1543
|
+
error: new v3_1.InputStreamTimeoutError("chat-stop", options?.timeoutMs ?? 0),
|
|
1544
|
+
});
|
|
1545
|
+
return;
|
|
1172
1546
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
[v3_1.SemanticInternalAttributes.ENTITY_TYPE]: "input-stream",
|
|
1177
|
-
...(runId
|
|
1178
|
-
? {
|
|
1179
|
-
[v3_1.SemanticInternalAttributes.ENTITY_ID]: `${runId}:chat-stop`,
|
|
1180
|
-
}
|
|
1181
|
-
: {}),
|
|
1182
|
-
streamId: "chat-stop",
|
|
1183
|
-
...(0, v3_1.accessoryAttributes)({
|
|
1184
|
-
items: [{ text: "chat-stop", variant: "normal" }],
|
|
1185
|
-
style: "codepath",
|
|
1186
|
-
}),
|
|
1187
|
-
},
|
|
1188
|
-
})
|
|
1189
|
-
.catch(reject);
|
|
1547
|
+
const chunk = record.data;
|
|
1548
|
+
resolve({ ok: true, output: { stop: true, message: chunk.message } });
|
|
1549
|
+
}, reject);
|
|
1190
1550
|
});
|
|
1191
1551
|
},
|
|
1192
1552
|
peek() {
|
|
1193
|
-
const
|
|
1194
|
-
if (
|
|
1195
|
-
return
|
|
1196
|
-
|
|
1197
|
-
return
|
|
1553
|
+
const record = chatInputRouter().peek(CHAT_ROUTE_STOP);
|
|
1554
|
+
if (!record)
|
|
1555
|
+
return undefined;
|
|
1556
|
+
const chunk = record.data;
|
|
1557
|
+
return { stop: true, message: chunk.message };
|
|
1198
1558
|
},
|
|
1199
1559
|
wait(options) {
|
|
1200
1560
|
return new v3_1.ManualWaitpointPromise(async (resolve, reject) => {
|
|
1201
1561
|
try {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
return;
|
|
1207
|
-
}
|
|
1208
|
-
if (result.output.kind === "stop") {
|
|
1209
|
-
resolve({
|
|
1210
|
-
ok: true,
|
|
1211
|
-
output: { stop: true, message: result.output.message },
|
|
1212
|
-
});
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1562
|
+
const result = await waitOnChatRoute(CHAT_ROUTE_STOP, { timeout: options?.timeout, spanName: options?.spanName });
|
|
1563
|
+
resolve(result.ok
|
|
1564
|
+
? { ok: true, output: { stop: true, message: result.output.message } }
|
|
1565
|
+
: { ok: false, error: result.error ?? new Error("Timed out") });
|
|
1216
1566
|
}
|
|
1217
1567
|
catch (error) {
|
|
1218
1568
|
reject(error);
|
|
@@ -1220,14 +1570,10 @@ const stopInput = {
|
|
|
1220
1570
|
});
|
|
1221
1571
|
},
|
|
1222
1572
|
async waitWithIdleTimeout(options) {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
if (result.output.kind === "stop") {
|
|
1228
|
-
return { ok: true, output: { stop: true, message: result.output.message } };
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1573
|
+
const result = await waitOnChatRoute(CHAT_ROUTE_STOP, options);
|
|
1574
|
+
return result.ok
|
|
1575
|
+
? { ok: true, output: { stop: true, message: result.output.message } }
|
|
1576
|
+
: { ok: false, error: result.error };
|
|
1231
1577
|
},
|
|
1232
1578
|
async send(_runId, data, options) {
|
|
1233
1579
|
await getChatSession().in.send({ kind: "stop", message: data?.message }, options?.requestOptions);
|
|
@@ -1241,17 +1587,8 @@ const stopInput = {
|
|
|
1241
1587
|
*/
|
|
1242
1588
|
const handoverInput = {
|
|
1243
1589
|
async waitWithIdleTimeout(options) {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
if (!result.ok)
|
|
1247
|
-
return result;
|
|
1248
|
-
if (result.output.kind === "handover" || result.output.kind === "handover-skip") {
|
|
1249
|
-
return { ok: true, output: result.output };
|
|
1250
|
-
}
|
|
1251
|
-
// Other kinds (message, stop) are not expected during handover-prepare.
|
|
1252
|
-
// Loop back; the message and stop facades have their own listeners
|
|
1253
|
-
// running so signals on those kinds aren't lost.
|
|
1254
|
-
}
|
|
1590
|
+
const result = await waitOnChatRoute(CHAT_ROUTE_HANDOVER, options);
|
|
1591
|
+
return result.ok ? { ok: true, output: result.output } : result;
|
|
1255
1592
|
},
|
|
1256
1593
|
};
|
|
1257
1594
|
/**
|
|
@@ -1267,22 +1604,229 @@ const handoverInput = {
|
|
|
1267
1604
|
* For the common case prefer `accumulator.consumeHandover()`, which also seeds
|
|
1268
1605
|
* `payload.headStartMessages` and applies the partial for you.
|
|
1269
1606
|
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1272
|
-
* handover signal.
|
|
1607
|
+
* Safe to call at any point in turn 0: the handover signal has its own route,
|
|
1608
|
+
* so a message facade waiting at the same time cannot take it.
|
|
1273
1609
|
*/
|
|
1274
1610
|
async function waitForHandover(options) {
|
|
1275
1611
|
if (options.payload.trigger !== "handover-prepare")
|
|
1276
1612
|
return null;
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1613
|
+
try {
|
|
1614
|
+
const result = await handoverInput.waitWithIdleTimeout({
|
|
1615
|
+
idleTimeoutInSeconds: options.idleTimeoutInSeconds ?? options.payload.idleTimeoutInSeconds ?? 60,
|
|
1616
|
+
timeout: options.timeout,
|
|
1617
|
+
spanName: options.spanName ?? "waiting for handover signal",
|
|
1618
|
+
});
|
|
1619
|
+
// Non-ok = idle timeout or the warm handler crashed without signaling.
|
|
1620
|
+
if (!result.ok)
|
|
1621
|
+
return null;
|
|
1622
|
+
return result.output;
|
|
1623
|
+
}
|
|
1624
|
+
finally {
|
|
1625
|
+
chatInputRouter().clearRoute(CHAT_ROUTE_HANDOVER);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Everything `session.in` carries, and what happens to each kind.
|
|
1630
|
+
*
|
|
1631
|
+
* A route's two properties are what make the resume protocol derivable rather
|
|
1632
|
+
* than hand-maintained. `messages` is replayable because losing a user message
|
|
1633
|
+
* is data loss. `stop` is neither queued nor replayable: it only means anything
|
|
1634
|
+
* to the turn that is live when it lands, and a replayed one would abort
|
|
1635
|
+
* whichever turn happened to be running. `handover` is queued but not
|
|
1636
|
+
* replayable, because it can arrive before its consumer is ready yet is
|
|
1637
|
+
* meaningless to any later boot.
|
|
1638
|
+
*
|
|
1639
|
+
* A kind absent from this table has no consumer, so the router discards it
|
|
1640
|
+
* instead of letting it park at the head of a queue.
|
|
1641
|
+
* @internal
|
|
1642
|
+
*/
|
|
1643
|
+
const CHAT_INPUT_ROUTES = {
|
|
1644
|
+
kindOf: (data) => data?.kind,
|
|
1645
|
+
routes: [
|
|
1646
|
+
{ name: "messages", delivery: "queue", replayable: true, kinds: ["message"] },
|
|
1647
|
+
{ name: "stop", delivery: "at-arrival", replayable: false, kinds: ["stop"] },
|
|
1648
|
+
{
|
|
1649
|
+
name: "handover",
|
|
1650
|
+
delivery: "queue",
|
|
1651
|
+
replayable: false,
|
|
1652
|
+
kinds: ["handover", "handover-skip"],
|
|
1653
|
+
},
|
|
1654
|
+
],
|
|
1655
|
+
};
|
|
1656
|
+
const CHAT_ROUTE_MESSAGES = "messages";
|
|
1657
|
+
const CHAT_ROUTE_STOP = "stop";
|
|
1658
|
+
const CHAT_ROUTE_HANDOVER = "handover";
|
|
1659
|
+
/**
|
|
1660
|
+
* The `.in` router for the run this worker is currently serving.
|
|
1661
|
+
*
|
|
1662
|
+
* One slot rather than a map, because the facades have to reach the same router
|
|
1663
|
+
* the boot attached without depending on a locals scope being active. Tagged
|
|
1664
|
+
* with the run as well as the chat: a warm process is reused across runs and the
|
|
1665
|
+
* executor tears the channel subscription down at the end of each one, so
|
|
1666
|
+
* reusing a router across runs would leave the new run with no input at all. A
|
|
1667
|
+
* nested `chat.createSession` within the same run still shares it.
|
|
1668
|
+
* @internal
|
|
1669
|
+
*/
|
|
1670
|
+
let currentChatInputRouter;
|
|
1671
|
+
/**
|
|
1672
|
+
* Both cursors from the latest `turn-complete` on `.out`, in one scan.
|
|
1673
|
+
*
|
|
1674
|
+
* Absent for a chat whose turns predate the headers, in which case the router
|
|
1675
|
+
* starts from the beginning of the channel and nothing is treated as replayed.
|
|
1676
|
+
* @internal
|
|
1677
|
+
*/
|
|
1678
|
+
async function findLatestSessionInCheckpoint(chatId) {
|
|
1679
|
+
const apiClient = v3_1.apiClientManager.clientOrThrow();
|
|
1680
|
+
const response = await apiClient.readSessionStreamRecords(chatId, "out");
|
|
1681
|
+
const checkpoint = {};
|
|
1682
|
+
for (const record of response.records) {
|
|
1683
|
+
if ((0, v3_1.controlSubtype)(record.headers) !== v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE)
|
|
1684
|
+
continue;
|
|
1685
|
+
const resumeFrom = Number.parseInt((0, v3_1.headerValue)(record.headers, v3_1.SESSION_IN_EVENT_ID_HEADER) ?? "", 10);
|
|
1686
|
+
if (Number.isFinite(resumeFrom))
|
|
1687
|
+
checkpoint.resumeFrom = resumeFrom;
|
|
1688
|
+
const appliedThrough = Number.parseInt((0, v3_1.headerValue)(record.headers, v3_1.SESSION_IN_CONSUMED_ID_HEADER) ?? "", 10);
|
|
1689
|
+
if (Number.isFinite(appliedThrough))
|
|
1690
|
+
checkpoint.appliedThrough = appliedThrough;
|
|
1691
|
+
}
|
|
1692
|
+
return checkpoint;
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* The highest `.in` sequence already on the channel at boot, above the floor.
|
|
1696
|
+
*
|
|
1697
|
+
* Everything already on the channel when a run boots is, by definition, not
|
|
1698
|
+
* arriving live on this run, so the channel's own tail is the end of this run's
|
|
1699
|
+
* replay window.
|
|
1700
|
+
*
|
|
1701
|
+
* The turn boundary's value is not enough on its own. A boundary is written when
|
|
1702
|
+
* a turn ends, so a control record that arrived after the last boundary is not
|
|
1703
|
+
* covered by it, and a boundary written by an older SDK does not carry one at
|
|
1704
|
+
* all. Both cases leave an already-applied record looking live.
|
|
1705
|
+
*
|
|
1706
|
+
* Bounded by `afterEventId` when a floor is known, which is the common case, so
|
|
1707
|
+
* the read covers the replay window rather than the conversation. An absent
|
|
1708
|
+
* floor means no boundary has committed a cursor yet, and the channel is still
|
|
1709
|
+
* short.
|
|
1710
|
+
*
|
|
1711
|
+
* Returns `undefined` if the read fails; the caller then falls back to the
|
|
1712
|
+
* floor, which is the previous release's behaviour.
|
|
1713
|
+
* @internal
|
|
1714
|
+
*/
|
|
1715
|
+
async function findSessionInReplayWindowEnd(chatId, afterSeqNum) {
|
|
1716
|
+
try {
|
|
1717
|
+
const apiClient = v3_1.apiClientManager.clientOrThrow();
|
|
1718
|
+
const response = await apiClient.readSessionStreamRecords(chatId, "in", {
|
|
1719
|
+
...(afterSeqNum === undefined ? {} : { afterEventId: String(afterSeqNum) }),
|
|
1720
|
+
});
|
|
1721
|
+
let highest;
|
|
1722
|
+
for (const record of response.records) {
|
|
1723
|
+
const seqNum = typeof record.seqNum === "number" ? record.seqNum : Number.NaN;
|
|
1724
|
+
if (Number.isFinite(seqNum) && (highest === undefined || seqNum > highest)) {
|
|
1725
|
+
highest = seqNum;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return highest;
|
|
1729
|
+
}
|
|
1730
|
+
catch {
|
|
1731
|
+
return undefined;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Attach the `.in` router for this run.
|
|
1736
|
+
*
|
|
1737
|
+
* Reads the checkpoint and subscribes in one call, so there is no window in
|
|
1738
|
+
* which a listener is attached before the resume cursor is seeded. Attaching
|
|
1739
|
+
* first would open the tail at sequence 0 and replay every record the previous
|
|
1740
|
+
* run already answered, which is a mistake the previous shape of this code made
|
|
1741
|
+
* possible and this shape does not.
|
|
1742
|
+
*
|
|
1743
|
+
* The router consumes every record at dispatch, so the channel's own buffer
|
|
1744
|
+
* stays empty for chat and its cursor bookkeeping never engages. Delivery,
|
|
1745
|
+
* ordering and the published cursors are the router's, entirely.
|
|
1746
|
+
* @internal
|
|
1747
|
+
*/
|
|
1748
|
+
async function installChatInputRouter(chatId, options) {
|
|
1749
|
+
const entry = chatInputRouterEntry(chatId);
|
|
1750
|
+
if (entry.attached)
|
|
1751
|
+
return entry.router;
|
|
1752
|
+
let checkpoint = {};
|
|
1753
|
+
try {
|
|
1754
|
+
checkpoint = await findLatestSessionInCheckpoint(chatId);
|
|
1755
|
+
}
|
|
1756
|
+
catch (error) {
|
|
1757
|
+
v3_1.logger.warn("chat session: session.in resume cursor lookup failed; old messages may replay", {
|
|
1758
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
if (checkpoint.resumeFrom === undefined && options?.fallbackResumeFrom !== undefined) {
|
|
1762
|
+
checkpoint.resumeFrom = options.fallbackResumeFrom;
|
|
1763
|
+
}
|
|
1764
|
+
// Only a resuming run has a replay window. On a first boot nothing has been
|
|
1765
|
+
// applied by anyone, so treating what is already on the channel as replayed
|
|
1766
|
+
// would discard a signal that arrived before the agent got here, which is
|
|
1767
|
+
// exactly how a head-start handover reaches a cold run.
|
|
1768
|
+
const resuming = checkpoint.resumeFrom !== undefined || options?.resuming === true;
|
|
1769
|
+
if (resuming) {
|
|
1770
|
+
const replayWindowEnd = await findSessionInReplayWindowEnd(chatId, checkpoint.resumeFrom);
|
|
1771
|
+
if (replayWindowEnd !== undefined) {
|
|
1772
|
+
checkpoint.appliedThrough = Math.max(checkpoint.appliedThrough ?? replayWindowEnd, replayWindowEnd);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
const router = entry.router;
|
|
1776
|
+
router.restore(checkpoint);
|
|
1777
|
+
const floor = router.resumeFrom();
|
|
1778
|
+
if (floor !== undefined) {
|
|
1779
|
+
v3_1.sessionStreams.setLastSeqNum(chatId, "in", floor);
|
|
1780
|
+
v3_1.sessionStreams.setLastDispatchedSeqNum(chatId, "in", floor);
|
|
1781
|
+
}
|
|
1782
|
+
v3_1.sessionStreams.onRecord(chatId, "in", (record) => {
|
|
1783
|
+
router.ingest(record);
|
|
1784
|
+
return true;
|
|
1281
1785
|
});
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1786
|
+
entry.attached = true;
|
|
1787
|
+
return router;
|
|
1788
|
+
}
|
|
1789
|
+
function chatInputRouterEntry(chatId) {
|
|
1790
|
+
const runId = v3_1.taskContext.ctx?.run.id;
|
|
1791
|
+
if (currentChatInputRouter?.chatId === chatId && currentChatInputRouter.runId === runId) {
|
|
1792
|
+
return currentChatInputRouter;
|
|
1793
|
+
}
|
|
1794
|
+
currentChatInputRouter = {
|
|
1795
|
+
chatId,
|
|
1796
|
+
runId,
|
|
1797
|
+
router: new v3_1.SessionChannelRouter(CHAT_INPUT_ROUTES, {
|
|
1798
|
+
onDrop: (record, reason) => {
|
|
1799
|
+
if (reason === "unroutable" || reason === "malformed") {
|
|
1800
|
+
v3_1.logger.warn("chat: discarded a session.in record nothing on this worker can consume", {
|
|
1801
|
+
reason,
|
|
1802
|
+
seqNum: record.seqNum,
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
},
|
|
1806
|
+
}),
|
|
1807
|
+
attached: false,
|
|
1808
|
+
};
|
|
1809
|
+
return currentChatInputRouter;
|
|
1810
|
+
}
|
|
1811
|
+
/** Drop the router so the next boot attaches a fresh one. @internal */
|
|
1812
|
+
function __resetChatInputRouterForTests() {
|
|
1813
|
+
currentChatInputRouter = undefined;
|
|
1814
|
+
}
|
|
1815
|
+
/** The cursors this run would publish on its next turn boundary. @internal */
|
|
1816
|
+
function __chatInputCheckpointForTests() {
|
|
1817
|
+
return currentChatInputRouter?.router.checkpoint() ?? {};
|
|
1818
|
+
}
|
|
1819
|
+
/** Test-only entry point for the turn-boundary cursor scan. @internal */
|
|
1820
|
+
async function __findLatestSessionInCheckpointForTests(chatId) {
|
|
1821
|
+
return findLatestSessionInCheckpoint(chatId);
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* This chat's router. Created on first use so a facade reached before the
|
|
1825
|
+
* install still shares the one the install will attach.
|
|
1826
|
+
* @internal
|
|
1827
|
+
*/
|
|
1828
|
+
function chatInputRouter() {
|
|
1829
|
+
return chatInputRouterEntry(getChatSession().id).router;
|
|
1286
1830
|
}
|
|
1287
1831
|
/**
|
|
1288
1832
|
* Per-turn deferred promises. Registered via `chat.defer()`, awaited
|
|
@@ -1878,6 +2422,30 @@ const chatCompactionStateKey = locals_js_1.locals.create("chat.compaction");
|
|
|
1878
2422
|
const chatOnCompactedKey = locals_js_1.locals.create("chat.onCompacted");
|
|
1879
2423
|
/** @internal Full task `ctx` for the active `chat.agent` run (for hooks invoked from nested compaction). */
|
|
1880
2424
|
const chatAgentRunContextKey = locals_js_1.locals.create("chat.agentRunContext");
|
|
2425
|
+
/** @internal When a mid-turn validation failure writes its terminal error. */
|
|
2426
|
+
const chatCustomAgentClientDataErrorTimingKey = locals_js_1.locals.create("chat.customAgent.clientDataErrorTiming");
|
|
2427
|
+
/**
|
|
2428
|
+
* @internal Client-data errors held back until the open turn closes.
|
|
2429
|
+
*
|
|
2430
|
+
* The default timing exists so a bad send cannot truncate an answer already
|
|
2431
|
+
* being read, which means the write has to happen later rather than not at all:
|
|
2432
|
+
* the callback and the task log are server-side, so dropping the stream write
|
|
2433
|
+
* would leave the client with no signal that its frame was rejected.
|
|
2434
|
+
*/
|
|
2435
|
+
const chatCustomAgentDeferredClientDataErrorsKey = locals_js_1.locals.create("chat.customAgent.deferredClientDataErrors");
|
|
2436
|
+
/**
|
|
2437
|
+
* @internal Sequences the validating observer has claimed.
|
|
2438
|
+
*
|
|
2439
|
+
* The observer watches without consuming, so a record it is still validating is
|
|
2440
|
+
* also visible to a turn's read. Claiming makes ownership single: a read that
|
|
2441
|
+
* pulls a claimed record puts it back and keeps waiting, so exactly one of them
|
|
2442
|
+
* validates it and the schema runs once per frame.
|
|
2443
|
+
*/
|
|
2444
|
+
const chatObserverClaimedSeqNumsKey = locals_js_1.locals.create("chat.customAgent.observerClaims");
|
|
2445
|
+
/** @internal Marks the root run created by `chat.customAgent()`. */
|
|
2446
|
+
const chatCustomAgentRunKey = locals_js_1.locals.create("chat.customAgentRun");
|
|
2447
|
+
/** @internal Number of active `chat.createSession()` iterators in this run. */
|
|
2448
|
+
const chatActiveSessionIteratorsKey = locals_js_1.locals.create("chat.createSession.activeIterators");
|
|
1881
2449
|
const chatPrepareMessagesKey = locals_js_1.locals.create("chat.prepareMessages");
|
|
1882
2450
|
/**
|
|
1883
2451
|
* @internal The raw `tools` option from `chat.agent({ tools })`, either a
|
|
@@ -2273,7 +2841,14 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2273
2841
|
return [];
|
|
2274
2842
|
const ctx = locals_js_1.locals.get(chatTurnContextKey);
|
|
2275
2843
|
const stepNumber = steps.length - 1;
|
|
2276
|
-
|
|
2844
|
+
/**
|
|
2845
|
+
* Snapshot, because `shouldInject` and `prepare` can await. A record arriving
|
|
2846
|
+
* during either is not in this batch, so it must not be consumed by it: the
|
|
2847
|
+
* callbacks never saw it and could not have injected it, and taking it would
|
|
2848
|
+
* lose a message that should become a later turn instead.
|
|
2849
|
+
*/
|
|
2850
|
+
const batch = [...queue];
|
|
2851
|
+
const uiMessages = batch.map((e) => e.uiMessage);
|
|
2277
2852
|
const batchEvent = {
|
|
2278
2853
|
messages: uiMessages,
|
|
2279
2854
|
modelMessages: messages,
|
|
@@ -2287,22 +2862,79 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2287
2862
|
const shouldInject = config.shouldInject ? await config.shouldInject(batchEvent) : false;
|
|
2288
2863
|
if (!shouldInject)
|
|
2289
2864
|
return [];
|
|
2290
|
-
|
|
2291
|
-
const messageTexts = uiMessages.map((m) => (m.parts ?? [])
|
|
2865
|
+
const textOfUIMessage = (m) => (m.parts ?? [])
|
|
2292
2866
|
.filter((p) => p.type === "text")
|
|
2293
2867
|
.map((p) => p.text)
|
|
2294
|
-
.join("") || ""
|
|
2868
|
+
.join("") || "";
|
|
2869
|
+
// Span attributes describe the offered batch; the chunk and callback below
|
|
2870
|
+
// describe what was actually claimed and injected.
|
|
2871
|
+
const messageTexts = uiMessages.map(textOfUIMessage);
|
|
2295
2872
|
const _previewText = messageTexts.length === 1 ? messageTexts[0].slice(0, 80) : `${queue.length} messages`;
|
|
2296
2873
|
return tracer_js_1.tracer.startActiveSpan("pending message injected", async () => {
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2874
|
+
/**
|
|
2875
|
+
* Claim before transforming, and inject only what was claimed.
|
|
2876
|
+
*
|
|
2877
|
+
* Injection is the point of consumption: the records were only observed
|
|
2878
|
+
* on arrival, so they are still queued on the router and still holding
|
|
2879
|
+
* the resume floor, and taking them here is what stops the same message
|
|
2880
|
+
* also being answered as a later turn. A declined batch never reaches
|
|
2881
|
+
* this line, so its records stay queued and become later turns, which is
|
|
2882
|
+
* what "messages queue for the next turn" means.
|
|
2883
|
+
*
|
|
2884
|
+
* A failed claim means something else already consumed that record while
|
|
2885
|
+
* `shouldInject` was awaiting, so it is already being answered as a turn
|
|
2886
|
+
* of its own. Injecting it as well would process the same message twice.
|
|
2887
|
+
* An entry with no `seqNum` did not come from a channel record (the
|
|
2888
|
+
* accumulator's own queue), so there is nothing to claim and it is kept.
|
|
2889
|
+
*/
|
|
2890
|
+
const router = chatInputRouter();
|
|
2891
|
+
const claimed = [];
|
|
2892
|
+
const takenRecords = [];
|
|
2893
|
+
for (const entry of batch) {
|
|
2894
|
+
if (entry.seqNum === undefined) {
|
|
2895
|
+
claimed.push(entry);
|
|
2896
|
+
continue;
|
|
2897
|
+
}
|
|
2898
|
+
const record = router.take(CHAT_ROUTE_MESSAGES, entry.seqNum);
|
|
2899
|
+
if (!record)
|
|
2900
|
+
continue;
|
|
2901
|
+
takenRecords.push(record);
|
|
2902
|
+
claimed.push(entry);
|
|
2903
|
+
}
|
|
2904
|
+
for (const entry of claimed) {
|
|
2905
|
+
const at = queue.indexOf(entry);
|
|
2906
|
+
if (at !== -1)
|
|
2907
|
+
queue.splice(at, 1);
|
|
2908
|
+
}
|
|
2909
|
+
if (claimed.length === 0)
|
|
2910
|
+
return [];
|
|
2911
|
+
/**
|
|
2912
|
+
* Give the claim back if the transform fails. `prepare` is caller code and
|
|
2913
|
+
* can throw; the records have already left the router by this point, so
|
|
2914
|
+
* without returning them a failed transform would consume the messages and
|
|
2915
|
+
* they would never be answered at all.
|
|
2916
|
+
*/
|
|
2917
|
+
const releaseClaim = () => {
|
|
2918
|
+
for (const record of takenRecords)
|
|
2919
|
+
router.untake(CHAT_ROUTE_MESSAGES, record);
|
|
2920
|
+
for (const entry of claimed)
|
|
2921
|
+
if (!queue.includes(entry))
|
|
2922
|
+
queue.push(entry);
|
|
2923
|
+
};
|
|
2924
|
+
const claimedUIMessages = claimed.map((e) => e.uiMessage);
|
|
2925
|
+
let injected;
|
|
2926
|
+
try {
|
|
2927
|
+
injected = config.prepare
|
|
2928
|
+
? await config.prepare({ ...batchEvent, messages: claimedUIMessages })
|
|
2929
|
+
: claimed.flatMap((e) => e.modelMessages);
|
|
2930
|
+
}
|
|
2931
|
+
catch (err) {
|
|
2932
|
+
releaseClaim();
|
|
2933
|
+
throw err;
|
|
2934
|
+
}
|
|
2303
2935
|
const injectedIds = locals_js_1.locals.get(chatInjectedMessageIdsKey);
|
|
2304
2936
|
if (injectedIds) {
|
|
2305
|
-
for (const m of
|
|
2937
|
+
for (const m of claimedUIMessages)
|
|
2306
2938
|
injectedIds.add(m.id);
|
|
2307
2939
|
}
|
|
2308
2940
|
// Write injection confirmation chunk to the stream so the frontend
|
|
@@ -2316,10 +2948,10 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2316
2948
|
type: ai_shared_js_1.PENDING_MESSAGE_INJECTED_TYPE,
|
|
2317
2949
|
id: (0, ai_runtime_js_1.generateId)(),
|
|
2318
2950
|
data: {
|
|
2319
|
-
messageIds:
|
|
2320
|
-
messages:
|
|
2951
|
+
messageIds: claimedUIMessages.map((m) => m.id),
|
|
2952
|
+
messages: claimedUIMessages.map((m) => ({
|
|
2321
2953
|
id: m.id,
|
|
2322
|
-
text:
|
|
2954
|
+
text: textOfUIMessage(m),
|
|
2323
2955
|
})),
|
|
2324
2956
|
},
|
|
2325
2957
|
});
|
|
@@ -2335,7 +2967,7 @@ async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
|
2335
2967
|
if (config.onInjected && injected.length > 0) {
|
|
2336
2968
|
try {
|
|
2337
2969
|
await config.onInjected({
|
|
2338
|
-
messages:
|
|
2970
|
+
messages: claimedUIMessages,
|
|
2339
2971
|
injectedModelMessages: injected,
|
|
2340
2972
|
chatId: ctx?.chatId ?? "",
|
|
2341
2973
|
turn: ctx?.turn ?? 0,
|
|
@@ -2769,7 +3401,11 @@ async function pipeChat(source, options) {
|
|
|
2769
3401
|
await waitUntilComplete();
|
|
2770
3402
|
}
|
|
2771
3403
|
function chatCustomAgent(options) {
|
|
2772
|
-
const { clientDataSchema, run: userRun, ...restOptions } = options;
|
|
3404
|
+
const { clientDataSchema, onClientDataValidationError, clientDataReportErrorAt, run: userRun, ...restOptions } = options;
|
|
3405
|
+
const parseClientData = clientDataSchema ? (0, v3_1.getSchemaParseFn)(clientDataSchema) : undefined;
|
|
3406
|
+
const parseClientDataSync = clientDataSchema
|
|
3407
|
+
? getChatCustomAgentSyncSchemaParseFn(clientDataSchema)
|
|
3408
|
+
: undefined;
|
|
2773
3409
|
const task = (0, shared_js_1.createTask)({
|
|
2774
3410
|
...restOptions,
|
|
2775
3411
|
triggerSource: "agent",
|
|
@@ -2788,6 +3424,17 @@ function chatCustomAgent(options) {
|
|
|
2788
3424
|
locals_js_1.locals.set(chatSessionHandleKey, sessions_js_1.sessions.open(payload.chatId));
|
|
2789
3425
|
locals_js_1.locals.set(chatExternalIdKey, payload.chatId);
|
|
2790
3426
|
locals_js_1.locals.set(chatAgentRunContextKey, runOptions.ctx);
|
|
3427
|
+
locals_js_1.locals.set(chatCustomAgentRunKey, true);
|
|
3428
|
+
if (parseClientData && parseClientDataSync) {
|
|
3429
|
+
locals_js_1.locals.set(chatCustomAgentClientDataParserKey, {
|
|
3430
|
+
parse: parseClientData,
|
|
3431
|
+
parseSync: parseClientDataSync,
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
if (onClientDataValidationError) {
|
|
3435
|
+
locals_js_1.locals.set(chatCustomAgentClientDataErrorHandlerKey, onClientDataValidationError);
|
|
3436
|
+
}
|
|
3437
|
+
locals_js_1.locals.set(chatCustomAgentClientDataErrorTimingKey, clientDataReportErrorAt ?? "turn-end");
|
|
2791
3438
|
// Initialize the turn-complete trim slot so `chat.writeTurnComplete`
|
|
2792
3439
|
// trims `session.out` back to the previous turn boundary. Without
|
|
2793
3440
|
// this the slot is undefined and the trim never runs, so `.out`
|
|
@@ -2796,11 +3443,65 @@ function chatCustomAgent(options) {
|
|
|
2796
3443
|
(0, streams_js_1.markChatAgentRunForStreamsWarning)();
|
|
2797
3444
|
v3_1.taskContext.setConversationId(payload.chatId);
|
|
2798
3445
|
stampConversationIdOnActiveSpan(payload.chatId);
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
3446
|
+
await installChatInputRouter(payload.chatId, {
|
|
3447
|
+
resuming: Boolean(payload.continuation),
|
|
3448
|
+
});
|
|
3449
|
+
// Keep the schema-free path identical to the original custom-agent
|
|
3450
|
+
// wrapper, including when userRun starts executing.
|
|
3451
|
+
if (!parseClientData) {
|
|
3452
|
+
return userRun(payload, runOptions);
|
|
3453
|
+
}
|
|
3454
|
+
const isHandoverBoot = payload.trigger === "handover-prepare";
|
|
3455
|
+
const isMessagelessBoot = payload.trigger === "preload" ||
|
|
3456
|
+
(payload.continuation === true &&
|
|
3457
|
+
payload.message === undefined &&
|
|
3458
|
+
payload.trigger !== "action" &&
|
|
3459
|
+
payload.trigger !== "regenerate-message" &&
|
|
3460
|
+
!isHandoverBoot);
|
|
3461
|
+
const validated = await validateChatCustomAgentPayload(payload, {
|
|
3462
|
+
// Preload and continuation boots do not represent a submitted turn,
|
|
3463
|
+
// so there is no sender waiting for a terminal frame. Handover errors
|
|
3464
|
+
// must be written after the warm response flushes and signals below.
|
|
3465
|
+
writeErrorToStream: !isMessagelessBoot && !isHandoverBoot,
|
|
3466
|
+
});
|
|
3467
|
+
if (validated.ok) {
|
|
3468
|
+
return userRun(validated.payload, runOptions);
|
|
3469
|
+
}
|
|
3470
|
+
if (isHandoverBoot) {
|
|
3471
|
+
const signal = await waitForHandover({
|
|
3472
|
+
payload,
|
|
3473
|
+
timeout: "1h",
|
|
3474
|
+
spanName: "waiting for handover signal (invalid clientData)",
|
|
3475
|
+
});
|
|
3476
|
+
if (!signal || signal.kind === "handover-skip") {
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
// The head-start writer flushes before sending this signal. Writing
|
|
3480
|
+
// the terminal error now preserves stream order and closes the stitch.
|
|
3481
|
+
await writeChatCustomAgentClientDataErrorToStream(payload);
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3484
|
+
// The Session base payload is sticky across continuation runs. If it is
|
|
3485
|
+
// invalid, returning here would boot the same bad metadata again on the
|
|
3486
|
+
// next message. Stay attached and wait for a valid wire frame instead.
|
|
3487
|
+
const next = await messagesInput.waitWithIdleTimeout({
|
|
3488
|
+
idleTimeoutInSeconds: payload.idleTimeoutInSeconds ?? 30,
|
|
3489
|
+
timeout: "1h",
|
|
3490
|
+
spanName: "waiting for valid clientData",
|
|
3491
|
+
});
|
|
3492
|
+
if (!next.ok || next.output.trigger === "close") {
|
|
3493
|
+
return;
|
|
3494
|
+
}
|
|
3495
|
+
// Normal input frames omit run-level boot context. Carry it forward so
|
|
3496
|
+
// a continuation still tells the custom loop to restore prior state.
|
|
3497
|
+
const recoveredPayload = {
|
|
3498
|
+
...next.output,
|
|
3499
|
+
continuation: next.output.continuation ?? payload.continuation,
|
|
3500
|
+
previousRunId: next.output.previousRunId ?? payload.previousRunId,
|
|
3501
|
+
sessionId: next.output.sessionId ?? payload.sessionId,
|
|
3502
|
+
idleTimeoutInSeconds: next.output.idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds,
|
|
3503
|
+
};
|
|
3504
|
+
return userRun(recoveredPayload, runOptions);
|
|
2804
3505
|
},
|
|
2805
3506
|
});
|
|
2806
3507
|
// Register clientDataSchema so the CLI converts it to JSONSchema
|
|
@@ -2830,6 +3531,7 @@ function chatAgent(options) {
|
|
|
2830
3531
|
agentConfig: { type: "ai-sdk-chat" },
|
|
2831
3532
|
run: async (payload, { signal: runSignal, ctx }) => {
|
|
2832
3533
|
locals_js_1.locals.set(chatAgentRunContextKey, ctx);
|
|
3534
|
+
locals_js_1.locals.set(chatCustomAgentRunKey, false);
|
|
2833
3535
|
// On AI SDK 7, register the `@ai-sdk/otel` integration (once per process)
|
|
2834
3536
|
// so `experimental_telemetry` spans flow into the run trace. Awaited here
|
|
2835
3537
|
// at run boot — before any `streamText` — and a no-op on v5/v6 or when the
|
|
@@ -2922,11 +3624,6 @@ function chatAgent(options) {
|
|
|
2922
3624
|
// default, `inFlightUsers`). The turn-loop checks this queue ahead of
|
|
2923
3625
|
// `messagesInput.waitWithIdleTimeout` so recovered turns fire first.
|
|
2924
3626
|
const bootInjectedQueue = [];
|
|
2925
|
-
// Messages consumed by a turn's `messagesInput.on` handler, dispatched
|
|
2926
|
-
// one per turn by the end-of-turn pickup. Loop-level on purpose:
|
|
2927
|
-
// consuming a record advances the committed `.in` cursor, so entries
|
|
2928
|
-
// dropped with a turn-local buffer are lost permanently.
|
|
2929
|
-
const pendingWireMessages = [];
|
|
2930
3627
|
const couldHavePriorState = payload.continuation === true || ctx.attempt.number > 1;
|
|
2931
3628
|
// `.in` resume cursor, computed at most once per boot. The boot
|
|
2932
3629
|
// block below resolves it (snapshot field or records scan) and the
|
|
@@ -3049,49 +3746,16 @@ function chatAgent(options) {
|
|
|
3049
3746
|
},
|
|
3050
3747
|
});
|
|
3051
3748
|
}
|
|
3052
|
-
// ── session.in
|
|
3053
|
-
//
|
|
3054
|
-
// A fresh worker subscribes to `session.in` from seq 0 and would
|
|
3055
|
-
// re-deliver every record ever appended — including user messages
|
|
3056
|
-
// from turns already completed on a prior run. Without a cursor,
|
|
3057
|
-
// the loop would re-process them as fresh turns and the slim-wire
|
|
3058
|
-
// merge would replace-by-id against snapshot-restored copies,
|
|
3059
|
-
// yielding no-op replaces while the customer's actual new message
|
|
3060
|
-
// waits in the queue.
|
|
3749
|
+
// ── session.in router ──────────────────────────────────────────
|
|
3061
3750
|
//
|
|
3062
|
-
//
|
|
3063
|
-
//
|
|
3064
|
-
//
|
|
3065
|
-
//
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
// Applies in three cases (any of which means `.in` has records
|
|
3071
|
-
// belonging to completed turns the new run should skip):
|
|
3072
|
-
// - OOM retry (`ctx.attempt.number > 1`)
|
|
3073
|
-
// - Continuation run (`payload.continuation === true`) — prior run
|
|
3074
|
-
// crashed / was canceled / requested upgrade
|
|
3075
|
-
// - Snapshot exists at all (catches edge cases where the wire
|
|
3076
|
-
// didn't set `continuation` but a snapshot indicates prior turns)
|
|
3077
|
-
const needsResumeCursor = ctx.attempt.number > 1 || payload.continuation === true || bootSnapshot !== undefined;
|
|
3078
|
-
if (needsResumeCursor) {
|
|
3079
|
-
try {
|
|
3080
|
-
// Reuse the cursor the boot block already resolved (snapshot
|
|
3081
|
-
// field or records scan) — only scan here when the boot block
|
|
3082
|
-
// was skipped (hydrateMessages, or snapshot-only signals).
|
|
3083
|
-
const cursor = bootInCursorResolved
|
|
3084
|
-
? bootInCursor
|
|
3085
|
-
: await findLatestSessionInCursor(payload.chatId);
|
|
3086
|
-
if (cursor !== undefined) {
|
|
3087
|
-
v3_1.sessionStreams.setLastSeqNum(payload.chatId, "in", cursor);
|
|
3088
|
-
v3_1.sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor);
|
|
3089
|
-
}
|
|
3090
|
-
}
|
|
3091
|
-
catch (error) {
|
|
3092
|
-
v3_1.logger.warn("chat.agent: session.in resume cursor lookup failed; old messages may replay", { error: error instanceof Error ? error.message : String(error) });
|
|
3093
|
-
}
|
|
3094
|
-
}
|
|
3751
|
+
// Reads the turn boundary and subscribes in one call. `bootInCursor` is
|
|
3752
|
+
// only a fallback: the boot block above may already have resolved a
|
|
3753
|
+
// cursor from the snapshot, which is used when the boundary itself
|
|
3754
|
+
// carries none.
|
|
3755
|
+
await installChatInputRouter(payload.chatId, {
|
|
3756
|
+
fallbackResumeFrom: bootInCursorResolved ? bootInCursor : undefined,
|
|
3757
|
+
resuming: Boolean(payload.continuation) || ctx.attempt.number > 1,
|
|
3758
|
+
});
|
|
3095
3759
|
// ── Recovery boot + chain reconstruction ────────────────────────
|
|
3096
3760
|
if (!hydrateMessages) {
|
|
3097
3761
|
const settledMessages = mergeByIdReplaceWins(bootSnapshot?.messages ?? [], replayedSettled);
|
|
@@ -3160,14 +3824,23 @@ function chatAgent(options) {
|
|
|
3160
3824
|
hookBeforeBoot = hookResult.beforeBoot;
|
|
3161
3825
|
}
|
|
3162
3826
|
}
|
|
3163
|
-
// Default: splice partial + the user it was answering into
|
|
3164
|
-
//
|
|
3827
|
+
// Default: splice partial + the user it was answering into the chain
|
|
3828
|
+
// so follow-ups like "keep going" still have context, and re-dispatch
|
|
3829
|
+
// the users that arrived after it.
|
|
3830
|
+
//
|
|
3831
|
+
// The splice needs a follow-up user to answer — it consumes
|
|
3832
|
+
// `inFlightUsers[0]` into the chain instead of dispatching it. With
|
|
3833
|
+
// exactly ONE in-flight user (the plain OOM / crash-mid-answer case)
|
|
3834
|
+
// there is nothing left to dispatch, so splicing would strand that
|
|
3835
|
+
// user unanswered and idle the run. Require `length > 1` on both
|
|
3836
|
+
// branches: at n=1 the orphan partial is dropped and the interrupted
|
|
3837
|
+
// user is re-dispatched as a fresh turn instead.
|
|
3165
3838
|
let seedChain;
|
|
3166
3839
|
let recoveredTurns;
|
|
3167
3840
|
if (hookChain !== undefined) {
|
|
3168
3841
|
seedChain = hookChain;
|
|
3169
3842
|
}
|
|
3170
|
-
else if (partialAssistant !== undefined && inFlightUsers.length >
|
|
3843
|
+
else if (partialAssistant !== undefined && inFlightUsers.length > 1) {
|
|
3171
3844
|
seedChain = [...settledMessages, inFlightUsers[0], partialAssistant];
|
|
3172
3845
|
}
|
|
3173
3846
|
else {
|
|
@@ -3176,7 +3849,7 @@ function chatAgent(options) {
|
|
|
3176
3849
|
if (hookRecoveredTurns !== undefined) {
|
|
3177
3850
|
recoveredTurns = hookRecoveredTurns;
|
|
3178
3851
|
}
|
|
3179
|
-
else if (partialAssistant !== undefined && inFlightUsers.length >
|
|
3852
|
+
else if (partialAssistant !== undefined && inFlightUsers.length > 1) {
|
|
3180
3853
|
recoveredTurns = inFlightUsers.slice(1);
|
|
3181
3854
|
}
|
|
3182
3855
|
else {
|
|
@@ -3729,11 +4402,18 @@ function chatAgent(options) {
|
|
|
3729
4402
|
const cancelSignal = runSignal;
|
|
3730
4403
|
const combinedSignal = AbortSignal.any([runSignal, stopController.signal]);
|
|
3731
4404
|
const pmConfig = locals_js_1.locals.get(chatPendingMessagesKey);
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
4405
|
+
/**
|
|
4406
|
+
* Only attached when there is a steering config to feed. Without
|
|
4407
|
+
* one a mid-turn message is left queued on the router, which is
|
|
4408
|
+
* what holds the resume floor behind it: a record handed to a
|
|
4409
|
+
* handler counts as terminally decided, so buffering one here
|
|
4410
|
+
* published a cursor past a message held only in memory and a
|
|
4411
|
+
* crash before the next turn lost it.
|
|
4412
|
+
*/
|
|
4413
|
+
const msgSub = pmConfig
|
|
4414
|
+
? chatInputRouter().observe(CHAT_ROUTE_MESSAGES, async (record) => {
|
|
4415
|
+
const msg = record.data
|
|
4416
|
+
.payload;
|
|
3737
4417
|
// Slim wire: at most one delta message per record. The
|
|
3738
4418
|
// pendingMessages handler reads `msg.message` directly
|
|
3739
4419
|
// instead of slicing an array — a wire record arrives
|
|
@@ -3764,6 +4444,7 @@ function chatAgent(options) {
|
|
|
3764
4444
|
queue.push({
|
|
3765
4445
|
uiMessage: lastUIMessage,
|
|
3766
4446
|
modelMessages: modelMsgs,
|
|
4447
|
+
seqNum: record.seqNum,
|
|
3767
4448
|
});
|
|
3768
4449
|
locals_js_1.locals.set(chatSteeringQueueKey, queue);
|
|
3769
4450
|
}
|
|
@@ -3771,11 +4452,8 @@ function chatAgent(options) {
|
|
|
3771
4452
|
/* conversion failed — skip steering queue */
|
|
3772
4453
|
}
|
|
3773
4454
|
}
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
// No pendingMessages config — standard wire buffer for next turn
|
|
3777
|
-
pendingWireMessages.push(msg);
|
|
3778
|
-
});
|
|
4455
|
+
})
|
|
4456
|
+
: undefined;
|
|
3779
4457
|
turnMsgSub = msgSub;
|
|
3780
4458
|
// Track new messages for this turn (user input + assistant response).
|
|
3781
4459
|
const turnNewModelMessages = [];
|
|
@@ -4097,7 +4775,7 @@ function chatAgent(options) {
|
|
|
4097
4775
|
// The turn counter is decremented so the next iteration
|
|
4098
4776
|
// sees the same `turn` value — actions don't count.
|
|
4099
4777
|
if (isAction) {
|
|
4100
|
-
msgSub
|
|
4778
|
+
msgSub?.off();
|
|
4101
4779
|
if ((locals_js_1.locals.get(chatPipeCountKey) ?? 0) === 0 &&
|
|
4102
4780
|
isUIMessageStreamable(actionStreamResult)) {
|
|
4103
4781
|
try {
|
|
@@ -4354,7 +5032,7 @@ function chatAgent(options) {
|
|
|
4354
5032
|
}
|
|
4355
5033
|
}
|
|
4356
5034
|
finally {
|
|
4357
|
-
msgSub
|
|
5035
|
+
msgSub?.off();
|
|
4358
5036
|
}
|
|
4359
5037
|
// Wait for onFinish to fire — on abort this may resolve slightly
|
|
4360
5038
|
// after pipeChat, since the stream's cancel() handler is async.
|
|
@@ -4798,7 +5476,7 @@ function chatAgent(options) {
|
|
|
4798
5476
|
if (!hydrateMessages) {
|
|
4799
5477
|
try {
|
|
4800
5478
|
await tracer_js_1.tracer.startActiveSpan("snapshot.write", async () => {
|
|
4801
|
-
const snapshotInCursor =
|
|
5479
|
+
const snapshotInCursor = chatInputRouter().resumeFloor();
|
|
4802
5480
|
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
4803
5481
|
version: 1,
|
|
4804
5482
|
savedAt: Date.now(),
|
|
@@ -4837,13 +5515,6 @@ function chatAgent(options) {
|
|
|
4837
5515
|
currentWirePayload = bootInjectedQueue.shift();
|
|
4838
5516
|
return "continue";
|
|
4839
5517
|
}
|
|
4840
|
-
// If messages arrived during streaming (without pendingMessages config),
|
|
4841
|
-
// dispatch the oldest as the next turn. The rest stay queued
|
|
4842
|
-
// and drain one per turn.
|
|
4843
|
-
if (pendingWireMessages.length > 0) {
|
|
4844
|
-
currentWirePayload = pendingWireMessages.shift();
|
|
4845
|
-
return "continue";
|
|
4846
|
-
}
|
|
4847
5518
|
// chat.requestUpgrade() was called — exit the loop so the
|
|
4848
5519
|
// transport triggers a new run on the latest version.
|
|
4849
5520
|
// chat.endRun() — same exit, no upgrade semantics.
|
|
@@ -5077,7 +5748,7 @@ function chatAgent(options) {
|
|
|
5077
5748
|
// neither the snapshot nor the replayable `.in` tail.
|
|
5078
5749
|
if (!hydrateMessages) {
|
|
5079
5750
|
try {
|
|
5080
|
-
const errorSnapshotInCursor =
|
|
5751
|
+
const errorSnapshotInCursor = chatInputRouter().resumeFloor();
|
|
5081
5752
|
await writeChatSnapshot(sessionIdForSnapshot, {
|
|
5082
5753
|
version: 1,
|
|
5083
5754
|
savedAt: Date.now(),
|
|
@@ -5104,11 +5775,6 @@ function chatAgent(options) {
|
|
|
5104
5775
|
currentWirePayload = bootInjectedQueue.shift();
|
|
5105
5776
|
continue;
|
|
5106
5777
|
}
|
|
5107
|
-
// Same for messages buffered during the errored turn — already consumed, idling strands them.
|
|
5108
|
-
if (pendingWireMessages.length > 0) {
|
|
5109
|
-
currentWirePayload = pendingWireMessages.shift();
|
|
5110
|
-
continue;
|
|
5111
|
-
}
|
|
5112
5778
|
// Wait for the next message — same as after a successful turn
|
|
5113
5779
|
const effectiveIdleTimeout = metadata_js_1.metadata.get(IDLE_TIMEOUT_METADATA_KEY) ??
|
|
5114
5780
|
idleTimeoutInSeconds;
|
|
@@ -5168,6 +5834,8 @@ function createChatBuilder(config) {
|
|
|
5168
5834
|
return createChatBuilder({
|
|
5169
5835
|
...config,
|
|
5170
5836
|
clientDataSchema: cdConfig.schema,
|
|
5837
|
+
clientDataReportErrorAt: cdConfig.reportErrorAt ?? config.clientDataReportErrorAt,
|
|
5838
|
+
clientDataOnValidationError: cdConfig.onValidationError ?? config.clientDataOnValidationError,
|
|
5171
5839
|
});
|
|
5172
5840
|
},
|
|
5173
5841
|
onBoot(fn) {
|
|
@@ -5248,6 +5916,10 @@ function createChatBuilder(config) {
|
|
|
5248
5916
|
return chatCustomAgent({
|
|
5249
5917
|
...options,
|
|
5250
5918
|
...(config.clientDataSchema ? { clientDataSchema: config.clientDataSchema } : {}),
|
|
5919
|
+
...(config.clientDataReportErrorAt
|
|
5920
|
+
? { clientDataReportErrorAt: config.clientDataReportErrorAt }
|
|
5921
|
+
: {}),
|
|
5922
|
+
onClientDataValidationError: composeHooks(config.clientDataOnValidationError, options.onClientDataValidationError),
|
|
5251
5923
|
});
|
|
5252
5924
|
},
|
|
5253
5925
|
};
|
|
@@ -5300,6 +5972,8 @@ function withUIMessage(config) {
|
|
|
5300
5972
|
function withClientData(config) {
|
|
5301
5973
|
return createChatBuilder({
|
|
5302
5974
|
clientDataSchema: config.schema,
|
|
5975
|
+
clientDataReportErrorAt: config.reportErrorAt,
|
|
5976
|
+
clientDataOnValidationError: config.onValidationError,
|
|
5303
5977
|
hooks: {},
|
|
5304
5978
|
});
|
|
5305
5979
|
}
|
|
@@ -5490,6 +6164,58 @@ function isStopped() {
|
|
|
5490
6164
|
function requestUpgrade() {
|
|
5491
6165
|
locals_js_1.locals.set(chatUpgradeRequestedKey, true);
|
|
5492
6166
|
}
|
|
6167
|
+
/**
|
|
6168
|
+
* Hand off the current custom agent Session to a fresh run.
|
|
6169
|
+
*
|
|
6170
|
+
* This is the low-level handoff for a fully hand-rolled
|
|
6171
|
+
* `chat.customAgent()` loop. This method rejects while a
|
|
6172
|
+
* `chat.createSession()` iterator is active. Close the iterator before calling
|
|
6173
|
+
* it. If `return()` races an active `next()`, it waits for that read to settle
|
|
6174
|
+
* before releasing the handoff guard. Call only between turns and after
|
|
6175
|
+
* detaching input listeners for the old run. If the old run completed its
|
|
6176
|
+
* current turn, persist its state and call {@link chatWriteTurnComplete} before
|
|
6177
|
+
* handing off.
|
|
6178
|
+
* Do not write a new turn boundary after input that the continuation run should
|
|
6179
|
+
* process has been dispatched: the boundary acknowledges that input.
|
|
6180
|
+
*
|
|
6181
|
+
* The server starts the continuation run but does not stop this run, so return
|
|
6182
|
+
* from the task immediately after awaiting this function. The promise rejects
|
|
6183
|
+
* if the server cannot complete the handoff.
|
|
6184
|
+
*
|
|
6185
|
+
* Pending Session input that the old run has not consumed remains on the
|
|
6186
|
+
* durable `.in` stream and is delivered to the continuation run.
|
|
6187
|
+
*
|
|
6188
|
+
* @example
|
|
6189
|
+
* ```ts
|
|
6190
|
+
* // Detach any chat.messages.on() subscriptions you created.
|
|
6191
|
+
* await persistMessages();
|
|
6192
|
+
* await chat.writeTurnComplete();
|
|
6193
|
+
* await chat.endAndContinue();
|
|
6194
|
+
* return;
|
|
6195
|
+
* ```
|
|
6196
|
+
*/
|
|
6197
|
+
async function endAndContinue() {
|
|
6198
|
+
if (locals_js_1.locals.get(chatCustomAgentRunKey) !== true) {
|
|
6199
|
+
throw new Error("chat.endAndContinue() can only be called from inside a chat.customAgent() run");
|
|
6200
|
+
}
|
|
6201
|
+
if ((locals_js_1.locals.get(chatActiveSessionIteratorsKey) ?? 0) > 0) {
|
|
6202
|
+
throw new Error("chat.endAndContinue() cannot be called while a chat.createSession() iterator is active. Close the iterator, then call chat.endAndContinue().");
|
|
6203
|
+
}
|
|
6204
|
+
await performEndAndContinue();
|
|
6205
|
+
}
|
|
6206
|
+
/** @internal Shared server handoff used by managed and custom agent loops. */
|
|
6207
|
+
async function performEndAndContinue() {
|
|
6208
|
+
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
6209
|
+
const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
|
|
6210
|
+
if (!chatId || !callingRunId) {
|
|
6211
|
+
throw new Error("Cannot end and continue without an active chat agent run");
|
|
6212
|
+
}
|
|
6213
|
+
const apiClient = v3_1.apiClientManager.clientOrThrow();
|
|
6214
|
+
await apiClient.endAndContinueSession(chatId, {
|
|
6215
|
+
callingRunId,
|
|
6216
|
+
reason: "upgrade",
|
|
6217
|
+
});
|
|
6218
|
+
}
|
|
5493
6219
|
/**
|
|
5494
6220
|
* Exit the run after the current turn completes, without waiting for the
|
|
5495
6221
|
* next message. Unlike {@link requestUpgrade}, no upgrade-required signal
|
|
@@ -5695,10 +6421,14 @@ function createStopSignal() {
|
|
|
5695
6421
|
* task instead of round-tripping them back from the client:
|
|
5696
6422
|
* - `lastEventId` — the turn-complete control record's seq_num on
|
|
5697
6423
|
* `session.out`; where the next turn's output stream resumes.
|
|
5698
|
-
* - `sessionInEventId` — the
|
|
5699
|
-
* this turn-complete
|
|
5700
|
-
*
|
|
5701
|
-
*
|
|
6424
|
+
* - `sessionInEventId` — the safe-to-resume-from cursor on `session.in` as of
|
|
6425
|
+
* this turn-complete. It is the highest sequence that can be resumed past
|
|
6426
|
+
* without skipping an unhandled message, so it is held back behind any
|
|
6427
|
+
* message still buffered unconsumed and is NOT necessarily the sequence of
|
|
6428
|
+
* the record this turn answered. Trigger owns input-cursor recovery, so this
|
|
6429
|
+
* is for correlation / out-of-sync detection, not required. Treat it as a
|
|
6430
|
+
* lower bound: a value below the record you just handled is expected, not a
|
|
6431
|
+
* sign of a lost turn.
|
|
5702
6432
|
*
|
|
5703
6433
|
* Either is `undefined` when the corresponding cursor isn't available.
|
|
5704
6434
|
*
|
|
@@ -5710,15 +6440,36 @@ function createStopSignal() {
|
|
|
5710
6440
|
* ```
|
|
5711
6441
|
*/
|
|
5712
6442
|
async function chatWriteTurnComplete(options) {
|
|
6443
|
+
await flushDeferredChatCustomAgentClientDataErrors();
|
|
5713
6444
|
const result = await writeTurnCompleteChunk(undefined, options?.publicAccessToken);
|
|
5714
6445
|
// Same cursor written to the `session-in-event-id` header inside
|
|
5715
6446
|
// `writeTurnCompleteChunk`; surfaced here so the caller can persist it.
|
|
5716
|
-
const inCursor =
|
|
6447
|
+
const inCursor = chatInputRouter().resumeFloor();
|
|
5717
6448
|
return {
|
|
5718
6449
|
lastEventId: result?.lastEventId,
|
|
5719
6450
|
...(inCursor !== undefined ? { sessionInEventId: String(inCursor) } : {}),
|
|
5720
6451
|
};
|
|
5721
6452
|
}
|
|
6453
|
+
/**
|
|
6454
|
+
* Writes the client-data errors held back by the default `"turn-end"` timing.
|
|
6455
|
+
*
|
|
6456
|
+
* Ordered before the turn-complete chunk, matching the error-then-turn-complete
|
|
6457
|
+
* shape the submitted-turn and async-read paths already write.
|
|
6458
|
+
*/
|
|
6459
|
+
async function flushDeferredChatCustomAgentClientDataErrors() {
|
|
6460
|
+
const deferred = locals_js_1.locals.get(chatCustomAgentDeferredClientDataErrorsKey);
|
|
6461
|
+
if (!deferred || deferred.length === 0)
|
|
6462
|
+
return;
|
|
6463
|
+
locals_js_1.locals.set(chatCustomAgentDeferredClientDataErrorsKey, []);
|
|
6464
|
+
for (const payload of deferred) {
|
|
6465
|
+
try {
|
|
6466
|
+
await writeChatCustomAgentClientDataErrorToStream(payload);
|
|
6467
|
+
}
|
|
6468
|
+
catch {
|
|
6469
|
+
/* non-fatal */
|
|
6470
|
+
}
|
|
6471
|
+
}
|
|
6472
|
+
}
|
|
5722
6473
|
/**
|
|
5723
6474
|
* Pass every chunk through untouched while recording it in `buffer`. Handles
|
|
5724
6475
|
* both the `AsyncIterable` and `ReadableStream` shapes `toUIMessageStream()`
|
|
@@ -6105,6 +6856,95 @@ class ChatMessageAccumulator {
|
|
|
6105
6856
|
return true;
|
|
6106
6857
|
}
|
|
6107
6858
|
}
|
|
6859
|
+
function trackActiveChatSessionIterator(iterator) {
|
|
6860
|
+
locals_js_1.locals.set(chatActiveSessionIteratorsKey, (locals_js_1.locals.get(chatActiveSessionIteratorsKey) ?? 0) + 1);
|
|
6861
|
+
let active = true;
|
|
6862
|
+
let closing = false;
|
|
6863
|
+
let activeNextCalls = 0;
|
|
6864
|
+
let closePromise;
|
|
6865
|
+
const nextSettledWaiters = new Set();
|
|
6866
|
+
function finish() {
|
|
6867
|
+
if (!active)
|
|
6868
|
+
return;
|
|
6869
|
+
active = false;
|
|
6870
|
+
const remaining = Math.max((locals_js_1.locals.get(chatActiveSessionIteratorsKey) ?? 1) - 1, 0);
|
|
6871
|
+
locals_js_1.locals.set(chatActiveSessionIteratorsKey, remaining);
|
|
6872
|
+
}
|
|
6873
|
+
function settleNextCall() {
|
|
6874
|
+
activeNextCalls = Math.max(activeNextCalls - 1, 0);
|
|
6875
|
+
if (activeNextCalls > 0)
|
|
6876
|
+
return;
|
|
6877
|
+
for (const resolve of nextSettledWaiters) {
|
|
6878
|
+
resolve();
|
|
6879
|
+
}
|
|
6880
|
+
nextSettledWaiters.clear();
|
|
6881
|
+
}
|
|
6882
|
+
function waitForNextCalls() {
|
|
6883
|
+
if (activeNextCalls === 0)
|
|
6884
|
+
return Promise.resolve();
|
|
6885
|
+
return new Promise((resolve) => nextSettledWaiters.add(resolve));
|
|
6886
|
+
}
|
|
6887
|
+
function closeIterator() {
|
|
6888
|
+
closing = true;
|
|
6889
|
+
if (!closePromise) {
|
|
6890
|
+
closePromise = (async () => {
|
|
6891
|
+
// A blocked next() can install a new message listener after it resumes.
|
|
6892
|
+
// Let every started call settle, then make the inner cleanup final.
|
|
6893
|
+
await waitForNextCalls();
|
|
6894
|
+
try {
|
|
6895
|
+
return iterator.return
|
|
6896
|
+
? await iterator.return()
|
|
6897
|
+
: { done: true, value: undefined };
|
|
6898
|
+
}
|
|
6899
|
+
finally {
|
|
6900
|
+
finish();
|
|
6901
|
+
}
|
|
6902
|
+
})();
|
|
6903
|
+
}
|
|
6904
|
+
return closePromise;
|
|
6905
|
+
}
|
|
6906
|
+
return {
|
|
6907
|
+
async next() {
|
|
6908
|
+
if (closing) {
|
|
6909
|
+
return { done: true, value: undefined };
|
|
6910
|
+
}
|
|
6911
|
+
activeNextCalls++;
|
|
6912
|
+
let result;
|
|
6913
|
+
try {
|
|
6914
|
+
result = await iterator.next();
|
|
6915
|
+
}
|
|
6916
|
+
catch (error) {
|
|
6917
|
+
settleNextCall();
|
|
6918
|
+
try {
|
|
6919
|
+
await closeIterator();
|
|
6920
|
+
}
|
|
6921
|
+
catch {
|
|
6922
|
+
// Preserve the original iterator error after best-effort cleanup.
|
|
6923
|
+
}
|
|
6924
|
+
throw error;
|
|
6925
|
+
}
|
|
6926
|
+
settleNextCall();
|
|
6927
|
+
if (result.done) {
|
|
6928
|
+
try {
|
|
6929
|
+
await closeIterator();
|
|
6930
|
+
}
|
|
6931
|
+
catch {
|
|
6932
|
+
// The inner next() already ended cleanly; cleanup remains best-effort.
|
|
6933
|
+
}
|
|
6934
|
+
}
|
|
6935
|
+
else if (closing) {
|
|
6936
|
+
// return() won the race. Do not expose a turn the caller has already
|
|
6937
|
+
// abandoned; without a new turn-complete boundary its input remains
|
|
6938
|
+
// replayable by the continuation run.
|
|
6939
|
+
return { done: true, value: undefined };
|
|
6940
|
+
}
|
|
6941
|
+
return result;
|
|
6942
|
+
},
|
|
6943
|
+
return() {
|
|
6944
|
+
return closeIterator();
|
|
6945
|
+
},
|
|
6946
|
+
};
|
|
6947
|
+
}
|
|
6108
6948
|
/**
|
|
6109
6949
|
* Create a chat session that yields turns as an async iterator.
|
|
6110
6950
|
*
|
|
@@ -6156,20 +6996,18 @@ function createChatSession(payload, options) {
|
|
|
6156
6996
|
const accumulator = new ChatMessageAccumulator();
|
|
6157
6997
|
let previousTurnUsage;
|
|
6158
6998
|
let cumulativeUsage = emptyUsage();
|
|
6159
|
-
// Messages consumed mid-turn, dispatched one per next(). Iterator-level
|
|
6160
|
-
// for the same reason as the agent loop's `pendingWireMessages`:
|
|
6161
|
-
// consumed records never replay, so a turn-local buffer loses them.
|
|
6162
|
-
const sessionPendingWire = [];
|
|
6163
6999
|
// The current turn's message subscription — detached defensively at the
|
|
6164
7000
|
// top of next() in case user code threw without complete()/done().
|
|
6165
7001
|
let activeMsgSub;
|
|
6166
|
-
|
|
7002
|
+
const iterator = {
|
|
6167
7003
|
async next() {
|
|
6168
7004
|
activeMsgSub?.off();
|
|
6169
7005
|
activeMsgSub = undefined;
|
|
6170
7006
|
if (!booted) {
|
|
6171
7007
|
booted = true;
|
|
6172
|
-
await
|
|
7008
|
+
await installChatInputRouter(currentPayload.chatId, {
|
|
7009
|
+
resuming: Boolean(currentPayload.continuation),
|
|
7010
|
+
});
|
|
6173
7011
|
stop = createStopSignal();
|
|
6174
7012
|
}
|
|
6175
7013
|
turn++;
|
|
@@ -6226,29 +7064,32 @@ function createChatSession(payload, options) {
|
|
|
6226
7064
|
currentPayload = { ...currentPayload, continuation: true };
|
|
6227
7065
|
}
|
|
6228
7066
|
}
|
|
6229
|
-
|
|
6230
|
-
|
|
7067
|
+
/**
|
|
7068
|
+
* Subsequent turns take the next message from the router. A record
|
|
7069
|
+
* that arrived mid-turn is already queued there, so this returns it
|
|
7070
|
+
* without suspending.
|
|
7071
|
+
*/
|
|
6231
7072
|
if (turn > 0) {
|
|
6232
|
-
|
|
6233
|
-
|
|
7073
|
+
// chat.requestUpgrade() / chat.endRun() — exit before waiting
|
|
7074
|
+
if (locals_js_1.locals.get(chatUpgradeRequestedKey) || locals_js_1.locals.get(chatEndRunRequestedKey)) {
|
|
7075
|
+
stop.cleanup();
|
|
7076
|
+
return { done: true, value: undefined };
|
|
6234
7077
|
}
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
timeout,
|
|
6244
|
-
spanName: "waiting for next message",
|
|
6245
|
-
});
|
|
6246
|
-
if (!next.ok || runSignal.aborted) {
|
|
6247
|
-
stop.cleanup();
|
|
6248
|
-
return { done: true, value: undefined };
|
|
6249
|
-
}
|
|
6250
|
-
currentPayload = next.output;
|
|
7078
|
+
const next = await messagesInput.waitWithIdleTimeout({
|
|
7079
|
+
idleTimeoutInSeconds,
|
|
7080
|
+
timeout,
|
|
7081
|
+
spanName: "waiting for next message",
|
|
7082
|
+
});
|
|
7083
|
+
if (!next.ok || runSignal.aborted) {
|
|
7084
|
+
stop.cleanup();
|
|
7085
|
+
return { done: true, value: undefined };
|
|
6251
7086
|
}
|
|
7087
|
+
/**
|
|
7088
|
+
* The facade validated `clientData` against the configured schema
|
|
7089
|
+
* before returning, so the parsed shape is what arrives here. The
|
|
7090
|
+
* cast carries that across the untyped wire payload boundary.
|
|
7091
|
+
*/
|
|
7092
|
+
currentPayload = next.output;
|
|
6252
7093
|
}
|
|
6253
7094
|
// Check limits
|
|
6254
7095
|
if (turn >= maxTurns || runSignal.aborted) {
|
|
@@ -6272,39 +7113,49 @@ function createChatSession(payload, options) {
|
|
|
6272
7113
|
continuation: currentPayload.continuation ?? false,
|
|
6273
7114
|
clientData: currentPayload.metadata,
|
|
6274
7115
|
});
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
if (
|
|
6284
|
-
if (sessionPendingMessages.onReceived) {
|
|
6285
|
-
try {
|
|
6286
|
-
await sessionPendingMessages.onReceived({
|
|
6287
|
-
message: lastUIMessage,
|
|
6288
|
-
chatId: currentPayload.chatId,
|
|
6289
|
-
turn,
|
|
6290
|
-
});
|
|
6291
|
-
}
|
|
6292
|
-
catch {
|
|
6293
|
-
/* non-fatal */
|
|
6294
|
-
}
|
|
6295
|
-
}
|
|
7116
|
+
/**
|
|
7117
|
+
* Only attached when there is a steering config to feed. Without one a
|
|
7118
|
+
* mid-turn message stays queued on the router, which is what keeps the
|
|
7119
|
+
* resume floor behind it.
|
|
7120
|
+
*/
|
|
7121
|
+
const handleSteeringMessage = async (msg, seqNum, isActive = () => true) => {
|
|
7122
|
+
const lastUIMessage = msg.message;
|
|
7123
|
+
if (lastUIMessage) {
|
|
7124
|
+
if (sessionPendingMessages?.onReceived) {
|
|
6296
7125
|
try {
|
|
6297
|
-
|
|
6298
|
-
|
|
7126
|
+
await sessionPendingMessages.onReceived({
|
|
7127
|
+
message: lastUIMessage,
|
|
7128
|
+
chatId: currentPayload.chatId,
|
|
7129
|
+
turn,
|
|
7130
|
+
});
|
|
6299
7131
|
}
|
|
6300
7132
|
catch {
|
|
6301
7133
|
/* non-fatal */
|
|
6302
7134
|
}
|
|
6303
7135
|
}
|
|
6304
|
-
|
|
7136
|
+
if (!isActive())
|
|
7137
|
+
return;
|
|
7138
|
+
try {
|
|
7139
|
+
const modelMsgs = await toModelMessages([lastUIMessage]);
|
|
7140
|
+
turnSteeringQueue.push({
|
|
7141
|
+
uiMessage: lastUIMessage,
|
|
7142
|
+
modelMessages: modelMsgs,
|
|
7143
|
+
seqNum,
|
|
7144
|
+
});
|
|
7145
|
+
}
|
|
7146
|
+
catch {
|
|
7147
|
+
/* non-fatal */
|
|
7148
|
+
}
|
|
6305
7149
|
}
|
|
6306
|
-
|
|
6307
|
-
|
|
7150
|
+
};
|
|
7151
|
+
const sessionMsgSub = sessionPendingMessages
|
|
7152
|
+
? locals_js_1.locals.get(chatCustomAgentClientDataParserKey)
|
|
7153
|
+
? observeValidatedChatMessages((msg, seqNum, isActive) => handleSteeringMessage(msg, seqNum, isActive))
|
|
7154
|
+
: chatInputRouter().observe(CHAT_ROUTE_MESSAGES, (record) => {
|
|
7155
|
+
const msg = record.data.payload;
|
|
7156
|
+
void handleSteeringMessage(msg, record.seqNum);
|
|
7157
|
+
})
|
|
7158
|
+
: undefined;
|
|
6308
7159
|
activeMsgSub = sessionMsgSub;
|
|
6309
7160
|
// Accumulate messages. Slim wire: pass the single delta message as
|
|
6310
7161
|
// a 0-or-1-length array. The accumulator's behavior is unchanged —
|
|
@@ -6326,7 +7177,7 @@ function createChatSession(payload, options) {
|
|
|
6326
7177
|
// chat.requestUpgrade() called before this turn — signal transport and exit
|
|
6327
7178
|
if (locals_js_1.locals.get(chatUpgradeRequestedKey)) {
|
|
6328
7179
|
await writeUpgradeRequiredChunk();
|
|
6329
|
-
sessionMsgSub
|
|
7180
|
+
sessionMsgSub?.off();
|
|
6330
7181
|
stop.cleanup();
|
|
6331
7182
|
return { done: true, value: undefined };
|
|
6332
7183
|
}
|
|
@@ -6366,7 +7217,7 @@ function createChatSession(payload, options) {
|
|
|
6366
7217
|
if (!response || response.role !== "assistant") {
|
|
6367
7218
|
throw new Error("turn.complete() could not find the spliced handover response");
|
|
6368
7219
|
}
|
|
6369
|
-
sessionMsgSub
|
|
7220
|
+
sessionMsgSub?.off();
|
|
6370
7221
|
await chatWriteTurnComplete();
|
|
6371
7222
|
return response;
|
|
6372
7223
|
}
|
|
@@ -6383,7 +7234,7 @@ function createChatSession(payload, options) {
|
|
|
6383
7234
|
});
|
|
6384
7235
|
if (runSignal.aborted) {
|
|
6385
7236
|
// Full cancel — don't accumulate
|
|
6386
|
-
sessionMsgSub
|
|
7237
|
+
sessionMsgSub?.off();
|
|
6387
7238
|
await chatWriteTurnComplete();
|
|
6388
7239
|
return undefined;
|
|
6389
7240
|
}
|
|
@@ -6406,7 +7257,7 @@ function createChatSession(payload, options) {
|
|
|
6406
7257
|
finally {
|
|
6407
7258
|
// Detach at stream end (like the agent loop): the steering queue
|
|
6408
7259
|
// can't inject anymore, so later arrivals must buffer for the next turn.
|
|
6409
|
-
sessionMsgSub
|
|
7260
|
+
sessionMsgSub?.off();
|
|
6410
7261
|
}
|
|
6411
7262
|
if (response) {
|
|
6412
7263
|
const cleaned = stop.signal.aborted && !runSignal.aborted
|
|
@@ -6502,7 +7353,7 @@ function createChatSession(payload, options) {
|
|
|
6502
7353
|
}
|
|
6503
7354
|
}
|
|
6504
7355
|
}
|
|
6505
|
-
sessionMsgSub
|
|
7356
|
+
sessionMsgSub?.off();
|
|
6506
7357
|
await chatWriteTurnComplete();
|
|
6507
7358
|
return response;
|
|
6508
7359
|
},
|
|
@@ -6519,7 +7370,7 @@ function createChatSession(payload, options) {
|
|
|
6519
7370
|
await accumulator.addResponse(response);
|
|
6520
7371
|
},
|
|
6521
7372
|
async done() {
|
|
6522
|
-
sessionMsgSub
|
|
7373
|
+
sessionMsgSub?.off();
|
|
6523
7374
|
await chatWriteTurnComplete();
|
|
6524
7375
|
},
|
|
6525
7376
|
prepareStep() {
|
|
@@ -6558,6 +7409,7 @@ function createChatSession(payload, options) {
|
|
|
6558
7409
|
return { done: true, value: undefined };
|
|
6559
7410
|
},
|
|
6560
7411
|
};
|
|
7412
|
+
return trackActiveChatSessionIterator(iterator);
|
|
6561
7413
|
},
|
|
6562
7414
|
};
|
|
6563
7415
|
}
|
|
@@ -7006,6 +7858,8 @@ exports.chat = {
|
|
|
7006
7858
|
isStopped,
|
|
7007
7859
|
/** Request that the run exits after the current turn so the next message starts on the latest version. See {@link requestUpgrade}. */
|
|
7008
7860
|
requestUpgrade,
|
|
7861
|
+
/** Hand off a custom agent Session to a fresh run. See {@link endAndContinue}. */
|
|
7862
|
+
endAndContinue,
|
|
7009
7863
|
/** Exit the run after the current turn completes, without any upgrade signal. See {@link endRun}. */
|
|
7010
7864
|
endRun,
|
|
7011
7865
|
/** Clean up aborted parts from a UIMessage. See {@link cleanupAbortedParts}. */
|
|
@@ -7113,6 +7967,16 @@ exports.chat = {
|
|
|
7113
7967
|
*/
|
|
7114
7968
|
async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
7115
7969
|
const session = getChatSession();
|
|
7970
|
+
// A handover-prepare boot claims the handover kinds so a signal arriving
|
|
7971
|
+
// before `waitForHandover` attaches is not drained. Released here rather than
|
|
7972
|
+
// only in `waitForHandover`, because a loop that never calls it would
|
|
7973
|
+
// otherwise hold the claim for the life of the run and leave a handover
|
|
7974
|
+
// record parked at the head of the channel, where it wedges
|
|
7975
|
+
// `chat.messages.next()`. Every surface reaches a turn boundary through this
|
|
7976
|
+
// function, including the managed agent, which does not call the public
|
|
7977
|
+
// `chat.writeTurnComplete`. By the time a turn completes the handover window
|
|
7978
|
+
// is over either way.
|
|
7979
|
+
chatInputRouter().clearRoute(CHAT_ROUTE_HANDOVER);
|
|
7116
7980
|
// 1. Write the turn-complete control record. The ack's `lastEventId` is
|
|
7117
7981
|
// this record's seq_num — that's the trim target for the NEXT turn.
|
|
7118
7982
|
//
|
|
@@ -7127,10 +7991,15 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
7127
7991
|
if (publicAccessToken) {
|
|
7128
7992
|
extraHeaders.push(["public-access-token", publicAccessToken]);
|
|
7129
7993
|
}
|
|
7130
|
-
const
|
|
7994
|
+
const routerCheckpoint = chatInputRouter().checkpoint();
|
|
7995
|
+
const inCursor = routerCheckpoint.resumeFrom;
|
|
7131
7996
|
if (inCursor !== undefined) {
|
|
7132
7997
|
extraHeaders.push([v3_1.SESSION_IN_EVENT_ID_HEADER, String(inCursor)]);
|
|
7133
7998
|
}
|
|
7999
|
+
const consumedCursor = routerCheckpoint.appliedThrough;
|
|
8000
|
+
if (consumedCursor !== undefined) {
|
|
8001
|
+
extraHeaders.push([v3_1.SESSION_IN_CONSUMED_ID_HEADER, String(consumedCursor)]);
|
|
8002
|
+
}
|
|
7134
8003
|
const result = await session.out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE, extraHeaders);
|
|
7135
8004
|
const T_N = result.lastEventId ? Number.parseInt(result.lastEventId, 10) : undefined;
|
|
7136
8005
|
// 2. Trim back to the previous turn-complete, if we have one. Skipping on
|
|
@@ -7189,16 +8058,11 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
7189
8058
|
* @internal
|
|
7190
8059
|
*/
|
|
7191
8060
|
async function writeUpgradeRequiredChunk() {
|
|
7192
|
-
const
|
|
7193
|
-
const
|
|
7194
|
-
const callingRunId = ctx?.run.id;
|
|
8061
|
+
const chatId = locals_js_1.locals.get(chatExternalIdKey);
|
|
8062
|
+
const callingRunId = locals_js_1.locals.get(chatAgentRunContextKey)?.run.id;
|
|
7195
8063
|
if (chatId && callingRunId) {
|
|
7196
|
-
const apiClient = v3_1.apiClientManager.clientOrThrow();
|
|
7197
8064
|
try {
|
|
7198
|
-
await
|
|
7199
|
-
callingRunId,
|
|
7200
|
-
reason: "upgrade",
|
|
7201
|
-
});
|
|
8065
|
+
await performEndAndContinue();
|
|
7202
8066
|
}
|
|
7203
8067
|
catch (error) {
|
|
7204
8068
|
// Non-fatal: the next `.in/append` re-triggers via the probe.
|
|
@@ -7213,16 +8077,6 @@ async function writeUpgradeRequiredChunk() {
|
|
|
7213
8077
|
const session = getChatSession();
|
|
7214
8078
|
return session.out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.UPGRADE_REQUIRED);
|
|
7215
8079
|
}
|
|
7216
|
-
/**
|
|
7217
|
-
* Resolves the current chat's `chatId` (used as session externalId) from
|
|
7218
|
-
* the bound session handle. Returns `undefined` if no agent is bound —
|
|
7219
|
-
* shouldn't happen at the call sites that invoke
|
|
7220
|
-
* `writeUpgradeRequiredChunk`, but defensive against misuse.
|
|
7221
|
-
* @internal
|
|
7222
|
-
*/
|
|
7223
|
-
function getChatIdFromContext() {
|
|
7224
|
-
return locals_js_1.locals.get(chatSessionHandleKey)?.id;
|
|
7225
|
-
}
|
|
7226
8080
|
/**
|
|
7227
8081
|
* Extracts the text content of the last user message from a UIMessage array.
|
|
7228
8082
|
* Returns undefined if no user message is found.
|