@kenkaiiii/gg-agent 5.68.1 → 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.cjs CHANGED
@@ -253,6 +253,29 @@ ${rule.reminder.trim()}
253
253
  // src/agent-loop.ts
254
254
  var DEFAULT_MAX_TURNS = 300;
255
255
  var DEFAULT_TOOL_TIMEOUT_MS = 3e5;
256
+ var PREEMPT_GRACE_MS = 500;
257
+ var STEER_INTERRUPTED_TEXT = "Interrupted: the user sent a new message.";
258
+ var ATOMIC_TOOL_NAMES = /* @__PURE__ */ new Set(["edit", "write", "multi_edit", "apply_patch", "notebook_edit"]);
259
+ function isInterruptibleTool(tool, name) {
260
+ if (tool?.interruptible !== void 0) return tool.interruptible;
261
+ return !ATOMIC_TOOL_NAMES.has(name);
262
+ }
263
+ function markSteerInterrupted(record) {
264
+ const content = record.content;
265
+ let next;
266
+ if (typeof content === "string") {
267
+ next = content.trim() ? `${STEER_INTERRUPTED_TEXT}
268
+ Partial output:
269
+ ${content}` : STEER_INTERRUPTED_TEXT;
270
+ } else {
271
+ next = [
272
+ { type: "text", text: `${STEER_INTERRUPTED_TEXT}
273
+ Partial output:` },
274
+ ...content
275
+ ];
276
+ }
277
+ return { toolCallId: record.toolCallId, content: next, isError: true };
278
+ }
256
279
  var _toolRedaction;
257
280
  function toolRedactionOptions() {
258
281
  _toolRedaction ??= { secrets: (0, import_gg_ai.environmentSecrets)(process.env) };
@@ -1403,10 +1426,14 @@ async function* agentLoop(messages, options) {
1403
1426
  fatalToolArgumentRecoverable = recoverable;
1404
1427
  fatalToolArgumentToolName = toolName;
1405
1428
  };
1429
+ const preempt = new AbortController();
1430
+ const unsubscribeSteering = options.onSteeringAvailable?.(() => preempt.abort());
1406
1431
  const executionOptions = {
1407
1432
  signal: options.signal,
1433
+ preemptSignal: options.onSteeringAvailable ? preempt.signal : void 0,
1408
1434
  maxToolResultChars: options.maxToolResultChars,
1409
1435
  maxTurnToolResultChars: options.maxTurnToolResultChars,
1436
+ transformToolResult: options.transformToolResult,
1410
1437
  toolMap,
1411
1438
  invalidToolArgumentCounts,
1412
1439
  markFatalToolArgumentError,
@@ -1415,7 +1442,15 @@ async function* agentLoop(messages, options) {
1415
1442
  const hasSequentialToolCall = toolCalls.some(
1416
1443
  (toolCall) => toolMap.get(toolCall.name)?.executionMode === "sequential"
1417
1444
  );
1418
- const executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
1445
+ let executionResult;
1446
+ try {
1447
+ executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
1448
+ } finally {
1449
+ unsubscribeSteering?.();
1450
+ }
1451
+ if (executionResult.preempted) {
1452
+ diag("steer_preempted_tools", { turn, provider: options.provider, model: options.model });
1453
+ }
1419
1454
  messages.push({ role: "tool", content: executionResult.toolResults });
1420
1455
  yield { type: "checkpoint", turn };
1421
1456
  const toolsAborted = executionResult.aborted;
@@ -1589,8 +1624,12 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1589
1624
  }
1590
1625
  const callerSignal = options.signal;
1591
1626
  const toolTimeout = AbortSignal.timeout(tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS);
1627
+ const preemptSignal = isInterruptibleTool(tool, toolCall.name) ? options.preemptSignal : void 0;
1628
+ const toolSignals = [callerSignal, toolTimeout, preemptSignal].filter(
1629
+ (s) => s !== void 0
1630
+ );
1592
1631
  const ctx = {
1593
- signal: callerSignal ? AbortSignal.any([callerSignal, toolTimeout]) : toolTimeout,
1632
+ signal: toolSignals.length === 1 ? toolTimeout : AbortSignal.any(toolSignals),
1594
1633
  toolCallId: toolCall.id,
1595
1634
  onUpdate: (update) => {
1596
1635
  pushEvent({
@@ -1604,6 +1643,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1604
1643
  const normalized = normalizeToolResult(raw);
1605
1644
  resultContent = (0, import_gg_ai.redactValue)(normalized.content, toolRedactionOptions());
1606
1645
  details = (0, import_gg_ai.redactValue)(normalized.details, toolRedactionOptions());
1646
+ if (options.transformToolResult) {
1647
+ try {
1648
+ resultContent = options.transformToolResult(
1649
+ { name: toolCall.name, args: toolCall.args },
1650
+ resultContent
1651
+ );
1652
+ } catch {
1653
+ }
1654
+ }
1607
1655
  for (const key of options.invalidToolArgumentCounts.keys()) {
1608
1656
  if (key.startsWith(`${toolCall.name}:`)) options.invalidToolArgumentCounts.delete(key);
1609
1657
  }
@@ -1641,6 +1689,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1641
1689
  }
1642
1690
  resultContent = (0, import_gg_ai.redactValue)(resultContent, toolRedactionOptions());
1643
1691
  details = (0, import_gg_ai.redactValue)(details, toolRedactionOptions());
1692
+ if (options.preemptSignal?.aborted && isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name)) {
1693
+ const marked = markSteerInterrupted({
1694
+ toolCallId: toolCall.id,
1695
+ content: resultContent,
1696
+ isError
1697
+ });
1698
+ resultContent = marked.content;
1699
+ isError = true;
1700
+ }
1644
1701
  const durationMs = Date.now() - startTime;
1645
1702
  pushEvent({
1646
1703
  type: "tool_call_end",
@@ -1653,6 +1710,59 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1653
1710
  });
1654
1711
  return { toolCallId: toolCall.id, content: resultContent, isError };
1655
1712
  }
1713
+ function createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds) {
1714
+ const running = /* @__PURE__ */ new Map();
1715
+ let graceTimer;
1716
+ const preemptSignal = options.preemptSignal;
1717
+ const abandon = () => {
1718
+ graceTimer = void 0;
1719
+ for (const [id, entry] of running) {
1720
+ if (!entry.interruptible || resultsById.has(id)) continue;
1721
+ resultsById.set(id, { toolCallId: id, content: STEER_INTERRUPTED_TEXT, isError: true });
1722
+ pushToolEvent(eventStream, state, {
1723
+ type: "tool_call_end",
1724
+ toolCallId: id,
1725
+ result: STEER_INTERRUPTED_TEXT,
1726
+ isError: true,
1727
+ durationMs: Date.now() - entry.startedAt
1728
+ });
1729
+ running.delete(id);
1730
+ }
1731
+ if (running.size === 0) eventStream.close();
1732
+ };
1733
+ const onPreempt = () => {
1734
+ if (graceTimer === void 0) graceTimer = setTimeout(abandon, PREEMPT_GRACE_MS);
1735
+ };
1736
+ preemptSignal?.addEventListener("abort", onPreempt, { once: true });
1737
+ return {
1738
+ async run(toolCall) {
1739
+ if (options.preemptSignal?.aborted) return;
1740
+ dispatchedIds.add(toolCall.id);
1741
+ const interruptible = isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name);
1742
+ running.set(toolCall.id, { toolCall, interruptible, startedAt: Date.now() });
1743
+ const record = await executeSingleToolCall(
1744
+ toolCall,
1745
+ options,
1746
+ (event) => pushToolEvent(eventStream, state, event)
1747
+ );
1748
+ running.delete(toolCall.id);
1749
+ if (resultsById.has(toolCall.id)) return;
1750
+ 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);
1751
+ resultsById.set(
1752
+ toolCall.id,
1753
+ interruptible && preemptSignal?.aborted && !alreadyMarked ? markSteerInterrupted(record) : record
1754
+ );
1755
+ if (preemptSignal?.aborted && graceTimer === void 0 && running.size === 0) {
1756
+ eventStream.close();
1757
+ }
1758
+ },
1759
+ preempted: () => preemptSignal?.aborted === true,
1760
+ dispose: () => {
1761
+ if (graceTimer !== void 0) clearTimeout(graceTimer);
1762
+ preemptSignal?.removeEventListener("abort", onPreempt);
1763
+ }
1764
+ };
1765
+ }
1656
1766
  async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1657
1767
  const eventStream = new import_gg_ai.EventStream();
1658
1768
  const state = { finalized: false };
@@ -1660,6 +1770,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1660
1770
  const dispatchedIds = /* @__PURE__ */ new Set();
1661
1771
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1662
1772
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1773
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
1663
1774
  const phases = [];
1664
1775
  let currentParallel = [];
1665
1776
  for (const toolCall of toolCalls) {
@@ -1680,40 +1791,18 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1680
1791
  void (async () => {
1681
1792
  try {
1682
1793
  for (const phase of phases) {
1683
- if (options.signal?.aborted) break;
1794
+ if (options.signal?.aborted || options.preemptSignal?.aborted) break;
1684
1795
  if (phase.sequential) {
1685
1796
  const signature = JSON.stringify([
1686
1797
  phase.sequential.name,
1687
1798
  canonicalToolArgs(phase.sequential.args)
1688
1799
  ]);
1689
1800
  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);
1801
+ await tracker.run(phase.sequential);
1697
1802
  } 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);
1803
+ await tracker.run(phase.parallel[0]);
1705
1804
  } 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
- );
1805
+ await Promise.all(phase.parallel.map((toolCall) => tracker.run(toolCall)));
1717
1806
  }
1718
1807
  }
1719
1808
  if (!state.finalized) eventStream.close();
@@ -1734,12 +1823,13 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1734
1823
  }
1735
1824
  } finally {
1736
1825
  options.signal?.removeEventListener("abort", abortHandler);
1826
+ tracker.dispose();
1737
1827
  state.finalized = true;
1738
1828
  }
1739
1829
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1740
1830
  capToolResults(toolResults, options.maxToolResultChars);
1741
1831
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1742
- return { toolResults, aborted };
1832
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1743
1833
  }
1744
1834
  async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
1745
1835
  const eventStream = new import_gg_ai.EventStream();
@@ -1748,17 +1838,8 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1748
1838
  const dispatchedIds = /* @__PURE__ */ new Set();
1749
1839
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1750
1840
  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(() => {
1841
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
1842
+ Promise.all(toolCalls.map((toolCall) => tracker.run(toolCall))).then(() => {
1762
1843
  if (!state.finalized) eventStream.close();
1763
1844
  }).catch((err) => {
1764
1845
  if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
@@ -1776,12 +1857,13 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1776
1857
  }
1777
1858
  } finally {
1778
1859
  options.signal?.removeEventListener("abort", abortHandler);
1860
+ tracker.dispose();
1779
1861
  state.finalized = true;
1780
1862
  }
1781
1863
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1782
1864
  capToolResults(toolResults, options.maxToolResultChars);
1783
1865
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1784
- return { toolResults, aborted };
1866
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1785
1867
  }
1786
1868
  function cancelledBeforeStartText(name) {
1787
1869
  return `\`${name}\` was cancelled before it started, so it had no effect. Safe to retry.`;
@@ -2077,6 +2159,7 @@ var Agent = class {
2077
2159
  options;
2078
2160
  steeringQueue = [];
2079
2161
  followUpQueue = [];
2162
+ steeringListeners = /* @__PURE__ */ new Set();
2080
2163
  constructor(options) {
2081
2164
  this.options = options;
2082
2165
  if (options.system) {
@@ -2101,9 +2184,14 @@ var Agent = class {
2101
2184
  get running() {
2102
2185
  return this._running;
2103
2186
  }
2104
- /** Queue a steering message for injection after current tool execution completes. */
2187
+ /**
2188
+ * Queue a steering message. Instant interrupt: if tools are running, the
2189
+ * interruptible ones are preempted right away and the message is injected
2190
+ * before the next model call (the turn continues — unlike abort/Stop).
2191
+ */
2105
2192
  steer(msg) {
2106
2193
  this.steeringQueue.push(msg);
2194
+ for (const listener of [...this.steeringListeners]) listener();
2107
2195
  }
2108
2196
  /** Queue a follow-up message for injection when the agent would otherwise stop. */
2109
2197
  followUp(msg) {
@@ -2123,6 +2211,14 @@ var Agent = class {
2123
2211
  const all = [...callerResult ?? [], ...queued];
2124
2212
  return all.length > 0 ? all : null;
2125
2213
  },
2214
+ onSteeringAvailable: (listener) => {
2215
+ this.steeringListeners.add(listener);
2216
+ const unsubscribeCaller = this.options.onSteeringAvailable?.(listener);
2217
+ return () => {
2218
+ this.steeringListeners.delete(listener);
2219
+ unsubscribeCaller?.();
2220
+ };
2221
+ },
2126
2222
  getFollowUpMessages: async () => {
2127
2223
  const callerResult = await this.options.getFollowUpMessages?.() ?? [];
2128
2224
  const queued = this.followUpQueue.splice(0);