@trigger.dev/sdk 0.0.0-prerelease-20260827143549 → 0.0.0-prerelease-20260828141059
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 +123 -12
- package/dist/commonjs/v3/ai.js +751 -84
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +4 -1
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +123 -12
- package/dist/esm/v3/ai.js +751 -84
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +4 -1
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/client-protocol.mdx +4 -2
- package/docs/ai-chat/custom-agents.mdx +123 -45
- package/docs/ai-chat/patterns/recovery-boot.mdx +9 -2
- package/docs/ai-chat/patterns/version-upgrades.mdx +26 -6
- package/docs/ai-chat/reference.mdx +22 -6
- package/docs/ai-chat/types.mdx +5 -1
- package/package.json +2 -2
package/dist/commonjs/v3/ai.js
CHANGED
|
@@ -984,6 +984,132 @@ async function withChatWriter(fn) {
|
|
|
984
984
|
await flush();
|
|
985
985
|
return result;
|
|
986
986
|
}
|
|
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
|
+
}
|
|
987
1113
|
/**
|
|
988
1114
|
* Read one record from a route, suspending the run if nothing is there yet.
|
|
989
1115
|
*
|
|
@@ -1002,14 +1128,14 @@ async function waitOnChatRoute(route, options) {
|
|
|
1002
1128
|
const warm = await router.next(route, { timeoutMs: idleMs });
|
|
1003
1129
|
if (warm) {
|
|
1004
1130
|
span.setAttribute("wait.resolved", "idle");
|
|
1005
|
-
return { ok: true, output: warm.data };
|
|
1131
|
+
return { ok: true, output: warm.data, record: warm };
|
|
1006
1132
|
}
|
|
1007
1133
|
}
|
|
1008
1134
|
else {
|
|
1009
1135
|
const buffered = await router.next(route, { timeoutMs: 0 });
|
|
1010
1136
|
if (buffered) {
|
|
1011
1137
|
span.setAttribute("wait.resolved", "buffered");
|
|
1012
|
-
return { ok: true, output: buffered.data };
|
|
1138
|
+
return { ok: true, output: buffered.data, record: buffered };
|
|
1013
1139
|
}
|
|
1014
1140
|
}
|
|
1015
1141
|
if (options.skipSuspend) {
|
|
@@ -1045,7 +1171,7 @@ async function waitOnChatRoute(route, options) {
|
|
|
1045
1171
|
continue;
|
|
1046
1172
|
if (options.onResume)
|
|
1047
1173
|
await options.onResume();
|
|
1048
|
-
return { ok: true, output: record.data };
|
|
1174
|
+
return { ok: true, output: record.data, record };
|
|
1049
1175
|
}
|
|
1050
1176
|
}, {
|
|
1051
1177
|
attributes: {
|
|
@@ -1060,36 +1186,236 @@ async function waitOnChatRoute(route, options) {
|
|
|
1060
1186
|
},
|
|
1061
1187
|
});
|
|
1062
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
|
+
}
|
|
1063
1360
|
const messagesInput = {
|
|
1064
1361
|
id: "chat-messages",
|
|
1065
1362
|
on(handler) {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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 });
|
|
1070
1368
|
},
|
|
1071
1369
|
once(options) {
|
|
1072
1370
|
return new v3_1.InputStreamOncePromise((resolve, reject) => {
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
+
});
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
const claim = observerClaimFor(record);
|
|
1391
|
+
if (claim) {
|
|
1392
|
+
chatInputRouter().untake(CHAT_ROUTE_MESSAGES, record);
|
|
1393
|
+
await claim;
|
|
1394
|
+
take();
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
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();
|
|
1086
1411
|
});
|
|
1087
1412
|
},
|
|
1088
1413
|
peek() {
|
|
1089
1414
|
const record = chatInputRouter().peek(CHAT_ROUTE_MESSAGES);
|
|
1090
1415
|
if (!record)
|
|
1091
1416
|
return undefined;
|
|
1092
|
-
|
|
1417
|
+
const payload = record.data.payload;
|
|
1418
|
+
return validateChatCustomAgentPayloadSync(payload);
|
|
1093
1419
|
},
|
|
1094
1420
|
async hasPending() {
|
|
1095
1421
|
return chatInputRouter().hasPending(CHAT_ROUTE_MESSAGES);
|
|
@@ -1100,21 +1426,67 @@ const messagesInput = {
|
|
|
1100
1426
|
(!Number.isFinite(timeoutInSeconds) || timeoutInSeconds < 0)) {
|
|
1101
1427
|
throw new TypeError("chat.messages.next() timeoutInSeconds must be a finite non-negative number");
|
|
1102
1428
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
+
}
|
|
1110
1461
|
},
|
|
1111
1462
|
wait(options) {
|
|
1112
1463
|
return new v3_1.ManualWaitpointPromise(async (resolve, reject) => {
|
|
1113
1464
|
try {
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1465
|
+
// Same skip-and-wait rule as `waitWithIdleTimeout`, looping back into
|
|
1466
|
+
// the suspending wait rather than surfacing an unvalidated payload.
|
|
1467
|
+
while (true) {
|
|
1468
|
+
const result = await waitOnChatRoute(CHAT_ROUTE_MESSAGES, { timeout: options?.timeout, spanName: options?.spanName });
|
|
1469
|
+
if (!result.ok) {
|
|
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 });
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const validated = await validateChatCustomAgentPayload(payload);
|
|
1485
|
+
if (validated.ok) {
|
|
1486
|
+
resolve({ ok: true, output: validated.payload });
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1118
1490
|
}
|
|
1119
1491
|
catch (error) {
|
|
1120
1492
|
reject(error);
|
|
@@ -1122,10 +1494,31 @@ const messagesInput = {
|
|
|
1122
1494
|
});
|
|
1123
1495
|
},
|
|
1124
1496
|
async waitWithIdleTimeout(options) {
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
+
*/
|
|
1503
|
+
while (true) {
|
|
1504
|
+
const result = await waitOnChatRoute(CHAT_ROUTE_MESSAGES, options);
|
|
1505
|
+
if (!result.ok)
|
|
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 };
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1129
1522
|
},
|
|
1130
1523
|
async send(_runId, data, options) {
|
|
1131
1524
|
await getChatSession().in.send({ kind: "message", payload: data }, options?.requestOptions);
|
|
@@ -2029,6 +2422,30 @@ const chatCompactionStateKey = locals_js_1.locals.create("chat.compaction");
|
|
|
2029
2422
|
const chatOnCompactedKey = locals_js_1.locals.create("chat.onCompacted");
|
|
2030
2423
|
/** @internal Full task `ctx` for the active `chat.agent` run (for hooks invoked from nested compaction). */
|
|
2031
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");
|
|
2032
2449
|
const chatPrepareMessagesKey = locals_js_1.locals.create("chat.prepareMessages");
|
|
2033
2450
|
/**
|
|
2034
2451
|
* @internal The raw `tools` option from `chat.agent({ tools })`, either a
|
|
@@ -2984,7 +3401,11 @@ async function pipeChat(source, options) {
|
|
|
2984
3401
|
await waitUntilComplete();
|
|
2985
3402
|
}
|
|
2986
3403
|
function chatCustomAgent(options) {
|
|
2987
|
-
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;
|
|
2988
3409
|
const task = (0, shared_js_1.createTask)({
|
|
2989
3410
|
...restOptions,
|
|
2990
3411
|
triggerSource: "agent",
|
|
@@ -3003,6 +3424,17 @@ function chatCustomAgent(options) {
|
|
|
3003
3424
|
locals_js_1.locals.set(chatSessionHandleKey, sessions_js_1.sessions.open(payload.chatId));
|
|
3004
3425
|
locals_js_1.locals.set(chatExternalIdKey, payload.chatId);
|
|
3005
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");
|
|
3006
3438
|
// Initialize the turn-complete trim slot so `chat.writeTurnComplete`
|
|
3007
3439
|
// trims `session.out` back to the previous turn boundary. Without
|
|
3008
3440
|
// this the slot is undefined and the trim never runs, so `.out`
|
|
@@ -3014,7 +3446,62 @@ function chatCustomAgent(options) {
|
|
|
3014
3446
|
await installChatInputRouter(payload.chatId, {
|
|
3015
3447
|
resuming: Boolean(payload.continuation),
|
|
3016
3448
|
});
|
|
3017
|
-
|
|
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);
|
|
3018
3505
|
},
|
|
3019
3506
|
});
|
|
3020
3507
|
// Register clientDataSchema so the CLI converts it to JSONSchema
|
|
@@ -3044,6 +3531,7 @@ function chatAgent(options) {
|
|
|
3044
3531
|
agentConfig: { type: "ai-sdk-chat" },
|
|
3045
3532
|
run: async (payload, { signal: runSignal, ctx }) => {
|
|
3046
3533
|
locals_js_1.locals.set(chatAgentRunContextKey, ctx);
|
|
3534
|
+
locals_js_1.locals.set(chatCustomAgentRunKey, false);
|
|
3047
3535
|
// On AI SDK 7, register the `@ai-sdk/otel` integration (once per process)
|
|
3048
3536
|
// so `experimental_telemetry` spans flow into the run trace. Awaited here
|
|
3049
3537
|
// at run boot — before any `streamText` — and a no-op on v5/v6 or when the
|
|
@@ -3336,14 +3824,23 @@ function chatAgent(options) {
|
|
|
3336
3824
|
hookBeforeBoot = hookResult.beforeBoot;
|
|
3337
3825
|
}
|
|
3338
3826
|
}
|
|
3339
|
-
// Default: splice partial + the user it was answering into
|
|
3340
|
-
//
|
|
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.
|
|
3341
3838
|
let seedChain;
|
|
3342
3839
|
let recoveredTurns;
|
|
3343
3840
|
if (hookChain !== undefined) {
|
|
3344
3841
|
seedChain = hookChain;
|
|
3345
3842
|
}
|
|
3346
|
-
else if (partialAssistant !== undefined && inFlightUsers.length >
|
|
3843
|
+
else if (partialAssistant !== undefined && inFlightUsers.length > 1) {
|
|
3347
3844
|
seedChain = [...settledMessages, inFlightUsers[0], partialAssistant];
|
|
3348
3845
|
}
|
|
3349
3846
|
else {
|
|
@@ -3352,7 +3849,7 @@ function chatAgent(options) {
|
|
|
3352
3849
|
if (hookRecoveredTurns !== undefined) {
|
|
3353
3850
|
recoveredTurns = hookRecoveredTurns;
|
|
3354
3851
|
}
|
|
3355
|
-
else if (partialAssistant !== undefined && inFlightUsers.length >
|
|
3852
|
+
else if (partialAssistant !== undefined && inFlightUsers.length > 1) {
|
|
3356
3853
|
recoveredTurns = inFlightUsers.slice(1);
|
|
3357
3854
|
}
|
|
3358
3855
|
else {
|
|
@@ -5337,6 +5834,8 @@ function createChatBuilder(config) {
|
|
|
5337
5834
|
return createChatBuilder({
|
|
5338
5835
|
...config,
|
|
5339
5836
|
clientDataSchema: cdConfig.schema,
|
|
5837
|
+
clientDataReportErrorAt: cdConfig.reportErrorAt ?? config.clientDataReportErrorAt,
|
|
5838
|
+
clientDataOnValidationError: cdConfig.onValidationError ?? config.clientDataOnValidationError,
|
|
5340
5839
|
});
|
|
5341
5840
|
},
|
|
5342
5841
|
onBoot(fn) {
|
|
@@ -5417,6 +5916,10 @@ function createChatBuilder(config) {
|
|
|
5417
5916
|
return chatCustomAgent({
|
|
5418
5917
|
...options,
|
|
5419
5918
|
...(config.clientDataSchema ? { clientDataSchema: config.clientDataSchema } : {}),
|
|
5919
|
+
...(config.clientDataReportErrorAt
|
|
5920
|
+
? { clientDataReportErrorAt: config.clientDataReportErrorAt }
|
|
5921
|
+
: {}),
|
|
5922
|
+
onClientDataValidationError: composeHooks(config.clientDataOnValidationError, options.onClientDataValidationError),
|
|
5420
5923
|
});
|
|
5421
5924
|
},
|
|
5422
5925
|
};
|
|
@@ -5469,6 +5972,8 @@ function withUIMessage(config) {
|
|
|
5469
5972
|
function withClientData(config) {
|
|
5470
5973
|
return createChatBuilder({
|
|
5471
5974
|
clientDataSchema: config.schema,
|
|
5975
|
+
clientDataReportErrorAt: config.reportErrorAt,
|
|
5976
|
+
clientDataOnValidationError: config.onValidationError,
|
|
5472
5977
|
hooks: {},
|
|
5473
5978
|
});
|
|
5474
5979
|
}
|
|
@@ -5659,6 +6164,58 @@ function isStopped() {
|
|
|
5659
6164
|
function requestUpgrade() {
|
|
5660
6165
|
locals_js_1.locals.set(chatUpgradeRequestedKey, true);
|
|
5661
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
|
+
}
|
|
5662
6219
|
/**
|
|
5663
6220
|
* Exit the run after the current turn completes, without waiting for the
|
|
5664
6221
|
* next message. Unlike {@link requestUpgrade}, no upgrade-required signal
|
|
@@ -5883,6 +6440,7 @@ function createStopSignal() {
|
|
|
5883
6440
|
* ```
|
|
5884
6441
|
*/
|
|
5885
6442
|
async function chatWriteTurnComplete(options) {
|
|
6443
|
+
await flushDeferredChatCustomAgentClientDataErrors();
|
|
5886
6444
|
const result = await writeTurnCompleteChunk(undefined, options?.publicAccessToken);
|
|
5887
6445
|
// Same cursor written to the `session-in-event-id` header inside
|
|
5888
6446
|
// `writeTurnCompleteChunk`; surfaced here so the caller can persist it.
|
|
@@ -5892,6 +6450,26 @@ async function chatWriteTurnComplete(options) {
|
|
|
5892
6450
|
...(inCursor !== undefined ? { sessionInEventId: String(inCursor) } : {}),
|
|
5893
6451
|
};
|
|
5894
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
|
+
}
|
|
5895
6473
|
/**
|
|
5896
6474
|
* Pass every chunk through untouched while recording it in `buffer`. Handles
|
|
5897
6475
|
* both the `AsyncIterable` and `ReadableStream` shapes `toUIMessageStream()`
|
|
@@ -6278,6 +6856,95 @@ class ChatMessageAccumulator {
|
|
|
6278
6856
|
return true;
|
|
6279
6857
|
}
|
|
6280
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
|
+
}
|
|
6281
6948
|
/**
|
|
6282
6949
|
* Create a chat session that yields turns as an async iterator.
|
|
6283
6950
|
*
|
|
@@ -6332,7 +6999,7 @@ function createChatSession(payload, options) {
|
|
|
6332
6999
|
// The current turn's message subscription — detached defensively at the
|
|
6333
7000
|
// top of next() in case user code threw without complete()/done().
|
|
6334
7001
|
let activeMsgSub;
|
|
6335
|
-
|
|
7002
|
+
const iterator = {
|
|
6336
7003
|
async next() {
|
|
6337
7004
|
activeMsgSub?.off();
|
|
6338
7005
|
activeMsgSub = undefined;
|
|
@@ -6417,6 +7084,11 @@ function createChatSession(payload, options) {
|
|
|
6417
7084
|
stop.cleanup();
|
|
6418
7085
|
return { done: true, value: undefined };
|
|
6419
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
|
+
*/
|
|
6420
7092
|
currentPayload = next.output;
|
|
6421
7093
|
}
|
|
6422
7094
|
// Check limits
|
|
@@ -6446,36 +7118,43 @@ function createChatSession(payload, options) {
|
|
|
6446
7118
|
* mid-turn message stays queued on the router, which is what keeps the
|
|
6447
7119
|
* resume floor behind it.
|
|
6448
7120
|
*/
|
|
6449
|
-
const
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
if (lastUIMessage) {
|
|
6454
|
-
if (sessionPendingMessages.onReceived) {
|
|
6455
|
-
try {
|
|
6456
|
-
await sessionPendingMessages.onReceived({
|
|
6457
|
-
message: lastUIMessage,
|
|
6458
|
-
chatId: currentPayload.chatId,
|
|
6459
|
-
turn,
|
|
6460
|
-
});
|
|
6461
|
-
}
|
|
6462
|
-
catch {
|
|
6463
|
-
/* non-fatal */
|
|
6464
|
-
}
|
|
6465
|
-
}
|
|
7121
|
+
const handleSteeringMessage = async (msg, seqNum, isActive = () => true) => {
|
|
7122
|
+
const lastUIMessage = msg.message;
|
|
7123
|
+
if (lastUIMessage) {
|
|
7124
|
+
if (sessionPendingMessages?.onReceived) {
|
|
6466
7125
|
try {
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
seqNum: record.seqNum,
|
|
7126
|
+
await sessionPendingMessages.onReceived({
|
|
7127
|
+
message: lastUIMessage,
|
|
7128
|
+
chatId: currentPayload.chatId,
|
|
7129
|
+
turn,
|
|
6472
7130
|
});
|
|
6473
7131
|
}
|
|
6474
7132
|
catch {
|
|
6475
7133
|
/* non-fatal */
|
|
6476
7134
|
}
|
|
6477
7135
|
}
|
|
6478
|
-
|
|
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
|
+
}
|
|
7149
|
+
}
|
|
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
|
+
})
|
|
6479
7158
|
: undefined;
|
|
6480
7159
|
activeMsgSub = sessionMsgSub;
|
|
6481
7160
|
// Accumulate messages. Slim wire: pass the single delta message as
|
|
@@ -6730,6 +7409,7 @@ function createChatSession(payload, options) {
|
|
|
6730
7409
|
return { done: true, value: undefined };
|
|
6731
7410
|
},
|
|
6732
7411
|
};
|
|
7412
|
+
return trackActiveChatSessionIterator(iterator);
|
|
6733
7413
|
},
|
|
6734
7414
|
};
|
|
6735
7415
|
}
|
|
@@ -7178,6 +7858,8 @@ exports.chat = {
|
|
|
7178
7858
|
isStopped,
|
|
7179
7859
|
/** Request that the run exits after the current turn so the next message starts on the latest version. See {@link requestUpgrade}. */
|
|
7180
7860
|
requestUpgrade,
|
|
7861
|
+
/** Hand off a custom agent Session to a fresh run. See {@link endAndContinue}. */
|
|
7862
|
+
endAndContinue,
|
|
7181
7863
|
/** Exit the run after the current turn completes, without any upgrade signal. See {@link endRun}. */
|
|
7182
7864
|
endRun,
|
|
7183
7865
|
/** Clean up aborted parts from a UIMessage. See {@link cleanupAbortedParts}. */
|
|
@@ -7376,16 +8058,11 @@ async function writeTurnCompleteChunk(_chatId, publicAccessToken) {
|
|
|
7376
8058
|
* @internal
|
|
7377
8059
|
*/
|
|
7378
8060
|
async function writeUpgradeRequiredChunk() {
|
|
7379
|
-
const
|
|
7380
|
-
const
|
|
7381
|
-
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;
|
|
7382
8063
|
if (chatId && callingRunId) {
|
|
7383
|
-
const apiClient = v3_1.apiClientManager.clientOrThrow();
|
|
7384
8064
|
try {
|
|
7385
|
-
await
|
|
7386
|
-
callingRunId,
|
|
7387
|
-
reason: "upgrade",
|
|
7388
|
-
});
|
|
8065
|
+
await performEndAndContinue();
|
|
7389
8066
|
}
|
|
7390
8067
|
catch (error) {
|
|
7391
8068
|
// Non-fatal: the next `.in/append` re-triggers via the probe.
|
|
@@ -7400,16 +8077,6 @@ async function writeUpgradeRequiredChunk() {
|
|
|
7400
8077
|
const session = getChatSession();
|
|
7401
8078
|
return session.out.writeControl(v3_1.TRIGGER_CONTROL_SUBTYPE.UPGRADE_REQUIRED);
|
|
7402
8079
|
}
|
|
7403
|
-
/**
|
|
7404
|
-
* Resolves the current chat's `chatId` (used as session externalId) from
|
|
7405
|
-
* the bound session handle. Returns `undefined` if no agent is bound —
|
|
7406
|
-
* shouldn't happen at the call sites that invoke
|
|
7407
|
-
* `writeUpgradeRequiredChunk`, but defensive against misuse.
|
|
7408
|
-
* @internal
|
|
7409
|
-
*/
|
|
7410
|
-
function getChatIdFromContext() {
|
|
7411
|
-
return locals_js_1.locals.get(chatSessionHandleKey)?.id;
|
|
7412
|
-
}
|
|
7413
8080
|
/**
|
|
7414
8081
|
* Extracts the text content of the last user message from a UIMessage array.
|
|
7415
8082
|
* Returns undefined if no user message is found.
|