@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.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
@@ -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) };
@@ -1370,10 +1393,14 @@ async function* agentLoop(messages, options) {
1370
1393
  fatalToolArgumentRecoverable = recoverable;
1371
1394
  fatalToolArgumentToolName = toolName;
1372
1395
  };
1396
+ const preempt = new AbortController();
1397
+ const unsubscribeSteering = options.onSteeringAvailable?.(() => preempt.abort());
1373
1398
  const executionOptions = {
1374
1399
  signal: options.signal,
1400
+ preemptSignal: options.onSteeringAvailable ? preempt.signal : void 0,
1375
1401
  maxToolResultChars: options.maxToolResultChars,
1376
1402
  maxTurnToolResultChars: options.maxTurnToolResultChars,
1403
+ transformToolResult: options.transformToolResult,
1377
1404
  toolMap,
1378
1405
  invalidToolArgumentCounts,
1379
1406
  markFatalToolArgumentError,
@@ -1382,7 +1409,15 @@ async function* agentLoop(messages, options) {
1382
1409
  const hasSequentialToolCall = toolCalls.some(
1383
1410
  (toolCall) => toolMap.get(toolCall.name)?.executionMode === "sequential"
1384
1411
  );
1385
- 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
+ }
1386
1421
  messages.push({ role: "tool", content: executionResult.toolResults });
1387
1422
  yield { type: "checkpoint", turn };
1388
1423
  const toolsAborted = executionResult.aborted;
@@ -1556,8 +1591,12 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1556
1591
  }
1557
1592
  const callerSignal = options.signal;
1558
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
+ );
1559
1598
  const ctx = {
1560
- signal: callerSignal ? AbortSignal.any([callerSignal, toolTimeout]) : toolTimeout,
1599
+ signal: toolSignals.length === 1 ? toolTimeout : AbortSignal.any(toolSignals),
1561
1600
  toolCallId: toolCall.id,
1562
1601
  onUpdate: (update) => {
1563
1602
  pushEvent({
@@ -1571,6 +1610,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1571
1610
  const normalized = normalizeToolResult(raw);
1572
1611
  resultContent = redactValue(normalized.content, toolRedactionOptions());
1573
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
+ }
1574
1622
  for (const key of options.invalidToolArgumentCounts.keys()) {
1575
1623
  if (key.startsWith(`${toolCall.name}:`)) options.invalidToolArgumentCounts.delete(key);
1576
1624
  }
@@ -1608,6 +1656,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1608
1656
  }
1609
1657
  resultContent = redactValue(resultContent, toolRedactionOptions());
1610
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
+ }
1611
1668
  const durationMs = Date.now() - startTime;
1612
1669
  pushEvent({
1613
1670
  type: "tool_call_end",
@@ -1620,6 +1677,59 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1620
1677
  });
1621
1678
  return { toolCallId: toolCall.id, content: resultContent, isError };
1622
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
+ }
1623
1733
  async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1624
1734
  const eventStream = new EventStream();
1625
1735
  const state = { finalized: false };
@@ -1627,6 +1737,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1627
1737
  const dispatchedIds = /* @__PURE__ */ new Set();
1628
1738
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1629
1739
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1740
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
1630
1741
  const phases = [];
1631
1742
  let currentParallel = [];
1632
1743
  for (const toolCall of toolCalls) {
@@ -1647,40 +1758,18 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1647
1758
  void (async () => {
1648
1759
  try {
1649
1760
  for (const phase of phases) {
1650
- if (options.signal?.aborted) break;
1761
+ if (options.signal?.aborted || options.preemptSignal?.aborted) break;
1651
1762
  if (phase.sequential) {
1652
1763
  const signature = JSON.stringify([
1653
1764
  phase.sequential.name,
1654
1765
  canonicalToolArgs(phase.sequential.args)
1655
1766
  ]);
1656
1767
  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);
1768
+ await tracker.run(phase.sequential);
1664
1769
  } 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);
1770
+ await tracker.run(phase.parallel[0]);
1672
1771
  } 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
- );
1772
+ await Promise.all(phase.parallel.map((toolCall) => tracker.run(toolCall)));
1684
1773
  }
1685
1774
  }
1686
1775
  if (!state.finalized) eventStream.close();
@@ -1701,12 +1790,13 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1701
1790
  }
1702
1791
  } finally {
1703
1792
  options.signal?.removeEventListener("abort", abortHandler);
1793
+ tracker.dispose();
1704
1794
  state.finalized = true;
1705
1795
  }
1706
1796
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1707
1797
  capToolResults(toolResults, options.maxToolResultChars);
1708
1798
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1709
- return { toolResults, aborted };
1799
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1710
1800
  }
1711
1801
  async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
1712
1802
  const eventStream = new EventStream();
@@ -1715,17 +1805,8 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1715
1805
  const dispatchedIds = /* @__PURE__ */ new Set();
1716
1806
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1717
1807
  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(() => {
1808
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
1809
+ Promise.all(toolCalls.map((toolCall) => tracker.run(toolCall))).then(() => {
1729
1810
  if (!state.finalized) eventStream.close();
1730
1811
  }).catch((err) => {
1731
1812
  if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
@@ -1743,12 +1824,13 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1743
1824
  }
1744
1825
  } finally {
1745
1826
  options.signal?.removeEventListener("abort", abortHandler);
1827
+ tracker.dispose();
1746
1828
  state.finalized = true;
1747
1829
  }
1748
1830
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1749
1831
  capToolResults(toolResults, options.maxToolResultChars);
1750
1832
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1751
- return { toolResults, aborted };
1833
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1752
1834
  }
1753
1835
  function cancelledBeforeStartText(name) {
1754
1836
  return `\`${name}\` was cancelled before it started, so it had no effect. Safe to retry.`;
@@ -2044,6 +2126,7 @@ var Agent = class {
2044
2126
  options;
2045
2127
  steeringQueue = [];
2046
2128
  followUpQueue = [];
2129
+ steeringListeners = /* @__PURE__ */ new Set();
2047
2130
  constructor(options) {
2048
2131
  this.options = options;
2049
2132
  if (options.system) {
@@ -2068,9 +2151,14 @@ var Agent = class {
2068
2151
  get running() {
2069
2152
  return this._running;
2070
2153
  }
2071
- /** 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
+ */
2072
2159
  steer(msg) {
2073
2160
  this.steeringQueue.push(msg);
2161
+ for (const listener of [...this.steeringListeners]) listener();
2074
2162
  }
2075
2163
  /** Queue a follow-up message for injection when the agent would otherwise stop. */
2076
2164
  followUp(msg) {
@@ -2090,6 +2178,14 @@ var Agent = class {
2090
2178
  const all = [...callerResult ?? [], ...queued];
2091
2179
  return all.length > 0 ? all : null;
2092
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
+ },
2093
2189
  getFollowUpMessages: async () => {
2094
2190
  const callerResult = await this.options.getFollowUpMessages?.() ?? [];
2095
2191
  const queued = this.followUpQueue.splice(0);