@prestyj/agent 5.13.0 → 5.16.1

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.d.cts CHANGED
@@ -401,6 +401,20 @@ declare function isBillingError(err: unknown): boolean;
401
401
  */
402
402
  declare function isUsageLimitError(err: unknown): boolean;
403
403
  declare function agentLoop(messages: Message[], options: AgentOptions): AsyncGenerator<AgentEvent, AgentResult>;
404
+ /**
405
+ * A tool call that never reached its tool. Nothing ran, so nothing changed.
406
+ */
407
+ declare function cancelledBeforeStartText(name: string): string;
408
+ /**
409
+ * A tool call that started running and was cut off before reporting back.
410
+ *
411
+ * The distinction from {@link cancelledBeforeStartText} is the whole point: a
412
+ * dispatched `git push`, deploy or MCP call may well have COMPLETED before the
413
+ * abort landed. Telling the model it was "interrupted" reads as "it did not
414
+ * happen", so the model repeats the side effect — or reports to the user that
415
+ * something never ran when it did.
416
+ */
417
+ declare function indeterminateOutcomeText(name: string): string;
404
418
 
405
419
  /**
406
420
  * A local inference server (llama.cpp, vLLM, Ollama, LM Studio) can spend
@@ -411,4 +425,4 @@ declare function agentLoop(messages: Message[], options: AgentOptions): AsyncGen
411
425
  */
412
426
  declare function isLocalBackendUrl(baseUrl?: string): boolean;
413
427
 
414
- export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, isAbortError, isBillingError, isContextOverflow, isLocalBackendUrl, isUsageLimitError, setStreamDiagnostic };
428
+ export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, cancelledBeforeStartText, indeterminateOutcomeText, isAbortError, isBillingError, isContextOverflow, isLocalBackendUrl, isUsageLimitError, setStreamDiagnostic };
package/dist/index.d.ts CHANGED
@@ -401,6 +401,20 @@ declare function isBillingError(err: unknown): boolean;
401
401
  */
402
402
  declare function isUsageLimitError(err: unknown): boolean;
403
403
  declare function agentLoop(messages: Message[], options: AgentOptions): AsyncGenerator<AgentEvent, AgentResult>;
404
+ /**
405
+ * A tool call that never reached its tool. Nothing ran, so nothing changed.
406
+ */
407
+ declare function cancelledBeforeStartText(name: string): string;
408
+ /**
409
+ * A tool call that started running and was cut off before reporting back.
410
+ *
411
+ * The distinction from {@link cancelledBeforeStartText} is the whole point: a
412
+ * dispatched `git push`, deploy or MCP call may well have COMPLETED before the
413
+ * abort landed. Telling the model it was "interrupted" reads as "it did not
414
+ * happen", so the model repeats the side effect — or reports to the user that
415
+ * something never ran when it did.
416
+ */
417
+ declare function indeterminateOutcomeText(name: string): string;
404
418
 
405
419
  /**
406
420
  * A local inference server (llama.cpp, vLLM, Ollama, LM Studio) can spend
@@ -411,4 +425,4 @@ declare function agentLoop(messages: Message[], options: AgentOptions): AsyncGen
411
425
  */
412
426
  declare function isLocalBackendUrl(baseUrl?: string): boolean;
413
427
 
414
- export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, isAbortError, isBillingError, isContextOverflow, isLocalBackendUrl, isUsageLimitError, setStreamDiagnostic };
428
+ export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, cancelledBeforeStartText, indeterminateOutcomeText, isAbortError, isBillingError, isContextOverflow, isLocalBackendUrl, isUsageLimitError, setStreamDiagnostic };
package/dist/index.js CHANGED
@@ -35,6 +35,60 @@ function isLocalBackendUrl(baseUrl) {
35
35
  return false;
36
36
  }
37
37
 
38
+ // src/output-ceiling.ts
39
+ var TTL_MS = 24 * 60 * 60 * 1e3;
40
+ var MIN_PLAUSIBLE_CEILING = 256;
41
+ var MAX_PLAUSIBLE_CEILING = 1e7;
42
+ var ceilings = /* @__PURE__ */ new Map();
43
+ function outputRouteKey(route) {
44
+ return `${route.provider}\0${route.baseUrl ?? "default"}\0${route.model}`;
45
+ }
46
+ function parseOutputTokenCeiling(err) {
47
+ if (!(err instanceof Error)) return null;
48
+ const msg = err.message;
49
+ if (!/max_tokens|max output tokens|output tokens|completion tokens/i.test(msg)) return null;
50
+ if (err.statusCode === 402) return null;
51
+ if (/credit|billing|payment|insufficient|balance|quota/i.test(msg)) return null;
52
+ const patterns = [
53
+ // Anthropic: "max_tokens: 100000 > 64000, which is the maximum allowed…"
54
+ /max_tokens:\s*\d+\s*>\s*(\d+)/i,
55
+ // OpenAI: "…this model supports at most 16384 completion tokens"
56
+ /(?:at most|maximum of|limit of)\s+([\d,_]+)\s*(?:output|completion)?\s*tokens/i,
57
+ // Generic: "max output tokens is 8192" / "max_tokens must be <= 4096"
58
+ /(?:max_tokens|max output tokens)\b[^\d]{0,30}?([\d,_]+)/i
59
+ ];
60
+ for (const pattern of patterns) {
61
+ const raw = msg.match(pattern)?.[1];
62
+ if (raw === void 0) continue;
63
+ const limit = Number(raw.replace(/[,_]/g, ""));
64
+ if (Number.isFinite(limit) && limit >= MIN_PLAUSIBLE_CEILING && limit <= MAX_PLAUSIBLE_CEILING) {
65
+ return limit;
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ function rememberOutputCeiling(key, limit) {
71
+ const known = outputTokenCeiling(key);
72
+ ceilings.set(key, {
73
+ limit: known === void 0 ? limit : Math.min(known, limit),
74
+ expiresAt: Date.now() + TTL_MS
75
+ });
76
+ }
77
+ function outputTokenCeiling(key) {
78
+ const entry = ceilings.get(key);
79
+ if (!entry) return void 0;
80
+ if (entry.expiresAt <= Date.now()) {
81
+ ceilings.delete(key);
82
+ return void 0;
83
+ }
84
+ return entry.limit;
85
+ }
86
+ function clampOutputTokens(key, requested) {
87
+ const ceiling = outputTokenCeiling(key);
88
+ if (ceiling === void 0) return requested;
89
+ return requested === void 0 ? ceiling : Math.min(requested, ceiling);
90
+ }
91
+
38
92
  // src/agent-loop.ts
39
93
  var DEFAULT_MAX_TURNS = 300;
40
94
  var DEFAULT_TOOL_TIMEOUT_MS = 3e5;
@@ -319,6 +373,16 @@ async function* agentLoop(messages, options) {
319
373
  const MAX_OUTPUT_CONTINUATIONS = 2;
320
374
  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.]";
321
375
  let maxTokensContinuations = 0;
376
+ let providerCalls = 0;
377
+ let nonStreamingCalls = 0;
378
+ let warnedNonStreaming = false;
379
+ const MAX_OUTPUT_CEILING_RETRIES = 1;
380
+ let outputCeilingRetries = 0;
381
+ const ceilingKey = outputRouteKey({
382
+ provider: options.provider,
383
+ model: options.model,
384
+ baseUrl: options.baseUrl
385
+ });
322
386
  const OVERLOAD_BASE_DELAY_MS = 2e3;
323
387
  const OVERLOAD_MAX_DELAY_MS = 3e4;
324
388
  const STREAM_FIRST_EVENT_TIMEOUT_MS = 45e3;
@@ -448,6 +512,18 @@ async function* agentLoop(messages, options) {
448
512
  let streamIterator = null;
449
513
  try {
450
514
  diag("stream_call", { nonStreaming: useNonStreamingFallback });
515
+ providerCalls++;
516
+ if (useNonStreamingFallback) nonStreamingCalls++;
517
+ if (!warnedNonStreaming && nonStreamingCalls >= 3) {
518
+ warnedNonStreaming = true;
519
+ diag("non_streaming_session", {
520
+ nonStreamingCalls,
521
+ providerCalls,
522
+ provider: options.provider,
523
+ model: options.model,
524
+ impact: "streaming is disabled for this session after repeated stalls: failed turns are re-billed in full instead of resuming from partial output, and replies appear only when complete"
525
+ });
526
+ }
451
527
  streamCallStart = Date.now();
452
528
  providerAttemptStartedAt = streamCallStart;
453
529
  let liveApiKey = options.apiKey;
@@ -473,7 +549,9 @@ async function* agentLoop(messages, options) {
473
549
  serverTools: options.serverTools,
474
550
  toolChoice: options.toolChoice,
475
551
  webSearch: options.webSearch,
476
- maxTokens: options.maxTokens,
552
+ // Clamped to whatever ceiling this route has already rejected us for
553
+ // (identity when nothing has been learned).
554
+ maxTokens: clampOutputTokens(ceilingKey, options.maxTokens),
477
555
  temperature: options.temperature,
478
556
  thinking: options.thinking,
479
557
  apiKey: liveApiKey,
@@ -630,6 +708,29 @@ async function* agentLoop(messages, options) {
630
708
  });
631
709
  throw err;
632
710
  }
711
+ const statedCeiling = parseOutputTokenCeiling(err);
712
+ if (statedCeiling !== null) {
713
+ rememberOutputCeiling(ceilingKey, statedCeiling);
714
+ diag("output_ceiling_learned", {
715
+ ceiling: statedCeiling,
716
+ requested: options.maxTokens,
717
+ provider: options.provider,
718
+ model: options.model
719
+ });
720
+ if (outputCeilingRetries < MAX_OUTPUT_CEILING_RETRIES) {
721
+ outputCeilingRetries++;
722
+ yield {
723
+ type: "retry",
724
+ reason: "provider_error",
725
+ attempt: outputCeilingRetries,
726
+ maxAttempts: MAX_OUTPUT_CEILING_RETRIES,
727
+ delayMs: 0,
728
+ silent: true
729
+ };
730
+ turn--;
731
+ continue;
732
+ }
733
+ }
633
734
  if (isContextOverflow(err)) {
634
735
  const overflowDetails = extractContextOverflowDetails(err);
635
736
  diag("context_overflow_detected", {
@@ -1260,6 +1361,8 @@ async function executeSingleToolCall(toolCall, options, pushEvent) {
1260
1361
  toolCall.name
1261
1362
  );
1262
1363
  }
1364
+ } else if (options.signal?.aborted && isAbortError(err)) {
1365
+ resultContent = indeterminateOutcomeText(toolCall.name);
1263
1366
  } else {
1264
1367
  resultContent = redactValue(err instanceof Error ? err.message : String(err));
1265
1368
  }
@@ -1283,6 +1386,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1283
1386
  const eventStream = new EventStream();
1284
1387
  const state = { finalized: false };
1285
1388
  const resultsById = /* @__PURE__ */ new Map();
1389
+ const dispatchedIds = /* @__PURE__ */ new Set();
1286
1390
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1287
1391
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1288
1392
  const phases = [];
@@ -1307,6 +1411,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1307
1411
  for (const phase of phases) {
1308
1412
  if (options.signal?.aborted) break;
1309
1413
  if (phase.sequential) {
1414
+ dispatchedIds.add(phase.sequential.id);
1310
1415
  const record = await executeSingleToolCall(
1311
1416
  phase.sequential,
1312
1417
  options,
@@ -1314,6 +1419,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1314
1419
  );
1315
1420
  resultsById.set(record.toolCallId, record);
1316
1421
  } else if (phase.parallel.length === 1) {
1422
+ dispatchedIds.add(phase.parallel[0].id);
1317
1423
  const record = await executeSingleToolCall(
1318
1424
  phase.parallel[0],
1319
1425
  options,
@@ -1323,6 +1429,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1323
1429
  } else {
1324
1430
  await Promise.all(
1325
1431
  phase.parallel.map(async (toolCall) => {
1432
+ dispatchedIds.add(toolCall.id);
1326
1433
  const record = await executeSingleToolCall(
1327
1434
  toolCall,
1328
1435
  options,
@@ -1353,7 +1460,7 @@ async function* executeToolCallsMixed(toolCalls, initialToolResults, options) {
1353
1460
  options.signal?.removeEventListener("abort", abortHandler);
1354
1461
  state.finalized = true;
1355
1462
  }
1356
- const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById);
1463
+ const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1357
1464
  capToolResults(toolResults, options.maxToolResultChars);
1358
1465
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1359
1466
  return { toolResults, aborted };
@@ -1362,10 +1469,12 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1362
1469
  const eventStream = new EventStream();
1363
1470
  const state = { finalized: false };
1364
1471
  const resultsById = /* @__PURE__ */ new Map();
1472
+ const dispatchedIds = /* @__PURE__ */ new Set();
1365
1473
  const abortHandler = () => eventStream.abort(new Error("aborted"));
1366
1474
  options.signal?.addEventListener("abort", abortHandler, { once: true });
1367
1475
  Promise.all(
1368
1476
  toolCalls.map(async (toolCall) => {
1477
+ dispatchedIds.add(toolCall.id);
1369
1478
  const record = await executeSingleToolCall(
1370
1479
  toolCall,
1371
1480
  options,
@@ -1393,12 +1502,18 @@ async function* executeToolCallsParallel(toolCalls, initialToolResults, options)
1393
1502
  options.signal?.removeEventListener("abort", abortHandler);
1394
1503
  state.finalized = true;
1395
1504
  }
1396
- const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById);
1505
+ const toolResults = buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds);
1397
1506
  capToolResults(toolResults, options.maxToolResultChars);
1398
1507
  capTurnToolResults(toolResults, options.maxTurnToolResultChars);
1399
1508
  return { toolResults, aborted };
1400
1509
  }
1401
- function buildToolResults(initialToolResults, toolCalls, resultsById) {
1510
+ function cancelledBeforeStartText(name) {
1511
+ return `\`${name}\` was cancelled before it started, so it had no effect. Safe to retry.`;
1512
+ }
1513
+ function indeterminateOutcomeText(name) {
1514
+ return `\`${name}\` started running and was cut off before it reported back, so its outcome is UNKNOWN \u2014 it may have completed. Check the real state (re-read the file, re-run a status command) before retrying it, and do not tell the user it did not happen.`;
1515
+ }
1516
+ function buildToolResults(initialToolResults, toolCalls, resultsById, dispatchedIds) {
1402
1517
  const toolResults = [...initialToolResults];
1403
1518
  for (const toolCall of toolCalls) {
1404
1519
  const result = resultsById.get(toolCall.id);
@@ -1410,10 +1525,11 @@ function buildToolResults(initialToolResults, toolCalls, resultsById) {
1410
1525
  isError: result.isError || void 0
1411
1526
  });
1412
1527
  } else {
1528
+ const dispatched = dispatchedIds?.has(toolCall.id) ?? true;
1413
1529
  toolResults.push({
1414
1530
  type: "tool_result",
1415
1531
  toolCallId: toolCall.id,
1416
- content: "Tool execution was aborted.",
1532
+ content: dispatched ? indeterminateOutcomeText(toolCall.name) : cancelledBeforeStartText(toolCall.name),
1417
1533
  isError: true
1418
1534
  });
1419
1535
  }
@@ -1561,32 +1677,22 @@ function repairToolPairingAdjacent(messages) {
1561
1677
  const msg = messages[i];
1562
1678
  if (msg.role !== "assistant") continue;
1563
1679
  if (typeof msg.content === "string" || !Array.isArray(msg.content)) continue;
1564
- const toolCallIds = msg.content.filter((p) => p.type === "tool_call").map((p) => p.id);
1565
- if (toolCallIds.length === 0) continue;
1680
+ const orphanCalls = msg.content.filter((p) => p.type === "tool_call").map((p) => p);
1681
+ if (orphanCalls.length === 0) continue;
1682
+ const repaired = (call) => ({
1683
+ type: "tool_result",
1684
+ toolCallId: call.id,
1685
+ content: indeterminateOutcomeText(call.name),
1686
+ isError: true
1687
+ });
1566
1688
  const next = messages[i + 1];
1567
1689
  if (next?.role === "tool" && Array.isArray(next.content)) {
1568
1690
  const existingIds = new Set(next.content.map((r) => r.toolCallId));
1569
- const missing = toolCallIds.filter((id) => !existingIds.has(id));
1570
- if (missing.length > 0) {
1571
- for (const id of missing) {
1572
- next.content.push({
1573
- type: "tool_result",
1574
- toolCallId: id,
1575
- content: "Tool execution was interrupted.",
1576
- isError: true
1577
- });
1578
- }
1691
+ for (const call of orphanCalls) {
1692
+ if (!existingIds.has(call.id)) next.content.push(repaired(call));
1579
1693
  }
1580
1694
  } else {
1581
- messages.splice(i + 1, 0, {
1582
- role: "tool",
1583
- content: toolCallIds.map((id) => ({
1584
- type: "tool_result",
1585
- toolCallId: id,
1586
- content: "Tool execution was interrupted.",
1587
- isError: true
1588
- }))
1589
- });
1695
+ messages.splice(i + 1, 0, { role: "tool", content: orphanCalls.map(repaired) });
1590
1696
  }
1591
1697
  }
1592
1698
  const toolCallIdSet = /* @__PURE__ */ new Set();
@@ -1745,6 +1851,8 @@ export {
1745
1851
  Agent,
1746
1852
  AgentStream,
1747
1853
  agentLoop,
1854
+ cancelledBeforeStartText,
1855
+ indeterminateOutcomeText,
1748
1856
  isAbortError,
1749
1857
  isBillingError,
1750
1858
  isContextOverflow,