@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.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
- let msgChars = 0;
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
- const executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
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 = `Unknown tool: ${toolCall.name}`;
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: callerSignal ? AbortSignal.any([callerSignal, toolTimeout]) : toolTimeout,
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
- ctx.signal
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 + "\nRe-issue the call with each field as the correct type.";
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
- dispatchedIds.add(phase.sequential.id);
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
- dispatchedIds.add(phase.parallel[0].id);
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
- Promise.all(
1603
- toolCalls.map(async (toolCall) => {
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
- /** Queue a steering message for injection after current tool execution completes. */
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