@prestyj/agent 5.28.1 → 5.29.1
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 +488 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +145 -5
- package/dist/index.d.ts +145 -5
- package/dist/index.js +460 -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,8 +868,11 @@ 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,
|
|
874
|
+
responsesLite: options.responsesLite,
|
|
875
|
+
strictTools: options.strictTools,
|
|
627
876
|
supportsImages: options.supportsImages,
|
|
628
877
|
supportsVideo: options.supportsVideo,
|
|
629
878
|
compaction: options.compaction,
|
|
@@ -699,8 +948,14 @@ async function* agentLoop(messages, options) {
|
|
|
699
948
|
}
|
|
700
949
|
if (event.type === "text_delta") {
|
|
701
950
|
attemptText += event.text;
|
|
951
|
+
streamRuleHit = streamRuleMonitor?.checkText(event.text) ?? null;
|
|
952
|
+
if (streamRuleHit) {
|
|
953
|
+
streamController.abort();
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
702
956
|
yield { type: "text_delta", text: event.text };
|
|
703
957
|
} else if (event.type === "thinking_delta") {
|
|
958
|
+
attemptThinkingChars += event.text.length;
|
|
704
959
|
yield { type: "thinking_delta", text: event.text };
|
|
705
960
|
} else if (event.type === "server_toolcall") {
|
|
706
961
|
yield {
|
|
@@ -721,6 +976,11 @@ async function* agentLoop(messages, options) {
|
|
|
721
976
|
toolcallDeltaChars += chunkChars;
|
|
722
977
|
toolcallDeltaCount++;
|
|
723
978
|
toolcallNoProgressCount = chunkChars > 0 ? 0 : toolcallNoProgressCount + 1;
|
|
979
|
+
streamRuleHit = streamRuleMonitor?.checkToolArgs(event.id, event.name, event.argsJson ?? "") ?? null;
|
|
980
|
+
if (streamRuleHit) {
|
|
981
|
+
streamController.abort();
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
724
984
|
if (!runawayDetected && (toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS || toolcallNoProgressCount > MAX_TOOLCALL_NO_PROGRESS_EVENTS)) {
|
|
725
985
|
runawayDetected = {
|
|
726
986
|
kind: toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS ? "chars" : "events",
|
|
@@ -743,6 +1003,54 @@ async function* agentLoop(messages, options) {
|
|
|
743
1003
|
lastYieldEndTime = Date.now();
|
|
744
1004
|
resetIdleTimer();
|
|
745
1005
|
}
|
|
1006
|
+
if (streamRuleHit && streamRuleMonitor) {
|
|
1007
|
+
const attempt = streamRuleMonitor.recordTrigger(streamRuleHit.rules);
|
|
1008
|
+
const ruleNames = streamRuleHit.rules.map((rule) => rule.name);
|
|
1009
|
+
const priorPrompt = latestProviderUsage ? latestProviderUsage.inputTokens + (latestProviderUsage.cacheRead ?? 0) + (latestProviderUsage.cacheWrite ?? 0) : 0;
|
|
1010
|
+
const newChars = priorPrompt > 0 && usageAnchorIndex !== void 0 ? countMessageChars(messages.slice(usageAnchorIndex + 1)) : msgChars;
|
|
1011
|
+
const abortedUsage = {
|
|
1012
|
+
inputTokens: Math.ceil(newChars / 4),
|
|
1013
|
+
outputTokens: Math.ceil(
|
|
1014
|
+
(attemptText.length + attemptThinkingChars + toolcallDeltaChars) / 4
|
|
1015
|
+
),
|
|
1016
|
+
...priorPrompt > 0 ? { cacheRead: priorPrompt } : {}
|
|
1017
|
+
};
|
|
1018
|
+
totalUsage.inputTokens += abortedUsage.inputTokens;
|
|
1019
|
+
totalUsage.outputTokens += abortedUsage.outputTokens;
|
|
1020
|
+
if (abortedUsage.cacheRead) {
|
|
1021
|
+
totalUsage.cacheRead = (totalUsage.cacheRead ?? 0) + abortedUsage.cacheRead;
|
|
1022
|
+
}
|
|
1023
|
+
diag("stream_rule_triggered", {
|
|
1024
|
+
rules: ruleNames.join(","),
|
|
1025
|
+
source: streamRuleHit.source,
|
|
1026
|
+
toolName: streamRuleHit.toolName,
|
|
1027
|
+
attempt,
|
|
1028
|
+
maxAttempts: streamRuleMonitor.maxRetries,
|
|
1029
|
+
discardedChars: attemptText.length + toolcallDeltaChars,
|
|
1030
|
+
provider: options.provider,
|
|
1031
|
+
model: options.model
|
|
1032
|
+
});
|
|
1033
|
+
messages.push(buildStreamRuleReminder(streamRuleHit.rules));
|
|
1034
|
+
yield {
|
|
1035
|
+
type: "stream_rule_triggered",
|
|
1036
|
+
rules: ruleNames,
|
|
1037
|
+
source: streamRuleHit.source,
|
|
1038
|
+
...streamRuleHit.toolName !== void 0 ? { toolName: streamRuleHit.toolName } : {},
|
|
1039
|
+
attempt,
|
|
1040
|
+
maxAttempts: streamRuleMonitor.maxRetries,
|
|
1041
|
+
usage: abortedUsage
|
|
1042
|
+
};
|
|
1043
|
+
yield {
|
|
1044
|
+
type: "retry",
|
|
1045
|
+
reason: "stream_rule",
|
|
1046
|
+
attempt,
|
|
1047
|
+
maxAttempts: streamRuleMonitor.maxRetries,
|
|
1048
|
+
delayMs: 0,
|
|
1049
|
+
silent: true
|
|
1050
|
+
};
|
|
1051
|
+
turn--;
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
746
1054
|
diag("stream_done", {
|
|
747
1055
|
events: streamEventCount,
|
|
748
1056
|
totalMs: Date.now() - streamCallStart,
|
|
@@ -1249,10 +1557,14 @@ async function* agentLoop(messages, options) {
|
|
|
1249
1557
|
fatalToolArgumentRecoverable = recoverable;
|
|
1250
1558
|
fatalToolArgumentToolName = toolName;
|
|
1251
1559
|
};
|
|
1560
|
+
const preempt = new AbortController();
|
|
1561
|
+
const unsubscribeSteering = options.onSteeringAvailable?.(() => preempt.abort());
|
|
1252
1562
|
const executionOptions = {
|
|
1253
1563
|
signal: options.signal,
|
|
1564
|
+
preemptSignal: options.onSteeringAvailable ? preempt.signal : void 0,
|
|
1254
1565
|
maxToolResultChars: options.maxToolResultChars,
|
|
1255
1566
|
maxTurnToolResultChars: options.maxTurnToolResultChars,
|
|
1567
|
+
transformToolResult: options.transformToolResult,
|
|
1256
1568
|
toolMap,
|
|
1257
1569
|
invalidToolArgumentCounts,
|
|
1258
1570
|
markFatalToolArgumentError,
|
|
@@ -1261,7 +1573,15 @@ async function* agentLoop(messages, options) {
|
|
|
1261
1573
|
const hasSequentialToolCall = toolCalls.some(
|
|
1262
1574
|
(toolCall) => toolMap.get(toolCall.name)?.executionMode === "sequential"
|
|
1263
1575
|
);
|
|
1264
|
-
|
|
1576
|
+
let executionResult;
|
|
1577
|
+
try {
|
|
1578
|
+
executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
|
|
1579
|
+
} finally {
|
|
1580
|
+
unsubscribeSteering?.();
|
|
1581
|
+
}
|
|
1582
|
+
if (executionResult.preempted) {
|
|
1583
|
+
diag("steer_preempted_tools", { turn, provider: options.provider, model: options.model });
|
|
1584
|
+
}
|
|
1265
1585
|
messages.push({ role: "tool", content: executionResult.toolResults });
|
|
1266
1586
|
yield { type: "checkpoint", turn };
|
|
1267
1587
|
const toolsAborted = executionResult.aborted;
|
|
@@ -1418,7 +1738,7 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1418
1738
|
options.seenToolCalls.add(signature);
|
|
1419
1739
|
}
|
|
1420
1740
|
if (!tool) {
|
|
1421
|
-
resultContent =
|
|
1741
|
+
resultContent = unknownToolMessage(toolCall.name, [...options.toolMap.keys()]);
|
|
1422
1742
|
isError = true;
|
|
1423
1743
|
} else {
|
|
1424
1744
|
try {
|
|
@@ -1435,8 +1755,12 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1435
1755
|
}
|
|
1436
1756
|
const callerSignal = options.signal;
|
|
1437
1757
|
const toolTimeout = AbortSignal.timeout(tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS);
|
|
1758
|
+
const preemptSignal = isInterruptibleTool(tool, toolCall.name) ? options.preemptSignal : void 0;
|
|
1759
|
+
const toolSignals = [callerSignal, toolTimeout, preemptSignal].filter(
|
|
1760
|
+
(s) => s !== void 0
|
|
1761
|
+
);
|
|
1438
1762
|
const ctx = {
|
|
1439
|
-
signal:
|
|
1763
|
+
signal: toolSignals.length === 1 ? toolTimeout : AbortSignal.any(toolSignals),
|
|
1440
1764
|
toolCallId: toolCall.id,
|
|
1441
1765
|
onUpdate: (update) => {
|
|
1442
1766
|
pushEvent({
|
|
@@ -1446,13 +1770,25 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1446
1770
|
});
|
|
1447
1771
|
}
|
|
1448
1772
|
};
|
|
1773
|
+
const hardSignals = [callerSignal, toolTimeout].filter(
|
|
1774
|
+
(s) => s !== void 0
|
|
1775
|
+
);
|
|
1449
1776
|
const raw = await abortablePromise(
|
|
1450
1777
|
Promise.resolve().then(() => tool.execute(parsed, ctx)),
|
|
1451
|
-
|
|
1778
|
+
hardSignals.length === 1 ? toolTimeout : AbortSignal.any(hardSignals)
|
|
1452
1779
|
);
|
|
1453
1780
|
const normalized = normalizeToolResult(raw);
|
|
1454
1781
|
resultContent = redactValue(normalized.content, toolRedactionOptions());
|
|
1455
1782
|
details = redactValue(normalized.details, toolRedactionOptions());
|
|
1783
|
+
if (options.transformToolResult) {
|
|
1784
|
+
try {
|
|
1785
|
+
resultContent = options.transformToolResult(
|
|
1786
|
+
{ name: toolCall.name, args: toolCall.args },
|
|
1787
|
+
resultContent
|
|
1788
|
+
);
|
|
1789
|
+
} catch {
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1456
1792
|
for (const key of options.invalidToolArgumentCounts.keys()) {
|
|
1457
1793
|
if (key.startsWith(`${toolCall.name}:`)) options.invalidToolArgumentCounts.delete(key);
|
|
1458
1794
|
}
|
|
@@ -1464,8 +1800,10 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1464
1800
|
const failureCount = (options.invalidToolArgumentCounts.get(failureKey) ?? 0) + 1;
|
|
1465
1801
|
options.invalidToolArgumentCounts.set(failureKey, failureCount);
|
|
1466
1802
|
invalidArgAttempt = failureCount;
|
|
1803
|
+
const hints = argumentHints(tool, toolCall.args);
|
|
1467
1804
|
resultContent = `Invalid arguments for tool \`${toolCall.name}\`:
|
|
1468
|
-
` + prettyError +
|
|
1805
|
+
` + prettyError + (hints ? `
|
|
1806
|
+
${hints}` : "") + "\nRe-issue the call with each field as the correct type.";
|
|
1469
1807
|
if (failureCount >= 3) {
|
|
1470
1808
|
const recoverable = Object.keys(toolCall.args ?? {}).length === 0;
|
|
1471
1809
|
options.markFatalToolArgumentError(
|
|
@@ -1492,6 +1830,15 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1492
1830
|
}
|
|
1493
1831
|
resultContent = redactValue(resultContent, toolRedactionOptions());
|
|
1494
1832
|
details = redactValue(details, toolRedactionOptions());
|
|
1833
|
+
if (options.preemptSignal?.aborted && isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name)) {
|
|
1834
|
+
const marked = markSteerInterrupted({
|
|
1835
|
+
toolCallId: toolCall.id,
|
|
1836
|
+
content: resultContent,
|
|
1837
|
+
isError
|
|
1838
|
+
});
|
|
1839
|
+
resultContent = marked.content;
|
|
1840
|
+
isError = true;
|
|
1841
|
+
}
|
|
1495
1842
|
const durationMs = Date.now() - startTime;
|
|
1496
1843
|
pushEvent({
|
|
1497
1844
|
type: "tool_call_end",
|
|
@@ -1504,6 +1851,59 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1504
1851
|
});
|
|
1505
1852
|
return { toolCallId: toolCall.id, content: resultContent, isError };
|
|
1506
1853
|
}
|
|
1854
|
+
function createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds) {
|
|
1855
|
+
const running = /* @__PURE__ */ new Map();
|
|
1856
|
+
let graceTimer;
|
|
1857
|
+
const preemptSignal = options.preemptSignal;
|
|
1858
|
+
const abandon = () => {
|
|
1859
|
+
graceTimer = void 0;
|
|
1860
|
+
for (const [id, entry] of running) {
|
|
1861
|
+
if (!entry.interruptible || resultsById.has(id)) continue;
|
|
1862
|
+
resultsById.set(id, { toolCallId: id, content: STEER_INTERRUPTED_TEXT, isError: true });
|
|
1863
|
+
pushToolEvent(eventStream, state, {
|
|
1864
|
+
type: "tool_call_end",
|
|
1865
|
+
toolCallId: id,
|
|
1866
|
+
result: STEER_INTERRUPTED_TEXT,
|
|
1867
|
+
isError: true,
|
|
1868
|
+
durationMs: Date.now() - entry.startedAt
|
|
1869
|
+
});
|
|
1870
|
+
running.delete(id);
|
|
1871
|
+
}
|
|
1872
|
+
if (running.size === 0) eventStream.close();
|
|
1873
|
+
};
|
|
1874
|
+
const onPreempt = () => {
|
|
1875
|
+
if (graceTimer === void 0) graceTimer = setTimeout(abandon, PREEMPT_GRACE_MS);
|
|
1876
|
+
};
|
|
1877
|
+
preemptSignal?.addEventListener("abort", onPreempt, { once: true });
|
|
1878
|
+
return {
|
|
1879
|
+
async run(toolCall) {
|
|
1880
|
+
if (options.preemptSignal?.aborted) return;
|
|
1881
|
+
dispatchedIds.add(toolCall.id);
|
|
1882
|
+
const interruptible = isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name);
|
|
1883
|
+
running.set(toolCall.id, { toolCall, interruptible, startedAt: Date.now() });
|
|
1884
|
+
const record = await executeSingleToolCall(
|
|
1885
|
+
toolCall,
|
|
1886
|
+
options,
|
|
1887
|
+
(event) => pushToolEvent(eventStream, state, event)
|
|
1888
|
+
);
|
|
1889
|
+
running.delete(toolCall.id);
|
|
1890
|
+
if (resultsById.has(toolCall.id)) return;
|
|
1891
|
+
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);
|
|
1892
|
+
resultsById.set(
|
|
1893
|
+
toolCall.id,
|
|
1894
|
+
interruptible && preemptSignal?.aborted && !alreadyMarked ? markSteerInterrupted(record) : record
|
|
1895
|
+
);
|
|
1896
|
+
if (preemptSignal?.aborted && graceTimer === void 0 && running.size === 0) {
|
|
1897
|
+
eventStream.close();
|
|
1898
|
+
}
|
|
1899
|
+
},
|
|
1900
|
+
preempted: () => preemptSignal?.aborted === true,
|
|
1901
|
+
dispose: () => {
|
|
1902
|
+
if (graceTimer !== void 0) clearTimeout(graceTimer);
|
|
1903
|
+
preemptSignal?.removeEventListener("abort", onPreempt);
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1507
1907
|
async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
|
|
1508
1908
|
const eventStream = new EventStream();
|
|
1509
1909
|
const state = { finalized: false };
|
|
@@ -1511,6 +1911,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
|
|
|
1511
1911
|
const dispatchedIds = /* @__PURE__ */ new Set();
|
|
1512
1912
|
const abortHandler = () => eventStream.abort(new Error("aborted"));
|
|
1513
1913
|
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
1914
|
+
const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
|
|
1514
1915
|
const phases = [];
|
|
1515
1916
|
let currentParallel = [];
|
|
1516
1917
|
for (const toolCall of toolCalls) {
|
|
@@ -1531,40 +1932,18 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
|
|
|
1531
1932
|
void (async () => {
|
|
1532
1933
|
try {
|
|
1533
1934
|
for (const phase of phases) {
|
|
1534
|
-
if (options.signal?.aborted) break;
|
|
1935
|
+
if (options.signal?.aborted || options.preemptSignal?.aborted) break;
|
|
1535
1936
|
if (phase.sequential) {
|
|
1536
1937
|
const signature = JSON.stringify([
|
|
1537
1938
|
phase.sequential.name,
|
|
1538
1939
|
canonicalToolArgs(phase.sequential.args)
|
|
1539
1940
|
]);
|
|
1540
1941
|
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);
|
|
1942
|
+
await tracker.run(phase.sequential);
|
|
1548
1943
|
} 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);
|
|
1944
|
+
await tracker.run(phase.parallel[0]);
|
|
1556
1945
|
} 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
|
-
);
|
|
1946
|
+
await Promise.all(phase.parallel.map((toolCall) => tracker.run(toolCall)));
|
|
1568
1947
|
}
|
|
1569
1948
|
}
|
|
1570
1949
|
if (!state.finalized) eventStream.close();
|
|
@@ -1585,12 +1964,13 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
|
|
|
1585
1964
|
}
|
|
1586
1965
|
} finally {
|
|
1587
1966
|
options.signal?.removeEventListener("abort", abortHandler);
|
|
1967
|
+
tracker.dispose();
|
|
1588
1968
|
state.finalized = true;
|
|
1589
1969
|
}
|
|
1590
1970
|
const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
|
|
1591
1971
|
capToolResults(toolResults, options.maxToolResultChars);
|
|
1592
1972
|
capTurnToolResults(toolResults, options.maxTurnToolResultChars);
|
|
1593
|
-
return { toolResults, aborted };
|
|
1973
|
+
return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
|
|
1594
1974
|
}
|
|
1595
1975
|
async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
|
|
1596
1976
|
const eventStream = new EventStream();
|
|
@@ -1599,17 +1979,8 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
|
|
|
1599
1979
|
const dispatchedIds = /* @__PURE__ */ new Set();
|
|
1600
1980
|
const abortHandler = () => eventStream.abort(new Error("aborted"));
|
|
1601
1981
|
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(() => {
|
|
1982
|
+
const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
|
|
1983
|
+
Promise.all(toolCalls.map((toolCall) => tracker.run(toolCall))).then(() => {
|
|
1613
1984
|
if (!state.finalized) eventStream.close();
|
|
1614
1985
|
}).catch((err) => {
|
|
1615
1986
|
if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1627,12 +1998,13 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
|
|
|
1627
1998
|
}
|
|
1628
1999
|
} finally {
|
|
1629
2000
|
options.signal?.removeEventListener("abort", abortHandler);
|
|
2001
|
+
tracker.dispose();
|
|
1630
2002
|
state.finalized = true;
|
|
1631
2003
|
}
|
|
1632
2004
|
const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
|
|
1633
2005
|
capToolResults(toolResults, options.maxToolResultChars);
|
|
1634
2006
|
capTurnToolResults(toolResults, options.maxTurnToolResultChars);
|
|
1635
|
-
return { toolResults, aborted };
|
|
2007
|
+
return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
|
|
1636
2008
|
}
|
|
1637
2009
|
function cancelledBeforeStartText(name) {
|
|
1638
2010
|
return `\`${name}\` was cancelled before it started, so it had no effect. Safe to retry.`;
|
|
@@ -1799,6 +2171,19 @@ function sanitizeOrphanedServerTools(messages) {
|
|
|
1799
2171
|
break;
|
|
1800
2172
|
}
|
|
1801
2173
|
}
|
|
2174
|
+
function countMessageChars(messages) {
|
|
2175
|
+
let chars = 0;
|
|
2176
|
+
for (const m of messages) {
|
|
2177
|
+
if (typeof m.content === "string") chars += m.content.length;
|
|
2178
|
+
else if (Array.isArray(m.content)) {
|
|
2179
|
+
for (const p of m.content) {
|
|
2180
|
+
if ("text" in p && typeof p.text === "string") chars += p.text.length;
|
|
2181
|
+
if ("content" in p && typeof p.content === "string") chars += p.content.length;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
return chars;
|
|
2186
|
+
}
|
|
1802
2187
|
function repairToolPairingAdjacent(messages) {
|
|
1803
2188
|
for (let i = 0; i < messages.length; i++) {
|
|
1804
2189
|
const msg = messages[i];
|
|
@@ -1915,6 +2300,7 @@ var Agent = class {
|
|
|
1915
2300
|
options;
|
|
1916
2301
|
steeringQueue = [];
|
|
1917
2302
|
followUpQueue = [];
|
|
2303
|
+
steeringListeners = /* @__PURE__ */ new Set();
|
|
1918
2304
|
constructor(options) {
|
|
1919
2305
|
this.options = options;
|
|
1920
2306
|
if (options.system) {
|
|
@@ -1939,9 +2325,14 @@ var Agent = class {
|
|
|
1939
2325
|
get running() {
|
|
1940
2326
|
return this._running;
|
|
1941
2327
|
}
|
|
1942
|
-
/**
|
|
2328
|
+
/**
|
|
2329
|
+
* Queue a steering message. Instant interrupt: if tools are running, the
|
|
2330
|
+
* interruptible ones are preempted right away and the message is injected
|
|
2331
|
+
* before the next model call (the turn continues — unlike abort/Stop).
|
|
2332
|
+
*/
|
|
1943
2333
|
steer(msg) {
|
|
1944
2334
|
this.steeringQueue.push(msg);
|
|
2335
|
+
for (const listener of [...this.steeringListeners]) listener();
|
|
1945
2336
|
}
|
|
1946
2337
|
/** Queue a follow-up message for injection when the agent would otherwise stop. */
|
|
1947
2338
|
followUp(msg) {
|
|
@@ -1961,6 +2352,14 @@ var Agent = class {
|
|
|
1961
2352
|
const all = [...callerResult ?? [], ...queued];
|
|
1962
2353
|
return all.length > 0 ? all : null;
|
|
1963
2354
|
},
|
|
2355
|
+
onSteeringAvailable: (listener) => {
|
|
2356
|
+
this.steeringListeners.add(listener);
|
|
2357
|
+
const unsubscribeCaller = this.options.onSteeringAvailable?.(listener);
|
|
2358
|
+
return () => {
|
|
2359
|
+
this.steeringListeners.delete(listener);
|
|
2360
|
+
unsubscribeCaller?.();
|
|
2361
|
+
};
|
|
2362
|
+
},
|
|
1964
2363
|
getFollowUpMessages: async () => {
|
|
1965
2364
|
const callerResult = await this.options.getFollowUpMessages?.() ?? [];
|
|
1966
2365
|
const queued = this.followUpQueue.splice(0);
|
|
@@ -1977,7 +2376,12 @@ var Agent = class {
|
|
|
1977
2376
|
export {
|
|
1978
2377
|
Agent,
|
|
1979
2378
|
AgentStream,
|
|
2379
|
+
DEFAULT_STREAM_RULE_MAX_RETRIES,
|
|
2380
|
+
DEFAULT_STREAM_RULE_WINDOW_CHARS,
|
|
2381
|
+
JsonEscapeDecoder,
|
|
2382
|
+
StreamRuleMonitor,
|
|
1980
2383
|
agentLoop,
|
|
2384
|
+
buildStreamRuleReminder,
|
|
1981
2385
|
cancelledBeforeStartText,
|
|
1982
2386
|
indeterminateOutcomeText,
|
|
1983
2387
|
isAbortError,
|
|
@@ -1985,6 +2389,7 @@ export {
|
|
|
1985
2389
|
isContextOverflow,
|
|
1986
2390
|
isLocalBackendUrl,
|
|
1987
2391
|
isUsageLimitError,
|
|
2392
|
+
repairToolPairingAdjacent,
|
|
1988
2393
|
setStreamDiagnostic
|
|
1989
2394
|
};
|
|
1990
2395
|
//# sourceMappingURL=index.js.map
|