@axlsdk/axl 0.23.0 → 0.23.2

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
@@ -167,6 +167,7 @@ __export(index_exports, {
167
167
  SQLiteStore: () => SQLiteStore,
168
168
  Session: () => Session,
169
169
  SqliteVectorStore: () => SqliteVectorStore,
170
+ StallTimeoutError: () => StallTimeoutError,
170
171
  ThinkTagScanner: () => ThinkTagScanner,
171
172
  TimeoutError: () => TimeoutError,
172
173
  ToolFailure: () => ToolFailure,
@@ -394,15 +395,22 @@ var NoConsensus = class extends AxlError {
394
395
  var TimeoutError = class extends AxlError {
395
396
  /** Latency attribution for the timed-out operation, when measurable. */
396
397
  breakdown;
397
- constructor(operation, timeoutMs, breakdown) {
398
+ constructor(operation, timeoutMs, breakdown, agent2) {
398
399
  super(
399
400
  "TIMEOUT",
400
- `${operation} exceeded timeout of ${timeoutMs}ms` + (breakdown ? ` (elapsed ${breakdown.elapsedMs}ms: queued ${breakdown.queuedMs}ms, retries ${breakdown.retryMs}ms, wire ${breakdown.wireMs}ms, other ${breakdown.otherMs}ms)` : "")
401
+ `${operation} exceeded timeout of ${timeoutMs}ms` + (breakdown ? ` (elapsed ${breakdown.elapsedMs}ms: queued ${breakdown.queuedMs}ms, retries ${breakdown.retryMs}ms, wire ${breakdown.wireMs}ms, other ${breakdown.otherMs}ms)` : "") + (agent2 ? ` for agent '${agent2}'` : "")
401
402
  );
402
403
  this.name = "TimeoutError";
403
404
  if (breakdown) this.breakdown = breakdown;
404
405
  }
405
406
  };
407
+ var StallTimeoutError = class extends TimeoutError {
408
+ constructor(operation, timeoutMs, agent2) {
409
+ super(operation, timeoutMs, void 0, agent2);
410
+ this.name = "StallTimeoutError";
411
+ this.message = `${operation} provider request stalled for ${timeoutMs}ms` + (agent2 ? ` for agent '${agent2}'` : "");
412
+ }
413
+ };
406
414
  function formatBudgetCost(cost) {
407
415
  if (Number.isNaN(cost)) return "$NaN";
408
416
  if (cost === Infinity) return "$Infinity";
@@ -1075,6 +1083,7 @@ async function fetchWithRetry(input, init, opts) {
1075
1083
  message: err instanceof Error ? err.message : String(err)
1076
1084
  });
1077
1085
  }
1086
+ observer?.onRetry?.(attempt + 1, Date.now());
1078
1087
  await sleep2(jitter(BASE_DELAY_MS * 2 ** attempt), init?.signal ?? void 0);
1079
1088
  continue;
1080
1089
  }
@@ -1090,6 +1099,7 @@ async function fetchWithRetry(input, init, opts) {
1090
1099
  }
1091
1100
  const retryAfterMs = parseRetryAfter(res.headers);
1092
1101
  const baseDelay = retryAfterMs !== void 0 ? Math.min(retryAfterMs, MAX_BACKOFF_MS) : BASE_DELAY_MS * 2 ** attempt;
1102
+ observer?.onRetry?.(attempt + 1, Date.now());
1093
1103
  await sleep2(jitter(baseDelay), init?.signal ?? void 0);
1094
1104
  }
1095
1105
  } finally {
@@ -1099,12 +1109,23 @@ async function fetchWithRetry(input, init, opts) {
1099
1109
 
1100
1110
  // src/providers/call-timing.ts
1101
1111
  var CallTimingRecorder = class {
1112
+ constructor(requestLifecycle) {
1113
+ this.requestLifecycle = requestLifecycle;
1114
+ }
1102
1115
  fetchTiming;
1103
1116
  firstTokenAt;
1117
+ /** Consumer suspension after pre-content chunks (for example, tool deltas). */
1118
+ preFirstTokenConsumerMs = 0;
1104
1119
  /** Cumulative time awaiting `reader.read()`, i.e. transport, not consumer. */
1105
1120
  readWaitMs = 0;
1106
1121
  /** Pass as `timing` in the `fetchWithRetry` options object. */
1107
1122
  observer = {
1123
+ onDispatch: () => {
1124
+ this.requestLifecycle?.onDispatch?.();
1125
+ },
1126
+ onRetry: () => {
1127
+ this.requestLifecycle?.onRetry?.();
1128
+ },
1108
1129
  onComplete: (timing) => {
1109
1130
  this.fetchTiming = timing;
1110
1131
  }
@@ -1126,6 +1147,12 @@ var CallTimingRecorder = class {
1126
1147
  markFirstToken() {
1127
1148
  this.firstTokenAt ??= Date.now();
1128
1149
  }
1150
+ /** Exclude backpressure after a pre-content yield from first-content latency. */
1151
+ recordPreFirstTokenConsumerPause(startedAt) {
1152
+ if (this.firstTokenAt === void 0) {
1153
+ this.preFirstTokenConsumerMs += Math.max(0, Date.now() - startedAt);
1154
+ }
1155
+ }
1129
1156
  /**
1130
1157
  * Timing for a non-streaming call. Call once the response body is parsed.
1131
1158
  *
@@ -1149,13 +1176,17 @@ var CallTimingRecorder = class {
1149
1176
  const t = this.fetchTiming;
1150
1177
  if (!t) return void 0;
1151
1178
  const ttfbMs = Math.max(0, t.headersAt - t.dispatchedAt);
1179
+ const firstTokenMs = this.firstTokenAt !== void 0 ? Math.max(0, this.firstTokenAt - t.dispatchedAt - this.preFirstTokenConsumerMs) : void 0;
1152
1180
  return {
1153
1181
  queuedMs: t.queuedMs,
1154
1182
  attempts: t.attempts,
1155
1183
  retryMs: t.retryMs,
1156
1184
  ttfbMs,
1157
- ...this.firstTokenAt !== void 0 ? { firstTokenMs: Math.max(0, this.firstTokenAt - t.dispatchedAt) } : {},
1158
- wireMs: ttfbMs + this.readWaitMs
1185
+ ...firstTokenMs !== void 0 ? { firstTokenMs } : {},
1186
+ // Parsing and generator scheduling can happen after the first read has
1187
+ // settled. Include that pre-first-token gap so the public relationship
1188
+ // stays intuitive, without charging pauses after any chunk is yielded.
1189
+ wireMs: Math.max(ttfbMs + this.readWaitMs, firstTokenMs ?? 0)
1159
1190
  };
1160
1191
  }
1161
1192
  };
@@ -1168,7 +1199,9 @@ async function* withCallTiming(recorder, source) {
1168
1199
  const timing = recorder.streamTiming();
1169
1200
  yield timing ? { ...chunk, timing } : chunk;
1170
1201
  } else {
1202
+ const yieldedAt = Date.now();
1171
1203
  yield chunk;
1204
+ recorder.recordPreFirstTokenConsumerPause(yieldedAt);
1172
1205
  }
1173
1206
  }
1174
1207
  }
@@ -1178,9 +1211,6 @@ function withChatTiming(recorder, response) {
1178
1211
  }
1179
1212
 
1180
1213
  // src/providers/rate-limiter.ts
1181
- function abortError() {
1182
- return Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
1183
- }
1184
1214
  var RateLimiter = class {
1185
1215
  maxConcurrent;
1186
1216
  minIntervalMs;
@@ -1229,11 +1259,11 @@ var RateLimiter = class {
1229
1259
  * With `acquireTimeoutMs`, a waiter that sits in the queue too long rejects.
1230
1260
  */
1231
1261
  acquire(signal) {
1232
- if (signal?.aborted) return Promise.reject(abortError());
1262
+ if (signal?.aborted) return Promise.reject(signal.reason);
1233
1263
  return new Promise((resolve2, reject) => {
1234
1264
  const waiter = { resolve: resolve2, reject, signal };
1235
1265
  if (signal) {
1236
- waiter.onAbort = () => this.settleWaiter(waiter, abortError());
1266
+ waiter.onAbort = () => this.settleWaiter(waiter, signal.reason);
1237
1267
  signal.addEventListener("abort", waiter.onAbort, { once: true });
1238
1268
  }
1239
1269
  if (this.acquireTimeoutMs != null) {
@@ -1533,6 +1563,7 @@ function normalizeMessageContent(content) {
1533
1563
  }
1534
1564
  var OpenAICompatibleProvider = class {
1535
1565
  name;
1566
+ reportsRequestLifecycle = true;
1536
1567
  inputCapabilities(model) {
1537
1568
  return this.name === "openrouter" && model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64"] } } : {};
1538
1569
  }
@@ -1629,7 +1660,7 @@ var OpenAICompatibleProvider = class {
1629
1660
  async chat(messages, options) {
1630
1661
  const headers = this.buildHeaders(await this.resolveKey());
1631
1662
  const body = this.buildRequestBody(messages, options, false);
1632
- const recorder = new CallTimingRecorder();
1663
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
1633
1664
  const res = await fetchWithRetry(
1634
1665
  `${this.baseUrl}/chat/completions`,
1635
1666
  {
@@ -1660,7 +1691,7 @@ var OpenAICompatibleProvider = class {
1660
1691
  async *stream(messages, options) {
1661
1692
  const headers = this.buildHeaders(await this.resolveKey());
1662
1693
  const body = this.buildRequestBody(messages, options, true);
1663
- const recorder = new CallTimingRecorder();
1694
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
1664
1695
  const res = await fetchWithRetry(
1665
1696
  `${this.baseUrl}/chat/completions`,
1666
1697
  {
@@ -2568,6 +2599,7 @@ function responseImageParts(parts) {
2568
2599
  }
2569
2600
  var OpenAIResponsesProvider = class {
2570
2601
  name = "openai-responses";
2602
+ reportsRequestLifecycle = true;
2571
2603
  inputCapabilities(model) {
2572
2604
  return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
2573
2605
  }
@@ -2654,7 +2686,7 @@ var OpenAIResponsesProvider = class {
2654
2686
  async chat(messages, options) {
2655
2687
  const headers = this.buildHeaders(await this.resolveKey());
2656
2688
  const body = this.buildRequestBody(messages, options, false);
2657
- const recorder = new CallTimingRecorder();
2689
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
2658
2690
  const res = await fetchWithRetry(
2659
2691
  `${this.baseUrl}/responses`,
2660
2692
  {
@@ -2689,7 +2721,7 @@ var OpenAIResponsesProvider = class {
2689
2721
  async *stream(messages, options) {
2690
2722
  const headers = this.buildHeaders(await this.resolveKey());
2691
2723
  const body = this.buildRequestBody(messages, options, true);
2692
- const recorder = new CallTimingRecorder();
2724
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
2693
2725
  const res = await fetchWithRetry(
2694
2726
  `${this.baseUrl}/responses`,
2695
2727
  {
@@ -3493,6 +3525,7 @@ function mergeAnthropicUsage(initial, update) {
3493
3525
  }
3494
3526
  var AnthropicProvider = class {
3495
3527
  name = "anthropic";
3528
+ reportsRequestLifecycle = true;
3496
3529
  /** Report a clamped `effort`. Anthropic clamps to each model's supported
3497
3530
  * `output_config.effort` levels, and models that always think fall back to
3498
3531
  * adaptive `'low'` when asked to disable thinking. Only reported when the
@@ -3610,7 +3643,7 @@ var AnthropicProvider = class {
3610
3643
  );
3611
3644
  const body = this.buildRequestBody(messages, options, false);
3612
3645
  const pricingContext = pricingContextFromBody(body);
3613
- const recorder = new CallTimingRecorder();
3646
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
3614
3647
  const res = await fetchWithRetry(
3615
3648
  `${this.baseUrl}/messages`,
3616
3649
  {
@@ -3646,7 +3679,7 @@ var AnthropicProvider = class {
3646
3679
  );
3647
3680
  const body = this.buildRequestBody(messages, options, true);
3648
3681
  const pricingContext = pricingContextFromBody(body);
3649
- const recorder = new CallTimingRecorder();
3682
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
3650
3683
  const res = await fetchWithRetry(
3651
3684
  `${this.baseUrl}/messages`,
3652
3685
  {
@@ -4473,6 +4506,7 @@ function resolveGeminiThinking(model, resolved) {
4473
4506
  }
4474
4507
  var GeminiProvider = class {
4475
4508
  name = "google";
4509
+ reportsRequestLifecycle = true;
4476
4510
  inputCapabilities(model) {
4477
4511
  return model.trim().length > 0 ? { image: { sources: ["bytes", "base64", "provider-file"] } } : {};
4478
4512
  }
@@ -4590,7 +4624,7 @@ var GeminiProvider = class {
4590
4624
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4591
4625
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4592
4626
  const headers = this.buildHeaders(await this.resolveKey());
4593
- const recorder = new CallTimingRecorder();
4627
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4594
4628
  const res = await fetchWithRetry(
4595
4629
  `${this.baseUrl}/models/${pricingContext.model}:generateContent`,
4596
4630
  {
@@ -4628,7 +4662,7 @@ var GeminiProvider = class {
4628
4662
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4629
4663
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4630
4664
  const headers = this.buildHeaders(await this.resolveKey());
4631
- const recorder = new CallTimingRecorder();
4665
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4632
4666
  const res = await fetchWithRetry(
4633
4667
  `${this.baseUrl}/models/${pricingContext.model}:streamGenerateContent?alt=sse`,
4634
4668
  {
@@ -4665,7 +4699,7 @@ var GeminiProvider = class {
4665
4699
  this.assertSafeInteractionOptions(options);
4666
4700
  const body = this.buildInteractionRequestBody(messages, options, false);
4667
4701
  const headers = this.buildHeaders(await this.resolveKey());
4668
- const recorder = new CallTimingRecorder();
4702
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4669
4703
  const res = await fetchWithRetry(
4670
4704
  `${this.baseUrl}/interactions`,
4671
4705
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
@@ -4691,7 +4725,7 @@ var GeminiProvider = class {
4691
4725
  this.assertSafeInteractionOptions(options);
4692
4726
  const body = this.buildInteractionRequestBody(messages, options, true);
4693
4727
  const headers = this.buildHeaders(await this.resolveKey());
4694
- const recorder = new CallTimingRecorder();
4728
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4695
4729
  const res = await fetchWithRetry(
4696
4730
  `${this.baseUrl}/interactions?alt=sse`,
4697
4731
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
@@ -6410,7 +6444,7 @@ var GEMINI_AUDIO_MEDIA_TYPES = /* @__PURE__ */ new Set([
6410
6444
  "audio/mulaw",
6411
6445
  "audio/webm"
6412
6446
  ]);
6413
- function abortError2() {
6447
+ function abortError() {
6414
6448
  return new DOMException("The transcription operation was aborted", "AbortError");
6415
6449
  }
6416
6450
  function audioMime(audio) {
@@ -6442,13 +6476,13 @@ function secondsOffset(value) {
6442
6476
  function sleep3(ms, signal) {
6443
6477
  return new Promise((resolve2, reject) => {
6444
6478
  if (signal?.aborted) {
6445
- reject(abortError2());
6479
+ reject(abortError());
6446
6480
  return;
6447
6481
  }
6448
6482
  const timer = setTimeout(done, ms);
6449
6483
  const onAbort = () => {
6450
6484
  clearTimeout(timer);
6451
- done(abortError2());
6485
+ done(abortError());
6452
6486
  };
6453
6487
  function done(error) {
6454
6488
  signal?.removeEventListener("abort", onAbort);
@@ -7769,7 +7803,7 @@ function isCostBearingLeaf(event) {
7769
7803
  return COST_LEAF_SET.has(event.type);
7770
7804
  }
7771
7805
  function isUnpricedLeaf(event) {
7772
- return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && hasPositiveBillableWork(event);
7806
+ return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && (event.unpriced === true || hasPositiveBillableWork(event));
7773
7807
  }
7774
7808
  function isRootLevel(event) {
7775
7809
  const d = event.depth;
@@ -9055,18 +9089,27 @@ function parseDuration(duration) {
9055
9089
  if (!match) throw new Error(`Invalid duration format: "${duration}"`);
9056
9090
  const value = parseFloat(match[1]);
9057
9091
  const unit = match[2];
9092
+ let milliseconds;
9058
9093
  switch (unit) {
9059
9094
  case "ms":
9060
- return value;
9095
+ milliseconds = value;
9096
+ break;
9061
9097
  case "s":
9062
- return value * 1e3;
9098
+ milliseconds = value * 1e3;
9099
+ break;
9063
9100
  case "m":
9064
- return value * 6e4;
9101
+ milliseconds = value * 6e4;
9102
+ break;
9065
9103
  case "h":
9066
- return value * 36e5;
9104
+ milliseconds = value * 36e5;
9105
+ break;
9067
9106
  default:
9068
9107
  throw new Error(`Unknown duration unit: "${unit}"`);
9069
9108
  }
9109
+ if (!Number.isFinite(milliseconds)) {
9110
+ throw new Error(`Duration is too large: "${duration}"`);
9111
+ }
9112
+ return milliseconds;
9070
9113
  }
9071
9114
  function parseCost(cost) {
9072
9115
  const match = cost.match(/^\$?([\d.]+)$/);
@@ -9722,6 +9765,42 @@ function recordToolSpanOutcome(span, outcome) {
9722
9765
 
9723
9766
  // src/context.ts
9724
9767
  var signalStorage = new import_node_async_hooks.AsyncLocalStorage();
9768
+ var hardSignalStorage = new import_node_async_hooks.AsyncLocalStorage();
9769
+ var timeoutTrackerStorage = new import_node_async_hooks.AsyncLocalStorage();
9770
+ var MAX_TIMER_DELAY_MS = 2147483647;
9771
+ function composeAbortSignals(...signals) {
9772
+ const active = signals.filter((signal) => signal !== void 0);
9773
+ if (active.length === 0) return void 0;
9774
+ if (active.length === 1) return active[0];
9775
+ return AbortSignal.any(active);
9776
+ }
9777
+ function raceWithAbortSignal(promise, signal) {
9778
+ if (!signal) return promise;
9779
+ if (signal.aborted) {
9780
+ void promise.catch(() => {
9781
+ });
9782
+ return Promise.reject(signal.reason);
9783
+ }
9784
+ return new Promise((resolve2, reject) => {
9785
+ let settled = false;
9786
+ const finish = (callback) => {
9787
+ if (settled) return;
9788
+ settled = true;
9789
+ signal.removeEventListener("abort", onAbort);
9790
+ callback();
9791
+ };
9792
+ const onAbort = () => finish(() => reject(signal.reason));
9793
+ signal.addEventListener("abort", onAbort, { once: true });
9794
+ if (signal.aborted) onAbort();
9795
+ promise.then(
9796
+ (value) => finish(() => resolve2(value)),
9797
+ (error) => finish(() => reject(error))
9798
+ );
9799
+ });
9800
+ }
9801
+ function pausedHumanWaitMs(tracker, now = Date.now()) {
9802
+ return tracker.humanWaitMs + (tracker.activeHumanWaits > 0 && tracker.humanWaitStartedAt !== void 0 ? now - tracker.humanWaitStartedAt : 0);
9803
+ }
9725
9804
  var DEFAULT_BRANCH_DRAIN_TIMEOUT_MS = 5e3;
9726
9805
  function resolveBranchDrainTimeoutMs(value) {
9727
9806
  const resolved = value === void 0 ? DEFAULT_BRANCH_DRAIN_TIMEOUT_MS : value;
@@ -10523,7 +10602,10 @@ var WorkflowContext = class _WorkflowContext {
10523
10602
  tool_call_id: parsed.callId
10524
10603
  });
10525
10604
  }
10526
- if (settlement.abortError !== void 0) throw settlement.abortError;
10605
+ if (settlement.abortError !== void 0) {
10606
+ signal?.throwIfAborted();
10607
+ throw settlement.abortError;
10608
+ }
10527
10609
  }
10528
10610
  // ── ctx.ask() ─────────────────────────────────────────────────────────
10529
10611
  async ask(agent2, prompt, options) {
@@ -10539,7 +10621,10 @@ var WorkflowContext = class _WorkflowContext {
10539
10621
  const promptText = inputText(normalizedInput);
10540
10622
  const inputDescriptor = describeModelInput(normalizedInput);
10541
10623
  const agentName = agent2._name;
10542
- return this._checkpoint(
10624
+ const inheritedHardSignal = hardSignalStorage.getStore() ?? this.signal;
10625
+ const hardAskSignal = composeAbortSignals(inheritedHardSignal, options?.signal);
10626
+ const askSignal = composeAbortSignals(this.currentSignal, options?.signal);
10627
+ const runAsk = () => this._checkpoint(
10543
10628
  this._autoCheckpointName("ask", agentName),
10544
10629
  async () => {
10545
10630
  const parentFrame = askStorage.getStore();
@@ -10694,8 +10779,25 @@ var WorkflowContext = class _WorkflowContext {
10694
10779
  },
10695
10780
  { agent: agentName }
10696
10781
  );
10782
+ const runWithAskSignal = () => askSignal ? signalStorage.run(askSignal, runAsk) : runAsk();
10783
+ return hardAskSignal ? hardSignalStorage.run(hardAskSignal, runWithAskSignal) : runWithAskSignal();
10697
10784
  }
10698
10785
  async executeAgentCall(agent2, input, options, handoffMessages, usageCapture, sessionHistory = normalizeSessionHistory(this.sessionHistory)) {
10786
+ const tracker = { humanWaitMs: 0, activeHumanWaits: 0 };
10787
+ const enclosing = timeoutTrackerStorage.getStore() ?? [];
10788
+ return timeoutTrackerStorage.run(
10789
+ [...enclosing, tracker],
10790
+ () => this.executeAgentCallImpl(
10791
+ agent2,
10792
+ input,
10793
+ options,
10794
+ handoffMessages,
10795
+ usageCapture,
10796
+ sessionHistory
10797
+ )
10798
+ );
10799
+ }
10800
+ async executeAgentCallImpl(agent2, input, options, handoffMessages, usageCapture, sessionHistory = normalizeSessionHistory(this.sessionHistory)) {
10699
10801
  if (this.budgetContext?.exceeded) {
10700
10802
  const { limit, totalCost: spent, policy } = this.budgetContext;
10701
10803
  if (policy === "warn") {
@@ -10935,8 +11037,18 @@ ${appendedSchemaText}`;
10935
11037
  }
10936
11038
  }
10937
11039
  const maxTurns = agent2._config.maxTurns ?? 25;
10938
- const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
11040
+ const timeoutMs = parseDuration(
11041
+ options?.timeout ?? agent2._config.timeout ?? this.config.defaults?.timeout ?? "60s"
11042
+ );
11043
+ const stallTimeout = options?.stallTimeout ?? agent2._config.stallTimeout ?? this.config.defaults?.stallTimeout;
11044
+ const stallTimeoutMs = stallTimeout === void 0 ? void 0 : parseDuration(stallTimeout);
11045
+ if (stallTimeoutMs !== void 0 && stallTimeoutMs > MAX_TIMER_DELAY_MS) {
11046
+ throw new RangeError(
11047
+ `stallTimeout exceeds the maximum supported timer duration of ${MAX_TIMER_DELAY_MS}ms`
11048
+ );
11049
+ }
10939
11050
  const startTime = Date.now();
11051
+ const timeoutTracker = timeoutTrackerStorage.getStore()?.at(-1);
10940
11052
  const timingTotals = { turns: 0, queuedMs: 0, retryMs: 0, wireMs: 0 };
10941
11053
  const currentMessages = [...messages];
10942
11054
  let turns = 0;
@@ -10951,7 +11063,8 @@ ${appendedSchemaText}`;
10951
11063
  let lastStartMaxAttempts = 1;
10952
11064
  while (turns < maxTurns) {
10953
11065
  this.currentSignal?.throwIfAborted();
10954
- const elapsedMs = Date.now() - startTime;
11066
+ const now = Date.now();
11067
+ const elapsedMs = now - startTime - (timeoutTracker ? pausedHumanWaitMs(timeoutTracker, now) : 0);
10955
11068
  if (elapsedMs > timeoutMs) {
10956
11069
  throw new TimeoutError(
10957
11070
  "ctx.ask()",
@@ -10965,7 +11078,8 @@ ${appendedSchemaText}`;
10965
11078
  0,
10966
11079
  elapsedMs - timingTotals.queuedMs - timingTotals.retryMs - timingTotals.wireMs
10967
11080
  )
10968
- } : void 0
11081
+ } : void 0,
11082
+ agent2._name
10969
11083
  );
10970
11084
  }
10971
11085
  turns++;
@@ -11012,6 +11126,43 @@ ${appendedSchemaText}`;
11012
11126
  providerOptions,
11013
11127
  signal: this.currentSignal
11014
11128
  };
11129
+ const requestSignal = chatOptions.signal;
11130
+ const hardRequestSignal = hardSignalStorage.getStore() ?? this.signal;
11131
+ const stallController = stallTimeoutMs === void 0 ? void 0 : new AbortController();
11132
+ const stallReason = stallController === void 0 ? void 0 : new DOMException("Provider request stalled", "StallTimeout");
11133
+ let stallTimer;
11134
+ let providerCallActive = true;
11135
+ let requestDispatched = false;
11136
+ const clearStallTimer = () => {
11137
+ if (stallTimer !== void 0) {
11138
+ clearTimeout(stallTimer);
11139
+ stallTimer = void 0;
11140
+ }
11141
+ };
11142
+ const armStallTimer = () => {
11143
+ if (!providerCallActive || !stallController || !stallReason || stallTimeoutMs === void 0)
11144
+ return;
11145
+ requestDispatched = true;
11146
+ clearStallTimer();
11147
+ if (stallController.signal.aborted) return;
11148
+ stallTimer = setTimeout(() => stallController.abort(stallReason), stallTimeoutMs);
11149
+ };
11150
+ if (stallController) {
11151
+ chatOptions.signal = composeAbortSignals(requestSignal, stallController.signal);
11152
+ chatOptions.requestLifecycle = {
11153
+ onDispatch: armStallTimer,
11154
+ onRetry: clearStallTimer
11155
+ };
11156
+ }
11157
+ const providerSettlementSignal = composeAbortSignals(
11158
+ hardRequestSignal,
11159
+ stallController?.signal
11160
+ );
11161
+ const throwIfRequestStalled = () => {
11162
+ if (stallReason !== void 0 && providerSettlementSignal?.reason === stallReason) {
11163
+ throw new StallTimeoutError("ctx.ask()", stallTimeoutMs, agent2._name);
11164
+ }
11165
+ };
11015
11166
  if (options?.schema && toolDefs.length === 0) {
11016
11167
  const nativeOut = options?.nativeStructuredOutput ?? agent2._config.nativeStructuredOutput ?? false;
11017
11168
  chatOptions.responseFormat = nativeOut ? {
@@ -11098,55 +11249,77 @@ ${appendedSchemaText}`;
11098
11249
  });
11099
11250
  }
11100
11251
  }) : void 0;
11101
- for await (const chunk of provider.stream(
11252
+ if (stallController && provider.reportsRequestLifecycle !== true) armStallTimer();
11253
+ const providerStream = provider.stream(
11102
11254
  cloneMessagesForProvider(currentMessages),
11103
11255
  chatOptions
11104
- )) {
11105
- if (chunk.type === "text_delta") {
11106
- content2 += chunk.content;
11107
- this.emitEvent({ type: "token", data: chunk.content });
11108
- if (walker) {
11109
- walker.processChunk(chunk.content);
11110
- if (walker.consumeBoundary()) {
11111
- let parsed;
11112
- try {
11113
- parsed = parsePartialJson(extractJson(content2));
11114
- } catch {
11115
- parsed = void 0;
11116
- }
11117
- if (parsed !== void 0) {
11118
- this.emitEvent({
11119
- type: "partial_object",
11120
- agent: agent2._name,
11121
- attempt: currentAttempt,
11122
- data: { object: parsed }
11123
- });
11256
+ );
11257
+ const iterator = providerStream[Symbol.asyncIterator]();
11258
+ try {
11259
+ while (true) {
11260
+ const next = await raceWithAbortSignal(
11261
+ Promise.resolve(iterator.next()),
11262
+ providerSettlementSignal
11263
+ );
11264
+ if (next.done) break;
11265
+ const chunk = next.value;
11266
+ if (stallController) armStallTimer();
11267
+ if (chunk.type === "text_delta") {
11268
+ content2 += chunk.content;
11269
+ this.emitEvent({ type: "token", data: chunk.content });
11270
+ if (walker) {
11271
+ walker.processChunk(chunk.content);
11272
+ if (walker.consumeBoundary()) {
11273
+ let parsed;
11274
+ try {
11275
+ parsed = parsePartialJson(extractJson(content2));
11276
+ } catch {
11277
+ parsed = void 0;
11278
+ }
11279
+ if (parsed !== void 0) {
11280
+ this.emitEvent({
11281
+ type: "partial_object",
11282
+ agent: agent2._name,
11283
+ attempt: currentAttempt,
11284
+ data: { object: parsed }
11285
+ });
11286
+ }
11124
11287
  }
11125
11288
  }
11126
- }
11127
- } else if (chunk.type === "thinking_delta") {
11128
- thinkingContent += chunk.content;
11129
- } else if (chunk.type === "tool_call_delta") {
11130
- let buffer = toolCallBuffers.get(chunk.id);
11131
- if (!buffer) {
11132
- buffer = { id: chunk.id, name: "", arguments: "" };
11133
- toolCallBuffers.set(chunk.id, buffer);
11134
- }
11135
- if (chunk.name) buffer.name = chunk.name;
11136
- if (chunk.arguments) buffer.arguments += chunk.arguments;
11137
- } else if (chunk.type === "done") {
11138
- streamProviderMetadata = chunk.providerMetadata;
11139
- streamTiming = chunk.timing;
11140
- if (chunk.usage) {
11289
+ } else if (chunk.type === "thinking_delta") {
11290
+ thinkingContent += chunk.content;
11291
+ } else if (chunk.type === "tool_call_delta") {
11292
+ let buffer = toolCallBuffers.get(chunk.id);
11293
+ if (!buffer) {
11294
+ buffer = { id: chunk.id, name: "", arguments: "" };
11295
+ toolCallBuffers.set(chunk.id, buffer);
11296
+ }
11297
+ if (chunk.name) buffer.name = chunk.name;
11298
+ if (chunk.arguments) buffer.arguments += chunk.arguments;
11299
+ } else if (chunk.type === "done") {
11300
+ streamProviderMetadata = chunk.providerMetadata;
11301
+ streamTiming = chunk.timing;
11141
11302
  response = {
11142
11303
  content: content2,
11143
11304
  tool_calls: void 0,
11144
11305
  usage: chunk.usage,
11145
- cost: chunk.cost
11306
+ cost: chunk.cost,
11307
+ providerMetadata: chunk.providerMetadata,
11308
+ timing: chunk.timing
11146
11309
  };
11147
11310
  }
11148
11311
  }
11312
+ } catch (error) {
11313
+ if (iterator.return) {
11314
+ try {
11315
+ void Promise.resolve(iterator.return(void 0)).catch(() => {
11316
+ });
11317
+ } catch {
11318
+ }
11319
+ }
11320
+ throw error;
11149
11321
  }
11322
+ throwIfRequestStalled();
11150
11323
  for (const buffer of toolCallBuffers.values()) {
11151
11324
  toolCalls.push({
11152
11325
  id: buffer.id,
@@ -11171,16 +11344,27 @@ ${appendedSchemaText}`;
11171
11344
  response.thinking_content = thinkingContent;
11172
11345
  }
11173
11346
  } else {
11174
- response = await provider.chat(cloneMessagesForProvider(currentMessages), chatOptions);
11347
+ if (stallController && provider.reportsRequestLifecycle !== true) armStallTimer();
11348
+ response = await raceWithAbortSignal(
11349
+ provider.chat(cloneMessagesForProvider(currentMessages), chatOptions),
11350
+ providerSettlementSignal
11351
+ );
11352
+ throwIfRequestStalled();
11175
11353
  }
11176
11354
  } catch (err) {
11177
- if (richRequest) this.recordRichFailure(err);
11178
- const failureTiming = err instanceof ProviderError ? err.timing : void 0;
11355
+ const callError = stallReason !== void 0 && providerSettlementSignal?.reason === stallReason ? new StallTimeoutError("ctx.ask()", stallTimeoutMs, agent2._name) : err;
11356
+ if (richRequest) this.recordRichFailure(callError);
11357
+ const failureTiming = callError instanceof ProviderError ? callError.timing : void 0;
11179
11358
  this.emitEvent({
11180
11359
  type: "agent_call_end",
11181
11360
  agent: agent2._name,
11182
11361
  model: effectiveModelUri,
11183
11362
  promptVersion: agent2._config.version,
11363
+ // A dispatched request abandoned at the runtime boundary may still
11364
+ // be billed even though a non-cooperative provider supplied no
11365
+ // usage. Preserve budget honesty by marking the known total as a
11366
+ // lower bound rather than presenting an exact $0.
11367
+ ...callError instanceof StallTimeoutError && requestDispatched ? { unpriced: true } : {},
11184
11368
  duration: Date.now() - turnStart,
11185
11369
  // Top-level beside `duration`, exactly as on the success path.
11186
11370
  ...failureTiming ? { timing: failureTiming } : {},
@@ -11188,14 +11372,17 @@ ${appendedSchemaText}`;
11188
11372
  response: "",
11189
11373
  turn: turns,
11190
11374
  ...retryReason ? { retryReason } : {},
11191
- error: this._observerErrorProjection(err)?.message ?? legacyErrorMessage(err),
11375
+ error: this._observerErrorProjection(callError)?.message ?? legacyErrorMessage(callError),
11192
11376
  // Enrich with typed-error metadata when available. `body` is
11193
11377
  // deliberately NOT emitted — it's redaction-eligible (see
11194
11378
  // docs/security.md). `data.error` already carries the message.
11195
- ...providerErrorEventMetadata(err, richRequest)
11379
+ ...providerErrorEventMetadata(callError, richRequest)
11196
11380
  }
11197
11381
  });
11198
- throw err;
11382
+ throw callError;
11383
+ } finally {
11384
+ providerCallActive = false;
11385
+ clearStallTimer();
11199
11386
  }
11200
11387
  if (usageCapture && response.usage) {
11201
11388
  usageCapture.value = response.usage;
@@ -12720,46 +12907,52 @@ ${summaryResponse.content}`
12720
12907
  }
12721
12908
  // ── ctx.awaitHuman() ──────────────────────────────────────────────────
12722
12909
  async awaitHuman(options) {
12723
- if (this.spanManager) {
12724
- return this.spanManager.withSpanAsync(
12725
- "axl.ctx.awaitHuman",
12726
- {
12727
- "axl.awaitHuman.channel": options.channel
12728
- },
12729
- async (span) => {
12730
- const start = Date.now();
12731
- const result = await this._awaitHumanImpl(options);
12732
- span.setAttribute("axl.awaitHuman.wait_duration", Date.now() - start);
12733
- span.setAttribute("axl.awaitHuman.approved", result.approved);
12734
- return result;
12910
+ const trackers = timeoutTrackerStorage.getStore();
12911
+ const startedAt = Date.now();
12912
+ for (const tracker of trackers ?? []) {
12913
+ if (tracker.activeHumanWaits++ === 0) tracker.humanWaitStartedAt = startedAt;
12914
+ }
12915
+ try {
12916
+ if (this.spanManager) {
12917
+ return await this.spanManager.withSpanAsync(
12918
+ "axl.ctx.awaitHuman",
12919
+ {
12920
+ "axl.awaitHuman.channel": options.channel
12921
+ },
12922
+ async (span) => {
12923
+ const start = Date.now();
12924
+ const result = await this._awaitHumanImpl(options);
12925
+ span.setAttribute("axl.awaitHuman.wait_duration", Date.now() - start);
12926
+ span.setAttribute("axl.awaitHuman.approved", result.approved);
12927
+ return result;
12928
+ }
12929
+ );
12930
+ }
12931
+ return await this._awaitHumanImpl(options);
12932
+ } finally {
12933
+ const finishedAt = Date.now();
12934
+ for (const tracker of trackers ?? []) {
12935
+ tracker.activeHumanWaits--;
12936
+ if (tracker.activeHumanWaits === 0 && tracker.humanWaitStartedAt !== void 0) {
12937
+ tracker.humanWaitMs += finishedAt - tracker.humanWaitStartedAt;
12938
+ tracker.humanWaitStartedAt = void 0;
12735
12939
  }
12736
- );
12940
+ }
12737
12941
  }
12738
- return this._awaitHumanImpl(options);
12739
12942
  }
12740
12943
  async _awaitHumanImpl(options) {
12741
12944
  const branchSignal = this.currentSignal;
12742
- const makeAbortError = (reason) => {
12743
- if (typeof DOMException !== "undefined") {
12744
- return new DOMException(
12745
- typeof reason === "string" ? reason : "awaitHuman aborted",
12746
- "AbortError"
12747
- );
12748
- }
12749
- const err = new Error(typeof reason === "string" ? reason : "awaitHuman aborted");
12750
- err.name = "AbortError";
12751
- return err;
12752
- };
12753
12945
  const awaitWithAbort = async (promise, signal2 = branchSignal) => {
12754
12946
  if (!signal2) return promise;
12755
- if (signal2.aborted) throw makeAbortError(signal2.reason);
12947
+ signal2.throwIfAborted();
12756
12948
  let onAbort;
12757
12949
  try {
12758
12950
  return await Promise.race([
12759
12951
  promise,
12760
12952
  new Promise((_, reject) => {
12761
- onAbort = () => reject(makeAbortError(signal2.reason));
12953
+ onAbort = () => reject(signal2.reason);
12762
12954
  signal2.addEventListener("abort", onAbort, { once: true });
12955
+ if (signal2.aborted) onAbort();
12763
12956
  })
12764
12957
  ]);
12765
12958
  } finally {
@@ -12767,12 +12960,12 @@ ${summaryResponse.content}`
12767
12960
  }
12768
12961
  };
12769
12962
  if (this.awaitHumanHandler) {
12770
- if (branchSignal?.aborted) throw makeAbortError(branchSignal.reason);
12963
+ branchSignal?.throwIfAborted();
12771
12964
  this.emitEvent({
12772
12965
  type: "await_human",
12773
12966
  data: { channel: options.channel, prompt: options.prompt }
12774
12967
  });
12775
- if (branchSignal?.aborted) throw makeAbortError(branchSignal.reason);
12968
+ branchSignal?.throwIfAborted();
12776
12969
  const decision2 = parseHumanDecision(
12777
12970
  await awaitWithAbort(Promise.resolve(this.awaitHumanHandler(options)))
12778
12971
  );
@@ -12827,12 +13020,12 @@ ${summaryResponse.content}`
12827
13020
  const abortListener = () => {
12828
13021
  registeredResolver.cancelled = true;
12829
13022
  markReady();
12830
- rejectDecision(makeAbortError(signal.reason));
13023
+ rejectDecision(signal.reason);
12831
13024
  };
12832
13025
  signal.addEventListener("abort", abortListener, { once: true });
12833
13026
  let decision;
12834
13027
  try {
12835
- if (signal.aborted) throw makeAbortError(signal.reason);
13028
+ signal.throwIfAborted();
12836
13029
  if (this.stateStore) {
12837
13030
  saveAttempted = true;
12838
13031
  await this.stateStore.savePendingDecision(this.executionId, {
@@ -12843,7 +13036,7 @@ ${summaryResponse.content}`
12843
13036
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
12844
13037
  });
12845
13038
  }
12846
- if (signal.aborted) throw makeAbortError(signal.reason);
13039
+ signal.throwIfAborted();
12847
13040
  this.emitEvent({
12848
13041
  type: "await_human",
12849
13042
  data: { channel: options.channel, prompt: options.prompt }
@@ -13313,7 +13506,7 @@ ${summaryResponse.content}`
13313
13506
  data: partial.data
13314
13507
  });
13315
13508
  };
13316
- const abortError3 = () => new DOMException("The transcription operation was aborted", "AbortError");
13509
+ const abortError2 = () => new DOMException("The transcription operation was aborted", "AbortError");
13317
13510
  const cleanupStatus = (value) => value === "not_required" || value === "deleted" || value === "failed" || value === "timed_out" ? value : void 0;
13318
13511
  const safeProviderFailure = (error, provider, model) => {
13319
13512
  const details = error && typeof error === "object" ? error : void 0;
@@ -13364,7 +13557,7 @@ ${summaryResponse.content}`
13364
13557
  hasWork: false
13365
13558
  })
13366
13559
  });
13367
- throw abortError3();
13560
+ throw abortError2();
13368
13561
  }
13369
13562
  if (this.budgetContext?.exceeded) {
13370
13563
  const { limit, totalCost: spent, policy } = this.budgetContext;
@@ -13430,7 +13623,7 @@ ${summaryResponse.content}`
13430
13623
  ...failure.cleanup ? [{ cleanupStatus: failure.cleanup }] : []
13431
13624
  )
13432
13625
  });
13433
- throw abortError3();
13626
+ throw abortError2();
13434
13627
  }
13435
13628
  if (failure.accounting.cost !== void 0)
13436
13629
  this._accumulateBudgetCost(failure.accounting.cost);
@@ -13473,7 +13666,7 @@ ${summaryResponse.content}`
13473
13666
  ...cleanupStatus(result.cleanupStatus) ? [{ cleanupStatus: cleanupStatus(result.cleanupStatus) }] : []
13474
13667
  )
13475
13668
  });
13476
- throw abortError3();
13669
+ throw abortError2();
13477
13670
  }
13478
13671
  let transcript;
13479
13672
  try {
@@ -13559,9 +13752,9 @@ ${summaryResponse.content}`
13559
13752
  const cost = partial.cost;
13560
13753
  if (COST_BEARING_LEAF_TYPES.includes(partial.type)) {
13561
13754
  const usable = isUsableCost(cost);
13562
- const unpriced = !usable && hasPositiveBillableWork(
13755
+ const unpriced = !usable && (partial.unpriced === true || hasPositiveBillableWork(
13563
13756
  partial
13564
- );
13757
+ ));
13565
13758
  if (frame) {
13566
13759
  if (usable) frame.askCost.value += cost;
13567
13760
  else if (unpriced) frame.askUnpriced = true;
@@ -13572,7 +13765,7 @@ ${summaryResponse.content}`
13572
13765
  if (!this.budgetContext.unpricedWarned && Number.isFinite(this.budgetContext.limit)) {
13573
13766
  this.budgetContext.unpricedWarned = true;
13574
13767
  console.warn(
13575
- "Budget honesty: unpriced work detected \u2014 this budget's cost limit is a lower bound and is NOT enforced on unpriced models. See docs/observability.md#budget-honesty."
13768
+ "Budget honesty: unknown-cost work detected \u2014 this budget's cost limit is a lower bound and is NOT enforced on unknown spend. See docs/observability.md#budget-honesty."
13576
13769
  );
13577
13770
  }
13578
13771
  }
@@ -15541,12 +15734,13 @@ function forwardAbortSignal(external, internal) {
15541
15734
  if (!external) return () => {
15542
15735
  };
15543
15736
  if (external.aborted) {
15544
- internal.abort();
15737
+ internal.abort(external.reason);
15545
15738
  return () => {
15546
15739
  };
15547
15740
  }
15548
- const onAbort = () => internal.abort();
15741
+ const onAbort = () => internal.abort(external.reason);
15549
15742
  external.addEventListener("abort", onAbort, { once: true });
15743
+ if (external.aborted) onAbort();
15550
15744
  return () => external.removeEventListener("abort", onAbort);
15551
15745
  }
15552
15746
  var AxlRuntime = class extends import_node_events2.EventEmitter {
@@ -18028,6 +18222,7 @@ function cosineSimilarity2(a, b) {
18028
18222
  SQLiteStore,
18029
18223
  Session,
18030
18224
  SqliteVectorStore,
18225
+ StallTimeoutError,
18031
18226
  ThinkTagScanner,
18032
18227
  TimeoutError,
18033
18228
  ToolFailure,