@prestyj/agent 5.27.0 → 5.28.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.js CHANGED
@@ -433,11 +433,14 @@ async function* agentLoop(messages, options) {
433
433
  const OVERLOAD_BASE_DELAY_MS = 2e3;
434
434
  const OVERLOAD_MAX_DELAY_MS = 3e4;
435
435
  const STREAM_IDLE_TIMEOUT_MS = 9e4;
436
+ const STREAM_TOOL_INPUT_IDLE_TIMEOUT_MS = 3e5;
436
437
  const STREAM_HARD_TIMEOUT_MS = 9e4;
437
438
  const STREAM_OUTPUT_HARD_TIMEOUT_MS = 3e5;
438
- const STREAM_THINKING_IDLE_TIMEOUT_MS = 3e5;
439
- const STREAM_THINKING_HARD_TIMEOUT_MS = 6e5;
439
+ const STREAM_THINKING_IDLE_TIMEOUT_MS = 6e5;
440
+ const STREAM_THINKING_HARD_TIMEOUT_MS = 9e5;
440
441
  const NON_STREAMING_HARD_TIMEOUT_MS = 3e5;
442
+ const nonStreamingHardTimeoutMs = options.thinking != null ? STREAM_THINKING_HARD_TIMEOUT_MS : NON_STREAMING_HARD_TIMEOUT_MS;
443
+ const VISIBLE_STALL_AFTER_MS = 6e4;
441
444
  const usesSilentReasoningBudget = options.provider === "sakana" || options.provider === "openai" && options.thinking != null;
442
445
  const localBackend = isLocalBackendUrl(options.baseUrl);
443
446
  const baseFirstEventTimeoutMs = localBackend ? Number.POSITIVE_INFINITY : usesSilentReasoningBudget ? STREAM_THINKING_IDLE_TIMEOUT_MS : STREAM_FIRST_EVENT_TIMEOUT_MS;
@@ -539,10 +542,11 @@ async function* agentLoop(messages, options) {
539
542
  options.signal?.addEventListener("abort", forwardAbort, { once: true });
540
543
  let hasReceivedEvent = false;
541
544
  let hasReceivedThinking = false;
545
+ let toolInputOpen = false;
542
546
  const resetIdleTimer = () => {
543
547
  if (useNonStreamingFallback) return;
544
548
  if (idleTimer) clearTimeout(idleTimer);
545
- const timeoutMs = hasReceivedEvent ? STREAM_IDLE_TIMEOUT_MS : hasReceivedThinking ? STREAM_THINKING_IDLE_TIMEOUT_MS : firstEventTimeoutMs;
549
+ const timeoutMs = hasReceivedEvent ? toolInputOpen ? STREAM_TOOL_INPUT_IDLE_TIMEOUT_MS : STREAM_IDLE_TIMEOUT_MS : hasReceivedThinking ? STREAM_THINKING_IDLE_TIMEOUT_MS : firstEventTimeoutMs;
546
550
  if (!Number.isFinite(timeoutMs)) return;
547
551
  idleTimer = setTimeout(() => {
548
552
  diag("idle_timeout_fired", {
@@ -550,14 +554,14 @@ async function* agentLoop(messages, options) {
550
554
  sinceLastEventMs: Date.now() - lastEventTime,
551
555
  lastEventType,
552
556
  maxConsumerLagMs,
553
- phase: hasReceivedEvent ? "mid_stream" : hasReceivedThinking ? "post_thinking" : "first_event",
557
+ phase: hasReceivedEvent ? toolInputOpen ? "mid_tool_input" : "mid_stream" : hasReceivedThinking ? "post_thinking" : "first_event",
554
558
  eventTypes: eventTypeCounts
555
559
  });
556
560
  idleTimedOut = true;
557
561
  streamController.abort();
558
562
  }, timeoutMs);
559
563
  };
560
- let hardTimeoutMs = useNonStreamingFallback ? NON_STREAMING_HARD_TIMEOUT_MS : initialHardTimeoutMs;
564
+ let hardTimeoutMs = useNonStreamingFallback ? nonStreamingHardTimeoutMs : initialHardTimeoutMs;
561
565
  hardTimer = setTimeout(() => {
562
566
  diag("hard_timeout_fired", {
563
567
  events: typeof streamEventCount !== "undefined" ? streamEventCount : 0,
@@ -652,6 +656,8 @@ async function* agentLoop(messages, options) {
652
656
  if (firstProviderEventAt === void 0) firstProviderEventAt = pullTime;
653
657
  eventTypeCounts[event.type] = (eventTypeCounts[event.type] ?? 0) + 1;
654
658
  lastEventType = event.type;
659
+ if (event.type === "toolcall_delta") toolInputOpen = true;
660
+ else if (event.type === "toolcall_done") toolInputOpen = false;
655
661
  if ((event.type === "text_delta" || event.type === "server_toolcall" || event.type === "toolcall_delta") && !hasReceivedEvent) {
656
662
  hasReceivedEvent = true;
657
663
  if (hardTimer && hardTimeoutMs < STREAM_OUTPUT_HARD_TIMEOUT_MS) {
@@ -986,7 +992,7 @@ async function* agentLoop(messages, options) {
986
992
  attempt: stallRetries,
987
993
  maxAttempts: MAX_STALL_RETRIES,
988
994
  delayMs,
989
- silent: stallRetries <= 2,
995
+ silent: stallRetries <= 2 && Date.now() - streamCallStart < VISIBLE_STALL_AFTER_MS,
990
996
  ...preservedChars > 0 ? { preservedChars } : {}
991
997
  };
992
998
  await abortableSleep(delayMs, options.signal);
@@ -1249,7 +1255,8 @@ async function* agentLoop(messages, options) {
1249
1255
  maxTurnToolResultChars: options.maxTurnToolResultChars,
1250
1256
  toolMap,
1251
1257
  invalidToolArgumentCounts,
1252
- markFatalToolArgumentError
1258
+ markFatalToolArgumentError,
1259
+ seenToolCalls: /* @__PURE__ */ new Set()
1253
1260
  };
1254
1261
  const hasSequentialToolCall = toolCalls.some(
1255
1262
  (toolCall) => toolMap.get(toolCall.name)?.executionMode === "sequential"
@@ -1370,6 +1377,15 @@ async function* agentLoop(messages, options) {
1370
1377
  totalUsage: { ...totalUsage }
1371
1378
  };
1372
1379
  }
1380
+ function canonicalToolArgs(value) {
1381
+ if (Array.isArray(value)) return value.map(canonicalToolArgs);
1382
+ if (value !== null && typeof value === "object") {
1383
+ return Object.fromEntries(
1384
+ Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, canonicalToolArgs(v)])
1385
+ );
1386
+ }
1387
+ return value;
1388
+ }
1373
1389
  function pushToolEvent(eventStream, state, event) {
1374
1390
  if (!state.finalized) eventStream.push(event);
1375
1391
  }
@@ -1386,6 +1402,21 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1386
1402
  let isError = false;
1387
1403
  let invalidArgAttempt;
1388
1404
  const tool = options.toolMap.get(toolCall.name);
1405
+ if (tool) {
1406
+ const signature = JSON.stringify([toolCall.name, canonicalToolArgs(toolCall.args)]);
1407
+ if (options.seenToolCalls.has(signature)) {
1408
+ const content = "Tool call cancelled: an identical call already appeared in this response; this call was not executed.";
1409
+ pushEvent({
1410
+ type: "tool_call_end",
1411
+ toolCallId: toolCall.id,
1412
+ result: content,
1413
+ isError: true,
1414
+ durationMs: Date.now() - startTime
1415
+ });
1416
+ return { toolCallId: toolCall.id, content, isError: true };
1417
+ }
1418
+ options.seenToolCalls.add(signature);
1419
+ }
1389
1420
  if (!tool) {
1390
1421
  resultContent = `Unknown tool: ${toolCall.name}`;
1391
1422
  isError = true;
@@ -1502,6 +1533,11 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1502
1533
  for (const phase of phases) {
1503
1534
  if (options.signal?.aborted) break;
1504
1535
  if (phase.sequential) {
1536
+ const signature = JSON.stringify([
1537
+ phase.sequential.name,
1538
+ canonicalToolArgs(phase.sequential.args)
1539
+ ]);
1540
+ if (!options.seenToolCalls.has(signature)) options.seenToolCalls.clear();
1505
1541
  dispatchedIds.add(phase.sequential.id);
1506
1542
  const record = await executeSingleToolCall(
1507
1543
  phase.sequential,