@prestyj/agent 5.12.0 → 5.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -55,6 +55,13 @@ interface AgentToolCallEndEvent {
55
55
  details?: unknown;
56
56
  isError: boolean;
57
57
  durationMs: number;
58
+ /**
59
+ * Set only when the call failed schema validation: how many consecutive
60
+ * times this tool produced this same validation error. 1 means the model
61
+ * still has room to self-correct; 3 is the threshold that ends the turn.
62
+ * Logged so a retry loop shows up as a count instead of identical lines.
63
+ */
64
+ invalidArgAttempt?: number;
58
65
  }
59
66
  interface AgentTurnTiming {
60
67
  /** Logical turn start, before context transforms or provider retries. Unix epoch milliseconds. */
@@ -133,7 +140,7 @@ interface AgentTurnBudgetExtendedEvent {
133
140
  */
134
141
  interface AgentTruncatedEvent {
135
142
  type: "truncated";
136
- reason: "max_tokens" | "refusal" | "provider_error";
143
+ reason: "max_tokens" | "refusal" | "provider_error" | "empty_response";
137
144
  /** True when the loop injected a continuation and will keep going. */
138
145
  continued: boolean;
139
146
  }
@@ -394,6 +401,20 @@ declare function isBillingError(err: unknown): boolean;
394
401
  */
395
402
  declare function isUsageLimitError(err: unknown): boolean;
396
403
  declare function agentLoop(messages: Message[], options: AgentOptions): AsyncGenerator<AgentEvent, AgentResult>;
404
+ /**
405
+ * A tool call that never reached its tool. Nothing ran, so nothing changed.
406
+ */
407
+ declare function cancelledBeforeStartText(name: string): string;
408
+ /**
409
+ * A tool call that started running and was cut off before reporting back.
410
+ *
411
+ * The distinction from {@link cancelledBeforeStartText} is the whole point: a
412
+ * dispatched `git push`, deploy or MCP call may well have COMPLETED before the
413
+ * abort landed. Telling the model it was "interrupted" reads as "it did not
414
+ * happen", so the model repeats the side effect — or reports to the user that
415
+ * something never ran when it did.
416
+ */
417
+ declare function indeterminateOutcomeText(name: string): string;
397
418
 
398
419
  /**
399
420
  * A local inference server (llama.cpp, vLLM, Ollama, LM Studio) can spend
@@ -404,4 +425,4 @@ declare function agentLoop(messages: Message[], options: AgentOptions): AsyncGen
404
425
  */
405
426
  declare function isLocalBackendUrl(baseUrl?: string): boolean;
406
427
 
407
- export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, isAbortError, isBillingError, isContextOverflow, isLocalBackendUrl, isUsageLimitError, setStreamDiagnostic };
428
+ export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, cancelledBeforeStartText, indeterminateOutcomeText, isAbortError, isBillingError, isContextOverflow, isLocalBackendUrl, isUsageLimitError, setStreamDiagnostic };
package/dist/index.d.ts CHANGED
@@ -55,6 +55,13 @@ interface AgentToolCallEndEvent {
55
55
  details?: unknown;
56
56
  isError: boolean;
57
57
  durationMs: number;
58
+ /**
59
+ * Set only when the call failed schema validation: how many consecutive
60
+ * times this tool produced this same validation error. 1 means the model
61
+ * still has room to self-correct; 3 is the threshold that ends the turn.
62
+ * Logged so a retry loop shows up as a count instead of identical lines.
63
+ */
64
+ invalidArgAttempt?: number;
58
65
  }
59
66
  interface AgentTurnTiming {
60
67
  /** Logical turn start, before context transforms or provider retries. Unix epoch milliseconds. */
@@ -133,7 +140,7 @@ interface AgentTurnBudgetExtendedEvent {
133
140
  */
134
141
  interface AgentTruncatedEvent {
135
142
  type: "truncated";
136
- reason: "max_tokens" | "refusal" | "provider_error";
143
+ reason: "max_tokens" | "refusal" | "provider_error" | "empty_response";
137
144
  /** True when the loop injected a continuation and will keep going. */
138
145
  continued: boolean;
139
146
  }
@@ -394,6 +401,20 @@ declare function isBillingError(err: unknown): boolean;
394
401
  */
395
402
  declare function isUsageLimitError(err: unknown): boolean;
396
403
  declare function agentLoop(messages: Message[], options: AgentOptions): AsyncGenerator<AgentEvent, AgentResult>;
404
+ /**
405
+ * A tool call that never reached its tool. Nothing ran, so nothing changed.
406
+ */
407
+ declare function cancelledBeforeStartText(name: string): string;
408
+ /**
409
+ * A tool call that started running and was cut off before reporting back.
410
+ *
411
+ * The distinction from {@link cancelledBeforeStartText} is the whole point: a
412
+ * dispatched `git push`, deploy or MCP call may well have COMPLETED before the
413
+ * abort landed. Telling the model it was "interrupted" reads as "it did not
414
+ * happen", so the model repeats the side effect — or reports to the user that
415
+ * something never ran when it did.
416
+ */
417
+ declare function indeterminateOutcomeText(name: string): string;
397
418
 
398
419
  /**
399
420
  * A local inference server (llama.cpp, vLLM, Ollama, LM Studio) can spend
@@ -404,4 +425,4 @@ declare function agentLoop(messages: Message[], options: AgentOptions): AsyncGen
404
425
  */
405
426
  declare function isLocalBackendUrl(baseUrl?: string): boolean;
406
427
 
407
- export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, isAbortError, isBillingError, isContextOverflow, isLocalBackendUrl, isUsageLimitError, setStreamDiagnostic };
428
+ export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, cancelledBeforeStartText, indeterminateOutcomeText, isAbortError, isBillingError, isContextOverflow, isLocalBackendUrl, isUsageLimitError, setStreamDiagnostic };
package/dist/index.js CHANGED
@@ -35,6 +35,60 @@ function isLocalBackendUrl(baseUrl) {
35
35
  return false;
36
36
  }
37
37
 
38
+ // src/output-ceiling.ts
39
+ var TTL_MS = 24 * 60 * 60 * 1e3;
40
+ var MIN_PLAUSIBLE_CEILING = 256;
41
+ var MAX_PLAUSIBLE_CEILING = 1e7;
42
+ var ceilings = /* @__PURE__ */ new Map();
43
+ function outputRouteKey(route) {
44
+ return `${route.provider}\0${route.baseUrl ?? "default"}\0${route.model}`;
45
+ }
46
+ function parseOutputTokenCeiling(err) {
47
+ if (!(err instanceof Error)) return null;
48
+ const msg = err.message;
49
+ if (!/max_tokens|max output tokens|output tokens|completion tokens/i.test(msg)) return null;
50
+ if (err.statusCode === 402) return null;
51
+ if (/credit|billing|payment|insufficient|balance|quota/i.test(msg)) return null;
52
+ const patterns = [
53
+ // Anthropic: "max_tokens: 100000 > 64000, which is the maximum allowed…"
54
+ /max_tokens:\s*\d+\s*>\s*(\d+)/i,
55
+ // OpenAI: "…this model supports at most 16384 completion tokens"
56
+ /(?:at most|maximum of|limit of)\s+([\d,_]+)\s*(?:output|completion)?\s*tokens/i,
57
+ // Generic: "max output tokens is 8192" / "max_tokens must be <= 4096"
58
+ /(?:max_tokens|max output tokens)\b[^\d]{0,30}?([\d,_]+)/i
59
+ ];
60
+ for (const pattern of patterns) {
61
+ const raw = msg.match(pattern)?.[1];
62
+ if (raw === void 0) continue;
63
+ const limit = Number(raw.replace(/[,_]/g, ""));
64
+ if (Number.isFinite(limit) && limit >= MIN_PLAUSIBLE_CEILING && limit <= MAX_PLAUSIBLE_CEILING) {
65
+ return limit;
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ function rememberOutputCeiling(key, limit) {
71
+ const known = outputTokenCeiling(key);
72
+ ceilings.set(key, {
73
+ limit: known === void 0 ? limit : Math.min(known, limit),
74
+ expiresAt: Date.now() + TTL_MS
75
+ });
76
+ }
77
+ function outputTokenCeiling(key) {
78
+ const entry = ceilings.get(key);
79
+ if (!entry) return void 0;
80
+ if (entry.expiresAt <= Date.now()) {
81
+ ceilings.delete(key);
82
+ return void 0;
83
+ }
84
+ return entry.limit;
85
+ }
86
+ function clampOutputTokens(key, requested) {
87
+ const ceiling = outputTokenCeiling(key);
88
+ if (ceiling === void 0) return requested;
89
+ return requested === void 0 ? ceiling : Math.min(requested, ceiling);
90
+ }
91
+
38
92
  // src/agent-loop.ts
39
93
  var DEFAULT_MAX_TURNS = 300;
40
94
  var DEFAULT_TOOL_TIMEOUT_MS = 3e5;
@@ -319,6 +373,16 @@ async function* agentLoop(messages, options) {
319
373
  const MAX_OUTPUT_CONTINUATIONS = 2;
320
374
  const MAX_TOKENS_CONTINUATION_PROMPT = "[Your previous response hit the output-token limit and was cut off. The text above is what was already delivered to the user. Continue exactly from where it stopped \u2014 do not repeat or restart it.]";
321
375
  let maxTokensContinuations = 0;
376
+ let providerCalls = 0;
377
+ let nonStreamingCalls = 0;
378
+ let warnedNonStreaming = false;
379
+ const MAX_OUTPUT_CEILING_RETRIES = 1;
380
+ let outputCeilingRetries = 0;
381
+ const ceilingKey = outputRouteKey({
382
+ provider: options.provider,
383
+ model: options.model,
384
+ baseUrl: options.baseUrl
385
+ });
322
386
  const OVERLOAD_BASE_DELAY_MS = 2e3;
323
387
  const OVERLOAD_MAX_DELAY_MS = 3e4;
324
388
  const STREAM_FIRST_EVENT_TIMEOUT_MS = 45e3;
@@ -448,6 +512,18 @@ async function* agentLoop(messages, options) {
448
512
  let streamIterator = null;
449
513
  try {
450
514
  diag("stream_call", { nonStreaming: useNonStreamingFallback });
515
+ providerCalls++;
516
+ if (useNonStreamingFallback) nonStreamingCalls++;
517
+ if (!warnedNonStreaming && nonStreamingCalls >= 3) {
518
+ warnedNonStreaming = true;
519
+ diag("non_streaming_session", {
520
+ nonStreamingCalls,
521
+ providerCalls,
522
+ provider: options.provider,
523
+ model: options.model,
524
+ impact: "streaming is disabled for this session after repeated stalls: failed turns are re-billed in full instead of resuming from partial output, and replies appear only when complete"
525
+ });
526
+ }
451
527
  streamCallStart = Date.now();
452
528
  providerAttemptStartedAt = streamCallStart;
453
529
  let liveApiKey = options.apiKey;
@@ -473,7 +549,9 @@ async function* agentLoop(messages, options) {
473
549
  serverTools: options.serverTools,
474
550
  toolChoice: options.toolChoice,
475
551
  webSearch: options.webSearch,
476
- maxTokens: options.maxTokens,
552
+ // Clamped to whatever ceiling this route has already rejected us for
553
+ // (identity when nothing has been learned).
554
+ maxTokens: clampOutputTokens(ceilingKey, options.maxTokens),
477
555
  temperature: options.temperature,
478
556
  thinking: options.thinking,
479
557
  apiKey: liveApiKey,
@@ -630,6 +708,29 @@ async function* agentLoop(messages, options) {
630
708
  });
631
709
  throw err;
632
710
  }
711
+ const statedCeiling = parseOutputTokenCeiling(err);
712
+ if (statedCeiling !== null) {
713
+ rememberOutputCeiling(ceilingKey, statedCeiling);
714
+ diag("output_ceiling_learned", {
715
+ ceiling: statedCeiling,
716
+ requested: options.maxTokens,
717
+ provider: options.provider,
718
+ model: options.model
719
+ });
720
+ if (outputCeilingRetries < MAX_OUTPUT_CEILING_RETRIES) {
721
+ outputCeilingRetries++;
722
+ yield {
723
+ type: "retry",
724
+ reason: "provider_error",
725
+ attempt: outputCeilingRetries,
726
+ maxAttempts: MAX_OUTPUT_CEILING_RETRIES,
727
+ delayMs: 0,
728
+ silent: true
729
+ };
730
+ turn--;
731
+ continue;
732
+ }
733
+ }
633
734
  if (isContextOverflow(err)) {
634
735
  const overflowDetails = extractContextOverflowDetails(err);
635
736
  diag("context_overflow_detected", {
@@ -925,6 +1026,7 @@ async function* agentLoop(messages, options) {
925
1026
  continue;
926
1027
  }
927
1028
  }
1029
+ const emptyExhausted = !hasActionableContent;
928
1030
  emptyResponseRetries = 0;
929
1031
  useNonStreamingFallback = false;
930
1032
  totalUsage.inputTokens += response.usage.inputTokens;
@@ -938,9 +1040,11 @@ async function* agentLoop(messages, options) {
938
1040
  if (response.usage.cacheWrite) {
939
1041
  totalUsage.cacheWrite = (totalUsage.cacheWrite ?? 0) + response.usage.cacheWrite;
940
1042
  }
941
- messages.push(response.message);
942
- latestProviderUsage = response.usage;
943
- usageAnchorIndex = messages.length - 1;
1043
+ if (!emptyExhausted) {
1044
+ messages.push(response.message);
1045
+ latestProviderUsage = response.usage;
1046
+ usageAnchorIndex = messages.length - 1;
1047
+ }
944
1048
  const completedAt = Date.now();
945
1049
  const outputTokensPerSecond = providerDurationMs > 0 && response.usage.outputTokens > 0 ? response.usage.outputTokens / (providerDurationMs / 1e3) : void 0;
946
1050
  const timing = {
@@ -972,8 +1076,15 @@ async function* agentLoop(messages, options) {
972
1076
  }
973
1077
  consecutivePauses = 0;
974
1078
  const allToolCalls = extractToolCalls(response.message.content);
975
- if (response.stopReason !== "tool_use" && allToolCalls.length === 0) {
976
- if (response.stopReason === "max_tokens") {
1079
+ if (emptyExhausted || response.stopReason !== "tool_use" && allToolCalls.length === 0) {
1080
+ if (emptyExhausted) {
1081
+ diag("empty_response_exhausted", {
1082
+ provider: options.provider,
1083
+ model: options.model,
1084
+ stopReason: response.stopReason
1085
+ });
1086
+ yield { type: "truncated", reason: "empty_response", continued: false };
1087
+ } else if (response.stopReason === "max_tokens") {
977
1088
  if (maxTokensContinuations < MAX_OUTPUT_CONTINUATIONS) {
978
1089
  maxTokensContinuations++;
979
1090
  diag("max_tokens_continuation", {
@@ -1195,6 +1306,7 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1195
1306
  let resultContent;
1196
1307
  let details;
1197
1308
  let isError = false;
1309
+ let invalidArgAttempt;
1198
1310
  const tool = options.toolMap.get(toolCall.name);
1199
1311
  if (!tool) {
1200
1312
  resultContent = `Unknown tool: ${toolCall.name}`;
@@ -1232,6 +1344,7 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1232
1344
  const failureKey = `${toolCall.name}:${prettyError}`;
1233
1345
  const failureCount = (options.invalidToolArgumentCounts.get(failureKey) ?? 0) + 1;
1234
1346
  options.invalidToolArgumentCounts.set(failureKey, failureCount);
1347
+ invalidArgAttempt = failureCount;
1235
1348
  resultContent = `Invalid arguments for tool \`${toolCall.name}\`:
1236
1349
  ` + prettyError + "\nRe-issue the call with each field as the correct type.";
1237
1350
  if (failureCount >= 3) {
@@ -1248,6 +1361,8 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1248
1361
  toolCall.name
1249
1362
  );
1250
1363
  }
1364
+ } else if (options.signal?.aborted && isAbortError(err)) {
1365
+ resultContent = indeterminateOutcomeText(toolCall.name);
1251
1366
  } else {
1252
1367
  resultContent = redactValue(err instanceof Error ? err.message : String(err));
1253
1368
  }
@@ -1262,7 +1377,8 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1262
1377
  result: toolResultPreview(resultContent),
1263
1378
  details,
1264
1379
  isError,
1265
- durationMs
1380
+ durationMs,
1381
+ ...invalidArgAttempt === void 0 ? {} : { invalidArgAttempt }
1266
1382
  });
1267
1383
  return { toolCallId: toolCall.id, content: resultContent, isError };
1268
1384
  }
@@ -1270,6 +1386,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1270
1386
  const eventStream = new EventStream();
1271
1387
  const state = { finalized: false };
1272
1388
  const resultsById = /* @__PURE__ */ new Map();
1389
+ const dispatchedIds = /* @__PURE__ */ new Set();
1273
1390
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1274
1391
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1275
1392
  const phases = [];
@@ -1294,6 +1411,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1294
1411
  for (const phase of phases) {
1295
1412
  if (options.signal?.aborted) break;
1296
1413
  if (phase.sequential) {
1414
+ dispatchedIds.add(phase.sequential.id);
1297
1415
  const record = await executeSingleToolCall(
1298
1416
  phase.sequential,
1299
1417
  options,
@@ -1301,6 +1419,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1301
1419
  );
1302
1420
  resultsById.set(record.toolCallId, record);
1303
1421
  } else if (phase.parallel.length === 1) {
1422
+ dispatchedIds.add(phase.parallel[0].id);
1304
1423
  const record = await executeSingleToolCall(
1305
1424
  phase.parallel[0],
1306
1425
  options,
@@ -1310,6 +1429,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1310
1429
  } else {
1311
1430
  await Promise.all(
1312
1431
  phase.parallel.map(async (toolCall) => {
1432
+ dispatchedIds.add(toolCall.id);
1313
1433
  const record = await executeSingleToolCall(
1314
1434
  toolCall,
1315
1435
  options,
@@ -1340,7 +1460,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1340
1460
  options.signal?.removeEventListener("abort", abortHandler);
1341
1461
  state.finalized = true;
1342
1462
  }
1343
- const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById);
1463
+ const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1344
1464
  capToolResults(toolResults, options.maxToolResultChars);
1345
1465
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1346
1466
  return { toolResults, aborted };
@@ -1349,10 +1469,12 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1349
1469
  const eventStream = new EventStream();
1350
1470
  const state = { finalized: false };
1351
1471
  const resultsById = /* @__PURE__ */ new Map();
1472
+ const dispatchedIds = /* @__PURE__ */ new Set();
1352
1473
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1353
1474
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1354
1475
  Promise.all(
1355
1476
  toolCalls.map(async (toolCall) => {
1477
+ dispatchedIds.add(toolCall.id);
1356
1478
  const record = await executeSingleToolCall(
1357
1479
  toolCall,
1358
1480
  options,
@@ -1380,12 +1502,18 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1380
1502
  options.signal?.removeEventListener("abort", abortHandler);
1381
1503
  state.finalized = true;
1382
1504
  }
1383
- const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById);
1505
+ const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1384
1506
  capToolResults(toolResults, options.maxToolResultChars);
1385
1507
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1386
1508
  return { toolResults, aborted };
1387
1509
  }
1388
- function buildToolResults(initialToolResults, toolCalls, resultsById) {
1510
+ function cancelledBeforeStartText(name) {
1511
+ return `\`${name}\` was cancelled before it started, so it had no effect. Safe to retry.`;
1512
+ }
1513
+ function indeterminateOutcomeText(name) {
1514
+ return `\`${name}\` started running and was cut off before it reported back, so its outcome is UNKNOWN \u2014 it may have completed. Check the real state (re-read the file, re-run a status command) before retrying it, and do not tell the user it did not happen.`;
1515
+ }
1516
+ function buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds) {
1389
1517
  const toolResults = [...initialToolResults];
1390
1518
  for (const toolCall of toolCalls) {
1391
1519
  const result = resultsById.get(toolCall.id);
@@ -1397,10 +1525,11 @@ function buildToolResults(initialToolResults, toolCalls, resultsById) {
1397
1525
  isError: result.isError || void 0
1398
1526
  });
1399
1527
  } else {
1528
+ const dispatched = dispatchedIds?.has(toolCall.id) ?? true;
1400
1529
  toolResults.push({
1401
1530
  type: "tool_result",
1402
1531
  toolCallId: toolCall.id,
1403
- content: "Tool execution was aborted.",
1532
+ content: dispatched ? indeterminateOutcomeText(toolCall.name) : cancelledBeforeStartText(toolCall.name),
1404
1533
  isError: true
1405
1534
  });
1406
1535
  }
@@ -1548,32 +1677,22 @@ function repairToolPairingAdjacent(messages) {
1548
1677
  const msg = messages[i];
1549
1678
  if (msg.role !== "assistant") continue;
1550
1679
  if (typeof msg.content === "string" || !Array.isArray(msg.content)) continue;
1551
- const toolCallIds = msg.content.filter((p) => p.type === "tool_call").map((p) => p.id);
1552
- if (toolCallIds.length === 0) continue;
1680
+ const orphanCalls = msg.content.filter((p) => p.type === "tool_call").map((p) => p);
1681
+ if (orphanCalls.length === 0) continue;
1682
+ const repaired = (call) => ({
1683
+ type: "tool_result",
1684
+ toolCallId: call.id,
1685
+ content: indeterminateOutcomeText(call.name),
1686
+ isError: true
1687
+ });
1553
1688
  const next = messages[i + 1];
1554
1689
  if (next?.role === "tool" && Array.isArray(next.content)) {
1555
1690
  const existingIds = new Set(next.content.map((r) => r.toolCallId));
1556
- const missing = toolCallIds.filter((id) => !existingIds.has(id));
1557
- if (missing.length > 0) {
1558
- for (const id of missing) {
1559
- next.content.push({
1560
- type: "tool_result",
1561
- toolCallId: id,
1562
- content: "Tool execution was interrupted.",
1563
- isError: true
1564
- });
1565
- }
1691
+ for (const call of orphanCalls) {
1692
+ if (!existingIds.has(call.id)) next.content.push(repaired(call));
1566
1693
  }
1567
1694
  } else {
1568
- messages.splice(i + 1, 0, {
1569
- role: "tool",
1570
- content: toolCallIds.map((id) => ({
1571
- type: "tool_result",
1572
- toolCallId: id,
1573
- content: "Tool execution was interrupted.",
1574
- isError: true
1575
- }))
1576
- });
1695
+ messages.splice(i + 1, 0, { role: "tool", content: orphanCalls.map(repaired) });
1577
1696
  }
1578
1697
  }
1579
1698
  const toolCallIdSet = /* @__PURE__ */ new Set();
@@ -1732,6 +1851,8 @@ export {
1732
1851
  Agent,
1733
1852
  AgentStream,
1734
1853
  agentLoop,
1854
+ cancelledBeforeStartText,
1855
+ indeterminateOutcomeText,
1735
1856
  isAbortError,
1736
1857
  isBillingError,
1737
1858
  isContextOverflow,