@prestyj/agent 5.9.0 → 5.11.0
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 +155 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -2
- package/dist/index.d.ts +66 -2
- package/dist/index.js +157 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
isAbortError: () => isAbortError,
|
|
27
27
|
isBillingError: () => isBillingError,
|
|
28
28
|
isContextOverflow: () => isContextOverflow,
|
|
29
|
+
isLocalBackendUrl: () => isLocalBackendUrl,
|
|
29
30
|
isUsageLimitError: () => isUsageLimitError,
|
|
30
31
|
setStreamDiagnostic: () => setStreamDiagnostic
|
|
31
32
|
});
|
|
@@ -37,7 +38,32 @@ var import_ai2 = require("@prestyj/ai");
|
|
|
37
38
|
// src/agent-loop.ts
|
|
38
39
|
var import_zod = require("zod");
|
|
39
40
|
var import_ai = require("@prestyj/ai");
|
|
41
|
+
|
|
42
|
+
// src/local-backend.ts
|
|
43
|
+
function isLocalBackendUrl(baseUrl) {
|
|
44
|
+
if (!baseUrl) return false;
|
|
45
|
+
let host;
|
|
46
|
+
try {
|
|
47
|
+
host = new URL(baseUrl).hostname.toLowerCase();
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (host === "[::1]" || host === "::1") return true;
|
|
52
|
+
if (host === "localhost" || host.endsWith(".localhost")) return true;
|
|
53
|
+
if (host === "0.0.0.0") return true;
|
|
54
|
+
if (host.endsWith(".local")) return true;
|
|
55
|
+
const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
56
|
+
if (ipv4) {
|
|
57
|
+
const octets = ipv4.slice(1).map(Number);
|
|
58
|
+
if (octets.some((n) => n > 255)) return false;
|
|
59
|
+
return octets[0] === 127;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/agent-loop.ts
|
|
40
65
|
var DEFAULT_MAX_TURNS = 300;
|
|
66
|
+
var DEFAULT_TOOL_TIMEOUT_MS = 3e5;
|
|
41
67
|
var _diagFn = null;
|
|
42
68
|
function setStreamDiagnostic(fn) {
|
|
43
69
|
_diagFn = fn;
|
|
@@ -240,6 +266,9 @@ function abortablePromise(promise, signal) {
|
|
|
240
266
|
promise.then(resolveOnce, rejectOnce);
|
|
241
267
|
});
|
|
242
268
|
}
|
|
269
|
+
function turnBudgetContinuationPrompt() {
|
|
270
|
+
return "[You reached the turn limit for this segment but the work is not finished, so you have been granted more turns. Before continuing, state in one or two sentences what is already done and what remains, then keep going from there \u2014 do not restart work that is already complete.]";
|
|
271
|
+
}
|
|
243
272
|
function abortableSleep(ms, signal) {
|
|
244
273
|
if (signal?.aborted) return Promise.reject(createAbortError());
|
|
245
274
|
return new Promise((resolve, reject) => {
|
|
@@ -261,6 +290,9 @@ function closeIterator(iterator) {
|
|
|
261
290
|
}
|
|
262
291
|
async function* agentLoop(messages, options) {
|
|
263
292
|
const maxTurns = options.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
293
|
+
let effectiveMaxTurns = maxTurns;
|
|
294
|
+
const maxTurnExtensions = Math.max(0, options.maxTurnExtensions ?? 2);
|
|
295
|
+
let turnExtensions = 0;
|
|
264
296
|
const maxContinuations = options.maxContinuations ?? 5;
|
|
265
297
|
let toolMap = new Map((options.tools ?? []).map((t) => [t.name, t]));
|
|
266
298
|
const totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
@@ -303,16 +335,17 @@ async function* agentLoop(messages, options) {
|
|
|
303
335
|
const STREAM_THINKING_IDLE_TIMEOUT_MS = 3e5;
|
|
304
336
|
const STREAM_THINKING_HARD_TIMEOUT_MS = 6e5;
|
|
305
337
|
const NON_STREAMING_HARD_TIMEOUT_MS = 3e5;
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
const
|
|
338
|
+
const usesSilentReasoningBudget = options.provider === "sakana" || options.provider === "openai" && options.thinking != null;
|
|
339
|
+
const localBackend = isLocalBackendUrl(options.baseUrl);
|
|
340
|
+
const firstEventTimeoutMs = localBackend ? Number.POSITIVE_INFINITY : usesSilentReasoningBudget ? STREAM_THINKING_IDLE_TIMEOUT_MS : STREAM_FIRST_EVENT_TIMEOUT_MS;
|
|
341
|
+
const initialHardTimeoutMs = localBackend || usesSilentReasoningBudget ? STREAM_THINKING_HARD_TIMEOUT_MS : STREAM_HARD_TIMEOUT_MS;
|
|
309
342
|
const MAX_TOOLCALL_DELTA_CHARS = 1e6;
|
|
310
|
-
const
|
|
343
|
+
const MAX_TOOLCALL_NO_PROGRESS_EVENTS = 2e4;
|
|
311
344
|
let logicalTurnStartedAt = 0;
|
|
312
345
|
let firstProviderEventAt;
|
|
313
346
|
let providerDurationMs = 0;
|
|
314
347
|
try {
|
|
315
|
-
while (turn <
|
|
348
|
+
while (turn < effectiveMaxTurns) {
|
|
316
349
|
options.signal?.throwIfAborted();
|
|
317
350
|
turn++;
|
|
318
351
|
if (logicalTurnStartedAt === 0) logicalTurnStartedAt = Date.now();
|
|
@@ -333,7 +366,11 @@ async function* agentLoop(messages, options) {
|
|
|
333
366
|
messages: messages.length,
|
|
334
367
|
chars: msgChars,
|
|
335
368
|
provider: options.provider,
|
|
336
|
-
model: options.model
|
|
369
|
+
model: options.model,
|
|
370
|
+
thinking: options.thinking ?? "off",
|
|
371
|
+
firstEventTimeoutMs,
|
|
372
|
+
initialHardTimeoutMs,
|
|
373
|
+
localBackend
|
|
337
374
|
});
|
|
338
375
|
}
|
|
339
376
|
if (firstTurn && options.getSteeringMessages) {
|
|
@@ -379,6 +416,7 @@ async function* agentLoop(messages, options) {
|
|
|
379
416
|
let lastEventType = "";
|
|
380
417
|
let toolcallDeltaChars = 0;
|
|
381
418
|
let toolcallDeltaCount = 0;
|
|
419
|
+
let toolcallNoProgressCount = 0;
|
|
382
420
|
let runawayDetected = null;
|
|
383
421
|
let attemptText = "";
|
|
384
422
|
let lastYieldEndTime = Date.now();
|
|
@@ -391,6 +429,7 @@ async function* agentLoop(messages, options) {
|
|
|
391
429
|
if (useNonStreamingFallback) return;
|
|
392
430
|
if (idleTimer) clearTimeout(idleTimer);
|
|
393
431
|
const timeoutMs = hasReceivedEvent ? STREAM_IDLE_TIMEOUT_MS : hasReceivedThinking ? STREAM_THINKING_IDLE_TIMEOUT_MS : firstEventTimeoutMs;
|
|
432
|
+
if (!Number.isFinite(timeoutMs)) return;
|
|
394
433
|
idleTimer = setTimeout(() => {
|
|
395
434
|
diag("idle_timeout_fired", {
|
|
396
435
|
events: streamEventCount,
|
|
@@ -532,11 +571,13 @@ async function* agentLoop(messages, options) {
|
|
|
532
571
|
const chunkChars = event.argsJson?.length ?? 0;
|
|
533
572
|
toolcallDeltaChars += chunkChars;
|
|
534
573
|
toolcallDeltaCount++;
|
|
535
|
-
|
|
574
|
+
toolcallNoProgressCount = chunkChars > 0 ? 0 : toolcallNoProgressCount + 1;
|
|
575
|
+
if (!runawayDetected && (toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS || toolcallNoProgressCount > MAX_TOOLCALL_NO_PROGRESS_EVENTS)) {
|
|
536
576
|
runawayDetected = {
|
|
537
577
|
kind: toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS ? "chars" : "events",
|
|
538
578
|
chars: toolcallDeltaChars,
|
|
539
|
-
events: toolcallDeltaCount
|
|
579
|
+
events: toolcallDeltaCount,
|
|
580
|
+
noProgressEvents: toolcallNoProgressCount
|
|
540
581
|
};
|
|
541
582
|
diag("runaway_toolcall_detected", {
|
|
542
583
|
...runawayDetected,
|
|
@@ -718,11 +759,15 @@ async function* agentLoop(messages, options) {
|
|
|
718
759
|
turn--;
|
|
719
760
|
continue;
|
|
720
761
|
}
|
|
721
|
-
const detail = runawayDetected.kind === "chars" ? `${(runawayDetected.chars / 1024).toFixed(0)} KB of tool-call arguments` : `${runawayDetected.
|
|
762
|
+
const detail = runawayDetected.kind === "chars" ? `${(runawayDetected.chars / 1024).toFixed(0)} KB of tool-call arguments` : `${runawayDetected.noProgressEvents} consecutive tool-call delta events without argument progress`;
|
|
722
763
|
yield {
|
|
723
764
|
type: "error",
|
|
724
|
-
error: new
|
|
725
|
-
`The model repeatedly failed to close a tool call after ${MAX_RUNAWAY_TOOLCALL_RETRIES} automatic retries (${detail}).
|
|
765
|
+
error: new import_ai.EZCoderAIError(
|
|
766
|
+
`The model repeatedly failed to close a tool call after ${MAX_RUNAWAY_TOOLCALL_RETRIES} automatic retries (${detail}). Your conversation is preserved.`,
|
|
767
|
+
{
|
|
768
|
+
source: "provider",
|
|
769
|
+
hint: "Retry once. If it keeps happening, switch models and continue the preserved conversation."
|
|
770
|
+
}
|
|
726
771
|
)
|
|
727
772
|
};
|
|
728
773
|
break;
|
|
@@ -749,7 +794,15 @@ async function* agentLoop(messages, options) {
|
|
|
749
794
|
role: "assistant",
|
|
750
795
|
content: [{ type: "text", text: attemptText }]
|
|
751
796
|
});
|
|
752
|
-
messages.push({
|
|
797
|
+
messages.push({
|
|
798
|
+
role: "user",
|
|
799
|
+
content: PARTIAL_CONTINUATION_PROMPT,
|
|
800
|
+
provenance: {
|
|
801
|
+
source: "runtime",
|
|
802
|
+
kind: "continuation",
|
|
803
|
+
visibility: "hidden"
|
|
804
|
+
}
|
|
805
|
+
});
|
|
753
806
|
preservedChars = attemptText.length;
|
|
754
807
|
}
|
|
755
808
|
diag("retry", {
|
|
@@ -775,15 +828,29 @@ async function* agentLoop(messages, options) {
|
|
|
775
828
|
continue;
|
|
776
829
|
}
|
|
777
830
|
if (transportFailure) {
|
|
831
|
+
const cause = malformed ? "malformed_stream" : socketDrop ? "socket_drop" : "stream_stall";
|
|
778
832
|
diag("stall_exhausted", {
|
|
779
833
|
stallRetries: MAX_STALL_RETRIES,
|
|
780
834
|
provider: options.provider,
|
|
781
|
-
model: options.model
|
|
835
|
+
model: options.model,
|
|
836
|
+
cause,
|
|
837
|
+
nonStreaming: useNonStreamingFallback,
|
|
838
|
+
events: streamEventCount,
|
|
839
|
+
eventTypes: eventTypeCounts,
|
|
840
|
+
lastEventType,
|
|
841
|
+
sinceLastEventMs: Date.now() - lastEventTime,
|
|
842
|
+
attemptDurationMs: Date.now() - streamCallStart,
|
|
843
|
+
maxConsumerLagMs
|
|
782
844
|
});
|
|
783
845
|
yield {
|
|
784
846
|
type: "error",
|
|
785
|
-
error: new
|
|
786
|
-
`The API provider
|
|
847
|
+
error: new import_ai.EZCoderAIError(
|
|
848
|
+
`The connection to the API provider stopped responding after ${MAX_STALL_RETRIES} automatic retries. Your conversation is preserved.`,
|
|
849
|
+
{
|
|
850
|
+
source: "network",
|
|
851
|
+
hint: "Retry once. If it keeps happening on this device, disable any VPN or proxy and allow EZ Coder through firewall or antivirus web protection.",
|
|
852
|
+
cause: err
|
|
853
|
+
}
|
|
787
854
|
)
|
|
788
855
|
};
|
|
789
856
|
break;
|
|
@@ -854,6 +921,9 @@ async function* agentLoop(messages, options) {
|
|
|
854
921
|
useNonStreamingFallback = false;
|
|
855
922
|
totalUsage.inputTokens += response.usage.inputTokens;
|
|
856
923
|
totalUsage.outputTokens += response.usage.outputTokens;
|
|
924
|
+
if (response.usage.reasoningTokens) {
|
|
925
|
+
totalUsage.reasoningTokens = (totalUsage.reasoningTokens ?? 0) + response.usage.reasoningTokens;
|
|
926
|
+
}
|
|
857
927
|
if (response.usage.cacheRead) {
|
|
858
928
|
totalUsage.cacheRead = (totalUsage.cacheRead ?? 0) + response.usage.cacheRead;
|
|
859
929
|
}
|
|
@@ -905,7 +975,15 @@ async function* agentLoop(messages, options) {
|
|
|
905
975
|
model: options.model
|
|
906
976
|
});
|
|
907
977
|
yield { type: "truncated", reason: "max_tokens", continued: true };
|
|
908
|
-
messages.push({
|
|
978
|
+
messages.push({
|
|
979
|
+
role: "user",
|
|
980
|
+
content: MAX_TOKENS_CONTINUATION_PROMPT,
|
|
981
|
+
provenance: {
|
|
982
|
+
source: "runtime",
|
|
983
|
+
kind: "continuation",
|
|
984
|
+
visibility: "hidden"
|
|
985
|
+
}
|
|
986
|
+
});
|
|
909
987
|
continue;
|
|
910
988
|
}
|
|
911
989
|
yield { type: "truncated", reason: "max_tokens", continued: false };
|
|
@@ -981,6 +1059,7 @@ async function* agentLoop(messages, options) {
|
|
|
981
1059
|
);
|
|
982
1060
|
const executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
|
|
983
1061
|
messages.push({ role: "tool", content: executionResult.toolResults });
|
|
1062
|
+
yield { type: "checkpoint", turn };
|
|
984
1063
|
const toolsAborted = executionResult.aborted;
|
|
985
1064
|
if (fatalToolArgumentError) {
|
|
986
1065
|
if (fatalToolArgumentRecoverable && !toolArgumentAutoContinueUsed) {
|
|
@@ -1012,8 +1091,51 @@ async function* agentLoop(messages, options) {
|
|
|
1012
1091
|
}
|
|
1013
1092
|
}
|
|
1014
1093
|
}
|
|
1015
|
-
if (turn >=
|
|
1016
|
-
|
|
1094
|
+
if (turn >= effectiveMaxTurns) {
|
|
1095
|
+
let extended = false;
|
|
1096
|
+
if (options.onTurnBudgetExhausted && turnExtensions < maxTurnExtensions) {
|
|
1097
|
+
const extension = turnExtensions + 1;
|
|
1098
|
+
let granted = false;
|
|
1099
|
+
try {
|
|
1100
|
+
granted = await options.onTurnBudgetExhausted({
|
|
1101
|
+
turn,
|
|
1102
|
+
maxTurns: effectiveMaxTurns,
|
|
1103
|
+
extension
|
|
1104
|
+
});
|
|
1105
|
+
} catch {
|
|
1106
|
+
granted = false;
|
|
1107
|
+
}
|
|
1108
|
+
if (granted) {
|
|
1109
|
+
turnExtensions = extension;
|
|
1110
|
+
effectiveMaxTurns += maxTurns;
|
|
1111
|
+
extended = true;
|
|
1112
|
+
diag("turn_budget_extended", {
|
|
1113
|
+
turn,
|
|
1114
|
+
grantedTurns: effectiveMaxTurns,
|
|
1115
|
+
extension,
|
|
1116
|
+
provider: options.provider,
|
|
1117
|
+
model: options.model
|
|
1118
|
+
});
|
|
1119
|
+
yield {
|
|
1120
|
+
type: "turn_budget_extended",
|
|
1121
|
+
turn,
|
|
1122
|
+
grantedTurns: effectiveMaxTurns,
|
|
1123
|
+
extension
|
|
1124
|
+
};
|
|
1125
|
+
messages.push({
|
|
1126
|
+
role: "user",
|
|
1127
|
+
content: turnBudgetContinuationPrompt(),
|
|
1128
|
+
provenance: {
|
|
1129
|
+
source: "runtime",
|
|
1130
|
+
kind: "continuation",
|
|
1131
|
+
visibility: "hidden"
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (!extended) {
|
|
1137
|
+
hitMaxTurns = true;
|
|
1138
|
+
}
|
|
1017
1139
|
}
|
|
1018
1140
|
}
|
|
1019
1141
|
} finally {
|
|
@@ -1029,14 +1151,15 @@ async function* agentLoop(messages, options) {
|
|
|
1029
1151
|
if (hitMaxTurns) {
|
|
1030
1152
|
diag("max_turns_reached", {
|
|
1031
1153
|
turn,
|
|
1032
|
-
maxTurns,
|
|
1154
|
+
maxTurns: effectiveMaxTurns,
|
|
1155
|
+
extensions: turnExtensions,
|
|
1033
1156
|
provider: options.provider,
|
|
1034
1157
|
model: options.model
|
|
1035
1158
|
});
|
|
1036
1159
|
yield {
|
|
1037
1160
|
type: "max_turns",
|
|
1038
1161
|
totalTurns: turn,
|
|
1039
|
-
maxTurns
|
|
1162
|
+
maxTurns: effectiveMaxTurns
|
|
1040
1163
|
};
|
|
1041
1164
|
}
|
|
1042
1165
|
yield {
|
|
@@ -1072,7 +1195,7 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
|
|
|
1072
1195
|
try {
|
|
1073
1196
|
const parsed = tool.parameters.parse(toolCall.args);
|
|
1074
1197
|
const callerSignal = options.signal;
|
|
1075
|
-
const toolTimeout = AbortSignal.timeout(
|
|
1198
|
+
const toolTimeout = AbortSignal.timeout(tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS);
|
|
1076
1199
|
const ctx = {
|
|
1077
1200
|
signal: callerSignal ? AbortSignal.any([callerSignal, toolTimeout]) : toolTimeout,
|
|
1078
1201
|
toolCallId: toolCall.id,
|
|
@@ -1285,9 +1408,9 @@ function capToolResults(toolResults, maxToolResultChars) {
|
|
|
1285
1408
|
const originalChars = toolResult.content.length;
|
|
1286
1409
|
const headChars = Math.floor(max * 0.7);
|
|
1287
1410
|
const tailChars = max - headChars;
|
|
1288
|
-
const head = toolResult.content
|
|
1289
|
-
const tail = toolResult.content
|
|
1290
|
-
const omitted = originalChars -
|
|
1411
|
+
const head = (0, import_ai.sliceHead)(toolResult.content, headChars);
|
|
1412
|
+
const tail = (0, import_ai.sliceTail)(toolResult.content, tailChars);
|
|
1413
|
+
const omitted = originalChars - head.length - tail.length;
|
|
1291
1414
|
toolResult.content = head + `
|
|
1292
1415
|
|
|
1293
1416
|
[... ${omitted} characters omitted ...]
|
|
@@ -1322,11 +1445,11 @@ function capTurnToolResults(toolResults, maxTurnToolResultChars) {
|
|
|
1322
1445
|
const headChars = Math.floor(fairShare * 0.7);
|
|
1323
1446
|
const tailChars = fairShare - headChars;
|
|
1324
1447
|
const omitted = originalChars - fairShare;
|
|
1325
|
-
toolResult.content = toolResult.content
|
|
1448
|
+
toolResult.content = (0, import_ai.sliceHead)(toolResult.content, headChars) + `
|
|
1326
1449
|
|
|
1327
1450
|
[... ${omitted} characters trimmed: this turn's combined tool results exceeded the per-turn budget. Re-run this call alone with narrower filters or offset/limit if you need the omitted content ...]
|
|
1328
1451
|
|
|
1329
|
-
` + (
|
|
1452
|
+
` + (0, import_ai.sliceTail)(toolResult.content, tailChars);
|
|
1330
1453
|
toolResult.capped = {
|
|
1331
1454
|
originalChars: toolResult.capped?.originalChars ?? originalChars,
|
|
1332
1455
|
keptChars: toolResult.content.length,
|
|
@@ -1345,12 +1468,14 @@ function truncateToolResultText(text, maxChars) {
|
|
|
1345
1468
|
if (text.length <= maxChars) return text;
|
|
1346
1469
|
const tailChars = Math.min(Math.floor(maxChars * 0.3), 2e4);
|
|
1347
1470
|
const headChars = Math.max(maxChars - tailChars, 0);
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1471
|
+
const head = (0, import_ai.sliceHead)(text, headChars);
|
|
1472
|
+
const tail = (0, import_ai.sliceTail)(text, tailChars);
|
|
1473
|
+
const omitted = text.length - head.length - tail.length;
|
|
1474
|
+
return `${head}
|
|
1350
1475
|
|
|
1351
1476
|
[... ${omitted} characters omitted after context overflow ...]
|
|
1352
1477
|
|
|
1353
|
-
${
|
|
1478
|
+
${tail}`;
|
|
1354
1479
|
}
|
|
1355
1480
|
function truncateOversizedToolResults(messages, maxChars) {
|
|
1356
1481
|
if (maxChars <= 0) return false;
|
|
@@ -1603,6 +1728,7 @@ var Agent = class {
|
|
|
1603
1728
|
isAbortError,
|
|
1604
1729
|
isBillingError,
|
|
1605
1730
|
isContextOverflow,
|
|
1731
|
+
isLocalBackendUrl,
|
|
1606
1732
|
isUsageLimitError,
|
|
1607
1733
|
setStreamDiagnostic
|
|
1608
1734
|
});
|