@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 +138 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +138 -42
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1719
|
-
|
|
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
|
-
/**
|
|
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);
|