@trigger.dev/sdk 0.0.0-prerelease-20260324161542 → 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.
@@ -279,6 +279,84 @@ export type ChatTaskCompactionOptions = {
279
279
  */
280
280
  compactModelMessages?: (event: CompactMessagesEvent) => ModelMessage[] | Promise<ModelMessage[]>;
281
281
  };
282
+ /**
283
+ * Event passed to `shouldInject` and `prepareMessages` callbacks.
284
+ */
285
+ export type PendingMessagesBatchEvent = {
286
+ /** All pending UI messages that arrived during streaming (batch). */
287
+ messages: UIMessage[];
288
+ /** Current model messages in the conversation. */
289
+ modelMessages: ModelMessage[];
290
+ /** Completed steps so far. */
291
+ steps: CompactionStep[];
292
+ /** Current step number (0-indexed). */
293
+ stepNumber: number;
294
+ /** Chat session ID. */
295
+ chatId: string;
296
+ /** Current turn number (0-indexed). */
297
+ turn: number;
298
+ /** Custom data from the frontend. */
299
+ clientData?: unknown;
300
+ };
301
+ /**
302
+ * Event passed to `onReceived` callback (per-message, as they arrive).
303
+ */
304
+ export type PendingMessageReceivedEvent = {
305
+ /** The UI message that arrived during streaming. */
306
+ message: UIMessage;
307
+ /** Chat session ID. */
308
+ chatId: string;
309
+ /** Current turn number (0-indexed). */
310
+ turn: number;
311
+ };
312
+ /**
313
+ * Event passed to `onInjected` callback (batch, after injection).
314
+ */
315
+ export type PendingMessagesInjectedEvent = {
316
+ /** All UI messages that were injected. */
317
+ messages: UIMessage[];
318
+ /** The model messages that were injected. */
319
+ injectedModelMessages: ModelMessage[];
320
+ /** Chat session ID. */
321
+ chatId: string;
322
+ /** Current turn number (0-indexed). */
323
+ turn: number;
324
+ /** Step number where injection occurred. */
325
+ stepNumber: number;
326
+ };
327
+ /**
328
+ * Options for the `pendingMessages` field on `chat.task()`, `chat.createSession()`,
329
+ * or `ChatMessageAccumulator`.
330
+ *
331
+ * Configures how messages that arrive during streaming are handled. When
332
+ * `shouldInject` is provided and returns `true`, the full batch of pending
333
+ * messages is injected between tool-call steps via `prepareStep`.
334
+ * Otherwise, messages queue for the next turn.
335
+ */
336
+ export type PendingMessagesOptions = {
337
+ /**
338
+ * Decide whether to inject pending messages between tool-call steps.
339
+ * Called once per step boundary with the full batch of pending messages.
340
+ * If absent, no injection happens — messages only queue for the next turn.
341
+ */
342
+ shouldInject?: (event: PendingMessagesBatchEvent) => boolean | Promise<boolean>;
343
+ /**
344
+ * Transform the batch of pending messages before injection.
345
+ * Return the model messages to inject.
346
+ * Default: convert each UI message via `convertToModelMessages`.
347
+ */
348
+ prepare?: (event: PendingMessagesBatchEvent) => ModelMessage[] | Promise<ModelMessage[]>;
349
+ /** Called when a message arrives during streaming (per-message). */
350
+ onReceived?: (event: PendingMessageReceivedEvent) => void | Promise<void>;
351
+ /** Called after a batch of messages is injected via `prepareStep`. */
352
+ onInjected?: (event: PendingMessagesInjectedEvent) => void | Promise<void>;
353
+ };
354
+ /**
355
+ * The data part type used to signal that pending messages were injected
356
+ * between tool-call steps. The frontend can match on this to render
357
+ * injection points inline in the assistant response.
358
+ */
359
+ export declare const PENDING_MESSAGE_INJECTED_TYPE: "data-pending-message-injected";
282
360
  /**
283
361
  * Event passed to the `prepareMessages` hook.
284
362
  */
@@ -888,7 +966,22 @@ export type ChatTaskOptions<TIdentifier extends string, TClientDataSchema extend
888
966
  */
889
967
  compaction?: ChatTaskCompactionOptions;
890
968
  /**
891
- * Called after the stream closes for this turn. Use this to persist the
969
+ * Configure how messages that arrive during streaming are handled.
970
+ *
971
+ * By default, messages queue for the next turn. When `shouldInject` is provided
972
+ * and returns `true`, messages are injected between tool-call steps via
973
+ * `prepareStep` — allowing users to steer the agent mid-execution.
974
+ *
975
+ * @example
976
+ * ```ts
977
+ * pendingMessages: {
978
+ * shouldInject: ({ steps }) => steps.length > 0,
979
+ * onReceived: ({ message }) => logger.info("Steering message received"),
980
+ * },
981
+ * ```
982
+ */
983
+ pendingMessages?: PendingMessagesOptions;
984
+ /**
892
985
  * conversation to your database after each assistant response.
893
986
  *
894
987
  * @example
@@ -1249,8 +1342,11 @@ declare class ChatMessageAccumulator {
1249
1342
  modelMessages: ModelMessage[];
1250
1343
  uiMessages: UIMessage[];
1251
1344
  private _compaction?;
1345
+ private _pendingMessages?;
1346
+ private _steeringQueue;
1252
1347
  constructor(options?: {
1253
1348
  compaction?: ChatTaskCompactionOptions;
1349
+ pendingMessages?: PendingMessagesOptions;
1254
1350
  });
1255
1351
  /**
1256
1352
  * Add incoming messages from the transport payload.
@@ -1268,9 +1364,21 @@ declare class ChatMessageAccumulator {
1268
1364
  setMessages(uiMessages: UIMessage[]): Promise<void>;
1269
1365
  addResponse(response: UIMessage): Promise<void>;
1270
1366
  /**
1271
- * Returns a `prepareStep` function for inner-loop compaction.
1272
- * Only available when `compaction` was provided to the constructor.
1273
- * Pass the result to `streamText({ prepareStep: conversation.prepareStep() })`.
1367
+ * Queue a message for injection via `prepareStep`. Call from a
1368
+ * `messagesInput.on()` listener when a message arrives during streaming.
1369
+ */
1370
+ steer(message: UIMessage, modelMessages?: ModelMessage[]): void;
1371
+ /**
1372
+ * Queue a message for injection, converting to model messages automatically.
1373
+ */
1374
+ steerAsync(message: UIMessage): Promise<void>;
1375
+ /**
1376
+ * Get and clear unconsumed steering messages.
1377
+ */
1378
+ drainSteering(): UIMessage[];
1379
+ /**
1380
+ * Returns a `prepareStep` function that handles both compaction and
1381
+ * pending message injection. Pass to `streamText({ prepareStep: conversation.prepareStep() })`.
1274
1382
  */
1275
1383
  prepareStep(): ((args: {
1276
1384
  messages: ModelMessage[];
@@ -1303,6 +1411,8 @@ export type ChatSessionOptions = {
1303
1411
  maxTurns?: number;
1304
1412
  /** Automatic context compaction — same options as `chat.task({ compaction })`. */
1305
1413
  compaction?: ChatTaskCompactionOptions;
1414
+ /** Configure mid-execution message injection — same options as `chat.task({ pendingMessages })`. */
1415
+ pendingMessages?: PendingMessagesOptions;
1306
1416
  };
1307
1417
  export type ChatTurn = {
1308
1418
  /** Turn number (0-indexed). */
@@ -1348,6 +1458,17 @@ export type ChatTurn = {
1348
1458
  * Use with `chat.pipeAndCapture` when you need control between pipe and done.
1349
1459
  */
1350
1460
  addResponse(response: UIMessage): Promise<void>;
1461
+ /**
1462
+ * Returns a `prepareStep` function that handles both compaction and
1463
+ * pending message injection. Pass to `streamText({ prepareStep: turn.prepareStep() })`.
1464
+ * Only needed when not using `chat.toStreamTextOptions()` (which auto-injects it).
1465
+ */
1466
+ prepareStep(): ((args: {
1467
+ messages: ModelMessage[];
1468
+ steps: CompactionStep[];
1469
+ }) => Promise<{
1470
+ messages: ModelMessage[];
1471
+ } | undefined>) | undefined;
1351
1472
  };
1352
1473
  /**
1353
1474
  * Create a chat session that yields turns as an async iterator.
@@ -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");
@@ -281,6 +281,18 @@ const chatOnCompactedKey = locals_js_1.locals.create("chat.onCompacted");
281
281
  const chatPrepareMessagesKey = locals_js_1.locals.create("chat.prepareMessages");
282
282
  /** @internal */
283
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");
284
296
  /**
285
297
  * Check that no tool calls are in-flight in a step's content.
286
298
  * Used before compaction to avoid losing tool state mid-execution.
@@ -514,6 +526,111 @@ function chatCompactionStep(options) {
514
526
  };
515
527
  }
516
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
+ // ---------------------------------------------------------------------------
517
634
  // chat.isCompactionSafe — check if it's safe to compact messages
518
635
  // ---------------------------------------------------------------------------
519
636
  /**
@@ -600,29 +717,42 @@ function toStreamTextOptions(options) {
600
717
  // Add telemetry (forward additional metadata from caller)
601
718
  const telemetry = prompt.toAISDKTelemetry(options?.telemetry);
602
719
  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.
720
+ // Auto-inject prepareStep when compaction or pendingMessages is configured.
606
721
  const taskCompaction = locals_js_1.locals.get(chatTaskCompactionKey);
607
- if (taskCompaction) {
722
+ const taskPendingMessages = locals_js_1.locals.get(chatPendingMessagesKey);
723
+ if (taskCompaction || taskPendingMessages) {
608
724
  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;
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;
626
756
  };
627
757
  }
628
758
  return result;
@@ -733,7 +863,7 @@ async function pipeChat(source, options) {
733
863
  * ```
734
864
  */
735
865
  function chatTask(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;
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;
737
867
  const parseClientData = clientDataSchema
738
868
  ? (0, v3_1.getSchemaParseFn)(clientDataSchema)
739
869
  : undefined;
@@ -759,6 +889,9 @@ function chatTask(options) {
759
889
  if (compaction) {
760
890
  locals_js_1.locals.set(chatTaskCompactionKey, compaction);
761
891
  }
892
+ if (pendingMessagesConfig) {
893
+ locals_js_1.locals.set(chatPendingMessagesKey, pendingMessagesConfig);
894
+ }
762
895
  let currentWirePayload = payload;
763
896
  const continuation = payload.continuation ?? false;
764
897
  const previousRunId = payload.previousRunId;
@@ -876,6 +1009,8 @@ function chatTask(options) {
876
1009
  locals_js_1.locals.set(chatPipeCountKey, 0);
877
1010
  locals_js_1.locals.set(chatDeferKey, new Set());
878
1011
  locals_js_1.locals.set(chatCompactionStateKey, undefined);
1012
+ locals_js_1.locals.set(chatSteeringQueueKey, []);
1013
+ locals_js_1.locals.set(chatInjectedMessageIdsKey, new Set());
879
1014
  // Store chat context for auto-detection by ai.tool subtasks
880
1015
  locals_js_1.locals.set(chatTurnContextKey, {
881
1016
  chatId: currentWirePayload.chatId,
@@ -893,7 +1028,39 @@ function chatTask(options) {
893
1028
  const combinedSignal = AbortSignal.any([runSignal, stopController.signal]);
894
1029
  // Buffer messages that arrive during streaming
895
1030
  const pendingMessages = [];
896
- 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
897
1064
  pendingMessages.push(msg);
898
1065
  });
899
1066
  // Clean up any incomplete tool parts in the incoming history.
@@ -1373,7 +1540,8 @@ function chatTask(options) {
1373
1540
  },
1374
1541
  });
1375
1542
  }
1376
- // 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.
1377
1545
  if (pendingMessages.length > 0) {
1378
1546
  currentWirePayload = pendingMessages[0];
1379
1547
  return "continue";
@@ -1742,8 +1910,11 @@ class ChatMessageAccumulator {
1742
1910
  modelMessages = [];
1743
1911
  uiMessages = [];
1744
1912
  _compaction;
1913
+ _pendingMessages;
1914
+ _steeringQueue = [];
1745
1915
  constructor(options) {
1746
1916
  this._compaction = options?.compaction;
1917
+ this._pendingMessages = options?.pendingMessages;
1747
1918
  }
1748
1919
  /**
1749
1920
  * Add incoming messages from the transport payload.
@@ -1788,20 +1959,63 @@ class ChatMessageAccumulator {
1788
1959
  }
1789
1960
  }
1790
1961
  /**
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() })`.
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() })`.
1794
1992
  */
1795
1993
  prepareStep() {
1796
- if (!this._compaction)
1994
+ if (!this._compaction && !this._pendingMessages)
1797
1995
  return undefined;
1798
1996
  const comp = this._compaction;
1997
+ const pm = this._pendingMessages;
1998
+ const queue = this._steeringQueue;
1799
1999
  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;
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;
1805
2019
  };
1806
2020
  }
1807
2021
  /**
@@ -1887,7 +2101,7 @@ class ChatMessageAccumulator {
1887
2101
  * ```
1888
2102
  */
1889
2103
  function createChatSession(payload, options) {
1890
- const { signal: runSignal, idleTimeoutInSeconds = 30, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, } = options;
2104
+ const { signal: runSignal, idleTimeoutInSeconds = 30, timeout = "1h", maxTurns = 100, compaction: sessionCompaction, pendingMessages: sessionPendingMessages, } = options;
1891
2105
  return {
1892
2106
  [Symbol.asyncIterator]() {
1893
2107
  let currentPayload = payload;
@@ -1932,6 +2146,44 @@ function createChatSession(payload, options) {
1932
2146
  }
1933
2147
  // Reset stop signal for this turn
1934
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
+ });
1935
2187
  // Accumulate messages
1936
2188
  const messages = await accumulator.addIncoming(currentPayload.messages, currentPayload.trigger, turn);
1937
2189
  const combinedSignal = AbortSignal.any([runSignal, stop.signal]);
@@ -1959,6 +2211,7 @@ function createChatSession(payload, options) {
1959
2211
  if (error instanceof Error && error.name === "AbortError") {
1960
2212
  if (runSignal.aborted) {
1961
2213
  // Full cancel — don't accumulate
2214
+ sessionMsgSub.off();
1962
2215
  await chatWriteTurnComplete();
1963
2216
  return undefined;
1964
2217
  }
@@ -2026,6 +2279,7 @@ function createChatSession(payload, options) {
2026
2279
  }
2027
2280
  }
2028
2281
  }
2282
+ sessionMsgSub.off();
2029
2283
  await chatWriteTurnComplete();
2030
2284
  return response;
2031
2285
  },
@@ -2033,8 +2287,34 @@ function createChatSession(payload, options) {
2033
2287
  await accumulator.addResponse(response);
2034
2288
  },
2035
2289
  async done() {
2290
+ sessionMsgSub.off();
2036
2291
  await chatWriteTurnComplete();
2037
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
+ },
2038
2318
  };
2039
2319
  return { done: false, value: turnObj };
2040
2320
  },