@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 +264 -65
- 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 +243 -44
- 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
|
@@ -36,6 +36,107 @@ function isLocalBackendUrl(baseUrl) {
|
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// src/tool-call-hints.ts
|
|
40
|
+
import { resolveToolSchema } from "@kenkaiiii/gg-ai";
|
|
41
|
+
function normalizeName(name) {
|
|
42
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
43
|
+
}
|
|
44
|
+
function editDistance(a, b) {
|
|
45
|
+
let beforePrevious = [];
|
|
46
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
47
|
+
for (let i = 1; i <= a.length; i++) {
|
|
48
|
+
const current = [i];
|
|
49
|
+
for (let j = 1; j <= b.length; j++) {
|
|
50
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
51
|
+
let distance = Math.min(
|
|
52
|
+
(previous[j] ?? 0) + 1,
|
|
53
|
+
(current[j - 1] ?? 0) + 1,
|
|
54
|
+
(previous[j - 1] ?? 0) + cost
|
|
55
|
+
);
|
|
56
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
57
|
+
distance = Math.min(distance, (beforePrevious[j - 2] ?? 0) + 1);
|
|
58
|
+
}
|
|
59
|
+
current[j] = distance;
|
|
60
|
+
}
|
|
61
|
+
beforePrevious = previous;
|
|
62
|
+
previous = current;
|
|
63
|
+
}
|
|
64
|
+
return previous[b.length] ?? 0;
|
|
65
|
+
}
|
|
66
|
+
function closestName(name, candidates) {
|
|
67
|
+
const target = normalizeName(name);
|
|
68
|
+
if (!target) return void 0;
|
|
69
|
+
const limit = Math.min(3, Math.max(1, Math.floor(target.length / 3)));
|
|
70
|
+
let best;
|
|
71
|
+
for (const candidate of [...candidates].sort()) {
|
|
72
|
+
if (candidate === name) continue;
|
|
73
|
+
const distance = editDistance(target, normalizeName(candidate));
|
|
74
|
+
if (distance > limit) continue;
|
|
75
|
+
if (!best || distance < best.distance) best = { name: candidate, distance };
|
|
76
|
+
}
|
|
77
|
+
return best?.name;
|
|
78
|
+
}
|
|
79
|
+
function unknownToolMessage(name, available) {
|
|
80
|
+
const suggestion = closestName(name, available);
|
|
81
|
+
if (suggestion) return `Unknown tool: ${name}. Did you mean \`${suggestion}\`?`;
|
|
82
|
+
const names = [...available].sort();
|
|
83
|
+
const listed = names.length > 40 ? `${names.slice(0, 40).join(", ")}, \u2026` : names.join(", ");
|
|
84
|
+
const searchHint = available.includes("tool_search") ? " If the capability is not listed, call `tool_search` to load it." : "";
|
|
85
|
+
return `Unknown tool: ${name}. Available tools: ${listed}.${searchHint}`;
|
|
86
|
+
}
|
|
87
|
+
function isRecord(value) {
|
|
88
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
89
|
+
}
|
|
90
|
+
function typeLabel(node) {
|
|
91
|
+
if (!isRecord(node)) return "any";
|
|
92
|
+
if (Array.isArray(node.enum)) {
|
|
93
|
+
return node.enum.map((value) => JSON.stringify(value)).join(" | ");
|
|
94
|
+
}
|
|
95
|
+
const variants = node.anyOf ?? node.oneOf;
|
|
96
|
+
if (Array.isArray(variants)) {
|
|
97
|
+
return [...new Set(variants.map(typeLabel))].filter((t) => t !== "null").join(" | ") || "any";
|
|
98
|
+
}
|
|
99
|
+
if (node.type === "array") return `${typeLabel(node.items)}[]`;
|
|
100
|
+
if (Array.isArray(node.type)) {
|
|
101
|
+
return node.type.filter((t) => t !== "null").join(" | ") || "any";
|
|
102
|
+
}
|
|
103
|
+
return typeof node.type === "string" ? node.type : "any";
|
|
104
|
+
}
|
|
105
|
+
function objectShape(tool) {
|
|
106
|
+
let schema;
|
|
107
|
+
try {
|
|
108
|
+
schema = resolveToolSchema(tool);
|
|
109
|
+
} catch {
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
if (!isRecord(schema) || !isRecord(schema.properties)) return void 0;
|
|
113
|
+
const required = new Set(
|
|
114
|
+
Array.isArray(schema.required) ? schema.required.filter((r) => typeof r === "string") : []
|
|
115
|
+
);
|
|
116
|
+
return { properties: schema.properties, required };
|
|
117
|
+
}
|
|
118
|
+
function argumentHints(tool, args) {
|
|
119
|
+
const shape = objectShape(tool);
|
|
120
|
+
if (!shape) return "";
|
|
121
|
+
const names = Object.keys(shape.properties);
|
|
122
|
+
if (names.length === 0) return `Expected arguments: none.`;
|
|
123
|
+
const fields = names.map((name) => {
|
|
124
|
+
const optional = shape.required.has(name) ? "" : "?";
|
|
125
|
+
return `${name}${optional}: ${typeLabel(shape.properties[name])}`;
|
|
126
|
+
});
|
|
127
|
+
const lines = [`Expected arguments: { ${fields.join(", ")} }`];
|
|
128
|
+
if (isRecord(args)) {
|
|
129
|
+
for (const key of Object.keys(args).sort()) {
|
|
130
|
+
if (key in shape.properties) continue;
|
|
131
|
+
const suggestion = closestName(key, names);
|
|
132
|
+
lines.push(
|
|
133
|
+
suggestion ? `Unknown field \`${key}\` \u2014 did you mean \`${suggestion}\`?` : `Unknown field \`${key}\` is not a parameter of this tool.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return lines.join("\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
39
140
|
// src/output-ceiling.ts
|
|
40
141
|
var TTL_MS = 24 * 60 * 60 * 1e3;
|
|
41
142
|
var MIN_PLAUSIBLE_CEILING = 256;
|
|
@@ -220,6 +321,29 @@ ${rule.reminder.trim()}
|
|
|
220
321
|
// src/agent-loop.ts
|
|
221
322
|
var DEFAULT_MAX_TURNS = 300;
|
|
222
323
|
var DEFAULT_TOOL_TIMEOUT_MS = 3e5;
|
|
324
|
+
var PREEMPT_GRACE_MS = 500;
|
|
325
|
+
var STEER_INTERRUPTED_TEXT = "Interrupted: the user sent a new message.";
|
|
326
|
+
var ATOMIC_TOOL_NAMES = /* @__PURE__ */ new Set(["edit", "write", "multi_edit", "apply_patch", "notebook_edit"]);
|
|
327
|
+
function isInterruptibleTool(tool, name) {
|
|
328
|
+
if (tool?.interruptible !== void 0) return tool.interruptible;
|
|
329
|
+
return !ATOMIC_TOOL_NAMES.has(name);
|
|
330
|
+
}
|
|
331
|
+
function markSteerInterrupted(record) {
|
|
332
|
+
const content = record.content;
|
|
333
|
+
let next;
|
|
334
|
+
if (typeof content === "string") {
|
|
335
|
+
next = content.trim() ? `${STEER_INTERRUPTED_TEXT}
|
|
336
|
+
Partial output:
|
|
337
|
+
${content}` : STEER_INTERRUPTED_TEXT;
|
|
338
|
+
} else {
|
|
339
|
+
next = [
|
|
340
|
+
{ type: "text", text: `${STEER_INTERRUPTED_TEXT}
|
|
341
|
+
Partial output:` },
|
|
342
|
+
...content
|
|
343
|
+
];
|
|
344
|
+
}
|
|
345
|
+
return { toolCallId: record.toolCallId, content: next, isError: true };
|
|
346
|
+
}
|
|
223
347
|
var _toolRedaction;
|
|
224
348
|
function toolRedactionOptions() {
|
|
225
349
|
_toolRedaction ??= { secrets: environmentSecrets(process.env) };
|
|
@@ -1370,10 +1494,14 @@ async function* agentLoop(messages, options) {
|
|
|
1370
1494
|
fatalToolArgumentRecoverable = recoverable;
|
|
1371
1495
|
fatalToolArgumentToolName = toolName;
|
|
1372
1496
|
};
|
|
1497
|
+
const preempt = new AbortController();
|
|
1498
|
+
const unsubscribeSteering = options.onSteeringAvailable?.(() => preempt.abort());
|
|
1373
1499
|
const executionOptions = {
|
|
1374
1500
|
signal: options.signal,
|
|
1501
|
+
preemptSignal: options.onSteeringAvailable ? preempt.signal : void 0,
|
|
1375
1502
|
maxToolResultChars: options.maxToolResultChars,
|
|
1376
1503
|
maxTurnToolResultChars: options.maxTurnToolResultChars,
|
|
1504
|
+
transformToolResult: options.transformToolResult,
|
|
1377
1505
|
toolMap,
|
|
1378
1506
|
invalidToolArgumentCounts,
|
|
1379
1507
|
markFatalToolArgumentError,
|
|
@@ -1382,7 +1510,15 @@ async function* agentLoop(messages, options) {
|
|
|
1382
1510
|
const hasSequentialToolCall = toolCalls.some(
|
|
1383
1511
|
(toolCall) => toolMap.get(toolCall.name)?.executionMode === "sequential"
|
|
1384
1512
|
);
|
|
1385
|
-
|
|
1513
|
+
let executionResult;
|
|
1514
|
+
try {
|
|
1515
|
+
executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
|
|
1516
|
+
} finally {
|
|
1517
|
+
unsubscribeSteering?.();
|
|
1518
|
+
}
|
|
1519
|
+
if (executionResult.preempted) {
|
|
1520
|
+
diag("steer_preempted_tools", { turn, provider: options.provider, model: options.model });
|
|
1521
|
+
}
|
|
1386
1522
|
messages.push({ role: "tool", content: executionResult.toolResults });
|
|
1387
1523
|
yield { type: "checkpoint", turn };
|
|
1388
1524
|
const toolsAborted = executionResult.aborted;
|
|
@@ -1539,7 +1675,7 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1539
1675
|
options.seenToolCalls.add(signature);
|
|
1540
1676
|
}
|
|
1541
1677
|
if (!tool) {
|
|
1542
|
-
resultContent =
|
|
1678
|
+
resultContent = unknownToolMessage(toolCall.name, [...options.toolMap.keys()]);
|
|
1543
1679
|
isError = true;
|
|
1544
1680
|
} else {
|
|
1545
1681
|
try {
|
|
@@ -1556,8 +1692,12 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1556
1692
|
}
|
|
1557
1693
|
const callerSignal = options.signal;
|
|
1558
1694
|
const toolTimeout = AbortSignal.timeout(tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS);
|
|
1695
|
+
const preemptSignal = isInterruptibleTool(tool, toolCall.name) ? options.preemptSignal : void 0;
|
|
1696
|
+
const toolSignals = [callerSignal, toolTimeout, preemptSignal].filter(
|
|
1697
|
+
(s) => s !== void 0
|
|
1698
|
+
);
|
|
1559
1699
|
const ctx = {
|
|
1560
|
-
signal:
|
|
1700
|
+
signal: toolSignals.length === 1 ? toolTimeout : AbortSignal.any(toolSignals),
|
|
1561
1701
|
toolCallId: toolCall.id,
|
|
1562
1702
|
onUpdate: (update) => {
|
|
1563
1703
|
pushEvent({
|
|
@@ -1571,6 +1711,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1571
1711
|
const normalized = normalizeToolResult(raw);
|
|
1572
1712
|
resultContent = redactValue(normalized.content, toolRedactionOptions());
|
|
1573
1713
|
details = redactValue(normalized.details, toolRedactionOptions());
|
|
1714
|
+
if (options.transformToolResult) {
|
|
1715
|
+
try {
|
|
1716
|
+
resultContent = options.transformToolResult(
|
|
1717
|
+
{ name: toolCall.name, args: toolCall.args },
|
|
1718
|
+
resultContent
|
|
1719
|
+
);
|
|
1720
|
+
} catch {
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1574
1723
|
for (const key of options.invalidToolArgumentCounts.keys()) {
|
|
1575
1724
|
if (key.startsWith(`${toolCall.name}:`)) options.invalidToolArgumentCounts.delete(key);
|
|
1576
1725
|
}
|
|
@@ -1582,8 +1731,10 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1582
1731
|
const failureCount = (options.invalidToolArgumentCounts.get(failureKey) ?? 0) + 1;
|
|
1583
1732
|
options.invalidToolArgumentCounts.set(failureKey, failureCount);
|
|
1584
1733
|
invalidArgAttempt = failureCount;
|
|
1734
|
+
const hints = argumentHints(tool, toolCall.args);
|
|
1585
1735
|
resultContent = `Invalid arguments for tool \`${toolCall.name}\`:
|
|
1586
|
-
` + prettyError +
|
|
1736
|
+
` + prettyError + (hints ? `
|
|
1737
|
+
${hints}` : "") + "\nRe-issue the call with each field as the correct type.";
|
|
1587
1738
|
if (failureCount >= 3) {
|
|
1588
1739
|
const recoverable = Object.keys(toolCall.args ?? {}).length === 0;
|
|
1589
1740
|
options.markFatalToolArgumentError(
|
|
@@ -1608,6 +1759,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1608
1759
|
}
|
|
1609
1760
|
resultContent = redactValue(resultContent, toolRedactionOptions());
|
|
1610
1761
|
details = redactValue(details, toolRedactionOptions());
|
|
1762
|
+
if (options.preemptSignal?.aborted && isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name)) {
|
|
1763
|
+
const marked = markSteerInterrupted({
|
|
1764
|
+
toolCallId: toolCall.id,
|
|
1765
|
+
content: resultContent,
|
|
1766
|
+
isError
|
|
1767
|
+
});
|
|
1768
|
+
resultContent = marked.content;
|
|
1769
|
+
isError = true;
|
|
1770
|
+
}
|
|
1611
1771
|
const durationMs = Date.now() - startTime;
|
|
1612
1772
|
pushEvent({
|
|
1613
1773
|
type: "tool_call_end",
|
|
@@ -1620,6 +1780,59 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1620
1780
|
});
|
|
1621
1781
|
return { toolCallId: toolCall.id, content: resultContent, isError };
|
|
1622
1782
|
}
|
|
1783
|
+
function createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds) {
|
|
1784
|
+
const running = /* @__PURE__ */ new Map();
|
|
1785
|
+
let graceTimer;
|
|
1786
|
+
const preemptSignal = options.preemptSignal;
|
|
1787
|
+
const abandon = () => {
|
|
1788
|
+
graceTimer = void 0;
|
|
1789
|
+
for (const [id, entry] of running) {
|
|
1790
|
+
if (!entry.interruptible || resultsById.has(id)) continue;
|
|
1791
|
+
resultsById.set(id, { toolCallId: id, content: STEER_INTERRUPTED_TEXT, isError: true });
|
|
1792
|
+
pushToolEvent(eventStream, state, {
|
|
1793
|
+
type: "tool_call_end",
|
|
1794
|
+
toolCallId: id,
|
|
1795
|
+
result: STEER_INTERRUPTED_TEXT,
|
|
1796
|
+
isError: true,
|
|
1797
|
+
durationMs: Date.now() - entry.startedAt
|
|
1798
|
+
});
|
|
1799
|
+
running.delete(id);
|
|
1800
|
+
}
|
|
1801
|
+
if (running.size === 0) eventStream.close();
|
|
1802
|
+
};
|
|
1803
|
+
const onPreempt = () => {
|
|
1804
|
+
if (graceTimer === void 0) graceTimer = setTimeout(abandon, PREEMPT_GRACE_MS);
|
|
1805
|
+
};
|
|
1806
|
+
preemptSignal?.addEventListener("abort", onPreempt, { once: true });
|
|
1807
|
+
return {
|
|
1808
|
+
async run(toolCall) {
|
|
1809
|
+
if (options.preemptSignal?.aborted) return;
|
|
1810
|
+
dispatchedIds.add(toolCall.id);
|
|
1811
|
+
const interruptible = isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name);
|
|
1812
|
+
running.set(toolCall.id, { toolCall, interruptible, startedAt: Date.now() });
|
|
1813
|
+
const record = await executeSingleToolCall(
|
|
1814
|
+
toolCall,
|
|
1815
|
+
options,
|
|
1816
|
+
(event) => pushToolEvent(eventStream, state, event)
|
|
1817
|
+
);
|
|
1818
|
+
running.delete(toolCall.id);
|
|
1819
|
+
if (resultsById.has(toolCall.id)) return;
|
|
1820
|
+
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);
|
|
1821
|
+
resultsById.set(
|
|
1822
|
+
toolCall.id,
|
|
1823
|
+
interruptible && preemptSignal?.aborted && !alreadyMarked ? markSteerInterrupted(record) : record
|
|
1824
|
+
);
|
|
1825
|
+
if (preemptSignal?.aborted && graceTimer === void 0 && running.size === 0) {
|
|
1826
|
+
eventStream.close();
|
|
1827
|
+
}
|
|
1828
|
+
},
|
|
1829
|
+
preempted: () => preemptSignal?.aborted === true,
|
|
1830
|
+
dispose: () => {
|
|
1831
|
+
if (graceTimer !== void 0) clearTimeout(graceTimer);
|
|
1832
|
+
preemptSignal?.removeEventListener("abort", onPreempt);
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1623
1836
|
async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
|
|
1624
1837
|
const eventStream = new EventStream();
|
|
1625
1838
|
const state = { finalized: false };
|
|
@@ -1627,6 +1840,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
|
|
|
1627
1840
|
const dispatchedIds = /* @__PURE__ */ new Set();
|
|
1628
1841
|
const abortHandler = () => eventStream.abort(new Error("aborted"));
|
|
1629
1842
|
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
1843
|
+
const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
|
|
1630
1844
|
const phases = [];
|
|
1631
1845
|
let currentParallel = [];
|
|
1632
1846
|
for (const toolCall of toolCalls) {
|
|
@@ -1647,40 +1861,18 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
|
|
|
1647
1861
|
void (async () => {
|
|
1648
1862
|
try {
|
|
1649
1863
|
for (const phase of phases) {
|
|
1650
|
-
if (options.signal?.aborted) break;
|
|
1864
|
+
if (options.signal?.aborted || options.preemptSignal?.aborted) break;
|
|
1651
1865
|
if (phase.sequential) {
|
|
1652
1866
|
const signature = JSON.stringify([
|
|
1653
1867
|
phase.sequential.name,
|
|
1654
1868
|
canonicalToolArgs(phase.sequential.args)
|
|
1655
1869
|
]);
|
|
1656
1870
|
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);
|
|
1871
|
+
await tracker.run(phase.sequential);
|
|
1664
1872
|
} 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);
|
|
1873
|
+
await tracker.run(phase.parallel[0]);
|
|
1672
1874
|
} 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
|
-
);
|
|
1875
|
+
await Promise.all(phase.parallel.map((toolCall) => tracker.run(toolCall)));
|
|
1684
1876
|
}
|
|
1685
1877
|
}
|
|
1686
1878
|
if (!state.finalized) eventStream.close();
|
|
@@ -1701,12 +1893,13 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
|
|
|
1701
1893
|
}
|
|
1702
1894
|
} finally {
|
|
1703
1895
|
options.signal?.removeEventListener("abort", abortHandler);
|
|
1896
|
+
tracker.dispose();
|
|
1704
1897
|
state.finalized = true;
|
|
1705
1898
|
}
|
|
1706
1899
|
const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
|
|
1707
1900
|
capToolResults(toolResults, options.maxToolResultChars);
|
|
1708
1901
|
capTurnToolResults(toolResults, options.maxTurnToolResultChars);
|
|
1709
|
-
return { toolResults, aborted };
|
|
1902
|
+
return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
|
|
1710
1903
|
}
|
|
1711
1904
|
async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
|
|
1712
1905
|
const eventStream = new EventStream();
|
|
@@ -1715,17 +1908,8 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
|
|
|
1715
1908
|
const dispatchedIds = /* @__PURE__ */ new Set();
|
|
1716
1909
|
const abortHandler = () => eventStream.abort(new Error("aborted"));
|
|
1717
1910
|
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(() => {
|
|
1911
|
+
const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
|
|
1912
|
+
Promise.all(toolCalls.map((toolCall) => tracker.run(toolCall))).then(() => {
|
|
1729
1913
|
if (!state.finalized) eventStream.close();
|
|
1730
1914
|
}).catch((err) => {
|
|
1731
1915
|
if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1743,12 +1927,13 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
|
|
|
1743
1927
|
}
|
|
1744
1928
|
} finally {
|
|
1745
1929
|
options.signal?.removeEventListener("abort", abortHandler);
|
|
1930
|
+
tracker.dispose();
|
|
1746
1931
|
state.finalized = true;
|
|
1747
1932
|
}
|
|
1748
1933
|
const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
|
|
1749
1934
|
capToolResults(toolResults, options.maxToolResultChars);
|
|
1750
1935
|
capTurnToolResults(toolResults, options.maxTurnToolResultChars);
|
|
1751
|
-
return { toolResults, aborted };
|
|
1936
|
+
return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
|
|
1752
1937
|
}
|
|
1753
1938
|
function cancelledBeforeStartText(name) {
|
|
1754
1939
|
return `\`${name}\` was cancelled before it started, so it had no effect. Safe to retry.`;
|
|
@@ -2044,6 +2229,7 @@ var Agent = class {
|
|
|
2044
2229
|
options;
|
|
2045
2230
|
steeringQueue = [];
|
|
2046
2231
|
followUpQueue = [];
|
|
2232
|
+
steeringListeners = /* @__PURE__ */ new Set();
|
|
2047
2233
|
constructor(options) {
|
|
2048
2234
|
this.options = options;
|
|
2049
2235
|
if (options.system) {
|
|
@@ -2068,9 +2254,14 @@ var Agent = class {
|
|
|
2068
2254
|
get running() {
|
|
2069
2255
|
return this._running;
|
|
2070
2256
|
}
|
|
2071
|
-
/**
|
|
2257
|
+
/**
|
|
2258
|
+
* Queue a steering message. Instant interrupt: if tools are running, the
|
|
2259
|
+
* interruptible ones are preempted right away and the message is injected
|
|
2260
|
+
* before the next model call (the turn continues — unlike abort/Stop).
|
|
2261
|
+
*/
|
|
2072
2262
|
steer(msg) {
|
|
2073
2263
|
this.steeringQueue.push(msg);
|
|
2264
|
+
for (const listener of [...this.steeringListeners]) listener();
|
|
2074
2265
|
}
|
|
2075
2266
|
/** Queue a follow-up message for injection when the agent would otherwise stop. */
|
|
2076
2267
|
followUp(msg) {
|
|
@@ -2090,6 +2281,14 @@ var Agent = class {
|
|
|
2090
2281
|
const all = [...callerResult ?? [], ...queued];
|
|
2091
2282
|
return all.length > 0 ? all : null;
|
|
2092
2283
|
},
|
|
2284
|
+
onSteeringAvailable: (listener) => {
|
|
2285
|
+
this.steeringListeners.add(listener);
|
|
2286
|
+
const unsubscribeCaller = this.options.onSteeringAvailable?.(listener);
|
|
2287
|
+
return () => {
|
|
2288
|
+
this.steeringListeners.delete(listener);
|
|
2289
|
+
unsubscribeCaller?.();
|
|
2290
|
+
};
|
|
2291
|
+
},
|
|
2093
2292
|
getFollowUpMessages: async () => {
|
|
2094
2293
|
const callerResult = await this.options.getFollowUpMessages?.() ?? [];
|
|
2095
2294
|
const queued = this.followUpQueue.splice(0);
|