@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 CHANGED
@@ -22,7 +22,12 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  Agent: () => Agent,
24
24
  AgentStream: () => AgentStream,
25
+ DEFAULT_STREAM_RULE_MAX_RETRIES: () => DEFAULT_STREAM_RULE_MAX_RETRIES,
26
+ DEFAULT_STREAM_RULE_WINDOW_CHARS: () => DEFAULT_STREAM_RULE_WINDOW_CHARS,
27
+ JsonEscapeDecoder: () => JsonEscapeDecoder,
28
+ StreamRuleMonitor: () => StreamRuleMonitor,
25
29
  agentLoop: () => agentLoop,
30
+ buildStreamRuleReminder: () => buildStreamRuleReminder,
26
31
  cancelledBeforeStartText: () => cancelledBeforeStartText,
27
32
  indeterminateOutcomeText: () => indeterminateOutcomeText,
28
33
  isAbortError: () => isAbortError,
@@ -30,16 +35,17 @@ __export(index_exports, {
30
35
  isContextOverflow: () => isContextOverflow,
31
36
  isLocalBackendUrl: () => isLocalBackendUrl,
32
37
  isUsageLimitError: () => isUsageLimitError,
38
+ repairToolPairingAdjacent: () => repairToolPairingAdjacent,
33
39
  setStreamDiagnostic: () => setStreamDiagnostic
34
40
  });
35
41
  module.exports = __toCommonJS(index_exports);
36
42
 
37
43
  // src/agent.ts
38
- var import_ai2 = require("@prestyj/ai");
44
+ var import_ai3 = require("@prestyj/ai");
39
45
 
40
46
  // src/agent-loop.ts
41
47
  var import_zod = require("zod");
42
- var import_ai = require("@prestyj/ai");
48
+ var import_ai2 = require("@prestyj/ai");
43
49
 
44
50
  // src/local-backend.ts
45
51
  function isLocalBackendUrl(baseUrl) {
@@ -63,6 +69,107 @@ function isLocalBackendUrl(baseUrl) {
63
69
  return false;
64
70
  }
65
71
 
72
+ // src/tool-call-hints.ts
73
+ var import_ai = require("@prestyj/ai");
74
+ function normalizeName(name) {
75
+ return name.toLowerCase().replace(/[^a-z0-9]/g, "");
76
+ }
77
+ function editDistance(a, b) {
78
+ let beforePrevious = [];
79
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
80
+ for (let i = 1; i <= a.length; i++) {
81
+ const current = [i];
82
+ for (let j = 1; j <= b.length; j++) {
83
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
84
+ let distance = Math.min(
85
+ (previous[j] ?? 0) + 1,
86
+ (current[j - 1] ?? 0) + 1,
87
+ (previous[j - 1] ?? 0) + cost
88
+ );
89
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
90
+ distance = Math.min(distance, (beforePrevious[j - 2] ?? 0) + 1);
91
+ }
92
+ current[j] = distance;
93
+ }
94
+ beforePrevious = previous;
95
+ previous = current;
96
+ }
97
+ return previous[b.length] ?? 0;
98
+ }
99
+ function closestName(name, candidates) {
100
+ const target = normalizeName(name);
101
+ if (!target) return void 0;
102
+ const limit = Math.min(3, Math.max(1, Math.floor(target.length / 3)));
103
+ let best;
104
+ for (const candidate of [...candidates].sort()) {
105
+ if (candidate === name) continue;
106
+ const distance = editDistance(target, normalizeName(candidate));
107
+ if (distance > limit) continue;
108
+ if (!best || distance < best.distance) best = { name: candidate, distance };
109
+ }
110
+ return best?.name;
111
+ }
112
+ function unknownToolMessage(name, available) {
113
+ const suggestion = closestName(name, available);
114
+ if (suggestion) return `Unknown tool: ${name}. Did you mean \`${suggestion}\`?`;
115
+ const names = [...available].sort();
116
+ const listed = names.length > 40 ? `${names.slice(0, 40).join(", ")}, \u2026` : names.join(", ");
117
+ const searchHint = available.includes("tool_search") ? " If the capability is not listed, call `tool_search` to load it." : "";
118
+ return `Unknown tool: ${name}. Available tools: ${listed}.${searchHint}`;
119
+ }
120
+ function isRecord(value) {
121
+ return typeof value === "object" && value !== null && !Array.isArray(value);
122
+ }
123
+ function typeLabel(node) {
124
+ if (!isRecord(node)) return "any";
125
+ if (Array.isArray(node.enum)) {
126
+ return node.enum.map((value) => JSON.stringify(value)).join(" | ");
127
+ }
128
+ const variants = node.anyOf ?? node.oneOf;
129
+ if (Array.isArray(variants)) {
130
+ return [...new Set(variants.map(typeLabel))].filter((t) => t !== "null").join(" | ") || "any";
131
+ }
132
+ if (node.type === "array") return `${typeLabel(node.items)}[]`;
133
+ if (Array.isArray(node.type)) {
134
+ return node.type.filter((t) => t !== "null").join(" | ") || "any";
135
+ }
136
+ return typeof node.type === "string" ? node.type : "any";
137
+ }
138
+ function objectShape(tool) {
139
+ let schema;
140
+ try {
141
+ schema = (0, import_ai.resolveToolSchema)(tool);
142
+ } catch {
143
+ return void 0;
144
+ }
145
+ if (!isRecord(schema) || !isRecord(schema.properties)) return void 0;
146
+ const required = new Set(
147
+ Array.isArray(schema.required) ? schema.required.filter((r) => typeof r === "string") : []
148
+ );
149
+ return { properties: schema.properties, required };
150
+ }
151
+ function argumentHints(tool, args) {
152
+ const shape = objectShape(tool);
153
+ if (!shape) return "";
154
+ const names = Object.keys(shape.properties);
155
+ if (names.length === 0) return `Expected arguments: none.`;
156
+ const fields = names.map((name) => {
157
+ const optional = shape.required.has(name) ? "" : "?";
158
+ return `${name}${optional}: ${typeLabel(shape.properties[name])}`;
159
+ });
160
+ const lines = [`Expected arguments: { ${fields.join(", ")} }`];
161
+ if (isRecord(args)) {
162
+ for (const key of Object.keys(args).sort()) {
163
+ if (key in shape.properties) continue;
164
+ const suggestion = closestName(key, names);
165
+ lines.push(
166
+ suggestion ? `Unknown field \`${key}\` \u2014 did you mean \`${suggestion}\`?` : `Unknown field \`${key}\` is not a parameter of this tool.`
167
+ );
168
+ }
169
+ }
170
+ return lines.join("\n");
171
+ }
172
+
66
173
  // src/output-ceiling.ts
67
174
  var TTL_MS = 24 * 60 * 60 * 1e3;
68
175
  var MIN_PLAUSIBLE_CEILING = 256;
@@ -117,12 +224,162 @@ function clampOutputTokens(key, requested) {
117
224
  return requested === void 0 ? ceiling : Math.min(requested, ceiling);
118
225
  }
119
226
 
227
+ // src/stream-rules.ts
228
+ var DEFAULT_STREAM_RULE_MAX_RETRIES = 3;
229
+ var DEFAULT_STREAM_RULE_WINDOW_CHARS = 1024;
230
+ var JSON_ESCAPES = {
231
+ n: "\n",
232
+ r: "\r",
233
+ t: " ",
234
+ b: "\b",
235
+ f: "\f",
236
+ '"': '"',
237
+ "\\": "\\",
238
+ "/": "/"
239
+ };
240
+ var JsonEscapeDecoder = class {
241
+ pending = "";
242
+ push(chunk) {
243
+ const input = this.pending + chunk;
244
+ this.pending = "";
245
+ if (!input.includes("\\")) return input;
246
+ let out = "";
247
+ let i = 0;
248
+ while (i < input.length) {
249
+ const ch = input.charAt(i);
250
+ if (ch !== "\\") {
251
+ out += ch;
252
+ i++;
253
+ continue;
254
+ }
255
+ if (i + 1 >= input.length) {
256
+ this.pending = input.slice(i);
257
+ break;
258
+ }
259
+ const next = input.charAt(i + 1);
260
+ if (next === "u") {
261
+ if (i + 6 > input.length) {
262
+ this.pending = input.slice(i);
263
+ break;
264
+ }
265
+ const hex = input.slice(i + 2, i + 6);
266
+ out += /^[0-9a-fA-F]{4}$/.test(hex) ? String.fromCharCode(Number.parseInt(hex, 16)) : input.slice(i, i + 6);
267
+ i += 6;
268
+ continue;
269
+ }
270
+ out += JSON_ESCAPES[next] ?? next;
271
+ i += 2;
272
+ }
273
+ return out;
274
+ }
275
+ };
276
+ var StreamRuleMonitor = class {
277
+ maxRetries;
278
+ rules;
279
+ windowChars;
280
+ fired = /* @__PURE__ */ new Set();
281
+ retries = 0;
282
+ textWindow = "";
283
+ toolStreams = /* @__PURE__ */ new Map();
284
+ constructor(config) {
285
+ this.rules = config.rules;
286
+ this.maxRetries = Math.max(0, config.maxRetries ?? DEFAULT_STREAM_RULE_MAX_RETRIES);
287
+ this.windowChars = Math.max(64, config.windowChars ?? DEFAULT_STREAM_RULE_WINDOW_CHARS);
288
+ }
289
+ /** False once the retry cap is spent or every rule has fired — skip all matching. */
290
+ get active() {
291
+ return this.retries < this.maxRetries && this.fired.size < this.rules.length;
292
+ }
293
+ get retriesUsed() {
294
+ return this.retries;
295
+ }
296
+ beginAttempt() {
297
+ this.textWindow = "";
298
+ this.toolStreams.clear();
299
+ }
300
+ checkText(delta) {
301
+ if (!this.active || !delta) return null;
302
+ const scan = this.textWindow + delta;
303
+ this.textWindow = scan.slice(-this.windowChars);
304
+ const rules = this.match(scan, "text");
305
+ return rules.length > 0 ? { rules, source: "text" } : null;
306
+ }
307
+ checkToolArgs(id, toolName, argsDelta) {
308
+ if (!this.active || !argsDelta) return null;
309
+ let state = this.toolStreams.get(id);
310
+ if (!state) {
311
+ state = { window: "", decoder: new JsonEscapeDecoder() };
312
+ this.toolStreams.set(id, state);
313
+ }
314
+ const scan = state.window + state.decoder.push(argsDelta);
315
+ state.window = scan.slice(-this.windowChars);
316
+ const rules = this.match(scan, "tool", toolName);
317
+ return rules.length > 0 ? { rules, source: "tool", toolName } : null;
318
+ }
319
+ /** Consume one retry and mark the rules fired. Returns the retry ordinal (1-based). */
320
+ recordTrigger(rules) {
321
+ for (const rule of rules) this.fired.add(rule.name);
322
+ this.retries++;
323
+ return this.retries;
324
+ }
325
+ match(scan, source, toolName) {
326
+ const hits = [];
327
+ for (const rule of this.rules) {
328
+ if (this.fired.has(rule.name)) continue;
329
+ if (rule.scope !== "both" && rule.scope !== source) continue;
330
+ if (source === "tool" && rule.tools && toolName !== void 0) {
331
+ if (!rule.tools.includes(toolName)) continue;
332
+ }
333
+ rule.pattern.lastIndex = 0;
334
+ if (rule.pattern.test(scan)) hits.push(rule);
335
+ }
336
+ return hits;
337
+ }
338
+ };
339
+ function buildStreamRuleReminder(rules) {
340
+ const blocks = rules.map(
341
+ (rule) => `<system-reminder reason="stream_rule" rule="${rule.name.replace(/"/g, "'")}">
342
+ 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:
343
+
344
+ ${rule.reminder.trim()}
345
+ </system-reminder>`
346
+ );
347
+ return {
348
+ role: "user",
349
+ content: blocks.join("\n\n"),
350
+ provenance: { source: "runtime", kind: "notification", visibility: "hidden" }
351
+ };
352
+ }
353
+
120
354
  // src/agent-loop.ts
121
355
  var DEFAULT_MAX_TURNS = 300;
122
356
  var DEFAULT_TOOL_TIMEOUT_MS = 3e5;
357
+ var PREEMPT_GRACE_MS = 500;
358
+ var STEER_INTERRUPTED_TEXT = "Interrupted: the user sent a new message.";
359
+ var ATOMIC_TOOL_NAMES = /* @__PURE__ */ new Set(["edit", "write", "multi_edit", "apply_patch", "notebook_edit"]);
360
+ function isInterruptibleTool(tool, name) {
361
+ if (tool?.interruptible !== void 0) return tool.interruptible;
362
+ return !ATOMIC_TOOL_NAMES.has(name);
363
+ }
364
+ function markSteerInterrupted(record) {
365
+ const content = record.content;
366
+ let next;
367
+ if (typeof content === "string") {
368
+ next = content.trim() ? `${STEER_INTERRUPTED_TEXT}
369
+ Partial output:
370
+ ${content}` : STEER_INTERRUPTED_TEXT;
371
+ } else {
372
+ next = [
373
+ { type: "text", text: `${STEER_INTERRUPTED_TEXT}
374
+ Partial output:` },
375
+ ...content
376
+ ];
377
+ }
378
+ return { toolCallId: record.toolCallId, content: next, isError: true };
379
+ }
123
380
  var _toolRedaction;
124
381
  function toolRedactionOptions() {
125
- _toolRedaction ??= { secrets: (0, import_ai.environmentSecrets)(process.env) };
382
+ _toolRedaction ??= { secrets: (0, import_ai2.environmentSecrets)(process.env) };
126
383
  return _toolRedaction;
127
384
  }
128
385
  var _diagFn = null;
@@ -205,7 +462,7 @@ function isBillingError(err) {
205
462
  if (!(err instanceof Error)) return false;
206
463
  const statusCode = err.statusCode;
207
464
  if (statusCode === 402) return true;
208
- return (0, import_ai.isHardBillingMessage)(err.message);
465
+ return (0, import_ai2.isHardBillingMessage)(err.message);
209
466
  }
210
467
  function isUsageLimitError(err) {
211
468
  if (!(err instanceof Error)) return false;
@@ -430,6 +687,7 @@ async function* agentLoop(messages, options) {
430
687
  let runawayToolcallRetries = 0;
431
688
  let overflowCompactionAttempts = 0;
432
689
  let toolResultTruncationAttempted = false;
690
+ const streamRuleMonitor = options.streamRules && options.streamRules.rules.length > 0 ? new StreamRuleMonitor(options.streamRules) : null;
433
691
  const invalidToolArgumentCounts = /* @__PURE__ */ new Map();
434
692
  let toolArgumentAutoContinueUsed = false;
435
693
  let useNonStreamingFallback = false;
@@ -483,16 +741,7 @@ async function* agentLoop(messages, options) {
483
741
  turn++;
484
742
  if (logicalTurnStartedAt === 0) logicalTurnStartedAt = Date.now();
485
743
  toolMap = new Map((options.tools ?? []).map((t) => [t.name, t]));
486
- let msgChars = 0;
487
- for (const m of messages) {
488
- if (typeof m.content === "string") msgChars += m.content.length;
489
- else if (Array.isArray(m.content)) {
490
- for (const p of m.content) {
491
- if ("text" in p && typeof p.text === "string") msgChars += p.text.length;
492
- if ("content" in p && typeof p.content === "string") msgChars += p.content.length;
493
- }
494
- }
495
- }
744
+ const msgChars = countMessageChars(messages);
496
745
  let firstEventTimeoutMs;
497
746
  let initialHardTimeoutMs;
498
747
  if (baseFirstEventTimeoutMs === STREAM_FIRST_EVENT_TIMEOUT_MS) {
@@ -563,6 +812,9 @@ async function* agentLoop(messages, options) {
563
812
  let toolcallNoProgressCount = 0;
564
813
  let runawayDetected = null;
565
814
  let attemptText = "";
815
+ let streamRuleHit = null;
816
+ let attemptThinkingChars = 0;
817
+ streamRuleMonitor?.beginAttempt();
566
818
  let lastYieldEndTime = Date.now();
567
819
  let maxConsumerLagMs = 0;
568
820
  const forwardAbort = () => streamController.abort();
@@ -629,7 +881,7 @@ async function* agentLoop(messages, options) {
629
881
  });
630
882
  }
631
883
  }
632
- const result = (0, import_ai.stream)({
884
+ const result = (0, import_ai2.stream)({
633
885
  provider: options.provider,
634
886
  model: options.model,
635
887
  messages,
@@ -649,8 +901,11 @@ async function* agentLoop(messages, options) {
649
901
  transportSessionId: options.transportSessionId,
650
902
  projectId: liveProjectId,
651
903
  cacheRetention: options.cacheRetention,
904
+ onContextPrepared: options.onContextPrepared,
652
905
  promptCacheKey: options.promptCacheKey,
653
906
  serviceTier: options.serviceTier,
907
+ responsesLite: options.responsesLite,
908
+ strictTools: options.strictTools,
654
909
  supportsImages: options.supportsImages,
655
910
  supportsVideo: options.supportsVideo,
656
911
  compaction: options.compaction,
@@ -726,8 +981,14 @@ async function* agentLoop(messages, options) {
726
981
  }
727
982
  if (event.type === "text_delta") {
728
983
  attemptText += event.text;
984
+ streamRuleHit = streamRuleMonitor?.checkText(event.text) ?? null;
985
+ if (streamRuleHit) {
986
+ streamController.abort();
987
+ break;
988
+ }
729
989
  yield { type: "text_delta", text: event.text };
730
990
  } else if (event.type === "thinking_delta") {
991
+ attemptThinkingChars += event.text.length;
731
992
  yield { type: "thinking_delta", text: event.text };
732
993
  } else if (event.type === "server_toolcall") {
733
994
  yield {
@@ -748,6 +1009,11 @@ async function* agentLoop(messages, options) {
748
1009
  toolcallDeltaChars += chunkChars;
749
1010
  toolcallDeltaCount++;
750
1011
  toolcallNoProgressCount = chunkChars > 0 ? 0 : toolcallNoProgressCount + 1;
1012
+ streamRuleHit = streamRuleMonitor?.checkToolArgs(event.id, event.name, event.argsJson ?? "") ?? null;
1013
+ if (streamRuleHit) {
1014
+ streamController.abort();
1015
+ break;
1016
+ }
751
1017
  if (!runawayDetected && (toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS || toolcallNoProgressCount > MAX_TOOLCALL_NO_PROGRESS_EVENTS)) {
752
1018
  runawayDetected = {
753
1019
  kind: toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS ? "chars" : "events",
@@ -770,6 +1036,54 @@ async function* agentLoop(messages, options) {
770
1036
  lastYieldEndTime = Date.now();
771
1037
  resetIdleTimer();
772
1038
  }
1039
+ if (streamRuleHit && streamRuleMonitor) {
1040
+ const attempt = streamRuleMonitor.recordTrigger(streamRuleHit.rules);
1041
+ const ruleNames = streamRuleHit.rules.map((rule) => rule.name);
1042
+ const priorPrompt = latestProviderUsage ? latestProviderUsage.inputTokens + (latestProviderUsage.cacheRead ?? 0) + (latestProviderUsage.cacheWrite ?? 0) : 0;
1043
+ const newChars = priorPrompt > 0 && usageAnchorIndex !== void 0 ? countMessageChars(messages.slice(usageAnchorIndex + 1)) : msgChars;
1044
+ const abortedUsage = {
1045
+ inputTokens: Math.ceil(newChars / 4),
1046
+ outputTokens: Math.ceil(
1047
+ (attemptText.length + attemptThinkingChars + toolcallDeltaChars) / 4
1048
+ ),
1049
+ ...priorPrompt > 0 ? { cacheRead: priorPrompt } : {}
1050
+ };
1051
+ totalUsage.inputTokens += abortedUsage.inputTokens;
1052
+ totalUsage.outputTokens += abortedUsage.outputTokens;
1053
+ if (abortedUsage.cacheRead) {
1054
+ totalUsage.cacheRead = (totalUsage.cacheRead ?? 0) + abortedUsage.cacheRead;
1055
+ }
1056
+ diag("stream_rule_triggered", {
1057
+ rules: ruleNames.join(","),
1058
+ source: streamRuleHit.source,
1059
+ toolName: streamRuleHit.toolName,
1060
+ attempt,
1061
+ maxAttempts: streamRuleMonitor.maxRetries,
1062
+ discardedChars: attemptText.length + toolcallDeltaChars,
1063
+ provider: options.provider,
1064
+ model: options.model
1065
+ });
1066
+ messages.push(buildStreamRuleReminder(streamRuleHit.rules));
1067
+ yield {
1068
+ type: "stream_rule_triggered",
1069
+ rules: ruleNames,
1070
+ source: streamRuleHit.source,
1071
+ ...streamRuleHit.toolName !== void 0 ? { toolName: streamRuleHit.toolName } : {},
1072
+ attempt,
1073
+ maxAttempts: streamRuleMonitor.maxRetries,
1074
+ usage: abortedUsage
1075
+ };
1076
+ yield {
1077
+ type: "retry",
1078
+ reason: "stream_rule",
1079
+ attempt,
1080
+ maxAttempts: streamRuleMonitor.maxRetries,
1081
+ delayMs: 0,
1082
+ silent: true
1083
+ };
1084
+ turn--;
1085
+ continue;
1086
+ }
773
1087
  diag("stream_done", {
774
1088
  events: streamEventCount,
775
1089
  totalMs: Date.now() - streamCallStart,
@@ -961,7 +1275,7 @@ async function* agentLoop(messages, options) {
961
1275
  const detail = runawayDetected.kind === "chars" ? `${(runawayDetected.chars / 1024).toFixed(0)} KB of tool-call arguments` : `${runawayDetected.noProgressEvents} consecutive tool-call delta events without argument progress`;
962
1276
  yield {
963
1277
  type: "error",
964
- error: new import_ai.EZCoderAIError(
1278
+ error: new import_ai2.EZCoderAIError(
965
1279
  `The model repeatedly failed to close a tool call after ${MAX_RUNAWAY_TOOLCALL_RETRIES} automatic retries (${detail}). Your conversation is preserved.`,
966
1280
  {
967
1281
  source: "provider",
@@ -1043,7 +1357,7 @@ async function* agentLoop(messages, options) {
1043
1357
  });
1044
1358
  yield {
1045
1359
  type: "error",
1046
- error: new import_ai.EZCoderAIError(
1360
+ error: new import_ai2.EZCoderAIError(
1047
1361
  `The connection to the API provider stopped responding after ${MAX_STALL_RETRIES} automatic retries. Your conversation is preserved.`,
1048
1362
  {
1049
1363
  source: "network",
@@ -1276,10 +1590,14 @@ async function* agentLoop(messages, options) {
1276
1590
  fatalToolArgumentRecoverable = recoverable;
1277
1591
  fatalToolArgumentToolName = toolName;
1278
1592
  };
1593
+ const preempt = new AbortController();
1594
+ const unsubscribeSteering = options.onSteeringAvailable?.(() => preempt.abort());
1279
1595
  const executionOptions = {
1280
1596
  signal: options.signal,
1597
+ preemptSignal: options.onSteeringAvailable ? preempt.signal : void 0,
1281
1598
  maxToolResultChars: options.maxToolResultChars,
1282
1599
  maxTurnToolResultChars: options.maxTurnToolResultChars,
1600
+ transformToolResult: options.transformToolResult,
1283
1601
  toolMap,
1284
1602
  invalidToolArgumentCounts,
1285
1603
  markFatalToolArgumentError,
@@ -1288,7 +1606,15 @@ async function* agentLoop(messages, options) {
1288
1606
  const hasSequentialToolCall = toolCalls.some(
1289
1607
  (toolCall) => toolMap.get(toolCall.name)?.executionMode === "sequential"
1290
1608
  );
1291
- const executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
1609
+ let executionResult;
1610
+ try {
1611
+ executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
1612
+ } finally {
1613
+ unsubscribeSteering?.();
1614
+ }
1615
+ if (executionResult.preempted) {
1616
+ diag("steer_preempted_tools", { turn, provider: options.provider, model: options.model });
1617
+ }
1292
1618
  messages.push({ role: "tool", content: executionResult.toolResults });
1293
1619
  yield { type: "checkpoint", turn };
1294
1620
  const toolsAborted = executionResult.aborted;
@@ -1445,7 +1771,7 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1445
1771
  options.seenToolCalls.add(signature);
1446
1772
  }
1447
1773
  if (!tool) {
1448
- resultContent = `Unknown tool: ${toolCall.name}`;
1774
+ resultContent = unknownToolMessage(toolCall.name, [...options.toolMap.keys()]);
1449
1775
  isError = true;
1450
1776
  } else {
1451
1777
  try {
@@ -1462,8 +1788,12 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1462
1788
  }
1463
1789
  const callerSignal = options.signal;
1464
1790
  const toolTimeout = AbortSignal.timeout(tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS);
1791
+ const preemptSignal = isInterruptibleTool(tool, toolCall.name) ? options.preemptSignal : void 0;
1792
+ const toolSignals = [callerSignal, toolTimeout, preemptSignal].filter(
1793
+ (s) => s !== void 0
1794
+ );
1465
1795
  const ctx = {
1466
- signal: callerSignal ? AbortSignal.any([callerSignal, toolTimeout]) : toolTimeout,
1796
+ signal: toolSignals.length === 1 ? toolTimeout : AbortSignal.any(toolSignals),
1467
1797
  toolCallId: toolCall.id,
1468
1798
  onUpdate: (update) => {
1469
1799
  pushEvent({
@@ -1473,13 +1803,25 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1473
1803
  });
1474
1804
  }
1475
1805
  };
1806
+ const hardSignals = [callerSignal, toolTimeout].filter(
1807
+ (s) => s !== void 0
1808
+ );
1476
1809
  const raw = await abortablePromise(
1477
1810
  Promise.resolve().then(() => tool.execute(parsed, ctx)),
1478
- ctx.signal
1811
+ hardSignals.length === 1 ? toolTimeout : AbortSignal.any(hardSignals)
1479
1812
  );
1480
1813
  const normalized = normalizeToolResult(raw);
1481
- resultContent = (0, import_ai.redactValue)(normalized.content, toolRedactionOptions());
1482
- details = (0, import_ai.redactValue)(normalized.details, toolRedactionOptions());
1814
+ resultContent = (0, import_ai2.redactValue)(normalized.content, toolRedactionOptions());
1815
+ details = (0, import_ai2.redactValue)(normalized.details, toolRedactionOptions());
1816
+ if (options.transformToolResult) {
1817
+ try {
1818
+ resultContent = options.transformToolResult(
1819
+ { name: toolCall.name, args: toolCall.args },
1820
+ resultContent
1821
+ );
1822
+ } catch {
1823
+ }
1824
+ }
1483
1825
  for (const key of options.invalidToolArgumentCounts.keys()) {
1484
1826
  if (key.startsWith(`${toolCall.name}:`)) options.invalidToolArgumentCounts.delete(key);
1485
1827
  }
@@ -1491,12 +1833,14 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1491
1833
  const failureCount = (options.invalidToolArgumentCounts.get(failureKey) ?? 0) + 1;
1492
1834
  options.invalidToolArgumentCounts.set(failureKey, failureCount);
1493
1835
  invalidArgAttempt = failureCount;
1836
+ const hints = argumentHints(tool, toolCall.args);
1494
1837
  resultContent = `Invalid arguments for tool \`${toolCall.name}\`:
1495
- ` + prettyError + "\nRe-issue the call with each field as the correct type.";
1838
+ ` + prettyError + (hints ? `
1839
+ ${hints}` : "") + "\nRe-issue the call with each field as the correct type.";
1496
1840
  if (failureCount >= 3) {
1497
1841
  const recoverable = Object.keys(toolCall.args ?? {}).length === 0;
1498
1842
  options.markFatalToolArgumentError(
1499
- new import_ai.EZCoderAIError(
1843
+ new import_ai2.EZCoderAIError(
1500
1844
  `The model repeatedly issued invalid arguments for tool \`${toolCall.name}\`. This is usually an upstream model/tool-calling bug` + (recoverable ? " (the provider's stream returned empty tool-call arguments)" : "") + `. Your conversation is preserved; send another message or switch models to continue.`,
1501
1845
  {
1502
1846
  source: "provider",
@@ -1510,15 +1854,24 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1510
1854
  } else if (options.signal?.aborted && isAbortError(err)) {
1511
1855
  resultContent = indeterminateOutcomeText(toolCall.name);
1512
1856
  } else {
1513
- resultContent = (0, import_ai.redactValue)(
1857
+ resultContent = (0, import_ai2.redactValue)(
1514
1858
  err instanceof Error ? err.message : String(err),
1515
1859
  toolRedactionOptions()
1516
1860
  );
1517
1861
  }
1518
1862
  }
1519
1863
  }
1520
- resultContent = (0, import_ai.redactValue)(resultContent, toolRedactionOptions());
1521
- details = (0, import_ai.redactValue)(details, toolRedactionOptions());
1864
+ resultContent = (0, import_ai2.redactValue)(resultContent, toolRedactionOptions());
1865
+ details = (0, import_ai2.redactValue)(details, toolRedactionOptions());
1866
+ if (options.preemptSignal?.aborted && isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name)) {
1867
+ const marked = markSteerInterrupted({
1868
+ toolCallId: toolCall.id,
1869
+ content: resultContent,
1870
+ isError
1871
+ });
1872
+ resultContent = marked.content;
1873
+ isError = true;
1874
+ }
1522
1875
  const durationMs = Date.now() - startTime;
1523
1876
  pushEvent({
1524
1877
  type: "tool_call_end",
@@ -1531,13 +1884,67 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1531
1884
  });
1532
1885
  return { toolCallId: toolCall.id, content: resultContent, isError };
1533
1886
  }
1887
+ function createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds) {
1888
+ const running = /* @__PURE__ */ new Map();
1889
+ let graceTimer;
1890
+ const preemptSignal = options.preemptSignal;
1891
+ const abandon = () => {
1892
+ graceTimer = void 0;
1893
+ for (const [id, entry] of running) {
1894
+ if (!entry.interruptible || resultsById.has(id)) continue;
1895
+ resultsById.set(id, { toolCallId: id, content: STEER_INTERRUPTED_TEXT, isError: true });
1896
+ pushToolEvent(eventStream, state, {
1897
+ type: "tool_call_end",
1898
+ toolCallId: id,
1899
+ result: STEER_INTERRUPTED_TEXT,
1900
+ isError: true,
1901
+ durationMs: Date.now() - entry.startedAt
1902
+ });
1903
+ running.delete(id);
1904
+ }
1905
+ if (running.size === 0) eventStream.close();
1906
+ };
1907
+ const onPreempt = () => {
1908
+ if (graceTimer === void 0) graceTimer = setTimeout(abandon, PREEMPT_GRACE_MS);
1909
+ };
1910
+ preemptSignal?.addEventListener("abort", onPreempt, { once: true });
1911
+ return {
1912
+ async run(toolCall) {
1913
+ if (options.preemptSignal?.aborted) return;
1914
+ dispatchedIds.add(toolCall.id);
1915
+ const interruptible = isInterruptibleTool(options.toolMap.get(toolCall.name), toolCall.name);
1916
+ running.set(toolCall.id, { toolCall, interruptible, startedAt: Date.now() });
1917
+ const record = await executeSingleToolCall(
1918
+ toolCall,
1919
+ options,
1920
+ (event) => pushToolEvent(eventStream, state, event)
1921
+ );
1922
+ running.delete(toolCall.id);
1923
+ if (resultsById.has(toolCall.id)) return;
1924
+ 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);
1925
+ resultsById.set(
1926
+ toolCall.id,
1927
+ interruptible && preemptSignal?.aborted && !alreadyMarked ? markSteerInterrupted(record) : record
1928
+ );
1929
+ if (preemptSignal?.aborted && graceTimer === void 0 && running.size === 0) {
1930
+ eventStream.close();
1931
+ }
1932
+ },
1933
+ preempted: () => preemptSignal?.aborted === true,
1934
+ dispose: () => {
1935
+ if (graceTimer !== void 0) clearTimeout(graceTimer);
1936
+ preemptSignal?.removeEventListener("abort", onPreempt);
1937
+ }
1938
+ };
1939
+ }
1534
1940
  async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1535
- const eventStream = new import_ai.EventStream();
1941
+ const eventStream = new import_ai2.EventStream();
1536
1942
  const state = { finalized: false };
1537
1943
  const resultsById = /* @__PURE__ */ new Map();
1538
1944
  const dispatchedIds = /* @__PURE__ */ new Set();
1539
1945
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1540
1946
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1947
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
1541
1948
  const phases = [];
1542
1949
  let currentParallel = [];
1543
1950
  for (const toolCall of toolCalls) {
@@ -1558,40 +1965,18 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1558
1965
  void (async () => {
1559
1966
  try {
1560
1967
  for (const phase of phases) {
1561
- if (options.signal?.aborted) break;
1968
+ if (options.signal?.aborted || options.preemptSignal?.aborted) break;
1562
1969
  if (phase.sequential) {
1563
1970
  const signature = JSON.stringify([
1564
1971
  phase.sequential.name,
1565
1972
  canonicalToolArgs(phase.sequential.args)
1566
1973
  ]);
1567
1974
  if (!options.seenToolCalls.has(signature)) options.seenToolCalls.clear();
1568
- dispatchedIds.add(phase.sequential.id);
1569
- const record = await executeSingleToolCall(
1570
- phase.sequential,
1571
- options,
1572
- (event) => pushToolEvent(eventStream, state, event)
1573
- );
1574
- resultsById.set(record.toolCallId, record);
1975
+ await tracker.run(phase.sequential);
1575
1976
  } else if (phase.parallel.length === 1) {
1576
- dispatchedIds.add(phase.parallel[0].id);
1577
- const record = await executeSingleToolCall(
1578
- phase.parallel[0],
1579
- options,
1580
- (event) => pushToolEvent(eventStream, state, event)
1581
- );
1582
- resultsById.set(record.toolCallId, record);
1977
+ await tracker.run(phase.parallel[0]);
1583
1978
  } else {
1584
- await Promise.all(
1585
- phase.parallel.map(async (toolCall) => {
1586
- dispatchedIds.add(toolCall.id);
1587
- const record = await executeSingleToolCall(
1588
- toolCall,
1589
- options,
1590
- (event) => pushToolEvent(eventStream, state, event)
1591
- );
1592
- resultsById.set(record.toolCallId, record);
1593
- })
1594
- );
1979
+ await Promise.all(phase.parallel.map((toolCall) => tracker.run(toolCall)));
1595
1980
  }
1596
1981
  }
1597
1982
  if (!state.finalized) eventStream.close();
@@ -1612,31 +1997,23 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1612
1997
  }
1613
1998
  } finally {
1614
1999
  options.signal?.removeEventListener("abort", abortHandler);
2000
+ tracker.dispose();
1615
2001
  state.finalized = true;
1616
2002
  }
1617
2003
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1618
2004
  capToolResults(toolResults, options.maxToolResultChars);
1619
2005
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1620
- return { toolResults, aborted };
2006
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1621
2007
  }
1622
2008
  async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
1623
- const eventStream = new import_ai.EventStream();
2009
+ const eventStream = new import_ai2.EventStream();
1624
2010
  const state = { finalized: false };
1625
2011
  const resultsById = /* @__PURE__ */ new Map();
1626
2012
  const dispatchedIds = /* @__PURE__ */ new Set();
1627
2013
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1628
2014
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1629
- Promise.all(
1630
- toolCalls.map(async (toolCall) => {
1631
- dispatchedIds.add(toolCall.id);
1632
- const record = await executeSingleToolCall(
1633
- toolCall,
1634
- options,
1635
- (event) => pushToolEvent(eventStream, state, event)
1636
- );
1637
- resultsById.set(record.toolCallId, record);
1638
- })
1639
- ).then(() => {
2015
+ const tracker = createPreemptTracker(options, eventStream, state, resultsById, dispatchedIds);
2016
+ Promise.all(toolCalls.map((toolCall) => tracker.run(toolCall))).then(() => {
1640
2017
  if (!state.finalized) eventStream.close();
1641
2018
  }).catch((err) => {
1642
2019
  if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
@@ -1654,12 +2031,13 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1654
2031
  }
1655
2032
  } finally {
1656
2033
  options.signal?.removeEventListener("abort", abortHandler);
2034
+ tracker.dispose();
1657
2035
  state.finalized = true;
1658
2036
  }
1659
2037
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1660
2038
  capToolResults(toolResults, options.maxToolResultChars);
1661
2039
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1662
- return { toolResults, aborted };
2040
+ return { toolResults, aborted, preempted: !aborted && tracker.preempted() };
1663
2041
  }
1664
2042
  function cancelledBeforeStartText(name) {
1665
2043
  return `\`${name}\` was cancelled before it started, so it had no effect. Safe to retry.`;
@@ -1699,8 +2077,8 @@ function capToolResults(toolResults, maxToolResultChars) {
1699
2077
  const originalChars = toolResult.content.length;
1700
2078
  const headChars = Math.floor(max * 0.7);
1701
2079
  const tailChars = max - headChars;
1702
- const head = (0, import_ai.sliceHead)(toolResult.content, headChars);
1703
- const tail = (0, import_ai.sliceTail)(toolResult.content, tailChars);
2080
+ const head = (0, import_ai2.sliceHead)(toolResult.content, headChars);
2081
+ const tail = (0, import_ai2.sliceTail)(toolResult.content, tailChars);
1704
2082
  const omitted = originalChars - head.length - tail.length;
1705
2083
  toolResult.content = head + `
1706
2084
 
@@ -1736,11 +2114,11 @@ function capTurnToolResults(toolResults, maxTurnToolResultChars) {
1736
2114
  const headChars = Math.floor(fairShare * 0.7);
1737
2115
  const tailChars = fairShare - headChars;
1738
2116
  const omitted = originalChars - fairShare;
1739
- toolResult.content = (0, import_ai.sliceHead)(toolResult.content, headChars) + `
2117
+ toolResult.content = (0, import_ai2.sliceHead)(toolResult.content, headChars) + `
1740
2118
 
1741
2119
  [... ${omitted} characters trimmed: this turn's combined tool results exceeded the per-turn budget. Re-run this call alone with narrower filters or offset/limit if you need the omitted content ...]
1742
2120
 
1743
- ` + (0, import_ai.sliceTail)(toolResult.content, tailChars);
2121
+ ` + (0, import_ai2.sliceTail)(toolResult.content, tailChars);
1744
2122
  toolResult.capped = {
1745
2123
  originalChars: toolResult.capped?.originalChars ?? originalChars,
1746
2124
  keptChars: toolResult.content.length,
@@ -1759,8 +2137,8 @@ function truncateToolResultText(text, maxChars) {
1759
2137
  if (text.length <= maxChars) return text;
1760
2138
  const tailChars = Math.min(Math.floor(maxChars * 0.3), 2e4);
1761
2139
  const headChars = Math.max(maxChars - tailChars, 0);
1762
- const head = (0, import_ai.sliceHead)(text, headChars);
1763
- const tail = (0, import_ai.sliceTail)(text, tailChars);
2140
+ const head = (0, import_ai2.sliceHead)(text, headChars);
2141
+ const tail = (0, import_ai2.sliceTail)(text, tailChars);
1764
2142
  const omitted = text.length - head.length - tail.length;
1765
2143
  return `${head}
1766
2144
 
@@ -1826,6 +2204,19 @@ function sanitizeOrphanedServerTools(messages) {
1826
2204
  break;
1827
2205
  }
1828
2206
  }
2207
+ function countMessageChars(messages) {
2208
+ let chars = 0;
2209
+ for (const m of messages) {
2210
+ if (typeof m.content === "string") chars += m.content.length;
2211
+ else if (Array.isArray(m.content)) {
2212
+ for (const p of m.content) {
2213
+ if ("text" in p && typeof p.text === "string") chars += p.text.length;
2214
+ if ("content" in p && typeof p.content === "string") chars += p.content.length;
2215
+ }
2216
+ }
2217
+ }
2218
+ return chars;
2219
+ }
1829
2220
  function repairToolPairingAdjacent(messages) {
1830
2221
  for (let i = 0; i < messages.length; i++) {
1831
2222
  const msg = messages[i];
@@ -1896,7 +2287,7 @@ var AgentStream = class {
1896
2287
  rejectResult;
1897
2288
  hasConsumer = false;
1898
2289
  constructor(generator, onDone) {
1899
- this.events = new import_ai2.EventStream();
2290
+ this.events = new import_ai3.EventStream();
1900
2291
  this.resultPromise = new Promise((resolve, reject) => {
1901
2292
  this.resolveResult = resolve;
1902
2293
  this.rejectResult = reject;
@@ -1942,6 +2333,7 @@ var Agent = class {
1942
2333
  options;
1943
2334
  steeringQueue = [];
1944
2335
  followUpQueue = [];
2336
+ steeringListeners = /* @__PURE__ */ new Set();
1945
2337
  constructor(options) {
1946
2338
  this.options = options;
1947
2339
  if (options.system) {
@@ -1966,9 +2358,14 @@ var Agent = class {
1966
2358
  get running() {
1967
2359
  return this._running;
1968
2360
  }
1969
- /** Queue a steering message for injection after current tool execution completes. */
2361
+ /**
2362
+ * Queue a steering message. Instant interrupt: if tools are running, the
2363
+ * interruptible ones are preempted right away and the message is injected
2364
+ * before the next model call (the turn continues — unlike abort/Stop).
2365
+ */
1970
2366
  steer(msg) {
1971
2367
  this.steeringQueue.push(msg);
2368
+ for (const listener of [...this.steeringListeners]) listener();
1972
2369
  }
1973
2370
  /** Queue a follow-up message for injection when the agent would otherwise stop. */
1974
2371
  followUp(msg) {
@@ -1988,6 +2385,14 @@ var Agent = class {
1988
2385
  const all = [...callerResult ?? [], ...queued];
1989
2386
  return all.length > 0 ? all : null;
1990
2387
  },
2388
+ onSteeringAvailable: (listener) => {
2389
+ this.steeringListeners.add(listener);
2390
+ const unsubscribeCaller = this.options.onSteeringAvailable?.(listener);
2391
+ return () => {
2392
+ this.steeringListeners.delete(listener);
2393
+ unsubscribeCaller?.();
2394
+ };
2395
+ },
1991
2396
  getFollowUpMessages: async () => {
1992
2397
  const callerResult = await this.options.getFollowUpMessages?.() ?? [];
1993
2398
  const queued = this.followUpQueue.splice(0);
@@ -2005,7 +2410,12 @@ var Agent = class {
2005
2410
  0 && (module.exports = {
2006
2411
  Agent,
2007
2412
  AgentStream,
2413
+ DEFAULT_STREAM_RULE_MAX_RETRIES,
2414
+ DEFAULT_STREAM_RULE_WINDOW_CHARS,
2415
+ JsonEscapeDecoder,
2416
+ StreamRuleMonitor,
2008
2417
  agentLoop,
2418
+ buildStreamRuleReminder,
2009
2419
  cancelledBeforeStartText,
2010
2420
  indeterminateOutcomeText,
2011
2421
  isAbortError,
@@ -2013,6 +2423,7 @@ var Agent = class {
2013
2423
  isContextOverflow,
2014
2424
  isLocalBackendUrl,
2015
2425
  isUsageLimitError,
2426
+ repairToolPairingAdjacent,
2016
2427
  setStreamDiagnostic
2017
2428
  });
2018
2429
  //# sourceMappingURL=index.cjs.map