@kenkaiiii/gg-agent 5.68.0 → 5.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -103,6 +103,12 @@ interface AgentTool<T extends z.ZodType = z.ZodType> extends Tool {
103
103
  * message — becomes unreachable.
104
104
  */
105
105
  timeoutMs?: number;
106
+ /**
107
+ * Whether a mid-run steering message may preempt this tool. Defaults to
108
+ * true, except for atomic file mutators (`edit`, `write`, …) which always
109
+ * run to completion so they are never left half-applied.
110
+ */
111
+ interruptible?: boolean;
106
112
  execute: (args: z.infer<T>, context: ToolContext) => ToolExecuteResult | Promise<ToolExecuteResult>;
107
113
  }
108
114
  interface AgentTextDeltaEvent {
@@ -343,6 +349,7 @@ interface AgentOptions {
343
349
  transportSessionId?: StreamOptions["transportSessionId"];
344
350
  projectId?: StreamOptions["projectId"];
345
351
  cacheRetention?: StreamOptions["cacheRetention"];
352
+ onContextPrepared?: StreamOptions["onContextPrepared"];
346
353
  /** Stable per-session cache routing key for providers that support it. */
347
354
  promptCacheKey?: StreamOptions["promptCacheKey"];
348
355
  /** Override the User-Agent sent with OAuth-authenticated Anthropic requests. */
@@ -370,6 +377,14 @@ interface AgentOptions {
370
377
  * against parallel fan-outs injecting huge uncached context in one turn;
371
378
  * the largest results are trimmed (water-filling) with a re-run notice. */
372
379
  maxTurnToolResultChars?: number;
380
+ /** Optional post-processing of a SUCCESSFUL tool result (after redaction,
381
+ * before the tool_call_end event and the provider context). Return the
382
+ * content unchanged to leave it alone. A throw is ignored (original kept).
383
+ * Used e.g. to append a warning to untrusted-content results. */
384
+ transformToolResult?: (call: {
385
+ name: string;
386
+ args: Record<string, unknown>;
387
+ }, content: ToolResultContent) => ToolResultContent;
373
388
  /** Max consecutive pause_turn continuations before stopping (default: 5).
374
389
  * Prevents infinite loops when server-side tools keep pausing. */
375
390
  maxContinuations?: number;
@@ -391,6 +406,15 @@ interface AgentOptions {
391
406
  * on read.
392
407
  */
393
408
  getSteeringMessages?: () => Promise<Message[] | null> | Message[] | null;
409
+ /**
410
+ * Instant interrupt (codex `instant_interrupt`): subscribe to "a steering
411
+ * message just arrived". While tools are running, the listener preempts
412
+ * interruptible tools (their AbortSignal fires; unfinished calls get an
413
+ * "Interrupted" error result) and the loop drains steering and continues.
414
+ * Distinct from `signal` (the Stop button), which ends the run. Returns an
415
+ * unsubscribe function.
416
+ */
417
+ onSteeringAvailable?: (listener: () => void) => () => void;
394
418
  /**
395
419
  * Polled when the agent would otherwise stop (no tool calls, no steering).
396
420
  * Returns messages to inject and continue the loop. Lower priority than
@@ -455,6 +479,7 @@ declare class Agent {
455
479
  private options;
456
480
  private steeringQueue;
457
481
  private followUpQueue;
482
+ private steeringListeners;
458
483
  constructor(options: AgentOptions);
459
484
  /** Snapshot of the current message history. Used for session persistence. */
460
485
  getMessages(): Message[];
@@ -465,7 +490,11 @@ declare class Agent {
465
490
  */
466
491
  setSignal(signal: AbortSignal | undefined): void;
467
492
  get running(): boolean;
468
- /** Queue a steering message for injection after current tool execution completes. */
493
+ /**
494
+ * Queue a steering message. Instant interrupt: if tools are running, the
495
+ * interruptible ones are preempted right away and the message is injected
496
+ * before the next model call (the turn continues — unlike abort/Stop).
497
+ */
469
498
  steer(msg: Message): void;
470
499
  /** Queue a follow-up message for injection when the agent would otherwise stop. */
471
500
  followUp(msg: Message): void;
package/dist/index.d.ts CHANGED
@@ -103,6 +103,12 @@ interface AgentTool<T extends z.ZodType = z.ZodType> extends Tool {
103
103
  * message — becomes unreachable.
104
104
  */
105
105
  timeoutMs?: number;
106
+ /**
107
+ * Whether a mid-run steering message may preempt this tool. Defaults to
108
+ * true, except for atomic file mutators (`edit`, `write`, …) which always
109
+ * run to completion so they are never left half-applied.
110
+ */
111
+ interruptible?: boolean;
106
112
  execute: (args: z.infer<T>, context: ToolContext) => ToolExecuteResult | Promise<ToolExecuteResult>;
107
113
  }
108
114
  interface AgentTextDeltaEvent {
@@ -343,6 +349,7 @@ interface AgentOptions {
343
349
  transportSessionId?: StreamOptions["transportSessionId"];
344
350
  projectId?: StreamOptions["projectId"];
345
351
  cacheRetention?: StreamOptions["cacheRetention"];
352
+ onContextPrepared?: StreamOptions["onContextPrepared"];
346
353
  /** Stable per-session cache routing key for providers that support it. */
347
354
  promptCacheKey?: StreamOptions["promptCacheKey"];
348
355
  /** Override the User-Agent sent with OAuth-authenticated Anthropic requests. */
@@ -370,6 +377,14 @@ interface AgentOptions {
370
377
  * against parallel fan-outs injecting huge uncached context in one turn;
371
378
  * the largest results are trimmed (water-filling) with a re-run notice. */
372
379
  maxTurnToolResultChars?: number;
380
+ /** Optional post-processing of a SUCCESSFUL tool result (after redaction,
381
+ * before the tool_call_end event and the provider context). Return the
382
+ * content unchanged to leave it alone. A throw is ignored (original kept).
383
+ * Used e.g. to append a warning to untrusted-content results. */
384
+ transformToolResult?: (call: {
385
+ name: string;
386
+ args: Record<string, unknown>;
387
+ }, content: ToolResultContent) => ToolResultContent;
373
388
  /** Max consecutive pause_turn continuations before stopping (default: 5).
374
389
  * Prevents infinite loops when server-side tools keep pausing. */
375
390
  maxContinuations?: number;
@@ -391,6 +406,15 @@ interface AgentOptions {
391
406
  * on read.
392
407
  */
393
408
  getSteeringMessages?: () => Promise<Message[] | null> | Message[] | null;
409
+ /**
410
+ * Instant interrupt (codex `instant_interrupt`): subscribe to "a steering
411
+ * message just arrived". While tools are running, the listener preempts
412
+ * interruptible tools (their AbortSignal fires; unfinished calls get an
413
+ * "Interrupted" error result) and the loop drains steering and continues.
414
+ * Distinct from `signal` (the Stop button), which ends the run. Returns an
415
+ * unsubscribe function.
416
+ */
417
+ onSteeringAvailable?: (listener: () => void) => () => void;
394
418
  /**
395
419
  * Polled when the agent would otherwise stop (no tool calls, no steering).
396
420
  * Returns messages to inject and continue the loop. Lower priority than
@@ -455,6 +479,7 @@ declare class Agent {
455
479
  private options;
456
480
  private steeringQueue;
457
481
  private followUpQueue;
482
+ private steeringListeners;
458
483
  constructor(options: AgentOptions);
459
484
  /** Snapshot of the current message history. Used for session persistence. */
460
485
  getMessages(): Message[];
@@ -465,7 +490,11 @@ declare class Agent {
465
490
  */
466
491
  setSignal(signal: AbortSignal | undefined): void;
467
492
  get running(): boolean;
468
- /** Queue a steering message for injection after current tool execution completes. */
493
+ /**
494
+ * Queue a steering message. Instant interrupt: if tools are running, the
495
+ * interruptible ones are preempted right away and the message is injected
496
+ * before the next model call (the turn continues — unlike abort/Stop).
497
+ */
469
498
  steer(msg: Message): void;
470
499
  /** Queue a follow-up message for injection when the agent would otherwise stop. */
471
500
  followUp(msg: Message): void;
package/dist/index.js CHANGED
@@ -220,6 +220,29 @@ ${rule.reminder.trim()}
220
220
  // src/agent-loop.ts
221
221
  var DEFAULT_MAX_TURNS = 300;
222
222
  var DEFAULT_TOOL_TIMEOUT_MS = 3e5;
223
+ var PREEMPT_GRACE_MS = 500;
224
+ var STEER_INTERRUPTED_TEXT = "Interrupted: the user sent a new message.";
225
+ var ATOMIC_TOOL_NAMES = /* @__PURE__ */ new Set(["edit", "write", "multi_edit", "apply_patch", "notebook_edit"]);
226
+ function isInterruptibleTool(tool, name) {
227
+ if (tool?.interruptible !== void 0) return tool.interruptible;
228
+ return !ATOMIC_TOOL_NAMES.has(name);
229
+ }
230
+ function markSteerInterrupted(record) {
231
+ const content = record.content;
232
+ let next;
233
+ if (typeof content === "string") {
234
+ next = content.trim() ? `${STEER_INTERRUPTED_TEXT}
235
+ Partial output:
236
+ ${content}` : STEER_INTERRUPTED_TEXT;
237
+ } else {
238
+ next = [
239
+ { type: "text", text: `${STEER_INTERRUPTED_TEXT}
240
+ Partial output:` },
241
+ ...content
242
+ ];
243
+ }
244
+ return { toolCallId: record.toolCallId, content: next, isError: true };
245
+ }
223
246
  var _toolRedaction;
224
247
  function toolRedactionOptions() {
225
248
  _toolRedaction ??= { secrets: environmentSecrets(process.env) };
@@ -694,6 +717,7 @@ async function* agentLoop(messages, options) {
694
717
  transportSessionId: options.transportSessionId,
695
718
  projectId: liveProjectId,
696
719
  cacheRetention: options.cacheRetention,
720
+ onContextPrepared: options.onContextPrepared,
697
721
  promptCacheKey: options.promptCacheKey,
698
722
  serviceTier: options.serviceTier,
699
723
  supportsImages: options.supportsImages,
@@ -1369,10 +1393,14 @@ async function* agentLoop(messages, options) {
1369
1393
  fatalToolArgumentRecoverable = recoverable;
1370
1394
  fatalToolArgumentToolName = toolName;
1371
1395
  };
1396
+ const preempt = new AbortController();
1397
+ const unsubscribeSteering = options.onSteeringAvailable?.(() => preempt.abort());
1372
1398
  const executionOptions = {
1373
1399
  signal: options.signal,
1400
+ preemptSignal: options.onSteeringAvailable ? preempt.signal : void 0,
1374
1401
  maxToolResultChars: options.maxToolResultChars,
1375
1402
  maxTurnToolResultChars: options.maxTurnToolResultChars,
1403
+ transformToolResult: options.transformToolResult,
1376
1404
  toolMap,
1377
1405
  invalidToolArgumentCounts,
1378
1406
  markFatalToolArgumentError,
@@ -1381,7 +1409,15 @@ async function* agentLoop(messages, options) {
1381
1409
  const hasSequentialToolCall = toolCalls.some(
1382
1410
  (toolCall) => toolMap.get(toolCall.name)?.executionMode === "sequential"
1383
1411
  );
1384
- const executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
1412
+ let executionResult;
1413
+ try {
1414
+ executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
1415
+ } finally {
1416
+ unsubscribeSteering?.();
1417
+ }
1418
+ if (executionResult.preempted) {
1419
+ diag("steer_preempted_tools", { turn, provider: options.provider, model: options.model });
1420
+ }
1385
1421
  messages.push({ role: "tool", content: executionResult.toolResults });
1386
1422
  yield { type: "checkpoint", turn };
1387
1423
  const toolsAborted = executionResult.aborted;
@@ -1555,8 +1591,12 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1555
1591
  }
1556
1592
  const callerSignal = options.signal;
1557
1593
  const toolTimeout = AbortSignal.timeout(tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS);
1594
+ const preemptSignal = isInterruptibleTool(tool, toolCall.name) ? options.preemptSignal : void 0;
1595
+ const toolSignals = [callerSignal, toolTimeout, preemptSignal].filter(
1596
+ (s) => s !== void 0
1597
+ );
1558
1598
  const ctx = {
1559
- signal: callerSignal ? AbortSignal.any([callerSignal, toolTimeout]) : toolTimeout,
1599
+ signal: toolSignals.length === 1 ? toolTimeout : AbortSignal.any(toolSignals),
1560
1600
  toolCallId: toolCall.id,
1561
1601
  onUpdate: (update) => {
1562
1602
  pushEvent({
@@ -1570,6 +1610,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1570
1610
  const normalized = normalizeToolResult(raw);
1571
1611
  resultContent = redactValue(normalized.content, toolRedactionOptions());
1572
1612
  details = redactValue(normalized.details, toolRedactionOptions());
1613
+ if (options.transformToolResult) {
1614
+ try {
1615
+ resultContent = options.transformToolResult(
1616
+ { name: toolCall.name, args: toolCall.args },
1617
+ resultContent
1618
+ );
1619
+ } catch {
1620
+ }
1621
+ }
1573
1622
  for (const key of options.invalidToolArgumentCounts.keys()) {
1574
1623
  if (key.startsWith(`${toolCall.name}:`)) options.invalidToolArgumentCounts.delete(key);
1575
1624
  }
@@ -1607,6 +1656,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1607
1656
  }
1608
1657
  resultContent = redactValue(resultContent, toolRedactionOptions());
1609
1658
  details = redactValue(details, toolRedactionOptions());
1659
+ if (options.preemptSignal?.aborted && isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name)) {
1660
+ const marked = markSteerInterrupted({
1661
+ toolCallId: toolCall.id,
1662
+ content: resultContent,
1663
+ isError
1664
+ });
1665
+ resultContent = marked.content;
1666
+ isError = true;
1667
+ }
1610
1668
  const durationMs = Date.now() - startTime;
1611
1669
  pushEvent({
1612
1670
  type: "tool_call_end",
@@ -1619,6 +1677,59 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1619
1677
  });
1620
1678
  return { toolCallId: toolCall.id, content: resultContent, isError };
1621
1679
  }
1680
+ function createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds) {
1681
+ const running = /* @__PURE__ */ new Map();
1682
+ let graceTimer;
1683
+ const preemptSignal = options.preemptSignal;
1684
+ const abandon = () => {
1685
+ graceTimer = void 0;
1686
+ for (const [id, entry] of running) {
1687
+ if (!entry.interruptible || resultsById.has(id)) continue;
1688
+ resultsById.set(id, { toolCallId: id, content: STEER_INTERRUPTED_TEXT, isError: true });
1689
+ pushToolEvent(eventStream, state, {
1690
+ type: "tool_call_end",
1691
+ toolCallId: id,
1692
+ result: STEER_INTERRUPTED_TEXT,
1693
+ isError: true,
1694
+ durationMs: Date.now() - entry.startedAt
1695
+ });
1696
+ running.delete(id);
1697
+ }
1698
+ if (running.size === 0) eventStream.close();
1699
+ };
1700
+ const onPreempt = () => {
1701
+ if (graceTimer === void 0) graceTimer = setTimeout(abandon, PREEMPT_GRACE_MS);
1702
+ };
1703
+ preemptSignal?.addEventListener("abort", onPreempt, { once: true });
1704
+ return {
1705
+ async run(toolCall) {
1706
+ if (options.preemptSignal?.aborted) return;
1707
+ dispatchedIds.add(toolCall.id);
1708
+ const interruptible = isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name);
1709
+ running.set(toolCall.id, { toolCall, interruptible, startedAt: Date.now() });
1710
+ const record = await executeSingleToolCall(
1711
+ toolCall,
1712
+ options,
1713
+ (event) => pushToolEvent(eventStream, state, event)
1714
+ );
1715
+ running.delete(toolCall.id);
1716
+ if (resultsById.has(toolCall.id)) return;
1717
+ const alreadyMarked = typeof record.content === "string" ? record.content.startsWith(STEER_INTERRUPTED_TEXT) : record.content[0]?.type === "text" && record.content[0].text.startsWith(STEER_INTERRUPTED_TEXT);
1718
+ resultsById.set(
1719
+ toolCall.id,
1720
+ interruptible && preemptSignal?.aborted && !alreadyMarked ? markSteerInterrupted(record) : record
1721
+ );
1722
+ if (preemptSignal?.aborted && graceTimer === void 0 && running.size === 0) {
1723
+ eventStream.close();
1724
+ }
1725
+ },
1726
+ preempted: () => preemptSignal?.aborted === true,
1727
+ dispose: () => {
1728
+ if (graceTimer !== void 0) clearTimeout(graceTimer);
1729
+ preemptSignal?.removeEventListener("abort", onPreempt);
1730
+ }
1731
+ };
1732
+ }
1622
1733
  async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1623
1734
  const eventStream = new EventStream();
1624
1735
  const state = { finalized: false };
@@ -1626,6 +1737,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1626
1737
  const dispatchedIds = /* @__PURE__ */ new Set();
1627
1738
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1628
1739
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1740
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
1629
1741
  const phases = [];
1630
1742
  let currentParallel = [];
1631
1743
  for (const toolCall of toolCalls) {
@@ -1646,40 +1758,18 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1646
1758
  void (async () => {
1647
1759
  try {
1648
1760
  for (const phase of phases) {
1649
- if (options.signal?.aborted) break;
1761
+ if (options.signal?.aborted || options.preemptSignal?.aborted) break;
1650
1762
  if (phase.sequential) {
1651
1763
  const signature = JSON.stringify([
1652
1764
  phase.sequential.name,
1653
1765
  canonicalToolArgs(phase.sequential.args)
1654
1766
  ]);
1655
1767
  if (!options.seenToolCalls.has(signature)) options.seenToolCalls.clear();
1656
- dispatchedIds.add(phase.sequential.id);
1657
- const record = await executeSingleToolCall(
1658
- phase.sequential,
1659
- options,
1660
- (event) => pushToolEvent(eventStream, state, event)
1661
- );
1662
- resultsById.set(record.toolCallId, record);
1768
+ await tracker.run(phase.sequential);
1663
1769
  } else if (phase.parallel.length === 1) {
1664
- dispatchedIds.add(phase.parallel[0].id);
1665
- const record = await executeSingleToolCall(
1666
- phase.parallel[0],
1667
- options,
1668
- (event) => pushToolEvent(eventStream, state, event)
1669
- );
1670
- resultsById.set(record.toolCallId, record);
1770
+ await tracker.run(phase.parallel[0]);
1671
1771
  } else {
1672
- await Promise.all(
1673
- phase.parallel.map(async (toolCall) => {
1674
- dispatchedIds.add(toolCall.id);
1675
- const record = await executeSingleToolCall(
1676
- toolCall,
1677
- options,
1678
- (event) => pushToolEvent(eventStream, state, event)
1679
- );
1680
- resultsById.set(record.toolCallId, record);
1681
- })
1682
- );
1772
+ await Promise.all(phase.parallel.map((toolCall) => tracker.run(toolCall)));
1683
1773
  }
1684
1774
  }
1685
1775
  if (!state.finalized) eventStream.close();
@@ -1700,12 +1790,13 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1700
1790
  }
1701
1791
  } finally {
1702
1792
  options.signal?.removeEventListener("abort", abortHandler);
1793
+ tracker.dispose();
1703
1794
  state.finalized = true;
1704
1795
  }
1705
1796
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1706
1797
  capToolResults(toolResults, options.maxToolResultChars);
1707
1798
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1708
- return { toolResults, aborted };
1799
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1709
1800
  }
1710
1801
  async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
1711
1802
  const eventStream = new EventStream();
@@ -1714,17 +1805,8 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1714
1805
  const dispatchedIds = /* @__PURE__ */ new Set();
1715
1806
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1716
1807
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1717
- Promise.all(
1718
- toolCalls.map(async (toolCall) => {
1719
- dispatchedIds.add(toolCall.id);
1720
- const record = await executeSingleToolCall(
1721
- toolCall,
1722
- options,
1723
- (event) => pushToolEvent(eventStream, state, event)
1724
- );
1725
- resultsById.set(record.toolCallId, record);
1726
- })
1727
- ).then(() => {
1808
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
1809
+ Promise.all(toolCalls.map((toolCall) => tracker.run(toolCall))).then(() => {
1728
1810
  if (!state.finalized) eventStream.close();
1729
1811
  }).catch((err) => {
1730
1812
  if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
@@ -1742,12 +1824,13 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1742
1824
  }
1743
1825
  } finally {
1744
1826
  options.signal?.removeEventListener("abort", abortHandler);
1827
+ tracker.dispose();
1745
1828
  state.finalized = true;
1746
1829
  }
1747
1830
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1748
1831
  capToolResults(toolResults, options.maxToolResultChars);
1749
1832
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1750
- return { toolResults, aborted };
1833
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1751
1834
  }
1752
1835
  function cancelledBeforeStartText(name) {
1753
1836
  return `\`${name}\` was cancelled before it started, so it had no effect. Safe to retry.`;
@@ -2043,6 +2126,7 @@ var Agent = class {
2043
2126
  options;
2044
2127
  steeringQueue = [];
2045
2128
  followUpQueue = [];
2129
+ steeringListeners = /* @__PURE__ */ new Set();
2046
2130
  constructor(options) {
2047
2131
  this.options = options;
2048
2132
  if (options.system) {
@@ -2067,9 +2151,14 @@ var Agent = class {
2067
2151
  get running() {
2068
2152
  return this._running;
2069
2153
  }
2070
- /** Queue a steering message for injection after current tool execution completes. */
2154
+ /**
2155
+ * Queue a steering message. Instant interrupt: if tools are running, the
2156
+ * interruptible ones are preempted right away and the message is injected
2157
+ * before the next model call (the turn continues — unlike abort/Stop).
2158
+ */
2071
2159
  steer(msg) {
2072
2160
  this.steeringQueue.push(msg);
2161
+ for (const listener of [...this.steeringListeners]) listener();
2073
2162
  }
2074
2163
  /** Queue a follow-up message for injection when the agent would otherwise stop. */
2075
2164
  followUp(msg) {
@@ -2089,6 +2178,14 @@ var Agent = class {
2089
2178
  const all = [...callerResult ?? [], ...queued];
2090
2179
  return all.length > 0 ? all : null;
2091
2180
  },
2181
+ onSteeringAvailable: (listener) => {
2182
+ this.steeringListeners.add(listener);
2183
+ const unsubscribeCaller = this.options.onSteeringAvailable?.(listener);
2184
+ return () => {
2185
+ this.steeringListeners.delete(listener);
2186
+ unsubscribeCaller?.();
2187
+ };
2188
+ },
2092
2189
  getFollowUpMessages: async () => {
2093
2190
  const callerResult = await this.options.getFollowUpMessages?.() ?? [];
2094
2191
  const queued = this.followUpQueue.splice(0);