@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.js CHANGED
@@ -172,15 +172,22 @@ var NoConsensus = class extends AxlError {
172
172
  var TimeoutError = class extends AxlError {
173
173
  /** Latency attribution for the timed-out operation, when measurable. */
174
174
  breakdown;
175
- constructor(operation, timeoutMs, breakdown) {
175
+ constructor(operation, timeoutMs, breakdown, agent2) {
176
176
  super(
177
177
  "TIMEOUT",
178
- `${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)` : "")
178
+ `${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}'` : "")
179
179
  );
180
180
  this.name = "TimeoutError";
181
181
  if (breakdown) this.breakdown = breakdown;
182
182
  }
183
183
  };
184
+ var StallTimeoutError = class extends TimeoutError {
185
+ constructor(operation, timeoutMs, agent2) {
186
+ super(operation, timeoutMs, void 0, agent2);
187
+ this.name = "StallTimeoutError";
188
+ this.message = `${operation} provider request stalled for ${timeoutMs}ms` + (agent2 ? ` for agent '${agent2}'` : "");
189
+ }
190
+ };
184
191
  function formatBudgetCost(cost) {
185
192
  if (Number.isNaN(cost)) return "$NaN";
186
193
  if (cost === Infinity) return "$Infinity";
@@ -853,6 +860,7 @@ async function fetchWithRetry(input, init, opts) {
853
860
  message: err instanceof Error ? err.message : String(err)
854
861
  });
855
862
  }
863
+ observer?.onRetry?.(attempt + 1, Date.now());
856
864
  await sleep2(jitter(BASE_DELAY_MS * 2 ** attempt), init?.signal ?? void 0);
857
865
  continue;
858
866
  }
@@ -868,6 +876,7 @@ async function fetchWithRetry(input, init, opts) {
868
876
  }
869
877
  const retryAfterMs = parseRetryAfter(res.headers);
870
878
  const baseDelay = retryAfterMs !== void 0 ? Math.min(retryAfterMs, MAX_BACKOFF_MS) : BASE_DELAY_MS * 2 ** attempt;
879
+ observer?.onRetry?.(attempt + 1, Date.now());
871
880
  await sleep2(jitter(baseDelay), init?.signal ?? void 0);
872
881
  }
873
882
  } finally {
@@ -877,12 +886,23 @@ async function fetchWithRetry(input, init, opts) {
877
886
 
878
887
  // src/providers/call-timing.ts
879
888
  var CallTimingRecorder = class {
889
+ constructor(requestLifecycle) {
890
+ this.requestLifecycle = requestLifecycle;
891
+ }
880
892
  fetchTiming;
881
893
  firstTokenAt;
894
+ /** Consumer suspension after pre-content chunks (for example, tool deltas). */
895
+ preFirstTokenConsumerMs = 0;
882
896
  /** Cumulative time awaiting `reader.read()`, i.e. transport, not consumer. */
883
897
  readWaitMs = 0;
884
898
  /** Pass as `timing` in the `fetchWithRetry` options object. */
885
899
  observer = {
900
+ onDispatch: () => {
901
+ this.requestLifecycle?.onDispatch?.();
902
+ },
903
+ onRetry: () => {
904
+ this.requestLifecycle?.onRetry?.();
905
+ },
886
906
  onComplete: (timing) => {
887
907
  this.fetchTiming = timing;
888
908
  }
@@ -904,6 +924,12 @@ var CallTimingRecorder = class {
904
924
  markFirstToken() {
905
925
  this.firstTokenAt ??= Date.now();
906
926
  }
927
+ /** Exclude backpressure after a pre-content yield from first-content latency. */
928
+ recordPreFirstTokenConsumerPause(startedAt) {
929
+ if (this.firstTokenAt === void 0) {
930
+ this.preFirstTokenConsumerMs += Math.max(0, Date.now() - startedAt);
931
+ }
932
+ }
907
933
  /**
908
934
  * Timing for a non-streaming call. Call once the response body is parsed.
909
935
  *
@@ -927,13 +953,17 @@ var CallTimingRecorder = class {
927
953
  const t = this.fetchTiming;
928
954
  if (!t) return void 0;
929
955
  const ttfbMs = Math.max(0, t.headersAt - t.dispatchedAt);
956
+ const firstTokenMs = this.firstTokenAt !== void 0 ? Math.max(0, this.firstTokenAt - t.dispatchedAt - this.preFirstTokenConsumerMs) : void 0;
930
957
  return {
931
958
  queuedMs: t.queuedMs,
932
959
  attempts: t.attempts,
933
960
  retryMs: t.retryMs,
934
961
  ttfbMs,
935
- ...this.firstTokenAt !== void 0 ? { firstTokenMs: Math.max(0, this.firstTokenAt - t.dispatchedAt) } : {},
936
- wireMs: ttfbMs + this.readWaitMs
962
+ ...firstTokenMs !== void 0 ? { firstTokenMs } : {},
963
+ // Parsing and generator scheduling can happen after the first read has
964
+ // settled. Include that pre-first-token gap so the public relationship
965
+ // stays intuitive, without charging pauses after any chunk is yielded.
966
+ wireMs: Math.max(ttfbMs + this.readWaitMs, firstTokenMs ?? 0)
937
967
  };
938
968
  }
939
969
  };
@@ -946,7 +976,9 @@ async function* withCallTiming(recorder, source) {
946
976
  const timing = recorder.streamTiming();
947
977
  yield timing ? { ...chunk, timing } : chunk;
948
978
  } else {
979
+ const yieldedAt = Date.now();
949
980
  yield chunk;
981
+ recorder.recordPreFirstTokenConsumerPause(yieldedAt);
950
982
  }
951
983
  }
952
984
  }
@@ -956,9 +988,6 @@ function withChatTiming(recorder, response) {
956
988
  }
957
989
 
958
990
  // src/providers/rate-limiter.ts
959
- function abortError() {
960
- return Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
961
- }
962
991
  var RateLimiter = class {
963
992
  maxConcurrent;
964
993
  minIntervalMs;
@@ -1007,11 +1036,11 @@ var RateLimiter = class {
1007
1036
  * With `acquireTimeoutMs`, a waiter that sits in the queue too long rejects.
1008
1037
  */
1009
1038
  acquire(signal) {
1010
- if (signal?.aborted) return Promise.reject(abortError());
1039
+ if (signal?.aborted) return Promise.reject(signal.reason);
1011
1040
  return new Promise((resolve2, reject) => {
1012
1041
  const waiter = { resolve: resolve2, reject, signal };
1013
1042
  if (signal) {
1014
- waiter.onAbort = () => this.settleWaiter(waiter, abortError());
1043
+ waiter.onAbort = () => this.settleWaiter(waiter, signal.reason);
1015
1044
  signal.addEventListener("abort", waiter.onAbort, { once: true });
1016
1045
  }
1017
1046
  if (this.acquireTimeoutMs != null) {
@@ -1311,6 +1340,7 @@ function normalizeMessageContent(content) {
1311
1340
  }
1312
1341
  var OpenAICompatibleProvider = class {
1313
1342
  name;
1343
+ reportsRequestLifecycle = true;
1314
1344
  inputCapabilities(model) {
1315
1345
  return this.name === "openrouter" && model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64"] } } : {};
1316
1346
  }
@@ -1407,7 +1437,7 @@ var OpenAICompatibleProvider = class {
1407
1437
  async chat(messages, options) {
1408
1438
  const headers = this.buildHeaders(await this.resolveKey());
1409
1439
  const body = this.buildRequestBody(messages, options, false);
1410
- const recorder = new CallTimingRecorder();
1440
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
1411
1441
  const res = await fetchWithRetry(
1412
1442
  `${this.baseUrl}/chat/completions`,
1413
1443
  {
@@ -1438,7 +1468,7 @@ var OpenAICompatibleProvider = class {
1438
1468
  async *stream(messages, options) {
1439
1469
  const headers = this.buildHeaders(await this.resolveKey());
1440
1470
  const body = this.buildRequestBody(messages, options, true);
1441
- const recorder = new CallTimingRecorder();
1471
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
1442
1472
  const res = await fetchWithRetry(
1443
1473
  `${this.baseUrl}/chat/completions`,
1444
1474
  {
@@ -2346,6 +2376,7 @@ function responseImageParts(parts) {
2346
2376
  }
2347
2377
  var OpenAIResponsesProvider = class {
2348
2378
  name = "openai-responses";
2379
+ reportsRequestLifecycle = true;
2349
2380
  inputCapabilities(model) {
2350
2381
  return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
2351
2382
  }
@@ -2432,7 +2463,7 @@ var OpenAIResponsesProvider = class {
2432
2463
  async chat(messages, options) {
2433
2464
  const headers = this.buildHeaders(await this.resolveKey());
2434
2465
  const body = this.buildRequestBody(messages, options, false);
2435
- const recorder = new CallTimingRecorder();
2466
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
2436
2467
  const res = await fetchWithRetry(
2437
2468
  `${this.baseUrl}/responses`,
2438
2469
  {
@@ -2467,7 +2498,7 @@ var OpenAIResponsesProvider = class {
2467
2498
  async *stream(messages, options) {
2468
2499
  const headers = this.buildHeaders(await this.resolveKey());
2469
2500
  const body = this.buildRequestBody(messages, options, true);
2470
- const recorder = new CallTimingRecorder();
2501
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
2471
2502
  const res = await fetchWithRetry(
2472
2503
  `${this.baseUrl}/responses`,
2473
2504
  {
@@ -3271,6 +3302,7 @@ function mergeAnthropicUsage(initial, update) {
3271
3302
  }
3272
3303
  var AnthropicProvider = class {
3273
3304
  name = "anthropic";
3305
+ reportsRequestLifecycle = true;
3274
3306
  /** Report a clamped `effort`. Anthropic clamps to each model's supported
3275
3307
  * `output_config.effort` levels, and models that always think fall back to
3276
3308
  * adaptive `'low'` when asked to disable thinking. Only reported when the
@@ -3388,7 +3420,7 @@ var AnthropicProvider = class {
3388
3420
  );
3389
3421
  const body = this.buildRequestBody(messages, options, false);
3390
3422
  const pricingContext = pricingContextFromBody(body);
3391
- const recorder = new CallTimingRecorder();
3423
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
3392
3424
  const res = await fetchWithRetry(
3393
3425
  `${this.baseUrl}/messages`,
3394
3426
  {
@@ -3424,7 +3456,7 @@ var AnthropicProvider = class {
3424
3456
  );
3425
3457
  const body = this.buildRequestBody(messages, options, true);
3426
3458
  const pricingContext = pricingContextFromBody(body);
3427
- const recorder = new CallTimingRecorder();
3459
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
3428
3460
  const res = await fetchWithRetry(
3429
3461
  `${this.baseUrl}/messages`,
3430
3462
  {
@@ -4251,6 +4283,7 @@ function resolveGeminiThinking(model, resolved) {
4251
4283
  }
4252
4284
  var GeminiProvider = class {
4253
4285
  name = "google";
4286
+ reportsRequestLifecycle = true;
4254
4287
  inputCapabilities(model) {
4255
4288
  return model.trim().length > 0 ? { image: { sources: ["bytes", "base64", "provider-file"] } } : {};
4256
4289
  }
@@ -4368,7 +4401,7 @@ var GeminiProvider = class {
4368
4401
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4369
4402
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4370
4403
  const headers = this.buildHeaders(await this.resolveKey());
4371
- const recorder = new CallTimingRecorder();
4404
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4372
4405
  const res = await fetchWithRetry(
4373
4406
  `${this.baseUrl}/models/${pricingContext.model}:generateContent`,
4374
4407
  {
@@ -4406,7 +4439,7 @@ var GeminiProvider = class {
4406
4439
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4407
4440
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4408
4441
  const headers = this.buildHeaders(await this.resolveKey());
4409
- const recorder = new CallTimingRecorder();
4442
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4410
4443
  const res = await fetchWithRetry(
4411
4444
  `${this.baseUrl}/models/${pricingContext.model}:streamGenerateContent?alt=sse`,
4412
4445
  {
@@ -4443,7 +4476,7 @@ var GeminiProvider = class {
4443
4476
  this.assertSafeInteractionOptions(options);
4444
4477
  const body = this.buildInteractionRequestBody(messages, options, false);
4445
4478
  const headers = this.buildHeaders(await this.resolveKey());
4446
- const recorder = new CallTimingRecorder();
4479
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4447
4480
  const res = await fetchWithRetry(
4448
4481
  `${this.baseUrl}/interactions`,
4449
4482
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
@@ -4469,7 +4502,7 @@ var GeminiProvider = class {
4469
4502
  this.assertSafeInteractionOptions(options);
4470
4503
  const body = this.buildInteractionRequestBody(messages, options, true);
4471
4504
  const headers = this.buildHeaders(await this.resolveKey());
4472
- const recorder = new CallTimingRecorder();
4505
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4473
4506
  const res = await fetchWithRetry(
4474
4507
  `${this.baseUrl}/interactions?alt=sse`,
4475
4508
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
@@ -6188,7 +6221,7 @@ var GEMINI_AUDIO_MEDIA_TYPES = /* @__PURE__ */ new Set([
6188
6221
  "audio/mulaw",
6189
6222
  "audio/webm"
6190
6223
  ]);
6191
- function abortError2() {
6224
+ function abortError() {
6192
6225
  return new DOMException("The transcription operation was aborted", "AbortError");
6193
6226
  }
6194
6227
  function audioMime(audio) {
@@ -6220,13 +6253,13 @@ function secondsOffset(value) {
6220
6253
  function sleep3(ms, signal) {
6221
6254
  return new Promise((resolve2, reject) => {
6222
6255
  if (signal?.aborted) {
6223
- reject(abortError2());
6256
+ reject(abortError());
6224
6257
  return;
6225
6258
  }
6226
6259
  const timer = setTimeout(done, ms);
6227
6260
  const onAbort = () => {
6228
6261
  clearTimeout(timer);
6229
- done(abortError2());
6262
+ done(abortError());
6230
6263
  };
6231
6264
  function done(error) {
6232
6265
  signal?.removeEventListener("abort", onAbort);
@@ -7547,7 +7580,7 @@ function isCostBearingLeaf(event) {
7547
7580
  return COST_LEAF_SET.has(event.type);
7548
7581
  }
7549
7582
  function isUnpricedLeaf(event) {
7550
- return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && hasPositiveBillableWork(event);
7583
+ return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && (event.unpriced === true || hasPositiveBillableWork(event));
7551
7584
  }
7552
7585
  function isRootLevel(event) {
7553
7586
  const d = event.depth;
@@ -8833,18 +8866,27 @@ function parseDuration(duration) {
8833
8866
  if (!match) throw new Error(`Invalid duration format: "${duration}"`);
8834
8867
  const value = parseFloat(match[1]);
8835
8868
  const unit = match[2];
8869
+ let milliseconds;
8836
8870
  switch (unit) {
8837
8871
  case "ms":
8838
- return value;
8872
+ milliseconds = value;
8873
+ break;
8839
8874
  case "s":
8840
- return value * 1e3;
8875
+ milliseconds = value * 1e3;
8876
+ break;
8841
8877
  case "m":
8842
- return value * 6e4;
8878
+ milliseconds = value * 6e4;
8879
+ break;
8843
8880
  case "h":
8844
- return value * 36e5;
8881
+ milliseconds = value * 36e5;
8882
+ break;
8845
8883
  default:
8846
8884
  throw new Error(`Unknown duration unit: "${unit}"`);
8847
8885
  }
8886
+ if (!Number.isFinite(milliseconds)) {
8887
+ throw new Error(`Duration is too large: "${duration}"`);
8888
+ }
8889
+ return milliseconds;
8848
8890
  }
8849
8891
  function parseCost(cost) {
8850
8892
  const match = cost.match(/^\$?([\d.]+)$/);
@@ -9500,6 +9542,42 @@ function recordToolSpanOutcome(span, outcome) {
9500
9542
 
9501
9543
  // src/context.ts
9502
9544
  var signalStorage = new AsyncLocalStorage();
9545
+ var hardSignalStorage = new AsyncLocalStorage();
9546
+ var timeoutTrackerStorage = new AsyncLocalStorage();
9547
+ var MAX_TIMER_DELAY_MS = 2147483647;
9548
+ function composeAbortSignals(...signals) {
9549
+ const active = signals.filter((signal) => signal !== void 0);
9550
+ if (active.length === 0) return void 0;
9551
+ if (active.length === 1) return active[0];
9552
+ return AbortSignal.any(active);
9553
+ }
9554
+ function raceWithAbortSignal(promise, signal) {
9555
+ if (!signal) return promise;
9556
+ if (signal.aborted) {
9557
+ void promise.catch(() => {
9558
+ });
9559
+ return Promise.reject(signal.reason);
9560
+ }
9561
+ return new Promise((resolve2, reject) => {
9562
+ let settled = false;
9563
+ const finish = (callback) => {
9564
+ if (settled) return;
9565
+ settled = true;
9566
+ signal.removeEventListener("abort", onAbort);
9567
+ callback();
9568
+ };
9569
+ const onAbort = () => finish(() => reject(signal.reason));
9570
+ signal.addEventListener("abort", onAbort, { once: true });
9571
+ if (signal.aborted) onAbort();
9572
+ promise.then(
9573
+ (value) => finish(() => resolve2(value)),
9574
+ (error) => finish(() => reject(error))
9575
+ );
9576
+ });
9577
+ }
9578
+ function pausedHumanWaitMs(tracker, now = Date.now()) {
9579
+ return tracker.humanWaitMs + (tracker.activeHumanWaits > 0 && tracker.humanWaitStartedAt !== void 0 ? now - tracker.humanWaitStartedAt : 0);
9580
+ }
9503
9581
  var DEFAULT_BRANCH_DRAIN_TIMEOUT_MS = 5e3;
9504
9582
  function resolveBranchDrainTimeoutMs(value) {
9505
9583
  const resolved = value === void 0 ? DEFAULT_BRANCH_DRAIN_TIMEOUT_MS : value;
@@ -10301,7 +10379,10 @@ var WorkflowContext = class _WorkflowContext {
10301
10379
  tool_call_id: parsed.callId
10302
10380
  });
10303
10381
  }
10304
- if (settlement.abortError !== void 0) throw settlement.abortError;
10382
+ if (settlement.abortError !== void 0) {
10383
+ signal?.throwIfAborted();
10384
+ throw settlement.abortError;
10385
+ }
10305
10386
  }
10306
10387
  // ── ctx.ask() ─────────────────────────────────────────────────────────
10307
10388
  async ask(agent2, prompt, options) {
@@ -10317,7 +10398,10 @@ var WorkflowContext = class _WorkflowContext {
10317
10398
  const promptText = inputText(normalizedInput);
10318
10399
  const inputDescriptor = describeModelInput(normalizedInput);
10319
10400
  const agentName = agent2._name;
10320
- return this._checkpoint(
10401
+ const inheritedHardSignal = hardSignalStorage.getStore() ?? this.signal;
10402
+ const hardAskSignal = composeAbortSignals(inheritedHardSignal, options?.signal);
10403
+ const askSignal = composeAbortSignals(this.currentSignal, options?.signal);
10404
+ const runAsk = () => this._checkpoint(
10321
10405
  this._autoCheckpointName("ask", agentName),
10322
10406
  async () => {
10323
10407
  const parentFrame = askStorage.getStore();
@@ -10472,8 +10556,25 @@ var WorkflowContext = class _WorkflowContext {
10472
10556
  },
10473
10557
  { agent: agentName }
10474
10558
  );
10559
+ const runWithAskSignal = () => askSignal ? signalStorage.run(askSignal, runAsk) : runAsk();
10560
+ return hardAskSignal ? hardSignalStorage.run(hardAskSignal, runWithAskSignal) : runWithAskSignal();
10475
10561
  }
10476
10562
  async executeAgentCall(agent2, input, options, handoffMessages, usageCapture, sessionHistory = normalizeSessionHistory(this.sessionHistory)) {
10563
+ const tracker = { humanWaitMs: 0, activeHumanWaits: 0 };
10564
+ const enclosing = timeoutTrackerStorage.getStore() ?? [];
10565
+ return timeoutTrackerStorage.run(
10566
+ [...enclosing, tracker],
10567
+ () => this.executeAgentCallImpl(
10568
+ agent2,
10569
+ input,
10570
+ options,
10571
+ handoffMessages,
10572
+ usageCapture,
10573
+ sessionHistory
10574
+ )
10575
+ );
10576
+ }
10577
+ async executeAgentCallImpl(agent2, input, options, handoffMessages, usageCapture, sessionHistory = normalizeSessionHistory(this.sessionHistory)) {
10477
10578
  if (this.budgetContext?.exceeded) {
10478
10579
  const { limit, totalCost: spent, policy } = this.budgetContext;
10479
10580
  if (policy === "warn") {
@@ -10713,8 +10814,18 @@ ${appendedSchemaText}`;
10713
10814
  }
10714
10815
  }
10715
10816
  const maxTurns = agent2._config.maxTurns ?? 25;
10716
- const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
10817
+ const timeoutMs = parseDuration(
10818
+ options?.timeout ?? agent2._config.timeout ?? this.config.defaults?.timeout ?? "60s"
10819
+ );
10820
+ const stallTimeout = options?.stallTimeout ?? agent2._config.stallTimeout ?? this.config.defaults?.stallTimeout;
10821
+ const stallTimeoutMs = stallTimeout === void 0 ? void 0 : parseDuration(stallTimeout);
10822
+ if (stallTimeoutMs !== void 0 && stallTimeoutMs > MAX_TIMER_DELAY_MS) {
10823
+ throw new RangeError(
10824
+ `stallTimeout exceeds the maximum supported timer duration of ${MAX_TIMER_DELAY_MS}ms`
10825
+ );
10826
+ }
10717
10827
  const startTime = Date.now();
10828
+ const timeoutTracker = timeoutTrackerStorage.getStore()?.at(-1);
10718
10829
  const timingTotals = { turns: 0, queuedMs: 0, retryMs: 0, wireMs: 0 };
10719
10830
  const currentMessages = [...messages];
10720
10831
  let turns = 0;
@@ -10729,7 +10840,8 @@ ${appendedSchemaText}`;
10729
10840
  let lastStartMaxAttempts = 1;
10730
10841
  while (turns < maxTurns) {
10731
10842
  this.currentSignal?.throwIfAborted();
10732
- const elapsedMs = Date.now() - startTime;
10843
+ const now = Date.now();
10844
+ const elapsedMs = now - startTime - (timeoutTracker ? pausedHumanWaitMs(timeoutTracker, now) : 0);
10733
10845
  if (elapsedMs > timeoutMs) {
10734
10846
  throw new TimeoutError(
10735
10847
  "ctx.ask()",
@@ -10743,7 +10855,8 @@ ${appendedSchemaText}`;
10743
10855
  0,
10744
10856
  elapsedMs - timingTotals.queuedMs - timingTotals.retryMs - timingTotals.wireMs
10745
10857
  )
10746
- } : void 0
10858
+ } : void 0,
10859
+ agent2._name
10747
10860
  );
10748
10861
  }
10749
10862
  turns++;
@@ -10790,6 +10903,43 @@ ${appendedSchemaText}`;
10790
10903
  providerOptions,
10791
10904
  signal: this.currentSignal
10792
10905
  };
10906
+ const requestSignal = chatOptions.signal;
10907
+ const hardRequestSignal = hardSignalStorage.getStore() ?? this.signal;
10908
+ const stallController = stallTimeoutMs === void 0 ? void 0 : new AbortController();
10909
+ const stallReason = stallController === void 0 ? void 0 : new DOMException("Provider request stalled", "StallTimeout");
10910
+ let stallTimer;
10911
+ let providerCallActive = true;
10912
+ let requestDispatched = false;
10913
+ const clearStallTimer = () => {
10914
+ if (stallTimer !== void 0) {
10915
+ clearTimeout(stallTimer);
10916
+ stallTimer = void 0;
10917
+ }
10918
+ };
10919
+ const armStallTimer = () => {
10920
+ if (!providerCallActive || !stallController || !stallReason || stallTimeoutMs === void 0)
10921
+ return;
10922
+ requestDispatched = true;
10923
+ clearStallTimer();
10924
+ if (stallController.signal.aborted) return;
10925
+ stallTimer = setTimeout(() => stallController.abort(stallReason), stallTimeoutMs);
10926
+ };
10927
+ if (stallController) {
10928
+ chatOptions.signal = composeAbortSignals(requestSignal, stallController.signal);
10929
+ chatOptions.requestLifecycle = {
10930
+ onDispatch: armStallTimer,
10931
+ onRetry: clearStallTimer
10932
+ };
10933
+ }
10934
+ const providerSettlementSignal = composeAbortSignals(
10935
+ hardRequestSignal,
10936
+ stallController?.signal
10937
+ );
10938
+ const throwIfRequestStalled = () => {
10939
+ if (stallReason !== void 0 && providerSettlementSignal?.reason === stallReason) {
10940
+ throw new StallTimeoutError("ctx.ask()", stallTimeoutMs, agent2._name);
10941
+ }
10942
+ };
10793
10943
  if (options?.schema && toolDefs.length === 0) {
10794
10944
  const nativeOut = options?.nativeStructuredOutput ?? agent2._config.nativeStructuredOutput ?? false;
10795
10945
  chatOptions.responseFormat = nativeOut ? {
@@ -10876,55 +11026,77 @@ ${appendedSchemaText}`;
10876
11026
  });
10877
11027
  }
10878
11028
  }) : void 0;
10879
- for await (const chunk of provider.stream(
11029
+ if (stallController && provider.reportsRequestLifecycle !== true) armStallTimer();
11030
+ const providerStream = provider.stream(
10880
11031
  cloneMessagesForProvider(currentMessages),
10881
11032
  chatOptions
10882
- )) {
10883
- if (chunk.type === "text_delta") {
10884
- content2 += chunk.content;
10885
- this.emitEvent({ type: "token", data: chunk.content });
10886
- if (walker) {
10887
- walker.processChunk(chunk.content);
10888
- if (walker.consumeBoundary()) {
10889
- let parsed;
10890
- try {
10891
- parsed = parsePartialJson(extractJson(content2));
10892
- } catch {
10893
- parsed = void 0;
10894
- }
10895
- if (parsed !== void 0) {
10896
- this.emitEvent({
10897
- type: "partial_object",
10898
- agent: agent2._name,
10899
- attempt: currentAttempt,
10900
- data: { object: parsed }
10901
- });
11033
+ );
11034
+ const iterator = providerStream[Symbol.asyncIterator]();
11035
+ try {
11036
+ while (true) {
11037
+ const next = await raceWithAbortSignal(
11038
+ Promise.resolve(iterator.next()),
11039
+ providerSettlementSignal
11040
+ );
11041
+ if (next.done) break;
11042
+ const chunk = next.value;
11043
+ if (stallController) armStallTimer();
11044
+ if (chunk.type === "text_delta") {
11045
+ content2 += chunk.content;
11046
+ this.emitEvent({ type: "token", data: chunk.content });
11047
+ if (walker) {
11048
+ walker.processChunk(chunk.content);
11049
+ if (walker.consumeBoundary()) {
11050
+ let parsed;
11051
+ try {
11052
+ parsed = parsePartialJson(extractJson(content2));
11053
+ } catch {
11054
+ parsed = void 0;
11055
+ }
11056
+ if (parsed !== void 0) {
11057
+ this.emitEvent({
11058
+ type: "partial_object",
11059
+ agent: agent2._name,
11060
+ attempt: currentAttempt,
11061
+ data: { object: parsed }
11062
+ });
11063
+ }
10902
11064
  }
10903
11065
  }
10904
- }
10905
- } else if (chunk.type === "thinking_delta") {
10906
- thinkingContent += chunk.content;
10907
- } else if (chunk.type === "tool_call_delta") {
10908
- let buffer = toolCallBuffers.get(chunk.id);
10909
- if (!buffer) {
10910
- buffer = { id: chunk.id, name: "", arguments: "" };
10911
- toolCallBuffers.set(chunk.id, buffer);
10912
- }
10913
- if (chunk.name) buffer.name = chunk.name;
10914
- if (chunk.arguments) buffer.arguments += chunk.arguments;
10915
- } else if (chunk.type === "done") {
10916
- streamProviderMetadata = chunk.providerMetadata;
10917
- streamTiming = chunk.timing;
10918
- if (chunk.usage) {
11066
+ } else if (chunk.type === "thinking_delta") {
11067
+ thinkingContent += chunk.content;
11068
+ } else if (chunk.type === "tool_call_delta") {
11069
+ let buffer = toolCallBuffers.get(chunk.id);
11070
+ if (!buffer) {
11071
+ buffer = { id: chunk.id, name: "", arguments: "" };
11072
+ toolCallBuffers.set(chunk.id, buffer);
11073
+ }
11074
+ if (chunk.name) buffer.name = chunk.name;
11075
+ if (chunk.arguments) buffer.arguments += chunk.arguments;
11076
+ } else if (chunk.type === "done") {
11077
+ streamProviderMetadata = chunk.providerMetadata;
11078
+ streamTiming = chunk.timing;
10919
11079
  response = {
10920
11080
  content: content2,
10921
11081
  tool_calls: void 0,
10922
11082
  usage: chunk.usage,
10923
- cost: chunk.cost
11083
+ cost: chunk.cost,
11084
+ providerMetadata: chunk.providerMetadata,
11085
+ timing: chunk.timing
10924
11086
  };
10925
11087
  }
10926
11088
  }
11089
+ } catch (error) {
11090
+ if (iterator.return) {
11091
+ try {
11092
+ void Promise.resolve(iterator.return(void 0)).catch(() => {
11093
+ });
11094
+ } catch {
11095
+ }
11096
+ }
11097
+ throw error;
10927
11098
  }
11099
+ throwIfRequestStalled();
10928
11100
  for (const buffer of toolCallBuffers.values()) {
10929
11101
  toolCalls.push({
10930
11102
  id: buffer.id,
@@ -10949,16 +11121,27 @@ ${appendedSchemaText}`;
10949
11121
  response.thinking_content = thinkingContent;
10950
11122
  }
10951
11123
  } else {
10952
- response = await provider.chat(cloneMessagesForProvider(currentMessages), chatOptions);
11124
+ if (stallController && provider.reportsRequestLifecycle !== true) armStallTimer();
11125
+ response = await raceWithAbortSignal(
11126
+ provider.chat(cloneMessagesForProvider(currentMessages), chatOptions),
11127
+ providerSettlementSignal
11128
+ );
11129
+ throwIfRequestStalled();
10953
11130
  }
10954
11131
  } catch (err) {
10955
- if (richRequest) this.recordRichFailure(err);
10956
- const failureTiming = err instanceof ProviderError ? err.timing : void 0;
11132
+ const callError = stallReason !== void 0 && providerSettlementSignal?.reason === stallReason ? new StallTimeoutError("ctx.ask()", stallTimeoutMs, agent2._name) : err;
11133
+ if (richRequest) this.recordRichFailure(callError);
11134
+ const failureTiming = callError instanceof ProviderError ? callError.timing : void 0;
10957
11135
  this.emitEvent({
10958
11136
  type: "agent_call_end",
10959
11137
  agent: agent2._name,
10960
11138
  model: effectiveModelUri,
10961
11139
  promptVersion: agent2._config.version,
11140
+ // A dispatched request abandoned at the runtime boundary may still
11141
+ // be billed even though a non-cooperative provider supplied no
11142
+ // usage. Preserve budget honesty by marking the known total as a
11143
+ // lower bound rather than presenting an exact $0.
11144
+ ...callError instanceof StallTimeoutError && requestDispatched ? { unpriced: true } : {},
10962
11145
  duration: Date.now() - turnStart,
10963
11146
  // Top-level beside `duration`, exactly as on the success path.
10964
11147
  ...failureTiming ? { timing: failureTiming } : {},
@@ -10966,14 +11149,17 @@ ${appendedSchemaText}`;
10966
11149
  response: "",
10967
11150
  turn: turns,
10968
11151
  ...retryReason ? { retryReason } : {},
10969
- error: this._observerErrorProjection(err)?.message ?? legacyErrorMessage(err),
11152
+ error: this._observerErrorProjection(callError)?.message ?? legacyErrorMessage(callError),
10970
11153
  // Enrich with typed-error metadata when available. `body` is
10971
11154
  // deliberately NOT emitted — it's redaction-eligible (see
10972
11155
  // docs/security.md). `data.error` already carries the message.
10973
- ...providerErrorEventMetadata(err, richRequest)
11156
+ ...providerErrorEventMetadata(callError, richRequest)
10974
11157
  }
10975
11158
  });
10976
- throw err;
11159
+ throw callError;
11160
+ } finally {
11161
+ providerCallActive = false;
11162
+ clearStallTimer();
10977
11163
  }
10978
11164
  if (usageCapture && response.usage) {
10979
11165
  usageCapture.value = response.usage;
@@ -12498,46 +12684,52 @@ ${summaryResponse.content}`
12498
12684
  }
12499
12685
  // ── ctx.awaitHuman() ──────────────────────────────────────────────────
12500
12686
  async awaitHuman(options) {
12501
- if (this.spanManager) {
12502
- return this.spanManager.withSpanAsync(
12503
- "axl.ctx.awaitHuman",
12504
- {
12505
- "axl.awaitHuman.channel": options.channel
12506
- },
12507
- async (span) => {
12508
- const start = Date.now();
12509
- const result = await this._awaitHumanImpl(options);
12510
- span.setAttribute("axl.awaitHuman.wait_duration", Date.now() - start);
12511
- span.setAttribute("axl.awaitHuman.approved", result.approved);
12512
- return result;
12687
+ const trackers = timeoutTrackerStorage.getStore();
12688
+ const startedAt = Date.now();
12689
+ for (const tracker of trackers ?? []) {
12690
+ if (tracker.activeHumanWaits++ === 0) tracker.humanWaitStartedAt = startedAt;
12691
+ }
12692
+ try {
12693
+ if (this.spanManager) {
12694
+ return await this.spanManager.withSpanAsync(
12695
+ "axl.ctx.awaitHuman",
12696
+ {
12697
+ "axl.awaitHuman.channel": options.channel
12698
+ },
12699
+ async (span) => {
12700
+ const start = Date.now();
12701
+ const result = await this._awaitHumanImpl(options);
12702
+ span.setAttribute("axl.awaitHuman.wait_duration", Date.now() - start);
12703
+ span.setAttribute("axl.awaitHuman.approved", result.approved);
12704
+ return result;
12705
+ }
12706
+ );
12707
+ }
12708
+ return await this._awaitHumanImpl(options);
12709
+ } finally {
12710
+ const finishedAt = Date.now();
12711
+ for (const tracker of trackers ?? []) {
12712
+ tracker.activeHumanWaits--;
12713
+ if (tracker.activeHumanWaits === 0 && tracker.humanWaitStartedAt !== void 0) {
12714
+ tracker.humanWaitMs += finishedAt - tracker.humanWaitStartedAt;
12715
+ tracker.humanWaitStartedAt = void 0;
12513
12716
  }
12514
- );
12717
+ }
12515
12718
  }
12516
- return this._awaitHumanImpl(options);
12517
12719
  }
12518
12720
  async _awaitHumanImpl(options) {
12519
12721
  const branchSignal = this.currentSignal;
12520
- const makeAbortError = (reason) => {
12521
- if (typeof DOMException !== "undefined") {
12522
- return new DOMException(
12523
- typeof reason === "string" ? reason : "awaitHuman aborted",
12524
- "AbortError"
12525
- );
12526
- }
12527
- const err = new Error(typeof reason === "string" ? reason : "awaitHuman aborted");
12528
- err.name = "AbortError";
12529
- return err;
12530
- };
12531
12722
  const awaitWithAbort = async (promise, signal2 = branchSignal) => {
12532
12723
  if (!signal2) return promise;
12533
- if (signal2.aborted) throw makeAbortError(signal2.reason);
12724
+ signal2.throwIfAborted();
12534
12725
  let onAbort;
12535
12726
  try {
12536
12727
  return await Promise.race([
12537
12728
  promise,
12538
12729
  new Promise((_, reject) => {
12539
- onAbort = () => reject(makeAbortError(signal2.reason));
12730
+ onAbort = () => reject(signal2.reason);
12540
12731
  signal2.addEventListener("abort", onAbort, { once: true });
12732
+ if (signal2.aborted) onAbort();
12541
12733
  })
12542
12734
  ]);
12543
12735
  } finally {
@@ -12545,12 +12737,12 @@ ${summaryResponse.content}`
12545
12737
  }
12546
12738
  };
12547
12739
  if (this.awaitHumanHandler) {
12548
- if (branchSignal?.aborted) throw makeAbortError(branchSignal.reason);
12740
+ branchSignal?.throwIfAborted();
12549
12741
  this.emitEvent({
12550
12742
  type: "await_human",
12551
12743
  data: { channel: options.channel, prompt: options.prompt }
12552
12744
  });
12553
- if (branchSignal?.aborted) throw makeAbortError(branchSignal.reason);
12745
+ branchSignal?.throwIfAborted();
12554
12746
  const decision2 = parseHumanDecision(
12555
12747
  await awaitWithAbort(Promise.resolve(this.awaitHumanHandler(options)))
12556
12748
  );
@@ -12605,12 +12797,12 @@ ${summaryResponse.content}`
12605
12797
  const abortListener = () => {
12606
12798
  registeredResolver.cancelled = true;
12607
12799
  markReady();
12608
- rejectDecision(makeAbortError(signal.reason));
12800
+ rejectDecision(signal.reason);
12609
12801
  };
12610
12802
  signal.addEventListener("abort", abortListener, { once: true });
12611
12803
  let decision;
12612
12804
  try {
12613
- if (signal.aborted) throw makeAbortError(signal.reason);
12805
+ signal.throwIfAborted();
12614
12806
  if (this.stateStore) {
12615
12807
  saveAttempted = true;
12616
12808
  await this.stateStore.savePendingDecision(this.executionId, {
@@ -12621,7 +12813,7 @@ ${summaryResponse.content}`
12621
12813
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
12622
12814
  });
12623
12815
  }
12624
- if (signal.aborted) throw makeAbortError(signal.reason);
12816
+ signal.throwIfAborted();
12625
12817
  this.emitEvent({
12626
12818
  type: "await_human",
12627
12819
  data: { channel: options.channel, prompt: options.prompt }
@@ -13091,7 +13283,7 @@ ${summaryResponse.content}`
13091
13283
  data: partial.data
13092
13284
  });
13093
13285
  };
13094
- const abortError3 = () => new DOMException("The transcription operation was aborted", "AbortError");
13286
+ const abortError2 = () => new DOMException("The transcription operation was aborted", "AbortError");
13095
13287
  const cleanupStatus = (value) => value === "not_required" || value === "deleted" || value === "failed" || value === "timed_out" ? value : void 0;
13096
13288
  const safeProviderFailure = (error, provider, model) => {
13097
13289
  const details = error && typeof error === "object" ? error : void 0;
@@ -13142,7 +13334,7 @@ ${summaryResponse.content}`
13142
13334
  hasWork: false
13143
13335
  })
13144
13336
  });
13145
- throw abortError3();
13337
+ throw abortError2();
13146
13338
  }
13147
13339
  if (this.budgetContext?.exceeded) {
13148
13340
  const { limit, totalCost: spent, policy } = this.budgetContext;
@@ -13208,7 +13400,7 @@ ${summaryResponse.content}`
13208
13400
  ...failure.cleanup ? [{ cleanupStatus: failure.cleanup }] : []
13209
13401
  )
13210
13402
  });
13211
- throw abortError3();
13403
+ throw abortError2();
13212
13404
  }
13213
13405
  if (failure.accounting.cost !== void 0)
13214
13406
  this._accumulateBudgetCost(failure.accounting.cost);
@@ -13251,7 +13443,7 @@ ${summaryResponse.content}`
13251
13443
  ...cleanupStatus(result.cleanupStatus) ? [{ cleanupStatus: cleanupStatus(result.cleanupStatus) }] : []
13252
13444
  )
13253
13445
  });
13254
- throw abortError3();
13446
+ throw abortError2();
13255
13447
  }
13256
13448
  let transcript;
13257
13449
  try {
@@ -13337,9 +13529,9 @@ ${summaryResponse.content}`
13337
13529
  const cost = partial.cost;
13338
13530
  if (COST_BEARING_LEAF_TYPES.includes(partial.type)) {
13339
13531
  const usable = isUsableCost(cost);
13340
- const unpriced = !usable && hasPositiveBillableWork(
13532
+ const unpriced = !usable && (partial.unpriced === true || hasPositiveBillableWork(
13341
13533
  partial
13342
- );
13534
+ ));
13343
13535
  if (frame) {
13344
13536
  if (usable) frame.askCost.value += cost;
13345
13537
  else if (unpriced) frame.askUnpriced = true;
@@ -13350,7 +13542,7 @@ ${summaryResponse.content}`
13350
13542
  if (!this.budgetContext.unpricedWarned && Number.isFinite(this.budgetContext.limit)) {
13351
13543
  this.budgetContext.unpricedWarned = true;
13352
13544
  console.warn(
13353
- "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."
13545
+ "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."
13354
13546
  );
13355
13547
  }
13356
13548
  }
@@ -15319,12 +15511,13 @@ function forwardAbortSignal(external, internal) {
15319
15511
  if (!external) return () => {
15320
15512
  };
15321
15513
  if (external.aborted) {
15322
- internal.abort();
15514
+ internal.abort(external.reason);
15323
15515
  return () => {
15324
15516
  };
15325
15517
  }
15326
- const onAbort = () => internal.abort();
15518
+ const onAbort = () => internal.abort(external.reason);
15327
15519
  external.addEventListener("abort", onAbort, { once: true });
15520
+ if (external.aborted) onAbort();
15328
15521
  return () => external.removeEventListener("abort", onAbort);
15329
15522
  }
15330
15523
  var AxlRuntime = class extends EventEmitter2 {
@@ -17805,6 +17998,7 @@ export {
17805
17998
  SQLiteStore,
17806
17999
  Session,
17807
18000
  SqliteVectorStore,
18001
+ StallTimeoutError,
17808
18002
  ThinkTagScanner,
17809
18003
  TimeoutError,
17810
18004
  ToolFailure,