@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.
Files changed (49) hide show
  1. package/dist/commonjs/v3/ai.d.ts +692 -27
  2. package/dist/commonjs/v3/ai.js +1148 -59
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/chat-react.d.ts +89 -1
  5. package/dist/commonjs/v3/chat-react.js +164 -0
  6. package/dist/commonjs/v3/chat-react.js.map +1 -1
  7. package/dist/commonjs/v3/chat.d.ts +18 -1
  8. package/dist/commonjs/v3/chat.js +40 -2
  9. package/dist/commonjs/v3/chat.js.map +1 -1
  10. package/dist/commonjs/v3/index.d.ts +1 -0
  11. package/dist/commonjs/v3/index.js +2 -1
  12. package/dist/commonjs/v3/index.js.map +1 -1
  13. package/dist/commonjs/v3/prompt.d.ts +38 -0
  14. package/dist/commonjs/v3/prompt.js +150 -0
  15. package/dist/commonjs/v3/prompt.js.map +1 -0
  16. package/dist/commonjs/v3/promptManagement.d.ts +26 -0
  17. package/dist/commonjs/v3/promptManagement.js +180 -0
  18. package/dist/commonjs/v3/promptManagement.js.map +1 -0
  19. package/dist/commonjs/v3/prompts.d.ts +3 -0
  20. package/dist/commonjs/v3/prompts.js +15 -0
  21. package/dist/commonjs/v3/prompts.js.map +1 -0
  22. package/dist/commonjs/v3/streams.js +18 -8
  23. package/dist/commonjs/v3/streams.js.map +1 -1
  24. package/dist/commonjs/version.js +1 -1
  25. package/dist/esm/v3/ai.d.ts +692 -27
  26. package/dist/esm/v3/ai.js +1147 -58
  27. package/dist/esm/v3/ai.js.map +1 -1
  28. package/dist/esm/v3/chat-react.d.ts +89 -1
  29. package/dist/esm/v3/chat-react.js +164 -1
  30. package/dist/esm/v3/chat-react.js.map +1 -1
  31. package/dist/esm/v3/chat.d.ts +18 -1
  32. package/dist/esm/v3/chat.js +40 -2
  33. package/dist/esm/v3/chat.js.map +1 -1
  34. package/dist/esm/v3/index.d.ts +1 -0
  35. package/dist/esm/v3/index.js +1 -0
  36. package/dist/esm/v3/index.js.map +1 -1
  37. package/dist/esm/v3/prompt.d.ts +38 -0
  38. package/dist/esm/v3/prompt.js +147 -0
  39. package/dist/esm/v3/prompt.js.map +1 -0
  40. package/dist/esm/v3/promptManagement.d.ts +26 -0
  41. package/dist/esm/v3/promptManagement.js +170 -0
  42. package/dist/esm/v3/promptManagement.js.map +1 -0
  43. package/dist/esm/v3/prompts.d.ts +3 -0
  44. package/dist/esm/v3/prompts.js +3 -0
  45. package/dist/esm/v3/prompts.js.map +1 -0
  46. package/dist/esm/v3/streams.js +18 -8
  47. package/dist/esm/v3/streams.js.map +1 -1
  48. package/dist/esm/version.js +1 -1
  49. package/package.json +2 -2
@@ -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", warmTimeoutInSeconds = 30, chatAccessTokenTTL = "1h", preloadWarmTimeoutInSeconds, preloadTimeout, uiMessageStreamOptions, ...restOptions } = options;
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 effectivePreloadWarmTimeout = payload.warmTimeoutInSeconds
407
- ?? preloadWarmTimeoutInSeconds
408
- ?? warmTimeoutInSeconds;
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.waitWithWarmup({
413
- warmTimeoutInSeconds: effectivePreloadWarmTimeout,
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 msgSub = messagesInput.on((msg) => {
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 (0, ai_1.convertToModelMessages)(cleanedUIMessages);
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
- const result = await userRun({
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(result)) {
1215
+ if ((locals_js_1.locals.get(chatPipeCountKey) ?? 0) === 0 && isUIMessageStreamable(runResult)) {
614
1216
  onFinishAttached = true;
615
- const uiStream = result.toUIMessageStream({
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 (0, ai_1.convertToModelMessages)([
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 onTurnComplete so hooks can rely on the work being done.
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
- // Fire onTurnComplete after response capture
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
- chatId: currentWirePayload.chatId,
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, use the first one immediately
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 warm briefly, then suspend
738
- const effectiveWarmTimeout = metadata_js_1.metadata.get(WARM_TIMEOUT_METADATA_KEY) ?? warmTimeoutInSeconds;
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.waitWithWarmup({
741
- warmTimeoutInSeconds: effectiveWarmTimeout,
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 WARM_TIMEOUT_METADATA_KEY = "chat.warmTimeout";
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 warm timeout for subsequent turns in the current run.
1642
+ * Override the idle timeout for subsequent turns in the current run.
831
1643
  *
832
- * The warm timeout controls how long the run stays active (using compute)
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 warm (0 to suspend immediately)
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.setWarmTimeoutInSeconds(60);
1653
+ * chat.setIdleTimeoutInSeconds(60);
842
1654
  * return streamText({ model, messages, abortSignal: signal });
843
1655
  * }
844
1656
  * ```
845
1657
  */
846
- function setWarmTimeoutInSeconds(seconds) {
847
- metadata_js_1.metadata.set(WARM_TIMEOUT_METADATA_KEY, seconds);
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 (0, ai_1.convertToModelMessages)(cleaned);
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 (0, ai_1.convertToModelMessages)([stripProviderMetadata(response)]);
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 warm/suspend between turns. You control: initialization,
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, warmTimeoutInSeconds = 30, timeout = "1h", maxTurns = 100, } = options;
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.waitWithWarmup({
1180
- warmTimeoutInSeconds: currentPayload.warmTimeoutInSeconds ?? warmTimeoutInSeconds,
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.waitWithWarmup({
1193
- warmTimeoutInSeconds,
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: accumulator.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 warm timeout at runtime. See {@link setWarmTimeoutInSeconds}. */
1461
- setWarmTimeoutInSeconds,
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.