@prestyj/agent 5.7.0 → 5.9.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
@@ -154,7 +154,7 @@ function classifyOverload(err) {
154
154
  if (statusCode === 529 || msg.includes("overloaded") || msg.includes("529")) {
155
155
  return "overloaded";
156
156
  }
157
- 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")) {
157
+ if (statusCode === 500 || statusCode === 502 || statusCode === 503 || statusCode === 504 || statusCode === 507 || 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") || msg.includes("exceeded request buffer limit while retrying upstream")) {
158
158
  return "provider_error";
159
159
  }
160
160
  if (isOpaqueProviderMessage(err.message)) {
@@ -215,7 +215,11 @@ function createAbortError() {
215
215
  }
216
216
  function abortablePromise(promise, signal) {
217
217
  if (!signal) return promise;
218
- if (signal.aborted) return Promise.reject(createAbortError());
218
+ if (signal.aborted) {
219
+ promise.catch(() => {
220
+ });
221
+ return Promise.reject(createAbortError());
222
+ }
219
223
  return new Promise((resolve, reject) => {
220
224
  let settled = false;
221
225
  const cleanup = () => signal.removeEventListener("abort", onAbort);
@@ -260,6 +264,8 @@ async function* agentLoop(messages, options) {
260
264
  const maxContinuations = options.maxContinuations ?? 5;
261
265
  let toolMap = new Map((options.tools ?? []).map((t) => [t.name, t]));
262
266
  const totalUsage = { inputTokens: 0, outputTokens: 0 };
267
+ let latestProviderUsage;
268
+ let usageAnchorIndex;
263
269
  let turn = 0;
264
270
  let hitMaxTurns = false;
265
271
  let firstTurn = true;
@@ -269,6 +275,7 @@ async function* agentLoop(messages, options) {
269
275
  let overloadRetries = 0;
270
276
  let emptyResponseRetries = 0;
271
277
  let stallRetries = 0;
278
+ let runawayToolcallRetries = 0;
272
279
  let overflowCompactionAttempts = 0;
273
280
  let toolResultTruncationAttempted = false;
274
281
  const invalidToolArgumentCounts = /* @__PURE__ */ new Map();
@@ -277,11 +284,16 @@ async function* agentLoop(messages, options) {
277
284
  const MAX_OVERLOAD_RETRIES = 10;
278
285
  const MAX_EMPTY_RESPONSE_RETRIES = 2;
279
286
  const MAX_STALL_RETRIES = 10;
287
+ const MAX_RUNAWAY_TOOLCALL_RETRIES = 2;
288
+ const RUNAWAY_TOOLCALL_RETRY_DELAY_MS = 1e3;
280
289
  const MAX_OVERFLOW_COMPACTIONS = 2;
281
290
  const STALL_RETRIES_BEFORE_NON_STREAMING = 2;
282
291
  const STALL_DELAY_MS = 1e3;
283
292
  const MIN_PARTIAL_PRESERVE_CHARS = 200;
284
293
  const PARTIAL_CONTINUATION_PROMPT = "[Your previous response was cut off by a connection failure. The text above is what was already delivered to the user. Continue exactly from where it stopped \u2014 do not repeat or restart it.]";
294
+ const MAX_OUTPUT_CONTINUATIONS = 2;
295
+ const MAX_TOKENS_CONTINUATION_PROMPT = "[Your previous response hit the output-token limit and was cut off. The text above is what was already delivered to the user. Continue exactly from where it stopped \u2014 do not repeat or restart it.]";
296
+ let maxTokensContinuations = 0;
285
297
  const OVERLOAD_BASE_DELAY_MS = 2e3;
286
298
  const OVERLOAD_MAX_DELAY_MS = 3e4;
287
299
  const STREAM_FIRST_EVENT_TIMEOUT_MS = 45e3;
@@ -336,7 +348,11 @@ async function* agentLoop(messages, options) {
336
348
  firstTurn = false;
337
349
  if (options.transformContext) {
338
350
  diag("transform_start");
339
- const transformed = await options.transformContext(messages);
351
+ const pendingMessages = usageAnchorIndex === void 0 ? [] : messages.slice(usageAnchorIndex + 1);
352
+ const transformed = await options.transformContext(messages, {
353
+ usage: latestProviderUsage,
354
+ pendingMessages
355
+ });
340
356
  if (transformed !== messages) {
341
357
  diag("transform_compacted", {
342
358
  before: messages.length,
@@ -344,6 +360,8 @@ async function* agentLoop(messages, options) {
344
360
  });
345
361
  messages.length = 0;
346
362
  messages.push(...transformed);
363
+ latestProviderUsage = void 0;
364
+ usageAnchorIndex = void 0;
347
365
  }
348
366
  diag("transform_end");
349
367
  }
@@ -604,10 +622,17 @@ async function* agentLoop(messages, options) {
604
622
  ...overflowDetails
605
623
  });
606
624
  try {
607
- const compacted = await options.transformContext(messages, { force: true });
625
+ const pendingMessages = usageAnchorIndex === void 0 ? [] : messages.slice(usageAnchorIndex + 1);
626
+ const compacted = await options.transformContext(messages, {
627
+ force: true,
628
+ usage: latestProviderUsage,
629
+ pendingMessages
630
+ });
608
631
  if (compacted !== messages && compacted.length < messages.length) {
609
632
  messages.length = 0;
610
633
  messages.push(...compacted);
634
+ latestProviderUsage = void 0;
635
+ usageAnchorIndex = void 0;
611
636
  diag("overflow_compact_success", {
612
637
  attempt: overflowCompactionAttempts,
613
638
  messages: messages.length,
@@ -671,11 +696,33 @@ async function* agentLoop(messages, options) {
671
696
  provider: options.provider,
672
697
  model: options.model
673
698
  });
699
+ if (runawayToolcallRetries < MAX_RUNAWAY_TOOLCALL_RETRIES) {
700
+ runawayToolcallRetries++;
701
+ const delayMs = RUNAWAY_TOOLCALL_RETRY_DELAY_MS * runawayToolcallRetries;
702
+ diag("retry", {
703
+ reason: "runaway_toolcall",
704
+ attempt: runawayToolcallRetries,
705
+ maxAttempts: MAX_RUNAWAY_TOOLCALL_RETRIES,
706
+ delayMs,
707
+ ...runawayDetected
708
+ });
709
+ yield {
710
+ type: "retry",
711
+ reason: "runaway_toolcall",
712
+ attempt: runawayToolcallRetries,
713
+ maxAttempts: MAX_RUNAWAY_TOOLCALL_RETRIES,
714
+ delayMs,
715
+ silent: true
716
+ };
717
+ await abortableSleep(delayMs, options.signal);
718
+ turn--;
719
+ continue;
720
+ }
674
721
  const detail = runawayDetected.kind === "chars" ? `${(runawayDetected.chars / 1024).toFixed(0)} KB of tool-call arguments` : `${runawayDetected.events} tool-call delta events`;
675
722
  yield {
676
723
  type: "error",
677
724
  error: new Error(
678
- `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.`
725
+ `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.`
679
726
  )
680
727
  };
681
728
  break;
@@ -776,6 +823,7 @@ async function* agentLoop(messages, options) {
776
823
  }
777
824
  overloadRetries = 0;
778
825
  stallRetries = 0;
826
+ runawayToolcallRetries = 0;
779
827
  const contentArr = Array.isArray(response.message.content) ? response.message.content : null;
780
828
  const hasActionableContent = response.message.content !== "" && contentArr !== null && contentArr.some(
781
829
  (p) => p.type === "text" || p.type === "tool_call" || p.type === "server_tool_call"
@@ -813,6 +861,8 @@ async function* agentLoop(messages, options) {
813
861
  totalUsage.cacheWrite = (totalUsage.cacheWrite ?? 0) + response.usage.cacheWrite;
814
862
  }
815
863
  messages.push(response.message);
864
+ latestProviderUsage = response.usage;
865
+ usageAnchorIndex = messages.length - 1;
816
866
  const completedAt = Date.now();
817
867
  const outputTokensPerSecond = providerDurationMs > 0 && response.usage.outputTokens > 0 ? response.usage.outputTokens / (providerDurationMs / 1e3) : void 0;
818
868
  const timing = {
@@ -845,6 +895,27 @@ async function* agentLoop(messages, options) {
845
895
  consecutivePauses = 0;
846
896
  const allToolCalls = extractToolCalls(response.message.content);
847
897
  if (response.stopReason !== "tool_use" && allToolCalls.length === 0) {
898
+ if (response.stopReason === "max_tokens") {
899
+ if (maxTokensContinuations < MAX_OUTPUT_CONTINUATIONS) {
900
+ maxTokensContinuations++;
901
+ diag("max_tokens_continuation", {
902
+ attempt: maxTokensContinuations,
903
+ maxAttempts: MAX_OUTPUT_CONTINUATIONS,
904
+ provider: options.provider,
905
+ model: options.model
906
+ });
907
+ yield { type: "truncated", reason: "max_tokens", continued: true };
908
+ messages.push({ role: "user", content: MAX_TOKENS_CONTINUATION_PROMPT });
909
+ continue;
910
+ }
911
+ yield { type: "truncated", reason: "max_tokens", continued: false };
912
+ } else if (response.stopReason === "refusal" || response.stopReason === "error") {
913
+ yield {
914
+ type: "truncated",
915
+ reason: response.stopReason === "refusal" ? "refusal" : "provider_error",
916
+ continued: false
917
+ };
918
+ }
848
919
  if (options.getSteeringMessages) {
849
920
  const steering = await options.getSteeringMessages();
850
921
  if (steering && steering.length > 0) {
@@ -900,6 +971,7 @@ async function* agentLoop(messages, options) {
900
971
  const executionOptions = {
901
972
  signal: options.signal,
902
973
  maxToolResultChars: options.maxToolResultChars,
974
+ maxTurnToolResultChars: options.maxTurnToolResultChars,
903
975
  toolMap,
904
976
  invalidToolArgumentCounts,
905
977
  markFatalToolArgumentError
@@ -1139,6 +1211,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1139
1211
  }
1140
1212
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById);
1141
1213
  capToolResults(toolResults, options.maxToolResultChars);
1214
+ capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1142
1215
  return { toolResults, aborted };
1143
1216
  }
1144
1217
  async function* executeToolCallsParallel(toolCalls, initialToolResults, options) {
@@ -1178,6 +1251,7 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1178
1251
  }
1179
1252
  const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById);
1180
1253
  capToolResults(toolResults, options.maxToolResultChars);
1254
+ capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1181
1255
  return { toolResults, aborted };
1182
1256
  }
1183
1257
  function buildToolResults(initialToolResults, toolCalls, resultsById) {
@@ -1208,16 +1282,56 @@ function capToolResults(toolResults, maxToolResultChars) {
1208
1282
  const max = Math.min(maxToolResultChars, hardMax);
1209
1283
  for (const toolResult of toolResults) {
1210
1284
  if (typeof toolResult.content !== "string" || toolResult.content.length <= max) continue;
1285
+ const originalChars = toolResult.content.length;
1211
1286
  const headChars = Math.floor(max * 0.7);
1212
1287
  const tailChars = max - headChars;
1213
1288
  const head = toolResult.content.slice(0, headChars);
1214
1289
  const tail = toolResult.content.slice(-tailChars);
1215
- const omitted = toolResult.content.length - headChars - tailChars;
1290
+ const omitted = originalChars - headChars - tailChars;
1216
1291
  toolResult.content = head + `
1217
1292
 
1218
1293
  [... ${omitted} characters omitted ...]
1219
1294
 
1220
1295
  ` + tail;
1296
+ toolResult.capped = {
1297
+ originalChars,
1298
+ keptChars: toolResult.content.length,
1299
+ scope: "per-result"
1300
+ };
1301
+ }
1302
+ }
1303
+ function capTurnToolResults(toolResults, maxTurnToolResultChars) {
1304
+ if (!maxTurnToolResultChars) return;
1305
+ const textResults = toolResults.filter(
1306
+ (toolResult) => typeof toolResult.content === "string"
1307
+ );
1308
+ const total = textResults.reduce((sum, toolResult) => sum + toolResult.content.length, 0);
1309
+ if (total <= maxTurnToolResultChars) return;
1310
+ const bySize = [...textResults].sort((a, b) => a.content.length - b.content.length);
1311
+ let remaining = maxTurnToolResultChars;
1312
+ let left = bySize.length;
1313
+ for (const toolResult of bySize) {
1314
+ const fairShare = Math.floor(remaining / left);
1315
+ left--;
1316
+ if (toolResult.content.length <= fairShare) {
1317
+ remaining -= toolResult.content.length;
1318
+ continue;
1319
+ }
1320
+ remaining -= fairShare;
1321
+ const originalChars = toolResult.content.length;
1322
+ const headChars = Math.floor(fairShare * 0.7);
1323
+ const tailChars = fairShare - headChars;
1324
+ const omitted = originalChars - fairShare;
1325
+ toolResult.content = toolResult.content.slice(0, headChars) + `
1326
+
1327
+ [... ${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
+
1329
+ ` + (tailChars > 0 ? toolResult.content.slice(-tailChars) : "");
1330
+ toolResult.capped = {
1331
+ originalChars: toolResult.capped?.originalChars ?? originalChars,
1332
+ keptChars: toolResult.content.length,
1333
+ scope: "per-turn"
1334
+ };
1221
1335
  }
1222
1336
  }
1223
1337
  function normalizeToolResult(raw) {