@prestyj/agent 4.3.164 → 4.3.201
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 +427 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -2
- package/dist/index.d.ts +19 -2
- package/dist/index.js +427 -123
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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();
|
|
@@ -41,12 +80,17 @@ function classifyOverload(err) {
|
|
|
41
80
|
if (!(err instanceof Error)) return null;
|
|
42
81
|
if (isBillingError(err)) return null;
|
|
43
82
|
const msg = err.message.toLowerCase();
|
|
44
|
-
|
|
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")) {
|
|
45
86
|
return "rate_limit";
|
|
46
87
|
}
|
|
47
|
-
if (msg.includes("overloaded") || msg.includes("529")) {
|
|
88
|
+
if (statusCode === 529 || msg.includes("overloaded") || msg.includes("529")) {
|
|
48
89
|
return "overloaded";
|
|
49
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
|
+
}
|
|
50
94
|
return null;
|
|
51
95
|
}
|
|
52
96
|
function isMalformedStream(err) {
|
|
@@ -57,6 +101,46 @@ function isMalformedStream(err) {
|
|
|
57
101
|
const msg = err.message;
|
|
58
102
|
return /\bin JSON at position \d+/i.test(msg);
|
|
59
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
|
+
}
|
|
60
144
|
function abortableSleep(ms, signal) {
|
|
61
145
|
if (signal?.aborted) {
|
|
62
146
|
return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
@@ -87,6 +171,8 @@ async function* agentLoop(messages, options) {
|
|
|
87
171
|
let emptyResponseRetries = 0;
|
|
88
172
|
let stallRetries = 0;
|
|
89
173
|
let overflowCompactionAttempts = 0;
|
|
174
|
+
let toolResultTruncationAttempted = false;
|
|
175
|
+
const invalidToolArgumentCounts = /* @__PURE__ */ new Map();
|
|
90
176
|
let useNonStreamingFallback = false;
|
|
91
177
|
const MAX_OVERLOAD_RETRIES = 10;
|
|
92
178
|
const MAX_EMPTY_RESPONSE_RETRIES = 2;
|
|
@@ -103,6 +189,8 @@ async function* agentLoop(messages, options) {
|
|
|
103
189
|
const STREAM_THINKING_IDLE_TIMEOUT_MS = 3e5;
|
|
104
190
|
const STREAM_THINKING_HARD_TIMEOUT_MS = 6e5;
|
|
105
191
|
const NON_STREAMING_HARD_TIMEOUT_MS = 3e5;
|
|
192
|
+
const MAX_TOOLCALL_DELTA_CHARS = 1e6;
|
|
193
|
+
const MAX_TOOLCALL_DELTA_EVENTS = 2e4;
|
|
106
194
|
try {
|
|
107
195
|
while (turn < maxTurns) {
|
|
108
196
|
options.signal?.throwIfAborted();
|
|
@@ -158,6 +246,9 @@ async function* agentLoop(messages, options) {
|
|
|
158
246
|
let streamCallStart = Date.now();
|
|
159
247
|
const eventTypeCounts = {};
|
|
160
248
|
let lastEventType = "";
|
|
249
|
+
let toolcallDeltaChars = 0;
|
|
250
|
+
let toolcallDeltaCount = 0;
|
|
251
|
+
let runawayDetected = null;
|
|
161
252
|
let lastYieldEndTime = Date.now();
|
|
162
253
|
let maxConsumerLagMs = 0;
|
|
163
254
|
const forwardAbort = () => streamController.abort();
|
|
@@ -208,9 +299,12 @@ async function* agentLoop(messages, options) {
|
|
|
208
299
|
signal: streamController.signal,
|
|
209
300
|
accountId: options.accountId,
|
|
210
301
|
cacheRetention: options.cacheRetention,
|
|
302
|
+
promptCacheKey: options.promptCacheKey,
|
|
303
|
+
serviceTier: options.serviceTier,
|
|
211
304
|
supportsImages: options.supportsImages,
|
|
212
305
|
compaction: options.compaction,
|
|
213
306
|
clearToolUses: options.clearToolUses,
|
|
307
|
+
userAgent: options.userAgent,
|
|
214
308
|
// Flip to non-streaming fallback after repeated stream stalls.
|
|
215
309
|
...useNonStreamingFallback ? { streaming: false } : {}
|
|
216
310
|
});
|
|
@@ -221,11 +315,14 @@ async function* agentLoop(messages, options) {
|
|
|
221
315
|
hasReceivedEvent = false;
|
|
222
316
|
lastEventTime = Date.now();
|
|
223
317
|
streamCallStart = Date.now();
|
|
318
|
+
lastYieldEndTime = Date.now();
|
|
224
319
|
resetIdleTimer();
|
|
225
320
|
for await (const event of result) {
|
|
226
321
|
const pullTime = Date.now();
|
|
227
322
|
const consumerLag = pullTime - lastYieldEndTime;
|
|
228
|
-
if (consumerLag > maxConsumerLagMs)
|
|
323
|
+
if (streamEventCount > 0 && consumerLag > maxConsumerLagMs) {
|
|
324
|
+
maxConsumerLagMs = consumerLag;
|
|
325
|
+
}
|
|
229
326
|
streamEventCount++;
|
|
230
327
|
eventTypeCounts[event.type] = (eventTypeCounts[event.type] ?? 0) + 1;
|
|
231
328
|
lastEventType = event.type;
|
|
@@ -284,9 +381,25 @@ async function* agentLoop(messages, options) {
|
|
|
284
381
|
data: event.data
|
|
285
382
|
};
|
|
286
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
|
+
}
|
|
287
400
|
yield {
|
|
288
401
|
type: "toolcall_delta",
|
|
289
|
-
chars:
|
|
402
|
+
chars: chunkChars
|
|
290
403
|
};
|
|
291
404
|
}
|
|
292
405
|
lastYieldEndTime = Date.now();
|
|
@@ -311,12 +424,44 @@ async function* agentLoop(messages, options) {
|
|
|
311
424
|
model: options.model
|
|
312
425
|
});
|
|
313
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
|
+
}
|
|
314
458
|
if (options.transformContext && overflowCompactionAttempts < MAX_OVERFLOW_COMPACTIONS) {
|
|
315
459
|
overflowCompactionAttempts++;
|
|
316
460
|
diag("overflow_compact_start", {
|
|
317
461
|
attempt: overflowCompactionAttempts,
|
|
318
462
|
maxAttempts: MAX_OVERFLOW_COMPACTIONS,
|
|
319
|
-
messages: messages.length
|
|
463
|
+
messages: messages.length,
|
|
464
|
+
...overflowDetails
|
|
320
465
|
});
|
|
321
466
|
try {
|
|
322
467
|
const compacted = await options.transformContext(messages, { force: true });
|
|
@@ -325,14 +470,16 @@ async function* agentLoop(messages, options) {
|
|
|
325
470
|
messages.push(...compacted);
|
|
326
471
|
diag("overflow_compact_success", {
|
|
327
472
|
attempt: overflowCompactionAttempts,
|
|
328
|
-
messages: messages.length
|
|
473
|
+
messages: messages.length,
|
|
474
|
+
...overflowDetails
|
|
329
475
|
});
|
|
330
476
|
yield {
|
|
331
477
|
type: "retry",
|
|
332
478
|
reason: "overflow_compact",
|
|
333
479
|
attempt: overflowCompactionAttempts,
|
|
334
480
|
maxAttempts: MAX_OVERFLOW_COMPACTIONS,
|
|
335
|
-
delayMs: 0
|
|
481
|
+
delayMs: 0,
|
|
482
|
+
...overflowDetails
|
|
336
483
|
};
|
|
337
484
|
turn--;
|
|
338
485
|
continue;
|
|
@@ -340,11 +487,13 @@ async function* agentLoop(messages, options) {
|
|
|
340
487
|
diag("overflow_compact_noop", {
|
|
341
488
|
attempt: overflowCompactionAttempts,
|
|
342
489
|
before: messages.length,
|
|
343
|
-
after: compacted.length
|
|
490
|
+
after: compacted.length,
|
|
491
|
+
...overflowDetails
|
|
344
492
|
});
|
|
345
493
|
} catch (compactErr) {
|
|
346
494
|
diag("overflow_compact_failed", {
|
|
347
|
-
error: compactErr instanceof Error ? compactErr.message : String(compactErr)
|
|
495
|
+
error: compactErr instanceof Error ? compactErr.message : String(compactErr),
|
|
496
|
+
...overflowDetails
|
|
348
497
|
});
|
|
349
498
|
}
|
|
350
499
|
}
|
|
@@ -375,22 +524,39 @@ async function* agentLoop(messages, options) {
|
|
|
375
524
|
turn--;
|
|
376
525
|
continue;
|
|
377
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
|
+
}
|
|
378
542
|
const malformed = isMalformedStream(err);
|
|
379
|
-
const
|
|
543
|
+
const socketDrop = isTransportFailure(err);
|
|
544
|
+
const transportFailure = (idleTimedOut || malformed || socketDrop) && !options.signal?.aborted;
|
|
380
545
|
if (transportFailure && stallRetries < MAX_STALL_RETRIES) {
|
|
381
546
|
stallRetries++;
|
|
547
|
+
const cause = malformed ? "malformed_stream" : socketDrop ? "socket_drop" : "stream_stall";
|
|
382
548
|
if (!useNonStreamingFallback && stallRetries >= STALL_RETRIES_BEFORE_NON_STREAMING) {
|
|
383
549
|
useNonStreamingFallback = true;
|
|
384
550
|
diag("non_streaming_fallback_enabled", {
|
|
385
551
|
stallRetries,
|
|
386
552
|
provider: options.provider,
|
|
387
553
|
model: options.model,
|
|
388
|
-
cause
|
|
554
|
+
cause
|
|
389
555
|
});
|
|
390
556
|
}
|
|
391
557
|
const delayMs = Math.min(STALL_DELAY_MS * 2 ** (stallRetries - 1), 8e3);
|
|
392
558
|
diag("retry", {
|
|
393
|
-
reason:
|
|
559
|
+
reason: cause,
|
|
394
560
|
attempt: stallRetries,
|
|
395
561
|
maxAttempts: MAX_STALL_RETRIES,
|
|
396
562
|
delayMs,
|
|
@@ -545,117 +711,26 @@ async function* agentLoop(messages, options) {
|
|
|
545
711
|
toolCalls.push(tc);
|
|
546
712
|
}
|
|
547
713
|
}
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
signal: options.signal ?? AbortSignal.timeout(3e5),
|
|
569
|
-
toolCallId: toolCall.id,
|
|
570
|
-
onUpdate: (update) => {
|
|
571
|
-
eventStream.push({
|
|
572
|
-
type: "tool_call_update",
|
|
573
|
-
toolCallId: toolCall.id,
|
|
574
|
-
update
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
};
|
|
578
|
-
const raw = await tool.execute(parsed, ctx);
|
|
579
|
-
const normalized = normalizeToolResult(raw);
|
|
580
|
-
resultContent = normalized.content;
|
|
581
|
-
details = normalized.details;
|
|
582
|
-
} catch (err) {
|
|
583
|
-
isError = true;
|
|
584
|
-
resultContent = err instanceof Error ? err.message : String(err);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
const durationMs = Date.now() - startTime;
|
|
588
|
-
eventStream.push({
|
|
589
|
-
type: "tool_call_end",
|
|
590
|
-
toolCallId: toolCall.id,
|
|
591
|
-
result: toolResultPreview(resultContent),
|
|
592
|
-
details,
|
|
593
|
-
isError,
|
|
594
|
-
durationMs
|
|
595
|
-
});
|
|
596
|
-
return { toolCallId: toolCall.id, content: resultContent, isError };
|
|
597
|
-
});
|
|
598
|
-
const abortHandler = () => eventStream.abort(new Error("aborted"));
|
|
599
|
-
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
600
|
-
let toolResultsFinalized = false;
|
|
601
|
-
Promise.all(executions).then((results) => {
|
|
602
|
-
if (toolResultsFinalized) return;
|
|
603
|
-
const resultsMap = new Map(results.map((r) => [r.toolCallId, r]));
|
|
604
|
-
for (const tc of toolCalls) {
|
|
605
|
-
const r = resultsMap.get(tc.id);
|
|
606
|
-
toolResults.push({
|
|
607
|
-
type: "tool_result",
|
|
608
|
-
toolCallId: tc.id,
|
|
609
|
-
content: r.content,
|
|
610
|
-
isError: r.isError || void 0
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
eventStream.close();
|
|
614
|
-
}).catch((err) => eventStream.abort(err instanceof Error ? err : new Error(String(err))));
|
|
615
|
-
let toolsAborted = false;
|
|
616
|
-
try {
|
|
617
|
-
for await (const event of eventStream) {
|
|
618
|
-
yield event;
|
|
619
|
-
}
|
|
620
|
-
} catch (err) {
|
|
621
|
-
if (isAbortError(err) || options.signal?.aborted) {
|
|
622
|
-
toolsAborted = true;
|
|
623
|
-
} else {
|
|
624
|
-
throw err;
|
|
625
|
-
}
|
|
626
|
-
} finally {
|
|
627
|
-
options.signal?.removeEventListener("abort", abortHandler);
|
|
628
|
-
toolResultsFinalized = true;
|
|
629
|
-
const resolvedIds = new Set(toolResults.map((r) => r.toolCallId));
|
|
630
|
-
for (const tc of toolCalls) {
|
|
631
|
-
if (!resolvedIds.has(tc.id)) {
|
|
632
|
-
toolResults.push({
|
|
633
|
-
type: "tool_result",
|
|
634
|
-
toolCallId: tc.id,
|
|
635
|
-
content: "Tool execution was aborted.",
|
|
636
|
-
isError: true
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
if (options.maxToolResultChars) {
|
|
641
|
-
const HARD_MAX = 4e5;
|
|
642
|
-
const max = Math.min(options.maxToolResultChars, HARD_MAX);
|
|
643
|
-
for (const tr of toolResults) {
|
|
644
|
-
if (typeof tr.content === "string" && tr.content.length > max) {
|
|
645
|
-
const headChars = Math.floor(max * 0.7);
|
|
646
|
-
const tailChars = max - headChars;
|
|
647
|
-
const head = tr.content.slice(0, headChars);
|
|
648
|
-
const tail = tr.content.slice(-tailChars);
|
|
649
|
-
const omitted = tr.content.length - headChars - tailChars;
|
|
650
|
-
tr.content = head + `
|
|
651
|
-
|
|
652
|
-
[... ${omitted} characters omitted ...]
|
|
653
|
-
|
|
654
|
-
` + tail;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
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;
|
|
659
734
|
}
|
|
660
735
|
if (toolsAborted) break;
|
|
661
736
|
if (options.getSteeringMessages) {
|
|
@@ -689,6 +764,197 @@ async function* agentLoop(messages, options) {
|
|
|
689
764
|
totalUsage: { ...totalUsage }
|
|
690
765
|
};
|
|
691
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
|
+
}
|
|
692
958
|
function normalizeToolResult(raw) {
|
|
693
959
|
return typeof raw === "string" ? { content: raw } : raw;
|
|
694
960
|
}
|
|
@@ -696,6 +962,44 @@ function toolResultPreview(content) {
|
|
|
696
962
|
if (typeof content === "string") return content;
|
|
697
963
|
return content.map((block) => block.type === "text" ? block.text : `[image ${block.mediaType}]`).join("\n");
|
|
698
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
|
+
}
|
|
699
1003
|
function extractToolCalls(content) {
|
|
700
1004
|
if (typeof content === "string") return [];
|
|
701
1005
|
return content.filter((part) => part.type === "tool_call");
|