@trigger.dev/sdk 0.0.0-prerelease-20260310150337 → 0.0.0-prerelease-20260325143642
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 +692 -27
- package/dist/commonjs/v3/ai.js +1148 -59
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-react.d.ts +89 -1
- package/dist/commonjs/v3/chat-react.js +164 -0
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +18 -1
- package/dist/commonjs/v3/chat.js +40 -2
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/index.d.ts +1 -0
- package/dist/commonjs/v3/index.js +2 -1
- package/dist/commonjs/v3/index.js.map +1 -1
- package/dist/commonjs/v3/prompt.d.ts +38 -0
- package/dist/commonjs/v3/prompt.js +150 -0
- package/dist/commonjs/v3/prompt.js.map +1 -0
- package/dist/commonjs/v3/promptManagement.d.ts +26 -0
- package/dist/commonjs/v3/promptManagement.js +180 -0
- package/dist/commonjs/v3/promptManagement.js.map +1 -0
- package/dist/commonjs/v3/prompts.d.ts +3 -0
- package/dist/commonjs/v3/prompts.js +15 -0
- package/dist/commonjs/v3/prompts.js.map +1 -0
- package/dist/commonjs/v3/streams.js +18 -8
- package/dist/commonjs/v3/streams.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +692 -27
- package/dist/esm/v3/ai.js +1147 -58
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-react.d.ts +89 -1
- package/dist/esm/v3/chat-react.js +164 -1
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +18 -1
- package/dist/esm/v3/chat.js +40 -2
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/index.d.ts +1 -0
- package/dist/esm/v3/index.js +1 -0
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/esm/v3/prompt.d.ts +38 -0
- package/dist/esm/v3/prompt.js +147 -0
- package/dist/esm/v3/prompt.js.map +1 -0
- package/dist/esm/v3/promptManagement.d.ts +26 -0
- package/dist/esm/v3/promptManagement.js +170 -0
- package/dist/esm/v3/promptManagement.js.map +1 -0
- package/dist/esm/v3/prompts.d.ts +3 -0
- package/dist/esm/v3/prompts.js +3 -0
- package/dist/esm/v3/prompts.js.map +1 -0
- package/dist/esm/v3/streams.js +18 -8
- package/dist/esm/v3/streams.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +2 -2
package/dist/commonjs/v3/ai.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.chat = exports.CHAT_STOP_STREAM_ID = exports.CHAT_MESSAGES_STREAM_ID = exports.CHAT_STREAM_KEY = exports.ai = void 0;
|
|
3
|
+
exports.chat = exports.PENDING_MESSAGE_INJECTED_TYPE = exports.CHAT_STOP_STREAM_ID = exports.CHAT_MESSAGES_STREAM_ID = exports.CHAT_STREAM_KEY = exports.ai = void 0;
|
|
4
4
|
const v3_1 = require("@trigger.dev/core/v3");
|
|
5
5
|
const ai_1 = require("ai");
|
|
6
6
|
const api_1 = require("@opentelemetry/api");
|
|
@@ -14,6 +14,14 @@ const chat_constants_js_1 = require("./chat-constants.js");
|
|
|
14
14
|
Object.defineProperty(exports, "CHAT_MESSAGES_STREAM_ID", { enumerable: true, get: function () { return chat_constants_js_1.CHAT_MESSAGES_STREAM_ID; } });
|
|
15
15
|
Object.defineProperty(exports, "CHAT_STOP_STREAM_ID", { enumerable: true, get: function () { return chat_constants_js_1.CHAT_STOP_STREAM_ID; } });
|
|
16
16
|
const METADATA_KEY = "tool.execute.options";
|
|
17
|
+
/**
|
|
18
|
+
* Wrapper around `convertToModelMessages` that always passes
|
|
19
|
+
* `ignoreIncompleteToolCalls: true` to prevent failures from
|
|
20
|
+
* stopped/aborted conversations with partial tool parts.
|
|
21
|
+
*/
|
|
22
|
+
function toModelMessages(messages) {
|
|
23
|
+
return (0, ai_1.convertToModelMessages)(messages, { ignoreIncompleteToolCalls: true });
|
|
24
|
+
}
|
|
17
25
|
const chatTurnContextKey = locals_js_1.locals.create("chat.turnContext");
|
|
18
26
|
function toolFromTask(task, options) {
|
|
19
27
|
if (("schema" in task && !task.schema) || ("jsonSchema" in task && !task.jsonSchema)) {
|
|
@@ -218,6 +226,537 @@ const chatStopControllerKey = locals_js_1.locals.create("chat.stopController");
|
|
|
218
226
|
const chatUIStreamStaticKey = locals_js_1.locals.create("chat.uiMessageStreamOptions.static");
|
|
219
227
|
/** Per-turn UIMessageStream options, set via chat.setUIMessageStreamOptions(). @internal */
|
|
220
228
|
const chatUIStreamPerTurnKey = locals_js_1.locals.create("chat.uiMessageStreamOptions.perTurn");
|
|
229
|
+
function emptyUsage() {
|
|
230
|
+
return {
|
|
231
|
+
inputTokens: undefined,
|
|
232
|
+
outputTokens: undefined,
|
|
233
|
+
totalTokens: undefined,
|
|
234
|
+
inputTokenDetails: { noCacheTokens: undefined, cacheReadTokens: undefined, cacheWriteTokens: undefined },
|
|
235
|
+
outputTokenDetails: { textTokens: undefined, reasoningTokens: undefined },
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function addUsage(a, b) {
|
|
239
|
+
const add = (x, y) => x != null || y != null ? (x ?? 0) + (y ?? 0) : undefined;
|
|
240
|
+
return {
|
|
241
|
+
inputTokens: add(a.inputTokens, b.inputTokens),
|
|
242
|
+
outputTokens: add(a.outputTokens, b.outputTokens),
|
|
243
|
+
totalTokens: add(a.totalTokens, b.totalTokens),
|
|
244
|
+
inputTokenDetails: {
|
|
245
|
+
noCacheTokens: add(a.inputTokenDetails?.noCacheTokens, b.inputTokenDetails?.noCacheTokens),
|
|
246
|
+
cacheReadTokens: add(a.inputTokenDetails?.cacheReadTokens, b.inputTokenDetails?.cacheReadTokens),
|
|
247
|
+
cacheWriteTokens: add(a.inputTokenDetails?.cacheWriteTokens, b.inputTokenDetails?.cacheWriteTokens),
|
|
248
|
+
},
|
|
249
|
+
outputTokenDetails: {
|
|
250
|
+
textTokens: add(a.outputTokenDetails?.textTokens, b.outputTokenDetails?.textTokens),
|
|
251
|
+
reasoningTokens: add(a.outputTokenDetails?.reasoningTokens, b.outputTokenDetails?.reasoningTokens),
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// chat.setMessages — replace accumulated messages for compaction
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
/** @internal */
|
|
259
|
+
const chatOverrideMessagesKey = locals_js_1.locals.create("chat.overrideMessages");
|
|
260
|
+
/**
|
|
261
|
+
* Replace the accumulated conversation messages for the current run.
|
|
262
|
+
*
|
|
263
|
+
* Call from `onTurnStart` to compact before `run()` executes, or from
|
|
264
|
+
* `onTurnComplete` to compact before the next turn. Takes `UIMessage[]`
|
|
265
|
+
* and converts to `ModelMessage[]` internally.
|
|
266
|
+
*/
|
|
267
|
+
function setChatMessages(uiMessages) {
|
|
268
|
+
locals_js_1.locals.set(chatOverrideMessagesKey, uiMessages);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Model-only message override. Set by compaction to replace only the model
|
|
272
|
+
* messages (what goes to the LLM) without affecting UI messages (what gets
|
|
273
|
+
* persisted and displayed). This preserves full conversation history for the
|
|
274
|
+
* user while keeping LLM context compact.
|
|
275
|
+
* @internal
|
|
276
|
+
*/
|
|
277
|
+
const chatOverrideModelMessagesKey = locals_js_1.locals.create("chat.overrideModelMessages");
|
|
278
|
+
/** @internal */
|
|
279
|
+
const chatCompactionStateKey = locals_js_1.locals.create("chat.compaction");
|
|
280
|
+
const chatOnCompactedKey = locals_js_1.locals.create("chat.onCompacted");
|
|
281
|
+
const chatPrepareMessagesKey = locals_js_1.locals.create("chat.prepareMessages");
|
|
282
|
+
/** @internal */
|
|
283
|
+
const chatTaskCompactionKey = locals_js_1.locals.create("chat.taskCompaction");
|
|
284
|
+
/**
|
|
285
|
+
* The data part type used to signal that pending messages were injected
|
|
286
|
+
* between tool-call steps. The frontend can match on this to render
|
|
287
|
+
* injection points inline in the assistant response.
|
|
288
|
+
*/
|
|
289
|
+
exports.PENDING_MESSAGE_INJECTED_TYPE = "data-pending-message-injected";
|
|
290
|
+
/** @internal */
|
|
291
|
+
const chatPendingMessagesKey = locals_js_1.locals.create("chat.pendingMessages");
|
|
292
|
+
/** @internal */
|
|
293
|
+
const chatSteeringQueueKey = locals_js_1.locals.create("chat.steeringQueue");
|
|
294
|
+
/** @internal — IDs of messages that were successfully injected via prepareStep */
|
|
295
|
+
const chatInjectedMessageIdsKey = locals_js_1.locals.create("chat.injectedMessageIds");
|
|
296
|
+
/**
|
|
297
|
+
* Check that no tool calls are in-flight in a step's content.
|
|
298
|
+
* Used before compaction to avoid losing tool state mid-execution.
|
|
299
|
+
* @internal
|
|
300
|
+
*/
|
|
301
|
+
function isStepBoundarySafe(step) {
|
|
302
|
+
if (step.finishReason === "error")
|
|
303
|
+
return false;
|
|
304
|
+
const callIds = new Set(step.content.filter((p) => p.type === "tool-call").map((p) => p.toolCallId));
|
|
305
|
+
const settledIds = new Set(step.content
|
|
306
|
+
.filter((p) => p.type === "tool-result" || p.type === "tool-error")
|
|
307
|
+
.map((p) => p.toolCallId));
|
|
308
|
+
return ![...callIds].some((id) => !settledIds.has(id));
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Apply the prepareMessages hook if one is set in locals.
|
|
312
|
+
* @internal
|
|
313
|
+
*/
|
|
314
|
+
async function applyPrepareMessages(messages, reason) {
|
|
315
|
+
const hook = locals_js_1.locals.get(chatPrepareMessagesKey);
|
|
316
|
+
if (!hook)
|
|
317
|
+
return messages;
|
|
318
|
+
const turnCtx = locals_js_1.locals.get(chatTurnContextKey);
|
|
319
|
+
return tracer_js_1.tracer.startActiveSpan("prepareMessages()", async () => {
|
|
320
|
+
return hook({
|
|
321
|
+
messages,
|
|
322
|
+
reason,
|
|
323
|
+
chatId: turnCtx?.chatId ?? "",
|
|
324
|
+
turn: turnCtx?.turn ?? 0,
|
|
325
|
+
clientData: turnCtx?.clientData,
|
|
326
|
+
});
|
|
327
|
+
}, {
|
|
328
|
+
attributes: {
|
|
329
|
+
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart",
|
|
330
|
+
[v3_1.SemanticInternalAttributes.COLLAPSED]: true,
|
|
331
|
+
"chat.prepareMessages.reason": reason,
|
|
332
|
+
"chat.prepareMessages.messageCount": messages.length,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Read the current compaction state. Returns the summary and base message count
|
|
338
|
+
* if compaction has occurred in this turn, or `undefined` if not.
|
|
339
|
+
*
|
|
340
|
+
* Use in a custom `prepareStep` to rebuild from a previous compaction:
|
|
341
|
+
* ```ts
|
|
342
|
+
* const state = chat.getCompactionState();
|
|
343
|
+
* if (state) {
|
|
344
|
+
* return { messages: [{ role: "user", content: state.summary }, ...newMsgs] };
|
|
345
|
+
* }
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
function getCompactionState() {
|
|
349
|
+
return locals_js_1.locals.get(chatCompactionStateKey);
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Low-level compaction for use inside a custom `prepareStep`.
|
|
353
|
+
*
|
|
354
|
+
* Handles the full decision tree: first step, already-compacted rebuild,
|
|
355
|
+
* boundary safety, threshold check, summarization, stream chunks, state
|
|
356
|
+
* storage, and accumulator update.
|
|
357
|
+
*
|
|
358
|
+
* Returns a `CompactResult` — inspect `result.type` to see what happened,
|
|
359
|
+
* or convert to a prepareStep return with `result.type === "skipped" ? undefined : result`.
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```ts
|
|
363
|
+
* prepareStep: async ({ messages, steps }) => {
|
|
364
|
+
* // your custom logic here...
|
|
365
|
+
* const result = await chat.compact(messages, steps, {
|
|
366
|
+
* threshold: 80_000,
|
|
367
|
+
* summarize: async (msgs) => generateText({ model, messages: msgs }).then(r => r.text),
|
|
368
|
+
* });
|
|
369
|
+
* if (result.type === "compacted") {
|
|
370
|
+
* logger.info("Compacted!", { summary: result.summary });
|
|
371
|
+
* }
|
|
372
|
+
* return result.type === "skipped" ? undefined : result;
|
|
373
|
+
* },
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
376
|
+
async function chatCompact(messages, steps, options) {
|
|
377
|
+
const currentStep = steps.at(-1);
|
|
378
|
+
// First step — nothing to check
|
|
379
|
+
if (!currentStep) {
|
|
380
|
+
return { type: "skipped" };
|
|
381
|
+
}
|
|
382
|
+
// Already compacted — rebuild from summary + new response messages
|
|
383
|
+
const state = locals_js_1.locals.get(chatCompactionStateKey);
|
|
384
|
+
if (state && isStepBoundarySafe(currentStep)) {
|
|
385
|
+
return {
|
|
386
|
+
type: "rebuilt",
|
|
387
|
+
messages: await applyPrepareMessages([
|
|
388
|
+
{ role: "user", content: state.summary },
|
|
389
|
+
...currentStep.response.messages.slice(state.baseResponseMessageCount),
|
|
390
|
+
], "compaction-rebuild"),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
// Boundary unsafe — skip
|
|
394
|
+
if (!isStepBoundarySafe(currentStep)) {
|
|
395
|
+
return { type: "skipped" };
|
|
396
|
+
}
|
|
397
|
+
const totalTokens = currentStep.usage.totalTokens;
|
|
398
|
+
const inputTokens = currentStep.usage.inputTokens;
|
|
399
|
+
const outputTokens = currentStep.usage.outputTokens;
|
|
400
|
+
const turnCtx = locals_js_1.locals.get(chatTurnContextKey);
|
|
401
|
+
const stepNumber = steps.length - 1;
|
|
402
|
+
const shouldTrigger = options.shouldCompact
|
|
403
|
+
? await options.shouldCompact({
|
|
404
|
+
messages,
|
|
405
|
+
totalTokens,
|
|
406
|
+
inputTokens,
|
|
407
|
+
outputTokens,
|
|
408
|
+
usage: currentStep.usage,
|
|
409
|
+
source: "inner",
|
|
410
|
+
stepNumber,
|
|
411
|
+
steps,
|
|
412
|
+
chatId: turnCtx?.chatId,
|
|
413
|
+
turn: turnCtx?.turn,
|
|
414
|
+
clientData: turnCtx?.clientData,
|
|
415
|
+
})
|
|
416
|
+
: (totalTokens != null && options.threshold != null && totalTokens > options.threshold);
|
|
417
|
+
if (!shouldTrigger) {
|
|
418
|
+
return { type: "skipped" };
|
|
419
|
+
}
|
|
420
|
+
const result = await tracer_js_1.tracer.startActiveSpan("context compaction", async (span) => {
|
|
421
|
+
const compactionId = (0, ai_1.generateId)();
|
|
422
|
+
let summary;
|
|
423
|
+
const { waitUntilComplete } = streams_js_1.streams.writer(exports.CHAT_STREAM_KEY, {
|
|
424
|
+
spanName: "stream compaction chunks",
|
|
425
|
+
collapsed: true,
|
|
426
|
+
execute: async ({ write }) => {
|
|
427
|
+
write({ type: "step-start" });
|
|
428
|
+
write({
|
|
429
|
+
type: "data-compaction",
|
|
430
|
+
id: compactionId,
|
|
431
|
+
data: { status: "compacting", totalTokens },
|
|
432
|
+
});
|
|
433
|
+
// Generate summary
|
|
434
|
+
summary = await options.summarize(messages);
|
|
435
|
+
// Store state in locals for subsequent steps
|
|
436
|
+
locals_js_1.locals.set(chatCompactionStateKey, {
|
|
437
|
+
summary,
|
|
438
|
+
baseResponseMessageCount: currentStep.response.messages.length,
|
|
439
|
+
});
|
|
440
|
+
// Set model-only override — UI messages stay intact for persistence.
|
|
441
|
+
// The summary becomes the model message history for the next turn,
|
|
442
|
+
// while accumulatedUIMessages keeps the full conversation for display.
|
|
443
|
+
locals_js_1.locals.set(chatOverrideModelMessagesKey, [
|
|
444
|
+
{ role: "assistant", content: [{ type: "text", text: `[Conversation summary]\n\n${summary}` }] },
|
|
445
|
+
]);
|
|
446
|
+
// Fire onCompacted hook
|
|
447
|
+
const onCompactedHook = locals_js_1.locals.get(chatOnCompactedKey);
|
|
448
|
+
if (onCompactedHook) {
|
|
449
|
+
await onCompactedHook({
|
|
450
|
+
summary,
|
|
451
|
+
messages,
|
|
452
|
+
messageCount: messages.length,
|
|
453
|
+
usage: currentStep.usage,
|
|
454
|
+
totalTokens,
|
|
455
|
+
inputTokens,
|
|
456
|
+
outputTokens,
|
|
457
|
+
stepNumber,
|
|
458
|
+
chatId: turnCtx?.chatId,
|
|
459
|
+
turn: turnCtx?.turn,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
write({
|
|
463
|
+
type: "data-compaction",
|
|
464
|
+
id: compactionId,
|
|
465
|
+
data: { status: "complete", totalTokens },
|
|
466
|
+
});
|
|
467
|
+
write({ type: "finish-step" });
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
await waitUntilComplete();
|
|
471
|
+
// Set attributes after we have the summary
|
|
472
|
+
span.setAttribute("compaction.summary_length", summary.length);
|
|
473
|
+
return {
|
|
474
|
+
type: "compacted",
|
|
475
|
+
messages: await applyPrepareMessages([{ role: "user", content: summary }], "compaction-result"),
|
|
476
|
+
summary,
|
|
477
|
+
};
|
|
478
|
+
}, {
|
|
479
|
+
attributes: {
|
|
480
|
+
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "tabler-scissors",
|
|
481
|
+
"compaction.threshold": options.threshold,
|
|
482
|
+
"compaction.total_tokens": totalTokens ?? 0,
|
|
483
|
+
"compaction.input_tokens": inputTokens ?? 0,
|
|
484
|
+
"compaction.message_count": messages.length,
|
|
485
|
+
"compaction.step_number": stepNumber,
|
|
486
|
+
...(turnCtx?.chatId ? { "compaction.chat_id": turnCtx.chatId } : {}),
|
|
487
|
+
...(turnCtx?.turn != null ? { "compaction.turn": turnCtx.turn } : {}),
|
|
488
|
+
...(0, v3_1.accessoryAttributes)({
|
|
489
|
+
items: [
|
|
490
|
+
{ text: `${totalTokens ?? 0} tokens`, variant: "normal" },
|
|
491
|
+
{ text: `${messages.length} msgs`, variant: "normal" },
|
|
492
|
+
],
|
|
493
|
+
style: "codepath",
|
|
494
|
+
}),
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Returns a `prepareStep` function that handles context compaction automatically.
|
|
501
|
+
*
|
|
502
|
+
* Monitors token usage between tool-call steps. When `totalTokens` exceeds
|
|
503
|
+
* the threshold, generates a summary via `summarize()`, replaces the message
|
|
504
|
+
* history, and emits `data-compaction` stream chunks for the frontend.
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```ts
|
|
508
|
+
* return streamText({
|
|
509
|
+
* ...chat.toStreamTextOptions({ registry }),
|
|
510
|
+
* messages: chat.addCacheBreaks(messages),
|
|
511
|
+
* prepareStep: chat.compactionStep({
|
|
512
|
+
* threshold: 80_000,
|
|
513
|
+
* summarize: async (messages) => {
|
|
514
|
+
* return generateText({ model, messages: [...messages, { role: "user", content: "Summarize." }] })
|
|
515
|
+
* .then((r) => r.text);
|
|
516
|
+
* },
|
|
517
|
+
* }),
|
|
518
|
+
* tools: { ... },
|
|
519
|
+
* });
|
|
520
|
+
* ```
|
|
521
|
+
*/
|
|
522
|
+
function chatCompactionStep(options) {
|
|
523
|
+
return async ({ messages, steps }) => {
|
|
524
|
+
const result = await chatCompact(messages, steps, options);
|
|
525
|
+
return result.type === "skipped" ? undefined : result;
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
// Steering queue drain — shared by toStreamTextOptions, session, accumulator
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
/**
|
|
532
|
+
* Drain the steering queue as a batch. Calls `shouldInject` once with all
|
|
533
|
+
* pending messages. If it returns true, calls `prepareMessages` once to
|
|
534
|
+
* transform the batch, then clears the queue.
|
|
535
|
+
* Returns the model messages to inject (empty if none).
|
|
536
|
+
* @internal
|
|
537
|
+
*/
|
|
538
|
+
async function drainSteeringQueue(config, messages, steps, queueOverride) {
|
|
539
|
+
const queue = queueOverride ?? locals_js_1.locals.get(chatSteeringQueueKey);
|
|
540
|
+
if (!queue || queue.length === 0)
|
|
541
|
+
return [];
|
|
542
|
+
const ctx = locals_js_1.locals.get(chatTurnContextKey);
|
|
543
|
+
const stepNumber = steps.length - 1;
|
|
544
|
+
const uiMessages = queue.map((e) => e.uiMessage);
|
|
545
|
+
const batchEvent = {
|
|
546
|
+
messages: uiMessages,
|
|
547
|
+
modelMessages: messages,
|
|
548
|
+
steps,
|
|
549
|
+
stepNumber,
|
|
550
|
+
chatId: ctx?.chatId ?? "",
|
|
551
|
+
turn: ctx?.turn ?? 0,
|
|
552
|
+
clientData: ctx?.clientData,
|
|
553
|
+
};
|
|
554
|
+
// Call shouldInject once for the whole batch
|
|
555
|
+
const shouldInject = config.shouldInject
|
|
556
|
+
? await config.shouldInject(batchEvent)
|
|
557
|
+
: false;
|
|
558
|
+
if (!shouldInject)
|
|
559
|
+
return [];
|
|
560
|
+
// Extract message texts for span attributes
|
|
561
|
+
const messageTexts = uiMessages.map((m) => (m.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join("") || "");
|
|
562
|
+
const previewText = messageTexts.length === 1
|
|
563
|
+
? messageTexts[0].slice(0, 80)
|
|
564
|
+
: `${queue.length} messages`;
|
|
565
|
+
return tracer_js_1.tracer.startActiveSpan("pending message injected", async () => {
|
|
566
|
+
// Transform the batch — default: concatenate all pre-converted model messages
|
|
567
|
+
const injected = config.prepare
|
|
568
|
+
? await config.prepare(batchEvent)
|
|
569
|
+
: queue.flatMap((e) => e.modelMessages);
|
|
570
|
+
// Clear the queue and record injected IDs
|
|
571
|
+
queue.length = 0;
|
|
572
|
+
const injectedIds = locals_js_1.locals.get(chatInjectedMessageIdsKey);
|
|
573
|
+
if (injectedIds) {
|
|
574
|
+
for (const m of uiMessages)
|
|
575
|
+
injectedIds.add(m.id);
|
|
576
|
+
}
|
|
577
|
+
// Write injection confirmation chunk to the stream so the frontend
|
|
578
|
+
// knows which messages were injected and where in the response.
|
|
579
|
+
if (injected.length > 0) {
|
|
580
|
+
try {
|
|
581
|
+
const { waitUntilComplete } = streams_js_1.streams.writer(exports.CHAT_STREAM_KEY, {
|
|
582
|
+
collapsed: true,
|
|
583
|
+
execute: ({ write }) => {
|
|
584
|
+
write({
|
|
585
|
+
type: exports.PENDING_MESSAGE_INJECTED_TYPE,
|
|
586
|
+
id: (0, ai_1.generateId)(),
|
|
587
|
+
data: {
|
|
588
|
+
messageIds: uiMessages.map((m) => m.id),
|
|
589
|
+
messages: uiMessages.map((m, idx) => ({
|
|
590
|
+
id: m.id,
|
|
591
|
+
text: messageTexts[idx] ?? "",
|
|
592
|
+
})),
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
await waitUntilComplete();
|
|
598
|
+
}
|
|
599
|
+
catch { /* non-fatal — stream write failed */ }
|
|
600
|
+
}
|
|
601
|
+
// Fire onInjected callback
|
|
602
|
+
if (config.onInjected && injected.length > 0) {
|
|
603
|
+
try {
|
|
604
|
+
await config.onInjected({
|
|
605
|
+
messages: uiMessages,
|
|
606
|
+
injectedModelMessages: injected,
|
|
607
|
+
chatId: ctx?.chatId ?? "",
|
|
608
|
+
turn: ctx?.turn ?? 0,
|
|
609
|
+
stepNumber,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
catch { /* non-fatal */ }
|
|
613
|
+
}
|
|
614
|
+
return injected;
|
|
615
|
+
}, {
|
|
616
|
+
attributes: {
|
|
617
|
+
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "tabler-message-forward",
|
|
618
|
+
"pending.message_count": uiMessages.length,
|
|
619
|
+
"pending.step_number": stepNumber,
|
|
620
|
+
"pending.messages": messageTexts,
|
|
621
|
+
...(ctx?.chatId ? { "pending.chat_id": ctx.chatId } : {}),
|
|
622
|
+
...(ctx?.turn != null ? { "pending.turn": ctx.turn } : {}),
|
|
623
|
+
...(0, v3_1.accessoryAttributes)({
|
|
624
|
+
items: [
|
|
625
|
+
{ text: `${uiMessages.length} message${uiMessages.length === 1 ? "" : "s"}`, variant: "normal" },
|
|
626
|
+
{ text: `between steps ${stepNumber} and ${stepNumber + 1}`, variant: "normal" },
|
|
627
|
+
],
|
|
628
|
+
style: "codepath",
|
|
629
|
+
}),
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
// chat.isCompactionSafe — check if it's safe to compact messages
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
/**
|
|
637
|
+
* Checks whether it's safe to compact the message history. Returns `false`
|
|
638
|
+
* if any tool calls are in-flight (incomplete tool invocations without results).
|
|
639
|
+
*
|
|
640
|
+
* Call before `chat.setMessages()` to avoid corrupting tool-call state.
|
|
641
|
+
*/
|
|
642
|
+
function isCompactionSafe(messages) {
|
|
643
|
+
for (const msg of messages) {
|
|
644
|
+
if (msg.role !== "assistant")
|
|
645
|
+
continue;
|
|
646
|
+
for (const part of msg.parts) {
|
|
647
|
+
if (part.type === "tool-invocation") {
|
|
648
|
+
const state = part.toolInvocation?.state ?? part.state;
|
|
649
|
+
if (state !== "result" && state !== "error") {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
/** @internal */
|
|
658
|
+
const chatPromptKey = locals_js_1.locals.create("chat.prompt");
|
|
659
|
+
/**
|
|
660
|
+
* Store a resolved prompt (or plain string) for the current run.
|
|
661
|
+
* Call from any hook (`onPreload`, `onChatStart`, `onTurnStart`) or `run()`.
|
|
662
|
+
*/
|
|
663
|
+
function setChatPrompt(resolved) {
|
|
664
|
+
if (typeof resolved === "string") {
|
|
665
|
+
locals_js_1.locals.set(chatPromptKey, {
|
|
666
|
+
text: resolved,
|
|
667
|
+
model: undefined,
|
|
668
|
+
config: undefined,
|
|
669
|
+
promptId: "",
|
|
670
|
+
version: 0,
|
|
671
|
+
labels: [],
|
|
672
|
+
toAISDKTelemetry: () => ({
|
|
673
|
+
experimental_telemetry: { isEnabled: true, metadata: {} },
|
|
674
|
+
}),
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
locals_js_1.locals.set(chatPromptKey, resolved);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Read the stored prompt. Throws if `chat.prompt.set()` has not been called.
|
|
683
|
+
*/
|
|
684
|
+
function getChatPrompt() {
|
|
685
|
+
const prompt = locals_js_1.locals.get(chatPromptKey);
|
|
686
|
+
if (!prompt) {
|
|
687
|
+
throw new Error("chat.prompt() called before chat.prompt.set(). Set a prompt in onPreload, onChatStart, onTurnStart, or run() first.");
|
|
688
|
+
}
|
|
689
|
+
return prompt;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Returns an options object ready to spread into `streamText()`.
|
|
693
|
+
*
|
|
694
|
+
* Includes `system`, `experimental_telemetry`, and any config fields
|
|
695
|
+
* (temperature, maxTokens, etc.) from the stored prompt.
|
|
696
|
+
*
|
|
697
|
+
* When a `registry` is provided and the prompt has a `model` string,
|
|
698
|
+
* the resolved `LanguageModel` is included as `model`.
|
|
699
|
+
*
|
|
700
|
+
* If no prompt has been set, returns `{}` (no-op spread).
|
|
701
|
+
*/
|
|
702
|
+
function toStreamTextOptions(options) {
|
|
703
|
+
const prompt = locals_js_1.locals.get(chatPromptKey);
|
|
704
|
+
if (!prompt)
|
|
705
|
+
return {};
|
|
706
|
+
const result = {
|
|
707
|
+
system: prompt.text,
|
|
708
|
+
};
|
|
709
|
+
// Resolve model via registry if both are present
|
|
710
|
+
if (options?.registry && prompt.model) {
|
|
711
|
+
result.model = options.registry.languageModel(prompt.model);
|
|
712
|
+
}
|
|
713
|
+
// Spread config (temperature, maxTokens, etc.)
|
|
714
|
+
if (prompt.config) {
|
|
715
|
+
Object.assign(result, prompt.config);
|
|
716
|
+
}
|
|
717
|
+
// Add telemetry (forward additional metadata from caller)
|
|
718
|
+
const telemetry = prompt.toAISDKTelemetry(options?.telemetry);
|
|
719
|
+
Object.assign(result, telemetry);
|
|
720
|
+
// Auto-inject prepareStep when compaction or pendingMessages is configured.
|
|
721
|
+
const taskCompaction = locals_js_1.locals.get(chatTaskCompactionKey);
|
|
722
|
+
const taskPendingMessages = locals_js_1.locals.get(chatPendingMessagesKey);
|
|
723
|
+
if (taskCompaction || taskPendingMessages) {
|
|
724
|
+
result.prepareStep = async ({ messages, steps }) => {
|
|
725
|
+
let resultMessages;
|
|
726
|
+
// 1. Compaction
|
|
727
|
+
if (taskCompaction) {
|
|
728
|
+
const compactResult = await chatCompact(messages, steps, {
|
|
729
|
+
shouldCompact: taskCompaction.shouldCompact,
|
|
730
|
+
summarize: (msgs) => {
|
|
731
|
+
const ctx = locals_js_1.locals.get(chatTurnContextKey);
|
|
732
|
+
const lastStep = steps.at(-1);
|
|
733
|
+
return taskCompaction.summarize({
|
|
734
|
+
messages: msgs,
|
|
735
|
+
usage: lastStep?.usage,
|
|
736
|
+
source: "inner",
|
|
737
|
+
stepNumber: steps.length - 1,
|
|
738
|
+
chatId: ctx?.chatId,
|
|
739
|
+
turn: ctx?.turn,
|
|
740
|
+
clientData: ctx?.clientData,
|
|
741
|
+
});
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
if (compactResult.type !== "skipped") {
|
|
745
|
+
resultMessages = compactResult.messages;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// 2. Pending message injection (steering)
|
|
749
|
+
if (taskPendingMessages) {
|
|
750
|
+
const injected = await drainSteeringQueue(taskPendingMessages, resultMessages ?? messages, steps);
|
|
751
|
+
if (injected.length > 0) {
|
|
752
|
+
resultMessages = [...(resultMessages ?? messages), ...injected];
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return resultMessages ? { messages: resultMessages } : undefined;
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
221
760
|
function isUIMessageStreamable(value) {
|
|
222
761
|
return (typeof value === "object" &&
|
|
223
762
|
value !== null &&
|
|
@@ -324,7 +863,7 @@ async function pipeChat(source, options) {
|
|
|
324
863
|
* ```
|
|
325
864
|
*/
|
|
326
865
|
function chatTask(options) {
|
|
327
|
-
const { run: userRun, clientDataSchema, onPreload, onChatStart, onTurnStart, onTurnComplete, maxTurns = 100, turnTimeout = "1h",
|
|
866
|
+
const { run: userRun, clientDataSchema, onPreload, onChatStart, onTurnStart, onBeforeTurnComplete, onCompacted, compaction, pendingMessages: pendingMessagesConfig, prepareMessages, onTurnComplete, maxTurns = 100, turnTimeout = "1h", idleTimeoutInSeconds = 30, chatAccessTokenTTL = "1h", preloadIdleTimeoutInSeconds, preloadTimeout, uiMessageStreamOptions, ...restOptions } = options;
|
|
328
867
|
const parseClientData = clientDataSchema
|
|
329
868
|
? (0, v3_1.getSchemaParseFn)(clientDataSchema)
|
|
330
869
|
: undefined;
|
|
@@ -340,6 +879,19 @@ function chatTask(options) {
|
|
|
340
879
|
if (uiMessageStreamOptions) {
|
|
341
880
|
locals_js_1.locals.set(chatUIStreamStaticKey, uiMessageStreamOptions);
|
|
342
881
|
}
|
|
882
|
+
// Store onCompacted hook in locals so chat.compact() can call it
|
|
883
|
+
if (onCompacted) {
|
|
884
|
+
locals_js_1.locals.set(chatOnCompactedKey, onCompacted);
|
|
885
|
+
}
|
|
886
|
+
if (prepareMessages) {
|
|
887
|
+
locals_js_1.locals.set(chatPrepareMessagesKey, prepareMessages);
|
|
888
|
+
}
|
|
889
|
+
if (compaction) {
|
|
890
|
+
locals_js_1.locals.set(chatTaskCompactionKey, compaction);
|
|
891
|
+
}
|
|
892
|
+
if (pendingMessagesConfig) {
|
|
893
|
+
locals_js_1.locals.set(chatPendingMessagesKey, pendingMessagesConfig);
|
|
894
|
+
}
|
|
343
895
|
let currentWirePayload = payload;
|
|
344
896
|
const continuation = payload.continuation ?? false;
|
|
345
897
|
const previousRunId = payload.previousRunId;
|
|
@@ -351,6 +903,9 @@ function chatTask(options) {
|
|
|
351
903
|
// Accumulated UI messages for persistence. Mirrors the model accumulator
|
|
352
904
|
// but in frontend-friendly UIMessage format (with parts, id, etc.).
|
|
353
905
|
let accumulatedUIMessages = [];
|
|
906
|
+
// Token usage tracking across turns
|
|
907
|
+
let previousTurnUsage;
|
|
908
|
+
let cumulativeUsage = emptyUsage();
|
|
354
909
|
// Mutable reference to the current turn's stop controller so the
|
|
355
910
|
// stop input stream listener (registered once) can abort the right turn.
|
|
356
911
|
let currentStopController;
|
|
@@ -403,14 +958,14 @@ function chatTask(options) {
|
|
|
403
958
|
});
|
|
404
959
|
}
|
|
405
960
|
// Wait for the first real message — use preload-specific timeouts if configured
|
|
406
|
-
const
|
|
407
|
-
??
|
|
408
|
-
??
|
|
961
|
+
const effectivePreloadIdleTimeout = payload.idleTimeoutInSeconds
|
|
962
|
+
?? preloadIdleTimeoutInSeconds
|
|
963
|
+
?? idleTimeoutInSeconds;
|
|
409
964
|
const effectivePreloadTimeout = metadata_js_1.metadata.get(TURN_TIMEOUT_METADATA_KEY)
|
|
410
965
|
?? preloadTimeout
|
|
411
966
|
?? turnTimeout;
|
|
412
|
-
const preloadResult = await messagesInput.
|
|
413
|
-
|
|
967
|
+
const preloadResult = await messagesInput.waitWithIdleTimeout({
|
|
968
|
+
idleTimeoutInSeconds: effectivePreloadIdleTimeout,
|
|
414
969
|
timeout: effectivePreloadTimeout,
|
|
415
970
|
spanName: "waiting for first message",
|
|
416
971
|
});
|
|
@@ -450,9 +1005,12 @@ function chatTask(options) {
|
|
|
450
1005
|
turnAttributes["chat.client_data"] =
|
|
451
1006
|
typeof wireMetadata === "string" ? wireMetadata : JSON.stringify(wireMetadata);
|
|
452
1007
|
}
|
|
453
|
-
const turnResult = await tracer_js_1.tracer.startActiveSpan(`chat turn ${turn + 1}`, async () => {
|
|
1008
|
+
const turnResult = await tracer_js_1.tracer.startActiveSpan(`chat turn ${turn + 1}`, async (turnSpan) => {
|
|
454
1009
|
locals_js_1.locals.set(chatPipeCountKey, 0);
|
|
455
1010
|
locals_js_1.locals.set(chatDeferKey, new Set());
|
|
1011
|
+
locals_js_1.locals.set(chatCompactionStateKey, undefined);
|
|
1012
|
+
locals_js_1.locals.set(chatSteeringQueueKey, []);
|
|
1013
|
+
locals_js_1.locals.set(chatInjectedMessageIdsKey, new Set());
|
|
456
1014
|
// Store chat context for auto-detection by ai.tool subtasks
|
|
457
1015
|
locals_js_1.locals.set(chatTurnContextKey, {
|
|
458
1016
|
chatId: currentWirePayload.chatId,
|
|
@@ -470,7 +1028,39 @@ function chatTask(options) {
|
|
|
470
1028
|
const combinedSignal = AbortSignal.any([runSignal, stopController.signal]);
|
|
471
1029
|
// Buffer messages that arrive during streaming
|
|
472
1030
|
const pendingMessages = [];
|
|
473
|
-
const
|
|
1031
|
+
const pmConfig = locals_js_1.locals.get(chatPendingMessagesKey);
|
|
1032
|
+
const msgSub = messagesInput.on(async (msg) => {
|
|
1033
|
+
// If pendingMessages is configured, route to the steering queue
|
|
1034
|
+
// instead of the wire buffer. The frontend handles re-sending
|
|
1035
|
+
// non-injected messages via sendMessage on turn complete.
|
|
1036
|
+
if (pmConfig) {
|
|
1037
|
+
const lastUIMessage = msg.messages?.[msg.messages.length - 1];
|
|
1038
|
+
if (lastUIMessage) {
|
|
1039
|
+
if (pmConfig.onReceived) {
|
|
1040
|
+
try {
|
|
1041
|
+
await pmConfig.onReceived({
|
|
1042
|
+
message: lastUIMessage,
|
|
1043
|
+
chatId: currentWirePayload.chatId,
|
|
1044
|
+
turn,
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
catch { /* non-fatal */ }
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
const queue = locals_js_1.locals.get(chatSteeringQueueKey) ?? [];
|
|
1051
|
+
// Deduplicate by message ID — guards against double-sends
|
|
1052
|
+
if (lastUIMessage.id && queue.some((e) => e.uiMessage.id === lastUIMessage.id)) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
const modelMsgs = await toModelMessages([lastUIMessage]);
|
|
1056
|
+
queue.push({ uiMessage: lastUIMessage, modelMessages: modelMsgs });
|
|
1057
|
+
locals_js_1.locals.set(chatSteeringQueueKey, queue);
|
|
1058
|
+
}
|
|
1059
|
+
catch { /* conversion failed — skip steering queue */ }
|
|
1060
|
+
}
|
|
1061
|
+
return; // Don't add to wire buffer — frontend handles non-injected case
|
|
1062
|
+
}
|
|
1063
|
+
// No pendingMessages config — standard wire buffer for next turn
|
|
474
1064
|
pendingMessages.push(msg);
|
|
475
1065
|
});
|
|
476
1066
|
// Clean up any incomplete tool parts in the incoming history.
|
|
@@ -482,7 +1072,7 @@ function chatTask(options) {
|
|
|
482
1072
|
// Convert the incoming UIMessages to model messages and update the accumulator.
|
|
483
1073
|
// Turn 1: full history from the frontend → replaces the accumulator.
|
|
484
1074
|
// Turn 2+: only the new message(s) → appended to the accumulator.
|
|
485
|
-
const incomingModelMessages = await (
|
|
1075
|
+
const incomingModelMessages = await toModelMessages(cleanedUIMessages);
|
|
486
1076
|
// Track new messages for this turn (user input + assistant response).
|
|
487
1077
|
const turnNewModelMessages = [];
|
|
488
1078
|
const turnNewUIMessages = [];
|
|
@@ -570,7 +1160,16 @@ function chatTask(options) {
|
|
|
570
1160
|
continuation,
|
|
571
1161
|
previousRunId,
|
|
572
1162
|
preloaded,
|
|
1163
|
+
previousTurnUsage,
|
|
1164
|
+
totalUsage: cumulativeUsage,
|
|
573
1165
|
});
|
|
1166
|
+
// Check if onTurnStart replaced messages (compaction)
|
|
1167
|
+
const turnStartOverride = locals_js_1.locals.get(chatOverrideMessagesKey);
|
|
1168
|
+
if (turnStartOverride) {
|
|
1169
|
+
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
1170
|
+
accumulatedUIMessages = [...turnStartOverride];
|
|
1171
|
+
accumulatedMessages = await toModelMessages(turnStartOverride);
|
|
1172
|
+
}
|
|
574
1173
|
}, {
|
|
575
1174
|
attributes: {
|
|
576
1175
|
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart",
|
|
@@ -595,14 +1194,17 @@ function chatTask(options) {
|
|
|
595
1194
|
let resolveOnFinish;
|
|
596
1195
|
const onFinishPromise = new Promise((r) => { resolveOnFinish = r; });
|
|
597
1196
|
let onFinishAttached = false;
|
|
1197
|
+
let runResult;
|
|
598
1198
|
try {
|
|
599
|
-
|
|
1199
|
+
runResult = await userRun({
|
|
600
1200
|
...restWire,
|
|
601
|
-
messages: accumulatedMessages,
|
|
1201
|
+
messages: await applyPrepareMessages(accumulatedMessages, "run"),
|
|
602
1202
|
clientData,
|
|
603
1203
|
continuation,
|
|
604
1204
|
previousRunId,
|
|
605
1205
|
preloaded,
|
|
1206
|
+
previousTurnUsage,
|
|
1207
|
+
totalUsage: cumulativeUsage,
|
|
606
1208
|
signal: combinedSignal,
|
|
607
1209
|
cancelSignal,
|
|
608
1210
|
stopSignal,
|
|
@@ -610,9 +1212,9 @@ function chatTask(options) {
|
|
|
610
1212
|
// Auto-pipe if the run function returned a StreamTextResult or similar,
|
|
611
1213
|
// but only if pipeChat() wasn't already called manually during this turn.
|
|
612
1214
|
// We call toUIMessageStream ourselves to attach onFinish for response capture.
|
|
613
|
-
if ((locals_js_1.locals.get(chatPipeCountKey) ?? 0) === 0 && isUIMessageStreamable(
|
|
1215
|
+
if ((locals_js_1.locals.get(chatPipeCountKey) ?? 0) === 0 && isUIMessageStreamable(runResult)) {
|
|
614
1216
|
onFinishAttached = true;
|
|
615
|
-
const uiStream =
|
|
1217
|
+
const uiStream = runResult.toUIMessageStream({
|
|
616
1218
|
...resolveUIMessageStreamOptions(),
|
|
617
1219
|
onFinish: ({ responseMessage }) => {
|
|
618
1220
|
capturedResponseMessage = responseMessage;
|
|
@@ -642,6 +1244,72 @@ function chatTask(options) {
|
|
|
642
1244
|
if (onFinishAttached) {
|
|
643
1245
|
await onFinishPromise;
|
|
644
1246
|
}
|
|
1247
|
+
// Capture token usage from the streamText result (if available).
|
|
1248
|
+
// totalUsage is a PromiseLike that resolves after the stream is consumed.
|
|
1249
|
+
let turnUsage;
|
|
1250
|
+
if (runResult != null && typeof runResult.totalUsage?.then === "function") {
|
|
1251
|
+
try {
|
|
1252
|
+
turnUsage = await runResult.totalUsage;
|
|
1253
|
+
}
|
|
1254
|
+
catch { /* non-fatal — usage capture failed */ }
|
|
1255
|
+
}
|
|
1256
|
+
if (turnUsage) {
|
|
1257
|
+
cumulativeUsage = addUsage(cumulativeUsage, turnUsage);
|
|
1258
|
+
previousTurnUsage = turnUsage;
|
|
1259
|
+
// Add usage attributes to the turn span
|
|
1260
|
+
if (turnUsage.inputTokens != null) {
|
|
1261
|
+
turnSpan.setAttribute("gen_ai.usage.input_tokens", turnUsage.inputTokens);
|
|
1262
|
+
}
|
|
1263
|
+
if (turnUsage.outputTokens != null) {
|
|
1264
|
+
turnSpan.setAttribute("gen_ai.usage.output_tokens", turnUsage.outputTokens);
|
|
1265
|
+
}
|
|
1266
|
+
if (turnUsage.totalTokens != null) {
|
|
1267
|
+
turnSpan.setAttribute("gen_ai.usage.total_tokens", turnUsage.totalTokens);
|
|
1268
|
+
}
|
|
1269
|
+
if (cumulativeUsage.totalTokens != null) {
|
|
1270
|
+
turnSpan.setAttribute("gen_ai.usage.cumulative_total_tokens", cumulativeUsage.totalTokens);
|
|
1271
|
+
}
|
|
1272
|
+
if (cumulativeUsage.inputTokens != null) {
|
|
1273
|
+
turnSpan.setAttribute("gen_ai.usage.cumulative_input_tokens", cumulativeUsage.inputTokens);
|
|
1274
|
+
}
|
|
1275
|
+
if (cumulativeUsage.outputTokens != null) {
|
|
1276
|
+
turnSpan.setAttribute("gen_ai.usage.cumulative_output_tokens", cumulativeUsage.outputTokens);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
// Check if run() (e.g. via prepareStep) replaced messages during this turn.
|
|
1280
|
+
// This supports intra-turn compaction — the compacted messages become the
|
|
1281
|
+
// new base, and the response gets appended on top.
|
|
1282
|
+
const runOverride = locals_js_1.locals.get(chatOverrideMessagesKey);
|
|
1283
|
+
if (runOverride) {
|
|
1284
|
+
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
1285
|
+
accumulatedUIMessages = [...runOverride];
|
|
1286
|
+
accumulatedMessages = await toModelMessages(runOverride);
|
|
1287
|
+
}
|
|
1288
|
+
// Check if compaction set a model-only override (preserves UI messages).
|
|
1289
|
+
// Apply compactUIMessages/compactModelMessages callbacks if configured.
|
|
1290
|
+
const modelOnlyOverride = locals_js_1.locals.get(chatOverrideModelMessagesKey);
|
|
1291
|
+
if (modelOnlyOverride) {
|
|
1292
|
+
const compactionSummary = locals_js_1.locals.get(chatCompactionStateKey)?.summary ?? "";
|
|
1293
|
+
const taskCompactionConfig = locals_js_1.locals.get(chatTaskCompactionKey);
|
|
1294
|
+
locals_js_1.locals.set(chatOverrideModelMessagesKey, undefined);
|
|
1295
|
+
const compactEvent = {
|
|
1296
|
+
summary: compactionSummary,
|
|
1297
|
+
uiMessages: accumulatedUIMessages,
|
|
1298
|
+
modelMessages: accumulatedMessages,
|
|
1299
|
+
chatId: currentWirePayload.chatId,
|
|
1300
|
+
turn,
|
|
1301
|
+
clientData,
|
|
1302
|
+
source: "inner",
|
|
1303
|
+
};
|
|
1304
|
+
// Apply model messages: callback or default (use override)
|
|
1305
|
+
accumulatedMessages = taskCompactionConfig?.compactModelMessages
|
|
1306
|
+
? await taskCompactionConfig.compactModelMessages(compactEvent)
|
|
1307
|
+
: modelOnlyOverride;
|
|
1308
|
+
// Apply UI messages: callback or default (preserve all)
|
|
1309
|
+
if (taskCompactionConfig?.compactUIMessages) {
|
|
1310
|
+
accumulatedUIMessages = await taskCompactionConfig.compactUIMessages(compactEvent);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
645
1313
|
// Determine if the user stopped generation this turn (not a full run cancel).
|
|
646
1314
|
const wasStopped = stopController.signal.aborted && !runSignal.aborted;
|
|
647
1315
|
// Append the assistant's response (partial or complete) to the accumulator.
|
|
@@ -664,7 +1332,7 @@ function chatTask(options) {
|
|
|
664
1332
|
accumulatedUIMessages.push(capturedResponseMessage);
|
|
665
1333
|
turnNewUIMessages.push(capturedResponseMessage);
|
|
666
1334
|
try {
|
|
667
|
-
const responseModelMessages = await (
|
|
1335
|
+
const responseModelMessages = await toModelMessages([
|
|
668
1336
|
stripProviderMetadata(capturedResponseMessage),
|
|
669
1337
|
]);
|
|
670
1338
|
accumulatedMessages.push(...responseModelMessages);
|
|
@@ -680,11 +1348,8 @@ function chatTask(options) {
|
|
|
680
1348
|
// accumulation (e.g. via a `chat.addMessages()` helper).
|
|
681
1349
|
if (runSignal.aborted)
|
|
682
1350
|
return "exit";
|
|
683
|
-
// Write turn-complete control chunk so frontend closes its stream.
|
|
684
|
-
// Capture the lastEventId from the stream writer for resume support.
|
|
685
|
-
const turnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId, turnAccessToken);
|
|
686
1351
|
// Await deferred background work (e.g. DB writes from onTurnStart)
|
|
687
|
-
// before firing
|
|
1352
|
+
// before firing hooks so they can rely on the work being done.
|
|
688
1353
|
const deferredWork = locals_js_1.locals.get(chatDeferKey);
|
|
689
1354
|
if (deferredWork && deferredWork.size > 0) {
|
|
690
1355
|
await Promise.race([
|
|
@@ -692,27 +1357,169 @@ function chatTask(options) {
|
|
|
692
1357
|
new Promise((r) => setTimeout(r, 5_000)),
|
|
693
1358
|
]);
|
|
694
1359
|
}
|
|
695
|
-
//
|
|
1360
|
+
// Outer-loop compaction: runs between turns for single-step responses
|
|
1361
|
+
// where prepareStep never fires (no tool calls = no step boundaries).
|
|
1362
|
+
// Only triggers when: task has compaction configured, prepareStep didn't
|
|
1363
|
+
// already compact this turn, and shouldCompact returns true.
|
|
1364
|
+
const outerCompaction = locals_js_1.locals.get(chatTaskCompactionKey);
|
|
1365
|
+
const innerCompactionState = locals_js_1.locals.get(chatCompactionStateKey);
|
|
1366
|
+
if (outerCompaction && !innerCompactionState && turnUsage && !wasStopped) {
|
|
1367
|
+
const shouldTrigger = await outerCompaction.shouldCompact({
|
|
1368
|
+
messages: accumulatedMessages,
|
|
1369
|
+
totalTokens: turnUsage.totalTokens,
|
|
1370
|
+
inputTokens: turnUsage.inputTokens,
|
|
1371
|
+
outputTokens: turnUsage.outputTokens,
|
|
1372
|
+
usage: turnUsage,
|
|
1373
|
+
totalUsage: cumulativeUsage,
|
|
1374
|
+
chatId: currentWirePayload.chatId,
|
|
1375
|
+
turn,
|
|
1376
|
+
clientData,
|
|
1377
|
+
source: "outer",
|
|
1378
|
+
});
|
|
1379
|
+
if (shouldTrigger) {
|
|
1380
|
+
await tracer_js_1.tracer.startActiveSpan("context compaction (outer loop)", async (compactionSpan) => {
|
|
1381
|
+
const compactionId = (0, ai_1.generateId)();
|
|
1382
|
+
const { waitUntilComplete } = streams_js_1.streams.writer(exports.CHAT_STREAM_KEY, {
|
|
1383
|
+
spanName: "stream compaction chunks",
|
|
1384
|
+
collapsed: true,
|
|
1385
|
+
execute: async ({ write }) => {
|
|
1386
|
+
write({
|
|
1387
|
+
type: "data-compaction",
|
|
1388
|
+
id: compactionId,
|
|
1389
|
+
data: { status: "compacting", totalTokens: turnUsage.totalTokens },
|
|
1390
|
+
});
|
|
1391
|
+
const summary = await outerCompaction.summarize({
|
|
1392
|
+
messages: accumulatedMessages,
|
|
1393
|
+
usage: turnUsage,
|
|
1394
|
+
totalUsage: cumulativeUsage,
|
|
1395
|
+
chatId: currentWirePayload.chatId,
|
|
1396
|
+
turn,
|
|
1397
|
+
clientData,
|
|
1398
|
+
source: "outer",
|
|
1399
|
+
});
|
|
1400
|
+
// Apply compactModelMessages/compactUIMessages callbacks, or defaults.
|
|
1401
|
+
const outerCompactEvent = {
|
|
1402
|
+
summary,
|
|
1403
|
+
uiMessages: accumulatedUIMessages,
|
|
1404
|
+
modelMessages: accumulatedMessages,
|
|
1405
|
+
chatId: currentWirePayload.chatId,
|
|
1406
|
+
turn,
|
|
1407
|
+
clientData,
|
|
1408
|
+
source: "outer",
|
|
1409
|
+
};
|
|
1410
|
+
// Model messages: callback or default (replace with summary)
|
|
1411
|
+
accumulatedMessages = outerCompaction.compactModelMessages
|
|
1412
|
+
? await outerCompaction.compactModelMessages(outerCompactEvent)
|
|
1413
|
+
: [{ role: "assistant", content: [{ type: "text", text: `[Conversation summary]\n\n${summary}` }] }];
|
|
1414
|
+
// UI messages: callback or default (preserve all)
|
|
1415
|
+
if (outerCompaction.compactUIMessages) {
|
|
1416
|
+
accumulatedUIMessages = await outerCompaction.compactUIMessages(outerCompactEvent);
|
|
1417
|
+
}
|
|
1418
|
+
// Fire onCompacted hook
|
|
1419
|
+
const onCompactedHook = locals_js_1.locals.get(chatOnCompactedKey);
|
|
1420
|
+
if (onCompactedHook) {
|
|
1421
|
+
await onCompactedHook({
|
|
1422
|
+
summary,
|
|
1423
|
+
messages: accumulatedMessages,
|
|
1424
|
+
messageCount: accumulatedMessages.length,
|
|
1425
|
+
usage: turnUsage,
|
|
1426
|
+
totalTokens: turnUsage.totalTokens,
|
|
1427
|
+
inputTokens: turnUsage.inputTokens,
|
|
1428
|
+
outputTokens: turnUsage.outputTokens,
|
|
1429
|
+
stepNumber: -1, // outer loop, not a step
|
|
1430
|
+
chatId: currentWirePayload.chatId,
|
|
1431
|
+
turn,
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
compactionSpan.setAttribute("compaction.summary_length", summary.length);
|
|
1435
|
+
write({
|
|
1436
|
+
type: "data-compaction",
|
|
1437
|
+
id: compactionId,
|
|
1438
|
+
data: { status: "complete", totalTokens: turnUsage.totalTokens },
|
|
1439
|
+
});
|
|
1440
|
+
},
|
|
1441
|
+
});
|
|
1442
|
+
await waitUntilComplete();
|
|
1443
|
+
}, {
|
|
1444
|
+
attributes: {
|
|
1445
|
+
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "tabler-scissors",
|
|
1446
|
+
"compaction.total_tokens": turnUsage.totalTokens ?? 0,
|
|
1447
|
+
"compaction.input_tokens": turnUsage.inputTokens ?? 0,
|
|
1448
|
+
"compaction.message_count": accumulatedMessages.length,
|
|
1449
|
+
"compaction.outer_loop": true,
|
|
1450
|
+
"compaction.turn": turn,
|
|
1451
|
+
...(currentWirePayload.chatId ? { "compaction.chat_id": currentWirePayload.chatId } : {}),
|
|
1452
|
+
...(0, v3_1.accessoryAttributes)({
|
|
1453
|
+
items: [
|
|
1454
|
+
{ text: `${turnUsage.totalTokens ?? 0} tokens`, variant: "normal" },
|
|
1455
|
+
{ text: `${accumulatedMessages.length} msgs`, variant: "normal" },
|
|
1456
|
+
{ text: "outer loop", variant: "normal" },
|
|
1457
|
+
],
|
|
1458
|
+
style: "codepath",
|
|
1459
|
+
}),
|
|
1460
|
+
},
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
const turnCompleteEvent = {
|
|
1465
|
+
chatId: currentWirePayload.chatId,
|
|
1466
|
+
messages: accumulatedMessages,
|
|
1467
|
+
uiMessages: accumulatedUIMessages,
|
|
1468
|
+
newMessages: turnNewModelMessages,
|
|
1469
|
+
newUIMessages: turnNewUIMessages,
|
|
1470
|
+
responseMessage: capturedResponseMessage,
|
|
1471
|
+
rawResponseMessage,
|
|
1472
|
+
turn,
|
|
1473
|
+
runId: currentRunId,
|
|
1474
|
+
chatAccessToken: turnAccessToken,
|
|
1475
|
+
clientData,
|
|
1476
|
+
stopped: wasStopped,
|
|
1477
|
+
continuation,
|
|
1478
|
+
previousRunId,
|
|
1479
|
+
preloaded,
|
|
1480
|
+
usage: turnUsage,
|
|
1481
|
+
totalUsage: cumulativeUsage,
|
|
1482
|
+
};
|
|
1483
|
+
// Fire onBeforeTurnComplete — stream is still open so the hook
|
|
1484
|
+
// can write custom chunks to the frontend (e.g. compaction progress).
|
|
1485
|
+
if (onBeforeTurnComplete) {
|
|
1486
|
+
await tracer_js_1.tracer.startActiveSpan("onBeforeTurnComplete()", async () => {
|
|
1487
|
+
await onBeforeTurnComplete(turnCompleteEvent);
|
|
1488
|
+
// Check if the hook replaced messages (compaction)
|
|
1489
|
+
const override = locals_js_1.locals.get(chatOverrideMessagesKey);
|
|
1490
|
+
if (override) {
|
|
1491
|
+
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
1492
|
+
accumulatedUIMessages = [...override];
|
|
1493
|
+
accumulatedMessages = await toModelMessages(override);
|
|
1494
|
+
// Update event so onTurnComplete sees compacted messages
|
|
1495
|
+
turnCompleteEvent.messages = accumulatedMessages;
|
|
1496
|
+
turnCompleteEvent.uiMessages = accumulatedUIMessages;
|
|
1497
|
+
}
|
|
1498
|
+
}, {
|
|
1499
|
+
attributes: {
|
|
1500
|
+
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete",
|
|
1501
|
+
[v3_1.SemanticInternalAttributes.COLLAPSED]: true,
|
|
1502
|
+
"chat.id": currentWirePayload.chatId,
|
|
1503
|
+
"chat.turn": turn + 1,
|
|
1504
|
+
},
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
// Write turn-complete control chunk — closes the frontend stream.
|
|
1508
|
+
const turnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId, turnAccessToken);
|
|
1509
|
+
// Fire onTurnComplete — stream is closed, use for persistence.
|
|
696
1510
|
if (onTurnComplete) {
|
|
697
1511
|
await tracer_js_1.tracer.startActiveSpan("onTurnComplete()", async () => {
|
|
698
1512
|
await onTurnComplete({
|
|
699
|
-
|
|
700
|
-
messages: accumulatedMessages,
|
|
701
|
-
uiMessages: accumulatedUIMessages,
|
|
702
|
-
newMessages: turnNewModelMessages,
|
|
703
|
-
newUIMessages: turnNewUIMessages,
|
|
704
|
-
responseMessage: capturedResponseMessage,
|
|
705
|
-
rawResponseMessage,
|
|
706
|
-
turn,
|
|
707
|
-
runId: currentRunId,
|
|
708
|
-
chatAccessToken: turnAccessToken,
|
|
1513
|
+
...turnCompleteEvent,
|
|
709
1514
|
lastEventId: turnCompleteResult.lastEventId,
|
|
710
|
-
clientData,
|
|
711
|
-
stopped: wasStopped,
|
|
712
|
-
continuation,
|
|
713
|
-
previousRunId,
|
|
714
|
-
preloaded,
|
|
715
1515
|
});
|
|
1516
|
+
// Check if onTurnComplete replaced messages (compaction)
|
|
1517
|
+
const turnCompleteOverride = locals_js_1.locals.get(chatOverrideMessagesKey);
|
|
1518
|
+
if (turnCompleteOverride) {
|
|
1519
|
+
locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
|
|
1520
|
+
accumulatedUIMessages = [...turnCompleteOverride];
|
|
1521
|
+
accumulatedMessages = await toModelMessages(turnCompleteOverride);
|
|
1522
|
+
}
|
|
716
1523
|
}, {
|
|
717
1524
|
attributes: {
|
|
718
1525
|
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete",
|
|
@@ -726,19 +1533,24 @@ function chatTask(options) {
|
|
|
726
1533
|
"chat.messages.count": accumulatedMessages.length,
|
|
727
1534
|
"chat.response.parts.count": capturedResponseMessage?.parts?.length ?? 0,
|
|
728
1535
|
"chat.new_messages.count": turnNewUIMessages.length,
|
|
1536
|
+
...(turnUsage?.inputTokens != null ? { "gen_ai.usage.input_tokens": turnUsage.inputTokens } : {}),
|
|
1537
|
+
...(turnUsage?.outputTokens != null ? { "gen_ai.usage.output_tokens": turnUsage.outputTokens } : {}),
|
|
1538
|
+
...(turnUsage?.totalTokens != null ? { "gen_ai.usage.total_tokens": turnUsage.totalTokens } : {}),
|
|
1539
|
+
...(cumulativeUsage.totalTokens != null ? { "gen_ai.usage.cumulative_total_tokens": cumulativeUsage.totalTokens } : {}),
|
|
729
1540
|
},
|
|
730
1541
|
});
|
|
731
1542
|
}
|
|
732
|
-
// If messages arrived during streaming
|
|
1543
|
+
// If messages arrived during streaming (without pendingMessages config),
|
|
1544
|
+
// use the first one immediately as the next turn.
|
|
733
1545
|
if (pendingMessages.length > 0) {
|
|
734
1546
|
currentWirePayload = pendingMessages[0];
|
|
735
1547
|
return "continue";
|
|
736
1548
|
}
|
|
737
|
-
// Wait for the next message — stay
|
|
738
|
-
const
|
|
1549
|
+
// Wait for the next message — stay idle briefly, then suspend
|
|
1550
|
+
const effectiveIdleTimeout = metadata_js_1.metadata.get(IDLE_TIMEOUT_METADATA_KEY) ?? idleTimeoutInSeconds;
|
|
739
1551
|
const effectiveTurnTimeout = metadata_js_1.metadata.get(TURN_TIMEOUT_METADATA_KEY) ?? turnTimeout;
|
|
740
|
-
const next = await messagesInput.
|
|
741
|
-
|
|
1552
|
+
const next = await messagesInput.waitWithIdleTimeout({
|
|
1553
|
+
idleTimeoutInSeconds: effectiveIdleTimeout,
|
|
742
1554
|
timeout: effectiveTurnTimeout,
|
|
743
1555
|
spanName: "waiting for next message",
|
|
744
1556
|
});
|
|
@@ -787,7 +1599,7 @@ function chatTask(options) {
|
|
|
787
1599
|
// Runtime configuration helpers
|
|
788
1600
|
// ---------------------------------------------------------------------------
|
|
789
1601
|
const TURN_TIMEOUT_METADATA_KEY = "chat.turnTimeout";
|
|
790
|
-
const
|
|
1602
|
+
const IDLE_TIMEOUT_METADATA_KEY = "chat.idleTimeout";
|
|
791
1603
|
/**
|
|
792
1604
|
* Override the turn timeout for subsequent turns in the current run.
|
|
793
1605
|
*
|
|
@@ -827,24 +1639,24 @@ function setTurnTimeoutInSeconds(seconds) {
|
|
|
827
1639
|
metadata_js_1.metadata.set(TURN_TIMEOUT_METADATA_KEY, `${seconds}s`);
|
|
828
1640
|
}
|
|
829
1641
|
/**
|
|
830
|
-
* Override the
|
|
1642
|
+
* Override the idle timeout for subsequent turns in the current run.
|
|
831
1643
|
*
|
|
832
|
-
* The
|
|
1644
|
+
* The idle timeout controls how long the run stays active (using compute)
|
|
833
1645
|
* after each turn, waiting for the next message. During this window,
|
|
834
1646
|
* responses are instant. After it expires, the run suspends.
|
|
835
1647
|
*
|
|
836
|
-
* @param seconds - Number of seconds to stay
|
|
1648
|
+
* @param seconds - Number of seconds to stay idle (0 to suspend immediately)
|
|
837
1649
|
*
|
|
838
1650
|
* @example
|
|
839
1651
|
* ```ts
|
|
840
1652
|
* run: async ({ messages, signal }) => {
|
|
841
|
-
* chat.
|
|
1653
|
+
* chat.setIdleTimeoutInSeconds(60);
|
|
842
1654
|
* return streamText({ model, messages, abortSignal: signal });
|
|
843
1655
|
* }
|
|
844
1656
|
* ```
|
|
845
1657
|
*/
|
|
846
|
-
function
|
|
847
|
-
metadata_js_1.metadata.set(
|
|
1658
|
+
function setIdleTimeoutInSeconds(seconds) {
|
|
1659
|
+
metadata_js_1.metadata.set(IDLE_TIMEOUT_METADATA_KEY, seconds);
|
|
848
1660
|
}
|
|
849
1661
|
/**
|
|
850
1662
|
* Override the `toUIMessageStream()` options for the current turn.
|
|
@@ -1097,13 +1909,20 @@ async function pipeChatAndCapture(source, options) {
|
|
|
1097
1909
|
class ChatMessageAccumulator {
|
|
1098
1910
|
modelMessages = [];
|
|
1099
1911
|
uiMessages = [];
|
|
1912
|
+
_compaction;
|
|
1913
|
+
_pendingMessages;
|
|
1914
|
+
_steeringQueue = [];
|
|
1915
|
+
constructor(options) {
|
|
1916
|
+
this._compaction = options?.compaction;
|
|
1917
|
+
this._pendingMessages = options?.pendingMessages;
|
|
1918
|
+
}
|
|
1100
1919
|
/**
|
|
1101
1920
|
* Add incoming messages from the transport payload.
|
|
1102
1921
|
* Returns the full accumulated model messages for `streamText`.
|
|
1103
1922
|
*/
|
|
1104
1923
|
async addIncoming(messages, trigger, turn) {
|
|
1105
1924
|
const cleaned = messages.map((m) => m.role === "assistant" ? cleanupAbortedParts(m) : m);
|
|
1106
|
-
const model = await (
|
|
1925
|
+
const model = await toModelMessages(cleaned);
|
|
1107
1926
|
if (turn === 0 || trigger === "regenerate-message") {
|
|
1108
1927
|
this.modelMessages = model;
|
|
1109
1928
|
this.uiMessages = [...cleaned];
|
|
@@ -1118,25 +1937,143 @@ class ChatMessageAccumulator {
|
|
|
1118
1937
|
* Add the assistant's response to the accumulator.
|
|
1119
1938
|
* Call after `pipeAndCapture` with the captured response.
|
|
1120
1939
|
*/
|
|
1940
|
+
/**
|
|
1941
|
+
* Replace all accumulated messages (for compaction).
|
|
1942
|
+
* Converts UIMessages to ModelMessages internally.
|
|
1943
|
+
*/
|
|
1944
|
+
async setMessages(uiMessages) {
|
|
1945
|
+
this.uiMessages = [...uiMessages];
|
|
1946
|
+
this.modelMessages = await toModelMessages(uiMessages);
|
|
1947
|
+
}
|
|
1121
1948
|
async addResponse(response) {
|
|
1122
1949
|
if (!response.id) {
|
|
1123
1950
|
response = { ...response, id: (0, ai_1.generateId)() };
|
|
1124
1951
|
}
|
|
1125
1952
|
this.uiMessages.push(response);
|
|
1126
1953
|
try {
|
|
1127
|
-
const msgs = await (
|
|
1954
|
+
const msgs = await toModelMessages([stripProviderMetadata(response)]);
|
|
1128
1955
|
this.modelMessages.push(...msgs);
|
|
1129
1956
|
}
|
|
1130
1957
|
catch {
|
|
1131
1958
|
// Conversion failed — skip model message accumulation for this response
|
|
1132
1959
|
}
|
|
1133
1960
|
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Queue a message for injection via `prepareStep`. Call from a
|
|
1963
|
+
* `messagesInput.on()` listener when a message arrives during streaming.
|
|
1964
|
+
*/
|
|
1965
|
+
steer(message, modelMessages) {
|
|
1966
|
+
if (modelMessages) {
|
|
1967
|
+
this._steeringQueue.push({ uiMessage: message, modelMessages });
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
// Defer conversion — will be done in prepareStep if needed
|
|
1971
|
+
this._steeringQueue.push({ uiMessage: message, modelMessages: [] });
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Queue a message for injection, converting to model messages automatically.
|
|
1976
|
+
*/
|
|
1977
|
+
async steerAsync(message) {
|
|
1978
|
+
const modelMsgs = await toModelMessages([message]);
|
|
1979
|
+
this._steeringQueue.push({ uiMessage: message, modelMessages: modelMsgs });
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Get and clear unconsumed steering messages.
|
|
1983
|
+
*/
|
|
1984
|
+
drainSteering() {
|
|
1985
|
+
const result = this._steeringQueue.map((e) => e.uiMessage);
|
|
1986
|
+
this._steeringQueue = [];
|
|
1987
|
+
return result;
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Returns a `prepareStep` function that handles both compaction and
|
|
1991
|
+
* pending message injection. Pass to `streamText({ prepareStep: conversation.prepareStep() })`.
|
|
1992
|
+
*/
|
|
1993
|
+
prepareStep() {
|
|
1994
|
+
if (!this._compaction && !this._pendingMessages)
|
|
1995
|
+
return undefined;
|
|
1996
|
+
const comp = this._compaction;
|
|
1997
|
+
const pm = this._pendingMessages;
|
|
1998
|
+
const queue = this._steeringQueue;
|
|
1999
|
+
return async ({ messages, steps }) => {
|
|
2000
|
+
let resultMessages;
|
|
2001
|
+
// 1. Compaction
|
|
2002
|
+
if (comp) {
|
|
2003
|
+
const result = await chatCompact(messages, steps, {
|
|
2004
|
+
shouldCompact: comp.shouldCompact,
|
|
2005
|
+
summarize: (msgs) => comp.summarize({ messages: msgs, source: "inner" }),
|
|
2006
|
+
});
|
|
2007
|
+
if (result.type !== "skipped") {
|
|
2008
|
+
resultMessages = result.messages;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
// 2. Pending message injection
|
|
2012
|
+
if (pm && queue.length > 0) {
|
|
2013
|
+
const injected = await drainSteeringQueue(pm, resultMessages ?? messages, steps, queue);
|
|
2014
|
+
if (injected.length > 0) {
|
|
2015
|
+
resultMessages = [...(resultMessages ?? messages), ...injected];
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
return resultMessages ? { messages: resultMessages } : undefined;
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Run outer-loop compaction if needed. Call after adding the response
|
|
2023
|
+
* and capturing usage. Applies `compactModelMessages` and `compactUIMessages`
|
|
2024
|
+
* callbacks if configured.
|
|
2025
|
+
*
|
|
2026
|
+
* @returns `true` if compaction was performed, `false` otherwise.
|
|
2027
|
+
*/
|
|
2028
|
+
async compactIfNeeded(usage, context) {
|
|
2029
|
+
if (!this._compaction || !usage)
|
|
2030
|
+
return false;
|
|
2031
|
+
const shouldTrigger = await this._compaction.shouldCompact({
|
|
2032
|
+
messages: this.modelMessages,
|
|
2033
|
+
totalTokens: usage.totalTokens,
|
|
2034
|
+
inputTokens: usage.inputTokens,
|
|
2035
|
+
outputTokens: usage.outputTokens,
|
|
2036
|
+
usage,
|
|
2037
|
+
totalUsage: context?.totalUsage,
|
|
2038
|
+
chatId: context?.chatId,
|
|
2039
|
+
turn: context?.turn,
|
|
2040
|
+
clientData: context?.clientData,
|
|
2041
|
+
source: "outer",
|
|
2042
|
+
});
|
|
2043
|
+
if (!shouldTrigger)
|
|
2044
|
+
return false;
|
|
2045
|
+
const summary = await this._compaction.summarize({
|
|
2046
|
+
messages: this.modelMessages,
|
|
2047
|
+
usage,
|
|
2048
|
+
totalUsage: context?.totalUsage,
|
|
2049
|
+
chatId: context?.chatId,
|
|
2050
|
+
turn: context?.turn,
|
|
2051
|
+
clientData: context?.clientData,
|
|
2052
|
+
source: "outer",
|
|
2053
|
+
});
|
|
2054
|
+
const compactEvent = {
|
|
2055
|
+
summary,
|
|
2056
|
+
uiMessages: this.uiMessages,
|
|
2057
|
+
modelMessages: this.modelMessages,
|
|
2058
|
+
chatId: context?.chatId ?? "",
|
|
2059
|
+
turn: context?.turn ?? 0,
|
|
2060
|
+
clientData: context?.clientData,
|
|
2061
|
+
source: "outer",
|
|
2062
|
+
};
|
|
2063
|
+
this.modelMessages = this._compaction.compactModelMessages
|
|
2064
|
+
? await this._compaction.compactModelMessages(compactEvent)
|
|
2065
|
+
: [{ role: "assistant", content: [{ type: "text", text: `[Conversation summary]\n\n${summary}` }] }];
|
|
2066
|
+
if (this._compaction.compactUIMessages) {
|
|
2067
|
+
this.uiMessages = await this._compaction.compactUIMessages(compactEvent);
|
|
2068
|
+
}
|
|
2069
|
+
return true;
|
|
2070
|
+
}
|
|
1134
2071
|
}
|
|
1135
2072
|
/**
|
|
1136
2073
|
* Create a chat session that yields turns as an async iterator.
|
|
1137
2074
|
*
|
|
1138
2075
|
* Handles: preload wait, stop signals, message accumulation, turn-complete
|
|
1139
|
-
* signaling, and
|
|
2076
|
+
* signaling, and idle/suspend between turns. You control: initialization,
|
|
1140
2077
|
* model/tool selection, persistence, and any custom per-turn logic.
|
|
1141
2078
|
*
|
|
1142
2079
|
* @example
|
|
@@ -1164,20 +2101,22 @@ class ChatMessageAccumulator {
|
|
|
1164
2101
|
* ```
|
|
1165
2102
|
*/
|
|
1166
2103
|
function createChatSession(payload, options) {
|
|
1167
|
-
const { signal: runSignal,
|
|
2104
|
+
const { signal: runSignal, idleTimeoutInSeconds = 30, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, pendingMessages: sessionPendingMessages, } = options;
|
|
1168
2105
|
return {
|
|
1169
2106
|
[Symbol.asyncIterator]() {
|
|
1170
2107
|
let currentPayload = payload;
|
|
1171
2108
|
let turn = -1;
|
|
1172
2109
|
const stop = createStopSignal();
|
|
1173
2110
|
const accumulator = new ChatMessageAccumulator();
|
|
2111
|
+
let previousTurnUsage;
|
|
2112
|
+
let cumulativeUsage = emptyUsage();
|
|
1174
2113
|
return {
|
|
1175
2114
|
async next() {
|
|
1176
2115
|
turn++;
|
|
1177
2116
|
// First turn: handle preload — wait for the first real message
|
|
1178
2117
|
if (turn === 0 && currentPayload.trigger === "preload") {
|
|
1179
|
-
const result = await messagesInput.
|
|
1180
|
-
|
|
2118
|
+
const result = await messagesInput.waitWithIdleTimeout({
|
|
2119
|
+
idleTimeoutInSeconds: currentPayload.idleTimeoutInSeconds ?? idleTimeoutInSeconds,
|
|
1181
2120
|
timeout,
|
|
1182
2121
|
spanName: "waiting for first message",
|
|
1183
2122
|
});
|
|
@@ -1189,8 +2128,8 @@ function createChatSession(payload, options) {
|
|
|
1189
2128
|
}
|
|
1190
2129
|
// Subsequent turns: wait for the next message
|
|
1191
2130
|
if (turn > 0) {
|
|
1192
|
-
const next = await messagesInput.
|
|
1193
|
-
|
|
2131
|
+
const next = await messagesInput.waitWithIdleTimeout({
|
|
2132
|
+
idleTimeoutInSeconds,
|
|
1194
2133
|
timeout,
|
|
1195
2134
|
spanName: "waiting for next message",
|
|
1196
2135
|
});
|
|
@@ -1207,6 +2146,44 @@ function createChatSession(payload, options) {
|
|
|
1207
2146
|
}
|
|
1208
2147
|
// Reset stop signal for this turn
|
|
1209
2148
|
stop.reset();
|
|
2149
|
+
// Set up steering queue and pending messages config in locals
|
|
2150
|
+
// so toStreamTextOptions() auto-injects prepareStep for steering
|
|
2151
|
+
const turnSteeringQueue = [];
|
|
2152
|
+
locals_js_1.locals.set(chatSteeringQueueKey, turnSteeringQueue);
|
|
2153
|
+
if (sessionPendingMessages) {
|
|
2154
|
+
locals_js_1.locals.set(chatPendingMessagesKey, sessionPendingMessages);
|
|
2155
|
+
}
|
|
2156
|
+
locals_js_1.locals.set(chatTurnContextKey, {
|
|
2157
|
+
chatId: currentPayload.chatId,
|
|
2158
|
+
turn,
|
|
2159
|
+
continuation: currentPayload.continuation ?? false,
|
|
2160
|
+
clientData: currentPayload.metadata,
|
|
2161
|
+
});
|
|
2162
|
+
// Listen for messages during streaming (steering + next-turn buffer)
|
|
2163
|
+
const sessionPendingWire = [];
|
|
2164
|
+
const sessionMsgSub = messagesInput.on(async (msg) => {
|
|
2165
|
+
sessionPendingWire.push(msg);
|
|
2166
|
+
if (sessionPendingMessages) {
|
|
2167
|
+
const lastUIMessage = msg.messages?.[msg.messages.length - 1];
|
|
2168
|
+
if (lastUIMessage) {
|
|
2169
|
+
if (sessionPendingMessages.onReceived) {
|
|
2170
|
+
try {
|
|
2171
|
+
await sessionPendingMessages.onReceived({
|
|
2172
|
+
message: lastUIMessage,
|
|
2173
|
+
chatId: currentPayload.chatId,
|
|
2174
|
+
turn,
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
catch { /* non-fatal */ }
|
|
2178
|
+
}
|
|
2179
|
+
try {
|
|
2180
|
+
const modelMsgs = await toModelMessages([lastUIMessage]);
|
|
2181
|
+
turnSteeringQueue.push({ uiMessage: lastUIMessage, modelMessages: modelMsgs });
|
|
2182
|
+
}
|
|
2183
|
+
catch { /* non-fatal */ }
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
});
|
|
1210
2187
|
// Accumulate messages
|
|
1211
2188
|
const messages = await accumulator.addIncoming(currentPayload.messages, currentPayload.trigger, turn);
|
|
1212
2189
|
const combinedSignal = AbortSignal.any([runSignal, stop.signal]);
|
|
@@ -1215,11 +2192,16 @@ function createChatSession(payload, options) {
|
|
|
1215
2192
|
chatId: currentPayload.chatId,
|
|
1216
2193
|
trigger: currentPayload.trigger,
|
|
1217
2194
|
clientData: currentPayload.metadata,
|
|
1218
|
-
messages,
|
|
1219
|
-
uiMessages
|
|
2195
|
+
get messages() { return accumulator.modelMessages; },
|
|
2196
|
+
get uiMessages() { return accumulator.uiMessages; },
|
|
1220
2197
|
signal: combinedSignal,
|
|
1221
2198
|
get stopped() { return stop.signal.aborted && !runSignal.aborted; },
|
|
1222
2199
|
continuation: currentPayload.continuation ?? false,
|
|
2200
|
+
previousTurnUsage,
|
|
2201
|
+
totalUsage: cumulativeUsage,
|
|
2202
|
+
async setMessages(uiMessages) {
|
|
2203
|
+
await accumulator.setMessages(uiMessages);
|
|
2204
|
+
},
|
|
1223
2205
|
async complete(source) {
|
|
1224
2206
|
let response;
|
|
1225
2207
|
try {
|
|
@@ -1229,6 +2211,7 @@ function createChatSession(payload, options) {
|
|
|
1229
2211
|
if (error instanceof Error && error.name === "AbortError") {
|
|
1230
2212
|
if (runSignal.aborted) {
|
|
1231
2213
|
// Full cancel — don't accumulate
|
|
2214
|
+
sessionMsgSub.off();
|
|
1232
2215
|
await chatWriteTurnComplete();
|
|
1233
2216
|
return undefined;
|
|
1234
2217
|
}
|
|
@@ -1244,6 +2227,59 @@ function createChatSession(payload, options) {
|
|
|
1244
2227
|
: response;
|
|
1245
2228
|
await accumulator.addResponse(cleaned);
|
|
1246
2229
|
}
|
|
2230
|
+
// Capture token usage from the streamText result
|
|
2231
|
+
let turnUsage;
|
|
2232
|
+
if (typeof source.totalUsage?.then === "function") {
|
|
2233
|
+
try {
|
|
2234
|
+
const usage = await source.totalUsage;
|
|
2235
|
+
turnUsage = usage;
|
|
2236
|
+
previousTurnUsage = usage;
|
|
2237
|
+
cumulativeUsage = addUsage(cumulativeUsage, usage);
|
|
2238
|
+
}
|
|
2239
|
+
catch { /* non-fatal */ }
|
|
2240
|
+
}
|
|
2241
|
+
// Outer-loop compaction (same logic as chat.task)
|
|
2242
|
+
if (sessionCompaction && turnUsage && !turnObj.stopped) {
|
|
2243
|
+
const shouldTrigger = await sessionCompaction.shouldCompact({
|
|
2244
|
+
messages: accumulator.modelMessages,
|
|
2245
|
+
totalTokens: turnUsage.totalTokens,
|
|
2246
|
+
inputTokens: turnUsage.inputTokens,
|
|
2247
|
+
outputTokens: turnUsage.outputTokens,
|
|
2248
|
+
usage: turnUsage,
|
|
2249
|
+
totalUsage: cumulativeUsage,
|
|
2250
|
+
chatId: currentPayload.chatId,
|
|
2251
|
+
turn,
|
|
2252
|
+
clientData: currentPayload.metadata,
|
|
2253
|
+
source: "outer",
|
|
2254
|
+
});
|
|
2255
|
+
if (shouldTrigger) {
|
|
2256
|
+
const summary = await sessionCompaction.summarize({
|
|
2257
|
+
messages: accumulator.modelMessages,
|
|
2258
|
+
usage: turnUsage,
|
|
2259
|
+
totalUsage: cumulativeUsage,
|
|
2260
|
+
chatId: currentPayload.chatId,
|
|
2261
|
+
turn,
|
|
2262
|
+
clientData: currentPayload.metadata,
|
|
2263
|
+
source: "outer",
|
|
2264
|
+
});
|
|
2265
|
+
const compactEvent = {
|
|
2266
|
+
summary,
|
|
2267
|
+
uiMessages: accumulator.uiMessages,
|
|
2268
|
+
modelMessages: accumulator.modelMessages,
|
|
2269
|
+
chatId: currentPayload.chatId,
|
|
2270
|
+
turn,
|
|
2271
|
+
clientData: currentPayload.metadata,
|
|
2272
|
+
source: "outer",
|
|
2273
|
+
};
|
|
2274
|
+
accumulator.modelMessages = sessionCompaction.compactModelMessages
|
|
2275
|
+
? await sessionCompaction.compactModelMessages(compactEvent)
|
|
2276
|
+
: [{ role: "assistant", content: [{ type: "text", text: `[Conversation summary]\n\n${summary}` }] }];
|
|
2277
|
+
if (sessionCompaction.compactUIMessages) {
|
|
2278
|
+
accumulator.uiMessages = await sessionCompaction.compactUIMessages(compactEvent);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
sessionMsgSub.off();
|
|
1247
2283
|
await chatWriteTurnComplete();
|
|
1248
2284
|
return response;
|
|
1249
2285
|
},
|
|
@@ -1251,8 +2287,34 @@ function createChatSession(payload, options) {
|
|
|
1251
2287
|
await accumulator.addResponse(response);
|
|
1252
2288
|
},
|
|
1253
2289
|
async done() {
|
|
2290
|
+
sessionMsgSub.off();
|
|
1254
2291
|
await chatWriteTurnComplete();
|
|
1255
2292
|
},
|
|
2293
|
+
prepareStep() {
|
|
2294
|
+
const hasCompaction = !!sessionCompaction;
|
|
2295
|
+
const hasPending = !!sessionPendingMessages;
|
|
2296
|
+
if (!hasCompaction && !hasPending)
|
|
2297
|
+
return undefined;
|
|
2298
|
+
return async ({ messages: stepMsgs, steps }) => {
|
|
2299
|
+
let resultMessages;
|
|
2300
|
+
if (sessionCompaction) {
|
|
2301
|
+
const compactResult = await chatCompact(stepMsgs, steps, {
|
|
2302
|
+
shouldCompact: sessionCompaction.shouldCompact,
|
|
2303
|
+
summarize: (msgs) => sessionCompaction.summarize({ messages: msgs, source: "inner" }),
|
|
2304
|
+
});
|
|
2305
|
+
if (compactResult.type !== "skipped") {
|
|
2306
|
+
resultMessages = compactResult.messages;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
if (sessionPendingMessages) {
|
|
2310
|
+
const injected = await drainSteeringQueue(sessionPendingMessages, resultMessages ?? stepMsgs, steps, turnSteeringQueue);
|
|
2311
|
+
if (injected.length > 0) {
|
|
2312
|
+
resultMessages = [...(resultMessages ?? stepMsgs), ...injected];
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
return resultMessages ? { messages: resultMessages } : undefined;
|
|
2316
|
+
};
|
|
2317
|
+
},
|
|
1256
2318
|
};
|
|
1257
2319
|
return { done: false, value: turnObj };
|
|
1258
2320
|
},
|
|
@@ -1457,8 +2519,8 @@ exports.chat = {
|
|
|
1457
2519
|
setTurnTimeout,
|
|
1458
2520
|
/** Override the turn timeout at runtime (seconds). See {@link setTurnTimeoutInSeconds}. */
|
|
1459
2521
|
setTurnTimeoutInSeconds,
|
|
1460
|
-
/** Override the
|
|
1461
|
-
|
|
2522
|
+
/** Override the idle timeout at runtime. See {@link setIdleTimeoutInSeconds}. */
|
|
2523
|
+
setIdleTimeoutInSeconds,
|
|
1462
2524
|
/** Override toUIMessageStream() options for the current turn. See {@link setUIMessageStreamOptions}. */
|
|
1463
2525
|
setUIMessageStreamOptions,
|
|
1464
2526
|
/** Check if the current turn was stopped by the user. See {@link isStopped}. */
|
|
@@ -1481,6 +2543,33 @@ exports.chat = {
|
|
|
1481
2543
|
MessageAccumulator: ChatMessageAccumulator,
|
|
1482
2544
|
/** Create a chat session (async iterator). See {@link createChatSession}. */
|
|
1483
2545
|
createSession: createChatSession,
|
|
2546
|
+
/**
|
|
2547
|
+
* Store and retrieve a resolved prompt for the current run.
|
|
2548
|
+
*
|
|
2549
|
+
* - `chat.prompt.set(resolved)` — store a `ResolvedPrompt` or plain string
|
|
2550
|
+
* - `chat.prompt()` — read the stored prompt (throws if not set)
|
|
2551
|
+
*/
|
|
2552
|
+
prompt: Object.assign(getChatPrompt, { set: setChatPrompt }),
|
|
2553
|
+
/**
|
|
2554
|
+
* Returns an options object ready to spread into `streamText()`.
|
|
2555
|
+
* Reads the stored prompt and returns `{ system, experimental_telemetry, ...config }`.
|
|
2556
|
+
* Returns `{}` if no prompt has been set.
|
|
2557
|
+
*/
|
|
2558
|
+
toStreamTextOptions,
|
|
2559
|
+
/**
|
|
2560
|
+
* Replace the accumulated conversation messages for compaction.
|
|
2561
|
+
* Call from `onTurnStart` or `onTurnComplete`. Takes `UIMessage[]` and
|
|
2562
|
+
* converts to `ModelMessage[]` internally.
|
|
2563
|
+
*/
|
|
2564
|
+
setMessages: setChatMessages,
|
|
2565
|
+
/** Check if it's safe to compact messages (no in-flight tool calls). */
|
|
2566
|
+
isCompactionSafe,
|
|
2567
|
+
/** Returns a `prepareStep` function that handles context compaction automatically. */
|
|
2568
|
+
compactionStep: chatCompactionStep,
|
|
2569
|
+
/** Low-level compaction for use inside a custom `prepareStep`. */
|
|
2570
|
+
compact: chatCompact,
|
|
2571
|
+
/** Read the current compaction state (summary + base message count). */
|
|
2572
|
+
getCompactionState,
|
|
1484
2573
|
};
|
|
1485
2574
|
/**
|
|
1486
2575
|
* Writes a turn-complete control chunk to the chat output stream.
|