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