@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.cjs CHANGED
@@ -41,11 +41,11 @@ __export(index_exports, {
41
41
  module.exports = __toCommonJS(index_exports);
42
42
 
43
43
  // src/agent.ts
44
- var import_gg_ai2 = require("@kenkaiiii/gg-ai");
44
+ var import_gg_ai3 = require("@kenkaiiii/gg-ai");
45
45
 
46
46
  // src/agent-loop.ts
47
47
  var import_zod = require("zod");
48
- var import_gg_ai = require("@kenkaiiii/gg-ai");
48
+ var import_gg_ai2 = require("@kenkaiiii/gg-ai");
49
49
 
50
50
  // src/local-backend.ts
51
51
  function isLocalBackendUrl(baseUrl) {
@@ -69,6 +69,107 @@ function isLocalBackendUrl(baseUrl) {
69
69
  return false;
70
70
  }
71
71
 
72
+ // src/tool-call-hints.ts
73
+ var import_gg_ai = require("@kenkaiiii/gg-ai");
74
+ function normalizeName(name) {
75
+ return name.toLowerCase().replace(/[^a-z0-9]/g, "");
76
+ }
77
+ function editDistance(a, b) {
78
+ let beforePrevious = [];
79
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
80
+ for (let i = 1; i <= a.length; i++) {
81
+ const current = [i];
82
+ for (let j = 1; j <= b.length; j++) {
83
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
84
+ let distance = Math.min(
85
+ (previous[j] ?? 0) + 1,
86
+ (current[j - 1] ?? 0) + 1,
87
+ (previous[j - 1] ?? 0) + cost
88
+ );
89
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
90
+ distance = Math.min(distance, (beforePrevious[j - 2] ?? 0) + 1);
91
+ }
92
+ current[j] = distance;
93
+ }
94
+ beforePrevious = previous;
95
+ previous = current;
96
+ }
97
+ return previous[b.length] ?? 0;
98
+ }
99
+ function closestName(name, candidates) {
100
+ const target = normalizeName(name);
101
+ if (!target) return void 0;
102
+ const limit = Math.min(3, Math.max(1, Math.floor(target.length / 3)));
103
+ let best;
104
+ for (const candidate of [...candidates].sort()) {
105
+ if (candidate === name) continue;
106
+ const distance = editDistance(target, normalizeName(candidate));
107
+ if (distance > limit) continue;
108
+ if (!best || distance < best.distance) best = { name: candidate, distance };
109
+ }
110
+ return best?.name;
111
+ }
112
+ function unknownToolMessage(name, available) {
113
+ const suggestion = closestName(name, available);
114
+ if (suggestion) return `Unknown tool: ${name}. Did you mean \`${suggestion}\`?`;
115
+ const names = [...available].sort();
116
+ const listed = names.length > 40 ? `${names.slice(0, 40).join(", ")}, \u2026` : names.join(", ");
117
+ const searchHint = available.includes("tool_search") ? " If the capability is not listed, call `tool_search` to load it." : "";
118
+ return `Unknown tool: ${name}. Available tools: ${listed}.${searchHint}`;
119
+ }
120
+ function isRecord(value) {
121
+ return typeof value === "object" && value !== null && !Array.isArray(value);
122
+ }
123
+ function typeLabel(node) {
124
+ if (!isRecord(node)) return "any";
125
+ if (Array.isArray(node.enum)) {
126
+ return node.enum.map((value) => JSON.stringify(value)).join(" | ");
127
+ }
128
+ const variants = node.anyOf ?? node.oneOf;
129
+ if (Array.isArray(variants)) {
130
+ return [...new Set(variants.map(typeLabel))].filter((t) => t !== "null").join(" | ") || "any";
131
+ }
132
+ if (node.type === "array") return `${typeLabel(node.items)}[]`;
133
+ if (Array.isArray(node.type)) {
134
+ return node.type.filter((t) => t !== "null").join(" | ") || "any";
135
+ }
136
+ return typeof node.type === "string" ? node.type : "any";
137
+ }
138
+ function objectShape(tool) {
139
+ let schema;
140
+ try {
141
+ schema = (0, import_gg_ai.resolveToolSchema)(tool);
142
+ } catch {
143
+ return void 0;
144
+ }
145
+ if (!isRecord(schema) || !isRecord(schema.properties)) return void 0;
146
+ const required = new Set(
147
+ Array.isArray(schema.required) ? schema.required.filter((r) => typeof r === "string") : []
148
+ );
149
+ return { properties: schema.properties, required };
150
+ }
151
+ function argumentHints(tool, args) {
152
+ const shape = objectShape(tool);
153
+ if (!shape) return "";
154
+ const names = Object.keys(shape.properties);
155
+ if (names.length === 0) return `Expected arguments: none.`;
156
+ const fields = names.map((name) => {
157
+ const optional = shape.required.has(name) ? "" : "?";
158
+ return `${name}${optional}: ${typeLabel(shape.properties[name])}`;
159
+ });
160
+ const lines = [`Expected arguments: { ${fields.join(", ")} }`];
161
+ if (isRecord(args)) {
162
+ for (const key of Object.keys(args).sort()) {
163
+ if (key in shape.properties) continue;
164
+ const suggestion = closestName(key, names);
165
+ lines.push(
166
+ suggestion ? `Unknown field \`${key}\` \u2014 did you mean \`${suggestion}\`?` : `Unknown field \`${key}\` is not a parameter of this tool.`
167
+ );
168
+ }
169
+ }
170
+ return lines.join("\n");
171
+ }
172
+
72
173
  // src/output-ceiling.ts
73
174
  var TTL_MS = 24 * 60 * 60 * 1e3;
74
175
  var MIN_PLAUSIBLE_CEILING = 256;
@@ -253,9 +354,32 @@ ${rule.reminder.trim()}
253
354
  // src/agent-loop.ts
254
355
  var DEFAULT_MAX_TURNS = 300;
255
356
  var DEFAULT_TOOL_TIMEOUT_MS = 3e5;
357
+ var PREEMPT_GRACE_MS = 500;
358
+ var STEER_INTERRUPTED_TEXT = "Interrupted: the user sent a new message.";
359
+ var ATOMIC_TOOL_NAMES = /* @__PURE__ */ new Set(["edit", "write", "multi_edit", "apply_patch", "notebook_edit"]);
360
+ function isInterruptibleTool(tool, name) {
361
+ if (tool?.interruptible !== void 0) return tool.interruptible;
362
+ return !ATOMIC_TOOL_NAMES.has(name);
363
+ }
364
+ function markSteerInterrupted(record) {
365
+ const content = record.content;
366
+ let next;
367
+ if (typeof content === "string") {
368
+ next = content.trim() ? `${STEER_INTERRUPTED_TEXT}
369
+ Partial output:
370
+ ${content}` : STEER_INTERRUPTED_TEXT;
371
+ } else {
372
+ next = [
373
+ { type: "text", text: `${STEER_INTERRUPTED_TEXT}
374
+ Partial output:` },
375
+ ...content
376
+ ];
377
+ }
378
+ return { toolCallId: record.toolCallId, content: next, isError: true };
379
+ }
256
380
  var _toolRedaction;
257
381
  function toolRedactionOptions() {
258
- _toolRedaction ??= { secrets: (0, import_gg_ai.environmentSecrets)(process.env) };
382
+ _toolRedaction ??= { secrets: (0, import_gg_ai2.environmentSecrets)(process.env) };
259
383
  return _toolRedaction;
260
384
  }
261
385
  var _diagFn = null;
@@ -338,7 +462,7 @@ function isBillingError(err) {
338
462
  if (!(err instanceof Error)) return false;
339
463
  const statusCode = err.statusCode;
340
464
  if (statusCode === 402) return true;
341
- return (0, import_gg_ai.isHardBillingMessage)(err.message);
465
+ return (0, import_gg_ai2.isHardBillingMessage)(err.message);
342
466
  }
343
467
  function isUsageLimitError(err) {
344
468
  if (!(err instanceof Error)) return false;
@@ -707,7 +831,7 @@ async function* agentLoop(messages, options) {
707
831
  });
708
832
  }
709
833
  }
710
- const result = (0, import_gg_ai.stream)({
834
+ const result = (0, import_gg_ai2.stream)({
711
835
  provider: options.provider,
712
836
  model: options.model,
713
837
  messages,
@@ -1170,7 +1294,7 @@ async function* agentLoop(messages, options) {
1170
1294
  });
1171
1295
  yield {
1172
1296
  type: "error",
1173
- error: new import_gg_ai.GGAIError(
1297
+ error: new import_gg_ai2.GGAIError(
1174
1298
  `The connection to the API provider stopped responding after ${MAX_STALL_RETRIES} automatic retries. Your conversation is preserved.`,
1175
1299
  {
1176
1300
  source: "network",
@@ -1403,10 +1527,14 @@ async function* agentLoop(messages, options) {
1403
1527
  fatalToolArgumentRecoverable = recoverable;
1404
1528
  fatalToolArgumentToolName = toolName;
1405
1529
  };
1530
+ const preempt = new AbortController();
1531
+ const unsubscribeSteering = options.onSteeringAvailable?.(() => preempt.abort());
1406
1532
  const executionOptions = {
1407
1533
  signal: options.signal,
1534
+ preemptSignal: options.onSteeringAvailable ? preempt.signal : void 0,
1408
1535
  maxToolResultChars: options.maxToolResultChars,
1409
1536
  maxTurnToolResultChars: options.maxTurnToolResultChars,
1537
+ transformToolResult: options.transformToolResult,
1410
1538
  toolMap,
1411
1539
  invalidToolArgumentCounts,
1412
1540
  markFatalToolArgumentError,
@@ -1415,7 +1543,15 @@ async function* agentLoop(messages, options) {
1415
1543
  const hasSequentialToolCall = toolCalls.some(
1416
1544
  (toolCall) => toolMap.get(toolCall.name)?.executionMode === "sequential"
1417
1545
  );
1418
- const executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
1546
+ let executionResult;
1547
+ try {
1548
+ executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
1549
+ } finally {
1550
+ unsubscribeSteering?.();
1551
+ }
1552
+ if (executionResult.preempted) {
1553
+ diag("steer_preempted_tools", { turn, provider: options.provider, model: options.model });
1554
+ }
1419
1555
  messages.push({ role: "tool", content: executionResult.toolResults });
1420
1556
  yield { type: "checkpoint", turn };
1421
1557
  const toolsAborted = executionResult.aborted;
@@ -1572,7 +1708,7 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1572
1708
  options.seenToolCalls.add(signature);
1573
1709
  }
1574
1710
  if (!tool) {
1575
- resultContent = `Unknown tool: ${toolCall.name}`;
1711
+ resultContent = unknownToolMessage(toolCall.name, [...options.toolMap.keys()]);
1576
1712
  isError = true;
1577
1713
  } else {
1578
1714
  try {
@@ -1589,8 +1725,12 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1589
1725
  }
1590
1726
  const callerSignal = options.signal;
1591
1727
  const toolTimeout = AbortSignal.timeout(tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS);
1728
+ const preemptSignal = isInterruptibleTool(tool, toolCall.name) ? options.preemptSignal : void 0;
1729
+ const toolSignals = [callerSignal, toolTimeout, preemptSignal].filter(
1730
+ (s) => s !== void 0
1731
+ );
1592
1732
  const ctx = {
1593
- signal: callerSignal ? AbortSignal.any([callerSignal, toolTimeout]) : toolTimeout,
1733
+ signal: toolSignals.length === 1 ? toolTimeout : AbortSignal.any(toolSignals),
1594
1734
  toolCallId: toolCall.id,
1595
1735
  onUpdate: (update) => {
1596
1736
  pushEvent({
@@ -1602,8 +1742,17 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1602
1742
  };
1603
1743
  const raw = await tool.execute(parsed, ctx);
1604
1744
  const normalized = normalizeToolResult(raw);
1605
- resultContent = (0, import_gg_ai.redactValue)(normalized.content, toolRedactionOptions());
1606
- details = (0, import_gg_ai.redactValue)(normalized.details, toolRedactionOptions());
1745
+ resultContent = (0, import_gg_ai2.redactValue)(normalized.content, toolRedactionOptions());
1746
+ details = (0, import_gg_ai2.redactValue)(normalized.details, toolRedactionOptions());
1747
+ if (options.transformToolResult) {
1748
+ try {
1749
+ resultContent = options.transformToolResult(
1750
+ { name: toolCall.name, args: toolCall.args },
1751
+ resultContent
1752
+ );
1753
+ } catch {
1754
+ }
1755
+ }
1607
1756
  for (const key of options.invalidToolArgumentCounts.keys()) {
1608
1757
  if (key.startsWith(`${toolCall.name}:`)) options.invalidToolArgumentCounts.delete(key);
1609
1758
  }
@@ -1615,12 +1764,14 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1615
1764
  const failureCount = (options.invalidToolArgumentCounts.get(failureKey) ?? 0) + 1;
1616
1765
  options.invalidToolArgumentCounts.set(failureKey, failureCount);
1617
1766
  invalidArgAttempt = failureCount;
1767
+ const hints = argumentHints(tool, toolCall.args);
1618
1768
  resultContent = `Invalid arguments for tool \`${toolCall.name}\`:
1619
- ` + prettyError + "\nRe-issue the call with each field as the correct type.";
1769
+ ` + prettyError + (hints ? `
1770
+ ${hints}` : "") + "\nRe-issue the call with each field as the correct type.";
1620
1771
  if (failureCount >= 3) {
1621
1772
  const recoverable = Object.keys(toolCall.args ?? {}).length === 0;
1622
1773
  options.markFatalToolArgumentError(
1623
- new import_gg_ai.GGAIError(
1774
+ new import_gg_ai2.GGAIError(
1624
1775
  `The model repeatedly issued invalid arguments for tool \`${toolCall.name}\`. This is usually an upstream model/tool-calling bug` + (recoverable ? " (the provider's stream returned empty tool-call arguments)" : "") + `. Your conversation is preserved; send another message or switch models to continue.`,
1625
1776
  {
1626
1777
  source: "provider",
@@ -1632,15 +1783,24 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1632
1783
  );
1633
1784
  }
1634
1785
  } else {
1635
- resultContent = (0, import_gg_ai.redactValue)(
1786
+ resultContent = (0, import_gg_ai2.redactValue)(
1636
1787
  err instanceof Error ? err.message : String(err),
1637
1788
  toolRedactionOptions()
1638
1789
  );
1639
1790
  }
1640
1791
  }
1641
1792
  }
1642
- resultContent = (0, import_gg_ai.redactValue)(resultContent, toolRedactionOptions());
1643
- details = (0, import_gg_ai.redactValue)(details, toolRedactionOptions());
1793
+ resultContent = (0, import_gg_ai2.redactValue)(resultContent, toolRedactionOptions());
1794
+ details = (0, import_gg_ai2.redactValue)(details, toolRedactionOptions());
1795
+ if (options.preemptSignal?.aborted && isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name)) {
1796
+ const marked = markSteerInterrupted({
1797
+ toolCallId: toolCall.id,
1798
+ content: resultContent,
1799
+ isError
1800
+ });
1801
+ resultContent = marked.content;
1802
+ isError = true;
1803
+ }
1644
1804
  const durationMs = Date.now() - startTime;
1645
1805
  pushEvent({
1646
1806
  type: "tool_call_end",
@@ -1653,13 +1813,67 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1653
1813
  });
1654
1814
  return { toolCallId: toolCall.id, content: resultContent, isError };
1655
1815
  }
1816
+ function createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds) {
1817
+ const running = /* @__PURE__ */ new Map();
1818
+ let graceTimer;
1819
+ const preemptSignal = options.preemptSignal;
1820
+ const abandon = () => {
1821
+ graceTimer = void 0;
1822
+ for (const [id, entry] of running) {
1823
+ if (!entry.interruptible || resultsById.has(id)) continue;
1824
+ resultsById.set(id, { toolCallId: id, content: STEER_INTERRUPTED_TEXT, isError: true });
1825
+ pushToolEvent(eventStream, state, {
1826
+ type: "tool_call_end",
1827
+ toolCallId: id,
1828
+ result: STEER_INTERRUPTED_TEXT,
1829
+ isError: true,
1830
+ durationMs: Date.now() - entry.startedAt
1831
+ });
1832
+ running.delete(id);
1833
+ }
1834
+ if (running.size === 0) eventStream.close();
1835
+ };
1836
+ const onPreempt = () => {
1837
+ if (graceTimer === void 0) graceTimer = setTimeout(abandon, PREEMPT_GRACE_MS);
1838
+ };
1839
+ preemptSignal?.addEventListener("abort", onPreempt, { once: true });
1840
+ return {
1841
+ async run(toolCall) {
1842
+ if (options.preemptSignal?.aborted) return;
1843
+ dispatchedIds.add(toolCall.id);
1844
+ const interruptible = isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name);
1845
+ running.set(toolCall.id, { toolCall, interruptible, startedAt: Date.now() });
1846
+ const record = await executeSingleToolCall(
1847
+ toolCall,
1848
+ options,
1849
+ (event) => pushToolEvent(eventStream, state, event)
1850
+ );
1851
+ running.delete(toolCall.id);
1852
+ if (resultsById.has(toolCall.id)) return;
1853
+ 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);
1854
+ resultsById.set(
1855
+ toolCall.id,
1856
+ interruptible && preemptSignal?.aborted && !alreadyMarked ? markSteerInterrupted(record) : record
1857
+ );
1858
+ if (preemptSignal?.aborted && graceTimer === void 0 && running.size === 0) {
1859
+ eventStream.close();
1860
+ }
1861
+ },
1862
+ preempted: () => preemptSignal?.aborted === true,
1863
+ dispose: () => {
1864
+ if (graceTimer !== void 0) clearTimeout(graceTimer);
1865
+ preemptSignal?.removeEventListener("abort", onPreempt);
1866
+ }
1867
+ };
1868
+ }
1656
1869
  async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1657
- const eventStream = new import_gg_ai.EventStream();
1870
+ const eventStream = new import_gg_ai2.EventStream();
1658
1871
  const state = { finalized: false };
1659
1872
  const resultsById = /* @__PURE__ */ new Map();
1660
1873
  const dispatchedIds = /* @__PURE__ */ new Set();
1661
1874
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1662
1875
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1876
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
1663
1877
  const phases = [];
1664
1878
  let currentParallel = [];
1665
1879
  for (const toolCall of toolCalls) {
@@ -1680,40 +1894,18 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1680
1894
  void (async () => {
1681
1895
  try {
1682
1896
  for (const phase of phases) {
1683
- if (options.signal?.aborted) break;
1897
+ if (options.signal?.aborted || options.preemptSignal?.aborted) break;
1684
1898
  if (phase.sequential) {
1685
1899
  const signature = JSON.stringify([
1686
1900
  phase.sequential.name,
1687
1901
  canonicalToolArgs(phase.sequential.args)
1688
1902
  ]);
1689
1903
  if (!options.seenToolCalls.has(signature)) options.seenToolCalls.clear();
1690
- dispatchedIds.add(phase.sequential.id);
1691
- const record = await executeSingleToolCall(
1692
- phase.sequential,
1693
- options,
1694
- (event) => pushToolEvent(eventStream, state, event)
1695
- );
1696
- resultsById.set(record.toolCallId, record);
1904
+ await tracker.run(phase.sequential);
1697
1905
  } else if (phase.parallel.length === 1) {
1698
- dispatchedIds.add(phase.parallel[0].id);
1699
- const record = await executeSingleToolCall(
1700
- phase.parallel[0],
1701
- options,
1702
- (event) => pushToolEvent(eventStream, state, event)
1703
- );
1704
- resultsById.set(record.toolCallId, record);
1906
+ await tracker.run(phase.parallel[0]);
1705
1907
  } else {
1706
- await Promise.all(
1707
- phase.parallel.map(async (toolCall) => {
1708
- dispatchedIds.add(toolCall.id);
1709
- const record = await executeSingleToolCall(
1710
- toolCall,
1711
- options,
1712
- (event) => pushToolEvent(eventStream, state, event)
1713
- );
1714
- resultsById.set(record.toolCallId, record);
1715
- })
1716
- );
1908
+ await Promise.all(phase.parallel.map((toolCall) => tracker.run(toolCall)));
1717
1909
  }
1718
1910
  }
1719
1911
  if (!state.finalized) eventStream.close();
@@ -1734,31 +1926,23 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1734
1926
  }
1735
1927
  } finally {
1736
1928
  options.signal?.removeEventListener("abort", abortHandler);
1929
+ tracker.dispose();
1737
1930
  state.finalized = true;
1738
1931
  }
1739
1932
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1740
1933
  capToolResults(toolResults, options.maxToolResultChars);
1741
1934
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1742
- return { toolResults, aborted };
1935
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1743
1936
  }
1744
1937
  async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
1745
- const eventStream = new import_gg_ai.EventStream();
1938
+ const eventStream = new import_gg_ai2.EventStream();
1746
1939
  const state = { finalized: false };
1747
1940
  const resultsById = /* @__PURE__ */ new Map();
1748
1941
  const dispatchedIds = /* @__PURE__ */ new Set();
1749
1942
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1750
1943
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1751
- Promise.all(
1752
- toolCalls.map(async (toolCall) => {
1753
- dispatchedIds.add(toolCall.id);
1754
- const record = await executeSingleToolCall(
1755
- toolCall,
1756
- options,
1757
- (event) => pushToolEvent(eventStream, state, event)
1758
- );
1759
- resultsById.set(record.toolCallId, record);
1760
- })
1761
- ).then(() => {
1944
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
1945
+ Promise.all(toolCalls.map((toolCall) => tracker.run(toolCall))).then(() => {
1762
1946
  if (!state.finalized) eventStream.close();
1763
1947
  }).catch((err) => {
1764
1948
  if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
@@ -1776,12 +1960,13 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1776
1960
  }
1777
1961
  } finally {
1778
1962
  options.signal?.removeEventListener("abort", abortHandler);
1963
+ tracker.dispose();
1779
1964
  state.finalized = true;
1780
1965
  }
1781
1966
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1782
1967
  capToolResults(toolResults, options.maxToolResultChars);
1783
1968
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1784
- return { toolResults, aborted };
1969
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1785
1970
  }
1786
1971
  function cancelledBeforeStartText(name) {
1787
1972
  return `\`${name}\` was cancelled before it started, so it had no effect. Safe to retry.`;
@@ -1821,8 +2006,8 @@ function capToolResults(toolResults, maxToolResultChars) {
1821
2006
  const originalChars = toolResult.content.length;
1822
2007
  const headChars = Math.floor(max * 0.7);
1823
2008
  const tailChars = max - headChars;
1824
- const head = (0, import_gg_ai.sliceHead)(toolResult.content, headChars);
1825
- const tail = (0, import_gg_ai.sliceTail)(toolResult.content, tailChars);
2009
+ const head = (0, import_gg_ai2.sliceHead)(toolResult.content, headChars);
2010
+ const tail = (0, import_gg_ai2.sliceTail)(toolResult.content, tailChars);
1826
2011
  const omitted = originalChars - head.length - tail.length;
1827
2012
  toolResult.content = head + `
1828
2013
 
@@ -1858,11 +2043,11 @@ function capTurnToolResults(toolResults, maxTurnToolResultChars) {
1858
2043
  const headChars = Math.floor(fairShare * 0.7);
1859
2044
  const tailChars = fairShare - headChars;
1860
2045
  const omitted = originalChars - fairShare;
1861
- toolResult.content = (0, import_gg_ai.sliceHead)(toolResult.content, headChars) + `
2046
+ toolResult.content = (0, import_gg_ai2.sliceHead)(toolResult.content, headChars) + `
1862
2047
 
1863
2048
  [... ${omitted} characters trimmed: this turn's combined tool results exceeded the per-turn budget. Re-run this call alone with narrower filters or offset/limit if you need the omitted content ...]
1864
2049
 
1865
- ` + (0, import_gg_ai.sliceTail)(toolResult.content, tailChars);
2050
+ ` + (0, import_gg_ai2.sliceTail)(toolResult.content, tailChars);
1866
2051
  toolResult.capped = {
1867
2052
  originalChars: toolResult.capped?.originalChars ?? originalChars,
1868
2053
  keptChars: toolResult.content.length,
@@ -1881,8 +2066,8 @@ function truncateToolResultText(text, maxChars) {
1881
2066
  if (text.length <= maxChars) return text;
1882
2067
  const tailChars = Math.min(Math.floor(maxChars * 0.3), 2e4);
1883
2068
  const headChars = Math.max(maxChars - tailChars, 0);
1884
- const head = (0, import_gg_ai.sliceHead)(text, headChars);
1885
- const tail = (0, import_gg_ai.sliceTail)(text, tailChars);
2069
+ const head = (0, import_gg_ai2.sliceHead)(text, headChars);
2070
+ const tail = (0, import_gg_ai2.sliceTail)(text, tailChars);
1886
2071
  const omitted = text.length - head.length - tail.length;
1887
2072
  return `${head}
1888
2073
 
@@ -2031,7 +2216,7 @@ var AgentStream = class {
2031
2216
  rejectResult;
2032
2217
  hasConsumer = false;
2033
2218
  constructor(generator, onDone) {
2034
- this.events = new import_gg_ai2.EventStream();
2219
+ this.events = new import_gg_ai3.EventStream();
2035
2220
  this.resultPromise = new Promise((resolve, reject) => {
2036
2221
  this.resolveResult = resolve;
2037
2222
  this.rejectResult = reject;
@@ -2077,6 +2262,7 @@ var Agent = class {
2077
2262
  options;
2078
2263
  steeringQueue = [];
2079
2264
  followUpQueue = [];
2265
+ steeringListeners = /* @__PURE__ */ new Set();
2080
2266
  constructor(options) {
2081
2267
  this.options = options;
2082
2268
  if (options.system) {
@@ -2101,9 +2287,14 @@ var Agent = class {
2101
2287
  get running() {
2102
2288
  return this._running;
2103
2289
  }
2104
- /** Queue a steering message for injection after current tool execution completes. */
2290
+ /**
2291
+ * Queue a steering message. Instant interrupt: if tools are running, the
2292
+ * interruptible ones are preempted right away and the message is injected
2293
+ * before the next model call (the turn continues — unlike abort/Stop).
2294
+ */
2105
2295
  steer(msg) {
2106
2296
  this.steeringQueue.push(msg);
2297
+ for (const listener of [...this.steeringListeners]) listener();
2107
2298
  }
2108
2299
  /** Queue a follow-up message for injection when the agent would otherwise stop. */
2109
2300
  followUp(msg) {
@@ -2123,6 +2314,14 @@ var Agent = class {
2123
2314
  const all = [...callerResult ?? [], ...queued];
2124
2315
  return all.length > 0 ? all : null;
2125
2316
  },
2317
+ onSteeringAvailable: (listener) => {
2318
+ this.steeringListeners.add(listener);
2319
+ const unsubscribeCaller = this.options.onSteeringAvailable?.(listener);
2320
+ return () => {
2321
+ this.steeringListeners.delete(listener);
2322
+ unsubscribeCaller?.();
2323
+ };
2324
+ },
2126
2325
  getFollowUpMessages: async () => {
2127
2326
  const callerResult = await this.options.getFollowUpMessages?.() ?? [];
2128
2327
  const queued = this.followUpQueue.splice(0);