@prestyj/agent 4.3.161 → 4.3.200

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
@@ -2,6 +2,7 @@
2
2
  import { EventStream as EventStream2 } from "@prestyj/ai";
3
3
 
4
4
  // src/agent-loop.ts
5
+ import { ZodError, prettifyError } from "zod";
5
6
  import {
6
7
  stream,
7
8
  EventStream
@@ -26,6 +27,44 @@ function isContextOverflow(err) {
26
27
  const msg = err.message.toLowerCase();
27
28
  return msg.includes("prompt is too long") || msg.includes("prompt too long") || msg.includes("input is too long") || msg.includes("context_length_exceeded") || msg.includes("context_window_exceeded") || msg.includes("maximum context length") || msg.includes("exceeds model context window") || msg.includes("exceeds the context window") || msg.includes("content_too_large") || msg.includes("request_too_large") || msg.includes("reduce the length") || msg.includes("please shorten") || msg.includes("token") && msg.includes("exceed");
28
29
  }
30
+ function parseOverflowNumber(value) {
31
+ return Number(value.replace(/[,_\s]/g, ""));
32
+ }
33
+ function extractContextOverflowDetails(err) {
34
+ if (!(err instanceof Error)) return {};
35
+ const text = err.message;
36
+ const patterns = [
37
+ // Anthropic/OpenAI-compatible: "203456 tokens > 200000 maximum"
38
+ {
39
+ regex: /([\d,_.\s]+)\s*tokens?\s*>\s*([\d,_.\s]+)\s*(?:maximum|max|limit)?/i,
40
+ tokensGroup: 1,
41
+ limitGroup: 2
42
+ },
43
+ // OpenAI: "maximum context length is 128000 tokens ... resulted in 130000 tokens"
44
+ {
45
+ regex: /maximum context length is\s*([\d,_.\s]+)\s*tokens?[\s\S]*?resulted in\s*([\d,_.\s]+)\s*tokens?/i,
46
+ tokensGroup: 2,
47
+ limitGroup: 1
48
+ },
49
+ // Generic: "130000 input tokens exceeds 128000 token limit"
50
+ {
51
+ regex: /([\d,_.\s]+)\s*(?:input\s*)?tokens?[\s\S]{0,80}?exceeds?[\s\S]{0,80}?([\d,_.\s]+)\s*(?:token\s*)?(?:limit|maximum|max)/i,
52
+ tokensGroup: 1,
53
+ limitGroup: 2
54
+ }
55
+ ];
56
+ for (const pattern of patterns) {
57
+ const match = text.match(pattern.regex);
58
+ if (!match) continue;
59
+ const observedTokens = parseOverflowNumber(match[pattern.tokensGroup] ?? "");
60
+ const observedLimit = parseOverflowNumber(match[pattern.limitGroup] ?? "");
61
+ return {
62
+ ...Number.isFinite(observedTokens) && observedTokens > 0 ? { observedTokens } : {},
63
+ ...Number.isFinite(observedLimit) && observedLimit > 0 ? { observedLimit } : {}
64
+ };
65
+ }
66
+ return {};
67
+ }
29
68
  function isBillingError(err) {
30
69
  if (!(err instanceof Error)) return false;
31
70
  const msg = err.message.toLowerCase();
@@ -37,11 +76,22 @@ function isToolPairingError(err) {
37
76
  return msg.includes("tool_use") && msg.includes("tool_result") || msg.includes("unexpected `tool_use_id`") || msg.includes("tool_use ids found without") || // Moonshot/OpenAI-compatible: "tool call id <id> is not found"
38
77
  msg.includes("tool call id") && msg.includes("is not found");
39
78
  }
40
- function isOverloaded(err) {
41
- if (!(err instanceof Error)) return false;
42
- if (isBillingError(err)) return false;
79
+ function classifyOverload(err) {
80
+ if (!(err instanceof Error)) return null;
81
+ if (isBillingError(err)) return null;
43
82
  const msg = err.message.toLowerCase();
44
- return msg.includes("overloaded") || msg.includes("rate limit") || msg.includes("too many requests") || msg.includes("429") || msg.includes("529");
83
+ const errorWithStatus = err;
84
+ const statusCode = typeof errorWithStatus.statusCode === "number" ? errorWithStatus.statusCode : void 0;
85
+ if (statusCode === 429 || msg.includes("rate_limit") || msg.includes("rate limit") || msg.includes("too many requests") || msg.includes("429")) {
86
+ return "rate_limit";
87
+ }
88
+ if (statusCode === 529 || msg.includes("overloaded") || msg.includes("529")) {
89
+ return "overloaded";
90
+ }
91
+ if (statusCode === 500 || statusCode === 502 || statusCode === 503 || statusCode === 504 || msg.includes("api_error") || msg.includes("server_error") || msg.includes("internal server error") || msg.includes("bad gateway") || msg.includes("service unavailable") || msg.includes("gateway timeout")) {
92
+ return "provider_error";
93
+ }
94
+ return null;
45
95
  }
46
96
  function isMalformedStream(err) {
47
97
  if (!(err instanceof Error)) return false;
@@ -51,6 +101,46 @@ function isMalformedStream(err) {
51
101
  const msg = err.message;
52
102
  return /\bin JSON at position \d+/i.test(msg);
53
103
  }
104
+ function isTransportFailure(err) {
105
+ const codes = /* @__PURE__ */ new Set([
106
+ "ECONNRESET",
107
+ "ECONNREFUSED",
108
+ "ECONNABORTED",
109
+ "ETIMEDOUT",
110
+ "EPIPE",
111
+ "EHOSTUNREACH",
112
+ "ENETUNREACH",
113
+ "ENOTFOUND",
114
+ "UND_ERR_SOCKET",
115
+ "UND_ERR_CONNECT_TIMEOUT",
116
+ "UND_ERR_HEADERS_TIMEOUT",
117
+ "UND_ERR_BODY_TIMEOUT",
118
+ "UND_ERR_RESPONSE_STATUS_CODE",
119
+ "UND_ERR_REQ_CONTENT_LENGTH_MISMATCH",
120
+ "UND_ERR_RES_CONTENT_LENGTH_MISMATCH"
121
+ ]);
122
+ const messages = [
123
+ /^terminated$/i,
124
+ /\bother side closed\b/i,
125
+ /\bsocket hang up\b/i,
126
+ /\bfetch failed\b/i,
127
+ /\bbody timeout error\b/i,
128
+ /\bsse stream disconnected\b/i,
129
+ /\bfailed to reconnect sse stream\b/i
130
+ ];
131
+ const seen = /* @__PURE__ */ new Set();
132
+ let cur = err;
133
+ while (cur && typeof cur === "object" && !seen.has(cur)) {
134
+ seen.add(cur);
135
+ const e = cur;
136
+ if (typeof e.code === "string" && codes.has(e.code)) return true;
137
+ if (typeof e.message === "string") {
138
+ for (const re of messages) if (re.test(e.message)) return true;
139
+ }
140
+ cur = e.cause;
141
+ }
142
+ return false;
143
+ }
54
144
  function abortableSleep(ms, signal) {
55
145
  if (signal?.aborted) {
56
146
  return Promise.reject(new DOMException("Aborted", "AbortError"));
@@ -81,6 +171,8 @@ async function* agentLoop(messages, options) {
81
171
  let emptyResponseRetries = 0;
82
172
  let stallRetries = 0;
83
173
  let overflowCompactionAttempts = 0;
174
+ let toolResultTruncationAttempted = false;
175
+ const invalidToolArgumentCounts = /* @__PURE__ */ new Map();
84
176
  let useNonStreamingFallback = false;
85
177
  const MAX_OVERLOAD_RETRIES = 10;
86
178
  const MAX_EMPTY_RESPONSE_RETRIES = 2;
@@ -97,6 +189,8 @@ async function* agentLoop(messages, options) {
97
189
  const STREAM_THINKING_IDLE_TIMEOUT_MS = 3e5;
98
190
  const STREAM_THINKING_HARD_TIMEOUT_MS = 6e5;
99
191
  const NON_STREAMING_HARD_TIMEOUT_MS = 3e5;
192
+ const MAX_TOOLCALL_DELTA_CHARS = 1e6;
193
+ const MAX_TOOLCALL_DELTA_EVENTS = 2e4;
100
194
  try {
101
195
  while (turn < maxTurns) {
102
196
  options.signal?.throwIfAborted();
@@ -152,6 +246,9 @@ async function* agentLoop(messages, options) {
152
246
  let streamCallStart = Date.now();
153
247
  const eventTypeCounts = {};
154
248
  let lastEventType = "";
249
+ let toolcallDeltaChars = 0;
250
+ let toolcallDeltaCount = 0;
251
+ let runawayDetected = null;
155
252
  let lastYieldEndTime = Date.now();
156
253
  let maxConsumerLagMs = 0;
157
254
  const forwardAbort = () => streamController.abort();
@@ -202,9 +299,12 @@ async function* agentLoop(messages, options) {
202
299
  signal: streamController.signal,
203
300
  accountId: options.accountId,
204
301
  cacheRetention: options.cacheRetention,
302
+ promptCacheKey: options.promptCacheKey,
303
+ serviceTier: options.serviceTier,
205
304
  supportsImages: options.supportsImages,
206
305
  compaction: options.compaction,
207
306
  clearToolUses: options.clearToolUses,
307
+ userAgent: options.userAgent,
208
308
  // Flip to non-streaming fallback after repeated stream stalls.
209
309
  ...useNonStreamingFallback ? { streaming: false } : {}
210
310
  });
@@ -215,11 +315,14 @@ async function* agentLoop(messages, options) {
215
315
  hasReceivedEvent = false;
216
316
  lastEventTime = Date.now();
217
317
  streamCallStart = Date.now();
318
+ lastYieldEndTime = Date.now();
218
319
  resetIdleTimer();
219
320
  for await (const event of result) {
220
321
  const pullTime = Date.now();
221
322
  const consumerLag = pullTime - lastYieldEndTime;
222
- if (consumerLag > maxConsumerLagMs) maxConsumerLagMs = consumerLag;
323
+ if (streamEventCount > 0 && consumerLag > maxConsumerLagMs) {
324
+ maxConsumerLagMs = consumerLag;
325
+ }
223
326
  streamEventCount++;
224
327
  eventTypeCounts[event.type] = (eventTypeCounts[event.type] ?? 0) + 1;
225
328
  lastEventType = event.type;
@@ -278,9 +381,25 @@ async function* agentLoop(messages, options) {
278
381
  data: event.data
279
382
  };
280
383
  } else if (event.type === "toolcall_delta") {
384
+ const chunkChars = event.argsJson?.length ?? 0;
385
+ toolcallDeltaChars += chunkChars;
386
+ toolcallDeltaCount++;
387
+ if (!runawayDetected && (toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS || toolcallDeltaCount > MAX_TOOLCALL_DELTA_EVENTS)) {
388
+ runawayDetected = {
389
+ kind: toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS ? "chars" : "events",
390
+ chars: toolcallDeltaChars,
391
+ events: toolcallDeltaCount
392
+ };
393
+ diag("runaway_toolcall_detected", {
394
+ ...runawayDetected,
395
+ provider: options.provider,
396
+ model: options.model
397
+ });
398
+ streamController.abort();
399
+ }
281
400
  yield {
282
401
  type: "toolcall_delta",
283
- chars: event.argsJson?.length ?? 0
402
+ chars: chunkChars
284
403
  };
285
404
  }
286
405
  lastYieldEndTime = Date.now();
@@ -305,12 +424,44 @@ async function* agentLoop(messages, options) {
305
424
  model: options.model
306
425
  });
307
426
  if (isContextOverflow(err)) {
427
+ const overflowDetails = extractContextOverflowDetails(err);
428
+ diag("context_overflow_detected", {
429
+ ...overflowDetails,
430
+ error: errMsg.slice(0, 500),
431
+ messages: messages.length
432
+ });
433
+ const overflowToolResultMaxChars = Math.min(
434
+ options.maxToolResultChars ?? 1e5,
435
+ 1e5
436
+ );
437
+ if (!toolResultTruncationAttempted) {
438
+ toolResultTruncationAttempted = true;
439
+ const truncated = truncateOversizedToolResults(messages, overflowToolResultMaxChars);
440
+ diag("overflow_tool_result_truncation", {
441
+ truncated,
442
+ maxChars: overflowToolResultMaxChars
443
+ });
444
+ if (truncated) {
445
+ yield {
446
+ type: "retry",
447
+ reason: "overflow_compact",
448
+ attempt: overflowCompactionAttempts + 1,
449
+ maxAttempts: MAX_OVERFLOW_COMPACTIONS,
450
+ delayMs: 0,
451
+ ...overflowDetails,
452
+ silent: true
453
+ };
454
+ turn--;
455
+ continue;
456
+ }
457
+ }
308
458
  if (options.transformContext && overflowCompactionAttempts < MAX_OVERFLOW_COMPACTIONS) {
309
459
  overflowCompactionAttempts++;
310
460
  diag("overflow_compact_start", {
311
461
  attempt: overflowCompactionAttempts,
312
462
  maxAttempts: MAX_OVERFLOW_COMPACTIONS,
313
- messages: messages.length
463
+ messages: messages.length,
464
+ ...overflowDetails
314
465
  });
315
466
  try {
316
467
  const compacted = await options.transformContext(messages, { force: true });
@@ -319,14 +470,16 @@ async function* agentLoop(messages, options) {
319
470
  messages.push(...compacted);
320
471
  diag("overflow_compact_success", {
321
472
  attempt: overflowCompactionAttempts,
322
- messages: messages.length
473
+ messages: messages.length,
474
+ ...overflowDetails
323
475
  });
324
476
  yield {
325
477
  type: "retry",
326
478
  reason: "overflow_compact",
327
479
  attempt: overflowCompactionAttempts,
328
480
  maxAttempts: MAX_OVERFLOW_COMPACTIONS,
329
- delayMs: 0
481
+ delayMs: 0,
482
+ ...overflowDetails
330
483
  };
331
484
  turn--;
332
485
  continue;
@@ -334,32 +487,35 @@ async function* agentLoop(messages, options) {
334
487
  diag("overflow_compact_noop", {
335
488
  attempt: overflowCompactionAttempts,
336
489
  before: messages.length,
337
- after: compacted.length
490
+ after: compacted.length,
491
+ ...overflowDetails
338
492
  });
339
493
  } catch (compactErr) {
340
494
  diag("overflow_compact_failed", {
341
- error: compactErr instanceof Error ? compactErr.message : String(compactErr)
495
+ error: compactErr instanceof Error ? compactErr.message : String(compactErr),
496
+ ...overflowDetails
342
497
  });
343
498
  }
344
499
  }
345
500
  yield { type: "error", error: err instanceof Error ? err : new Error(errMsg) };
346
501
  throw err;
347
502
  }
348
- if (overloadRetries < MAX_OVERLOAD_RETRIES && isOverloaded(err)) {
503
+ const overloadKind = classifyOverload(err);
504
+ if (overloadRetries < MAX_OVERLOAD_RETRIES && overloadKind) {
349
505
  overloadRetries++;
350
506
  const delayMs = Math.min(
351
507
  OVERLOAD_BASE_DELAY_MS * 2 ** (overloadRetries - 1),
352
508
  OVERLOAD_MAX_DELAY_MS
353
509
  );
354
510
  diag("retry", {
355
- reason: "overloaded",
511
+ reason: overloadKind,
356
512
  attempt: overloadRetries,
357
513
  maxAttempts: MAX_OVERLOAD_RETRIES,
358
514
  delayMs
359
515
  });
360
516
  yield {
361
517
  type: "retry",
362
- reason: "overloaded",
518
+ reason: overloadKind,
363
519
  attempt: overloadRetries,
364
520
  maxAttempts: MAX_OVERLOAD_RETRIES,
365
521
  delayMs
@@ -368,22 +524,39 @@ async function* agentLoop(messages, options) {
368
524
  turn--;
369
525
  continue;
370
526
  }
527
+ if (runawayDetected) {
528
+ diag("runaway_toolcall_aborted", {
529
+ ...runawayDetected,
530
+ provider: options.provider,
531
+ model: options.model
532
+ });
533
+ const detail = runawayDetected.kind === "chars" ? `${(runawayDetected.chars / 1024).toFixed(0)} KB of tool-call arguments` : `${runawayDetected.events} tool-call delta events`;
534
+ yield {
535
+ type: "error",
536
+ error: new Error(
537
+ `The model glitched mid-tool-call and produced ${detail} without closing the call. This is usually an upstream model bug \u2014 try the same request again or switch models. Your conversation is preserved.`
538
+ )
539
+ };
540
+ break;
541
+ }
371
542
  const malformed = isMalformedStream(err);
372
- const transportFailure = (idleTimedOut || malformed) && !options.signal?.aborted;
543
+ const socketDrop = isTransportFailure(err);
544
+ const transportFailure = (idleTimedOut || malformed || socketDrop) && !options.signal?.aborted;
373
545
  if (transportFailure && stallRetries < MAX_STALL_RETRIES) {
374
546
  stallRetries++;
547
+ const cause = malformed ? "malformed_stream" : socketDrop ? "socket_drop" : "stream_stall";
375
548
  if (!useNonStreamingFallback && stallRetries >= STALL_RETRIES_BEFORE_NON_STREAMING) {
376
549
  useNonStreamingFallback = true;
377
550
  diag("non_streaming_fallback_enabled", {
378
551
  stallRetries,
379
552
  provider: options.provider,
380
553
  model: options.model,
381
- cause: malformed ? "malformed_stream" : "stream_stall"
554
+ cause
382
555
  });
383
556
  }
384
557
  const delayMs = Math.min(STALL_DELAY_MS * 2 ** (stallRetries - 1), 8e3);
385
558
  diag("retry", {
386
- reason: malformed ? "malformed_stream" : "stream_stall",
559
+ reason: cause,
387
560
  attempt: stallRetries,
388
561
  maxAttempts: MAX_STALL_RETRIES,
389
562
  delayMs,
@@ -538,117 +711,26 @@ async function* agentLoop(messages, options) {
538
711
  toolCalls.push(tc);
539
712
  }
540
713
  }
541
- const eventStream = new EventStream();
542
- const executions = toolCalls.map(async (toolCall) => {
543
- const startTime = Date.now();
544
- eventStream.push({
545
- type: "tool_call_start",
546
- toolCallId: toolCall.id,
547
- name: toolCall.name,
548
- args: toolCall.args
549
- });
550
- let resultContent;
551
- let details;
552
- let isError = false;
553
- const tool = toolMap.get(toolCall.name);
554
- if (!tool) {
555
- resultContent = `Unknown tool: ${toolCall.name}`;
556
- isError = true;
557
- } else {
558
- try {
559
- const parsed = tool.parameters.parse(toolCall.args);
560
- const ctx = {
561
- signal: options.signal ?? AbortSignal.timeout(3e5),
562
- toolCallId: toolCall.id,
563
- onUpdate: (update) => {
564
- eventStream.push({
565
- type: "tool_call_update",
566
- toolCallId: toolCall.id,
567
- update
568
- });
569
- }
570
- };
571
- const raw = await tool.execute(parsed, ctx);
572
- const normalized = normalizeToolResult(raw);
573
- resultContent = normalized.content;
574
- details = normalized.details;
575
- } catch (err) {
576
- isError = true;
577
- resultContent = err instanceof Error ? err.message : String(err);
578
- }
579
- }
580
- const durationMs = Date.now() - startTime;
581
- eventStream.push({
582
- type: "tool_call_end",
583
- toolCallId: toolCall.id,
584
- result: toolResultPreview(resultContent),
585
- details,
586
- isError,
587
- durationMs
588
- });
589
- return { toolCallId: toolCall.id, content: resultContent, isError };
590
- });
591
- const abortHandler = () => eventStream.abort(new Error("aborted"));
592
- options.signal?.addEventListener("abort", abortHandler, { once: true });
593
- let toolResultsFinalized = false;
594
- Promise.all(executions).then((results) => {
595
- if (toolResultsFinalized) return;
596
- const resultsMap = new Map(results.map((r) => [r.toolCallId, r]));
597
- for (const tc of toolCalls) {
598
- const r = resultsMap.get(tc.id);
599
- toolResults.push({
600
- type: "tool_result",
601
- toolCallId: tc.id,
602
- content: r.content,
603
- isError: r.isError || void 0
604
- });
605
- }
606
- eventStream.close();
607
- }).catch((err) => eventStream.abort(err instanceof Error ? err : new Error(String(err))));
608
- let toolsAborted = false;
609
- try {
610
- for await (const event of eventStream) {
611
- yield event;
612
- }
613
- } catch (err) {
614
- if (isAbortError(err) || options.signal?.aborted) {
615
- toolsAborted = true;
616
- } else {
617
- throw err;
618
- }
619
- } finally {
620
- options.signal?.removeEventListener("abort", abortHandler);
621
- toolResultsFinalized = true;
622
- const resolvedIds = new Set(toolResults.map((r) => r.toolCallId));
623
- for (const tc of toolCalls) {
624
- if (!resolvedIds.has(tc.id)) {
625
- toolResults.push({
626
- type: "tool_result",
627
- toolCallId: tc.id,
628
- content: "Tool execution was aborted.",
629
- isError: true
630
- });
631
- }
632
- }
633
- if (options.maxToolResultChars) {
634
- const HARD_MAX = 4e5;
635
- const max = Math.min(options.maxToolResultChars, HARD_MAX);
636
- for (const tr of toolResults) {
637
- if (typeof tr.content === "string" && tr.content.length > max) {
638
- const headChars = Math.floor(max * 0.7);
639
- const tailChars = max - headChars;
640
- const head = tr.content.slice(0, headChars);
641
- const tail = tr.content.slice(-tailChars);
642
- const omitted = tr.content.length - headChars - tailChars;
643
- tr.content = head + `
644
-
645
- [... ${omitted} characters omitted ...]
646
-
647
- ` + tail;
648
- }
649
- }
650
- }
651
- messages.push({ role: "tool", content: toolResults });
714
+ let fatalToolArgumentError = null;
715
+ const markFatalToolArgumentError = (error) => {
716
+ fatalToolArgumentError = error;
717
+ };
718
+ const executionOptions = {
719
+ signal: options.signal,
720
+ maxToolResultChars: options.maxToolResultChars,
721
+ toolMap,
722
+ invalidToolArgumentCounts,
723
+ markFatalToolArgumentError
724
+ };
725
+ const hasSequentialToolCall = toolCalls.some(
726
+ (toolCall) => toolMap.get(toolCall.name)?.executionMode === "sequential"
727
+ );
728
+ const executionResult = hasSequentialToolCall ? yield* executeToolCallsSequential(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
729
+ messages.push({ role: "tool", content: executionResult.toolResults });
730
+ const toolsAborted = executionResult.aborted;
731
+ if (fatalToolArgumentError) {
732
+ yield { type: "error", error: fatalToolArgumentError };
733
+ break;
652
734
  }
653
735
  if (toolsAborted) break;
654
736
  if (options.getSteeringMessages) {
@@ -682,6 +764,197 @@ async function* agentLoop(messages, options) {
682
764
  totalUsage: { ...totalUsage }
683
765
  };
684
766
  }
767
+ function pushToolEvent(eventStream, state, event) {
768
+ if (!state.finalized) eventStream.push(event);
769
+ }
770
+ async function executeSingleToolCall(toolCall, options, pushEvent) {
771
+ const startTime = Date.now();
772
+ pushEvent({
773
+ type: "tool_call_start",
774
+ toolCallId: toolCall.id,
775
+ name: toolCall.name,
776
+ args: toolCall.args
777
+ });
778
+ let resultContent;
779
+ let details;
780
+ let isError = false;
781
+ const tool = options.toolMap.get(toolCall.name);
782
+ if (!tool) {
783
+ resultContent = `Unknown tool: ${toolCall.name}`;
784
+ isError = true;
785
+ } else {
786
+ try {
787
+ const parsed = tool.parameters.parse(toolCall.args);
788
+ const ctx = {
789
+ signal: options.signal ?? AbortSignal.timeout(3e5),
790
+ toolCallId: toolCall.id,
791
+ onUpdate: (update) => {
792
+ pushEvent({
793
+ type: "tool_call_update",
794
+ toolCallId: toolCall.id,
795
+ update
796
+ });
797
+ }
798
+ };
799
+ const raw = await tool.execute(parsed, ctx);
800
+ const normalized = normalizeToolResult(raw);
801
+ resultContent = normalized.content;
802
+ details = normalized.details;
803
+ for (const key of options.invalidToolArgumentCounts.keys()) {
804
+ if (key.startsWith(`${toolCall.name}:`)) options.invalidToolArgumentCounts.delete(key);
805
+ }
806
+ } catch (err) {
807
+ isError = true;
808
+ if (err instanceof ZodError) {
809
+ const prettyError = prettifyError(err);
810
+ const failureKey = `${toolCall.name}:${prettyError}`;
811
+ const failureCount = (options.invalidToolArgumentCounts.get(failureKey) ?? 0) + 1;
812
+ options.invalidToolArgumentCounts.set(failureKey, failureCount);
813
+ resultContent = `Invalid arguments for tool \`${toolCall.name}\`:
814
+ ` + prettyError + "\nRe-issue the call with each field as the correct type.";
815
+ if (failureCount >= 3) {
816
+ options.markFatalToolArgumentError(
817
+ new Error(
818
+ `The model repeatedly issued invalid arguments for tool \`${toolCall.name}\`. This is usually an upstream model/tool-calling bug. Your conversation is preserved; send another message or switch models to continue.`
819
+ )
820
+ );
821
+ }
822
+ } else {
823
+ resultContent = err instanceof Error ? err.message : String(err);
824
+ }
825
+ }
826
+ }
827
+ const durationMs = Date.now() - startTime;
828
+ pushEvent({
829
+ type: "tool_call_end",
830
+ toolCallId: toolCall.id,
831
+ result: toolResultPreview(resultContent),
832
+ details,
833
+ isError,
834
+ durationMs
835
+ });
836
+ return { toolCallId: toolCall.id, content: resultContent, isError };
837
+ }
838
+ async function* executeToolCallsSequential(toolCalls, initialToolResults, options) {
839
+ const eventStream = new EventStream();
840
+ const state = { finalized: false };
841
+ const resultsById = /* @__PURE__ */ new Map();
842
+ const abortHandler = () => eventStream.abort(new Error("aborted"));
843
+ options.signal?.addEventListener("abort", abortHandler, { once: true });
844
+ void (async () => {
845
+ try {
846
+ for (const toolCall of toolCalls) {
847
+ if (options.signal?.aborted) break;
848
+ const record = await executeSingleToolCall(
849
+ toolCall,
850
+ options,
851
+ (event) => pushToolEvent(eventStream, state, event)
852
+ );
853
+ resultsById.set(record.toolCallId, record);
854
+ }
855
+ if (!state.finalized) eventStream.close();
856
+ } catch (err) {
857
+ if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
858
+ }
859
+ })();
860
+ let aborted = false;
861
+ try {
862
+ for await (const event of eventStream) {
863
+ yield event;
864
+ }
865
+ } catch (err) {
866
+ if (isAbortError(err) || options.signal?.aborted) {
867
+ aborted = true;
868
+ } else {
869
+ throw err;
870
+ }
871
+ } finally {
872
+ options.signal?.removeEventListener("abort", abortHandler);
873
+ state.finalized = true;
874
+ }
875
+ const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById);
876
+ capToolResults(toolResults, options.maxToolResultChars);
877
+ return { toolResults, aborted };
878
+ }
879
+ async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
880
+ const eventStream = new EventStream();
881
+ const state = { finalized: false };
882
+ const resultsById = /* @__PURE__ */ new Map();
883
+ const abortHandler = () => eventStream.abort(new Error("aborted"));
884
+ options.signal?.addEventListener("abort", abortHandler, { once: true });
885
+ Promise.all(
886
+ toolCalls.map(async (toolCall) => {
887
+ const record = await executeSingleToolCall(
888
+ toolCall,
889
+ options,
890
+ (event) => pushToolEvent(eventStream, state, event)
891
+ );
892
+ resultsById.set(record.toolCallId, record);
893
+ })
894
+ ).then(() => {
895
+ if (!state.finalized) eventStream.close();
896
+ }).catch((err) => {
897
+ if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
898
+ });
899
+ let aborted = false;
900
+ try {
901
+ for await (const event of eventStream) {
902
+ yield event;
903
+ }
904
+ } catch (err) {
905
+ if (isAbortError(err) || options.signal?.aborted) {
906
+ aborted = true;
907
+ } else {
908
+ throw err;
909
+ }
910
+ } finally {
911
+ options.signal?.removeEventListener("abort", abortHandler);
912
+ state.finalized = true;
913
+ }
914
+ const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById);
915
+ capToolResults(toolResults, options.maxToolResultChars);
916
+ return { toolResults, aborted };
917
+ }
918
+ function buildToolResults(initialToolResults, toolCalls, resultsById) {
919
+ const toolResults = [...initialToolResults];
920
+ for (const toolCall of toolCalls) {
921
+ const result = resultsById.get(toolCall.id);
922
+ if (result) {
923
+ toolResults.push({
924
+ type: "tool_result",
925
+ toolCallId: toolCall.id,
926
+ content: result.content,
927
+ isError: result.isError || void 0
928
+ });
929
+ } else {
930
+ toolResults.push({
931
+ type: "tool_result",
932
+ toolCallId: toolCall.id,
933
+ content: "Tool execution was aborted.",
934
+ isError: true
935
+ });
936
+ }
937
+ }
938
+ return toolResults;
939
+ }
940
+ function capToolResults(toolResults, maxToolResultChars) {
941
+ if (!maxToolResultChars) return;
942
+ const hardMax = 4e5;
943
+ const max = Math.min(maxToolResultChars, hardMax);
944
+ for (const toolResult of toolResults) {
945
+ if (typeof toolResult.content !== "string" || toolResult.content.length <= max) continue;
946
+ const headChars = Math.floor(max * 0.7);
947
+ const tailChars = max - headChars;
948
+ const head = toolResult.content.slice(0, headChars);
949
+ const tail = toolResult.content.slice(-tailChars);
950
+ const omitted = toolResult.content.length - headChars - tailChars;
951
+ toolResult.content = head + `
952
+
953
+ [... ${omitted} characters omitted ...]
954
+
955
+ ` + tail;
956
+ }
957
+ }
685
958
  function normalizeToolResult(raw) {
686
959
  return typeof raw === "string" ? { content: raw } : raw;
687
960
  }
@@ -689,6 +962,44 @@ function toolResultPreview(content) {
689
962
  if (typeof content === "string") return content;
690
963
  return content.map((block) => block.type === "text" ? block.text : `[image ${block.mediaType}]`).join("\n");
691
964
  }
965
+ function truncateToolResultText(text, maxChars) {
966
+ if (text.length <= maxChars) return text;
967
+ const tailChars = Math.min(Math.floor(maxChars * 0.3), 2e4);
968
+ const headChars = Math.max(maxChars - tailChars, 0);
969
+ const omitted = text.length - headChars - tailChars;
970
+ return `${text.slice(0, headChars)}
971
+
972
+ [... ${omitted} characters omitted after context overflow ...]
973
+
974
+ ${text.slice(-tailChars)}`;
975
+ }
976
+ function truncateOversizedToolResults(messages, maxChars) {
977
+ if (maxChars <= 0) return false;
978
+ let changed = false;
979
+ for (const msg of messages) {
980
+ if (msg.role !== "tool" || !Array.isArray(msg.content)) continue;
981
+ const results = msg.content;
982
+ for (const result of results) {
983
+ if (typeof result.content === "string") {
984
+ const truncated = truncateToolResultText(result.content, maxChars);
985
+ if (truncated !== result.content) {
986
+ result.content = truncated;
987
+ changed = true;
988
+ }
989
+ } else {
990
+ for (const block of result.content) {
991
+ if (block.type !== "text") continue;
992
+ const truncated = truncateToolResultText(block.text, maxChars);
993
+ if (truncated !== block.text) {
994
+ block.text = truncated;
995
+ changed = true;
996
+ }
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+ return changed;
1002
+ }
692
1003
  function extractToolCalls(content) {
693
1004
  if (typeof content === "string") return [];
694
1005
  return content.filter((part) => part.type === "tool_call");