@axlsdk/axl 0.22.3 → 0.23.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.js CHANGED
@@ -170,9 +170,22 @@ var NoConsensus = class extends AxlError {
170
170
  }
171
171
  };
172
172
  var TimeoutError = class extends AxlError {
173
- constructor(operation, timeoutMs) {
174
- super("TIMEOUT", `${operation} exceeded timeout of ${timeoutMs}ms`);
173
+ /** Latency attribution for the timed-out operation, when measurable. */
174
+ breakdown;
175
+ constructor(operation, timeoutMs, breakdown, agent2) {
176
+ super(
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)` : "") + (agent2 ? ` for agent '${agent2}'` : "")
179
+ );
175
180
  this.name = "TimeoutError";
181
+ if (breakdown) this.breakdown = breakdown;
182
+ }
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}'` : "");
176
189
  }
177
190
  };
178
191
  function formatBudgetCost(cost) {
@@ -719,6 +732,7 @@ var ProviderError = class extends AxlError {
719
732
  this.retryAfterMs = args.retryAfterMs;
720
733
  this.requestId = args.requestId;
721
734
  this.body = args.body;
735
+ if (args.timing) this.timing = args.timing;
722
736
  }
723
737
  };
724
738
  function isRetryableStatus(status) {
@@ -770,7 +784,8 @@ function buildProviderError(args) {
770
784
  message: args.message,
771
785
  retryAfterMs: args.headers ? parseRetryAfter(args.headers) : void 0,
772
786
  requestId: args.headers ? extractRequestId(args.headers) : void 0,
773
- body: args.body
787
+ body: args.body,
788
+ timing: args.timing
774
789
  });
775
790
  }
776
791
 
@@ -807,14 +822,33 @@ async function fetchWithRetry(input, init, opts) {
807
822
  const maxRetries = opts?.maxRetries ?? MAX_RETRIES;
808
823
  const governor = opts?.governor;
809
824
  const provider = opts?.provider ?? "unknown";
825
+ const observer = opts?.timing;
810
826
  let acquired = false;
827
+ let queuedMs = 0;
811
828
  if (governor) {
829
+ const acquireStart = Date.now();
812
830
  await governor.acquire(init?.signal ?? void 0);
813
831
  acquired = true;
814
- }
832
+ queuedMs = Date.now() - acquireStart;
833
+ }
834
+ let firstDispatchedAt = 0;
835
+ let dispatchedAt = 0;
836
+ let headersAt = 0;
837
+ const reportComplete = (attempts) => {
838
+ observer?.onComplete?.({
839
+ queuedMs,
840
+ attempts,
841
+ retryMs: dispatchedAt - firstDispatchedAt,
842
+ dispatchedAt,
843
+ headersAt
844
+ });
845
+ };
815
846
  try {
816
847
  for (let attempt = 0; ; attempt++) {
817
848
  let res;
849
+ dispatchedAt = Date.now();
850
+ if (attempt === 0) firstDispatchedAt = dispatchedAt;
851
+ observer?.onDispatch?.(attempt + 1, dispatchedAt);
818
852
  try {
819
853
  res = await fetch(input, { ...init, redirect: "manual" });
820
854
  } catch (err) {
@@ -826,18 +860,23 @@ async function fetchWithRetry(input, init, opts) {
826
860
  message: err instanceof Error ? err.message : String(err)
827
861
  });
828
862
  }
863
+ observer?.onRetry?.(attempt + 1, Date.now());
829
864
  await sleep2(jitter(BASE_DELAY_MS * 2 ** attempt), init?.signal ?? void 0);
830
865
  continue;
831
866
  }
867
+ headersAt = Date.now();
832
868
  governor?.observe(res);
833
869
  if (res.ok || !RETRYABLE_STATUS_CODES.has(res.status) || attempt >= maxRetries) {
870
+ reportComplete(attempt + 1);
834
871
  return res;
835
872
  }
836
873
  if (init?.signal?.aborted) {
874
+ reportComplete(attempt + 1);
837
875
  return res;
838
876
  }
839
877
  const retryAfterMs = parseRetryAfter(res.headers);
840
878
  const baseDelay = retryAfterMs !== void 0 ? Math.min(retryAfterMs, MAX_BACKOFF_MS) : BASE_DELAY_MS * 2 ** attempt;
879
+ observer?.onRetry?.(attempt + 1, Date.now());
841
880
  await sleep2(jitter(baseDelay), init?.signal ?? void 0);
842
881
  }
843
882
  } finally {
@@ -845,10 +884,110 @@ async function fetchWithRetry(input, init, opts) {
845
884
  }
846
885
  }
847
886
 
848
- // src/providers/rate-limiter.ts
849
- function abortError() {
850
- return Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
887
+ // src/providers/call-timing.ts
888
+ var CallTimingRecorder = class {
889
+ constructor(requestLifecycle) {
890
+ this.requestLifecycle = requestLifecycle;
891
+ }
892
+ fetchTiming;
893
+ firstTokenAt;
894
+ /** Consumer suspension after pre-content chunks (for example, tool deltas). */
895
+ preFirstTokenConsumerMs = 0;
896
+ /** Cumulative time awaiting `reader.read()`, i.e. transport, not consumer. */
897
+ readWaitMs = 0;
898
+ /** Pass as `timing` in the `fetchWithRetry` options object. */
899
+ observer = {
900
+ onDispatch: () => {
901
+ this.requestLifecycle?.onDispatch?.();
902
+ },
903
+ onRetry: () => {
904
+ this.requestLifecycle?.onRetry?.();
905
+ },
906
+ onComplete: (timing) => {
907
+ this.fetchTiming = timing;
908
+ }
909
+ };
910
+ /**
911
+ * Await one body read, charging only the wait itself to `wireMs`. Time the
912
+ * consumer spends between reads is excluded because it elapses outside this
913
+ * call.
914
+ */
915
+ async read(reader) {
916
+ const start = Date.now();
917
+ try {
918
+ return await reader.read();
919
+ } finally {
920
+ this.readWaitMs += Date.now() - start;
921
+ }
922
+ }
923
+ /** Stamp the first content delta. Idempotent — only the first call counts. */
924
+ markFirstToken() {
925
+ this.firstTokenAt ??= Date.now();
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
+ }
933
+ /**
934
+ * Timing for a non-streaming call. Call once the response body is parsed.
935
+ *
936
+ * Also the right reading at an adapter's `!res.ok` throw site on BOTH
937
+ * transports: headers have arrived, no body will be streamed, so
938
+ * dispatch → now is the whole of what the provider cost us.
939
+ */
940
+ chatTiming() {
941
+ const t = this.fetchTiming;
942
+ if (!t) return void 0;
943
+ return {
944
+ queuedMs: t.queuedMs,
945
+ attempts: t.attempts,
946
+ retryMs: t.retryMs,
947
+ ttfbMs: Math.max(0, t.headersAt - t.dispatchedAt),
948
+ wireMs: Math.max(0, Date.now() - t.dispatchedAt)
949
+ };
950
+ }
951
+ /** Timing for a streamed call. Call when building the terminal `done` chunk. */
952
+ streamTiming() {
953
+ const t = this.fetchTiming;
954
+ if (!t) return void 0;
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;
957
+ return {
958
+ queuedMs: t.queuedMs,
959
+ attempts: t.attempts,
960
+ retryMs: t.retryMs,
961
+ ttfbMs,
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)
967
+ };
968
+ }
969
+ };
970
+ async function* withCallTiming(recorder, source) {
971
+ for await (const chunk of source) {
972
+ if (chunk.type === "text_delta" || chunk.type === "thinking_delta") {
973
+ recorder.markFirstToken();
974
+ yield chunk;
975
+ } else if (chunk.type === "done") {
976
+ const timing = recorder.streamTiming();
977
+ yield timing ? { ...chunk, timing } : chunk;
978
+ } else {
979
+ const yieldedAt = Date.now();
980
+ yield chunk;
981
+ recorder.recordPreFirstTokenConsumerPause(yieldedAt);
982
+ }
983
+ }
851
984
  }
985
+ function withChatTiming(recorder, response) {
986
+ const timing = recorder.chatTiming();
987
+ return timing ? { ...response, timing } : response;
988
+ }
989
+
990
+ // src/providers/rate-limiter.ts
852
991
  var RateLimiter = class {
853
992
  maxConcurrent;
854
993
  minIntervalMs;
@@ -897,11 +1036,11 @@ var RateLimiter = class {
897
1036
  * With `acquireTimeoutMs`, a waiter that sits in the queue too long rejects.
898
1037
  */
899
1038
  acquire(signal) {
900
- if (signal?.aborted) return Promise.reject(abortError());
1039
+ if (signal?.aborted) return Promise.reject(signal.reason);
901
1040
  return new Promise((resolve2, reject) => {
902
1041
  const waiter = { resolve: resolve2, reject, signal };
903
1042
  if (signal) {
904
- waiter.onAbort = () => this.settleWaiter(waiter, abortError());
1043
+ waiter.onAbort = () => this.settleWaiter(waiter, signal.reason);
905
1044
  signal.addEventListener("abort", waiter.onAbort, { once: true });
906
1045
  }
907
1046
  if (this.acquireTimeoutMs != null) {
@@ -1201,6 +1340,7 @@ function normalizeMessageContent(content) {
1201
1340
  }
1202
1341
  var OpenAICompatibleProvider = class {
1203
1342
  name;
1343
+ reportsRequestLifecycle = true;
1204
1344
  inputCapabilities(model) {
1205
1345
  return this.name === "openrouter" && model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64"] } } : {};
1206
1346
  }
@@ -1297,6 +1437,7 @@ var OpenAICompatibleProvider = class {
1297
1437
  async chat(messages, options) {
1298
1438
  const headers = this.buildHeaders(await this.resolveKey());
1299
1439
  const body = this.buildRequestBody(messages, options, false);
1440
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
1300
1441
  const res = await fetchWithRetry(
1301
1442
  `${this.baseUrl}/chat/completions`,
1302
1443
  {
@@ -1305,7 +1446,7 @@ var OpenAICompatibleProvider = class {
1305
1446
  body: JSON.stringify(body),
1306
1447
  signal: options.signal
1307
1448
  },
1308
- { governor: this.governor, provider: this.name }
1449
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
1309
1450
  );
1310
1451
  if (!res.ok) {
1311
1452
  const errorBody = await res.text();
@@ -1314,15 +1455,20 @@ var OpenAICompatibleProvider = class {
1314
1455
  status: res.status,
1315
1456
  headers: res.headers,
1316
1457
  message: this.extractErrorMessage(errorBody, res.status),
1317
- body: errorBody
1458
+ body: errorBody,
1459
+ timing: recorder.chatTiming()
1318
1460
  });
1319
1461
  }
1320
1462
  const json = await res.json();
1321
- return this.parseResponse(json, this.requestModel(body, options.model), body);
1463
+ return withChatTiming(
1464
+ recorder,
1465
+ this.parseResponse(json, this.requestModel(body, options.model), body)
1466
+ );
1322
1467
  }
1323
1468
  async *stream(messages, options) {
1324
1469
  const headers = this.buildHeaders(await this.resolveKey());
1325
1470
  const body = this.buildRequestBody(messages, options, true);
1471
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
1326
1472
  const res = await fetchWithRetry(
1327
1473
  `${this.baseUrl}/chat/completions`,
1328
1474
  {
@@ -1331,7 +1477,7 @@ var OpenAICompatibleProvider = class {
1331
1477
  body: JSON.stringify(body),
1332
1478
  signal: options.signal
1333
1479
  },
1334
- { governor: this.governor, provider: this.name }
1480
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
1335
1481
  );
1336
1482
  if (!res.ok) {
1337
1483
  const errorBody = await res.text();
@@ -1340,13 +1486,17 @@ var OpenAICompatibleProvider = class {
1340
1486
  status: res.status,
1341
1487
  headers: res.headers,
1342
1488
  message: this.extractErrorMessage(errorBody, res.status),
1343
- body: errorBody
1489
+ body: errorBody,
1490
+ timing: recorder.chatTiming()
1344
1491
  });
1345
1492
  }
1346
1493
  if (!res.body) {
1347
1494
  throw new Error(`${this.profile.label ?? this.profile.name} stream response has no body`);
1348
1495
  }
1349
- yield* this.parseSSEStream(res.body, this.requestModel(body, options.model), body);
1496
+ yield* withCallTiming(
1497
+ recorder,
1498
+ this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
1499
+ );
1350
1500
  }
1351
1501
  // ---------------------------------------------------------------------------
1352
1502
  // Request building
@@ -1573,7 +1723,21 @@ var OpenAICompatibleProvider = class {
1573
1723
  // ---------------------------------------------------------------------------
1574
1724
  // SSE streaming
1575
1725
  // ---------------------------------------------------------------------------
1576
- async *parseSSEStream(body, model, request) {
1726
+ /**
1727
+ * PRIVATE, and deliberately not an extension point. It was `protected`, but
1728
+ * nothing in or out of this repo overrides it, and `protected` could not
1729
+ * carry the guarantee it appeared to: TypeScript checks an override for
1730
+ * assignability, and a function type with fewer parameters is assignable to
1731
+ * one with more — so an override that simply dropped the recorder still
1732
+ * compiled and then reported `wireMs === ttfbMs` for the whole subclass. Its
1733
+ * type is not exported from the barrel either, so a subclass author could not
1734
+ * name what they were required to accept.
1735
+ *
1736
+ * A provider with a different wire format implements `Provider` (see
1737
+ * docs/providers.md); an OpenAI-compatible one adds a `ProviderProfile`.
1738
+ * Neither needs to reach in here.
1739
+ */
1740
+ async *parseSSEStream(body, model, request, timing) {
1577
1741
  const reader = body.getReader();
1578
1742
  const decoder = new TextDecoder();
1579
1743
  let buffer = "";
@@ -1633,7 +1797,7 @@ var OpenAICompatibleProvider = class {
1633
1797
  };
1634
1798
  try {
1635
1799
  for (; ; ) {
1636
- const { done, value } = await reader.read();
1800
+ const { done, value } = await timing.read(reader);
1637
1801
  if (done) break;
1638
1802
  buffer += decoder.decode(value, { stream: true });
1639
1803
  const lines = buffer.split("\n");
@@ -1665,7 +1829,8 @@ var OpenAICompatibleProvider = class {
1665
1829
  status: 0,
1666
1830
  retryable: false,
1667
1831
  message,
1668
- body: JSON.stringify(parsed.error)
1832
+ body: JSON.stringify(parsed.error),
1833
+ timing: timing.streamTiming()
1669
1834
  });
1670
1835
  }
1671
1836
  if (parsed.model !== void 0 || parsed.service_tier !== void 0 || parsed.serviceTier !== void 0) {
@@ -1737,7 +1902,8 @@ var OpenAICompatibleProvider = class {
1737
1902
  provider: this.name,
1738
1903
  status: 0,
1739
1904
  retryable: true,
1740
- message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`
1905
+ message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`,
1906
+ timing: timing.streamTiming()
1741
1907
  });
1742
1908
  } finally {
1743
1909
  reader.releaseLock();
@@ -1776,12 +1942,12 @@ var DIRECT_OPENAI_CATALOG = [
1776
1942
  {
1777
1943
  aliases: ["gpt-5.6", "gpt-5.6-sol"],
1778
1944
  snapshotBase: "gpt-5.6-sol",
1779
- short: { input: 5 / M, cachedInput: 0.5 / M, cacheWrite: 6.25 / M, output: 30 / M },
1945
+ short: { input: 4 / M, cachedInput: 0.4 / M, cacheWrite: 5 / M, output: 20 / M },
1780
1946
  long: withLongContext({
1781
- input: 5 / M,
1782
- cachedInput: 0.5 / M,
1783
- cacheWrite: 6.25 / M,
1784
- output: 30 / M
1947
+ input: 4 / M,
1948
+ cachedInput: 0.4 / M,
1949
+ cacheWrite: 5 / M,
1950
+ output: 20 / M
1785
1951
  }),
1786
1952
  contextBoundary: LONG_CONTEXT_BOUNDARY
1787
1953
  },
@@ -2210,6 +2376,7 @@ function responseImageParts(parts) {
2210
2376
  }
2211
2377
  var OpenAIResponsesProvider = class {
2212
2378
  name = "openai-responses";
2379
+ reportsRequestLifecycle = true;
2213
2380
  inputCapabilities(model) {
2214
2381
  return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
2215
2382
  }
@@ -2296,6 +2463,7 @@ var OpenAIResponsesProvider = class {
2296
2463
  async chat(messages, options) {
2297
2464
  const headers = this.buildHeaders(await this.resolveKey());
2298
2465
  const body = this.buildRequestBody(messages, options, false);
2466
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
2299
2467
  const res = await fetchWithRetry(
2300
2468
  `${this.baseUrl}/responses`,
2301
2469
  {
@@ -2304,7 +2472,7 @@ var OpenAIResponsesProvider = class {
2304
2472
  body: JSON.stringify(body),
2305
2473
  signal: options.signal
2306
2474
  },
2307
- { governor: this.governor, provider: this.name }
2475
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
2308
2476
  );
2309
2477
  if (!res.ok) {
2310
2478
  const errorBody = await res.text();
@@ -2314,11 +2482,15 @@ var OpenAIResponsesProvider = class {
2314
2482
  status: res.status,
2315
2483
  headers: res.headers,
2316
2484
  message,
2317
- body: errorBody
2485
+ body: errorBody,
2486
+ timing: recorder.chatTiming()
2318
2487
  });
2319
2488
  }
2320
2489
  const json = await res.json();
2321
- return this.parseResponse(json, this.requestModel(body, options.model), body);
2490
+ return withChatTiming(
2491
+ recorder,
2492
+ this.parseResponse(json, this.requestModel(body, options.model), body)
2493
+ );
2322
2494
  }
2323
2495
  // ---------------------------------------------------------------------------
2324
2496
  // stream - SSE streaming
@@ -2326,6 +2498,7 @@ var OpenAIResponsesProvider = class {
2326
2498
  async *stream(messages, options) {
2327
2499
  const headers = this.buildHeaders(await this.resolveKey());
2328
2500
  const body = this.buildRequestBody(messages, options, true);
2501
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
2329
2502
  const res = await fetchWithRetry(
2330
2503
  `${this.baseUrl}/responses`,
2331
2504
  {
@@ -2334,7 +2507,7 @@ var OpenAIResponsesProvider = class {
2334
2507
  body: JSON.stringify(body),
2335
2508
  signal: options.signal
2336
2509
  },
2337
- { governor: this.governor, provider: this.name }
2510
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
2338
2511
  );
2339
2512
  if (!res.ok) {
2340
2513
  const errorBody = await res.text();
@@ -2344,13 +2517,17 @@ var OpenAIResponsesProvider = class {
2344
2517
  status: res.status,
2345
2518
  headers: res.headers,
2346
2519
  message,
2347
- body: errorBody
2520
+ body: errorBody,
2521
+ timing: recorder.chatTiming()
2348
2522
  });
2349
2523
  }
2350
2524
  if (!res.body) {
2351
2525
  throw new Error("OpenAI Responses stream has no body");
2352
2526
  }
2353
- yield* this.parseSSEStream(res.body, this.requestModel(body, options.model), body);
2527
+ yield* withCallTiming(
2528
+ recorder,
2529
+ this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
2530
+ );
2354
2531
  }
2355
2532
  // ---------------------------------------------------------------------------
2356
2533
  // Internal: build request body
@@ -2552,7 +2729,7 @@ var OpenAIResponsesProvider = class {
2552
2729
  // ---------------------------------------------------------------------------
2553
2730
  // Internal: parse SSE stream
2554
2731
  // ---------------------------------------------------------------------------
2555
- async *parseSSEStream(body, model, request) {
2732
+ async *parseSSEStream(body, model, request, timing) {
2556
2733
  const reader = body.getReader();
2557
2734
  const decoder = new TextDecoder();
2558
2735
  let buffer = "";
@@ -2560,7 +2737,7 @@ var OpenAIResponsesProvider = class {
2560
2737
  let eventType = "";
2561
2738
  try {
2562
2739
  while (true) {
2563
- const { done, value } = await reader.read();
2740
+ const { done, value } = await timing.read(reader);
2564
2741
  if (done) break;
2565
2742
  buffer += decoder.decode(value, { stream: true });
2566
2743
  const lines = buffer.split("\n");
@@ -2580,7 +2757,14 @@ var OpenAIResponsesProvider = class {
2580
2757
  } catch {
2581
2758
  continue;
2582
2759
  }
2583
- const chunk = this.handleStreamEvent(eventType, data, model, callIdMap, request);
2760
+ const chunk = this.handleStreamEvent(
2761
+ eventType,
2762
+ data,
2763
+ model,
2764
+ callIdMap,
2765
+ request,
2766
+ timing
2767
+ );
2584
2768
  if (chunk) {
2585
2769
  yield chunk;
2586
2770
  if (chunk.type === "done") return;
@@ -2594,7 +2778,7 @@ var OpenAIResponsesProvider = class {
2594
2778
  reader.releaseLock();
2595
2779
  }
2596
2780
  }
2597
- handleStreamEvent(eventType, data, model, callIdMap, request) {
2781
+ handleStreamEvent(eventType, data, model, callIdMap, request, timing) {
2598
2782
  switch (eventType) {
2599
2783
  case "response.output_text.delta":
2600
2784
  return { type: "text_delta", content: data.delta ?? "" };
@@ -2650,7 +2834,8 @@ var OpenAIResponsesProvider = class {
2650
2834
  provider: this.name,
2651
2835
  status: 0,
2652
2836
  retryable: false,
2653
- message: `OpenAI Responses API error: ${errorMsg}`
2837
+ message: `OpenAI Responses API error: ${errorMsg}`,
2838
+ timing: timing.streamTiming()
2654
2839
  });
2655
2840
  }
2656
2841
  default:
@@ -2718,6 +2903,12 @@ function hasAnthropicProviderFileImage(messages) {
2718
2903
  );
2719
2904
  }
2720
2905
  var CLAUDE_CAPABILITIES = {
2906
+ "claude-fable-5-1": {
2907
+ thinking: "adaptive-always-on",
2908
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
2909
+ manualBudget: false,
2910
+ stripTemperature: true
2911
+ },
2721
2912
  "claude-fable-5": {
2722
2913
  thinking: "adaptive-always-on",
2723
2914
  effortLevels: ["low", "medium", "high", "xhigh", "max"],
@@ -2808,10 +2999,18 @@ var PRE_INFERENCE_GEO_MODELS = /* @__PURE__ */ new Set([
2808
2999
  function resolveClaudeCapability(model) {
2809
3000
  return CLAUDE_CAPABILITIES[model] ?? (LEGACY_CLAUDE_MODELS.has(model) ? LEGACY_CLAUDE_CAPABILITY : void 0);
2810
3001
  }
3002
+ var DEFAULT_CACHE_READ_MULTIPLIER = 0.1;
2811
3003
  var ANTHROPIC_RATES = {
3004
+ "claude-fable-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
2812
3005
  "claude-fable-5": { input: 1e-5, output: 5e-5 },
3006
+ // Limited availability (Glasswing). Priced from the public pricing table; no
3007
+ // CLAUDE_CAPABILITIES entry, because their thinking semantics are not
3008
+ // documented per-model and an unverified capability would send the wrong
3009
+ // request shape. Unknown IDs pass through unmodified, which is the safe side.
3010
+ "claude-mythos-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
3011
+ "claude-mythos-5": { input: 1e-5, output: 5e-5 },
2813
3012
  "claude-opus-5": { input: 5e-6, output: 25e-6 },
2814
- "claude-sonnet-5": { input: 3e-6, output: 15e-6, sonnet5Intro: true },
3013
+ "claude-sonnet-5": { input: 2e-6, output: 1e-5 },
2815
3014
  "claude-opus-4-8": { input: 5e-6, output: 25e-6 },
2816
3015
  "claude-opus-4-7": { input: 5e-6, output: 25e-6 },
2817
3016
  "claude-opus-4-6": { input: 5e-6, output: 25e-6 },
@@ -2850,7 +3049,7 @@ function anthropicStreamErrorStatus(type) {
2850
3049
  return 0;
2851
3050
  }
2852
3051
  }
2853
- function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
3052
+ function estimateAnthropicCost(model, usage) {
2854
3053
  if (!model || !isValidTokenCount(usage.inputTokens) || !isValidTokenCount(usage.outputTokens)) {
2855
3054
  return void 0;
2856
3055
  }
@@ -2869,8 +3068,9 @@ function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
2869
3068
  if (usage.aggregateCacheWriteTokens !== void 0 && (!isValidTokenCount(usage.aggregateCacheWriteTokens) || usage.aggregateCacheWriteTokens !== cacheWriteTotal)) {
2870
3069
  return void 0;
2871
3070
  }
2872
- const { input, output } = rate.sonnet5Intro && now < /* @__PURE__ */ new Date("2026-09-01T00:00:00Z") ? { input: 2e-6, output: 1e-5 } : rate;
2873
- return usage.inputTokens * input + cacheRead * input * 0.1 + cacheWrite5m * input * 1.25 + cacheWrite1h * input * 2 + usage.outputTokens * output;
3071
+ const { input, output } = rate;
3072
+ const cacheReadRate = input * (rate.cacheReadMultiplier ?? DEFAULT_CACHE_READ_MULTIPLIER);
3073
+ return usage.inputTokens * input + cacheRead * cacheReadRate + cacheWrite5m * input * 1.25 + cacheWrite1h * input * 2 + usage.outputTokens * output;
2874
3074
  }
2875
3075
  var THINKING_BUDGETS = {
2876
3076
  low: 1024,
@@ -3102,6 +3302,7 @@ function mergeAnthropicUsage(initial, update) {
3102
3302
  }
3103
3303
  var AnthropicProvider = class {
3104
3304
  name = "anthropic";
3305
+ reportsRequestLifecycle = true;
3105
3306
  /** Report a clamped `effort`. Anthropic clamps to each model's supported
3106
3307
  * `output_config.effort` levels, and models that always think fall back to
3107
3308
  * adaptive `'low'` when asked to disable thinking. Only reported when the
@@ -3111,6 +3312,14 @@ var AnthropicProvider = class {
3111
3312
  const resolved = resolveThinkingOptions(options);
3112
3313
  if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
3113
3314
  const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
3315
+ if (ANTHROPIC_RATES[model] && !resolveClaudeCapability(model)) {
3316
+ return {
3317
+ requested: resolved.effort,
3318
+ effective: "unset",
3319
+ clamped: true,
3320
+ cause: `Axl has no capability entry for Anthropic ${model}, so no reasoning control was sent and effort '${resolved.effort}' had no effect`
3321
+ };
3322
+ }
3114
3323
  const config = resolveClaudeThinking(model, resolved);
3115
3324
  const wireEffort = typeof config.outputConfig?.effort === "string" ? config.outputConfig.effort : void 0;
3116
3325
  const effective = config.clamp?.effective ?? wireEffort;
@@ -3166,6 +3375,10 @@ var AnthropicProvider = class {
3166
3375
  }
3167
3376
  /** Anthropic ignores native `json_schema` structurally — Axl uses a system
3168
3377
  * prompt JSON instruction + client-side Zod validation (see buildRequest). */
3378
+ /** The only built-in adapter that honors `promptCache` (see buildBody). */
3379
+ realizesPromptCache() {
3380
+ return true;
3381
+ }
3169
3382
  nativeStructuredOutputSupport() {
3170
3383
  return "unsupported";
3171
3384
  }
@@ -3207,6 +3420,7 @@ var AnthropicProvider = class {
3207
3420
  );
3208
3421
  const body = this.buildRequestBody(messages, options, false);
3209
3422
  const pricingContext = pricingContextFromBody(body);
3423
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
3210
3424
  const res = await fetchWithRetry(
3211
3425
  `${this.baseUrl}/messages`,
3212
3426
  {
@@ -3215,7 +3429,7 @@ var AnthropicProvider = class {
3215
3429
  body: JSON.stringify(body),
3216
3430
  signal: options.signal
3217
3431
  },
3218
- { governor: this.governor, provider: this.name }
3432
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
3219
3433
  );
3220
3434
  if (!res.ok) {
3221
3435
  const errorBody = await res.text();
@@ -3225,11 +3439,12 @@ var AnthropicProvider = class {
3225
3439
  status: res.status,
3226
3440
  headers: res.headers,
3227
3441
  message,
3228
- body: errorBody
3442
+ body: errorBody,
3443
+ timing: recorder.chatTiming()
3229
3444
  });
3230
3445
  }
3231
3446
  const json = await res.json();
3232
- return this.parseResponse(json, pricingContext);
3447
+ return withChatTiming(recorder, this.parseResponse(json, pricingContext));
3233
3448
  }
3234
3449
  // ---------------------------------------------------------------------------
3235
3450
  // stream - SSE streaming completion
@@ -3241,6 +3456,7 @@ var AnthropicProvider = class {
3241
3456
  );
3242
3457
  const body = this.buildRequestBody(messages, options, true);
3243
3458
  const pricingContext = pricingContextFromBody(body);
3459
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
3244
3460
  const res = await fetchWithRetry(
3245
3461
  `${this.baseUrl}/messages`,
3246
3462
  {
@@ -3249,7 +3465,7 @@ var AnthropicProvider = class {
3249
3465
  body: JSON.stringify(body),
3250
3466
  signal: options.signal
3251
3467
  },
3252
- { governor: this.governor, provider: this.name }
3468
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
3253
3469
  );
3254
3470
  if (!res.ok) {
3255
3471
  const errorBody = await res.text();
@@ -3259,13 +3475,17 @@ var AnthropicProvider = class {
3259
3475
  status: res.status,
3260
3476
  headers: res.headers,
3261
3477
  message,
3262
- body: errorBody
3478
+ body: errorBody,
3479
+ timing: recorder.chatTiming()
3263
3480
  });
3264
3481
  }
3265
3482
  if (!res.body) {
3266
3483
  throw new Error("Anthropic stream response has no body");
3267
3484
  }
3268
- yield* this.parseSSEStream(res.body, pricingContext, res.headers);
3485
+ yield* withCallTiming(
3486
+ recorder,
3487
+ this.parseSSEStream(res.body, pricingContext, res.headers, recorder)
3488
+ );
3269
3489
  }
3270
3490
  // ---------------------------------------------------------------------------
3271
3491
  // Internal: request building
@@ -3300,14 +3520,31 @@ var AnthropicProvider = class {
3300
3520
  max_tokens: options.maxTokens ?? 4096,
3301
3521
  stream
3302
3522
  };
3303
- if (systemText) {
3523
+ const po = options.providerOptions;
3524
+ const callerOwnsPrefix = po !== void 0 && (po.cache_control !== void 0 || po.system !== void 0 || po.tools !== void 0);
3525
+ const promptCache = options.promptCache === true && !callerOwnsPrefix;
3526
+ const anchorOnSystem = promptCache && systemMessages.length > 0 && systemMessages[0].origin !== "runtime";
3527
+ if (anchorOnSystem) {
3528
+ body.system = systemMessages.map((m, index) => ({
3529
+ type: "text",
3530
+ text: typeof m.content === "string" ? m.content : "",
3531
+ ...index === 0 ? { cache_control: { type: "ephemeral" } } : {}
3532
+ }));
3533
+ } else if (systemText) {
3304
3534
  body.system = systemText;
3305
3535
  }
3306
3536
  if (options.stop) {
3307
3537
  body.stop_sequences = options.stop;
3308
3538
  }
3309
3539
  if (options.tools && options.tools.length > 0) {
3310
- body.tools = options.tools.map((t) => this.mapToolDefinition(t));
3540
+ const tools = options.tools.map((t) => this.mapToolDefinition(t));
3541
+ if (promptCache && !anchorOnSystem) {
3542
+ tools[tools.length - 1] = {
3543
+ ...tools[tools.length - 1],
3544
+ cache_control: { type: "ephemeral" }
3545
+ };
3546
+ }
3547
+ body.tools = tools;
3311
3548
  }
3312
3549
  if (options.toolChoice !== void 0) {
3313
3550
  body.tool_choice = this.mapToolChoice(options.toolChoice);
@@ -3326,9 +3563,13 @@ var AnthropicProvider = class {
3326
3563
  }
3327
3564
  if (options.responseFormat && options.responseFormat.type !== "text") {
3328
3565
  const jsonInstruction = "You must respond with valid JSON only. No markdown fences, no extra text.";
3329
- body.system = body.system ? `${body.system}
3566
+ if (Array.isArray(body.system)) {
3567
+ body.system = [...body.system, { type: "text", text: jsonInstruction }];
3568
+ } else {
3569
+ body.system = body.system ? `${body.system}
3330
3570
 
3331
3571
  ${jsonInstruction}` : jsonInstruction;
3572
+ }
3332
3573
  }
3333
3574
  if (options.providerOptions) {
3334
3575
  Object.assign(body, options.providerOptions);
@@ -3508,7 +3749,7 @@ ${jsonInstruction}` : jsonInstruction;
3508
3749
  // ---------------------------------------------------------------------------
3509
3750
  // Internal: SSE stream parsing
3510
3751
  // ---------------------------------------------------------------------------
3511
- async *parseSSEStream(body, pricingContext, responseHeaders) {
3752
+ async *parseSSEStream(body, pricingContext, responseHeaders, timing) {
3512
3753
  const reader = body.getReader();
3513
3754
  const decoder = new TextDecoder();
3514
3755
  let buffer = "";
@@ -3537,7 +3778,7 @@ ${jsonInstruction}` : jsonInstruction;
3537
3778
  });
3538
3779
  try {
3539
3780
  while (true) {
3540
- const { done, value } = await reader.read();
3781
+ const { done, value } = await timing.read(reader);
3541
3782
  if (done) break;
3542
3783
  buffer += decoder.decode(value, { stream: true });
3543
3784
  const lines = buffer.split("\n");
@@ -3560,7 +3801,8 @@ ${jsonInstruction}` : jsonInstruction;
3560
3801
  provider: this.name,
3561
3802
  status,
3562
3803
  headers: responseHeaders,
3563
- message: event.error?.message ?? "Anthropic stream failed"
3804
+ message: event.error?.message ?? "Anthropic stream failed",
3805
+ timing: timing.streamTiming()
3564
3806
  });
3565
3807
  }
3566
3808
  case "content_block_start": {
@@ -3796,7 +4038,6 @@ var GEMINI_PRICING = {
3796
4038
  },
3797
4039
  "gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
3798
4040
  "gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
3799
- "gemini-3-flash-preview": { input: 5e-7, cached: 5e-8, output: 3e-6 },
3800
4041
  "gemini-3.1-pro-preview": {
3801
4042
  input: 2e-6,
3802
4043
  cached: 2e-7,
@@ -3812,7 +4053,9 @@ var GEMINI_PRICING = {
3812
4053
  "gemini-3.1-flash-lite": { input: 25e-8, cached: 25e-9, output: 15e-7 },
3813
4054
  "gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
3814
4055
  "gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
3815
- "gemini-3.6-flash": { input: 15e-7, cached: 15e-8, output: 75e-7 }
4056
+ "gemini-3.6-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
4057
+ "gemini-3.7-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
4058
+ "gemini-3.8-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 }
3816
4059
  };
3817
4060
  function isValidTokenCount2(value) {
3818
4061
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
@@ -4040,6 +4283,7 @@ function resolveGeminiThinking(model, resolved) {
4040
4283
  }
4041
4284
  var GeminiProvider = class {
4042
4285
  name = "google";
4286
+ reportsRequestLifecycle = true;
4043
4287
  inputCapabilities(model) {
4044
4288
  return model.trim().length > 0 ? { image: { sources: ["bytes", "base64", "provider-file"] } } : {};
4045
4289
  }
@@ -4157,6 +4401,7 @@ var GeminiProvider = class {
4157
4401
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4158
4402
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4159
4403
  const headers = this.buildHeaders(await this.resolveKey());
4404
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4160
4405
  const res = await fetchWithRetry(
4161
4406
  `${this.baseUrl}/models/${pricingContext.model}:generateContent`,
4162
4407
  {
@@ -4165,7 +4410,7 @@ var GeminiProvider = class {
4165
4410
  body: JSON.stringify(body),
4166
4411
  signal: options.signal
4167
4412
  },
4168
- { governor: this.governor, provider: this.name }
4413
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4169
4414
  );
4170
4415
  if (!res.ok) {
4171
4416
  const errorBody = await res.text();
@@ -4175,11 +4420,12 @@ var GeminiProvider = class {
4175
4420
  status: res.status,
4176
4421
  headers: res.headers,
4177
4422
  message,
4178
- body: errorBody
4423
+ body: errorBody,
4424
+ timing: recorder.chatTiming()
4179
4425
  });
4180
4426
  }
4181
4427
  const json = await res.json();
4182
- return this.parseResponse(json, pricingContext);
4428
+ return withChatTiming(recorder, this.parseResponse(json, pricingContext));
4183
4429
  }
4184
4430
  // ---------------------------------------------------------------------------
4185
4431
  // stream - SSE streaming completion
@@ -4193,6 +4439,7 @@ var GeminiProvider = class {
4193
4439
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4194
4440
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4195
4441
  const headers = this.buildHeaders(await this.resolveKey());
4442
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4196
4443
  const res = await fetchWithRetry(
4197
4444
  `${this.baseUrl}/models/${pricingContext.model}:streamGenerateContent?alt=sse`,
4198
4445
  {
@@ -4201,7 +4448,7 @@ var GeminiProvider = class {
4201
4448
  body: JSON.stringify(body),
4202
4449
  signal: options.signal
4203
4450
  },
4204
- { governor: this.governor, provider: this.name }
4451
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4205
4452
  );
4206
4453
  if (!res.ok) {
4207
4454
  const errorBody = await res.text();
@@ -4211,13 +4458,14 @@ var GeminiProvider = class {
4211
4458
  status: res.status,
4212
4459
  headers: res.headers,
4213
4460
  message,
4214
- body: errorBody
4461
+ body: errorBody,
4462
+ timing: recorder.chatTiming()
4215
4463
  });
4216
4464
  }
4217
4465
  if (!res.body) {
4218
4466
  throw new Error("Gemini stream response has no body");
4219
4467
  }
4220
- yield* this.parseSSEStream(res.body, pricingContext);
4468
+ yield* withCallTiming(recorder, this.parseSSEStream(res.body, pricingContext, recorder));
4221
4469
  }
4222
4470
  // ---------------------------------------------------------------------------
4223
4471
  // Rich image requests use Gemini Interactions. It is deliberately stateless:
@@ -4228,10 +4476,11 @@ var GeminiProvider = class {
4228
4476
  this.assertSafeInteractionOptions(options);
4229
4477
  const body = this.buildInteractionRequestBody(messages, options, false);
4230
4478
  const headers = this.buildHeaders(await this.resolveKey());
4479
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4231
4480
  const res = await fetchWithRetry(
4232
4481
  `${this.baseUrl}/interactions`,
4233
4482
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4234
- { governor: this.governor, provider: this.name }
4483
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4235
4484
  );
4236
4485
  if (!res.ok) {
4237
4486
  const errorBody = await res.text();
@@ -4240,19 +4489,24 @@ var GeminiProvider = class {
4240
4489
  status: res.status,
4241
4490
  headers: res.headers,
4242
4491
  message: this.extractErrorMessage(errorBody, res.status),
4243
- body: errorBody
4492
+ body: errorBody,
4493
+ timing: recorder.chatTiming()
4244
4494
  });
4245
4495
  }
4246
- return this.parseInteractionResponse(await res.json());
4496
+ return withChatTiming(
4497
+ recorder,
4498
+ this.parseInteractionResponse(await res.json())
4499
+ );
4247
4500
  }
4248
4501
  async *streamInteraction(messages, options) {
4249
4502
  this.assertSafeInteractionOptions(options);
4250
4503
  const body = this.buildInteractionRequestBody(messages, options, true);
4251
4504
  const headers = this.buildHeaders(await this.resolveKey());
4505
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4252
4506
  const res = await fetchWithRetry(
4253
4507
  `${this.baseUrl}/interactions?alt=sse`,
4254
4508
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4255
- { governor: this.governor, provider: this.name }
4509
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4256
4510
  );
4257
4511
  if (!res.ok) {
4258
4512
  const errorBody = await res.text();
@@ -4261,11 +4515,15 @@ var GeminiProvider = class {
4261
4515
  status: res.status,
4262
4516
  headers: res.headers,
4263
4517
  message: this.extractErrorMessage(errorBody, res.status),
4264
- body: errorBody
4518
+ body: errorBody,
4519
+ timing: recorder.chatTiming()
4265
4520
  });
4266
4521
  }
4267
4522
  if (!res.body) throw new Error("Gemini Interactions stream has no body");
4268
- yield* this.parseInteractionSSEStream(res.body, res.headers);
4523
+ yield* withCallTiming(
4524
+ recorder,
4525
+ this.parseInteractionSSEStream(res.body, res.headers, recorder)
4526
+ );
4269
4527
  }
4270
4528
  buildInteractionRequestBody(messages, options, stream) {
4271
4529
  const model = this.requestModel(options);
@@ -4421,7 +4679,7 @@ var GeminiProvider = class {
4421
4679
  providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
4422
4680
  };
4423
4681
  }
4424
- async *parseInteractionSSEStream(body, responseHeaders) {
4682
+ async *parseInteractionSSEStream(body, responseHeaders, timing) {
4425
4683
  const reader = body.getReader();
4426
4684
  const decoder = new TextDecoder();
4427
4685
  let buffer = "";
@@ -4431,7 +4689,7 @@ var GeminiProvider = class {
4431
4689
  let completed = false;
4432
4690
  try {
4433
4691
  while (true) {
4434
- const { done, value } = await reader.read();
4692
+ const { done, value } = await timing.read(reader);
4435
4693
  if (done) break;
4436
4694
  buffer += decoder.decode(value, { stream: true });
4437
4695
  const lines = buffer.split("\n");
@@ -4454,14 +4712,16 @@ var GeminiProvider = class {
4454
4712
  provider: this.name,
4455
4713
  status,
4456
4714
  headers: responseHeaders,
4457
- message
4715
+ message,
4716
+ timing: timing.streamTiming()
4458
4717
  });
4459
4718
  }
4460
4719
  throw new ProviderError({
4461
4720
  provider: this.name,
4462
4721
  status: 0,
4463
4722
  retryable: false,
4464
- message
4723
+ message,
4724
+ timing: timing.streamTiming()
4465
4725
  });
4466
4726
  }
4467
4727
  if (event.event_type === "step.start" && event.step) {
@@ -4939,7 +5199,7 @@ var GeminiProvider = class {
4939
5199
  // ---------------------------------------------------------------------------
4940
5200
  // Internal: SSE stream parsing
4941
5201
  // ---------------------------------------------------------------------------
4942
- async *parseSSEStream(body, pricingContext) {
5202
+ async *parseSSEStream(body, pricingContext, timing) {
4943
5203
  const reader = body.getReader();
4944
5204
  const decoder = new TextDecoder();
4945
5205
  let buffer = "";
@@ -4952,7 +5212,7 @@ var GeminiProvider = class {
4952
5212
  const accumulatedParts = [];
4953
5213
  try {
4954
5214
  while (true) {
4955
- const { done, value } = await reader.read();
5215
+ const { done, value } = await timing.read(reader);
4956
5216
  if (done) break;
4957
5217
  buffer += decoder.decode(value, { stream: true });
4958
5218
  const lines = buffer.split("\n");
@@ -5961,7 +6221,7 @@ var GEMINI_AUDIO_MEDIA_TYPES = /* @__PURE__ */ new Set([
5961
6221
  "audio/mulaw",
5962
6222
  "audio/webm"
5963
6223
  ]);
5964
- function abortError2() {
6224
+ function abortError() {
5965
6225
  return new DOMException("The transcription operation was aborted", "AbortError");
5966
6226
  }
5967
6227
  function audioMime(audio) {
@@ -5993,13 +6253,13 @@ function secondsOffset(value) {
5993
6253
  function sleep3(ms, signal) {
5994
6254
  return new Promise((resolve2, reject) => {
5995
6255
  if (signal?.aborted) {
5996
- reject(abortError2());
6256
+ reject(abortError());
5997
6257
  return;
5998
6258
  }
5999
6259
  const timer = setTimeout(done, ms);
6000
6260
  const onAbort = () => {
6001
6261
  clearTimeout(timer);
6002
- done(abortError2());
6262
+ done(abortError());
6003
6263
  };
6004
6264
  function done(error) {
6005
6265
  signal?.removeEventListener("abort", onAbort);
@@ -7320,7 +7580,7 @@ function isCostBearingLeaf(event) {
7320
7580
  return COST_LEAF_SET.has(event.type);
7321
7581
  }
7322
7582
  function isUnpricedLeaf(event) {
7323
- 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));
7324
7584
  }
7325
7585
  function isRootLevel(event) {
7326
7586
  const d = event.depth;
@@ -8606,18 +8866,27 @@ function parseDuration(duration) {
8606
8866
  if (!match) throw new Error(`Invalid duration format: "${duration}"`);
8607
8867
  const value = parseFloat(match[1]);
8608
8868
  const unit = match[2];
8869
+ let milliseconds;
8609
8870
  switch (unit) {
8610
8871
  case "ms":
8611
- return value;
8872
+ milliseconds = value;
8873
+ break;
8612
8874
  case "s":
8613
- return value * 1e3;
8875
+ milliseconds = value * 1e3;
8876
+ break;
8614
8877
  case "m":
8615
- return value * 6e4;
8878
+ milliseconds = value * 6e4;
8879
+ break;
8616
8880
  case "h":
8617
- return value * 36e5;
8881
+ milliseconds = value * 36e5;
8882
+ break;
8618
8883
  default:
8619
8884
  throw new Error(`Unknown duration unit: "${unit}"`);
8620
8885
  }
8886
+ if (!Number.isFinite(milliseconds)) {
8887
+ throw new Error(`Duration is too large: "${duration}"`);
8888
+ }
8889
+ return milliseconds;
8621
8890
  }
8622
8891
  function parseCost(cost) {
8623
8892
  const match = cost.match(/^\$?([\d.]+)$/);
@@ -9273,6 +9542,42 @@ function recordToolSpanOutcome(span, outcome) {
9273
9542
 
9274
9543
  // src/context.ts
9275
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
+ }
9276
9581
  var DEFAULT_BRANCH_DRAIN_TIMEOUT_MS = 5e3;
9277
9582
  function resolveBranchDrainTimeoutMs(value) {
9278
9583
  const resolved = value === void 0 ? DEFAULT_BRANCH_DRAIN_TIMEOUT_MS : value;
@@ -9462,6 +9767,13 @@ function describeValue(value) {
9462
9767
  }
9463
9768
  return `a value of type ${typeof value}`;
9464
9769
  }
9770
+ var PROMPT_CACHE_COLD_WRITE_WARN_AFTER = 2;
9771
+ function nextUserTurn(history, from) {
9772
+ for (let i = Math.max(0, from); i < history.length; i++) {
9773
+ if (history[i].role === "user") return i;
9774
+ }
9775
+ return void 0;
9776
+ }
9465
9777
  function estimateMessagesTokens(messages) {
9466
9778
  let total = 0;
9467
9779
  let unmeasured = false;
@@ -9601,6 +9913,13 @@ var WorkflowContext = class _WorkflowContext {
9601
9913
  workflowLifecycleState;
9602
9914
  signal;
9603
9915
  summaryCache;
9916
+ /** Consecutive provider calls with promptCache on that wrote to the cache
9917
+ * without reading from it (prefix changes per call), and consecutive calls
9918
+ * with no cache activity at all (prefix below the model's minimum, which
9919
+ * Anthropic silently ignores). Either reports once per context. */
9920
+ promptCacheColdWrites = 0;
9921
+ promptCacheInactive = 0;
9922
+ promptCacheWarned = false;
9604
9923
  workflowName;
9605
9924
  mcpManager;
9606
9925
  spanManager;
@@ -10060,7 +10379,10 @@ var WorkflowContext = class _WorkflowContext {
10060
10379
  tool_call_id: parsed.callId
10061
10380
  });
10062
10381
  }
10063
- if (settlement.abortError !== void 0) throw settlement.abortError;
10382
+ if (settlement.abortError !== void 0) {
10383
+ signal?.throwIfAborted();
10384
+ throw settlement.abortError;
10385
+ }
10064
10386
  }
10065
10387
  // ── ctx.ask() ─────────────────────────────────────────────────────────
10066
10388
  async ask(agent2, prompt, options) {
@@ -10076,7 +10398,10 @@ var WorkflowContext = class _WorkflowContext {
10076
10398
  const promptText = inputText(normalizedInput);
10077
10399
  const inputDescriptor = describeModelInput(normalizedInput);
10078
10400
  const agentName = agent2._name;
10079
- 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(
10080
10405
  this._autoCheckpointName("ask", agentName),
10081
10406
  async () => {
10082
10407
  const parentFrame = askStorage.getStore();
@@ -10133,6 +10458,17 @@ var WorkflowContext = class _WorkflowContext {
10133
10458
  const costAfter2 = this.budgetContext?.totalCost ?? 0;
10134
10459
  span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
10135
10460
  span.setAttribute("axl.agent.duration", Date.now() - askStart);
10461
+ if (usageCapture.timing) {
10462
+ const t = usageCapture.timing;
10463
+ span.setAttribute("axl.agent.queued_ms", t.queuedMs);
10464
+ span.setAttribute("axl.agent.retry_ms", t.retryMs);
10465
+ span.setAttribute("axl.agent.attempts", t.attempts);
10466
+ span.setAttribute("axl.agent.ttfb_ms", t.ttfbMs);
10467
+ span.setAttribute("axl.agent.wire_ms", t.wireMs);
10468
+ if (t.firstTokenMs !== void 0) {
10469
+ span.setAttribute("axl.agent.first_token_ms", t.firstTokenMs);
10470
+ }
10471
+ }
10136
10472
  if (usageCapture.value) {
10137
10473
  span.setAttribute(
10138
10474
  "axl.agent.prompt_tokens",
@@ -10220,8 +10556,25 @@ var WorkflowContext = class _WorkflowContext {
10220
10556
  },
10221
10557
  { agent: agentName }
10222
10558
  );
10559
+ const runWithAskSignal = () => askSignal ? signalStorage.run(askSignal, runAsk) : runAsk();
10560
+ return hardAskSignal ? hardSignalStorage.run(hardAskSignal, runWithAskSignal) : runWithAskSignal();
10223
10561
  }
10224
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)) {
10225
10578
  if (this.budgetContext?.exceeded) {
10226
10579
  const { limit, totalCost: spent, policy } = this.budgetContext;
10227
10580
  if (policy === "warn") {
@@ -10340,6 +10693,14 @@ var WorkflowContext = class _WorkflowContext {
10340
10693
  const toolTokens = toolDefs.length > 0 ? estimateTokens(JSON.stringify(toolDefs)) : 0;
10341
10694
  const overhead = systemTokens + toolTokens + reserveTokens;
10342
10695
  const availableForHistory = maxContext - overhead;
10696
+ if (availableForHistory <= 0) {
10697
+ this.emitEvent({
10698
+ type: "log",
10699
+ data: {
10700
+ warning: `Agent '${agent2._name}' maxContext (${maxContext}) is at or below its fixed overhead (${overhead} = system ${systemTokens} + tools ${toolTokens} + reserve ${reserveTokens}); no session history can fit, so every ask summarizes, retaining at most the most recent exchange and sometimes none of it verbatim. The current input is always sent. Raise maxContext or lower contextManagement.reserveTokens.`
10701
+ }
10702
+ });
10703
+ }
10343
10704
  const historyEstimate = estimateMessagesTokens(sessionHistory);
10344
10705
  if (historyEstimate.unmeasured) {
10345
10706
  this.emitEvent({
@@ -10405,6 +10766,7 @@ ${appendedSchemaText}`;
10405
10766
  if (handoffContext.length > 0) {
10406
10767
  messages.push({
10407
10768
  role: "system",
10769
+ origin: "runtime",
10408
10770
  content: "The following is the conversation history from the previous agent that handed off to you:"
10409
10771
  });
10410
10772
  for (const msg of handoffContext) {
@@ -10452,8 +10814,19 @@ ${appendedSchemaText}`;
10452
10814
  }
10453
10815
  }
10454
10816
  const maxTurns = agent2._config.maxTurns ?? 25;
10455
- 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
+ }
10456
10827
  const startTime = Date.now();
10828
+ const timeoutTracker = timeoutTrackerStorage.getStore()?.at(-1);
10829
+ const timingTotals = { turns: 0, queuedMs: 0, retryMs: 0, wireMs: 0 };
10457
10830
  const currentMessages = [...messages];
10458
10831
  let turns = 0;
10459
10832
  let guardrailOutputRetries = 0;
@@ -10467,8 +10840,24 @@ ${appendedSchemaText}`;
10467
10840
  let lastStartMaxAttempts = 1;
10468
10841
  while (turns < maxTurns) {
10469
10842
  this.currentSignal?.throwIfAborted();
10470
- if (Date.now() - startTime > timeoutMs) {
10471
- throw new TimeoutError("ctx.ask()", timeoutMs);
10843
+ const now = Date.now();
10844
+ const elapsedMs = now - startTime - (timeoutTracker ? pausedHumanWaitMs(timeoutTracker, now) : 0);
10845
+ if (elapsedMs > timeoutMs) {
10846
+ throw new TimeoutError(
10847
+ "ctx.ask()",
10848
+ timeoutMs,
10849
+ timingTotals.turns > 0 ? {
10850
+ elapsedMs,
10851
+ queuedMs: timingTotals.queuedMs,
10852
+ retryMs: timingTotals.retryMs,
10853
+ wireMs: timingTotals.wireMs,
10854
+ otherMs: Math.max(
10855
+ 0,
10856
+ elapsedMs - timingTotals.queuedMs - timingTotals.retryMs - timingTotals.wireMs
10857
+ )
10858
+ } : void 0,
10859
+ agent2._name
10860
+ );
10472
10861
  }
10473
10862
  turns++;
10474
10863
  const isFirstTurn = turns === 1;
@@ -10508,11 +10897,49 @@ ${appendedSchemaText}`;
10508
10897
  effort: options?.effort ?? agent2._config.effort,
10509
10898
  thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
10510
10899
  includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
10900
+ promptCache: options?.promptCache ?? agent2._config.promptCache,
10511
10901
  toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
10512
10902
  stop: options?.stop ?? agent2._config.stop,
10513
10903
  providerOptions,
10514
10904
  signal: this.currentSignal
10515
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
+ };
10516
10943
  if (options?.schema && toolDefs.length === 0) {
10517
10944
  const nativeOut = options?.nativeStructuredOutput ?? agent2._config.nativeStructuredOutput ?? false;
10518
10945
  chatOptions.responseFormat = nativeOut ? {
@@ -10585,6 +11012,7 @@ ${appendedSchemaText}`;
10585
11012
  const toolCalls = [];
10586
11013
  const toolCallBuffers = /* @__PURE__ */ new Map();
10587
11014
  let streamProviderMetadata;
11015
+ let streamTiming;
10588
11016
  let thinkingContent = "";
10589
11017
  const streamingObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof z.ZodObject;
10590
11018
  const currentAttempt = schemaRetries + 1;
@@ -10598,54 +11026,77 @@ ${appendedSchemaText}`;
10598
11026
  });
10599
11027
  }
10600
11028
  }) : void 0;
10601
- for await (const chunk of provider.stream(
11029
+ if (stallController && provider.reportsRequestLifecycle !== true) armStallTimer();
11030
+ const providerStream = provider.stream(
10602
11031
  cloneMessagesForProvider(currentMessages),
10603
11032
  chatOptions
10604
- )) {
10605
- if (chunk.type === "text_delta") {
10606
- content2 += chunk.content;
10607
- this.emitEvent({ type: "token", data: chunk.content });
10608
- if (walker) {
10609
- walker.processChunk(chunk.content);
10610
- if (walker.consumeBoundary()) {
10611
- let parsed;
10612
- try {
10613
- parsed = parsePartialJson(extractJson(content2));
10614
- } catch {
10615
- parsed = void 0;
10616
- }
10617
- if (parsed !== void 0) {
10618
- this.emitEvent({
10619
- type: "partial_object",
10620
- agent: agent2._name,
10621
- attempt: currentAttempt,
10622
- data: { object: parsed }
10623
- });
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
+ }
10624
11064
  }
10625
11065
  }
10626
- }
10627
- } else if (chunk.type === "thinking_delta") {
10628
- thinkingContent += chunk.content;
10629
- } else if (chunk.type === "tool_call_delta") {
10630
- let buffer = toolCallBuffers.get(chunk.id);
10631
- if (!buffer) {
10632
- buffer = { id: chunk.id, name: "", arguments: "" };
10633
- toolCallBuffers.set(chunk.id, buffer);
10634
- }
10635
- if (chunk.name) buffer.name = chunk.name;
10636
- if (chunk.arguments) buffer.arguments += chunk.arguments;
10637
- } else if (chunk.type === "done") {
10638
- streamProviderMetadata = chunk.providerMetadata;
10639
- 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;
10640
11079
  response = {
10641
11080
  content: content2,
10642
11081
  tool_calls: void 0,
10643
11082
  usage: chunk.usage,
10644
- cost: chunk.cost
11083
+ cost: chunk.cost,
11084
+ providerMetadata: chunk.providerMetadata,
11085
+ timing: chunk.timing
10645
11086
  };
10646
11087
  }
10647
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;
10648
11098
  }
11099
+ throwIfRequestStalled();
10649
11100
  for (const buffer of toolCallBuffers.values()) {
10650
11101
  toolCalls.push({
10651
11102
  id: buffer.id,
@@ -10663,39 +11114,91 @@ ${appendedSchemaText}`;
10663
11114
  if (streamProviderMetadata) {
10664
11115
  response.providerMetadata = streamProviderMetadata;
10665
11116
  }
11117
+ if (streamTiming) {
11118
+ response.timing = streamTiming;
11119
+ }
10666
11120
  if (thinkingContent) {
10667
11121
  response.thinking_content = thinkingContent;
10668
11122
  }
10669
11123
  } else {
10670
- 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();
10671
11130
  }
10672
11131
  } catch (err) {
10673
- if (richRequest) this.recordRichFailure(err);
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;
10674
11135
  this.emitEvent({
10675
11136
  type: "agent_call_end",
10676
11137
  agent: agent2._name,
10677
11138
  model: effectiveModelUri,
10678
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 } : {},
10679
11145
  duration: Date.now() - turnStart,
11146
+ // Top-level beside `duration`, exactly as on the success path.
11147
+ ...failureTiming ? { timing: failureTiming } : {},
10680
11148
  data: {
10681
11149
  response: "",
10682
11150
  turn: turns,
10683
11151
  ...retryReason ? { retryReason } : {},
10684
- error: this._observerErrorProjection(err)?.message ?? legacyErrorMessage(err),
11152
+ error: this._observerErrorProjection(callError)?.message ?? legacyErrorMessage(callError),
10685
11153
  // Enrich with typed-error metadata when available. `body` is
10686
11154
  // deliberately NOT emitted — it's redaction-eligible (see
10687
11155
  // docs/security.md). `data.error` already carries the message.
10688
- ...providerErrorEventMetadata(err, richRequest)
11156
+ ...providerErrorEventMetadata(callError, richRequest)
10689
11157
  }
10690
11158
  });
10691
- throw err;
11159
+ throw callError;
11160
+ } finally {
11161
+ providerCallActive = false;
11162
+ clearStallTimer();
10692
11163
  }
10693
11164
  if (usageCapture && response.usage) {
10694
11165
  usageCapture.value = response.usage;
10695
11166
  }
11167
+ if (usageCapture && response.timing) {
11168
+ usageCapture.timing = response.timing;
11169
+ }
11170
+ if (chatOptions.promptCache && provider.realizesPromptCache?.(model) === true && response.usage && !this.promptCacheWarned) {
11171
+ const wrote = (response.usage.cache_write_tokens ?? 0) > 0;
11172
+ const read = (response.usage.cached_tokens ?? 0) > 0;
11173
+ if (read) {
11174
+ this.promptCacheColdWrites = 0;
11175
+ this.promptCacheInactive = 0;
11176
+ } else if (wrote) {
11177
+ this.promptCacheColdWrites += 1;
11178
+ this.promptCacheInactive = 0;
11179
+ } else {
11180
+ this.promptCacheInactive += 1;
11181
+ }
11182
+ let warning;
11183
+ if (this.promptCacheColdWrites >= PROMPT_CACHE_COLD_WRITE_WARN_AFTER) {
11184
+ warning = `Agent '${agent2._name}' has promptCache on, but its prefix was written to the cache ${this.promptCacheColdWrites} times in a row with no cache reads. Each write bills 1.25x input. If this agent's system prompt or tool set changes per call, set promptCache: false for it.`;
11185
+ } else if (this.promptCacheInactive >= PROMPT_CACHE_COLD_WRITE_WARN_AFTER) {
11186
+ warning = `Agent '${agent2._name}' has promptCache on, but ${this.promptCacheInactive} calls in a row showed no cache writes or reads. Anthropic silently ignores a breakpoint below the model's minimum cacheable length (4,096 tokens on Haiku 4.5; 1,024 or 512 on others) -- the system prompt plus tools may be too short to cache on this model, or the provider may not support prompt caching.`;
11187
+ }
11188
+ if (warning) {
11189
+ this.promptCacheWarned = true;
11190
+ this.emitEvent({ type: "log", agent: agent2._name, data: { warning } });
11191
+ }
11192
+ }
10696
11193
  if (response.cost) {
10697
11194
  this._accumulateBudgetCost(response.cost);
10698
11195
  }
11196
+ if (response.timing) {
11197
+ timingTotals.turns += 1;
11198
+ timingTotals.queuedMs += response.timing.queuedMs;
11199
+ timingTotals.retryMs += response.timing.retryMs;
11200
+ timingTotals.wireMs += response.timing.wireMs;
11201
+ }
10699
11202
  this.emitEvent({
10700
11203
  type: "agent_call_end",
10701
11204
  agent: agent2._name,
@@ -10710,6 +11213,8 @@ ${appendedSchemaText}`;
10710
11213
  cacheWrite: response.usage.cache_write_tokens
10711
11214
  } : void 0,
10712
11215
  duration: Date.now() - turnStart,
11216
+ // Additive beside `duration`, which keeps its turn-wall-clock meaning.
11217
+ ...response.timing ? { timing: response.timing } : {},
10713
11218
  data: {
10714
11219
  response: response.content,
10715
11220
  ...response.thinking_content ? { thinking: response.thinking_content } : {},
@@ -11428,6 +11933,7 @@ ${appendedSchemaText}`;
11428
11933
  if (this.summaryCache) {
11429
11934
  const summaryMsg2 = {
11430
11935
  role: "system",
11936
+ origin: "runtime",
11431
11937
  content: `Summary of earlier conversation:
11432
11938
  ${this.summaryCache}`
11433
11939
  };
@@ -11441,6 +11947,7 @@ ${this.summaryCache}`
11441
11947
  recentTokens2 += msgTokens;
11442
11948
  splitIdx2 = i;
11443
11949
  }
11950
+ splitIdx2 = nextUserTurn(history, splitIdx2) ?? history.length;
11444
11951
  if (splitIdx2 < history.length) {
11445
11952
  return [summaryMsg2, ...history.slice(splitIdx2)];
11446
11953
  }
@@ -11454,6 +11961,19 @@ ${this.summaryCache}`
11454
11961
  recentTokens += msgTokens;
11455
11962
  splitIdx = i;
11456
11963
  }
11964
+ const anchored = nextUserTurn(history, splitIdx);
11965
+ if (anchored !== void 0) {
11966
+ splitIdx = anchored;
11967
+ } else if (splitIdx === history.length && history.length > 1) {
11968
+ for (let i = history.length - 1; i > 0; i--) {
11969
+ if (history[i].role === "user") {
11970
+ splitIdx = i;
11971
+ break;
11972
+ }
11973
+ }
11974
+ } else {
11975
+ splitIdx = history.length;
11976
+ }
11457
11977
  if (splitIdx === 0) return history;
11458
11978
  const oldMessages = history.slice(0, splitIdx);
11459
11979
  const summaryModelUri = this.config.contextManagement?.summaryModel;
@@ -11485,6 +12005,7 @@ ${this.summaryCache}`
11485
12005
  }
11486
12006
  const summaryMsg = {
11487
12007
  role: "system",
12008
+ origin: "runtime",
11488
12009
  content: `Summary of earlier conversation:
11489
12010
  ${summaryResponse.content}`
11490
12011
  };
@@ -12163,46 +12684,52 @@ ${summaryResponse.content}`
12163
12684
  }
12164
12685
  // ── ctx.awaitHuman() ──────────────────────────────────────────────────
12165
12686
  async awaitHuman(options) {
12166
- if (this.spanManager) {
12167
- return this.spanManager.withSpanAsync(
12168
- "axl.ctx.awaitHuman",
12169
- {
12170
- "axl.awaitHuman.channel": options.channel
12171
- },
12172
- async (span) => {
12173
- const start = Date.now();
12174
- const result = await this._awaitHumanImpl(options);
12175
- span.setAttribute("axl.awaitHuman.wait_duration", Date.now() - start);
12176
- span.setAttribute("axl.awaitHuman.approved", result.approved);
12177
- 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;
12178
12716
  }
12179
- );
12717
+ }
12180
12718
  }
12181
- return this._awaitHumanImpl(options);
12182
12719
  }
12183
12720
  async _awaitHumanImpl(options) {
12184
12721
  const branchSignal = this.currentSignal;
12185
- const makeAbortError = (reason) => {
12186
- if (typeof DOMException !== "undefined") {
12187
- return new DOMException(
12188
- typeof reason === "string" ? reason : "awaitHuman aborted",
12189
- "AbortError"
12190
- );
12191
- }
12192
- const err = new Error(typeof reason === "string" ? reason : "awaitHuman aborted");
12193
- err.name = "AbortError";
12194
- return err;
12195
- };
12196
12722
  const awaitWithAbort = async (promise, signal2 = branchSignal) => {
12197
12723
  if (!signal2) return promise;
12198
- if (signal2.aborted) throw makeAbortError(signal2.reason);
12724
+ signal2.throwIfAborted();
12199
12725
  let onAbort;
12200
12726
  try {
12201
12727
  return await Promise.race([
12202
12728
  promise,
12203
12729
  new Promise((_, reject) => {
12204
- onAbort = () => reject(makeAbortError(signal2.reason));
12730
+ onAbort = () => reject(signal2.reason);
12205
12731
  signal2.addEventListener("abort", onAbort, { once: true });
12732
+ if (signal2.aborted) onAbort();
12206
12733
  })
12207
12734
  ]);
12208
12735
  } finally {
@@ -12210,12 +12737,12 @@ ${summaryResponse.content}`
12210
12737
  }
12211
12738
  };
12212
12739
  if (this.awaitHumanHandler) {
12213
- if (branchSignal?.aborted) throw makeAbortError(branchSignal.reason);
12740
+ branchSignal?.throwIfAborted();
12214
12741
  this.emitEvent({
12215
12742
  type: "await_human",
12216
12743
  data: { channel: options.channel, prompt: options.prompt }
12217
12744
  });
12218
- if (branchSignal?.aborted) throw makeAbortError(branchSignal.reason);
12745
+ branchSignal?.throwIfAborted();
12219
12746
  const decision2 = parseHumanDecision(
12220
12747
  await awaitWithAbort(Promise.resolve(this.awaitHumanHandler(options)))
12221
12748
  );
@@ -12270,12 +12797,12 @@ ${summaryResponse.content}`
12270
12797
  const abortListener = () => {
12271
12798
  registeredResolver.cancelled = true;
12272
12799
  markReady();
12273
- rejectDecision(makeAbortError(signal.reason));
12800
+ rejectDecision(signal.reason);
12274
12801
  };
12275
12802
  signal.addEventListener("abort", abortListener, { once: true });
12276
12803
  let decision;
12277
12804
  try {
12278
- if (signal.aborted) throw makeAbortError(signal.reason);
12805
+ signal.throwIfAborted();
12279
12806
  if (this.stateStore) {
12280
12807
  saveAttempted = true;
12281
12808
  await this.stateStore.savePendingDecision(this.executionId, {
@@ -12286,7 +12813,7 @@ ${summaryResponse.content}`
12286
12813
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
12287
12814
  });
12288
12815
  }
12289
- if (signal.aborted) throw makeAbortError(signal.reason);
12816
+ signal.throwIfAborted();
12290
12817
  this.emitEvent({
12291
12818
  type: "await_human",
12292
12819
  data: { channel: options.channel, prompt: options.prompt }
@@ -12756,7 +13283,7 @@ ${summaryResponse.content}`
12756
13283
  data: partial.data
12757
13284
  });
12758
13285
  };
12759
- const abortError3 = () => new DOMException("The transcription operation was aborted", "AbortError");
13286
+ const abortError2 = () => new DOMException("The transcription operation was aborted", "AbortError");
12760
13287
  const cleanupStatus = (value) => value === "not_required" || value === "deleted" || value === "failed" || value === "timed_out" ? value : void 0;
12761
13288
  const safeProviderFailure = (error, provider, model) => {
12762
13289
  const details = error && typeof error === "object" ? error : void 0;
@@ -12807,7 +13334,7 @@ ${summaryResponse.content}`
12807
13334
  hasWork: false
12808
13335
  })
12809
13336
  });
12810
- throw abortError3();
13337
+ throw abortError2();
12811
13338
  }
12812
13339
  if (this.budgetContext?.exceeded) {
12813
13340
  const { limit, totalCost: spent, policy } = this.budgetContext;
@@ -12873,7 +13400,7 @@ ${summaryResponse.content}`
12873
13400
  ...failure.cleanup ? [{ cleanupStatus: failure.cleanup }] : []
12874
13401
  )
12875
13402
  });
12876
- throw abortError3();
13403
+ throw abortError2();
12877
13404
  }
12878
13405
  if (failure.accounting.cost !== void 0)
12879
13406
  this._accumulateBudgetCost(failure.accounting.cost);
@@ -12916,7 +13443,7 @@ ${summaryResponse.content}`
12916
13443
  ...cleanupStatus(result.cleanupStatus) ? [{ cleanupStatus: cleanupStatus(result.cleanupStatus) }] : []
12917
13444
  )
12918
13445
  });
12919
- throw abortError3();
13446
+ throw abortError2();
12920
13447
  }
12921
13448
  let transcript;
12922
13449
  try {
@@ -13002,9 +13529,9 @@ ${summaryResponse.content}`
13002
13529
  const cost = partial.cost;
13003
13530
  if (COST_BEARING_LEAF_TYPES.includes(partial.type)) {
13004
13531
  const usable = isUsableCost(cost);
13005
- const unpriced = !usable && hasPositiveBillableWork(
13532
+ const unpriced = !usable && (partial.unpriced === true || hasPositiveBillableWork(
13006
13533
  partial
13007
- );
13534
+ ));
13008
13535
  if (frame) {
13009
13536
  if (usable) frame.askCost.value += cost;
13010
13537
  else if (unpriced) frame.askUnpriced = true;
@@ -13015,7 +13542,7 @@ ${summaryResponse.content}`
13015
13542
  if (!this.budgetContext.unpricedWarned && Number.isFinite(this.budgetContext.limit)) {
13016
13543
  this.budgetContext.unpricedWarned = true;
13017
13544
  console.warn(
13018
- "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."
13019
13546
  );
13020
13547
  }
13021
13548
  }
@@ -14984,12 +15511,13 @@ function forwardAbortSignal(external, internal) {
14984
15511
  if (!external) return () => {
14985
15512
  };
14986
15513
  if (external.aborted) {
14987
- internal.abort();
15514
+ internal.abort(external.reason);
14988
15515
  return () => {
14989
15516
  };
14990
15517
  }
14991
- const onAbort = () => internal.abort();
15518
+ const onAbort = () => internal.abort(external.reason);
14992
15519
  external.addEventListener("abort", onAbort, { once: true });
15520
+ if (external.aborted) onAbort();
14993
15521
  return () => external.removeEventListener("abort", onAbort);
14994
15522
  }
14995
15523
  var AxlRuntime = class extends EventEmitter2 {
@@ -16260,6 +16788,8 @@ var AxlRuntime = class extends EventEmitter2 {
16260
16788
  parent: parentScope
16261
16789
  };
16262
16790
  const modelCalls = /* @__PURE__ */ new Map();
16791
+ const modelTiming = /* @__PURE__ */ new Map();
16792
+ const captureTimingSamples = options?.captureTimingSamples === true;
16263
16793
  const workflowCalls = /* @__PURE__ */ new Map();
16264
16794
  const tokens = { input: 0, output: 0, reasoning: 0 };
16265
16795
  let agentCalls = 0;
@@ -16277,6 +16807,30 @@ var AxlRuntime = class extends EventEmitter2 {
16277
16807
  tokens.output += event.tokens.output ?? 0;
16278
16808
  tokens.reasoning += event.tokens.reasoning ?? 0;
16279
16809
  }
16810
+ const failed2 = event.data?.error != null;
16811
+ if (event.model && event.timing && !failed2) {
16812
+ const t = event.timing;
16813
+ let bucket = modelTiming.get(event.model);
16814
+ if (!bucket) {
16815
+ bucket = {
16816
+ calls: 0,
16817
+ queuedMs: 0,
16818
+ retryMs: 0,
16819
+ wireMs: 0,
16820
+ ...captureTimingSamples ? { samples: [] } : {}
16821
+ };
16822
+ modelTiming.set(event.model, bucket);
16823
+ }
16824
+ bucket.calls++;
16825
+ bucket.samples?.push(t);
16826
+ bucket.queuedMs += t.queuedMs;
16827
+ bucket.retryMs += t.retryMs;
16828
+ bucket.wireMs += t.wireMs;
16829
+ if (t.firstTokenMs != null) {
16830
+ bucket.firstTokenMs = (bucket.firstTokenMs ?? 0) + t.firstTokenMs;
16831
+ bucket.firstTokenCalls = (bucket.firstTokenCalls ?? 0) + 1;
16832
+ }
16833
+ }
16280
16834
  }
16281
16835
  if (event.type === "workflow_start" && event.workflow) {
16282
16836
  workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
@@ -16307,6 +16861,7 @@ var AxlRuntime = class extends EventEmitter2 {
16307
16861
  result,
16308
16862
  cost: scope.totalCost,
16309
16863
  unpriced,
16864
+ ...modelTiming.size > 0 ? { modelTiming: Object.fromEntries(modelTiming) } : {},
16310
16865
  ...capturedTraces ? { traces: capturedTraces } : {},
16311
16866
  metadata: {
16312
16867
  models: [...modelCalls.keys()],
@@ -17443,6 +17998,7 @@ export {
17443
17998
  SQLiteStore,
17444
17999
  Session,
17445
18000
  SqliteVectorStore,
18001
+ StallTimeoutError,
17446
18002
  ThinkTagScanner,
17447
18003
  TimeoutError,
17448
18004
  ToolFailure,