@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.cjs
CHANGED
|
@@ -34,6 +34,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
34
34
|
var import_ai2 = require("@prestyj/ai");
|
|
35
35
|
|
|
36
36
|
// src/agent-loop.ts
|
|
37
|
+
var import_zod = require("zod");
|
|
37
38
|
var import_ai = require("@prestyj/ai");
|
|
38
39
|
var DEFAULT_MAX_TURNS = 200;
|
|
39
40
|
var _diagFn = null;
|
|
@@ -55,6 +56,44 @@ function isContextOverflow(err) {
|
|
|
55
56
|
const msg = err.message.toLowerCase();
|
|
56
57
|
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");
|
|
57
58
|
}
|
|
59
|
+
function parseOverflowNumber(value) {
|
|
60
|
+
return Number(value.replace(/[,_\s]/g, ""));
|
|
61
|
+
}
|
|
62
|
+
function extractContextOverflowDetails(err) {
|
|
63
|
+
if (!(err instanceof Error)) return {};
|
|
64
|
+
const text = err.message;
|
|
65
|
+
const patterns = [
|
|
66
|
+
// Anthropic/OpenAI-compatible: "203456 tokens > 200000 maximum"
|
|
67
|
+
{
|
|
68
|
+
regex: /([\d,_.\s]+)\s*tokens?\s*>\s*([\d,_.\s]+)\s*(?:maximum|max|limit)?/i,
|
|
69
|
+
tokensGroup: 1,
|
|
70
|
+
limitGroup: 2
|
|
71
|
+
},
|
|
72
|
+
// OpenAI: "maximum context length is 128000 tokens ... resulted in 130000 tokens"
|
|
73
|
+
{
|
|
74
|
+
regex: /maximum context length is\s*([\d,_.\s]+)\s*tokens?[\s\S]*?resulted in\s*([\d,_.\s]+)\s*tokens?/i,
|
|
75
|
+
tokensGroup: 2,
|
|
76
|
+
limitGroup: 1
|
|
77
|
+
},
|
|
78
|
+
// Generic: "130000 input tokens exceeds 128000 token limit"
|
|
79
|
+
{
|
|
80
|
+
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,
|
|
81
|
+
tokensGroup: 1,
|
|
82
|
+
limitGroup: 2
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
for (const pattern of patterns) {
|
|
86
|
+
const match = text.match(pattern.regex);
|
|
87
|
+
if (!match) continue;
|
|
88
|
+
const observedTokens = parseOverflowNumber(match[pattern.tokensGroup] ?? "");
|
|
89
|
+
const observedLimit = parseOverflowNumber(match[pattern.limitGroup] ?? "");
|
|
90
|
+
return {
|
|
91
|
+
...Number.isFinite(observedTokens) && observedTokens > 0 ? { observedTokens } : {},
|
|
92
|
+
...Number.isFinite(observedLimit) && observedLimit > 0 ? { observedLimit } : {}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
58
97
|
function isBillingError(err) {
|
|
59
98
|
if (!(err instanceof Error)) return false;
|
|
60
99
|
const msg = err.message.toLowerCase();
|
|
@@ -70,12 +109,17 @@ function classifyOverload(err) {
|
|
|
70
109
|
if (!(err instanceof Error)) return null;
|
|
71
110
|
if (isBillingError(err)) return null;
|
|
72
111
|
const msg = err.message.toLowerCase();
|
|
73
|
-
|
|
112
|
+
const errorWithStatus = err;
|
|
113
|
+
const statusCode = typeof errorWithStatus.statusCode === "number" ? errorWithStatus.statusCode : void 0;
|
|
114
|
+
if (statusCode === 429 || msg.includes("rate_limit") || msg.includes("rate limit") || msg.includes("too many requests") || msg.includes("429")) {
|
|
74
115
|
return "rate_limit";
|
|
75
116
|
}
|
|
76
|
-
if (msg.includes("overloaded") || msg.includes("529")) {
|
|
117
|
+
if (statusCode === 529 || msg.includes("overloaded") || msg.includes("529")) {
|
|
77
118
|
return "overloaded";
|
|
78
119
|
}
|
|
120
|
+
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")) {
|
|
121
|
+
return "provider_error";
|
|
122
|
+
}
|
|
79
123
|
return null;
|
|
80
124
|
}
|
|
81
125
|
function isMalformedStream(err) {
|
|
@@ -86,6 +130,46 @@ function isMalformedStream(err) {
|
|
|
86
130
|
const msg = err.message;
|
|
87
131
|
return /\bin JSON at position \d+/i.test(msg);
|
|
88
132
|
}
|
|
133
|
+
function isTransportFailure(err) {
|
|
134
|
+
const codes = /* @__PURE__ */ new Set([
|
|
135
|
+
"ECONNRESET",
|
|
136
|
+
"ECONNREFUSED",
|
|
137
|
+
"ECONNABORTED",
|
|
138
|
+
"ETIMEDOUT",
|
|
139
|
+
"EPIPE",
|
|
140
|
+
"EHOSTUNREACH",
|
|
141
|
+
"ENETUNREACH",
|
|
142
|
+
"ENOTFOUND",
|
|
143
|
+
"UND_ERR_SOCKET",
|
|
144
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
145
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
146
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
147
|
+
"UND_ERR_RESPONSE_STATUS_CODE",
|
|
148
|
+
"UND_ERR_REQ_CONTENT_LENGTH_MISMATCH",
|
|
149
|
+
"UND_ERR_RES_CONTENT_LENGTH_MISMATCH"
|
|
150
|
+
]);
|
|
151
|
+
const messages = [
|
|
152
|
+
/^terminated$/i,
|
|
153
|
+
/\bother side closed\b/i,
|
|
154
|
+
/\bsocket hang up\b/i,
|
|
155
|
+
/\bfetch failed\b/i,
|
|
156
|
+
/\bbody timeout error\b/i,
|
|
157
|
+
/\bsse stream disconnected\b/i,
|
|
158
|
+
/\bfailed to reconnect sse stream\b/i
|
|
159
|
+
];
|
|
160
|
+
const seen = /* @__PURE__ */ new Set();
|
|
161
|
+
let cur = err;
|
|
162
|
+
while (cur && typeof cur === "object" && !seen.has(cur)) {
|
|
163
|
+
seen.add(cur);
|
|
164
|
+
const e = cur;
|
|
165
|
+
if (typeof e.code === "string" && codes.has(e.code)) return true;
|
|
166
|
+
if (typeof e.message === "string") {
|
|
167
|
+
for (const re of messages) if (re.test(e.message)) return true;
|
|
168
|
+
}
|
|
169
|
+
cur = e.cause;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
89
173
|
function abortableSleep(ms, signal) {
|
|
90
174
|
if (signal?.aborted) {
|
|
91
175
|
return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
@@ -116,6 +200,8 @@ async function* agentLoop(messages, options) {
|
|
|
116
200
|
let emptyResponseRetries = 0;
|
|
117
201
|
let stallRetries = 0;
|
|
118
202
|
let overflowCompactionAttempts = 0;
|
|
203
|
+
let toolResultTruncationAttempted = false;
|
|
204
|
+
const invalidToolArgumentCounts = /* @__PURE__ */ new Map();
|
|
119
205
|
let useNonStreamingFallback = false;
|
|
120
206
|
const MAX_OVERLOAD_RETRIES = 10;
|
|
121
207
|
const MAX_EMPTY_RESPONSE_RETRIES = 2;
|
|
@@ -132,6 +218,8 @@ async function* agentLoop(messages, options) {
|
|
|
132
218
|
const STREAM_THINKING_IDLE_TIMEOUT_MS = 3e5;
|
|
133
219
|
const STREAM_THINKING_HARD_TIMEOUT_MS = 6e5;
|
|
134
220
|
const NON_STREAMING_HARD_TIMEOUT_MS = 3e5;
|
|
221
|
+
const MAX_TOOLCALL_DELTA_CHARS = 1e6;
|
|
222
|
+
const MAX_TOOLCALL_DELTA_EVENTS = 2e4;
|
|
135
223
|
try {
|
|
136
224
|
while (turn < maxTurns) {
|
|
137
225
|
options.signal?.throwIfAborted();
|
|
@@ -187,6 +275,9 @@ async function* agentLoop(messages, options) {
|
|
|
187
275
|
let streamCallStart = Date.now();
|
|
188
276
|
const eventTypeCounts = {};
|
|
189
277
|
let lastEventType = "";
|
|
278
|
+
let toolcallDeltaChars = 0;
|
|
279
|
+
let toolcallDeltaCount = 0;
|
|
280
|
+
let runawayDetected = null;
|
|
190
281
|
let lastYieldEndTime = Date.now();
|
|
191
282
|
let maxConsumerLagMs = 0;
|
|
192
283
|
const forwardAbort = () => streamController.abort();
|
|
@@ -237,9 +328,12 @@ async function* agentLoop(messages, options) {
|
|
|
237
328
|
signal: streamController.signal,
|
|
238
329
|
accountId: options.accountId,
|
|
239
330
|
cacheRetention: options.cacheRetention,
|
|
331
|
+
promptCacheKey: options.promptCacheKey,
|
|
332
|
+
serviceTier: options.serviceTier,
|
|
240
333
|
supportsImages: options.supportsImages,
|
|
241
334
|
compaction: options.compaction,
|
|
242
335
|
clearToolUses: options.clearToolUses,
|
|
336
|
+
userAgent: options.userAgent,
|
|
243
337
|
// Flip to non-streaming fallback after repeated stream stalls.
|
|
244
338
|
...useNonStreamingFallback ? { streaming: false } : {}
|
|
245
339
|
});
|
|
@@ -250,11 +344,14 @@ async function* agentLoop(messages, options) {
|
|
|
250
344
|
hasReceivedEvent = false;
|
|
251
345
|
lastEventTime = Date.now();
|
|
252
346
|
streamCallStart = Date.now();
|
|
347
|
+
lastYieldEndTime = Date.now();
|
|
253
348
|
resetIdleTimer();
|
|
254
349
|
for await (const event of result) {
|
|
255
350
|
const pullTime = Date.now();
|
|
256
351
|
const consumerLag = pullTime - lastYieldEndTime;
|
|
257
|
-
if (consumerLag > maxConsumerLagMs)
|
|
352
|
+
if (streamEventCount > 0 && consumerLag > maxConsumerLagMs) {
|
|
353
|
+
maxConsumerLagMs = consumerLag;
|
|
354
|
+
}
|
|
258
355
|
streamEventCount++;
|
|
259
356
|
eventTypeCounts[event.type] = (eventTypeCounts[event.type] ?? 0) + 1;
|
|
260
357
|
lastEventType = event.type;
|
|
@@ -313,9 +410,25 @@ async function* agentLoop(messages, options) {
|
|
|
313
410
|
data: event.data
|
|
314
411
|
};
|
|
315
412
|
} else if (event.type === "toolcall_delta") {
|
|
413
|
+
const chunkChars = event.argsJson?.length ?? 0;
|
|
414
|
+
toolcallDeltaChars += chunkChars;
|
|
415
|
+
toolcallDeltaCount++;
|
|
416
|
+
if (!runawayDetected && (toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS || toolcallDeltaCount > MAX_TOOLCALL_DELTA_EVENTS)) {
|
|
417
|
+
runawayDetected = {
|
|
418
|
+
kind: toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS ? "chars" : "events",
|
|
419
|
+
chars: toolcallDeltaChars,
|
|
420
|
+
events: toolcallDeltaCount
|
|
421
|
+
};
|
|
422
|
+
diag("runaway_toolcall_detected", {
|
|
423
|
+
...runawayDetected,
|
|
424
|
+
provider: options.provider,
|
|
425
|
+
model: options.model
|
|
426
|
+
});
|
|
427
|
+
streamController.abort();
|
|
428
|
+
}
|
|
316
429
|
yield {
|
|
317
430
|
type: "toolcall_delta",
|
|
318
|
-
chars:
|
|
431
|
+
chars: chunkChars
|
|
319
432
|
};
|
|
320
433
|
}
|
|
321
434
|
lastYieldEndTime = Date.now();
|
|
@@ -340,12 +453,44 @@ async function* agentLoop(messages, options) {
|
|
|
340
453
|
model: options.model
|
|
341
454
|
});
|
|
342
455
|
if (isContextOverflow(err)) {
|
|
456
|
+
const overflowDetails = extractContextOverflowDetails(err);
|
|
457
|
+
diag("context_overflow_detected", {
|
|
458
|
+
...overflowDetails,
|
|
459
|
+
error: errMsg.slice(0, 500),
|
|
460
|
+
messages: messages.length
|
|
461
|
+
});
|
|
462
|
+
const overflowToolResultMaxChars = Math.min(
|
|
463
|
+
options.maxToolResultChars ?? 1e5,
|
|
464
|
+
1e5
|
|
465
|
+
);
|
|
466
|
+
if (!toolResultTruncationAttempted) {
|
|
467
|
+
toolResultTruncationAttempted = true;
|
|
468
|
+
const truncated = truncateOversizedToolResults(messages, overflowToolResultMaxChars);
|
|
469
|
+
diag("overflow_tool_result_truncation", {
|
|
470
|
+
truncated,
|
|
471
|
+
maxChars: overflowToolResultMaxChars
|
|
472
|
+
});
|
|
473
|
+
if (truncated) {
|
|
474
|
+
yield {
|
|
475
|
+
type: "retry",
|
|
476
|
+
reason: "overflow_compact",
|
|
477
|
+
attempt: overflowCompactionAttempts + 1,
|
|
478
|
+
maxAttempts: MAX_OVERFLOW_COMPACTIONS,
|
|
479
|
+
delayMs: 0,
|
|
480
|
+
...overflowDetails,
|
|
481
|
+
silent: true
|
|
482
|
+
};
|
|
483
|
+
turn--;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
343
487
|
if (options.transformContext && overflowCompactionAttempts < MAX_OVERFLOW_COMPACTIONS) {
|
|
344
488
|
overflowCompactionAttempts++;
|
|
345
489
|
diag("overflow_compact_start", {
|
|
346
490
|
attempt: overflowCompactionAttempts,
|
|
347
491
|
maxAttempts: MAX_OVERFLOW_COMPACTIONS,
|
|
348
|
-
messages: messages.length
|
|
492
|
+
messages: messages.length,
|
|
493
|
+
...overflowDetails
|
|
349
494
|
});
|
|
350
495
|
try {
|
|
351
496
|
const compacted = await options.transformContext(messages, { force: true });
|
|
@@ -354,14 +499,16 @@ async function* agentLoop(messages, options) {
|
|
|
354
499
|
messages.push(...compacted);
|
|
355
500
|
diag("overflow_compact_success", {
|
|
356
501
|
attempt: overflowCompactionAttempts,
|
|
357
|
-
messages: messages.length
|
|
502
|
+
messages: messages.length,
|
|
503
|
+
...overflowDetails
|
|
358
504
|
});
|
|
359
505
|
yield {
|
|
360
506
|
type: "retry",
|
|
361
507
|
reason: "overflow_compact",
|
|
362
508
|
attempt: overflowCompactionAttempts,
|
|
363
509
|
maxAttempts: MAX_OVERFLOW_COMPACTIONS,
|
|
364
|
-
delayMs: 0
|
|
510
|
+
delayMs: 0,
|
|
511
|
+
...overflowDetails
|
|
365
512
|
};
|
|
366
513
|
turn--;
|
|
367
514
|
continue;
|
|
@@ -369,11 +516,13 @@ async function* agentLoop(messages, options) {
|
|
|
369
516
|
diag("overflow_compact_noop", {
|
|
370
517
|
attempt: overflowCompactionAttempts,
|
|
371
518
|
before: messages.length,
|
|
372
|
-
after: compacted.length
|
|
519
|
+
after: compacted.length,
|
|
520
|
+
...overflowDetails
|
|
373
521
|
});
|
|
374
522
|
} catch (compactErr) {
|
|
375
523
|
diag("overflow_compact_failed", {
|
|
376
|
-
error: compactErr instanceof Error ? compactErr.message : String(compactErr)
|
|
524
|
+
error: compactErr instanceof Error ? compactErr.message : String(compactErr),
|
|
525
|
+
...overflowDetails
|
|
377
526
|
});
|
|
378
527
|
}
|
|
379
528
|
}
|
|
@@ -404,22 +553,39 @@ async function* agentLoop(messages, options) {
|
|
|
404
553
|
turn--;
|
|
405
554
|
continue;
|
|
406
555
|
}
|
|
556
|
+
if (runawayDetected) {
|
|
557
|
+
diag("runaway_toolcall_aborted", {
|
|
558
|
+
...runawayDetected,
|
|
559
|
+
provider: options.provider,
|
|
560
|
+
model: options.model
|
|
561
|
+
});
|
|
562
|
+
const detail = runawayDetected.kind === "chars" ? `${(runawayDetected.chars / 1024).toFixed(0)} KB of tool-call arguments` : `${runawayDetected.events} tool-call delta events`;
|
|
563
|
+
yield {
|
|
564
|
+
type: "error",
|
|
565
|
+
error: new Error(
|
|
566
|
+
`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.`
|
|
567
|
+
)
|
|
568
|
+
};
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
407
571
|
const malformed = isMalformedStream(err);
|
|
408
|
-
const
|
|
572
|
+
const socketDrop = isTransportFailure(err);
|
|
573
|
+
const transportFailure = (idleTimedOut || malformed || socketDrop) && !options.signal?.aborted;
|
|
409
574
|
if (transportFailure && stallRetries < MAX_STALL_RETRIES) {
|
|
410
575
|
stallRetries++;
|
|
576
|
+
const cause = malformed ? "malformed_stream" : socketDrop ? "socket_drop" : "stream_stall";
|
|
411
577
|
if (!useNonStreamingFallback && stallRetries >= STALL_RETRIES_BEFORE_NON_STREAMING) {
|
|
412
578
|
useNonStreamingFallback = true;
|
|
413
579
|
diag("non_streaming_fallback_enabled", {
|
|
414
580
|
stallRetries,
|
|
415
581
|
provider: options.provider,
|
|
416
582
|
model: options.model,
|
|
417
|
-
cause
|
|
583
|
+
cause
|
|
418
584
|
});
|
|
419
585
|
}
|
|
420
586
|
const delayMs = Math.min(STALL_DELAY_MS * 2 ** (stallRetries - 1), 8e3);
|
|
421
587
|
diag("retry", {
|
|
422
|
-
reason:
|
|
588
|
+
reason: cause,
|
|
423
589
|
attempt: stallRetries,
|
|
424
590
|
maxAttempts: MAX_STALL_RETRIES,
|
|
425
591
|
delayMs,
|
|
@@ -574,117 +740,26 @@ async function* agentLoop(messages, options) {
|
|
|
574
740
|
toolCalls.push(tc);
|
|
575
741
|
}
|
|
576
742
|
}
|
|
577
|
-
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
signal: options.signal ?? AbortSignal.timeout(3e5),
|
|
598
|
-
toolCallId: toolCall.id,
|
|
599
|
-
onUpdate: (update) => {
|
|
600
|
-
eventStream.push({
|
|
601
|
-
type: "tool_call_update",
|
|
602
|
-
toolCallId: toolCall.id,
|
|
603
|
-
update
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
};
|
|
607
|
-
const raw = await tool.execute(parsed, ctx);
|
|
608
|
-
const normalized = normalizeToolResult(raw);
|
|
609
|
-
resultContent = normalized.content;
|
|
610
|
-
details = normalized.details;
|
|
611
|
-
} catch (err) {
|
|
612
|
-
isError = true;
|
|
613
|
-
resultContent = err instanceof Error ? err.message : String(err);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
const durationMs = Date.now() - startTime;
|
|
617
|
-
eventStream.push({
|
|
618
|
-
type: "tool_call_end",
|
|
619
|
-
toolCallId: toolCall.id,
|
|
620
|
-
result: toolResultPreview(resultContent),
|
|
621
|
-
details,
|
|
622
|
-
isError,
|
|
623
|
-
durationMs
|
|
624
|
-
});
|
|
625
|
-
return { toolCallId: toolCall.id, content: resultContent, isError };
|
|
626
|
-
});
|
|
627
|
-
const abortHandler = () => eventStream.abort(new Error("aborted"));
|
|
628
|
-
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
629
|
-
let toolResultsFinalized = false;
|
|
630
|
-
Promise.all(executions).then((results) => {
|
|
631
|
-
if (toolResultsFinalized) return;
|
|
632
|
-
const resultsMap = new Map(results.map((r) => [r.toolCallId, r]));
|
|
633
|
-
for (const tc of toolCalls) {
|
|
634
|
-
const r = resultsMap.get(tc.id);
|
|
635
|
-
toolResults.push({
|
|
636
|
-
type: "tool_result",
|
|
637
|
-
toolCallId: tc.id,
|
|
638
|
-
content: r.content,
|
|
639
|
-
isError: r.isError || void 0
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
eventStream.close();
|
|
643
|
-
}).catch((err) => eventStream.abort(err instanceof Error ? err : new Error(String(err))));
|
|
644
|
-
let toolsAborted = false;
|
|
645
|
-
try {
|
|
646
|
-
for await (const event of eventStream) {
|
|
647
|
-
yield event;
|
|
648
|
-
}
|
|
649
|
-
} catch (err) {
|
|
650
|
-
if (isAbortError(err) || options.signal?.aborted) {
|
|
651
|
-
toolsAborted = true;
|
|
652
|
-
} else {
|
|
653
|
-
throw err;
|
|
654
|
-
}
|
|
655
|
-
} finally {
|
|
656
|
-
options.signal?.removeEventListener("abort", abortHandler);
|
|
657
|
-
toolResultsFinalized = true;
|
|
658
|
-
const resolvedIds = new Set(toolResults.map((r) => r.toolCallId));
|
|
659
|
-
for (const tc of toolCalls) {
|
|
660
|
-
if (!resolvedIds.has(tc.id)) {
|
|
661
|
-
toolResults.push({
|
|
662
|
-
type: "tool_result",
|
|
663
|
-
toolCallId: tc.id,
|
|
664
|
-
content: "Tool execution was aborted.",
|
|
665
|
-
isError: true
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
if (options.maxToolResultChars) {
|
|
670
|
-
const HARD_MAX = 4e5;
|
|
671
|
-
const max = Math.min(options.maxToolResultChars, HARD_MAX);
|
|
672
|
-
for (const tr of toolResults) {
|
|
673
|
-
if (typeof tr.content === "string" && tr.content.length > max) {
|
|
674
|
-
const headChars = Math.floor(max * 0.7);
|
|
675
|
-
const tailChars = max - headChars;
|
|
676
|
-
const head = tr.content.slice(0, headChars);
|
|
677
|
-
const tail = tr.content.slice(-tailChars);
|
|
678
|
-
const omitted = tr.content.length - headChars - tailChars;
|
|
679
|
-
tr.content = head + `
|
|
680
|
-
|
|
681
|
-
[... ${omitted} characters omitted ...]
|
|
682
|
-
|
|
683
|
-
` + tail;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
messages.push({ role: "tool", content: toolResults });
|
|
743
|
+
let fatalToolArgumentError = null;
|
|
744
|
+
const markFatalToolArgumentError = (error) => {
|
|
745
|
+
fatalToolArgumentError = error;
|
|
746
|
+
};
|
|
747
|
+
const executionOptions = {
|
|
748
|
+
signal: options.signal,
|
|
749
|
+
maxToolResultChars: options.maxToolResultChars,
|
|
750
|
+
toolMap,
|
|
751
|
+
invalidToolArgumentCounts,
|
|
752
|
+
markFatalToolArgumentError
|
|
753
|
+
};
|
|
754
|
+
const hasSequentialToolCall = toolCalls.some(
|
|
755
|
+
(toolCall) => toolMap.get(toolCall.name)?.executionMode === "sequential"
|
|
756
|
+
);
|
|
757
|
+
const executionResult = hasSequentialToolCall ? yield* executeToolCallsSequential(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
|
|
758
|
+
messages.push({ role: "tool", content: executionResult.toolResults });
|
|
759
|
+
const toolsAborted = executionResult.aborted;
|
|
760
|
+
if (fatalToolArgumentError) {
|
|
761
|
+
yield { type: "error", error: fatalToolArgumentError };
|
|
762
|
+
break;
|
|
688
763
|
}
|
|
689
764
|
if (toolsAborted) break;
|
|
690
765
|
if (options.getSteeringMessages) {
|
|
@@ -718,6 +793,197 @@ async function* agentLoop(messages, options) {
|
|
|
718
793
|
totalUsage: { ...totalUsage }
|
|
719
794
|
};
|
|
720
795
|
}
|
|
796
|
+
function pushToolEvent(eventStream, state, event) {
|
|
797
|
+
if (!state.finalized) eventStream.push(event);
|
|
798
|
+
}
|
|
799
|
+
async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
800
|
+
const startTime = Date.now();
|
|
801
|
+
pushEvent({
|
|
802
|
+
type: "tool_call_start",
|
|
803
|
+
toolCallId: toolCall.id,
|
|
804
|
+
name: toolCall.name,
|
|
805
|
+
args: toolCall.args
|
|
806
|
+
});
|
|
807
|
+
let resultContent;
|
|
808
|
+
let details;
|
|
809
|
+
let isError = false;
|
|
810
|
+
const tool = options.toolMap.get(toolCall.name);
|
|
811
|
+
if (!tool) {
|
|
812
|
+
resultContent = `Unknown tool: ${toolCall.name}`;
|
|
813
|
+
isError = true;
|
|
814
|
+
} else {
|
|
815
|
+
try {
|
|
816
|
+
const parsed = tool.parameters.parse(toolCall.args);
|
|
817
|
+
const ctx = {
|
|
818
|
+
signal: options.signal ?? AbortSignal.timeout(3e5),
|
|
819
|
+
toolCallId: toolCall.id,
|
|
820
|
+
onUpdate: (update) => {
|
|
821
|
+
pushEvent({
|
|
822
|
+
type: "tool_call_update",
|
|
823
|
+
toolCallId: toolCall.id,
|
|
824
|
+
update
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
const raw = await tool.execute(parsed, ctx);
|
|
829
|
+
const normalized = normalizeToolResult(raw);
|
|
830
|
+
resultContent = normalized.content;
|
|
831
|
+
details = normalized.details;
|
|
832
|
+
for (const key of options.invalidToolArgumentCounts.keys()) {
|
|
833
|
+
if (key.startsWith(`${toolCall.name}:`)) options.invalidToolArgumentCounts.delete(key);
|
|
834
|
+
}
|
|
835
|
+
} catch (err) {
|
|
836
|
+
isError = true;
|
|
837
|
+
if (err instanceof import_zod.ZodError) {
|
|
838
|
+
const prettyError = (0, import_zod.prettifyError)(err);
|
|
839
|
+
const failureKey = `${toolCall.name}:${prettyError}`;
|
|
840
|
+
const failureCount = (options.invalidToolArgumentCounts.get(failureKey) ?? 0) + 1;
|
|
841
|
+
options.invalidToolArgumentCounts.set(failureKey, failureCount);
|
|
842
|
+
resultContent = `Invalid arguments for tool \`${toolCall.name}\`:
|
|
843
|
+
` + prettyError + "\nRe-issue the call with each field as the correct type.";
|
|
844
|
+
if (failureCount >= 3) {
|
|
845
|
+
options.markFatalToolArgumentError(
|
|
846
|
+
new Error(
|
|
847
|
+
`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.`
|
|
848
|
+
)
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
} else {
|
|
852
|
+
resultContent = err instanceof Error ? err.message : String(err);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
const durationMs = Date.now() - startTime;
|
|
857
|
+
pushEvent({
|
|
858
|
+
type: "tool_call_end",
|
|
859
|
+
toolCallId: toolCall.id,
|
|
860
|
+
result: toolResultPreview(resultContent),
|
|
861
|
+
details,
|
|
862
|
+
isError,
|
|
863
|
+
durationMs
|
|
864
|
+
});
|
|
865
|
+
return { toolCallId: toolCall.id, content: resultContent, isError };
|
|
866
|
+
}
|
|
867
|
+
async function* executeToolCallsSequential(toolCalls, initialToolResults, options) {
|
|
868
|
+
const eventStream = new import_ai.EventStream();
|
|
869
|
+
const state = { finalized: false };
|
|
870
|
+
const resultsById = /* @__PURE__ */ new Map();
|
|
871
|
+
const abortHandler = () => eventStream.abort(new Error("aborted"));
|
|
872
|
+
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
873
|
+
void (async () => {
|
|
874
|
+
try {
|
|
875
|
+
for (const toolCall of toolCalls) {
|
|
876
|
+
if (options.signal?.aborted) break;
|
|
877
|
+
const record = await executeSingleToolCall(
|
|
878
|
+
toolCall,
|
|
879
|
+
options,
|
|
880
|
+
(event) => pushToolEvent(eventStream, state, event)
|
|
881
|
+
);
|
|
882
|
+
resultsById.set(record.toolCallId, record);
|
|
883
|
+
}
|
|
884
|
+
if (!state.finalized) eventStream.close();
|
|
885
|
+
} catch (err) {
|
|
886
|
+
if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
|
|
887
|
+
}
|
|
888
|
+
})();
|
|
889
|
+
let aborted = false;
|
|
890
|
+
try {
|
|
891
|
+
for await (const event of eventStream) {
|
|
892
|
+
yield event;
|
|
893
|
+
}
|
|
894
|
+
} catch (err) {
|
|
895
|
+
if (isAbortError(err) || options.signal?.aborted) {
|
|
896
|
+
aborted = true;
|
|
897
|
+
} else {
|
|
898
|
+
throw err;
|
|
899
|
+
}
|
|
900
|
+
} finally {
|
|
901
|
+
options.signal?.removeEventListener("abort", abortHandler);
|
|
902
|
+
state.finalized = true;
|
|
903
|
+
}
|
|
904
|
+
const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById);
|
|
905
|
+
capToolResults(toolResults, options.maxToolResultChars);
|
|
906
|
+
return { toolResults, aborted };
|
|
907
|
+
}
|
|
908
|
+
async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
|
|
909
|
+
const eventStream = new import_ai.EventStream();
|
|
910
|
+
const state = { finalized: false };
|
|
911
|
+
const resultsById = /* @__PURE__ */ new Map();
|
|
912
|
+
const abortHandler = () => eventStream.abort(new Error("aborted"));
|
|
913
|
+
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
914
|
+
Promise.all(
|
|
915
|
+
toolCalls.map(async (toolCall) => {
|
|
916
|
+
const record = await executeSingleToolCall(
|
|
917
|
+
toolCall,
|
|
918
|
+
options,
|
|
919
|
+
(event) => pushToolEvent(eventStream, state, event)
|
|
920
|
+
);
|
|
921
|
+
resultsById.set(record.toolCallId, record);
|
|
922
|
+
})
|
|
923
|
+
).then(() => {
|
|
924
|
+
if (!state.finalized) eventStream.close();
|
|
925
|
+
}).catch((err) => {
|
|
926
|
+
if (!state.finalized) eventStream.abort(err instanceof Error ? err : new Error(String(err)));
|
|
927
|
+
});
|
|
928
|
+
let aborted = false;
|
|
929
|
+
try {
|
|
930
|
+
for await (const event of eventStream) {
|
|
931
|
+
yield event;
|
|
932
|
+
}
|
|
933
|
+
} catch (err) {
|
|
934
|
+
if (isAbortError(err) || options.signal?.aborted) {
|
|
935
|
+
aborted = true;
|
|
936
|
+
} else {
|
|
937
|
+
throw err;
|
|
938
|
+
}
|
|
939
|
+
} finally {
|
|
940
|
+
options.signal?.removeEventListener("abort", abortHandler);
|
|
941
|
+
state.finalized = true;
|
|
942
|
+
}
|
|
943
|
+
const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById);
|
|
944
|
+
capToolResults(toolResults, options.maxToolResultChars);
|
|
945
|
+
return { toolResults, aborted };
|
|
946
|
+
}
|
|
947
|
+
function buildToolResults(initialToolResults, toolCalls, resultsById) {
|
|
948
|
+
const toolResults = [...initialToolResults];
|
|
949
|
+
for (const toolCall of toolCalls) {
|
|
950
|
+
const result = resultsById.get(toolCall.id);
|
|
951
|
+
if (result) {
|
|
952
|
+
toolResults.push({
|
|
953
|
+
type: "tool_result",
|
|
954
|
+
toolCallId: toolCall.id,
|
|
955
|
+
content: result.content,
|
|
956
|
+
isError: result.isError || void 0
|
|
957
|
+
});
|
|
958
|
+
} else {
|
|
959
|
+
toolResults.push({
|
|
960
|
+
type: "tool_result",
|
|
961
|
+
toolCallId: toolCall.id,
|
|
962
|
+
content: "Tool execution was aborted.",
|
|
963
|
+
isError: true
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return toolResults;
|
|
968
|
+
}
|
|
969
|
+
function capToolResults(toolResults, maxToolResultChars) {
|
|
970
|
+
if (!maxToolResultChars) return;
|
|
971
|
+
const hardMax = 4e5;
|
|
972
|
+
const max = Math.min(maxToolResultChars, hardMax);
|
|
973
|
+
for (const toolResult of toolResults) {
|
|
974
|
+
if (typeof toolResult.content !== "string" || toolResult.content.length <= max) continue;
|
|
975
|
+
const headChars = Math.floor(max * 0.7);
|
|
976
|
+
const tailChars = max - headChars;
|
|
977
|
+
const head = toolResult.content.slice(0, headChars);
|
|
978
|
+
const tail = toolResult.content.slice(-tailChars);
|
|
979
|
+
const omitted = toolResult.content.length - headChars - tailChars;
|
|
980
|
+
toolResult.content = head + `
|
|
981
|
+
|
|
982
|
+
[... ${omitted} characters omitted ...]
|
|
983
|
+
|
|
984
|
+
` + tail;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
721
987
|
function normalizeToolResult(raw) {
|
|
722
988
|
return typeof raw === "string" ? { content: raw } : raw;
|
|
723
989
|
}
|
|
@@ -725,6 +991,44 @@ function toolResultPreview(content) {
|
|
|
725
991
|
if (typeof content === "string") return content;
|
|
726
992
|
return content.map((block) => block.type === "text" ? block.text : `[image ${block.mediaType}]`).join("\n");
|
|
727
993
|
}
|
|
994
|
+
function truncateToolResultText(text, maxChars) {
|
|
995
|
+
if (text.length <= maxChars) return text;
|
|
996
|
+
const tailChars = Math.min(Math.floor(maxChars * 0.3), 2e4);
|
|
997
|
+
const headChars = Math.max(maxChars - tailChars, 0);
|
|
998
|
+
const omitted = text.length - headChars - tailChars;
|
|
999
|
+
return `${text.slice(0, headChars)}
|
|
1000
|
+
|
|
1001
|
+
[... ${omitted} characters omitted after context overflow ...]
|
|
1002
|
+
|
|
1003
|
+
${text.slice(-tailChars)}`;
|
|
1004
|
+
}
|
|
1005
|
+
function truncateOversizedToolResults(messages, maxChars) {
|
|
1006
|
+
if (maxChars <= 0) return false;
|
|
1007
|
+
let changed = false;
|
|
1008
|
+
for (const msg of messages) {
|
|
1009
|
+
if (msg.role !== "tool" || !Array.isArray(msg.content)) continue;
|
|
1010
|
+
const results = msg.content;
|
|
1011
|
+
for (const result of results) {
|
|
1012
|
+
if (typeof result.content === "string") {
|
|
1013
|
+
const truncated = truncateToolResultText(result.content, maxChars);
|
|
1014
|
+
if (truncated !== result.content) {
|
|
1015
|
+
result.content = truncated;
|
|
1016
|
+
changed = true;
|
|
1017
|
+
}
|
|
1018
|
+
} else {
|
|
1019
|
+
for (const block of result.content) {
|
|
1020
|
+
if (block.type !== "text") continue;
|
|
1021
|
+
const truncated = truncateToolResultText(block.text, maxChars);
|
|
1022
|
+
if (truncated !== block.text) {
|
|
1023
|
+
block.text = truncated;
|
|
1024
|
+
changed = true;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return changed;
|
|
1031
|
+
}
|
|
728
1032
|
function extractToolCalls(content) {
|
|
729
1033
|
if (typeof content === "string") return [];
|
|
730
1034
|
return content.filter((part) => part.type === "tool_call");
|