@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.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) };
@@ -727,6 +750,7 @@ async function* agentLoop(messages, options) {
727
750
  transportSessionId: options.transportSessionId,
728
751
  projectId: liveProjectId,
729
752
  cacheRetention: options.cacheRetention,
753
+ onContextPrepared: options.onContextPrepared,
730
754
  promptCacheKey: options.promptCacheKey,
731
755
  serviceTier: options.serviceTier,
732
756
  supportsImages: options.supportsImages,
@@ -1402,10 +1426,14 @@ async function* agentLoop(messages, options) {
1402
1426
  fatalToolArgumentRecoverable = recoverable;
1403
1427
  fatalToolArgumentToolName = toolName;
1404
1428
  };
1429
+ const preempt = new AbortController();
1430
+ const unsubscribeSteering = options.onSteeringAvailable?.(() => preempt.abort());
1405
1431
  const executionOptions = {
1406
1432
  signal: options.signal,
1433
+ preemptSignal: options.onSteeringAvailable ? preempt.signal : void 0,
1407
1434
  maxToolResultChars: options.maxToolResultChars,
1408
1435
  maxTurnToolResultChars: options.maxTurnToolResultChars,
1436
+ transformToolResult: options.transformToolResult,
1409
1437
  toolMap,
1410
1438
  invalidToolArgumentCounts,
1411
1439
  markFatalToolArgumentError,
@@ -1414,7 +1442,15 @@ async function* agentLoop(messages, options) {
1414
1442
  const hasSequentialToolCall = toolCalls.some(
1415
1443
  (toolCall) => toolMap.get(toolCall.name)?.executionMode === "sequential"
1416
1444
  );
1417
- 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
+ }
1418
1454
  messages.push({ role: "tool", content: executionResult.toolResults });
1419
1455
  yield { type: "checkpoint", turn };
1420
1456
  const toolsAborted = executionResult.aborted;
@@ -1588,8 +1624,12 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1588
1624
  }
1589
1625
  const callerSignal = options.signal;
1590
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
+ );
1591
1631
  const ctx = {
1592
- signal: callerSignal ? AbortSignal.any([callerSignal, toolTimeout]) : toolTimeout,
1632
+ signal: toolSignals.length === 1 ? toolTimeout : AbortSignal.any(toolSignals),
1593
1633
  toolCallId: toolCall.id,
1594
1634
  onUpdate: (update) => {
1595
1635
  pushEvent({
@@ -1603,6 +1643,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1603
1643
  const normalized = normalizeToolResult(raw);
1604
1644
  resultContent = (0, import_gg_ai.redactValue)(normalized.content, toolRedactionOptions());
1605
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
+ }
1606
1655
  for (const key of options.invalidToolArgumentCounts.keys()) {
1607
1656
  if (key.startsWith(`${toolCall.name}:`)) options.invalidToolArgumentCounts.delete(key);
1608
1657
  }
@@ -1640,6 +1689,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1640
1689
  }
1641
1690
  resultContent = (0, import_gg_ai.redactValue)(resultContent, toolRedactionOptions());
1642
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
+ }
1643
1701
  const durationMs = Date.now() - startTime;
1644
1702
  pushEvent({
1645
1703
  type: "tool_call_end",
@@ -1652,6 +1710,59 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1652
1710
  });
1653
1711
  return { toolCallId: toolCall.id, content: resultContent, isError };
1654
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
+ }
1655
1766
  async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1656
1767
  const eventStream = new import_gg_ai.EventStream();
1657
1768
  const state = { finalized: false };
@@ -1659,6 +1770,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1659
1770
  const dispatchedIds = /* @__PURE__ */ new Set();
1660
1771
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1661
1772
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1773
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
1662
1774
  const phases = [];
1663
1775
  let currentParallel = [];
1664
1776
  for (const toolCall of toolCalls) {
@@ -1679,40 +1791,18 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1679
1791
  void (async () => {
1680
1792
  try {
1681
1793
  for (const phase of phases) {
1682
- if (options.signal?.aborted) break;
1794
+ if (options.signal?.aborted || options.preemptSignal?.aborted) break;
1683
1795
  if (phase.sequential) {
1684
1796
  const signature = JSON.stringify([
1685
1797
  phase.sequential.name,
1686
1798
  canonicalToolArgs(phase.sequential.args)
1687
1799
  ]);
1688
1800
  if (!options.seenToolCalls.has(signature)) options.seenToolCalls.clear();
1689
- dispatchedIds.add(phase.sequential.id);
1690
- const record = await executeSingleToolCall(
1691
- phase.sequential,
1692
- options,
1693
- (event) => pushToolEvent(eventStream, state, event)
1694
- );
1695
- resultsById.set(record.toolCallId, record);
1801
+ await tracker.run(phase.sequential);
1696
1802
  } else if (phase.parallel.length === 1) {
1697
- dispatchedIds.add(phase.parallel[0].id);
1698
- const record = await executeSingleToolCall(
1699
- phase.parallel[0],
1700
- options,
1701
- (event) => pushToolEvent(eventStream, state, event)
1702
- );
1703
- resultsById.set(record.toolCallId, record);
1803
+ await tracker.run(phase.parallel[0]);
1704
1804
  } else {
1705
- await Promise.all(
1706
- phase.parallel.map(async (toolCall) => {
1707
- dispatchedIds.add(toolCall.id);
1708
- const record = await executeSingleToolCall(
1709
- toolCall,
1710
- options,
1711
- (event) => pushToolEvent(eventStream, state, event)
1712
- );
1713
- resultsById.set(record.toolCallId, record);
1714
- })
1715
- );
1805
+ await Promise.all(phase.parallel.map((toolCall) => tracker.run(toolCall)));
1716
1806
  }
1717
1807
  }
1718
1808
  if (!state.finalized) eventStream.close();
@@ -1733,12 +1823,13 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1733
1823
  }
1734
1824
  } finally {
1735
1825
  options.signal?.removeEventListener("abort", abortHandler);
1826
+ tracker.dispose();
1736
1827
  state.finalized = true;
1737
1828
  }
1738
1829
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1739
1830
  capToolResults(toolResults, options.maxToolResultChars);
1740
1831
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1741
- return { toolResults, aborted };
1832
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1742
1833
  }
1743
1834
  async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
1744
1835
  const eventStream = new import_gg_ai.EventStream();
@@ -1747,17 +1838,8 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1747
1838
  const dispatchedIds = /* @__PURE__ */ new Set();
1748
1839
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1749
1840
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1750
- Promise.all(
1751
- toolCalls.map(async (toolCall) => {
1752
- dispatchedIds.add(toolCall.id);
1753
- const record = await executeSingleToolCall(
1754
- toolCall,
1755
- options,
1756
- (event) => pushToolEvent(eventStream, state, event)
1757
- );
1758
- resultsById.set(record.toolCallId, record);
1759
- })
1760
- ).then(() => {
1841
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
1842
+ Promise.all(toolCalls.map((toolCall) => tracker.run(toolCall))).then(() => {
1761
1843
  if (!state.finalized) eventStream.close();
1762
1844
  }).catch((err) => {
1763
1845
  if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
@@ -1775,12 +1857,13 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1775
1857
  }
1776
1858
  } finally {
1777
1859
  options.signal?.removeEventListener("abort", abortHandler);
1860
+ tracker.dispose();
1778
1861
  state.finalized = true;
1779
1862
  }
1780
1863
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1781
1864
  capToolResults(toolResults, options.maxToolResultChars);
1782
1865
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1783
- return { toolResults, aborted };
1866
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1784
1867
  }
1785
1868
  function cancelledBeforeStartText(name) {
1786
1869
  return `\`${name}\` was cancelled before it started, so it had no effect. Safe to retry.`;
@@ -2076,6 +2159,7 @@ var Agent = class {
2076
2159
  options;
2077
2160
  steeringQueue = [];
2078
2161
  followUpQueue = [];
2162
+ steeringListeners = /* @__PURE__ */ new Set();
2079
2163
  constructor(options) {
2080
2164
  this.options = options;
2081
2165
  if (options.system) {
@@ -2100,9 +2184,14 @@ var Agent = class {
2100
2184
  get running() {
2101
2185
  return this._running;
2102
2186
  }
2103
- /** 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
+ */
2104
2192
  steer(msg) {
2105
2193
  this.steeringQueue.push(msg);
2194
+ for (const listener of [...this.steeringListeners]) listener();
2106
2195
  }
2107
2196
  /** Queue a follow-up message for injection when the agent would otherwise stop. */
2108
2197
  followUp(msg) {
@@ -2122,6 +2211,14 @@ var Agent = class {
2122
2211
  const all = [...callerResult ?? [], ...queued];
2123
2212
  return all.length > 0 ? all : null;
2124
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
+ },
2125
2222
  getFollowUpMessages: async () => {
2126
2223
  const callerResult = await this.options.getFollowUpMessages?.() ?? [];
2127
2224
  const queued = this.followUpQueue.splice(0);