@kenkaiiii/gg-agent 5.68.1 → 5.70.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 {
@@ -371,6 +377,14 @@ interface AgentOptions {
371
377
  * against parallel fan-outs injecting huge uncached context in one turn;
372
378
  * the largest results are trimmed (water-filling) with a re-run notice. */
373
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;
374
388
  /** Max consecutive pause_turn continuations before stopping (default: 5).
375
389
  * Prevents infinite loops when server-side tools keep pausing. */
376
390
  maxContinuations?: number;
@@ -392,6 +406,15 @@ interface AgentOptions {
392
406
  * on read.
393
407
  */
394
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;
395
418
  /**
396
419
  * Polled when the agent would otherwise stop (no tool calls, no steering).
397
420
  * Returns messages to inject and continue the loop. Lower priority than
@@ -456,6 +479,7 @@ declare class Agent {
456
479
  private options;
457
480
  private steeringQueue;
458
481
  private followUpQueue;
482
+ private steeringListeners;
459
483
  constructor(options: AgentOptions);
460
484
  /** Snapshot of the current message history. Used for session persistence. */
461
485
  getMessages(): Message[];
@@ -466,7 +490,11 @@ declare class Agent {
466
490
  */
467
491
  setSignal(signal: AbortSignal | undefined): void;
468
492
  get running(): boolean;
469
- /** 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
+ */
470
498
  steer(msg: Message): void;
471
499
  /** Queue a follow-up message for injection when the agent would otherwise stop. */
472
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 {
@@ -371,6 +377,14 @@ interface AgentOptions {
371
377
  * against parallel fan-outs injecting huge uncached context in one turn;
372
378
  * the largest results are trimmed (water-filling) with a re-run notice. */
373
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;
374
388
  /** Max consecutive pause_turn continuations before stopping (default: 5).
375
389
  * Prevents infinite loops when server-side tools keep pausing. */
376
390
  maxContinuations?: number;
@@ -392,6 +406,15 @@ interface AgentOptions {
392
406
  * on read.
393
407
  */
394
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;
395
418
  /**
396
419
  * Polled when the agent would otherwise stop (no tool calls, no steering).
397
420
  * Returns messages to inject and continue the loop. Lower priority than
@@ -456,6 +479,7 @@ declare class Agent {
456
479
  private options;
457
480
  private steeringQueue;
458
481
  private followUpQueue;
482
+ private steeringListeners;
459
483
  constructor(options: AgentOptions);
460
484
  /** Snapshot of the current message history. Used for session persistence. */
461
485
  getMessages(): Message[];
@@ -466,7 +490,11 @@ declare class Agent {
466
490
  */
467
491
  setSignal(signal: AbortSignal | undefined): void;
468
492
  get running(): boolean;
469
- /** 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
+ */
470
498
  steer(msg: Message): void;
471
499
  /** Queue a follow-up message for injection when the agent would otherwise stop. */
472
500
  followUp(msg: Message): void;
package/dist/index.js CHANGED
@@ -36,6 +36,107 @@ function isLocalBackendUrl(baseUrl) {
36
36
  return false;
37
37
  }
38
38
 
39
+ // src/tool-call-hints.ts
40
+ import { resolveToolSchema } from "@kenkaiiii/gg-ai";
41
+ function normalizeName(name) {
42
+ return name.toLowerCase().replace(/[^a-z0-9]/g, "");
43
+ }
44
+ function editDistance(a, b) {
45
+ let beforePrevious = [];
46
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
47
+ for (let i = 1; i <= a.length; i++) {
48
+ const current = [i];
49
+ for (let j = 1; j <= b.length; j++) {
50
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
51
+ let distance = Math.min(
52
+ (previous[j] ?? 0) + 1,
53
+ (current[j - 1] ?? 0) + 1,
54
+ (previous[j - 1] ?? 0) + cost
55
+ );
56
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
57
+ distance = Math.min(distance, (beforePrevious[j - 2] ?? 0) + 1);
58
+ }
59
+ current[j] = distance;
60
+ }
61
+ beforePrevious = previous;
62
+ previous = current;
63
+ }
64
+ return previous[b.length] ?? 0;
65
+ }
66
+ function closestName(name, candidates) {
67
+ const target = normalizeName(name);
68
+ if (!target) return void 0;
69
+ const limit = Math.min(3, Math.max(1, Math.floor(target.length / 3)));
70
+ let best;
71
+ for (const candidate of [...candidates].sort()) {
72
+ if (candidate === name) continue;
73
+ const distance = editDistance(target, normalizeName(candidate));
74
+ if (distance > limit) continue;
75
+ if (!best || distance < best.distance) best = { name: candidate, distance };
76
+ }
77
+ return best?.name;
78
+ }
79
+ function unknownToolMessage(name, available) {
80
+ const suggestion = closestName(name, available);
81
+ if (suggestion) return `Unknown tool: ${name}. Did you mean \`${suggestion}\`?`;
82
+ const names = [...available].sort();
83
+ const listed = names.length > 40 ? `${names.slice(0, 40).join(", ")}, \u2026` : names.join(", ");
84
+ const searchHint = available.includes("tool_search") ? " If the capability is not listed, call `tool_search` to load it." : "";
85
+ return `Unknown tool: ${name}. Available tools: ${listed}.${searchHint}`;
86
+ }
87
+ function isRecord(value) {
88
+ return typeof value === "object" && value !== null && !Array.isArray(value);
89
+ }
90
+ function typeLabel(node) {
91
+ if (!isRecord(node)) return "any";
92
+ if (Array.isArray(node.enum)) {
93
+ return node.enum.map((value) => JSON.stringify(value)).join(" | ");
94
+ }
95
+ const variants = node.anyOf ?? node.oneOf;
96
+ if (Array.isArray(variants)) {
97
+ return [...new Set(variants.map(typeLabel))].filter((t) => t !== "null").join(" | ") || "any";
98
+ }
99
+ if (node.type === "array") return `${typeLabel(node.items)}[]`;
100
+ if (Array.isArray(node.type)) {
101
+ return node.type.filter((t) => t !== "null").join(" | ") || "any";
102
+ }
103
+ return typeof node.type === "string" ? node.type : "any";
104
+ }
105
+ function objectShape(tool) {
106
+ let schema;
107
+ try {
108
+ schema = resolveToolSchema(tool);
109
+ } catch {
110
+ return void 0;
111
+ }
112
+ if (!isRecord(schema) || !isRecord(schema.properties)) return void 0;
113
+ const required = new Set(
114
+ Array.isArray(schema.required) ? schema.required.filter((r) => typeof r === "string") : []
115
+ );
116
+ return { properties: schema.properties, required };
117
+ }
118
+ function argumentHints(tool, args) {
119
+ const shape = objectShape(tool);
120
+ if (!shape) return "";
121
+ const names = Object.keys(shape.properties);
122
+ if (names.length === 0) return `Expected arguments: none.`;
123
+ const fields = names.map((name) => {
124
+ const optional = shape.required.has(name) ? "" : "?";
125
+ return `${name}${optional}: ${typeLabel(shape.properties[name])}`;
126
+ });
127
+ const lines = [`Expected arguments: { ${fields.join(", ")} }`];
128
+ if (isRecord(args)) {
129
+ for (const key of Object.keys(args).sort()) {
130
+ if (key in shape.properties) continue;
131
+ const suggestion = closestName(key, names);
132
+ lines.push(
133
+ suggestion ? `Unknown field \`${key}\` \u2014 did you mean \`${suggestion}\`?` : `Unknown field \`${key}\` is not a parameter of this tool.`
134
+ );
135
+ }
136
+ }
137
+ return lines.join("\n");
138
+ }
139
+
39
140
  // src/output-ceiling.ts
40
141
  var TTL_MS = 24 * 60 * 60 * 1e3;
41
142
  var MIN_PLAUSIBLE_CEILING = 256;
@@ -220,6 +321,29 @@ ${rule.reminder.trim()}
220
321
  // src/agent-loop.ts
221
322
  var DEFAULT_MAX_TURNS = 300;
222
323
  var DEFAULT_TOOL_TIMEOUT_MS = 3e5;
324
+ var PREEMPT_GRACE_MS = 500;
325
+ var STEER_INTERRUPTED_TEXT = "Interrupted: the user sent a new message.";
326
+ var ATOMIC_TOOL_NAMES = /* @__PURE__ */ new Set(["edit", "write", "multi_edit", "apply_patch", "notebook_edit"]);
327
+ function isInterruptibleTool(tool, name) {
328
+ if (tool?.interruptible !== void 0) return tool.interruptible;
329
+ return !ATOMIC_TOOL_NAMES.has(name);
330
+ }
331
+ function markSteerInterrupted(record) {
332
+ const content = record.content;
333
+ let next;
334
+ if (typeof content === "string") {
335
+ next = content.trim() ? `${STEER_INTERRUPTED_TEXT}
336
+ Partial output:
337
+ ${content}` : STEER_INTERRUPTED_TEXT;
338
+ } else {
339
+ next = [
340
+ { type: "text", text: `${STEER_INTERRUPTED_TEXT}
341
+ Partial output:` },
342
+ ...content
343
+ ];
344
+ }
345
+ return { toolCallId: record.toolCallId, content: next, isError: true };
346
+ }
223
347
  var _toolRedaction;
224
348
  function toolRedactionOptions() {
225
349
  _toolRedaction ??= { secrets: environmentSecrets(process.env) };
@@ -1370,10 +1494,14 @@ async function* agentLoop(messages, options) {
1370
1494
  fatalToolArgumentRecoverable = recoverable;
1371
1495
  fatalToolArgumentToolName = toolName;
1372
1496
  };
1497
+ const preempt = new AbortController();
1498
+ const unsubscribeSteering = options.onSteeringAvailable?.(() => preempt.abort());
1373
1499
  const executionOptions = {
1374
1500
  signal: options.signal,
1501
+ preemptSignal: options.onSteeringAvailable ? preempt.signal : void 0,
1375
1502
  maxToolResultChars: options.maxToolResultChars,
1376
1503
  maxTurnToolResultChars: options.maxTurnToolResultChars,
1504
+ transformToolResult: options.transformToolResult,
1377
1505
  toolMap,
1378
1506
  invalidToolArgumentCounts,
1379
1507
  markFatalToolArgumentError,
@@ -1382,7 +1510,15 @@ async function* agentLoop(messages, options) {
1382
1510
  const hasSequentialToolCall = toolCalls.some(
1383
1511
  (toolCall) => toolMap.get(toolCall.name)?.executionMode === "sequential"
1384
1512
  );
1385
- const executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
1513
+ let executionResult;
1514
+ try {
1515
+ executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
1516
+ } finally {
1517
+ unsubscribeSteering?.();
1518
+ }
1519
+ if (executionResult.preempted) {
1520
+ diag("steer_preempted_tools", { turn, provider: options.provider, model: options.model });
1521
+ }
1386
1522
  messages.push({ role: "tool", content: executionResult.toolResults });
1387
1523
  yield { type: "checkpoint", turn };
1388
1524
  const toolsAborted = executionResult.aborted;
@@ -1539,7 +1675,7 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1539
1675
  options.seenToolCalls.add(signature);
1540
1676
  }
1541
1677
  if (!tool) {
1542
- resultContent = `Unknown tool: ${toolCall.name}`;
1678
+ resultContent = unknownToolMessage(toolCall.name, [...options.toolMap.keys()]);
1543
1679
  isError = true;
1544
1680
  } else {
1545
1681
  try {
@@ -1556,8 +1692,12 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1556
1692
  }
1557
1693
  const callerSignal = options.signal;
1558
1694
  const toolTimeout = AbortSignal.timeout(tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS);
1695
+ const preemptSignal = isInterruptibleTool(tool, toolCall.name) ? options.preemptSignal : void 0;
1696
+ const toolSignals = [callerSignal, toolTimeout, preemptSignal].filter(
1697
+ (s) => s !== void 0
1698
+ );
1559
1699
  const ctx = {
1560
- signal: callerSignal ? AbortSignal.any([callerSignal, toolTimeout]) : toolTimeout,
1700
+ signal: toolSignals.length === 1 ? toolTimeout : AbortSignal.any(toolSignals),
1561
1701
  toolCallId: toolCall.id,
1562
1702
  onUpdate: (update) => {
1563
1703
  pushEvent({
@@ -1571,6 +1711,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1571
1711
  const normalized = normalizeToolResult(raw);
1572
1712
  resultContent = redactValue(normalized.content, toolRedactionOptions());
1573
1713
  details = redactValue(normalized.details, toolRedactionOptions());
1714
+ if (options.transformToolResult) {
1715
+ try {
1716
+ resultContent = options.transformToolResult(
1717
+ { name: toolCall.name, args: toolCall.args },
1718
+ resultContent
1719
+ );
1720
+ } catch {
1721
+ }
1722
+ }
1574
1723
  for (const key of options.invalidToolArgumentCounts.keys()) {
1575
1724
  if (key.startsWith(`${toolCall.name}:`)) options.invalidToolArgumentCounts.delete(key);
1576
1725
  }
@@ -1582,8 +1731,10 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1582
1731
  const failureCount = (options.invalidToolArgumentCounts.get(failureKey) ?? 0) + 1;
1583
1732
  options.invalidToolArgumentCounts.set(failureKey, failureCount);
1584
1733
  invalidArgAttempt = failureCount;
1734
+ const hints = argumentHints(tool, toolCall.args);
1585
1735
  resultContent = `Invalid arguments for tool \`${toolCall.name}\`:
1586
- ` + prettyError + "\nRe-issue the call with each field as the correct type.";
1736
+ ` + prettyError + (hints ? `
1737
+ ${hints}` : "") + "\nRe-issue the call with each field as the correct type.";
1587
1738
  if (failureCount >= 3) {
1588
1739
  const recoverable = Object.keys(toolCall.args ?? {}).length === 0;
1589
1740
  options.markFatalToolArgumentError(
@@ -1608,6 +1759,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1608
1759
  }
1609
1760
  resultContent = redactValue(resultContent, toolRedactionOptions());
1610
1761
  details = redactValue(details, toolRedactionOptions());
1762
+ if (options.preemptSignal?.aborted && isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name)) {
1763
+ const marked = markSteerInterrupted({
1764
+ toolCallId: toolCall.id,
1765
+ content: resultContent,
1766
+ isError
1767
+ });
1768
+ resultContent = marked.content;
1769
+ isError = true;
1770
+ }
1611
1771
  const durationMs = Date.now() - startTime;
1612
1772
  pushEvent({
1613
1773
  type: "tool_call_end",
@@ -1620,6 +1780,59 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1620
1780
  });
1621
1781
  return { toolCallId: toolCall.id, content: resultContent, isError };
1622
1782
  }
1783
+ function createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds) {
1784
+ const running = /* @__PURE__ */ new Map();
1785
+ let graceTimer;
1786
+ const preemptSignal = options.preemptSignal;
1787
+ const abandon = () => {
1788
+ graceTimer = void 0;
1789
+ for (const [id, entry] of running) {
1790
+ if (!entry.interruptible || resultsById.has(id)) continue;
1791
+ resultsById.set(id, { toolCallId: id, content: STEER_INTERRUPTED_TEXT, isError: true });
1792
+ pushToolEvent(eventStream, state, {
1793
+ type: "tool_call_end",
1794
+ toolCallId: id,
1795
+ result: STEER_INTERRUPTED_TEXT,
1796
+ isError: true,
1797
+ durationMs: Date.now() - entry.startedAt
1798
+ });
1799
+ running.delete(id);
1800
+ }
1801
+ if (running.size === 0) eventStream.close();
1802
+ };
1803
+ const onPreempt = () => {
1804
+ if (graceTimer === void 0) graceTimer = setTimeout(abandon, PREEMPT_GRACE_MS);
1805
+ };
1806
+ preemptSignal?.addEventListener("abort", onPreempt, { once: true });
1807
+ return {
1808
+ async run(toolCall) {
1809
+ if (options.preemptSignal?.aborted) return;
1810
+ dispatchedIds.add(toolCall.id);
1811
+ const interruptible = isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name);
1812
+ running.set(toolCall.id, { toolCall, interruptible, startedAt: Date.now() });
1813
+ const record = await executeSingleToolCall(
1814
+ toolCall,
1815
+ options,
1816
+ (event) => pushToolEvent(eventStream, state, event)
1817
+ );
1818
+ running.delete(toolCall.id);
1819
+ if (resultsById.has(toolCall.id)) return;
1820
+ 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);
1821
+ resultsById.set(
1822
+ toolCall.id,
1823
+ interruptible && preemptSignal?.aborted && !alreadyMarked ? markSteerInterrupted(record) : record
1824
+ );
1825
+ if (preemptSignal?.aborted && graceTimer === void 0 && running.size === 0) {
1826
+ eventStream.close();
1827
+ }
1828
+ },
1829
+ preempted: () => preemptSignal?.aborted === true,
1830
+ dispose: () => {
1831
+ if (graceTimer !== void 0) clearTimeout(graceTimer);
1832
+ preemptSignal?.removeEventListener("abort", onPreempt);
1833
+ }
1834
+ };
1835
+ }
1623
1836
  async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1624
1837
  const eventStream = new EventStream();
1625
1838
  const state = { finalized: false };
@@ -1627,6 +1840,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1627
1840
  const dispatchedIds = /* @__PURE__ */ new Set();
1628
1841
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1629
1842
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1843
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
1630
1844
  const phases = [];
1631
1845
  let currentParallel = [];
1632
1846
  for (const toolCall of toolCalls) {
@@ -1647,40 +1861,18 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1647
1861
  void (async () => {
1648
1862
  try {
1649
1863
  for (const phase of phases) {
1650
- if (options.signal?.aborted) break;
1864
+ if (options.signal?.aborted || options.preemptSignal?.aborted) break;
1651
1865
  if (phase.sequential) {
1652
1866
  const signature = JSON.stringify([
1653
1867
  phase.sequential.name,
1654
1868
  canonicalToolArgs(phase.sequential.args)
1655
1869
  ]);
1656
1870
  if (!options.seenToolCalls.has(signature)) options.seenToolCalls.clear();
1657
- dispatchedIds.add(phase.sequential.id);
1658
- const record = await executeSingleToolCall(
1659
- phase.sequential,
1660
- options,
1661
- (event) => pushToolEvent(eventStream, state, event)
1662
- );
1663
- resultsById.set(record.toolCallId, record);
1871
+ await tracker.run(phase.sequential);
1664
1872
  } else if (phase.parallel.length === 1) {
1665
- dispatchedIds.add(phase.parallel[0].id);
1666
- const record = await executeSingleToolCall(
1667
- phase.parallel[0],
1668
- options,
1669
- (event) => pushToolEvent(eventStream, state, event)
1670
- );
1671
- resultsById.set(record.toolCallId, record);
1873
+ await tracker.run(phase.parallel[0]);
1672
1874
  } else {
1673
- await Promise.all(
1674
- phase.parallel.map(async (toolCall) => {
1675
- dispatchedIds.add(toolCall.id);
1676
- const record = await executeSingleToolCall(
1677
- toolCall,
1678
- options,
1679
- (event) => pushToolEvent(eventStream, state, event)
1680
- );
1681
- resultsById.set(record.toolCallId, record);
1682
- })
1683
- );
1875
+ await Promise.all(phase.parallel.map((toolCall) => tracker.run(toolCall)));
1684
1876
  }
1685
1877
  }
1686
1878
  if (!state.finalized) eventStream.close();
@@ -1701,12 +1893,13 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1701
1893
  }
1702
1894
  } finally {
1703
1895
  options.signal?.removeEventListener("abort", abortHandler);
1896
+ tracker.dispose();
1704
1897
  state.finalized = true;
1705
1898
  }
1706
1899
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1707
1900
  capToolResults(toolResults, options.maxToolResultChars);
1708
1901
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1709
- return { toolResults, aborted };
1902
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1710
1903
  }
1711
1904
  async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
1712
1905
  const eventStream = new EventStream();
@@ -1715,17 +1908,8 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1715
1908
  const dispatchedIds = /* @__PURE__ */ new Set();
1716
1909
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1717
1910
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1718
- Promise.all(
1719
- toolCalls.map(async (toolCall) => {
1720
- dispatchedIds.add(toolCall.id);
1721
- const record = await executeSingleToolCall(
1722
- toolCall,
1723
- options,
1724
- (event) => pushToolEvent(eventStream, state, event)
1725
- );
1726
- resultsById.set(record.toolCallId, record);
1727
- })
1728
- ).then(() => {
1911
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
1912
+ Promise.all(toolCalls.map((toolCall) => tracker.run(toolCall))).then(() => {
1729
1913
  if (!state.finalized) eventStream.close();
1730
1914
  }).catch((err) => {
1731
1915
  if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
@@ -1743,12 +1927,13 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1743
1927
  }
1744
1928
  } finally {
1745
1929
  options.signal?.removeEventListener("abort", abortHandler);
1930
+ tracker.dispose();
1746
1931
  state.finalized = true;
1747
1932
  }
1748
1933
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1749
1934
  capToolResults(toolResults, options.maxToolResultChars);
1750
1935
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1751
- return { toolResults, aborted };
1936
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1752
1937
  }
1753
1938
  function cancelledBeforeStartText(name) {
1754
1939
  return `\`${name}\` was cancelled before it started, so it had no effect. Safe to retry.`;
@@ -2044,6 +2229,7 @@ var Agent = class {
2044
2229
  options;
2045
2230
  steeringQueue = [];
2046
2231
  followUpQueue = [];
2232
+ steeringListeners = /* @__PURE__ */ new Set();
2047
2233
  constructor(options) {
2048
2234
  this.options = options;
2049
2235
  if (options.system) {
@@ -2068,9 +2254,14 @@ var Agent = class {
2068
2254
  get running() {
2069
2255
  return this._running;
2070
2256
  }
2071
- /** Queue a steering message for injection after current tool execution completes. */
2257
+ /**
2258
+ * Queue a steering message. Instant interrupt: if tools are running, the
2259
+ * interruptible ones are preempted right away and the message is injected
2260
+ * before the next model call (the turn continues — unlike abort/Stop).
2261
+ */
2072
2262
  steer(msg) {
2073
2263
  this.steeringQueue.push(msg);
2264
+ for (const listener of [...this.steeringListeners]) listener();
2074
2265
  }
2075
2266
  /** Queue a follow-up message for injection when the agent would otherwise stop. */
2076
2267
  followUp(msg) {
@@ -2090,6 +2281,14 @@ var Agent = class {
2090
2281
  const all = [...callerResult ?? [], ...queued];
2091
2282
  return all.length > 0 ? all : null;
2092
2283
  },
2284
+ onSteeringAvailable: (listener) => {
2285
+ this.steeringListeners.add(listener);
2286
+ const unsubscribeCaller = this.options.onSteeringAvailable?.(listener);
2287
+ return () => {
2288
+ this.steeringListeners.delete(listener);
2289
+ unsubscribeCaller?.();
2290
+ };
2291
+ },
2093
2292
  getFollowUpMessages: async () => {
2094
2293
  const callerResult = await this.options.getFollowUpMessages?.() ?? [];
2095
2294
  const queued = this.followUpQueue.splice(0);