@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.cjs +439 -128
- 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 +439 -128
- 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();
|
|
@@ -66,11 +105,22 @@ function isToolPairingError(err) {
|
|
|
66
105
|
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"
|
|
67
106
|
msg.includes("tool call id") && msg.includes("is not found");
|
|
68
107
|
}
|
|
69
|
-
function
|
|
70
|
-
if (!(err instanceof Error)) return
|
|
71
|
-
if (isBillingError(err)) return
|
|
108
|
+
function classifyOverload(err) {
|
|
109
|
+
if (!(err instanceof Error)) return null;
|
|
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")) {
|
|
115
|
+
return "rate_limit";
|
|
116
|
+
}
|
|
117
|
+
if (statusCode === 529 || msg.includes("overloaded") || msg.includes("529")) {
|
|
118
|
+
return "overloaded";
|
|
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
|
+
}
|
|
123
|
+
return null;
|
|
74
124
|
}
|
|
75
125
|
function isMalformedStream(err) {
|
|
76
126
|
if (!(err instanceof Error)) return false;
|
|
@@ -80,6 +130,46 @@ function isMalformedStream(err) {
|
|
|
80
130
|
const msg = err.message;
|
|
81
131
|
return /\bin JSON at position \d+/i.test(msg);
|
|
82
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
|
+
}
|
|
83
173
|
function abortableSleep(ms, signal) {
|
|
84
174
|
if (signal?.aborted) {
|
|
85
175
|
return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
@@ -110,6 +200,8 @@ async function* agentLoop(messages, options) {
|
|
|
110
200
|
let emptyResponseRetries = 0;
|
|
111
201
|
let stallRetries = 0;
|
|
112
202
|
let overflowCompactionAttempts = 0;
|
|
203
|
+
let toolResultTruncationAttempted = false;
|
|
204
|
+
const invalidToolArgumentCounts = /* @__PURE__ */ new Map();
|
|
113
205
|
let useNonStreamingFallback = false;
|
|
114
206
|
const MAX_OVERLOAD_RETRIES = 10;
|
|
115
207
|
const MAX_EMPTY_RESPONSE_RETRIES = 2;
|
|
@@ -126,6 +218,8 @@ async function* agentLoop(messages, options) {
|
|
|
126
218
|
const STREAM_THINKING_IDLE_TIMEOUT_MS = 3e5;
|
|
127
219
|
const STREAM_THINKING_HARD_TIMEOUT_MS = 6e5;
|
|
128
220
|
const NON_STREAMING_HARD_TIMEOUT_MS = 3e5;
|
|
221
|
+
const MAX_TOOLCALL_DELTA_CHARS = 1e6;
|
|
222
|
+
const MAX_TOOLCALL_DELTA_EVENTS = 2e4;
|
|
129
223
|
try {
|
|
130
224
|
while (turn < maxTurns) {
|
|
131
225
|
options.signal?.throwIfAborted();
|
|
@@ -181,6 +275,9 @@ async function* agentLoop(messages, options) {
|
|
|
181
275
|
let streamCallStart = Date.now();
|
|
182
276
|
const eventTypeCounts = {};
|
|
183
277
|
let lastEventType = "";
|
|
278
|
+
let toolcallDeltaChars = 0;
|
|
279
|
+
let toolcallDeltaCount = 0;
|
|
280
|
+
let runawayDetected = null;
|
|
184
281
|
let lastYieldEndTime = Date.now();
|
|
185
282
|
let maxConsumerLagMs = 0;
|
|
186
283
|
const forwardAbort = () => streamController.abort();
|
|
@@ -231,9 +328,12 @@ async function* agentLoop(messages, options) {
|
|
|
231
328
|
signal: streamController.signal,
|
|
232
329
|
accountId: options.accountId,
|
|
233
330
|
cacheRetention: options.cacheRetention,
|
|
331
|
+
promptCacheKey: options.promptCacheKey,
|
|
332
|
+
serviceTier: options.serviceTier,
|
|
234
333
|
supportsImages: options.supportsImages,
|
|
235
334
|
compaction: options.compaction,
|
|
236
335
|
clearToolUses: options.clearToolUses,
|
|
336
|
+
userAgent: options.userAgent,
|
|
237
337
|
// Flip to non-streaming fallback after repeated stream stalls.
|
|
238
338
|
...useNonStreamingFallback ? { streaming: false } : {}
|
|
239
339
|
});
|
|
@@ -244,11 +344,14 @@ async function* agentLoop(messages, options) {
|
|
|
244
344
|
hasReceivedEvent = false;
|
|
245
345
|
lastEventTime = Date.now();
|
|
246
346
|
streamCallStart = Date.now();
|
|
347
|
+
lastYieldEndTime = Date.now();
|
|
247
348
|
resetIdleTimer();
|
|
248
349
|
for await (const event of result) {
|
|
249
350
|
const pullTime = Date.now();
|
|
250
351
|
const consumerLag = pullTime - lastYieldEndTime;
|
|
251
|
-
if (consumerLag > maxConsumerLagMs)
|
|
352
|
+
if (streamEventCount > 0 && consumerLag > maxConsumerLagMs) {
|
|
353
|
+
maxConsumerLagMs = consumerLag;
|
|
354
|
+
}
|
|
252
355
|
streamEventCount++;
|
|
253
356
|
eventTypeCounts[event.type] = (eventTypeCounts[event.type] ?? 0) + 1;
|
|
254
357
|
lastEventType = event.type;
|
|
@@ -307,9 +410,25 @@ async function* agentLoop(messages, options) {
|
|
|
307
410
|
data: event.data
|
|
308
411
|
};
|
|
309
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
|
+
}
|
|
310
429
|
yield {
|
|
311
430
|
type: "toolcall_delta",
|
|
312
|
-
chars:
|
|
431
|
+
chars: chunkChars
|
|
313
432
|
};
|
|
314
433
|
}
|
|
315
434
|
lastYieldEndTime = Date.now();
|
|
@@ -334,12 +453,44 @@ async function* agentLoop(messages, options) {
|
|
|
334
453
|
model: options.model
|
|
335
454
|
});
|
|
336
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
|
+
}
|
|
337
487
|
if (options.transformContext && overflowCompactionAttempts < MAX_OVERFLOW_COMPACTIONS) {
|
|
338
488
|
overflowCompactionAttempts++;
|
|
339
489
|
diag("overflow_compact_start", {
|
|
340
490
|
attempt: overflowCompactionAttempts,
|
|
341
491
|
maxAttempts: MAX_OVERFLOW_COMPACTIONS,
|
|
342
|
-
messages: messages.length
|
|
492
|
+
messages: messages.length,
|
|
493
|
+
...overflowDetails
|
|
343
494
|
});
|
|
344
495
|
try {
|
|
345
496
|
const compacted = await options.transformContext(messages, { force: true });
|
|
@@ -348,14 +499,16 @@ async function* agentLoop(messages, options) {
|
|
|
348
499
|
messages.push(...compacted);
|
|
349
500
|
diag("overflow_compact_success", {
|
|
350
501
|
attempt: overflowCompactionAttempts,
|
|
351
|
-
messages: messages.length
|
|
502
|
+
messages: messages.length,
|
|
503
|
+
...overflowDetails
|
|
352
504
|
});
|
|
353
505
|
yield {
|
|
354
506
|
type: "retry",
|
|
355
507
|
reason: "overflow_compact",
|
|
356
508
|
attempt: overflowCompactionAttempts,
|
|
357
509
|
maxAttempts: MAX_OVERFLOW_COMPACTIONS,
|
|
358
|
-
delayMs: 0
|
|
510
|
+
delayMs: 0,
|
|
511
|
+
...overflowDetails
|
|
359
512
|
};
|
|
360
513
|
turn--;
|
|
361
514
|
continue;
|
|
@@ -363,32 +516,35 @@ async function* agentLoop(messages, options) {
|
|
|
363
516
|
diag("overflow_compact_noop", {
|
|
364
517
|
attempt: overflowCompactionAttempts,
|
|
365
518
|
before: messages.length,
|
|
366
|
-
after: compacted.length
|
|
519
|
+
after: compacted.length,
|
|
520
|
+
...overflowDetails
|
|
367
521
|
});
|
|
368
522
|
} catch (compactErr) {
|
|
369
523
|
diag("overflow_compact_failed", {
|
|
370
|
-
error: compactErr instanceof Error ? compactErr.message : String(compactErr)
|
|
524
|
+
error: compactErr instanceof Error ? compactErr.message : String(compactErr),
|
|
525
|
+
...overflowDetails
|
|
371
526
|
});
|
|
372
527
|
}
|
|
373
528
|
}
|
|
374
529
|
yield { type: "error", error: err instanceof Error ? err : new Error(errMsg) };
|
|
375
530
|
throw err;
|
|
376
531
|
}
|
|
377
|
-
|
|
532
|
+
const overloadKind = classifyOverload(err);
|
|
533
|
+
if (overloadRetries < MAX_OVERLOAD_RETRIES && overloadKind) {
|
|
378
534
|
overloadRetries++;
|
|
379
535
|
const delayMs = Math.min(
|
|
380
536
|
OVERLOAD_BASE_DELAY_MS * 2 ** (overloadRetries - 1),
|
|
381
537
|
OVERLOAD_MAX_DELAY_MS
|
|
382
538
|
);
|
|
383
539
|
diag("retry", {
|
|
384
|
-
reason:
|
|
540
|
+
reason: overloadKind,
|
|
385
541
|
attempt: overloadRetries,
|
|
386
542
|
maxAttempts: MAX_OVERLOAD_RETRIES,
|
|
387
543
|
delayMs
|
|
388
544
|
});
|
|
389
545
|
yield {
|
|
390
546
|
type: "retry",
|
|
391
|
-
reason:
|
|
547
|
+
reason: overloadKind,
|
|
392
548
|
attempt: overloadRetries,
|
|
393
549
|
maxAttempts: MAX_OVERLOAD_RETRIES,
|
|
394
550
|
delayMs
|
|
@@ -397,22 +553,39 @@ async function* agentLoop(messages, options) {
|
|
|
397
553
|
turn--;
|
|
398
554
|
continue;
|
|
399
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
|
+
}
|
|
400
571
|
const malformed = isMalformedStream(err);
|
|
401
|
-
const
|
|
572
|
+
const socketDrop = isTransportFailure(err);
|
|
573
|
+
const transportFailure = (idleTimedOut || malformed || socketDrop) && !options.signal?.aborted;
|
|
402
574
|
if (transportFailure && stallRetries < MAX_STALL_RETRIES) {
|
|
403
575
|
stallRetries++;
|
|
576
|
+
const cause = malformed ? "malformed_stream" : socketDrop ? "socket_drop" : "stream_stall";
|
|
404
577
|
if (!useNonStreamingFallback && stallRetries >= STALL_RETRIES_BEFORE_NON_STREAMING) {
|
|
405
578
|
useNonStreamingFallback = true;
|
|
406
579
|
diag("non_streaming_fallback_enabled", {
|
|
407
580
|
stallRetries,
|
|
408
581
|
provider: options.provider,
|
|
409
582
|
model: options.model,
|
|
410
|
-
cause
|
|
583
|
+
cause
|
|
411
584
|
});
|
|
412
585
|
}
|
|
413
586
|
const delayMs = Math.min(STALL_DELAY_MS * 2 ** (stallRetries - 1), 8e3);
|
|
414
587
|
diag("retry", {
|
|
415
|
-
reason:
|
|
588
|
+
reason: cause,
|
|
416
589
|
attempt: stallRetries,
|
|
417
590
|
maxAttempts: MAX_STALL_RETRIES,
|
|
418
591
|
delayMs,
|
|
@@ -567,117 +740,26 @@ async function* agentLoop(messages, options) {
|
|
|
567
740
|
toolCalls.push(tc);
|
|
568
741
|
}
|
|
569
742
|
}
|
|
570
|
-
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
signal: options.signal ?? AbortSignal.timeout(3e5),
|
|
591
|
-
toolCallId: toolCall.id,
|
|
592
|
-
onUpdate: (update) => {
|
|
593
|
-
eventStream.push({
|
|
594
|
-
type: "tool_call_update",
|
|
595
|
-
toolCallId: toolCall.id,
|
|
596
|
-
update
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
};
|
|
600
|
-
const raw = await tool.execute(parsed, ctx);
|
|
601
|
-
const normalized = normalizeToolResult(raw);
|
|
602
|
-
resultContent = normalized.content;
|
|
603
|
-
details = normalized.details;
|
|
604
|
-
} catch (err) {
|
|
605
|
-
isError = true;
|
|
606
|
-
resultContent = err instanceof Error ? err.message : String(err);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
const durationMs = Date.now() - startTime;
|
|
610
|
-
eventStream.push({
|
|
611
|
-
type: "tool_call_end",
|
|
612
|
-
toolCallId: toolCall.id,
|
|
613
|
-
result: toolResultPreview(resultContent),
|
|
614
|
-
details,
|
|
615
|
-
isError,
|
|
616
|
-
durationMs
|
|
617
|
-
});
|
|
618
|
-
return { toolCallId: toolCall.id, content: resultContent, isError };
|
|
619
|
-
});
|
|
620
|
-
const abortHandler = () => eventStream.abort(new Error("aborted"));
|
|
621
|
-
options.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
622
|
-
let toolResultsFinalized = false;
|
|
623
|
-
Promise.all(executions).then((results) => {
|
|
624
|
-
if (toolResultsFinalized) return;
|
|
625
|
-
const resultsMap = new Map(results.map((r) => [r.toolCallId, r]));
|
|
626
|
-
for (const tc of toolCalls) {
|
|
627
|
-
const r = resultsMap.get(tc.id);
|
|
628
|
-
toolResults.push({
|
|
629
|
-
type: "tool_result",
|
|
630
|
-
toolCallId: tc.id,
|
|
631
|
-
content: r.content,
|
|
632
|
-
isError: r.isError || void 0
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
eventStream.close();
|
|
636
|
-
}).catch((err) => eventStream.abort(err instanceof Error ? err : new Error(String(err))));
|
|
637
|
-
let toolsAborted = false;
|
|
638
|
-
try {
|
|
639
|
-
for await (const event of eventStream) {
|
|
640
|
-
yield event;
|
|
641
|
-
}
|
|
642
|
-
} catch (err) {
|
|
643
|
-
if (isAbortError(err) || options.signal?.aborted) {
|
|
644
|
-
toolsAborted = true;
|
|
645
|
-
} else {
|
|
646
|
-
throw err;
|
|
647
|
-
}
|
|
648
|
-
} finally {
|
|
649
|
-
options.signal?.removeEventListener("abort", abortHandler);
|
|
650
|
-
toolResultsFinalized = true;
|
|
651
|
-
const resolvedIds = new Set(toolResults.map((r) => r.toolCallId));
|
|
652
|
-
for (const tc of toolCalls) {
|
|
653
|
-
if (!resolvedIds.has(tc.id)) {
|
|
654
|
-
toolResults.push({
|
|
655
|
-
type: "tool_result",
|
|
656
|
-
toolCallId: tc.id,
|
|
657
|
-
content: "Tool execution was aborted.",
|
|
658
|
-
isError: true
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
if (options.maxToolResultChars) {
|
|
663
|
-
const HARD_MAX = 4e5;
|
|
664
|
-
const max = Math.min(options.maxToolResultChars, HARD_MAX);
|
|
665
|
-
for (const tr of toolResults) {
|
|
666
|
-
if (typeof tr.content === "string" && tr.content.length > max) {
|
|
667
|
-
const headChars = Math.floor(max * 0.7);
|
|
668
|
-
const tailChars = max - headChars;
|
|
669
|
-
const head = tr.content.slice(0, headChars);
|
|
670
|
-
const tail = tr.content.slice(-tailChars);
|
|
671
|
-
const omitted = tr.content.length - headChars - tailChars;
|
|
672
|
-
tr.content = head + `
|
|
673
|
-
|
|
674
|
-
[... ${omitted} characters omitted ...]
|
|
675
|
-
|
|
676
|
-
` + tail;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
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;
|
|
681
763
|
}
|
|
682
764
|
if (toolsAborted) break;
|
|
683
765
|
if (options.getSteeringMessages) {
|
|
@@ -711,6 +793,197 @@ async function* agentLoop(messages, options) {
|
|
|
711
793
|
totalUsage: { ...totalUsage }
|
|
712
794
|
};
|
|
713
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
|
+
}
|
|
714
987
|
function normalizeToolResult(raw) {
|
|
715
988
|
return typeof raw === "string" ? { content: raw } : raw;
|
|
716
989
|
}
|
|
@@ -718,6 +991,44 @@ function toolResultPreview(content) {
|
|
|
718
991
|
if (typeof content === "string") return content;
|
|
719
992
|
return content.map((block) => block.type === "text" ? block.text : `[image ${block.mediaType}]`).join("\n");
|
|
720
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
|
+
}
|
|
721
1032
|
function extractToolCalls(content) {
|
|
722
1033
|
if (typeof content === "string") return [];
|
|
723
1034
|
return content.filter((part) => part.type === "tool_call");
|