@prestyj/agent 5.10.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 CHANGED
@@ -63,6 +63,7 @@ function isLocalBackendUrl(baseUrl) {
63
63
 
64
64
  // src/agent-loop.ts
65
65
  var DEFAULT_MAX_TURNS = 300;
66
+ var DEFAULT_TOOL_TIMEOUT_MS = 3e5;
66
67
  var _diagFn = null;
67
68
  function setStreamDiagnostic(fn) {
68
69
  _diagFn = fn;
@@ -265,6 +266,9 @@ function abortablePromise(promise, signal) {
265
266
  promise.then(resolveOnce, rejectOnce);
266
267
  });
267
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
+ }
268
272
  function abortableSleep(ms, signal) {
269
273
  if (signal?.aborted) return Promise.reject(createAbortError());
270
274
  return new Promise((resolve, reject) => {
@@ -286,6 +290,9 @@ function closeIterator(iterator) {
286
290
  }
287
291
  async function* agentLoop(messages, options) {
288
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;
289
296
  const maxContinuations = options.maxContinuations ?? 5;
290
297
  let toolMap = new Map((options.tools ?? []).map((t) => [t.name, t]));
291
298
  const totalUsage = { inputTokens: 0, outputTokens: 0 };
@@ -333,12 +340,12 @@ async function* agentLoop(messages, options) {
333
340
  const firstEventTimeoutMs = localBackend ? Number.POSITIVE_INFINITY : usesSilentReasoningBudget ? STREAM_THINKING_IDLE_TIMEOUT_MS : STREAM_FIRST_EVENT_TIMEOUT_MS;
334
341
  const initialHardTimeoutMs = localBackend || usesSilentReasoningBudget ? STREAM_THINKING_HARD_TIMEOUT_MS : STREAM_HARD_TIMEOUT_MS;
335
342
  const MAX_TOOLCALL_DELTA_CHARS = 1e6;
336
- const MAX_TOOLCALL_DELTA_EVENTS = 2e4;
343
+ const MAX_TOOLCALL_NO_PROGRESS_EVENTS = 2e4;
337
344
  let logicalTurnStartedAt = 0;
338
345
  let firstProviderEventAt;
339
346
  let providerDurationMs = 0;
340
347
  try {
341
- while (turn < maxTurns) {
348
+ while (turn < effectiveMaxTurns) {
342
349
  options.signal?.throwIfAborted();
343
350
  turn++;
344
351
  if (logicalTurnStartedAt === 0) logicalTurnStartedAt = Date.now();
@@ -409,6 +416,7 @@ async function* agentLoop(messages, options) {
409
416
  let lastEventType = "";
410
417
  let toolcallDeltaChars = 0;
411
418
  let toolcallDeltaCount = 0;
419
+ let toolcallNoProgressCount = 0;
412
420
  let runawayDetected = null;
413
421
  let attemptText = "";
414
422
  let lastYieldEndTime = Date.now();
@@ -563,11 +571,13 @@ async function* agentLoop(messages, options) {
563
571
  const chunkChars = event.argsJson?.length ?? 0;
564
572
  toolcallDeltaChars += chunkChars;
565
573
  toolcallDeltaCount++;
566
- if (!runawayDetected && (toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS || toolcallDeltaCount > MAX_TOOLCALL_DELTA_EVENTS)) {
574
+ toolcallNoProgressCount = chunkChars > 0 ? 0 : toolcallNoProgressCount + 1;
575
+ if (!runawayDetected && (toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS || toolcallNoProgressCount > MAX_TOOLCALL_NO_PROGRESS_EVENTS)) {
567
576
  runawayDetected = {
568
577
  kind: toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS ? "chars" : "events",
569
578
  chars: toolcallDeltaChars,
570
- events: toolcallDeltaCount
579
+ events: toolcallDeltaCount,
580
+ noProgressEvents: toolcallNoProgressCount
571
581
  };
572
582
  diag("runaway_toolcall_detected", {
573
583
  ...runawayDetected,
@@ -749,11 +759,15 @@ async function* agentLoop(messages, options) {
749
759
  turn--;
750
760
  continue;
751
761
  }
752
- const detail = runawayDetected.kind === "chars" ? `${(runawayDetected.chars / 1024).toFixed(0)} KB of tool-call arguments` : `${runawayDetected.events} tool-call delta events`;
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`;
753
763
  yield {
754
764
  type: "error",
755
- error: new Error(
756
- `The model repeatedly failed to close a tool call after ${MAX_RUNAWAY_TOOLCALL_RETRIES} automatic retries (${detail}). Switch models and retry; your conversation is preserved.`
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
+ }
757
771
  )
758
772
  };
759
773
  break;
@@ -780,7 +794,15 @@ async function* agentLoop(messages, options) {
780
794
  role: "assistant",
781
795
  content: [{ type: "text", text: attemptText }]
782
796
  });
783
- messages.push({ role: "user", content: PARTIAL_CONTINUATION_PROMPT });
797
+ messages.push({
798
+ role: "user",
799
+ content: PARTIAL_CONTINUATION_PROMPT,
800
+ provenance: {
801
+ source: "runtime",
802
+ kind: "continuation",
803
+ visibility: "hidden"
804
+ }
805
+ });
784
806
  preservedChars = attemptText.length;
785
807
  }
786
808
  diag("retry", {
@@ -899,6 +921,9 @@ async function* agentLoop(messages, options) {
899
921
  useNonStreamingFallback = false;
900
922
  totalUsage.inputTokens += response.usage.inputTokens;
901
923
  totalUsage.outputTokens += response.usage.outputTokens;
924
+ if (response.usage.reasoningTokens) {
925
+ totalUsage.reasoningTokens = (totalUsage.reasoningTokens ?? 0) + response.usage.reasoningTokens;
926
+ }
902
927
  if (response.usage.cacheRead) {
903
928
  totalUsage.cacheRead = (totalUsage.cacheRead ?? 0) + response.usage.cacheRead;
904
929
  }
@@ -950,7 +975,15 @@ async function* agentLoop(messages, options) {
950
975
  model: options.model
951
976
  });
952
977
  yield { type: "truncated", reason: "max_tokens", continued: true };
953
- messages.push({ role: "user", content: MAX_TOKENS_CONTINUATION_PROMPT });
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
+ });
954
987
  continue;
955
988
  }
956
989
  yield { type: "truncated", reason: "max_tokens", continued: false };
@@ -1026,6 +1059,7 @@ async function* agentLoop(messages, options) {
1026
1059
  );
1027
1060
  const executionResult = hasSequentialToolCall ? yield* executeToolCallsMixed(toolCalls, toolResults, executionOptions) : yield* executeToolCallsParallel(toolCalls, toolResults, executionOptions);
1028
1061
  messages.push({ role: "tool", content: executionResult.toolResults });
1062
+ yield { type: "checkpoint", turn };
1029
1063
  const toolsAborted = executionResult.aborted;
1030
1064
  if (fatalToolArgumentError) {
1031
1065
  if (fatalToolArgumentRecoverable && !toolArgumentAutoContinueUsed) {
@@ -1057,8 +1091,51 @@ async function* agentLoop(messages, options) {
1057
1091
  }
1058
1092
  }
1059
1093
  }
1060
- if (turn >= maxTurns) {
1061
- hitMaxTurns = true;
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
+ }
1062
1139
  }
1063
1140
  }
1064
1141
  } finally {
@@ -1074,14 +1151,15 @@ async function* agentLoop(messages, options) {
1074
1151
  if (hitMaxTurns) {
1075
1152
  diag("max_turns_reached", {
1076
1153
  turn,
1077
- maxTurns,
1154
+ maxTurns: effectiveMaxTurns,
1155
+ extensions: turnExtensions,
1078
1156
  provider: options.provider,
1079
1157
  model: options.model
1080
1158
  });
1081
1159
  yield {
1082
1160
  type: "max_turns",
1083
1161
  totalTurns: turn,
1084
- maxTurns
1162
+ maxTurns: effectiveMaxTurns
1085
1163
  };
1086
1164
  }
1087
1165
  yield {
@@ -1117,7 +1195,7 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1117
1195
  try {
1118
1196
  const parsed = tool.parameters.parse(toolCall.args);
1119
1197
  const callerSignal = options.signal;
1120
- const toolTimeout = AbortSignal.timeout(3e5);
1198
+ const toolTimeout = AbortSignal.timeout(tool.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS);
1121
1199
  const ctx = {
1122
1200
  signal: callerSignal ? AbortSignal.any([callerSignal, toolTimeout]) : toolTimeout,
1123
1201
  toolCallId: toolCall.id,
@@ -1330,9 +1408,9 @@ function capToolResults(toolResults, maxToolResultChars) {
1330
1408
  const originalChars = toolResult.content.length;
1331
1409
  const headChars = Math.floor(max * 0.7);
1332
1410
  const tailChars = max - headChars;
1333
- const head = toolResult.content.slice(0, headChars);
1334
- const tail = toolResult.content.slice(-tailChars);
1335
- const omitted = originalChars - headChars - tailChars;
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;
1336
1414
  toolResult.content = head + `
1337
1415
 
1338
1416
  [... ${omitted} characters omitted ...]
@@ -1367,11 +1445,11 @@ function capTurnToolResults(toolResults, maxTurnToolResultChars) {
1367
1445
  const headChars = Math.floor(fairShare * 0.7);
1368
1446
  const tailChars = fairShare - headChars;
1369
1447
  const omitted = originalChars - fairShare;
1370
- toolResult.content = toolResult.content.slice(0, headChars) + `
1448
+ toolResult.content = (0, import_ai.sliceHead)(toolResult.content, headChars) + `
1371
1449
 
1372
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 ...]
1373
1451
 
1374
- ` + (tailChars > 0 ? toolResult.content.slice(-tailChars) : "");
1452
+ ` + (0, import_ai.sliceTail)(toolResult.content, tailChars);
1375
1453
  toolResult.capped = {
1376
1454
  originalChars: toolResult.capped?.originalChars ?? originalChars,
1377
1455
  keptChars: toolResult.content.length,
@@ -1390,12 +1468,14 @@ function truncateToolResultText(text, maxChars) {
1390
1468
  if (text.length <= maxChars) return text;
1391
1469
  const tailChars = Math.min(Math.floor(maxChars * 0.3), 2e4);
1392
1470
  const headChars = Math.max(maxChars - tailChars, 0);
1393
- const omitted = text.length - headChars - tailChars;
1394
- return `${text.slice(0, headChars)}
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}
1395
1475
 
1396
1476
  [... ${omitted} characters omitted after context overflow ...]
1397
1477
 
1398
- ${text.slice(-tailChars)}`;
1478
+ ${tail}`;
1399
1479
  }
1400
1480
  function truncateOversizedToolResults(messages, maxChars) {
1401
1481
  if (maxChars <= 0) return false;