@trigger.dev/sdk 0.0.0-prerelease-20260310150337 → 0.0.0-prerelease-20260324161542

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 (41) hide show
  1. package/dist/commonjs/v3/ai.d.ts +571 -27
  2. package/dist/commonjs/v3/ai.js +865 -56
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/chat.d.ts +1 -1
  5. package/dist/commonjs/v3/chat.js +2 -2
  6. package/dist/commonjs/v3/index.d.ts +1 -0
  7. package/dist/commonjs/v3/index.js +2 -1
  8. package/dist/commonjs/v3/index.js.map +1 -1
  9. package/dist/commonjs/v3/prompt.d.ts +38 -0
  10. package/dist/commonjs/v3/prompt.js +150 -0
  11. package/dist/commonjs/v3/prompt.js.map +1 -0
  12. package/dist/commonjs/v3/promptManagement.d.ts +26 -0
  13. package/dist/commonjs/v3/promptManagement.js +180 -0
  14. package/dist/commonjs/v3/promptManagement.js.map +1 -0
  15. package/dist/commonjs/v3/prompts.d.ts +3 -0
  16. package/dist/commonjs/v3/prompts.js +15 -0
  17. package/dist/commonjs/v3/prompts.js.map +1 -0
  18. package/dist/commonjs/v3/streams.js +18 -8
  19. package/dist/commonjs/v3/streams.js.map +1 -1
  20. package/dist/commonjs/version.js +1 -1
  21. package/dist/esm/v3/ai.d.ts +571 -27
  22. package/dist/esm/v3/ai.js +865 -56
  23. package/dist/esm/v3/ai.js.map +1 -1
  24. package/dist/esm/v3/chat.d.ts +1 -1
  25. package/dist/esm/v3/chat.js +2 -2
  26. package/dist/esm/v3/index.d.ts +1 -0
  27. package/dist/esm/v3/index.js +1 -0
  28. package/dist/esm/v3/index.js.map +1 -1
  29. package/dist/esm/v3/prompt.d.ts +38 -0
  30. package/dist/esm/v3/prompt.js +147 -0
  31. package/dist/esm/v3/prompt.js.map +1 -0
  32. package/dist/esm/v3/promptManagement.d.ts +26 -0
  33. package/dist/esm/v3/promptManagement.js +170 -0
  34. package/dist/esm/v3/promptManagement.js.map +1 -0
  35. package/dist/esm/v3/prompts.d.ts +3 -0
  36. package/dist/esm/v3/prompts.js +3 -0
  37. package/dist/esm/v3/prompts.js.map +1 -0
  38. package/dist/esm/v3/streams.js +18 -8
  39. package/dist/esm/v3/streams.js.map +1 -1
  40. package/dist/esm/version.js +1 -1
  41. package/package.json +2 -2
@@ -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,407 @@ 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
+ * Check that no tool calls are in-flight in a step's content.
286
+ * Used before compaction to avoid losing tool state mid-execution.
287
+ * @internal
288
+ */
289
+ function isStepBoundarySafe(step) {
290
+ if (step.finishReason === "error")
291
+ return false;
292
+ const callIds = new Set(step.content.filter((p) => p.type === "tool-call").map((p) => p.toolCallId));
293
+ const settledIds = new Set(step.content
294
+ .filter((p) => p.type === "tool-result" || p.type === "tool-error")
295
+ .map((p) => p.toolCallId));
296
+ return ![...callIds].some((id) => !settledIds.has(id));
297
+ }
298
+ /**
299
+ * Apply the prepareMessages hook if one is set in locals.
300
+ * @internal
301
+ */
302
+ async function applyPrepareMessages(messages, reason) {
303
+ const hook = locals_js_1.locals.get(chatPrepareMessagesKey);
304
+ if (!hook)
305
+ return messages;
306
+ const turnCtx = locals_js_1.locals.get(chatTurnContextKey);
307
+ return tracer_js_1.tracer.startActiveSpan("prepareMessages()", async () => {
308
+ return hook({
309
+ messages,
310
+ reason,
311
+ chatId: turnCtx?.chatId ?? "",
312
+ turn: turnCtx?.turn ?? 0,
313
+ clientData: turnCtx?.clientData,
314
+ });
315
+ }, {
316
+ attributes: {
317
+ [v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart",
318
+ [v3_1.SemanticInternalAttributes.COLLAPSED]: true,
319
+ "chat.prepareMessages.reason": reason,
320
+ "chat.prepareMessages.messageCount": messages.length,
321
+ },
322
+ });
323
+ }
324
+ /**
325
+ * Read the current compaction state. Returns the summary and base message count
326
+ * if compaction has occurred in this turn, or `undefined` if not.
327
+ *
328
+ * Use in a custom `prepareStep` to rebuild from a previous compaction:
329
+ * ```ts
330
+ * const state = chat.getCompactionState();
331
+ * if (state) {
332
+ * return { messages: [{ role: "user", content: state.summary }, ...newMsgs] };
333
+ * }
334
+ * ```
335
+ */
336
+ function getCompactionState() {
337
+ return locals_js_1.locals.get(chatCompactionStateKey);
338
+ }
339
+ /**
340
+ * Low-level compaction for use inside a custom `prepareStep`.
341
+ *
342
+ * Handles the full decision tree: first step, already-compacted rebuild,
343
+ * boundary safety, threshold check, summarization, stream chunks, state
344
+ * storage, and accumulator update.
345
+ *
346
+ * Returns a `CompactResult` — inspect `result.type` to see what happened,
347
+ * or convert to a prepareStep return with `result.type === "skipped" ? undefined : result`.
348
+ *
349
+ * @example
350
+ * ```ts
351
+ * prepareStep: async ({ messages, steps }) => {
352
+ * // your custom logic here...
353
+ * const result = await chat.compact(messages, steps, {
354
+ * threshold: 80_000,
355
+ * summarize: async (msgs) => generateText({ model, messages: msgs }).then(r => r.text),
356
+ * });
357
+ * if (result.type === "compacted") {
358
+ * logger.info("Compacted!", { summary: result.summary });
359
+ * }
360
+ * return result.type === "skipped" ? undefined : result;
361
+ * },
362
+ * ```
363
+ */
364
+ async function chatCompact(messages, steps, options) {
365
+ const currentStep = steps.at(-1);
366
+ // First step — nothing to check
367
+ if (!currentStep) {
368
+ return { type: "skipped" };
369
+ }
370
+ // Already compacted — rebuild from summary + new response messages
371
+ const state = locals_js_1.locals.get(chatCompactionStateKey);
372
+ if (state && isStepBoundarySafe(currentStep)) {
373
+ return {
374
+ type: "rebuilt",
375
+ messages: await applyPrepareMessages([
376
+ { role: "user", content: state.summary },
377
+ ...currentStep.response.messages.slice(state.baseResponseMessageCount),
378
+ ], "compaction-rebuild"),
379
+ };
380
+ }
381
+ // Boundary unsafe — skip
382
+ if (!isStepBoundarySafe(currentStep)) {
383
+ return { type: "skipped" };
384
+ }
385
+ const totalTokens = currentStep.usage.totalTokens;
386
+ const inputTokens = currentStep.usage.inputTokens;
387
+ const outputTokens = currentStep.usage.outputTokens;
388
+ const turnCtx = locals_js_1.locals.get(chatTurnContextKey);
389
+ const stepNumber = steps.length - 1;
390
+ const shouldTrigger = options.shouldCompact
391
+ ? await options.shouldCompact({
392
+ messages,
393
+ totalTokens,
394
+ inputTokens,
395
+ outputTokens,
396
+ usage: currentStep.usage,
397
+ source: "inner",
398
+ stepNumber,
399
+ steps,
400
+ chatId: turnCtx?.chatId,
401
+ turn: turnCtx?.turn,
402
+ clientData: turnCtx?.clientData,
403
+ })
404
+ : (totalTokens != null && options.threshold != null && totalTokens > options.threshold);
405
+ if (!shouldTrigger) {
406
+ return { type: "skipped" };
407
+ }
408
+ const result = await tracer_js_1.tracer.startActiveSpan("context compaction", async (span) => {
409
+ const compactionId = (0, ai_1.generateId)();
410
+ let summary;
411
+ const { waitUntilComplete } = streams_js_1.streams.writer(exports.CHAT_STREAM_KEY, {
412
+ spanName: "stream compaction chunks",
413
+ collapsed: true,
414
+ execute: async ({ write }) => {
415
+ write({ type: "step-start" });
416
+ write({
417
+ type: "data-compaction",
418
+ id: compactionId,
419
+ data: { status: "compacting", totalTokens },
420
+ });
421
+ // Generate summary
422
+ summary = await options.summarize(messages);
423
+ // Store state in locals for subsequent steps
424
+ locals_js_1.locals.set(chatCompactionStateKey, {
425
+ summary,
426
+ baseResponseMessageCount: currentStep.response.messages.length,
427
+ });
428
+ // Set model-only override — UI messages stay intact for persistence.
429
+ // The summary becomes the model message history for the next turn,
430
+ // while accumulatedUIMessages keeps the full conversation for display.
431
+ locals_js_1.locals.set(chatOverrideModelMessagesKey, [
432
+ { role: "assistant", content: [{ type: "text", text: `[Conversation summary]\n\n${summary}` }] },
433
+ ]);
434
+ // Fire onCompacted hook
435
+ const onCompactedHook = locals_js_1.locals.get(chatOnCompactedKey);
436
+ if (onCompactedHook) {
437
+ await onCompactedHook({
438
+ summary,
439
+ messages,
440
+ messageCount: messages.length,
441
+ usage: currentStep.usage,
442
+ totalTokens,
443
+ inputTokens,
444
+ outputTokens,
445
+ stepNumber,
446
+ chatId: turnCtx?.chatId,
447
+ turn: turnCtx?.turn,
448
+ });
449
+ }
450
+ write({
451
+ type: "data-compaction",
452
+ id: compactionId,
453
+ data: { status: "complete", totalTokens },
454
+ });
455
+ write({ type: "finish-step" });
456
+ },
457
+ });
458
+ await waitUntilComplete();
459
+ // Set attributes after we have the summary
460
+ span.setAttribute("compaction.summary_length", summary.length);
461
+ return {
462
+ type: "compacted",
463
+ messages: await applyPrepareMessages([{ role: "user", content: summary }], "compaction-result"),
464
+ summary,
465
+ };
466
+ }, {
467
+ attributes: {
468
+ [v3_1.SemanticInternalAttributes.STYLE_ICON]: "tabler-scissors",
469
+ "compaction.threshold": options.threshold,
470
+ "compaction.total_tokens": totalTokens ?? 0,
471
+ "compaction.input_tokens": inputTokens ?? 0,
472
+ "compaction.message_count": messages.length,
473
+ "compaction.step_number": stepNumber,
474
+ ...(turnCtx?.chatId ? { "compaction.chat_id": turnCtx.chatId } : {}),
475
+ ...(turnCtx?.turn != null ? { "compaction.turn": turnCtx.turn } : {}),
476
+ ...(0, v3_1.accessoryAttributes)({
477
+ items: [
478
+ { text: `${totalTokens ?? 0} tokens`, variant: "normal" },
479
+ { text: `${messages.length} msgs`, variant: "normal" },
480
+ ],
481
+ style: "codepath",
482
+ }),
483
+ },
484
+ });
485
+ return result;
486
+ }
487
+ /**
488
+ * Returns a `prepareStep` function that handles context compaction automatically.
489
+ *
490
+ * Monitors token usage between tool-call steps. When `totalTokens` exceeds
491
+ * the threshold, generates a summary via `summarize()`, replaces the message
492
+ * history, and emits `data-compaction` stream chunks for the frontend.
493
+ *
494
+ * @example
495
+ * ```ts
496
+ * return streamText({
497
+ * ...chat.toStreamTextOptions({ registry }),
498
+ * messages: chat.addCacheBreaks(messages),
499
+ * prepareStep: chat.compactionStep({
500
+ * threshold: 80_000,
501
+ * summarize: async (messages) => {
502
+ * return generateText({ model, messages: [...messages, { role: "user", content: "Summarize." }] })
503
+ * .then((r) => r.text);
504
+ * },
505
+ * }),
506
+ * tools: { ... },
507
+ * });
508
+ * ```
509
+ */
510
+ function chatCompactionStep(options) {
511
+ return async ({ messages, steps }) => {
512
+ const result = await chatCompact(messages, steps, options);
513
+ return result.type === "skipped" ? undefined : result;
514
+ };
515
+ }
516
+ // ---------------------------------------------------------------------------
517
+ // chat.isCompactionSafe — check if it's safe to compact messages
518
+ // ---------------------------------------------------------------------------
519
+ /**
520
+ * Checks whether it's safe to compact the message history. Returns `false`
521
+ * if any tool calls are in-flight (incomplete tool invocations without results).
522
+ *
523
+ * Call before `chat.setMessages()` to avoid corrupting tool-call state.
524
+ */
525
+ function isCompactionSafe(messages) {
526
+ for (const msg of messages) {
527
+ if (msg.role !== "assistant")
528
+ continue;
529
+ for (const part of msg.parts) {
530
+ if (part.type === "tool-invocation") {
531
+ const state = part.toolInvocation?.state ?? part.state;
532
+ if (state !== "result" && state !== "error") {
533
+ return false;
534
+ }
535
+ }
536
+ }
537
+ }
538
+ return true;
539
+ }
540
+ /** @internal */
541
+ const chatPromptKey = locals_js_1.locals.create("chat.prompt");
542
+ /**
543
+ * Store a resolved prompt (or plain string) for the current run.
544
+ * Call from any hook (`onPreload`, `onChatStart`, `onTurnStart`) or `run()`.
545
+ */
546
+ function setChatPrompt(resolved) {
547
+ if (typeof resolved === "string") {
548
+ locals_js_1.locals.set(chatPromptKey, {
549
+ text: resolved,
550
+ model: undefined,
551
+ config: undefined,
552
+ promptId: "",
553
+ version: 0,
554
+ labels: [],
555
+ toAISDKTelemetry: () => ({
556
+ experimental_telemetry: { isEnabled: true, metadata: {} },
557
+ }),
558
+ });
559
+ }
560
+ else {
561
+ locals_js_1.locals.set(chatPromptKey, resolved);
562
+ }
563
+ }
564
+ /**
565
+ * Read the stored prompt. Throws if `chat.prompt.set()` has not been called.
566
+ */
567
+ function getChatPrompt() {
568
+ const prompt = locals_js_1.locals.get(chatPromptKey);
569
+ if (!prompt) {
570
+ throw new Error("chat.prompt() called before chat.prompt.set(). Set a prompt in onPreload, onChatStart, onTurnStart, or run() first.");
571
+ }
572
+ return prompt;
573
+ }
574
+ /**
575
+ * Returns an options object ready to spread into `streamText()`.
576
+ *
577
+ * Includes `system`, `experimental_telemetry`, and any config fields
578
+ * (temperature, maxTokens, etc.) from the stored prompt.
579
+ *
580
+ * When a `registry` is provided and the prompt has a `model` string,
581
+ * the resolved `LanguageModel` is included as `model`.
582
+ *
583
+ * If no prompt has been set, returns `{}` (no-op spread).
584
+ */
585
+ function toStreamTextOptions(options) {
586
+ const prompt = locals_js_1.locals.get(chatPromptKey);
587
+ if (!prompt)
588
+ return {};
589
+ const result = {
590
+ system: prompt.text,
591
+ };
592
+ // Resolve model via registry if both are present
593
+ if (options?.registry && prompt.model) {
594
+ result.model = options.registry.languageModel(prompt.model);
595
+ }
596
+ // Spread config (temperature, maxTokens, etc.)
597
+ if (prompt.config) {
598
+ Object.assign(result, prompt.config);
599
+ }
600
+ // Add telemetry (forward additional metadata from caller)
601
+ const telemetry = prompt.toAISDKTelemetry(options?.telemetry);
602
+ Object.assign(result, telemetry);
603
+ // Auto-inject prepareStep when task-level compaction is configured.
604
+ // We build a custom prepareStep instead of using chatCompactionStep so we
605
+ // can pass the enriched SummarizeEvent to taskCompaction.summarize.
606
+ const taskCompaction = locals_js_1.locals.get(chatTaskCompactionKey);
607
+ if (taskCompaction) {
608
+ result.prepareStep = async ({ messages, steps }) => {
609
+ const compactResult = await chatCompact(messages, steps, {
610
+ shouldCompact: taskCompaction.shouldCompact,
611
+ summarize: (msgs) => {
612
+ const ctx = locals_js_1.locals.get(chatTurnContextKey);
613
+ const lastStep = steps.at(-1);
614
+ return taskCompaction.summarize({
615
+ messages: msgs,
616
+ usage: lastStep?.usage,
617
+ source: "inner",
618
+ stepNumber: steps.length - 1,
619
+ chatId: ctx?.chatId,
620
+ turn: ctx?.turn,
621
+ clientData: ctx?.clientData,
622
+ });
623
+ },
624
+ });
625
+ return compactResult.type === "skipped" ? undefined : compactResult;
626
+ };
627
+ }
628
+ return result;
629
+ }
221
630
  function isUIMessageStreamable(value) {
222
631
  return (typeof value === "object" &&
223
632
  value !== null &&
@@ -324,7 +733,7 @@ async function pipeChat(source, options) {
324
733
  * ```
325
734
  */
326
735
  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;
736
+ const { run: userRun, clientDataSchema, onPreload, onChatStart, onTurnStart, onBeforeTurnComplete, onCompacted, compaction, prepareMessages, onTurnComplete, maxTurns = 100, turnTimeout = "1h", idleTimeoutInSeconds = 30, chatAccessTokenTTL = "1h", preloadIdleTimeoutInSeconds, preloadTimeout, uiMessageStreamOptions, ...restOptions } = options;
328
737
  const parseClientData = clientDataSchema
329
738
  ? (0, v3_1.getSchemaParseFn)(clientDataSchema)
330
739
  : undefined;
@@ -340,6 +749,16 @@ function chatTask(options) {
340
749
  if (uiMessageStreamOptions) {
341
750
  locals_js_1.locals.set(chatUIStreamStaticKey, uiMessageStreamOptions);
342
751
  }
752
+ // Store onCompacted hook in locals so chat.compact() can call it
753
+ if (onCompacted) {
754
+ locals_js_1.locals.set(chatOnCompactedKey, onCompacted);
755
+ }
756
+ if (prepareMessages) {
757
+ locals_js_1.locals.set(chatPrepareMessagesKey, prepareMessages);
758
+ }
759
+ if (compaction) {
760
+ locals_js_1.locals.set(chatTaskCompactionKey, compaction);
761
+ }
343
762
  let currentWirePayload = payload;
344
763
  const continuation = payload.continuation ?? false;
345
764
  const previousRunId = payload.previousRunId;
@@ -351,6 +770,9 @@ function chatTask(options) {
351
770
  // Accumulated UI messages for persistence. Mirrors the model accumulator
352
771
  // but in frontend-friendly UIMessage format (with parts, id, etc.).
353
772
  let accumulatedUIMessages = [];
773
+ // Token usage tracking across turns
774
+ let previousTurnUsage;
775
+ let cumulativeUsage = emptyUsage();
354
776
  // Mutable reference to the current turn's stop controller so the
355
777
  // stop input stream listener (registered once) can abort the right turn.
356
778
  let currentStopController;
@@ -403,14 +825,14 @@ function chatTask(options) {
403
825
  });
404
826
  }
405
827
  // Wait for the first real message — use preload-specific timeouts if configured
406
- const effectivePreloadWarmTimeout = payload.warmTimeoutInSeconds
407
- ?? preloadWarmTimeoutInSeconds
408
- ?? warmTimeoutInSeconds;
828
+ const effectivePreloadIdleTimeout = payload.idleTimeoutInSeconds
829
+ ?? preloadIdleTimeoutInSeconds
830
+ ?? idleTimeoutInSeconds;
409
831
  const effectivePreloadTimeout = metadata_js_1.metadata.get(TURN_TIMEOUT_METADATA_KEY)
410
832
  ?? preloadTimeout
411
833
  ?? turnTimeout;
412
- const preloadResult = await messagesInput.waitWithWarmup({
413
- warmTimeoutInSeconds: effectivePreloadWarmTimeout,
834
+ const preloadResult = await messagesInput.waitWithIdleTimeout({
835
+ idleTimeoutInSeconds: effectivePreloadIdleTimeout,
414
836
  timeout: effectivePreloadTimeout,
415
837
  spanName: "waiting for first message",
416
838
  });
@@ -450,9 +872,10 @@ function chatTask(options) {
450
872
  turnAttributes["chat.client_data"] =
451
873
  typeof wireMetadata === "string" ? wireMetadata : JSON.stringify(wireMetadata);
452
874
  }
453
- const turnResult = await tracer_js_1.tracer.startActiveSpan(`chat turn ${turn + 1}`, async () => {
875
+ const turnResult = await tracer_js_1.tracer.startActiveSpan(`chat turn ${turn + 1}`, async (turnSpan) => {
454
876
  locals_js_1.locals.set(chatPipeCountKey, 0);
455
877
  locals_js_1.locals.set(chatDeferKey, new Set());
878
+ locals_js_1.locals.set(chatCompactionStateKey, undefined);
456
879
  // Store chat context for auto-detection by ai.tool subtasks
457
880
  locals_js_1.locals.set(chatTurnContextKey, {
458
881
  chatId: currentWirePayload.chatId,
@@ -482,7 +905,7 @@ function chatTask(options) {
482
905
  // Convert the incoming UIMessages to model messages and update the accumulator.
483
906
  // Turn 1: full history from the frontend → replaces the accumulator.
484
907
  // Turn 2+: only the new message(s) → appended to the accumulator.
485
- const incomingModelMessages = await (0, ai_1.convertToModelMessages)(cleanedUIMessages);
908
+ const incomingModelMessages = await toModelMessages(cleanedUIMessages);
486
909
  // Track new messages for this turn (user input + assistant response).
487
910
  const turnNewModelMessages = [];
488
911
  const turnNewUIMessages = [];
@@ -570,7 +993,16 @@ function chatTask(options) {
570
993
  continuation,
571
994
  previousRunId,
572
995
  preloaded,
996
+ previousTurnUsage,
997
+ totalUsage: cumulativeUsage,
573
998
  });
999
+ // Check if onTurnStart replaced messages (compaction)
1000
+ const turnStartOverride = locals_js_1.locals.get(chatOverrideMessagesKey);
1001
+ if (turnStartOverride) {
1002
+ locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
1003
+ accumulatedUIMessages = [...turnStartOverride];
1004
+ accumulatedMessages = await toModelMessages(turnStartOverride);
1005
+ }
574
1006
  }, {
575
1007
  attributes: {
576
1008
  [v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart",
@@ -595,14 +1027,17 @@ function chatTask(options) {
595
1027
  let resolveOnFinish;
596
1028
  const onFinishPromise = new Promise((r) => { resolveOnFinish = r; });
597
1029
  let onFinishAttached = false;
1030
+ let runResult;
598
1031
  try {
599
- const result = await userRun({
1032
+ runResult = await userRun({
600
1033
  ...restWire,
601
- messages: accumulatedMessages,
1034
+ messages: await applyPrepareMessages(accumulatedMessages, "run"),
602
1035
  clientData,
603
1036
  continuation,
604
1037
  previousRunId,
605
1038
  preloaded,
1039
+ previousTurnUsage,
1040
+ totalUsage: cumulativeUsage,
606
1041
  signal: combinedSignal,
607
1042
  cancelSignal,
608
1043
  stopSignal,
@@ -610,9 +1045,9 @@ function chatTask(options) {
610
1045
  // Auto-pipe if the run function returned a StreamTextResult or similar,
611
1046
  // but only if pipeChat() wasn't already called manually during this turn.
612
1047
  // We call toUIMessageStream ourselves to attach onFinish for response capture.
613
- if ((locals_js_1.locals.get(chatPipeCountKey) ?? 0) === 0 && isUIMessageStreamable(result)) {
1048
+ if ((locals_js_1.locals.get(chatPipeCountKey) ?? 0) === 0 && isUIMessageStreamable(runResult)) {
614
1049
  onFinishAttached = true;
615
- const uiStream = result.toUIMessageStream({
1050
+ const uiStream = runResult.toUIMessageStream({
616
1051
  ...resolveUIMessageStreamOptions(),
617
1052
  onFinish: ({ responseMessage }) => {
618
1053
  capturedResponseMessage = responseMessage;
@@ -642,6 +1077,72 @@ function chatTask(options) {
642
1077
  if (onFinishAttached) {
643
1078
  await onFinishPromise;
644
1079
  }
1080
+ // Capture token usage from the streamText result (if available).
1081
+ // totalUsage is a PromiseLike that resolves after the stream is consumed.
1082
+ let turnUsage;
1083
+ if (runResult != null && typeof runResult.totalUsage?.then === "function") {
1084
+ try {
1085
+ turnUsage = await runResult.totalUsage;
1086
+ }
1087
+ catch { /* non-fatal — usage capture failed */ }
1088
+ }
1089
+ if (turnUsage) {
1090
+ cumulativeUsage = addUsage(cumulativeUsage, turnUsage);
1091
+ previousTurnUsage = turnUsage;
1092
+ // Add usage attributes to the turn span
1093
+ if (turnUsage.inputTokens != null) {
1094
+ turnSpan.setAttribute("gen_ai.usage.input_tokens", turnUsage.inputTokens);
1095
+ }
1096
+ if (turnUsage.outputTokens != null) {
1097
+ turnSpan.setAttribute("gen_ai.usage.output_tokens", turnUsage.outputTokens);
1098
+ }
1099
+ if (turnUsage.totalTokens != null) {
1100
+ turnSpan.setAttribute("gen_ai.usage.total_tokens", turnUsage.totalTokens);
1101
+ }
1102
+ if (cumulativeUsage.totalTokens != null) {
1103
+ turnSpan.setAttribute("gen_ai.usage.cumulative_total_tokens", cumulativeUsage.totalTokens);
1104
+ }
1105
+ if (cumulativeUsage.inputTokens != null) {
1106
+ turnSpan.setAttribute("gen_ai.usage.cumulative_input_tokens", cumulativeUsage.inputTokens);
1107
+ }
1108
+ if (cumulativeUsage.outputTokens != null) {
1109
+ turnSpan.setAttribute("gen_ai.usage.cumulative_output_tokens", cumulativeUsage.outputTokens);
1110
+ }
1111
+ }
1112
+ // Check if run() (e.g. via prepareStep) replaced messages during this turn.
1113
+ // This supports intra-turn compaction — the compacted messages become the
1114
+ // new base, and the response gets appended on top.
1115
+ const runOverride = locals_js_1.locals.get(chatOverrideMessagesKey);
1116
+ if (runOverride) {
1117
+ locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
1118
+ accumulatedUIMessages = [...runOverride];
1119
+ accumulatedMessages = await toModelMessages(runOverride);
1120
+ }
1121
+ // Check if compaction set a model-only override (preserves UI messages).
1122
+ // Apply compactUIMessages/compactModelMessages callbacks if configured.
1123
+ const modelOnlyOverride = locals_js_1.locals.get(chatOverrideModelMessagesKey);
1124
+ if (modelOnlyOverride) {
1125
+ const compactionSummary = locals_js_1.locals.get(chatCompactionStateKey)?.summary ?? "";
1126
+ const taskCompactionConfig = locals_js_1.locals.get(chatTaskCompactionKey);
1127
+ locals_js_1.locals.set(chatOverrideModelMessagesKey, undefined);
1128
+ const compactEvent = {
1129
+ summary: compactionSummary,
1130
+ uiMessages: accumulatedUIMessages,
1131
+ modelMessages: accumulatedMessages,
1132
+ chatId: currentWirePayload.chatId,
1133
+ turn,
1134
+ clientData,
1135
+ source: "inner",
1136
+ };
1137
+ // Apply model messages: callback or default (use override)
1138
+ accumulatedMessages = taskCompactionConfig?.compactModelMessages
1139
+ ? await taskCompactionConfig.compactModelMessages(compactEvent)
1140
+ : modelOnlyOverride;
1141
+ // Apply UI messages: callback or default (preserve all)
1142
+ if (taskCompactionConfig?.compactUIMessages) {
1143
+ accumulatedUIMessages = await taskCompactionConfig.compactUIMessages(compactEvent);
1144
+ }
1145
+ }
645
1146
  // Determine if the user stopped generation this turn (not a full run cancel).
646
1147
  const wasStopped = stopController.signal.aborted && !runSignal.aborted;
647
1148
  // Append the assistant's response (partial or complete) to the accumulator.
@@ -664,7 +1165,7 @@ function chatTask(options) {
664
1165
  accumulatedUIMessages.push(capturedResponseMessage);
665
1166
  turnNewUIMessages.push(capturedResponseMessage);
666
1167
  try {
667
- const responseModelMessages = await (0, ai_1.convertToModelMessages)([
1168
+ const responseModelMessages = await toModelMessages([
668
1169
  stripProviderMetadata(capturedResponseMessage),
669
1170
  ]);
670
1171
  accumulatedMessages.push(...responseModelMessages);
@@ -680,11 +1181,8 @@ function chatTask(options) {
680
1181
  // accumulation (e.g. via a `chat.addMessages()` helper).
681
1182
  if (runSignal.aborted)
682
1183
  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
1184
  // Await deferred background work (e.g. DB writes from onTurnStart)
687
- // before firing onTurnComplete so hooks can rely on the work being done.
1185
+ // before firing hooks so they can rely on the work being done.
688
1186
  const deferredWork = locals_js_1.locals.get(chatDeferKey);
689
1187
  if (deferredWork && deferredWork.size > 0) {
690
1188
  await Promise.race([
@@ -692,27 +1190,169 @@ function chatTask(options) {
692
1190
  new Promise((r) => setTimeout(r, 5_000)),
693
1191
  ]);
694
1192
  }
695
- // Fire onTurnComplete after response capture
1193
+ // Outer-loop compaction: runs between turns for single-step responses
1194
+ // where prepareStep never fires (no tool calls = no step boundaries).
1195
+ // Only triggers when: task has compaction configured, prepareStep didn't
1196
+ // already compact this turn, and shouldCompact returns true.
1197
+ const outerCompaction = locals_js_1.locals.get(chatTaskCompactionKey);
1198
+ const innerCompactionState = locals_js_1.locals.get(chatCompactionStateKey);
1199
+ if (outerCompaction && !innerCompactionState && turnUsage && !wasStopped) {
1200
+ const shouldTrigger = await outerCompaction.shouldCompact({
1201
+ messages: accumulatedMessages,
1202
+ totalTokens: turnUsage.totalTokens,
1203
+ inputTokens: turnUsage.inputTokens,
1204
+ outputTokens: turnUsage.outputTokens,
1205
+ usage: turnUsage,
1206
+ totalUsage: cumulativeUsage,
1207
+ chatId: currentWirePayload.chatId,
1208
+ turn,
1209
+ clientData,
1210
+ source: "outer",
1211
+ });
1212
+ if (shouldTrigger) {
1213
+ await tracer_js_1.tracer.startActiveSpan("context compaction (outer loop)", async (compactionSpan) => {
1214
+ const compactionId = (0, ai_1.generateId)();
1215
+ const { waitUntilComplete } = streams_js_1.streams.writer(exports.CHAT_STREAM_KEY, {
1216
+ spanName: "stream compaction chunks",
1217
+ collapsed: true,
1218
+ execute: async ({ write }) => {
1219
+ write({
1220
+ type: "data-compaction",
1221
+ id: compactionId,
1222
+ data: { status: "compacting", totalTokens: turnUsage.totalTokens },
1223
+ });
1224
+ const summary = await outerCompaction.summarize({
1225
+ messages: accumulatedMessages,
1226
+ usage: turnUsage,
1227
+ totalUsage: cumulativeUsage,
1228
+ chatId: currentWirePayload.chatId,
1229
+ turn,
1230
+ clientData,
1231
+ source: "outer",
1232
+ });
1233
+ // Apply compactModelMessages/compactUIMessages callbacks, or defaults.
1234
+ const outerCompactEvent = {
1235
+ summary,
1236
+ uiMessages: accumulatedUIMessages,
1237
+ modelMessages: accumulatedMessages,
1238
+ chatId: currentWirePayload.chatId,
1239
+ turn,
1240
+ clientData,
1241
+ source: "outer",
1242
+ };
1243
+ // Model messages: callback or default (replace with summary)
1244
+ accumulatedMessages = outerCompaction.compactModelMessages
1245
+ ? await outerCompaction.compactModelMessages(outerCompactEvent)
1246
+ : [{ role: "assistant", content: [{ type: "text", text: `[Conversation summary]\n\n${summary}` }] }];
1247
+ // UI messages: callback or default (preserve all)
1248
+ if (outerCompaction.compactUIMessages) {
1249
+ accumulatedUIMessages = await outerCompaction.compactUIMessages(outerCompactEvent);
1250
+ }
1251
+ // Fire onCompacted hook
1252
+ const onCompactedHook = locals_js_1.locals.get(chatOnCompactedKey);
1253
+ if (onCompactedHook) {
1254
+ await onCompactedHook({
1255
+ summary,
1256
+ messages: accumulatedMessages,
1257
+ messageCount: accumulatedMessages.length,
1258
+ usage: turnUsage,
1259
+ totalTokens: turnUsage.totalTokens,
1260
+ inputTokens: turnUsage.inputTokens,
1261
+ outputTokens: turnUsage.outputTokens,
1262
+ stepNumber: -1, // outer loop, not a step
1263
+ chatId: currentWirePayload.chatId,
1264
+ turn,
1265
+ });
1266
+ }
1267
+ compactionSpan.setAttribute("compaction.summary_length", summary.length);
1268
+ write({
1269
+ type: "data-compaction",
1270
+ id: compactionId,
1271
+ data: { status: "complete", totalTokens: turnUsage.totalTokens },
1272
+ });
1273
+ },
1274
+ });
1275
+ await waitUntilComplete();
1276
+ }, {
1277
+ attributes: {
1278
+ [v3_1.SemanticInternalAttributes.STYLE_ICON]: "tabler-scissors",
1279
+ "compaction.total_tokens": turnUsage.totalTokens ?? 0,
1280
+ "compaction.input_tokens": turnUsage.inputTokens ?? 0,
1281
+ "compaction.message_count": accumulatedMessages.length,
1282
+ "compaction.outer_loop": true,
1283
+ "compaction.turn": turn,
1284
+ ...(currentWirePayload.chatId ? { "compaction.chat_id": currentWirePayload.chatId } : {}),
1285
+ ...(0, v3_1.accessoryAttributes)({
1286
+ items: [
1287
+ { text: `${turnUsage.totalTokens ?? 0} tokens`, variant: "normal" },
1288
+ { text: `${accumulatedMessages.length} msgs`, variant: "normal" },
1289
+ { text: "outer loop", variant: "normal" },
1290
+ ],
1291
+ style: "codepath",
1292
+ }),
1293
+ },
1294
+ });
1295
+ }
1296
+ }
1297
+ const turnCompleteEvent = {
1298
+ chatId: currentWirePayload.chatId,
1299
+ messages: accumulatedMessages,
1300
+ uiMessages: accumulatedUIMessages,
1301
+ newMessages: turnNewModelMessages,
1302
+ newUIMessages: turnNewUIMessages,
1303
+ responseMessage: capturedResponseMessage,
1304
+ rawResponseMessage,
1305
+ turn,
1306
+ runId: currentRunId,
1307
+ chatAccessToken: turnAccessToken,
1308
+ clientData,
1309
+ stopped: wasStopped,
1310
+ continuation,
1311
+ previousRunId,
1312
+ preloaded,
1313
+ usage: turnUsage,
1314
+ totalUsage: cumulativeUsage,
1315
+ };
1316
+ // Fire onBeforeTurnComplete — stream is still open so the hook
1317
+ // can write custom chunks to the frontend (e.g. compaction progress).
1318
+ if (onBeforeTurnComplete) {
1319
+ await tracer_js_1.tracer.startActiveSpan("onBeforeTurnComplete()", async () => {
1320
+ await onBeforeTurnComplete(turnCompleteEvent);
1321
+ // Check if the hook replaced messages (compaction)
1322
+ const override = locals_js_1.locals.get(chatOverrideMessagesKey);
1323
+ if (override) {
1324
+ locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
1325
+ accumulatedUIMessages = [...override];
1326
+ accumulatedMessages = await toModelMessages(override);
1327
+ // Update event so onTurnComplete sees compacted messages
1328
+ turnCompleteEvent.messages = accumulatedMessages;
1329
+ turnCompleteEvent.uiMessages = accumulatedUIMessages;
1330
+ }
1331
+ }, {
1332
+ attributes: {
1333
+ [v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete",
1334
+ [v3_1.SemanticInternalAttributes.COLLAPSED]: true,
1335
+ "chat.id": currentWirePayload.chatId,
1336
+ "chat.turn": turn + 1,
1337
+ },
1338
+ });
1339
+ }
1340
+ // Write turn-complete control chunk — closes the frontend stream.
1341
+ const turnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId, turnAccessToken);
1342
+ // Fire onTurnComplete — stream is closed, use for persistence.
696
1343
  if (onTurnComplete) {
697
1344
  await tracer_js_1.tracer.startActiveSpan("onTurnComplete()", async () => {
698
1345
  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,
1346
+ ...turnCompleteEvent,
709
1347
  lastEventId: turnCompleteResult.lastEventId,
710
- clientData,
711
- stopped: wasStopped,
712
- continuation,
713
- previousRunId,
714
- preloaded,
715
1348
  });
1349
+ // Check if onTurnComplete replaced messages (compaction)
1350
+ const turnCompleteOverride = locals_js_1.locals.get(chatOverrideMessagesKey);
1351
+ if (turnCompleteOverride) {
1352
+ locals_js_1.locals.set(chatOverrideMessagesKey, undefined);
1353
+ accumulatedUIMessages = [...turnCompleteOverride];
1354
+ accumulatedMessages = await toModelMessages(turnCompleteOverride);
1355
+ }
716
1356
  }, {
717
1357
  attributes: {
718
1358
  [v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete",
@@ -726,6 +1366,10 @@ function chatTask(options) {
726
1366
  "chat.messages.count": accumulatedMessages.length,
727
1367
  "chat.response.parts.count": capturedResponseMessage?.parts?.length ?? 0,
728
1368
  "chat.new_messages.count": turnNewUIMessages.length,
1369
+ ...(turnUsage?.inputTokens != null ? { "gen_ai.usage.input_tokens": turnUsage.inputTokens } : {}),
1370
+ ...(turnUsage?.outputTokens != null ? { "gen_ai.usage.output_tokens": turnUsage.outputTokens } : {}),
1371
+ ...(turnUsage?.totalTokens != null ? { "gen_ai.usage.total_tokens": turnUsage.totalTokens } : {}),
1372
+ ...(cumulativeUsage.totalTokens != null ? { "gen_ai.usage.cumulative_total_tokens": cumulativeUsage.totalTokens } : {}),
729
1373
  },
730
1374
  });
731
1375
  }
@@ -734,11 +1378,11 @@ function chatTask(options) {
734
1378
  currentWirePayload = pendingMessages[0];
735
1379
  return "continue";
736
1380
  }
737
- // Wait for the next message — stay warm briefly, then suspend
738
- const effectiveWarmTimeout = metadata_js_1.metadata.get(WARM_TIMEOUT_METADATA_KEY) ?? warmTimeoutInSeconds;
1381
+ // Wait for the next message — stay idle briefly, then suspend
1382
+ const effectiveIdleTimeout = metadata_js_1.metadata.get(IDLE_TIMEOUT_METADATA_KEY) ?? idleTimeoutInSeconds;
739
1383
  const effectiveTurnTimeout = metadata_js_1.metadata.get(TURN_TIMEOUT_METADATA_KEY) ?? turnTimeout;
740
- const next = await messagesInput.waitWithWarmup({
741
- warmTimeoutInSeconds: effectiveWarmTimeout,
1384
+ const next = await messagesInput.waitWithIdleTimeout({
1385
+ idleTimeoutInSeconds: effectiveIdleTimeout,
742
1386
  timeout: effectiveTurnTimeout,
743
1387
  spanName: "waiting for next message",
744
1388
  });
@@ -787,7 +1431,7 @@ function chatTask(options) {
787
1431
  // Runtime configuration helpers
788
1432
  // ---------------------------------------------------------------------------
789
1433
  const TURN_TIMEOUT_METADATA_KEY = "chat.turnTimeout";
790
- const WARM_TIMEOUT_METADATA_KEY = "chat.warmTimeout";
1434
+ const IDLE_TIMEOUT_METADATA_KEY = "chat.idleTimeout";
791
1435
  /**
792
1436
  * Override the turn timeout for subsequent turns in the current run.
793
1437
  *
@@ -827,24 +1471,24 @@ function setTurnTimeoutInSeconds(seconds) {
827
1471
  metadata_js_1.metadata.set(TURN_TIMEOUT_METADATA_KEY, `${seconds}s`);
828
1472
  }
829
1473
  /**
830
- * Override the warm timeout for subsequent turns in the current run.
1474
+ * Override the idle timeout for subsequent turns in the current run.
831
1475
  *
832
- * The warm timeout controls how long the run stays active (using compute)
1476
+ * The idle timeout controls how long the run stays active (using compute)
833
1477
  * after each turn, waiting for the next message. During this window,
834
1478
  * responses are instant. After it expires, the run suspends.
835
1479
  *
836
- * @param seconds - Number of seconds to stay warm (0 to suspend immediately)
1480
+ * @param seconds - Number of seconds to stay idle (0 to suspend immediately)
837
1481
  *
838
1482
  * @example
839
1483
  * ```ts
840
1484
  * run: async ({ messages, signal }) => {
841
- * chat.setWarmTimeoutInSeconds(60);
1485
+ * chat.setIdleTimeoutInSeconds(60);
842
1486
  * return streamText({ model, messages, abortSignal: signal });
843
1487
  * }
844
1488
  * ```
845
1489
  */
846
- function setWarmTimeoutInSeconds(seconds) {
847
- metadata_js_1.metadata.set(WARM_TIMEOUT_METADATA_KEY, seconds);
1490
+ function setIdleTimeoutInSeconds(seconds) {
1491
+ metadata_js_1.metadata.set(IDLE_TIMEOUT_METADATA_KEY, seconds);
848
1492
  }
849
1493
  /**
850
1494
  * Override the `toUIMessageStream()` options for the current turn.
@@ -1097,13 +1741,17 @@ async function pipeChatAndCapture(source, options) {
1097
1741
  class ChatMessageAccumulator {
1098
1742
  modelMessages = [];
1099
1743
  uiMessages = [];
1744
+ _compaction;
1745
+ constructor(options) {
1746
+ this._compaction = options?.compaction;
1747
+ }
1100
1748
  /**
1101
1749
  * Add incoming messages from the transport payload.
1102
1750
  * Returns the full accumulated model messages for `streamText`.
1103
1751
  */
1104
1752
  async addIncoming(messages, trigger, turn) {
1105
1753
  const cleaned = messages.map((m) => m.role === "assistant" ? cleanupAbortedParts(m) : m);
1106
- const model = await (0, ai_1.convertToModelMessages)(cleaned);
1754
+ const model = await toModelMessages(cleaned);
1107
1755
  if (turn === 0 || trigger === "regenerate-message") {
1108
1756
  this.modelMessages = model;
1109
1757
  this.uiMessages = [...cleaned];
@@ -1118,25 +1766,100 @@ class ChatMessageAccumulator {
1118
1766
  * Add the assistant's response to the accumulator.
1119
1767
  * Call after `pipeAndCapture` with the captured response.
1120
1768
  */
1769
+ /**
1770
+ * Replace all accumulated messages (for compaction).
1771
+ * Converts UIMessages to ModelMessages internally.
1772
+ */
1773
+ async setMessages(uiMessages) {
1774
+ this.uiMessages = [...uiMessages];
1775
+ this.modelMessages = await toModelMessages(uiMessages);
1776
+ }
1121
1777
  async addResponse(response) {
1122
1778
  if (!response.id) {
1123
1779
  response = { ...response, id: (0, ai_1.generateId)() };
1124
1780
  }
1125
1781
  this.uiMessages.push(response);
1126
1782
  try {
1127
- const msgs = await (0, ai_1.convertToModelMessages)([stripProviderMetadata(response)]);
1783
+ const msgs = await toModelMessages([stripProviderMetadata(response)]);
1128
1784
  this.modelMessages.push(...msgs);
1129
1785
  }
1130
1786
  catch {
1131
1787
  // Conversion failed — skip model message accumulation for this response
1132
1788
  }
1133
1789
  }
1790
+ /**
1791
+ * Returns a `prepareStep` function for inner-loop compaction.
1792
+ * Only available when `compaction` was provided to the constructor.
1793
+ * Pass the result to `streamText({ prepareStep: conversation.prepareStep() })`.
1794
+ */
1795
+ prepareStep() {
1796
+ if (!this._compaction)
1797
+ return undefined;
1798
+ const comp = this._compaction;
1799
+ return async ({ messages, steps }) => {
1800
+ const result = await chatCompact(messages, steps, {
1801
+ shouldCompact: comp.shouldCompact,
1802
+ summarize: (msgs) => comp.summarize({ messages: msgs, source: "inner" }),
1803
+ });
1804
+ return result.type === "skipped" ? undefined : result;
1805
+ };
1806
+ }
1807
+ /**
1808
+ * Run outer-loop compaction if needed. Call after adding the response
1809
+ * and capturing usage. Applies `compactModelMessages` and `compactUIMessages`
1810
+ * callbacks if configured.
1811
+ *
1812
+ * @returns `true` if compaction was performed, `false` otherwise.
1813
+ */
1814
+ async compactIfNeeded(usage, context) {
1815
+ if (!this._compaction || !usage)
1816
+ return false;
1817
+ const shouldTrigger = await this._compaction.shouldCompact({
1818
+ messages: this.modelMessages,
1819
+ totalTokens: usage.totalTokens,
1820
+ inputTokens: usage.inputTokens,
1821
+ outputTokens: usage.outputTokens,
1822
+ usage,
1823
+ totalUsage: context?.totalUsage,
1824
+ chatId: context?.chatId,
1825
+ turn: context?.turn,
1826
+ clientData: context?.clientData,
1827
+ source: "outer",
1828
+ });
1829
+ if (!shouldTrigger)
1830
+ return false;
1831
+ const summary = await this._compaction.summarize({
1832
+ messages: this.modelMessages,
1833
+ usage,
1834
+ totalUsage: context?.totalUsage,
1835
+ chatId: context?.chatId,
1836
+ turn: context?.turn,
1837
+ clientData: context?.clientData,
1838
+ source: "outer",
1839
+ });
1840
+ const compactEvent = {
1841
+ summary,
1842
+ uiMessages: this.uiMessages,
1843
+ modelMessages: this.modelMessages,
1844
+ chatId: context?.chatId ?? "",
1845
+ turn: context?.turn ?? 0,
1846
+ clientData: context?.clientData,
1847
+ source: "outer",
1848
+ };
1849
+ this.modelMessages = this._compaction.compactModelMessages
1850
+ ? await this._compaction.compactModelMessages(compactEvent)
1851
+ : [{ role: "assistant", content: [{ type: "text", text: `[Conversation summary]\n\n${summary}` }] }];
1852
+ if (this._compaction.compactUIMessages) {
1853
+ this.uiMessages = await this._compaction.compactUIMessages(compactEvent);
1854
+ }
1855
+ return true;
1856
+ }
1134
1857
  }
1135
1858
  /**
1136
1859
  * Create a chat session that yields turns as an async iterator.
1137
1860
  *
1138
1861
  * Handles: preload wait, stop signals, message accumulation, turn-complete
1139
- * signaling, and warm/suspend between turns. You control: initialization,
1862
+ * signaling, and idle/suspend between turns. You control: initialization,
1140
1863
  * model/tool selection, persistence, and any custom per-turn logic.
1141
1864
  *
1142
1865
  * @example
@@ -1164,20 +1887,22 @@ class ChatMessageAccumulator {
1164
1887
  * ```
1165
1888
  */
1166
1889
  function createChatSession(payload, options) {
1167
- const { signal: runSignal, warmTimeoutInSeconds = 30, timeout = "1h", maxTurns = 100, } = options;
1890
+ const { signal: runSignal, idleTimeoutInSeconds = 30, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, } = options;
1168
1891
  return {
1169
1892
  [Symbol.asyncIterator]() {
1170
1893
  let currentPayload = payload;
1171
1894
  let turn = -1;
1172
1895
  const stop = createStopSignal();
1173
1896
  const accumulator = new ChatMessageAccumulator();
1897
+ let previousTurnUsage;
1898
+ let cumulativeUsage = emptyUsage();
1174
1899
  return {
1175
1900
  async next() {
1176
1901
  turn++;
1177
1902
  // First turn: handle preload — wait for the first real message
1178
1903
  if (turn === 0 && currentPayload.trigger === "preload") {
1179
- const result = await messagesInput.waitWithWarmup({
1180
- warmTimeoutInSeconds: currentPayload.warmTimeoutInSeconds ?? warmTimeoutInSeconds,
1904
+ const result = await messagesInput.waitWithIdleTimeout({
1905
+ idleTimeoutInSeconds: currentPayload.idleTimeoutInSeconds ?? idleTimeoutInSeconds,
1181
1906
  timeout,
1182
1907
  spanName: "waiting for first message",
1183
1908
  });
@@ -1189,8 +1914,8 @@ function createChatSession(payload, options) {
1189
1914
  }
1190
1915
  // Subsequent turns: wait for the next message
1191
1916
  if (turn > 0) {
1192
- const next = await messagesInput.waitWithWarmup({
1193
- warmTimeoutInSeconds,
1917
+ const next = await messagesInput.waitWithIdleTimeout({
1918
+ idleTimeoutInSeconds,
1194
1919
  timeout,
1195
1920
  spanName: "waiting for next message",
1196
1921
  });
@@ -1215,11 +1940,16 @@ function createChatSession(payload, options) {
1215
1940
  chatId: currentPayload.chatId,
1216
1941
  trigger: currentPayload.trigger,
1217
1942
  clientData: currentPayload.metadata,
1218
- messages,
1219
- uiMessages: accumulator.uiMessages,
1943
+ get messages() { return accumulator.modelMessages; },
1944
+ get uiMessages() { return accumulator.uiMessages; },
1220
1945
  signal: combinedSignal,
1221
1946
  get stopped() { return stop.signal.aborted && !runSignal.aborted; },
1222
1947
  continuation: currentPayload.continuation ?? false,
1948
+ previousTurnUsage,
1949
+ totalUsage: cumulativeUsage,
1950
+ async setMessages(uiMessages) {
1951
+ await accumulator.setMessages(uiMessages);
1952
+ },
1223
1953
  async complete(source) {
1224
1954
  let response;
1225
1955
  try {
@@ -1244,6 +1974,58 @@ function createChatSession(payload, options) {
1244
1974
  : response;
1245
1975
  await accumulator.addResponse(cleaned);
1246
1976
  }
1977
+ // Capture token usage from the streamText result
1978
+ let turnUsage;
1979
+ if (typeof source.totalUsage?.then === "function") {
1980
+ try {
1981
+ const usage = await source.totalUsage;
1982
+ turnUsage = usage;
1983
+ previousTurnUsage = usage;
1984
+ cumulativeUsage = addUsage(cumulativeUsage, usage);
1985
+ }
1986
+ catch { /* non-fatal */ }
1987
+ }
1988
+ // Outer-loop compaction (same logic as chat.task)
1989
+ if (sessionCompaction && turnUsage && !turnObj.stopped) {
1990
+ const shouldTrigger = await sessionCompaction.shouldCompact({
1991
+ messages: accumulator.modelMessages,
1992
+ totalTokens: turnUsage.totalTokens,
1993
+ inputTokens: turnUsage.inputTokens,
1994
+ outputTokens: turnUsage.outputTokens,
1995
+ usage: turnUsage,
1996
+ totalUsage: cumulativeUsage,
1997
+ chatId: currentPayload.chatId,
1998
+ turn,
1999
+ clientData: currentPayload.metadata,
2000
+ source: "outer",
2001
+ });
2002
+ if (shouldTrigger) {
2003
+ const summary = await sessionCompaction.summarize({
2004
+ messages: accumulator.modelMessages,
2005
+ usage: turnUsage,
2006
+ totalUsage: cumulativeUsage,
2007
+ chatId: currentPayload.chatId,
2008
+ turn,
2009
+ clientData: currentPayload.metadata,
2010
+ source: "outer",
2011
+ });
2012
+ const compactEvent = {
2013
+ summary,
2014
+ uiMessages: accumulator.uiMessages,
2015
+ modelMessages: accumulator.modelMessages,
2016
+ chatId: currentPayload.chatId,
2017
+ turn,
2018
+ clientData: currentPayload.metadata,
2019
+ source: "outer",
2020
+ };
2021
+ accumulator.modelMessages = sessionCompaction.compactModelMessages
2022
+ ? await sessionCompaction.compactModelMessages(compactEvent)
2023
+ : [{ role: "assistant", content: [{ type: "text", text: `[Conversation summary]\n\n${summary}` }] }];
2024
+ if (sessionCompaction.compactUIMessages) {
2025
+ accumulator.uiMessages = await sessionCompaction.compactUIMessages(compactEvent);
2026
+ }
2027
+ }
2028
+ }
1247
2029
  await chatWriteTurnComplete();
1248
2030
  return response;
1249
2031
  },
@@ -1457,8 +2239,8 @@ exports.chat = {
1457
2239
  setTurnTimeout,
1458
2240
  /** Override the turn timeout at runtime (seconds). See {@link setTurnTimeoutInSeconds}. */
1459
2241
  setTurnTimeoutInSeconds,
1460
- /** Override the warm timeout at runtime. See {@link setWarmTimeoutInSeconds}. */
1461
- setWarmTimeoutInSeconds,
2242
+ /** Override the idle timeout at runtime. See {@link setIdleTimeoutInSeconds}. */
2243
+ setIdleTimeoutInSeconds,
1462
2244
  /** Override toUIMessageStream() options for the current turn. See {@link setUIMessageStreamOptions}. */
1463
2245
  setUIMessageStreamOptions,
1464
2246
  /** Check if the current turn was stopped by the user. See {@link isStopped}. */
@@ -1481,6 +2263,33 @@ exports.chat = {
1481
2263
  MessageAccumulator: ChatMessageAccumulator,
1482
2264
  /** Create a chat session (async iterator). See {@link createChatSession}. */
1483
2265
  createSession: createChatSession,
2266
+ /**
2267
+ * Store and retrieve a resolved prompt for the current run.
2268
+ *
2269
+ * - `chat.prompt.set(resolved)` — store a `ResolvedPrompt` or plain string
2270
+ * - `chat.prompt()` — read the stored prompt (throws if not set)
2271
+ */
2272
+ prompt: Object.assign(getChatPrompt, { set: setChatPrompt }),
2273
+ /**
2274
+ * Returns an options object ready to spread into `streamText()`.
2275
+ * Reads the stored prompt and returns `{ system, experimental_telemetry, ...config }`.
2276
+ * Returns `{}` if no prompt has been set.
2277
+ */
2278
+ toStreamTextOptions,
2279
+ /**
2280
+ * Replace the accumulated conversation messages for compaction.
2281
+ * Call from `onTurnStart` or `onTurnComplete`. Takes `UIMessage[]` and
2282
+ * converts to `ModelMessage[]` internally.
2283
+ */
2284
+ setMessages: setChatMessages,
2285
+ /** Check if it's safe to compact messages (no in-flight tool calls). */
2286
+ isCompactionSafe,
2287
+ /** Returns a `prepareStep` function that handles context compaction automatically. */
2288
+ compactionStep: chatCompactionStep,
2289
+ /** Low-level compaction for use inside a custom `prepareStep`. */
2290
+ compact: chatCompact,
2291
+ /** Read the current compaction state (summary + base message count). */
2292
+ getCompactionState,
1484
2293
  };
1485
2294
  /**
1486
2295
  * Writes a turn-complete control chunk to the chat output stream.