@prestyj/agent 5.28.0 → 5.29.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 +486 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +141 -5
- package/dist/index.d.ts +141 -5
- package/dist/index.js +458 -55
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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 "@prestyj/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;
|
|
@@ -90,9 +191,159 @@ function clampOutputTokens(key, requested) {
|
|
|
90
191
|
return requested === void 0 ? ceiling : Math.min(requested, ceiling);
|
|
91
192
|
}
|
|
92
193
|
|
|
194
|
+
// src/stream-rules.ts
|
|
195
|
+
var DEFAULT_STREAM_RULE_MAX_RETRIES = 3;
|
|
196
|
+
var DEFAULT_STREAM_RULE_WINDOW_CHARS = 1024;
|
|
197
|
+
var JSON_ESCAPES = {
|
|
198
|
+
n: "\n",
|
|
199
|
+
r: "\r",
|
|
200
|
+
t: " ",
|
|
201
|
+
b: "\b",
|
|
202
|
+
f: "\f",
|
|
203
|
+
'"': '"',
|
|
204
|
+
"\\": "\\",
|
|
205
|
+
"/": "/"
|
|
206
|
+
};
|
|
207
|
+
var JsonEscapeDecoder = class {
|
|
208
|
+
pending = "";
|
|
209
|
+
push(chunk) {
|
|
210
|
+
const input = this.pending + chunk;
|
|
211
|
+
this.pending = "";
|
|
212
|
+
if (!input.includes("\\")) return input;
|
|
213
|
+
let out = "";
|
|
214
|
+
let i = 0;
|
|
215
|
+
while (i < input.length) {
|
|
216
|
+
const ch = input.charAt(i);
|
|
217
|
+
if (ch !== "\\") {
|
|
218
|
+
out += ch;
|
|
219
|
+
i++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (i + 1 >= input.length) {
|
|
223
|
+
this.pending = input.slice(i);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
const next = input.charAt(i + 1);
|
|
227
|
+
if (next === "u") {
|
|
228
|
+
if (i + 6 > input.length) {
|
|
229
|
+
this.pending = input.slice(i);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
const hex = input.slice(i + 2, i + 6);
|
|
233
|
+
out += /^[0-9a-fA-F]{4}$/.test(hex) ? String.fromCharCode(Number.parseInt(hex, 16)) : input.slice(i, i + 6);
|
|
234
|
+
i += 6;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
out += JSON_ESCAPES[next] ?? next;
|
|
238
|
+
i += 2;
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
var StreamRuleMonitor = class {
|
|
244
|
+
maxRetries;
|
|
245
|
+
rules;
|
|
246
|
+
windowChars;
|
|
247
|
+
fired = /* @__PURE__ */ new Set();
|
|
248
|
+
retries = 0;
|
|
249
|
+
textWindow = "";
|
|
250
|
+
toolStreams = /* @__PURE__ */ new Map();
|
|
251
|
+
constructor(config) {
|
|
252
|
+
this.rules = config.rules;
|
|
253
|
+
this.maxRetries = Math.max(0, config.maxRetries ?? DEFAULT_STREAM_RULE_MAX_RETRIES);
|
|
254
|
+
this.windowChars = Math.max(64, config.windowChars ?? DEFAULT_STREAM_RULE_WINDOW_CHARS);
|
|
255
|
+
}
|
|
256
|
+
/** False once the retry cap is spent or every rule has fired — skip all matching. */
|
|
257
|
+
get active() {
|
|
258
|
+
return this.retries < this.maxRetries && this.fired.size < this.rules.length;
|
|
259
|
+
}
|
|
260
|
+
get retriesUsed() {
|
|
261
|
+
return this.retries;
|
|
262
|
+
}
|
|
263
|
+
beginAttempt() {
|
|
264
|
+
this.textWindow = "";
|
|
265
|
+
this.toolStreams.clear();
|
|
266
|
+
}
|
|
267
|
+
checkText(delta) {
|
|
268
|
+
if (!this.active || !delta) return null;
|
|
269
|
+
const scan = this.textWindow + delta;
|
|
270
|
+
this.textWindow = scan.slice(-this.windowChars);
|
|
271
|
+
const rules = this.match(scan, "text");
|
|
272
|
+
return rules.length > 0 ? { rules, source: "text" } : null;
|
|
273
|
+
}
|
|
274
|
+
checkToolArgs(id, toolName, argsDelta) {
|
|
275
|
+
if (!this.active || !argsDelta) return null;
|
|
276
|
+
let state = this.toolStreams.get(id);
|
|
277
|
+
if (!state) {
|
|
278
|
+
state = { window: "", decoder: new JsonEscapeDecoder() };
|
|
279
|
+
this.toolStreams.set(id, state);
|
|
280
|
+
}
|
|
281
|
+
const scan = state.window + state.decoder.push(argsDelta);
|
|
282
|
+
state.window = scan.slice(-this.windowChars);
|
|
283
|
+
const rules = this.match(scan, "tool", toolName);
|
|
284
|
+
return rules.length > 0 ? { rules, source: "tool", toolName } : null;
|
|
285
|
+
}
|
|
286
|
+
/** Consume one retry and mark the rules fired. Returns the retry ordinal (1-based). */
|
|
287
|
+
recordTrigger(rules) {
|
|
288
|
+
for (const rule of rules) this.fired.add(rule.name);
|
|
289
|
+
this.retries++;
|
|
290
|
+
return this.retries;
|
|
291
|
+
}
|
|
292
|
+
match(scan, source, toolName) {
|
|
293
|
+
const hits = [];
|
|
294
|
+
for (const rule of this.rules) {
|
|
295
|
+
if (this.fired.has(rule.name)) continue;
|
|
296
|
+
if (rule.scope !== "both" && rule.scope !== source) continue;
|
|
297
|
+
if (source === "tool" && rule.tools && toolName !== void 0) {
|
|
298
|
+
if (!rule.tools.includes(toolName)) continue;
|
|
299
|
+
}
|
|
300
|
+
rule.pattern.lastIndex = 0;
|
|
301
|
+
if (rule.pattern.test(scan)) hits.push(rule);
|
|
302
|
+
}
|
|
303
|
+
return hits;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
function buildStreamRuleReminder(rules) {
|
|
307
|
+
const blocks = rules.map(
|
|
308
|
+
(rule) => `<system-reminder reason="stream_rule" rule="${rule.name.replace(/"/g, "'")}">
|
|
309
|
+
Your previous response was interrupted mid-stream because it violated this rule. The interrupted output was discarded: the user never saw it and no tool call from it ran. Regenerate the response from the start and follow the rule:
|
|
310
|
+
|
|
311
|
+
${rule.reminder.trim()}
|
|
312
|
+
</system-reminder>`
|
|
313
|
+
);
|
|
314
|
+
return {
|
|
315
|
+
role: "user",
|
|
316
|
+
content: blocks.join("\n\n"),
|
|
317
|
+
provenance: { source: "runtime", kind: "notification", visibility: "hidden" }
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
93
321
|
// src/agent-loop.ts
|
|
94
322
|
var DEFAULT_MAX_TURNS = 300;
|
|
95
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
|
+
}
|
|
96
347
|
var _toolRedaction;
|
|
97
348
|
function toolRedactionOptions() {
|
|
98
349
|
_toolRedaction ??= { secrets: environmentSecrets(process.env) };
|
|
@@ -403,6 +654,7 @@ async function* agentLoop(messages, options) {
|
|
|
403
654
|
let runawayToolcallRetries = 0;
|
|
404
655
|
let overflowCompactionAttempts = 0;
|
|
405
656
|
let toolResultTruncationAttempted = false;
|
|
657
|
+
const streamRuleMonitor = options.streamRules && options.streamRules.rules.length > 0 ? new StreamRuleMonitor(options.streamRules) : null;
|
|
406
658
|
const invalidToolArgumentCounts = /* @__PURE__ */ new Map();
|
|
407
659
|
let toolArgumentAutoContinueUsed = false;
|
|
408
660
|
let useNonStreamingFallback = false;
|
|
@@ -456,16 +708,7 @@ async function* agentLoop(messages, options) {
|
|
|
456
708
|
turn++;
|
|
457
709
|
if (logicalTurnStartedAt === 0) logicalTurnStartedAt = Date.now();
|
|
458
710
|
toolMap = new Map((options.tools ?? []).map((t) => [t.name, t]));
|
|
459
|
-
|
|
460
|
-
for (const m of messages) {
|
|
461
|
-
if (typeof m.content === "string") msgChars += m.content.length;
|
|
462
|
-
else if (Array.isArray(m.content)) {
|
|
463
|
-
for (const p of m.content) {
|
|
464
|
-
if ("text" in p && typeof p.text === "string") msgChars += p.text.length;
|
|
465
|
-
if ("content" in p && typeof p.content === "string") msgChars += p.content.length;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
711
|
+
const msgChars = countMessageChars(messages);
|
|
469
712
|
let firstEventTimeoutMs;
|
|
470
713
|
let initialHardTimeoutMs;
|
|
471
714
|
if (baseFirstEventTimeoutMs === STREAM_FIRST_EVENT_TIMEOUT_MS) {
|
|
@@ -536,6 +779,9 @@ async function* agentLoop(messages, options) {
|
|
|
536
779
|
let toolcallNoProgressCount = 0;
|
|
537
780
|
let runawayDetected = null;
|
|
538
781
|
let attemptText = "";
|
|
782
|
+
let streamRuleHit = null;
|
|
783
|
+
let attemptThinkingChars = 0;
|
|
784
|
+
streamRuleMonitor?.beginAttempt();
|
|
539
785
|
let lastYieldEndTime = Date.now();
|
|
540
786
|
let maxConsumerLagMs = 0;
|
|
541
787
|
const forwardAbort = () => streamController.abort();
|
|
@@ -622,6 +868,7 @@ async function* agentLoop(messages, options) {
|
|
|
622
868
|
transportSessionId: options.transportSessionId,
|
|
623
869
|
projectId: liveProjectId,
|
|
624
870
|
cacheRetention: options.cacheRetention,
|
|
871
|
+
onContextPrepared: options.onContextPrepared,
|
|
625
872
|
promptCacheKey: options.promptCacheKey,
|
|
626
873
|
serviceTier: options.serviceTier,
|
|
627
874
|
supportsImages: options.supportsImages,
|
|
@@ -699,8 +946,14 @@ async function* agentLoop(messages, options) {
|
|
|
699
946
|
}
|
|
700
947
|
if (event.type === "text_delta") {
|
|
701
948
|
attemptText += event.text;
|
|
949
|
+
streamRuleHit = streamRuleMonitor?.checkText(event.text) ?? null;
|
|
950
|
+
if (streamRuleHit) {
|
|
951
|
+
streamController.abort();
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
702
954
|
yield { type: "text_delta", text: event.text };
|
|
703
955
|
} else if (event.type === "thinking_delta") {
|
|
956
|
+
attemptThinkingChars += event.text.length;
|
|
704
957
|
yield { type: "thinking_delta", text: event.text };
|
|
705
958
|
} else if (event.type === "server_toolcall") {
|
|
706
959
|
yield {
|
|
@@ -721,6 +974,11 @@ async function* agentLoop(messages, options) {
|
|
|
721
974
|
toolcallDeltaChars += chunkChars;
|
|
722
975
|
toolcallDeltaCount++;
|
|
723
976
|
toolcallNoProgressCount = chunkChars > 0 ? 0 : toolcallNoProgressCount + 1;
|
|
977
|
+
streamRuleHit = streamRuleMonitor?.checkToolArgs(event.id, event.name, event.argsJson ?? "") ?? null;
|
|
978
|
+
if (streamRuleHit) {
|
|
979
|
+
streamController.abort();
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
724
982
|
if (!runawayDetected && (toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS || toolcallNoProgressCount > MAX_TOOLCALL_NO_PROGRESS_EVENTS)) {
|
|
725
983
|
runawayDetected = {
|
|
726
984
|
kind: toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS ? "chars" : "events",
|
|
@@ -743,6 +1001,54 @@ async function* agentLoop(messages, options) {
|
|
|
743
1001
|
lastYieldEndTime = Date.now();
|
|
744
1002
|
resetIdleTimer();
|
|
745
1003
|
}
|
|
1004
|
+
if (streamRuleHit && streamRuleMonitor) {
|
|
1005
|
+
const attempt = streamRuleMonitor.recordTrigger(streamRuleHit.rules);
|
|
1006
|
+
const ruleNames = streamRuleHit.rules.map((rule) => rule.name);
|
|
1007
|
+
const priorPrompt = latestProviderUsage ? latestProviderUsage.inputTokens + (latestProviderUsage.cacheRead ?? 0) + (latestProviderUsage.cacheWrite ?? 0) : 0;
|
|
1008
|
+
const newChars = priorPrompt > 0 && usageAnchorIndex !== void 0 ? countMessageChars(messages.slice(usageAnchorIndex + 1)) : msgChars;
|
|
1009
|
+
const abortedUsage = {
|
|
1010
|
+
inputTokens: Math.ceil(newChars / 4),
|
|
1011
|
+
outputTokens: Math.ceil(
|
|
1012
|
+
(attemptText.length + attemptThinkingChars + toolcallDeltaChars) / 4
|
|
1013
|
+
),
|
|
1014
|
+
...priorPrompt > 0 ? { cacheRead: priorPrompt } : {}
|
|
1015
|
+
};
|
|
1016
|
+
totalUsage.inputTokens += abortedUsage.inputTokens;
|
|
1017
|
+
totalUsage.outputTokens += abortedUsage.outputTokens;
|
|
1018
|
+
if (abortedUsage.cacheRead) {
|
|
1019
|
+
totalUsage.cacheRead = (totalUsage.cacheRead ?? 0) + abortedUsage.cacheRead;
|
|
1020
|
+
}
|
|
1021
|
+
diag("stream_rule_triggered", {
|
|
1022
|
+
rules: ruleNames.join(","),
|
|
1023
|
+
source: streamRuleHit.source,
|
|
1024
|
+
toolName: streamRuleHit.toolName,
|
|
1025
|
+
attempt,
|
|
1026
|
+
maxAttempts: streamRuleMonitor.maxRetries,
|
|
1027
|
+
discardedChars: attemptText.length + toolcallDeltaChars,
|
|
1028
|
+
provider: options.provider,
|
|
1029
|
+
model: options.model
|
|
1030
|
+
});
|
|
1031
|
+
messages.push(buildStreamRuleReminder(streamRuleHit.rules));
|
|
1032
|
+
yield {
|
|
1033
|
+
type: "stream_rule_triggered",
|
|
1034
|
+
rules: ruleNames,
|
|
1035
|
+
source: streamRuleHit.source,
|
|
1036
|
+
...streamRuleHit.toolName !== void 0 ? { toolName: streamRuleHit.toolName } : {},
|
|
1037
|
+
attempt,
|
|
1038
|
+
maxAttempts: streamRuleMonitor.maxRetries,
|
|
1039
|
+
usage: abortedUsage
|
|
1040
|
+
};
|
|
1041
|
+
yield {
|
|
1042
|
+
type: "retry",
|
|
1043
|
+
reason: "stream_rule",
|
|
1044
|
+
attempt,
|
|
1045
|
+
maxAttempts: streamRuleMonitor.maxRetries,
|
|
1046
|
+
delayMs: 0,
|
|
1047
|
+
silent: true
|
|
1048
|
+
};
|
|
1049
|
+
turn--;
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
746
1052
|
diag("stream_done", {
|
|
747
1053
|
events: streamEventCount,
|
|
748
1054
|
totalMs: Date.now() - streamCallStart,
|
|
@@ -1249,10 +1555,14 @@ async function* agentLoop(messages, options) {
|
|
|
1249
1555
|
fatalToolArgumentRecoverable = recoverable;
|
|
1250
1556
|
fatalToolArgumentToolName = toolName;
|
|
1251
1557
|
};
|
|
1558
|
+
const preempt = new AbortController();
|
|
1559
|
+
const unsubscribeSteering = options.onSteeringAvailable?.(() => preempt.abort());
|
|
1252
1560
|
const executionOptions = {
|
|
1253
1561
|
signal: options.signal,
|
|
1562
|
+
preemptSignal: options.onSteeringAvailable ? preempt.signal : void 0,
|
|
1254
1563
|
maxToolResultChars: options.maxToolResultChars,
|
|
1255
1564
|
maxTurnToolResultChars: options.maxTurnToolResultChars,
|
|
1565
|
+
transformToolResult: options.transformToolResult,
|
|
1256
1566
|
toolMap,
|
|
1257
1567
|
invalidToolArgumentCounts,
|
|
1258
1568
|
markFatalToolArgumentError,
|
|
@@ -1261,7 +1571,15 @@ async function* agentLoop(messages, options) {
|
|
|
1261
1571
|
const hasSequentialToolCall = toolCalls.some(
|
|
1262
1572
|
(toolCall) => toolMap.get(toolCall.name)?.executionMode === "sequential"
|
|
1263
1573
|
);
|
|
1264
|
-
|
|
1574
|
+
let executionResult;
|
|
1575
|
+
try {
|
|
1576
|
+
executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
|
|
1577
|
+
} finally {
|
|
1578
|
+
unsubscribeSteering?.();
|
|
1579
|
+
}
|
|
1580
|
+
if (executionResult.preempted) {
|
|
1581
|
+
diag("steer_preempted_tools", { turn, provider: options.provider, model: options.model });
|
|
1582
|
+
}
|
|
1265
1583
|
messages.push({ role: "tool", content: executionResult.toolResults });
|
|
1266
1584
|
yield { type: "checkpoint", turn };
|
|
1267
1585
|
const toolsAborted = executionResult.aborted;
|
|
@@ -1418,7 +1736,7 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1418
1736
|
options.seenToolCalls.add(signature);
|
|
1419
1737
|
}
|
|
1420
1738
|
if (!tool) {
|
|
1421
|
-
resultContent =
|
|
1739
|
+
resultContent = unknownToolMessage(toolCall.name, [...options.toolMap.keys()]);
|
|
1422
1740
|
isError = true;
|
|
1423
1741
|
} else {
|
|
1424
1742
|
try {
|
|
@@ -1435,8 +1753,12 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1435
1753
|
}
|
|
1436
1754
|
const callerSignal = options.signal;
|
|
1437
1755
|
const toolTimeout = AbortSignal.timeout(tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS);
|
|
1756
|
+
const preemptSignal = isInterruptibleTool(tool, toolCall.name) ? options.preemptSignal : void 0;
|
|
1757
|
+
const toolSignals = [callerSignal, toolTimeout, preemptSignal].filter(
|
|
1758
|
+
(s) => s !== void 0
|
|
1759
|
+
);
|
|
1438
1760
|
const ctx = {
|
|
1439
|
-
signal:
|
|
1761
|
+
signal: toolSignals.length === 1 ? toolTimeout : AbortSignal.any(toolSignals),
|
|
1440
1762
|
toolCallId: toolCall.id,
|
|
1441
1763
|
onUpdate: (update) => {
|
|
1442
1764
|
pushEvent({
|
|
@@ -1446,13 +1768,25 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1446
1768
|
});
|
|
1447
1769
|
}
|
|
1448
1770
|
};
|
|
1771
|
+
const hardSignals = [callerSignal, toolTimeout].filter(
|
|
1772
|
+
(s) => s !== void 0
|
|
1773
|
+
);
|
|
1449
1774
|
const raw = await abortablePromise(
|
|
1450
1775
|
Promise.resolve().then(() => tool.execute(parsed, ctx)),
|
|
1451
|
-
|
|
1776
|
+
hardSignals.length === 1 ? toolTimeout : AbortSignal.any(hardSignals)
|
|
1452
1777
|
);
|
|
1453
1778
|
const normalized = normalizeToolResult(raw);
|
|
1454
1779
|
resultContent = redactValue(normalized.content, toolRedactionOptions());
|
|
1455
1780
|
details = redactValue(normalized.details, toolRedactionOptions());
|
|
1781
|
+
if (options.transformToolResult) {
|
|
1782
|
+
try {
|
|
1783
|
+
resultContent = options.transformToolResult(
|
|
1784
|
+
{ name: toolCall.name, args: toolCall.args },
|
|
1785
|
+
resultContent
|
|
1786
|
+
);
|
|
1787
|
+
} catch {
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1456
1790
|
for (const key of options.invalidToolArgumentCounts.keys()) {
|
|
1457
1791
|
if (key.startsWith(`${toolCall.name}:`)) options.invalidToolArgumentCounts.delete(key);
|
|
1458
1792
|
}
|
|
@@ -1464,8 +1798,10 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1464
1798
|
const failureCount = (options.invalidToolArgumentCounts.get(failureKey) ?? 0) + 1;
|
|
1465
1799
|
options.invalidToolArgumentCounts.set(failureKey, failureCount);
|
|
1466
1800
|
invalidArgAttempt = failureCount;
|
|
1801
|
+
const hints = argumentHints(tool, toolCall.args);
|
|
1467
1802
|
resultContent = `Invalid arguments for tool \`${toolCall.name}\`:
|
|
1468
|
-
` + prettyError +
|
|
1803
|
+
` + prettyError + (hints ? `
|
|
1804
|
+
${hints}` : "") + "\nRe-issue the call with each field as the correct type.";
|
|
1469
1805
|
if (failureCount >= 3) {
|
|
1470
1806
|
const recoverable = Object.keys(toolCall.args ?? {}).length === 0;
|
|
1471
1807
|
options.markFatalToolArgumentError(
|
|
@@ -1492,6 +1828,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1492
1828
|
}
|
|
1493
1829
|
resultContent = redactValue(resultContent, toolRedactionOptions());
|
|
1494
1830
|
details = redactValue(details, toolRedactionOptions());
|
|
1831
|
+
if (options.preemptSignal?.aborted && isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name)) {
|
|
1832
|
+
const marked = markSteerInterrupted({
|
|
1833
|
+
toolCallId: toolCall.id,
|
|
1834
|
+
content: resultContent,
|
|
1835
|
+
isError
|
|
1836
|
+
});
|
|
1837
|
+
resultContent = marked.content;
|
|
1838
|
+
isError = true;
|
|
1839
|
+
}
|
|
1495
1840
|
const durationMs = Date.now() - startTime;
|
|
1496
1841
|
pushEvent({
|
|
1497
1842
|
type: "tool_call_end",
|
|
@@ -1504,6 +1849,59 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1504
1849
|
});
|
|
1505
1850
|
return { toolCallId: toolCall.id, content: resultContent, isError };
|
|
1506
1851
|
}
|
|
1852
|
+
function createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds) {
|
|
1853
|
+
const running = /* @__PURE__ */ new Map();
|
|
1854
|
+
let graceTimer;
|
|
1855
|
+
const preemptSignal = options.preemptSignal;
|
|
1856
|
+
const abandon = () => {
|
|
1857
|
+
graceTimer = void 0;
|
|
1858
|
+
for (const [id, entry] of running) {
|
|
1859
|
+
if (!entry.interruptible || resultsById.has(id)) continue;
|
|
1860
|
+
resultsById.set(id, { toolCallId: id, content: STEER_INTERRUPTED_TEXT, isError: true });
|
|
1861
|
+
pushToolEvent(eventStream, state, {
|
|
1862
|
+
type: "tool_call_end",
|
|
1863
|
+
toolCallId: id,
|
|
1864
|
+
result: STEER_INTERRUPTED_TEXT,
|
|
1865
|
+
isError: true,
|
|
1866
|
+
durationMs: Date.now() - entry.startedAt
|
|
1867
|
+
});
|
|
1868
|
+
running.delete(id);
|
|
1869
|
+
}
|
|
1870
|
+
if (running.size === 0) eventStream.close();
|
|
1871
|
+
};
|
|
1872
|
+
const onPreempt = () => {
|
|
1873
|
+
if (graceTimer === void 0) graceTimer = setTimeout(abandon, PREEMPT_GRACE_MS);
|
|
1874
|
+
};
|
|
1875
|
+
preemptSignal?.addEventListener("abort", onPreempt, { once: true });
|
|
1876
|
+
return {
|
|
1877
|
+
async run(toolCall) {
|
|
1878
|
+
if (options.preemptSignal?.aborted) return;
|
|
1879
|
+
dispatchedIds.add(toolCall.id);
|
|
1880
|
+
const interruptible = isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name);
|
|
1881
|
+
running.set(toolCall.id, { toolCall, interruptible, startedAt: Date.now() });
|
|
1882
|
+
const record = await executeSingleToolCall(
|
|
1883
|
+
toolCall,
|
|
1884
|
+
options,
|
|
1885
|
+
(event) => pushToolEvent(eventStream, state, event)
|
|
1886
|
+
);
|
|
1887
|
+
running.delete(toolCall.id);
|
|
1888
|
+
if (resultsById.has(toolCall.id)) return;
|
|
1889
|
+
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);
|
|
1890
|
+
resultsById.set(
|
|
1891
|
+
toolCall.id,
|
|
1892
|
+
interruptible && preemptSignal?.aborted && !alreadyMarked ? markSteerInterrupted(record) : record
|
|
1893
|
+
);
|
|
1894
|
+
if (preemptSignal?.aborted && graceTimer === void 0 && running.size === 0) {
|
|
1895
|
+
eventStream.close();
|
|
1896
|
+
}
|
|
1897
|
+
},
|
|
1898
|
+
preempted: () => preemptSignal?.aborted === true,
|
|
1899
|
+
dispose: () => {
|
|
1900
|
+
if (graceTimer !== void 0) clearTimeout(graceTimer);
|
|
1901
|
+
preemptSignal?.removeEventListener("abort", onPreempt);
|
|
1902
|
+
}
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1507
1905
|
async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
|
|
1508
1906
|
const eventStream = new EventStream();
|
|
1509
1907
|
const state = { finalized: false };
|
|
@@ -1511,6 +1909,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
|
|
|
1511
1909
|
const dispatchedIds = /* @__PURE__ */ new Set();
|
|
1512
1910
|
const abortHandler = () => eventStream.abort(new Error("aborted"));
|
|
1513
1911
|
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
1912
|
+
const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
|
|
1514
1913
|
const phases = [];
|
|
1515
1914
|
let currentParallel = [];
|
|
1516
1915
|
for (const toolCall of toolCalls) {
|
|
@@ -1531,40 +1930,18 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
|
|
|
1531
1930
|
void (async () => {
|
|
1532
1931
|
try {
|
|
1533
1932
|
for (const phase of phases) {
|
|
1534
|
-
if (options.signal?.aborted) break;
|
|
1933
|
+
if (options.signal?.aborted || options.preemptSignal?.aborted) break;
|
|
1535
1934
|
if (phase.sequential) {
|
|
1536
1935
|
const signature = JSON.stringify([
|
|
1537
1936
|
phase.sequential.name,
|
|
1538
1937
|
canonicalToolArgs(phase.sequential.args)
|
|
1539
1938
|
]);
|
|
1540
1939
|
if (!options.seenToolCalls.has(signature)) options.seenToolCalls.clear();
|
|
1541
|
-
|
|
1542
|
-
const record = await executeSingleToolCall(
|
|
1543
|
-
phase.sequential,
|
|
1544
|
-
options,
|
|
1545
|
-
(event) => pushToolEvent(eventStream, state, event)
|
|
1546
|
-
);
|
|
1547
|
-
resultsById.set(record.toolCallId, record);
|
|
1940
|
+
await tracker.run(phase.sequential);
|
|
1548
1941
|
} else if (phase.parallel.length === 1) {
|
|
1549
|
-
|
|
1550
|
-
const record = await executeSingleToolCall(
|
|
1551
|
-
phase.parallel[0],
|
|
1552
|
-
options,
|
|
1553
|
-
(event) => pushToolEvent(eventStream, state, event)
|
|
1554
|
-
);
|
|
1555
|
-
resultsById.set(record.toolCallId, record);
|
|
1942
|
+
await tracker.run(phase.parallel[0]);
|
|
1556
1943
|
} else {
|
|
1557
|
-
await Promise.all(
|
|
1558
|
-
phase.parallel.map(async (toolCall) => {
|
|
1559
|
-
dispatchedIds.add(toolCall.id);
|
|
1560
|
-
const record = await executeSingleToolCall(
|
|
1561
|
-
toolCall,
|
|
1562
|
-
options,
|
|
1563
|
-
(event) => pushToolEvent(eventStream, state, event)
|
|
1564
|
-
);
|
|
1565
|
-
resultsById.set(record.toolCallId, record);
|
|
1566
|
-
})
|
|
1567
|
-
);
|
|
1944
|
+
await Promise.all(phase.parallel.map((toolCall) => tracker.run(toolCall)));
|
|
1568
1945
|
}
|
|
1569
1946
|
}
|
|
1570
1947
|
if (!state.finalized) eventStream.close();
|
|
@@ -1585,12 +1962,13 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
|
|
|
1585
1962
|
}
|
|
1586
1963
|
} finally {
|
|
1587
1964
|
options.signal?.removeEventListener("abort", abortHandler);
|
|
1965
|
+
tracker.dispose();
|
|
1588
1966
|
state.finalized = true;
|
|
1589
1967
|
}
|
|
1590
1968
|
const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
|
|
1591
1969
|
capToolResults(toolResults, options.maxToolResultChars);
|
|
1592
1970
|
capTurnToolResults(toolResults, options.maxTurnToolResultChars);
|
|
1593
|
-
return { toolResults, aborted };
|
|
1971
|
+
return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
|
|
1594
1972
|
}
|
|
1595
1973
|
async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
|
|
1596
1974
|
const eventStream = new EventStream();
|
|
@@ -1599,17 +1977,8 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
|
|
|
1599
1977
|
const dispatchedIds = /* @__PURE__ */ new Set();
|
|
1600
1978
|
const abortHandler = () => eventStream.abort(new Error("aborted"));
|
|
1601
1979
|
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
dispatchedIds.add(toolCall.id);
|
|
1605
|
-
const record = await executeSingleToolCall(
|
|
1606
|
-
toolCall,
|
|
1607
|
-
options,
|
|
1608
|
-
(event) => pushToolEvent(eventStream, state, event)
|
|
1609
|
-
);
|
|
1610
|
-
resultsById.set(record.toolCallId, record);
|
|
1611
|
-
})
|
|
1612
|
-
).then(() => {
|
|
1980
|
+
const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
|
|
1981
|
+
Promise.all(toolCalls.map((toolCall) => tracker.run(toolCall))).then(() => {
|
|
1613
1982
|
if (!state.finalized) eventStream.close();
|
|
1614
1983
|
}).catch((err) => {
|
|
1615
1984
|
if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1627,12 +1996,13 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
|
|
|
1627
1996
|
}
|
|
1628
1997
|
} finally {
|
|
1629
1998
|
options.signal?.removeEventListener("abort", abortHandler);
|
|
1999
|
+
tracker.dispose();
|
|
1630
2000
|
state.finalized = true;
|
|
1631
2001
|
}
|
|
1632
2002
|
const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
|
|
1633
2003
|
capToolResults(toolResults, options.maxToolResultChars);
|
|
1634
2004
|
capTurnToolResults(toolResults, options.maxTurnToolResultChars);
|
|
1635
|
-
return { toolResults, aborted };
|
|
2005
|
+
return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
|
|
1636
2006
|
}
|
|
1637
2007
|
function cancelledBeforeStartText(name) {
|
|
1638
2008
|
return `\`${name}\` was cancelled before it started, so it had no effect. Safe to retry.`;
|
|
@@ -1799,6 +2169,19 @@ function sanitizeOrphanedServerTools(messages) {
|
|
|
1799
2169
|
break;
|
|
1800
2170
|
}
|
|
1801
2171
|
}
|
|
2172
|
+
function countMessageChars(messages) {
|
|
2173
|
+
let chars = 0;
|
|
2174
|
+
for (const m of messages) {
|
|
2175
|
+
if (typeof m.content === "string") chars += m.content.length;
|
|
2176
|
+
else if (Array.isArray(m.content)) {
|
|
2177
|
+
for (const p of m.content) {
|
|
2178
|
+
if ("text" in p && typeof p.text === "string") chars += p.text.length;
|
|
2179
|
+
if ("content" in p && typeof p.content === "string") chars += p.content.length;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return chars;
|
|
2184
|
+
}
|
|
1802
2185
|
function repairToolPairingAdjacent(messages) {
|
|
1803
2186
|
for (let i = 0; i < messages.length; i++) {
|
|
1804
2187
|
const msg = messages[i];
|
|
@@ -1915,6 +2298,7 @@ var Agent = class {
|
|
|
1915
2298
|
options;
|
|
1916
2299
|
steeringQueue = [];
|
|
1917
2300
|
followUpQueue = [];
|
|
2301
|
+
steeringListeners = /* @__PURE__ */ new Set();
|
|
1918
2302
|
constructor(options) {
|
|
1919
2303
|
this.options = options;
|
|
1920
2304
|
if (options.system) {
|
|
@@ -1939,9 +2323,14 @@ var Agent = class {
|
|
|
1939
2323
|
get running() {
|
|
1940
2324
|
return this._running;
|
|
1941
2325
|
}
|
|
1942
|
-
/**
|
|
2326
|
+
/**
|
|
2327
|
+
* Queue a steering message. Instant interrupt: if tools are running, the
|
|
2328
|
+
* interruptible ones are preempted right away and the message is injected
|
|
2329
|
+
* before the next model call (the turn continues — unlike abort/Stop).
|
|
2330
|
+
*/
|
|
1943
2331
|
steer(msg) {
|
|
1944
2332
|
this.steeringQueue.push(msg);
|
|
2333
|
+
for (const listener of [...this.steeringListeners]) listener();
|
|
1945
2334
|
}
|
|
1946
2335
|
/** Queue a follow-up message for injection when the agent would otherwise stop. */
|
|
1947
2336
|
followUp(msg) {
|
|
@@ -1961,6 +2350,14 @@ var Agent = class {
|
|
|
1961
2350
|
const all = [...callerResult ?? [], ...queued];
|
|
1962
2351
|
return all.length > 0 ? all : null;
|
|
1963
2352
|
},
|
|
2353
|
+
onSteeringAvailable: (listener) => {
|
|
2354
|
+
this.steeringListeners.add(listener);
|
|
2355
|
+
const unsubscribeCaller = this.options.onSteeringAvailable?.(listener);
|
|
2356
|
+
return () => {
|
|
2357
|
+
this.steeringListeners.delete(listener);
|
|
2358
|
+
unsubscribeCaller?.();
|
|
2359
|
+
};
|
|
2360
|
+
},
|
|
1964
2361
|
getFollowUpMessages: async () => {
|
|
1965
2362
|
const callerResult = await this.options.getFollowUpMessages?.() ?? [];
|
|
1966
2363
|
const queued = this.followUpQueue.splice(0);
|
|
@@ -1977,7 +2374,12 @@ var Agent = class {
|
|
|
1977
2374
|
export {
|
|
1978
2375
|
Agent,
|
|
1979
2376
|
AgentStream,
|
|
2377
|
+
DEFAULT_STREAM_RULE_MAX_RETRIES,
|
|
2378
|
+
DEFAULT_STREAM_RULE_WINDOW_CHARS,
|
|
2379
|
+
JsonEscapeDecoder,
|
|
2380
|
+
StreamRuleMonitor,
|
|
1980
2381
|
agentLoop,
|
|
2382
|
+
buildStreamRuleReminder,
|
|
1981
2383
|
cancelledBeforeStartText,
|
|
1982
2384
|
indeterminateOutcomeText,
|
|
1983
2385
|
isAbortError,
|
|
@@ -1985,6 +2387,7 @@ export {
|
|
|
1985
2387
|
isContextOverflow,
|
|
1986
2388
|
isLocalBackendUrl,
|
|
1987
2389
|
isUsageLimitError,
|
|
2390
|
+
repairToolPairingAdjacent,
|
|
1988
2391
|
setStreamDiagnostic
|
|
1989
2392
|
};
|
|
1990
2393
|
//# sourceMappingURL=index.js.map
|