@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.cjs CHANGED
@@ -167,6 +167,7 @@ __export(index_exports, {
167
167
  SQLiteStore: () => SQLiteStore,
168
168
  Session: () => Session,
169
169
  SqliteVectorStore: () => SqliteVectorStore,
170
+ StallTimeoutError: () => StallTimeoutError,
170
171
  ThinkTagScanner: () => ThinkTagScanner,
171
172
  TimeoutError: () => TimeoutError,
172
173
  ToolFailure: () => ToolFailure,
@@ -392,9 +393,22 @@ var NoConsensus = class extends AxlError {
392
393
  }
393
394
  };
394
395
  var TimeoutError = class extends AxlError {
395
- constructor(operation, timeoutMs) {
396
- super("TIMEOUT", `${operation} exceeded timeout of ${timeoutMs}ms`);
396
+ /** Latency attribution for the timed-out operation, when measurable. */
397
+ breakdown;
398
+ constructor(operation, timeoutMs, breakdown, agent2) {
399
+ super(
400
+ "TIMEOUT",
401
+ `${operation} exceeded timeout of ${timeoutMs}ms` + (breakdown ? ` (elapsed ${breakdown.elapsedMs}ms: queued ${breakdown.queuedMs}ms, retries ${breakdown.retryMs}ms, wire ${breakdown.wireMs}ms, other ${breakdown.otherMs}ms)` : "") + (agent2 ? ` for agent '${agent2}'` : "")
402
+ );
397
403
  this.name = "TimeoutError";
404
+ if (breakdown) this.breakdown = breakdown;
405
+ }
406
+ };
407
+ var StallTimeoutError = class extends TimeoutError {
408
+ constructor(operation, timeoutMs, agent2) {
409
+ super(operation, timeoutMs, void 0, agent2);
410
+ this.name = "StallTimeoutError";
411
+ this.message = `${operation} provider request stalled for ${timeoutMs}ms` + (agent2 ? ` for agent '${agent2}'` : "");
398
412
  }
399
413
  };
400
414
  function formatBudgetCost(cost) {
@@ -941,6 +955,7 @@ var ProviderError = class extends AxlError {
941
955
  this.retryAfterMs = args.retryAfterMs;
942
956
  this.requestId = args.requestId;
943
957
  this.body = args.body;
958
+ if (args.timing) this.timing = args.timing;
944
959
  }
945
960
  };
946
961
  function isRetryableStatus(status) {
@@ -992,7 +1007,8 @@ function buildProviderError(args) {
992
1007
  message: args.message,
993
1008
  retryAfterMs: args.headers ? parseRetryAfter(args.headers) : void 0,
994
1009
  requestId: args.headers ? extractRequestId(args.headers) : void 0,
995
- body: args.body
1010
+ body: args.body,
1011
+ timing: args.timing
996
1012
  });
997
1013
  }
998
1014
 
@@ -1029,14 +1045,33 @@ async function fetchWithRetry(input, init, opts) {
1029
1045
  const maxRetries = opts?.maxRetries ?? MAX_RETRIES;
1030
1046
  const governor = opts?.governor;
1031
1047
  const provider = opts?.provider ?? "unknown";
1048
+ const observer = opts?.timing;
1032
1049
  let acquired = false;
1050
+ let queuedMs = 0;
1033
1051
  if (governor) {
1052
+ const acquireStart = Date.now();
1034
1053
  await governor.acquire(init?.signal ?? void 0);
1035
1054
  acquired = true;
1036
- }
1055
+ queuedMs = Date.now() - acquireStart;
1056
+ }
1057
+ let firstDispatchedAt = 0;
1058
+ let dispatchedAt = 0;
1059
+ let headersAt = 0;
1060
+ const reportComplete = (attempts) => {
1061
+ observer?.onComplete?.({
1062
+ queuedMs,
1063
+ attempts,
1064
+ retryMs: dispatchedAt - firstDispatchedAt,
1065
+ dispatchedAt,
1066
+ headersAt
1067
+ });
1068
+ };
1037
1069
  try {
1038
1070
  for (let attempt = 0; ; attempt++) {
1039
1071
  let res;
1072
+ dispatchedAt = Date.now();
1073
+ if (attempt === 0) firstDispatchedAt = dispatchedAt;
1074
+ observer?.onDispatch?.(attempt + 1, dispatchedAt);
1040
1075
  try {
1041
1076
  res = await fetch(input, { ...init, redirect: "manual" });
1042
1077
  } catch (err) {
@@ -1048,18 +1083,23 @@ async function fetchWithRetry(input, init, opts) {
1048
1083
  message: err instanceof Error ? err.message : String(err)
1049
1084
  });
1050
1085
  }
1086
+ observer?.onRetry?.(attempt + 1, Date.now());
1051
1087
  await sleep2(jitter(BASE_DELAY_MS * 2 ** attempt), init?.signal ?? void 0);
1052
1088
  continue;
1053
1089
  }
1090
+ headersAt = Date.now();
1054
1091
  governor?.observe(res);
1055
1092
  if (res.ok || !RETRYABLE_STATUS_CODES.has(res.status) || attempt >= maxRetries) {
1093
+ reportComplete(attempt + 1);
1056
1094
  return res;
1057
1095
  }
1058
1096
  if (init?.signal?.aborted) {
1097
+ reportComplete(attempt + 1);
1059
1098
  return res;
1060
1099
  }
1061
1100
  const retryAfterMs = parseRetryAfter(res.headers);
1062
1101
  const baseDelay = retryAfterMs !== void 0 ? Math.min(retryAfterMs, MAX_BACKOFF_MS) : BASE_DELAY_MS * 2 ** attempt;
1102
+ observer?.onRetry?.(attempt + 1, Date.now());
1063
1103
  await sleep2(jitter(baseDelay), init?.signal ?? void 0);
1064
1104
  }
1065
1105
  } finally {
@@ -1067,10 +1107,110 @@ async function fetchWithRetry(input, init, opts) {
1067
1107
  }
1068
1108
  }
1069
1109
 
1070
- // src/providers/rate-limiter.ts
1071
- function abortError() {
1072
- return Object.assign(new Error("The operation was aborted"), { name: "AbortError" });
1110
+ // src/providers/call-timing.ts
1111
+ var CallTimingRecorder = class {
1112
+ constructor(requestLifecycle) {
1113
+ this.requestLifecycle = requestLifecycle;
1114
+ }
1115
+ fetchTiming;
1116
+ firstTokenAt;
1117
+ /** Consumer suspension after pre-content chunks (for example, tool deltas). */
1118
+ preFirstTokenConsumerMs = 0;
1119
+ /** Cumulative time awaiting `reader.read()`, i.e. transport, not consumer. */
1120
+ readWaitMs = 0;
1121
+ /** Pass as `timing` in the `fetchWithRetry` options object. */
1122
+ observer = {
1123
+ onDispatch: () => {
1124
+ this.requestLifecycle?.onDispatch?.();
1125
+ },
1126
+ onRetry: () => {
1127
+ this.requestLifecycle?.onRetry?.();
1128
+ },
1129
+ onComplete: (timing) => {
1130
+ this.fetchTiming = timing;
1131
+ }
1132
+ };
1133
+ /**
1134
+ * Await one body read, charging only the wait itself to `wireMs`. Time the
1135
+ * consumer spends between reads is excluded because it elapses outside this
1136
+ * call.
1137
+ */
1138
+ async read(reader) {
1139
+ const start = Date.now();
1140
+ try {
1141
+ return await reader.read();
1142
+ } finally {
1143
+ this.readWaitMs += Date.now() - start;
1144
+ }
1145
+ }
1146
+ /** Stamp the first content delta. Idempotent — only the first call counts. */
1147
+ markFirstToken() {
1148
+ this.firstTokenAt ??= Date.now();
1149
+ }
1150
+ /** Exclude backpressure after a pre-content yield from first-content latency. */
1151
+ recordPreFirstTokenConsumerPause(startedAt) {
1152
+ if (this.firstTokenAt === void 0) {
1153
+ this.preFirstTokenConsumerMs += Math.max(0, Date.now() - startedAt);
1154
+ }
1155
+ }
1156
+ /**
1157
+ * Timing for a non-streaming call. Call once the response body is parsed.
1158
+ *
1159
+ * Also the right reading at an adapter's `!res.ok` throw site on BOTH
1160
+ * transports: headers have arrived, no body will be streamed, so
1161
+ * dispatch → now is the whole of what the provider cost us.
1162
+ */
1163
+ chatTiming() {
1164
+ const t = this.fetchTiming;
1165
+ if (!t) return void 0;
1166
+ return {
1167
+ queuedMs: t.queuedMs,
1168
+ attempts: t.attempts,
1169
+ retryMs: t.retryMs,
1170
+ ttfbMs: Math.max(0, t.headersAt - t.dispatchedAt),
1171
+ wireMs: Math.max(0, Date.now() - t.dispatchedAt)
1172
+ };
1173
+ }
1174
+ /** Timing for a streamed call. Call when building the terminal `done` chunk. */
1175
+ streamTiming() {
1176
+ const t = this.fetchTiming;
1177
+ if (!t) return void 0;
1178
+ const ttfbMs = Math.max(0, t.headersAt - t.dispatchedAt);
1179
+ const firstTokenMs = this.firstTokenAt !== void 0 ? Math.max(0, this.firstTokenAt - t.dispatchedAt - this.preFirstTokenConsumerMs) : void 0;
1180
+ return {
1181
+ queuedMs: t.queuedMs,
1182
+ attempts: t.attempts,
1183
+ retryMs: t.retryMs,
1184
+ ttfbMs,
1185
+ ...firstTokenMs !== void 0 ? { firstTokenMs } : {},
1186
+ // Parsing and generator scheduling can happen after the first read has
1187
+ // settled. Include that pre-first-token gap so the public relationship
1188
+ // stays intuitive, without charging pauses after any chunk is yielded.
1189
+ wireMs: Math.max(ttfbMs + this.readWaitMs, firstTokenMs ?? 0)
1190
+ };
1191
+ }
1192
+ };
1193
+ async function* withCallTiming(recorder, source) {
1194
+ for await (const chunk of source) {
1195
+ if (chunk.type === "text_delta" || chunk.type === "thinking_delta") {
1196
+ recorder.markFirstToken();
1197
+ yield chunk;
1198
+ } else if (chunk.type === "done") {
1199
+ const timing = recorder.streamTiming();
1200
+ yield timing ? { ...chunk, timing } : chunk;
1201
+ } else {
1202
+ const yieldedAt = Date.now();
1203
+ yield chunk;
1204
+ recorder.recordPreFirstTokenConsumerPause(yieldedAt);
1205
+ }
1206
+ }
1073
1207
  }
1208
+ function withChatTiming(recorder, response) {
1209
+ const timing = recorder.chatTiming();
1210
+ return timing ? { ...response, timing } : response;
1211
+ }
1212
+
1213
+ // src/providers/rate-limiter.ts
1074
1214
  var RateLimiter = class {
1075
1215
  maxConcurrent;
1076
1216
  minIntervalMs;
@@ -1119,11 +1259,11 @@ var RateLimiter = class {
1119
1259
  * With `acquireTimeoutMs`, a waiter that sits in the queue too long rejects.
1120
1260
  */
1121
1261
  acquire(signal) {
1122
- if (signal?.aborted) return Promise.reject(abortError());
1262
+ if (signal?.aborted) return Promise.reject(signal.reason);
1123
1263
  return new Promise((resolve2, reject) => {
1124
1264
  const waiter = { resolve: resolve2, reject, signal };
1125
1265
  if (signal) {
1126
- waiter.onAbort = () => this.settleWaiter(waiter, abortError());
1266
+ waiter.onAbort = () => this.settleWaiter(waiter, signal.reason);
1127
1267
  signal.addEventListener("abort", waiter.onAbort, { once: true });
1128
1268
  }
1129
1269
  if (this.acquireTimeoutMs != null) {
@@ -1423,6 +1563,7 @@ function normalizeMessageContent(content) {
1423
1563
  }
1424
1564
  var OpenAICompatibleProvider = class {
1425
1565
  name;
1566
+ reportsRequestLifecycle = true;
1426
1567
  inputCapabilities(model) {
1427
1568
  return this.name === "openrouter" && model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64"] } } : {};
1428
1569
  }
@@ -1519,6 +1660,7 @@ var OpenAICompatibleProvider = class {
1519
1660
  async chat(messages, options) {
1520
1661
  const headers = this.buildHeaders(await this.resolveKey());
1521
1662
  const body = this.buildRequestBody(messages, options, false);
1663
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
1522
1664
  const res = await fetchWithRetry(
1523
1665
  `${this.baseUrl}/chat/completions`,
1524
1666
  {
@@ -1527,7 +1669,7 @@ var OpenAICompatibleProvider = class {
1527
1669
  body: JSON.stringify(body),
1528
1670
  signal: options.signal
1529
1671
  },
1530
- { governor: this.governor, provider: this.name }
1672
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
1531
1673
  );
1532
1674
  if (!res.ok) {
1533
1675
  const errorBody = await res.text();
@@ -1536,15 +1678,20 @@ var OpenAICompatibleProvider = class {
1536
1678
  status: res.status,
1537
1679
  headers: res.headers,
1538
1680
  message: this.extractErrorMessage(errorBody, res.status),
1539
- body: errorBody
1681
+ body: errorBody,
1682
+ timing: recorder.chatTiming()
1540
1683
  });
1541
1684
  }
1542
1685
  const json = await res.json();
1543
- return this.parseResponse(json, this.requestModel(body, options.model), body);
1686
+ return withChatTiming(
1687
+ recorder,
1688
+ this.parseResponse(json, this.requestModel(body, options.model), body)
1689
+ );
1544
1690
  }
1545
1691
  async *stream(messages, options) {
1546
1692
  const headers = this.buildHeaders(await this.resolveKey());
1547
1693
  const body = this.buildRequestBody(messages, options, true);
1694
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
1548
1695
  const res = await fetchWithRetry(
1549
1696
  `${this.baseUrl}/chat/completions`,
1550
1697
  {
@@ -1553,7 +1700,7 @@ var OpenAICompatibleProvider = class {
1553
1700
  body: JSON.stringify(body),
1554
1701
  signal: options.signal
1555
1702
  },
1556
- { governor: this.governor, provider: this.name }
1703
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
1557
1704
  );
1558
1705
  if (!res.ok) {
1559
1706
  const errorBody = await res.text();
@@ -1562,13 +1709,17 @@ var OpenAICompatibleProvider = class {
1562
1709
  status: res.status,
1563
1710
  headers: res.headers,
1564
1711
  message: this.extractErrorMessage(errorBody, res.status),
1565
- body: errorBody
1712
+ body: errorBody,
1713
+ timing: recorder.chatTiming()
1566
1714
  });
1567
1715
  }
1568
1716
  if (!res.body) {
1569
1717
  throw new Error(`${this.profile.label ?? this.profile.name} stream response has no body`);
1570
1718
  }
1571
- yield* this.parseSSEStream(res.body, this.requestModel(body, options.model), body);
1719
+ yield* withCallTiming(
1720
+ recorder,
1721
+ this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
1722
+ );
1572
1723
  }
1573
1724
  // ---------------------------------------------------------------------------
1574
1725
  // Request building
@@ -1795,7 +1946,21 @@ var OpenAICompatibleProvider = class {
1795
1946
  // ---------------------------------------------------------------------------
1796
1947
  // SSE streaming
1797
1948
  // ---------------------------------------------------------------------------
1798
- async *parseSSEStream(body, model, request) {
1949
+ /**
1950
+ * PRIVATE, and deliberately not an extension point. It was `protected`, but
1951
+ * nothing in or out of this repo overrides it, and `protected` could not
1952
+ * carry the guarantee it appeared to: TypeScript checks an override for
1953
+ * assignability, and a function type with fewer parameters is assignable to
1954
+ * one with more — so an override that simply dropped the recorder still
1955
+ * compiled and then reported `wireMs === ttfbMs` for the whole subclass. Its
1956
+ * type is not exported from the barrel either, so a subclass author could not
1957
+ * name what they were required to accept.
1958
+ *
1959
+ * A provider with a different wire format implements `Provider` (see
1960
+ * docs/providers.md); an OpenAI-compatible one adds a `ProviderProfile`.
1961
+ * Neither needs to reach in here.
1962
+ */
1963
+ async *parseSSEStream(body, model, request, timing) {
1799
1964
  const reader = body.getReader();
1800
1965
  const decoder = new TextDecoder();
1801
1966
  let buffer = "";
@@ -1855,7 +2020,7 @@ var OpenAICompatibleProvider = class {
1855
2020
  };
1856
2021
  try {
1857
2022
  for (; ; ) {
1858
- const { done, value } = await reader.read();
2023
+ const { done, value } = await timing.read(reader);
1859
2024
  if (done) break;
1860
2025
  buffer += decoder.decode(value, { stream: true });
1861
2026
  const lines = buffer.split("\n");
@@ -1887,7 +2052,8 @@ var OpenAICompatibleProvider = class {
1887
2052
  status: 0,
1888
2053
  retryable: false,
1889
2054
  message,
1890
- body: JSON.stringify(parsed.error)
2055
+ body: JSON.stringify(parsed.error),
2056
+ timing: timing.streamTiming()
1891
2057
  });
1892
2058
  }
1893
2059
  if (parsed.model !== void 0 || parsed.service_tier !== void 0 || parsed.serviceTier !== void 0) {
@@ -1959,7 +2125,8 @@ var OpenAICompatibleProvider = class {
1959
2125
  provider: this.name,
1960
2126
  status: 0,
1961
2127
  retryable: true,
1962
- message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`
2128
+ message: `${this.profile.label ?? this.profile.name} stream ended before [DONE]`,
2129
+ timing: timing.streamTiming()
1963
2130
  });
1964
2131
  } finally {
1965
2132
  reader.releaseLock();
@@ -1998,12 +2165,12 @@ var DIRECT_OPENAI_CATALOG = [
1998
2165
  {
1999
2166
  aliases: ["gpt-5.6", "gpt-5.6-sol"],
2000
2167
  snapshotBase: "gpt-5.6-sol",
2001
- short: { input: 5 / M, cachedInput: 0.5 / M, cacheWrite: 6.25 / M, output: 30 / M },
2168
+ short: { input: 4 / M, cachedInput: 0.4 / M, cacheWrite: 5 / M, output: 20 / M },
2002
2169
  long: withLongContext({
2003
- input: 5 / M,
2004
- cachedInput: 0.5 / M,
2005
- cacheWrite: 6.25 / M,
2006
- output: 30 / M
2170
+ input: 4 / M,
2171
+ cachedInput: 0.4 / M,
2172
+ cacheWrite: 5 / M,
2173
+ output: 20 / M
2007
2174
  }),
2008
2175
  contextBoundary: LONG_CONTEXT_BOUNDARY
2009
2176
  },
@@ -2432,6 +2599,7 @@ function responseImageParts(parts) {
2432
2599
  }
2433
2600
  var OpenAIResponsesProvider = class {
2434
2601
  name = "openai-responses";
2602
+ reportsRequestLifecycle = true;
2435
2603
  inputCapabilities(model) {
2436
2604
  return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
2437
2605
  }
@@ -2518,6 +2686,7 @@ var OpenAIResponsesProvider = class {
2518
2686
  async chat(messages, options) {
2519
2687
  const headers = this.buildHeaders(await this.resolveKey());
2520
2688
  const body = this.buildRequestBody(messages, options, false);
2689
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
2521
2690
  const res = await fetchWithRetry(
2522
2691
  `${this.baseUrl}/responses`,
2523
2692
  {
@@ -2526,7 +2695,7 @@ var OpenAIResponsesProvider = class {
2526
2695
  body: JSON.stringify(body),
2527
2696
  signal: options.signal
2528
2697
  },
2529
- { governor: this.governor, provider: this.name }
2698
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
2530
2699
  );
2531
2700
  if (!res.ok) {
2532
2701
  const errorBody = await res.text();
@@ -2536,11 +2705,15 @@ var OpenAIResponsesProvider = class {
2536
2705
  status: res.status,
2537
2706
  headers: res.headers,
2538
2707
  message,
2539
- body: errorBody
2708
+ body: errorBody,
2709
+ timing: recorder.chatTiming()
2540
2710
  });
2541
2711
  }
2542
2712
  const json = await res.json();
2543
- return this.parseResponse(json, this.requestModel(body, options.model), body);
2713
+ return withChatTiming(
2714
+ recorder,
2715
+ this.parseResponse(json, this.requestModel(body, options.model), body)
2716
+ );
2544
2717
  }
2545
2718
  // ---------------------------------------------------------------------------
2546
2719
  // stream - SSE streaming
@@ -2548,6 +2721,7 @@ var OpenAIResponsesProvider = class {
2548
2721
  async *stream(messages, options) {
2549
2722
  const headers = this.buildHeaders(await this.resolveKey());
2550
2723
  const body = this.buildRequestBody(messages, options, true);
2724
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
2551
2725
  const res = await fetchWithRetry(
2552
2726
  `${this.baseUrl}/responses`,
2553
2727
  {
@@ -2556,7 +2730,7 @@ var OpenAIResponsesProvider = class {
2556
2730
  body: JSON.stringify(body),
2557
2731
  signal: options.signal
2558
2732
  },
2559
- { governor: this.governor, provider: this.name }
2733
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
2560
2734
  );
2561
2735
  if (!res.ok) {
2562
2736
  const errorBody = await res.text();
@@ -2566,13 +2740,17 @@ var OpenAIResponsesProvider = class {
2566
2740
  status: res.status,
2567
2741
  headers: res.headers,
2568
2742
  message,
2569
- body: errorBody
2743
+ body: errorBody,
2744
+ timing: recorder.chatTiming()
2570
2745
  });
2571
2746
  }
2572
2747
  if (!res.body) {
2573
2748
  throw new Error("OpenAI Responses stream has no body");
2574
2749
  }
2575
- yield* this.parseSSEStream(res.body, this.requestModel(body, options.model), body);
2750
+ yield* withCallTiming(
2751
+ recorder,
2752
+ this.parseSSEStream(res.body, this.requestModel(body, options.model), body, recorder)
2753
+ );
2576
2754
  }
2577
2755
  // ---------------------------------------------------------------------------
2578
2756
  // Internal: build request body
@@ -2774,7 +2952,7 @@ var OpenAIResponsesProvider = class {
2774
2952
  // ---------------------------------------------------------------------------
2775
2953
  // Internal: parse SSE stream
2776
2954
  // ---------------------------------------------------------------------------
2777
- async *parseSSEStream(body, model, request) {
2955
+ async *parseSSEStream(body, model, request, timing) {
2778
2956
  const reader = body.getReader();
2779
2957
  const decoder = new TextDecoder();
2780
2958
  let buffer = "";
@@ -2782,7 +2960,7 @@ var OpenAIResponsesProvider = class {
2782
2960
  let eventType = "";
2783
2961
  try {
2784
2962
  while (true) {
2785
- const { done, value } = await reader.read();
2963
+ const { done, value } = await timing.read(reader);
2786
2964
  if (done) break;
2787
2965
  buffer += decoder.decode(value, { stream: true });
2788
2966
  const lines = buffer.split("\n");
@@ -2802,7 +2980,14 @@ var OpenAIResponsesProvider = class {
2802
2980
  } catch {
2803
2981
  continue;
2804
2982
  }
2805
- const chunk = this.handleStreamEvent(eventType, data, model, callIdMap, request);
2983
+ const chunk = this.handleStreamEvent(
2984
+ eventType,
2985
+ data,
2986
+ model,
2987
+ callIdMap,
2988
+ request,
2989
+ timing
2990
+ );
2806
2991
  if (chunk) {
2807
2992
  yield chunk;
2808
2993
  if (chunk.type === "done") return;
@@ -2816,7 +3001,7 @@ var OpenAIResponsesProvider = class {
2816
3001
  reader.releaseLock();
2817
3002
  }
2818
3003
  }
2819
- handleStreamEvent(eventType, data, model, callIdMap, request) {
3004
+ handleStreamEvent(eventType, data, model, callIdMap, request, timing) {
2820
3005
  switch (eventType) {
2821
3006
  case "response.output_text.delta":
2822
3007
  return { type: "text_delta", content: data.delta ?? "" };
@@ -2872,7 +3057,8 @@ var OpenAIResponsesProvider = class {
2872
3057
  provider: this.name,
2873
3058
  status: 0,
2874
3059
  retryable: false,
2875
- message: `OpenAI Responses API error: ${errorMsg}`
3060
+ message: `OpenAI Responses API error: ${errorMsg}`,
3061
+ timing: timing.streamTiming()
2876
3062
  });
2877
3063
  }
2878
3064
  default:
@@ -2940,6 +3126,12 @@ function hasAnthropicProviderFileImage(messages) {
2940
3126
  );
2941
3127
  }
2942
3128
  var CLAUDE_CAPABILITIES = {
3129
+ "claude-fable-5-1": {
3130
+ thinking: "adaptive-always-on",
3131
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
3132
+ manualBudget: false,
3133
+ stripTemperature: true
3134
+ },
2943
3135
  "claude-fable-5": {
2944
3136
  thinking: "adaptive-always-on",
2945
3137
  effortLevels: ["low", "medium", "high", "xhigh", "max"],
@@ -3030,10 +3222,18 @@ var PRE_INFERENCE_GEO_MODELS = /* @__PURE__ */ new Set([
3030
3222
  function resolveClaudeCapability(model) {
3031
3223
  return CLAUDE_CAPABILITIES[model] ?? (LEGACY_CLAUDE_MODELS.has(model) ? LEGACY_CLAUDE_CAPABILITY : void 0);
3032
3224
  }
3225
+ var DEFAULT_CACHE_READ_MULTIPLIER = 0.1;
3033
3226
  var ANTHROPIC_RATES = {
3227
+ "claude-fable-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
3034
3228
  "claude-fable-5": { input: 1e-5, output: 5e-5 },
3229
+ // Limited availability (Glasswing). Priced from the public pricing table; no
3230
+ // CLAUDE_CAPABILITIES entry, because their thinking semantics are not
3231
+ // documented per-model and an unverified capability would send the wrong
3232
+ // request shape. Unknown IDs pass through unmodified, which is the safe side.
3233
+ "claude-mythos-5-1": { input: 1e-5, output: 5e-5, cacheReadMultiplier: 0.025 },
3234
+ "claude-mythos-5": { input: 1e-5, output: 5e-5 },
3035
3235
  "claude-opus-5": { input: 5e-6, output: 25e-6 },
3036
- "claude-sonnet-5": { input: 3e-6, output: 15e-6, sonnet5Intro: true },
3236
+ "claude-sonnet-5": { input: 2e-6, output: 1e-5 },
3037
3237
  "claude-opus-4-8": { input: 5e-6, output: 25e-6 },
3038
3238
  "claude-opus-4-7": { input: 5e-6, output: 25e-6 },
3039
3239
  "claude-opus-4-6": { input: 5e-6, output: 25e-6 },
@@ -3072,7 +3272,7 @@ function anthropicStreamErrorStatus(type) {
3072
3272
  return 0;
3073
3273
  }
3074
3274
  }
3075
- function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
3275
+ function estimateAnthropicCost(model, usage) {
3076
3276
  if (!model || !isValidTokenCount(usage.inputTokens) || !isValidTokenCount(usage.outputTokens)) {
3077
3277
  return void 0;
3078
3278
  }
@@ -3091,8 +3291,9 @@ function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
3091
3291
  if (usage.aggregateCacheWriteTokens !== void 0 && (!isValidTokenCount(usage.aggregateCacheWriteTokens) || usage.aggregateCacheWriteTokens !== cacheWriteTotal)) {
3092
3292
  return void 0;
3093
3293
  }
3094
- const { input, output } = rate.sonnet5Intro && now < /* @__PURE__ */ new Date("2026-09-01T00:00:00Z") ? { input: 2e-6, output: 1e-5 } : rate;
3095
- return usage.inputTokens * input + cacheRead * input * 0.1 + cacheWrite5m * input * 1.25 + cacheWrite1h * input * 2 + usage.outputTokens * output;
3294
+ const { input, output } = rate;
3295
+ const cacheReadRate = input * (rate.cacheReadMultiplier ?? DEFAULT_CACHE_READ_MULTIPLIER);
3296
+ return usage.inputTokens * input + cacheRead * cacheReadRate + cacheWrite5m * input * 1.25 + cacheWrite1h * input * 2 + usage.outputTokens * output;
3096
3297
  }
3097
3298
  var THINKING_BUDGETS = {
3098
3299
  low: 1024,
@@ -3324,6 +3525,7 @@ function mergeAnthropicUsage(initial, update) {
3324
3525
  }
3325
3526
  var AnthropicProvider = class {
3326
3527
  name = "anthropic";
3528
+ reportsRequestLifecycle = true;
3327
3529
  /** Report a clamped `effort`. Anthropic clamps to each model's supported
3328
3530
  * `output_config.effort` levels, and models that always think fall back to
3329
3531
  * adaptive `'low'` when asked to disable thinking. Only reported when the
@@ -3333,6 +3535,14 @@ var AnthropicProvider = class {
3333
3535
  const resolved = resolveThinkingOptions(options);
3334
3536
  if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
3335
3537
  const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
3538
+ if (ANTHROPIC_RATES[model] && !resolveClaudeCapability(model)) {
3539
+ return {
3540
+ requested: resolved.effort,
3541
+ effective: "unset",
3542
+ clamped: true,
3543
+ cause: `Axl has no capability entry for Anthropic ${model}, so no reasoning control was sent and effort '${resolved.effort}' had no effect`
3544
+ };
3545
+ }
3336
3546
  const config = resolveClaudeThinking(model, resolved);
3337
3547
  const wireEffort = typeof config.outputConfig?.effort === "string" ? config.outputConfig.effort : void 0;
3338
3548
  const effective = config.clamp?.effective ?? wireEffort;
@@ -3388,6 +3598,10 @@ var AnthropicProvider = class {
3388
3598
  }
3389
3599
  /** Anthropic ignores native `json_schema` structurally — Axl uses a system
3390
3600
  * prompt JSON instruction + client-side Zod validation (see buildRequest). */
3601
+ /** The only built-in adapter that honors `promptCache` (see buildBody). */
3602
+ realizesPromptCache() {
3603
+ return true;
3604
+ }
3391
3605
  nativeStructuredOutputSupport() {
3392
3606
  return "unsupported";
3393
3607
  }
@@ -3429,6 +3643,7 @@ var AnthropicProvider = class {
3429
3643
  );
3430
3644
  const body = this.buildRequestBody(messages, options, false);
3431
3645
  const pricingContext = pricingContextFromBody(body);
3646
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
3432
3647
  const res = await fetchWithRetry(
3433
3648
  `${this.baseUrl}/messages`,
3434
3649
  {
@@ -3437,7 +3652,7 @@ var AnthropicProvider = class {
3437
3652
  body: JSON.stringify(body),
3438
3653
  signal: options.signal
3439
3654
  },
3440
- { governor: this.governor, provider: this.name }
3655
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
3441
3656
  );
3442
3657
  if (!res.ok) {
3443
3658
  const errorBody = await res.text();
@@ -3447,11 +3662,12 @@ var AnthropicProvider = class {
3447
3662
  status: res.status,
3448
3663
  headers: res.headers,
3449
3664
  message,
3450
- body: errorBody
3665
+ body: errorBody,
3666
+ timing: recorder.chatTiming()
3451
3667
  });
3452
3668
  }
3453
3669
  const json = await res.json();
3454
- return this.parseResponse(json, pricingContext);
3670
+ return withChatTiming(recorder, this.parseResponse(json, pricingContext));
3455
3671
  }
3456
3672
  // ---------------------------------------------------------------------------
3457
3673
  // stream - SSE streaming completion
@@ -3463,6 +3679,7 @@ var AnthropicProvider = class {
3463
3679
  );
3464
3680
  const body = this.buildRequestBody(messages, options, true);
3465
3681
  const pricingContext = pricingContextFromBody(body);
3682
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
3466
3683
  const res = await fetchWithRetry(
3467
3684
  `${this.baseUrl}/messages`,
3468
3685
  {
@@ -3471,7 +3688,7 @@ var AnthropicProvider = class {
3471
3688
  body: JSON.stringify(body),
3472
3689
  signal: options.signal
3473
3690
  },
3474
- { governor: this.governor, provider: this.name }
3691
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
3475
3692
  );
3476
3693
  if (!res.ok) {
3477
3694
  const errorBody = await res.text();
@@ -3481,13 +3698,17 @@ var AnthropicProvider = class {
3481
3698
  status: res.status,
3482
3699
  headers: res.headers,
3483
3700
  message,
3484
- body: errorBody
3701
+ body: errorBody,
3702
+ timing: recorder.chatTiming()
3485
3703
  });
3486
3704
  }
3487
3705
  if (!res.body) {
3488
3706
  throw new Error("Anthropic stream response has no body");
3489
3707
  }
3490
- yield* this.parseSSEStream(res.body, pricingContext, res.headers);
3708
+ yield* withCallTiming(
3709
+ recorder,
3710
+ this.parseSSEStream(res.body, pricingContext, res.headers, recorder)
3711
+ );
3491
3712
  }
3492
3713
  // ---------------------------------------------------------------------------
3493
3714
  // Internal: request building
@@ -3522,14 +3743,31 @@ var AnthropicProvider = class {
3522
3743
  max_tokens: options.maxTokens ?? 4096,
3523
3744
  stream
3524
3745
  };
3525
- if (systemText) {
3746
+ const po = options.providerOptions;
3747
+ const callerOwnsPrefix = po !== void 0 && (po.cache_control !== void 0 || po.system !== void 0 || po.tools !== void 0);
3748
+ const promptCache = options.promptCache === true && !callerOwnsPrefix;
3749
+ const anchorOnSystem = promptCache && systemMessages.length > 0 && systemMessages[0].origin !== "runtime";
3750
+ if (anchorOnSystem) {
3751
+ body.system = systemMessages.map((m, index) => ({
3752
+ type: "text",
3753
+ text: typeof m.content === "string" ? m.content : "",
3754
+ ...index === 0 ? { cache_control: { type: "ephemeral" } } : {}
3755
+ }));
3756
+ } else if (systemText) {
3526
3757
  body.system = systemText;
3527
3758
  }
3528
3759
  if (options.stop) {
3529
3760
  body.stop_sequences = options.stop;
3530
3761
  }
3531
3762
  if (options.tools && options.tools.length > 0) {
3532
- body.tools = options.tools.map((t) => this.mapToolDefinition(t));
3763
+ const tools = options.tools.map((t) => this.mapToolDefinition(t));
3764
+ if (promptCache && !anchorOnSystem) {
3765
+ tools[tools.length - 1] = {
3766
+ ...tools[tools.length - 1],
3767
+ cache_control: { type: "ephemeral" }
3768
+ };
3769
+ }
3770
+ body.tools = tools;
3533
3771
  }
3534
3772
  if (options.toolChoice !== void 0) {
3535
3773
  body.tool_choice = this.mapToolChoice(options.toolChoice);
@@ -3548,9 +3786,13 @@ var AnthropicProvider = class {
3548
3786
  }
3549
3787
  if (options.responseFormat && options.responseFormat.type !== "text") {
3550
3788
  const jsonInstruction = "You must respond with valid JSON only. No markdown fences, no extra text.";
3551
- body.system = body.system ? `${body.system}
3789
+ if (Array.isArray(body.system)) {
3790
+ body.system = [...body.system, { type: "text", text: jsonInstruction }];
3791
+ } else {
3792
+ body.system = body.system ? `${body.system}
3552
3793
 
3553
3794
  ${jsonInstruction}` : jsonInstruction;
3795
+ }
3554
3796
  }
3555
3797
  if (options.providerOptions) {
3556
3798
  Object.assign(body, options.providerOptions);
@@ -3730,7 +3972,7 @@ ${jsonInstruction}` : jsonInstruction;
3730
3972
  // ---------------------------------------------------------------------------
3731
3973
  // Internal: SSE stream parsing
3732
3974
  // ---------------------------------------------------------------------------
3733
- async *parseSSEStream(body, pricingContext, responseHeaders) {
3975
+ async *parseSSEStream(body, pricingContext, responseHeaders, timing) {
3734
3976
  const reader = body.getReader();
3735
3977
  const decoder = new TextDecoder();
3736
3978
  let buffer = "";
@@ -3759,7 +4001,7 @@ ${jsonInstruction}` : jsonInstruction;
3759
4001
  });
3760
4002
  try {
3761
4003
  while (true) {
3762
- const { done, value } = await reader.read();
4004
+ const { done, value } = await timing.read(reader);
3763
4005
  if (done) break;
3764
4006
  buffer += decoder.decode(value, { stream: true });
3765
4007
  const lines = buffer.split("\n");
@@ -3782,7 +4024,8 @@ ${jsonInstruction}` : jsonInstruction;
3782
4024
  provider: this.name,
3783
4025
  status,
3784
4026
  headers: responseHeaders,
3785
- message: event.error?.message ?? "Anthropic stream failed"
4027
+ message: event.error?.message ?? "Anthropic stream failed",
4028
+ timing: timing.streamTiming()
3786
4029
  });
3787
4030
  }
3788
4031
  case "content_block_start": {
@@ -4018,7 +4261,6 @@ var GEMINI_PRICING = {
4018
4261
  },
4019
4262
  "gemini-2.5-flash": { input: 3e-7, cached: 3e-8, output: 25e-7 },
4020
4263
  "gemini-2.5-flash-lite": { input: 1e-7, cached: 1e-8, output: 4e-7 },
4021
- "gemini-3-flash-preview": { input: 5e-7, cached: 5e-8, output: 3e-6 },
4022
4264
  "gemini-3.1-pro-preview": {
4023
4265
  input: 2e-6,
4024
4266
  cached: 2e-7,
@@ -4034,7 +4276,9 @@ var GEMINI_PRICING = {
4034
4276
  "gemini-3.1-flash-lite": { input: 25e-8, cached: 25e-9, output: 15e-7 },
4035
4277
  "gemini-3.5-flash": { input: 15e-7, cached: 15e-8, output: 9e-6 },
4036
4278
  "gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
4037
- "gemini-3.6-flash": { input: 15e-7, cached: 15e-8, output: 75e-7 }
4279
+ "gemini-3.6-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
4280
+ "gemini-3.7-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 },
4281
+ "gemini-3.8-flash": { input: 75e-8, cached: 75e-9, output: 375e-8 }
4038
4282
  };
4039
4283
  function isValidTokenCount2(value) {
4040
4284
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
@@ -4262,6 +4506,7 @@ function resolveGeminiThinking(model, resolved) {
4262
4506
  }
4263
4507
  var GeminiProvider = class {
4264
4508
  name = "google";
4509
+ reportsRequestLifecycle = true;
4265
4510
  inputCapabilities(model) {
4266
4511
  return model.trim().length > 0 ? { image: { sources: ["bytes", "base64", "provider-file"] } } : {};
4267
4512
  }
@@ -4379,6 +4624,7 @@ var GeminiProvider = class {
4379
4624
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4380
4625
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4381
4626
  const headers = this.buildHeaders(await this.resolveKey());
4627
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4382
4628
  const res = await fetchWithRetry(
4383
4629
  `${this.baseUrl}/models/${pricingContext.model}:generateContent`,
4384
4630
  {
@@ -4387,7 +4633,7 @@ var GeminiProvider = class {
4387
4633
  body: JSON.stringify(body),
4388
4634
  signal: options.signal
4389
4635
  },
4390
- { governor: this.governor, provider: this.name }
4636
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4391
4637
  );
4392
4638
  if (!res.ok) {
4393
4639
  const errorBody = await res.text();
@@ -4397,11 +4643,12 @@ var GeminiProvider = class {
4397
4643
  status: res.status,
4398
4644
  headers: res.headers,
4399
4645
  message,
4400
- body: errorBody
4646
+ body: errorBody,
4647
+ timing: recorder.chatTiming()
4401
4648
  });
4402
4649
  }
4403
4650
  const json = await res.json();
4404
- return this.parseResponse(json, pricingContext);
4651
+ return withChatTiming(recorder, this.parseResponse(json, pricingContext));
4405
4652
  }
4406
4653
  // ---------------------------------------------------------------------------
4407
4654
  // stream - SSE streaming completion
@@ -4415,6 +4662,7 @@ var GeminiProvider = class {
4415
4662
  const pricingContext = this.pricingContext(this.requestModel(options), body);
4416
4663
  this.assertSafeGemini3Continuation(messages, pricingContext.model);
4417
4664
  const headers = this.buildHeaders(await this.resolveKey());
4665
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4418
4666
  const res = await fetchWithRetry(
4419
4667
  `${this.baseUrl}/models/${pricingContext.model}:streamGenerateContent?alt=sse`,
4420
4668
  {
@@ -4423,7 +4671,7 @@ var GeminiProvider = class {
4423
4671
  body: JSON.stringify(body),
4424
4672
  signal: options.signal
4425
4673
  },
4426
- { governor: this.governor, provider: this.name }
4674
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4427
4675
  );
4428
4676
  if (!res.ok) {
4429
4677
  const errorBody = await res.text();
@@ -4433,13 +4681,14 @@ var GeminiProvider = class {
4433
4681
  status: res.status,
4434
4682
  headers: res.headers,
4435
4683
  message,
4436
- body: errorBody
4684
+ body: errorBody,
4685
+ timing: recorder.chatTiming()
4437
4686
  });
4438
4687
  }
4439
4688
  if (!res.body) {
4440
4689
  throw new Error("Gemini stream response has no body");
4441
4690
  }
4442
- yield* this.parseSSEStream(res.body, pricingContext);
4691
+ yield* withCallTiming(recorder, this.parseSSEStream(res.body, pricingContext, recorder));
4443
4692
  }
4444
4693
  // ---------------------------------------------------------------------------
4445
4694
  // Rich image requests use Gemini Interactions. It is deliberately stateless:
@@ -4450,10 +4699,11 @@ var GeminiProvider = class {
4450
4699
  this.assertSafeInteractionOptions(options);
4451
4700
  const body = this.buildInteractionRequestBody(messages, options, false);
4452
4701
  const headers = this.buildHeaders(await this.resolveKey());
4702
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4453
4703
  const res = await fetchWithRetry(
4454
4704
  `${this.baseUrl}/interactions`,
4455
4705
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4456
- { governor: this.governor, provider: this.name }
4706
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4457
4707
  );
4458
4708
  if (!res.ok) {
4459
4709
  const errorBody = await res.text();
@@ -4462,19 +4712,24 @@ var GeminiProvider = class {
4462
4712
  status: res.status,
4463
4713
  headers: res.headers,
4464
4714
  message: this.extractErrorMessage(errorBody, res.status),
4465
- body: errorBody
4715
+ body: errorBody,
4716
+ timing: recorder.chatTiming()
4466
4717
  });
4467
4718
  }
4468
- return this.parseInteractionResponse(await res.json());
4719
+ return withChatTiming(
4720
+ recorder,
4721
+ this.parseInteractionResponse(await res.json())
4722
+ );
4469
4723
  }
4470
4724
  async *streamInteraction(messages, options) {
4471
4725
  this.assertSafeInteractionOptions(options);
4472
4726
  const body = this.buildInteractionRequestBody(messages, options, true);
4473
4727
  const headers = this.buildHeaders(await this.resolveKey());
4728
+ const recorder = new CallTimingRecorder(options.requestLifecycle);
4474
4729
  const res = await fetchWithRetry(
4475
4730
  `${this.baseUrl}/interactions?alt=sse`,
4476
4731
  { method: "POST", headers, body: JSON.stringify(body), signal: options.signal },
4477
- { governor: this.governor, provider: this.name }
4732
+ { governor: this.governor, provider: this.name, timing: recorder.observer }
4478
4733
  );
4479
4734
  if (!res.ok) {
4480
4735
  const errorBody = await res.text();
@@ -4483,11 +4738,15 @@ var GeminiProvider = class {
4483
4738
  status: res.status,
4484
4739
  headers: res.headers,
4485
4740
  message: this.extractErrorMessage(errorBody, res.status),
4486
- body: errorBody
4741
+ body: errorBody,
4742
+ timing: recorder.chatTiming()
4487
4743
  });
4488
4744
  }
4489
4745
  if (!res.body) throw new Error("Gemini Interactions stream has no body");
4490
- yield* this.parseInteractionSSEStream(res.body, res.headers);
4746
+ yield* withCallTiming(
4747
+ recorder,
4748
+ this.parseInteractionSSEStream(res.body, res.headers, recorder)
4749
+ );
4491
4750
  }
4492
4751
  buildInteractionRequestBody(messages, options, stream) {
4493
4752
  const model = this.requestModel(options);
@@ -4643,7 +4902,7 @@ var GeminiProvider = class {
4643
4902
  providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
4644
4903
  };
4645
4904
  }
4646
- async *parseInteractionSSEStream(body, responseHeaders) {
4905
+ async *parseInteractionSSEStream(body, responseHeaders, timing) {
4647
4906
  const reader = body.getReader();
4648
4907
  const decoder = new TextDecoder();
4649
4908
  let buffer = "";
@@ -4653,7 +4912,7 @@ var GeminiProvider = class {
4653
4912
  let completed = false;
4654
4913
  try {
4655
4914
  while (true) {
4656
- const { done, value } = await reader.read();
4915
+ const { done, value } = await timing.read(reader);
4657
4916
  if (done) break;
4658
4917
  buffer += decoder.decode(value, { stream: true });
4659
4918
  const lines = buffer.split("\n");
@@ -4676,14 +4935,16 @@ var GeminiProvider = class {
4676
4935
  provider: this.name,
4677
4936
  status,
4678
4937
  headers: responseHeaders,
4679
- message
4938
+ message,
4939
+ timing: timing.streamTiming()
4680
4940
  });
4681
4941
  }
4682
4942
  throw new ProviderError({
4683
4943
  provider: this.name,
4684
4944
  status: 0,
4685
4945
  retryable: false,
4686
- message
4946
+ message,
4947
+ timing: timing.streamTiming()
4687
4948
  });
4688
4949
  }
4689
4950
  if (event.event_type === "step.start" && event.step) {
@@ -5161,7 +5422,7 @@ var GeminiProvider = class {
5161
5422
  // ---------------------------------------------------------------------------
5162
5423
  // Internal: SSE stream parsing
5163
5424
  // ---------------------------------------------------------------------------
5164
- async *parseSSEStream(body, pricingContext) {
5425
+ async *parseSSEStream(body, pricingContext, timing) {
5165
5426
  const reader = body.getReader();
5166
5427
  const decoder = new TextDecoder();
5167
5428
  let buffer = "";
@@ -5174,7 +5435,7 @@ var GeminiProvider = class {
5174
5435
  const accumulatedParts = [];
5175
5436
  try {
5176
5437
  while (true) {
5177
- const { done, value } = await reader.read();
5438
+ const { done, value } = await timing.read(reader);
5178
5439
  if (done) break;
5179
5440
  buffer += decoder.decode(value, { stream: true });
5180
5441
  const lines = buffer.split("\n");
@@ -6183,7 +6444,7 @@ var GEMINI_AUDIO_MEDIA_TYPES = /* @__PURE__ */ new Set([
6183
6444
  "audio/mulaw",
6184
6445
  "audio/webm"
6185
6446
  ]);
6186
- function abortError2() {
6447
+ function abortError() {
6187
6448
  return new DOMException("The transcription operation was aborted", "AbortError");
6188
6449
  }
6189
6450
  function audioMime(audio) {
@@ -6215,13 +6476,13 @@ function secondsOffset(value) {
6215
6476
  function sleep3(ms, signal) {
6216
6477
  return new Promise((resolve2, reject) => {
6217
6478
  if (signal?.aborted) {
6218
- reject(abortError2());
6479
+ reject(abortError());
6219
6480
  return;
6220
6481
  }
6221
6482
  const timer = setTimeout(done, ms);
6222
6483
  const onAbort = () => {
6223
6484
  clearTimeout(timer);
6224
- done(abortError2());
6485
+ done(abortError());
6225
6486
  };
6226
6487
  function done(error) {
6227
6488
  signal?.removeEventListener("abort", onAbort);
@@ -7542,7 +7803,7 @@ function isCostBearingLeaf(event) {
7542
7803
  return COST_LEAF_SET.has(event.type);
7543
7804
  }
7544
7805
  function isUnpricedLeaf(event) {
7545
- return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && hasPositiveBillableWork(event);
7806
+ return event.type !== void 0 && COST_LEAF_SET.has(event.type) && !isUsableCost(event.cost) && (event.unpriced === true || hasPositiveBillableWork(event));
7546
7807
  }
7547
7808
  function isRootLevel(event) {
7548
7809
  const d = event.depth;
@@ -8828,18 +9089,27 @@ function parseDuration(duration) {
8828
9089
  if (!match) throw new Error(`Invalid duration format: "${duration}"`);
8829
9090
  const value = parseFloat(match[1]);
8830
9091
  const unit = match[2];
9092
+ let milliseconds;
8831
9093
  switch (unit) {
8832
9094
  case "ms":
8833
- return value;
9095
+ milliseconds = value;
9096
+ break;
8834
9097
  case "s":
8835
- return value * 1e3;
9098
+ milliseconds = value * 1e3;
9099
+ break;
8836
9100
  case "m":
8837
- return value * 6e4;
9101
+ milliseconds = value * 6e4;
9102
+ break;
8838
9103
  case "h":
8839
- return value * 36e5;
9104
+ milliseconds = value * 36e5;
9105
+ break;
8840
9106
  default:
8841
9107
  throw new Error(`Unknown duration unit: "${unit}"`);
8842
9108
  }
9109
+ if (!Number.isFinite(milliseconds)) {
9110
+ throw new Error(`Duration is too large: "${duration}"`);
9111
+ }
9112
+ return milliseconds;
8843
9113
  }
8844
9114
  function parseCost(cost) {
8845
9115
  const match = cost.match(/^\$?([\d.]+)$/);
@@ -9495,6 +9765,42 @@ function recordToolSpanOutcome(span, outcome) {
9495
9765
 
9496
9766
  // src/context.ts
9497
9767
  var signalStorage = new import_node_async_hooks.AsyncLocalStorage();
9768
+ var hardSignalStorage = new import_node_async_hooks.AsyncLocalStorage();
9769
+ var timeoutTrackerStorage = new import_node_async_hooks.AsyncLocalStorage();
9770
+ var MAX_TIMER_DELAY_MS = 2147483647;
9771
+ function composeAbortSignals(...signals) {
9772
+ const active = signals.filter((signal) => signal !== void 0);
9773
+ if (active.length === 0) return void 0;
9774
+ if (active.length === 1) return active[0];
9775
+ return AbortSignal.any(active);
9776
+ }
9777
+ function raceWithAbortSignal(promise, signal) {
9778
+ if (!signal) return promise;
9779
+ if (signal.aborted) {
9780
+ void promise.catch(() => {
9781
+ });
9782
+ return Promise.reject(signal.reason);
9783
+ }
9784
+ return new Promise((resolve2, reject) => {
9785
+ let settled = false;
9786
+ const finish = (callback) => {
9787
+ if (settled) return;
9788
+ settled = true;
9789
+ signal.removeEventListener("abort", onAbort);
9790
+ callback();
9791
+ };
9792
+ const onAbort = () => finish(() => reject(signal.reason));
9793
+ signal.addEventListener("abort", onAbort, { once: true });
9794
+ if (signal.aborted) onAbort();
9795
+ promise.then(
9796
+ (value) => finish(() => resolve2(value)),
9797
+ (error) => finish(() => reject(error))
9798
+ );
9799
+ });
9800
+ }
9801
+ function pausedHumanWaitMs(tracker, now = Date.now()) {
9802
+ return tracker.humanWaitMs + (tracker.activeHumanWaits > 0 && tracker.humanWaitStartedAt !== void 0 ? now - tracker.humanWaitStartedAt : 0);
9803
+ }
9498
9804
  var DEFAULT_BRANCH_DRAIN_TIMEOUT_MS = 5e3;
9499
9805
  function resolveBranchDrainTimeoutMs(value) {
9500
9806
  const resolved = value === void 0 ? DEFAULT_BRANCH_DRAIN_TIMEOUT_MS : value;
@@ -9684,6 +9990,13 @@ function describeValue(value) {
9684
9990
  }
9685
9991
  return `a value of type ${typeof value}`;
9686
9992
  }
9993
+ var PROMPT_CACHE_COLD_WRITE_WARN_AFTER = 2;
9994
+ function nextUserTurn(history, from) {
9995
+ for (let i = Math.max(0, from); i < history.length; i++) {
9996
+ if (history[i].role === "user") return i;
9997
+ }
9998
+ return void 0;
9999
+ }
9687
10000
  function estimateMessagesTokens(messages) {
9688
10001
  let total = 0;
9689
10002
  let unmeasured = false;
@@ -9823,6 +10136,13 @@ var WorkflowContext = class _WorkflowContext {
9823
10136
  workflowLifecycleState;
9824
10137
  signal;
9825
10138
  summaryCache;
10139
+ /** Consecutive provider calls with promptCache on that wrote to the cache
10140
+ * without reading from it (prefix changes per call), and consecutive calls
10141
+ * with no cache activity at all (prefix below the model's minimum, which
10142
+ * Anthropic silently ignores). Either reports once per context. */
10143
+ promptCacheColdWrites = 0;
10144
+ promptCacheInactive = 0;
10145
+ promptCacheWarned = false;
9826
10146
  workflowName;
9827
10147
  mcpManager;
9828
10148
  spanManager;
@@ -10282,7 +10602,10 @@ var WorkflowContext = class _WorkflowContext {
10282
10602
  tool_call_id: parsed.callId
10283
10603
  });
10284
10604
  }
10285
- if (settlement.abortError !== void 0) throw settlement.abortError;
10605
+ if (settlement.abortError !== void 0) {
10606
+ signal?.throwIfAborted();
10607
+ throw settlement.abortError;
10608
+ }
10286
10609
  }
10287
10610
  // ── ctx.ask() ─────────────────────────────────────────────────────────
10288
10611
  async ask(agent2, prompt, options) {
@@ -10298,7 +10621,10 @@ var WorkflowContext = class _WorkflowContext {
10298
10621
  const promptText = inputText(normalizedInput);
10299
10622
  const inputDescriptor = describeModelInput(normalizedInput);
10300
10623
  const agentName = agent2._name;
10301
- return this._checkpoint(
10624
+ const inheritedHardSignal = hardSignalStorage.getStore() ?? this.signal;
10625
+ const hardAskSignal = composeAbortSignals(inheritedHardSignal, options?.signal);
10626
+ const askSignal = composeAbortSignals(this.currentSignal, options?.signal);
10627
+ const runAsk = () => this._checkpoint(
10302
10628
  this._autoCheckpointName("ask", agentName),
10303
10629
  async () => {
10304
10630
  const parentFrame = askStorage.getStore();
@@ -10355,6 +10681,17 @@ var WorkflowContext = class _WorkflowContext {
10355
10681
  const costAfter2 = this.budgetContext?.totalCost ?? 0;
10356
10682
  span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
10357
10683
  span.setAttribute("axl.agent.duration", Date.now() - askStart);
10684
+ if (usageCapture.timing) {
10685
+ const t = usageCapture.timing;
10686
+ span.setAttribute("axl.agent.queued_ms", t.queuedMs);
10687
+ span.setAttribute("axl.agent.retry_ms", t.retryMs);
10688
+ span.setAttribute("axl.agent.attempts", t.attempts);
10689
+ span.setAttribute("axl.agent.ttfb_ms", t.ttfbMs);
10690
+ span.setAttribute("axl.agent.wire_ms", t.wireMs);
10691
+ if (t.firstTokenMs !== void 0) {
10692
+ span.setAttribute("axl.agent.first_token_ms", t.firstTokenMs);
10693
+ }
10694
+ }
10358
10695
  if (usageCapture.value) {
10359
10696
  span.setAttribute(
10360
10697
  "axl.agent.prompt_tokens",
@@ -10442,8 +10779,25 @@ var WorkflowContext = class _WorkflowContext {
10442
10779
  },
10443
10780
  { agent: agentName }
10444
10781
  );
10782
+ const runWithAskSignal = () => askSignal ? signalStorage.run(askSignal, runAsk) : runAsk();
10783
+ return hardAskSignal ? hardSignalStorage.run(hardAskSignal, runWithAskSignal) : runWithAskSignal();
10445
10784
  }
10446
10785
  async executeAgentCall(agent2, input, options, handoffMessages, usageCapture, sessionHistory = normalizeSessionHistory(this.sessionHistory)) {
10786
+ const tracker = { humanWaitMs: 0, activeHumanWaits: 0 };
10787
+ const enclosing = timeoutTrackerStorage.getStore() ?? [];
10788
+ return timeoutTrackerStorage.run(
10789
+ [...enclosing, tracker],
10790
+ () => this.executeAgentCallImpl(
10791
+ agent2,
10792
+ input,
10793
+ options,
10794
+ handoffMessages,
10795
+ usageCapture,
10796
+ sessionHistory
10797
+ )
10798
+ );
10799
+ }
10800
+ async executeAgentCallImpl(agent2, input, options, handoffMessages, usageCapture, sessionHistory = normalizeSessionHistory(this.sessionHistory)) {
10447
10801
  if (this.budgetContext?.exceeded) {
10448
10802
  const { limit, totalCost: spent, policy } = this.budgetContext;
10449
10803
  if (policy === "warn") {
@@ -10562,6 +10916,14 @@ var WorkflowContext = class _WorkflowContext {
10562
10916
  const toolTokens = toolDefs.length > 0 ? estimateTokens(JSON.stringify(toolDefs)) : 0;
10563
10917
  const overhead = systemTokens + toolTokens + reserveTokens;
10564
10918
  const availableForHistory = maxContext - overhead;
10919
+ if (availableForHistory <= 0) {
10920
+ this.emitEvent({
10921
+ type: "log",
10922
+ data: {
10923
+ 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.`
10924
+ }
10925
+ });
10926
+ }
10565
10927
  const historyEstimate = estimateMessagesTokens(sessionHistory);
10566
10928
  if (historyEstimate.unmeasured) {
10567
10929
  this.emitEvent({
@@ -10627,6 +10989,7 @@ ${appendedSchemaText}`;
10627
10989
  if (handoffContext.length > 0) {
10628
10990
  messages.push({
10629
10991
  role: "system",
10992
+ origin: "runtime",
10630
10993
  content: "The following is the conversation history from the previous agent that handed off to you:"
10631
10994
  });
10632
10995
  for (const msg of handoffContext) {
@@ -10674,8 +11037,19 @@ ${appendedSchemaText}`;
10674
11037
  }
10675
11038
  }
10676
11039
  const maxTurns = agent2._config.maxTurns ?? 25;
10677
- const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
11040
+ const timeoutMs = parseDuration(
11041
+ options?.timeout ?? agent2._config.timeout ?? this.config.defaults?.timeout ?? "60s"
11042
+ );
11043
+ const stallTimeout = options?.stallTimeout ?? agent2._config.stallTimeout ?? this.config.defaults?.stallTimeout;
11044
+ const stallTimeoutMs = stallTimeout === void 0 ? void 0 : parseDuration(stallTimeout);
11045
+ if (stallTimeoutMs !== void 0 && stallTimeoutMs > MAX_TIMER_DELAY_MS) {
11046
+ throw new RangeError(
11047
+ `stallTimeout exceeds the maximum supported timer duration of ${MAX_TIMER_DELAY_MS}ms`
11048
+ );
11049
+ }
10678
11050
  const startTime = Date.now();
11051
+ const timeoutTracker = timeoutTrackerStorage.getStore()?.at(-1);
11052
+ const timingTotals = { turns: 0, queuedMs: 0, retryMs: 0, wireMs: 0 };
10679
11053
  const currentMessages = [...messages];
10680
11054
  let turns = 0;
10681
11055
  let guardrailOutputRetries = 0;
@@ -10689,8 +11063,24 @@ ${appendedSchemaText}`;
10689
11063
  let lastStartMaxAttempts = 1;
10690
11064
  while (turns < maxTurns) {
10691
11065
  this.currentSignal?.throwIfAborted();
10692
- if (Date.now() - startTime > timeoutMs) {
10693
- throw new TimeoutError("ctx.ask()", timeoutMs);
11066
+ const now = Date.now();
11067
+ const elapsedMs = now - startTime - (timeoutTracker ? pausedHumanWaitMs(timeoutTracker, now) : 0);
11068
+ if (elapsedMs > timeoutMs) {
11069
+ throw new TimeoutError(
11070
+ "ctx.ask()",
11071
+ timeoutMs,
11072
+ timingTotals.turns > 0 ? {
11073
+ elapsedMs,
11074
+ queuedMs: timingTotals.queuedMs,
11075
+ retryMs: timingTotals.retryMs,
11076
+ wireMs: timingTotals.wireMs,
11077
+ otherMs: Math.max(
11078
+ 0,
11079
+ elapsedMs - timingTotals.queuedMs - timingTotals.retryMs - timingTotals.wireMs
11080
+ )
11081
+ } : void 0,
11082
+ agent2._name
11083
+ );
10694
11084
  }
10695
11085
  turns++;
10696
11086
  const isFirstTurn = turns === 1;
@@ -10730,11 +11120,49 @@ ${appendedSchemaText}`;
10730
11120
  effort: options?.effort ?? agent2._config.effort,
10731
11121
  thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
10732
11122
  includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
11123
+ promptCache: options?.promptCache ?? agent2._config.promptCache,
10733
11124
  toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
10734
11125
  stop: options?.stop ?? agent2._config.stop,
10735
11126
  providerOptions,
10736
11127
  signal: this.currentSignal
10737
11128
  };
11129
+ const requestSignal = chatOptions.signal;
11130
+ const hardRequestSignal = hardSignalStorage.getStore() ?? this.signal;
11131
+ const stallController = stallTimeoutMs === void 0 ? void 0 : new AbortController();
11132
+ const stallReason = stallController === void 0 ? void 0 : new DOMException("Provider request stalled", "StallTimeout");
11133
+ let stallTimer;
11134
+ let providerCallActive = true;
11135
+ let requestDispatched = false;
11136
+ const clearStallTimer = () => {
11137
+ if (stallTimer !== void 0) {
11138
+ clearTimeout(stallTimer);
11139
+ stallTimer = void 0;
11140
+ }
11141
+ };
11142
+ const armStallTimer = () => {
11143
+ if (!providerCallActive || !stallController || !stallReason || stallTimeoutMs === void 0)
11144
+ return;
11145
+ requestDispatched = true;
11146
+ clearStallTimer();
11147
+ if (stallController.signal.aborted) return;
11148
+ stallTimer = setTimeout(() => stallController.abort(stallReason), stallTimeoutMs);
11149
+ };
11150
+ if (stallController) {
11151
+ chatOptions.signal = composeAbortSignals(requestSignal, stallController.signal);
11152
+ chatOptions.requestLifecycle = {
11153
+ onDispatch: armStallTimer,
11154
+ onRetry: clearStallTimer
11155
+ };
11156
+ }
11157
+ const providerSettlementSignal = composeAbortSignals(
11158
+ hardRequestSignal,
11159
+ stallController?.signal
11160
+ );
11161
+ const throwIfRequestStalled = () => {
11162
+ if (stallReason !== void 0 && providerSettlementSignal?.reason === stallReason) {
11163
+ throw new StallTimeoutError("ctx.ask()", stallTimeoutMs, agent2._name);
11164
+ }
11165
+ };
10738
11166
  if (options?.schema && toolDefs.length === 0) {
10739
11167
  const nativeOut = options?.nativeStructuredOutput ?? agent2._config.nativeStructuredOutput ?? false;
10740
11168
  chatOptions.responseFormat = nativeOut ? {
@@ -10807,6 +11235,7 @@ ${appendedSchemaText}`;
10807
11235
  const toolCalls = [];
10808
11236
  const toolCallBuffers = /* @__PURE__ */ new Map();
10809
11237
  let streamProviderMetadata;
11238
+ let streamTiming;
10810
11239
  let thinkingContent = "";
10811
11240
  const streamingObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof import_zod2.z.ZodObject;
10812
11241
  const currentAttempt = schemaRetries + 1;
@@ -10820,54 +11249,77 @@ ${appendedSchemaText}`;
10820
11249
  });
10821
11250
  }
10822
11251
  }) : void 0;
10823
- for await (const chunk of provider.stream(
11252
+ if (stallController && provider.reportsRequestLifecycle !== true) armStallTimer();
11253
+ const providerStream = provider.stream(
10824
11254
  cloneMessagesForProvider(currentMessages),
10825
11255
  chatOptions
10826
- )) {
10827
- if (chunk.type === "text_delta") {
10828
- content2 += chunk.content;
10829
- this.emitEvent({ type: "token", data: chunk.content });
10830
- if (walker) {
10831
- walker.processChunk(chunk.content);
10832
- if (walker.consumeBoundary()) {
10833
- let parsed;
10834
- try {
10835
- parsed = parsePartialJson(extractJson(content2));
10836
- } catch {
10837
- parsed = void 0;
10838
- }
10839
- if (parsed !== void 0) {
10840
- this.emitEvent({
10841
- type: "partial_object",
10842
- agent: agent2._name,
10843
- attempt: currentAttempt,
10844
- data: { object: parsed }
10845
- });
11256
+ );
11257
+ const iterator = providerStream[Symbol.asyncIterator]();
11258
+ try {
11259
+ while (true) {
11260
+ const next = await raceWithAbortSignal(
11261
+ Promise.resolve(iterator.next()),
11262
+ providerSettlementSignal
11263
+ );
11264
+ if (next.done) break;
11265
+ const chunk = next.value;
11266
+ if (stallController) armStallTimer();
11267
+ if (chunk.type === "text_delta") {
11268
+ content2 += chunk.content;
11269
+ this.emitEvent({ type: "token", data: chunk.content });
11270
+ if (walker) {
11271
+ walker.processChunk(chunk.content);
11272
+ if (walker.consumeBoundary()) {
11273
+ let parsed;
11274
+ try {
11275
+ parsed = parsePartialJson(extractJson(content2));
11276
+ } catch {
11277
+ parsed = void 0;
11278
+ }
11279
+ if (parsed !== void 0) {
11280
+ this.emitEvent({
11281
+ type: "partial_object",
11282
+ agent: agent2._name,
11283
+ attempt: currentAttempt,
11284
+ data: { object: parsed }
11285
+ });
11286
+ }
10846
11287
  }
10847
11288
  }
10848
- }
10849
- } else if (chunk.type === "thinking_delta") {
10850
- thinkingContent += chunk.content;
10851
- } else if (chunk.type === "tool_call_delta") {
10852
- let buffer = toolCallBuffers.get(chunk.id);
10853
- if (!buffer) {
10854
- buffer = { id: chunk.id, name: "", arguments: "" };
10855
- toolCallBuffers.set(chunk.id, buffer);
10856
- }
10857
- if (chunk.name) buffer.name = chunk.name;
10858
- if (chunk.arguments) buffer.arguments += chunk.arguments;
10859
- } else if (chunk.type === "done") {
10860
- streamProviderMetadata = chunk.providerMetadata;
10861
- if (chunk.usage) {
11289
+ } else if (chunk.type === "thinking_delta") {
11290
+ thinkingContent += chunk.content;
11291
+ } else if (chunk.type === "tool_call_delta") {
11292
+ let buffer = toolCallBuffers.get(chunk.id);
11293
+ if (!buffer) {
11294
+ buffer = { id: chunk.id, name: "", arguments: "" };
11295
+ toolCallBuffers.set(chunk.id, buffer);
11296
+ }
11297
+ if (chunk.name) buffer.name = chunk.name;
11298
+ if (chunk.arguments) buffer.arguments += chunk.arguments;
11299
+ } else if (chunk.type === "done") {
11300
+ streamProviderMetadata = chunk.providerMetadata;
11301
+ streamTiming = chunk.timing;
10862
11302
  response = {
10863
11303
  content: content2,
10864
11304
  tool_calls: void 0,
10865
11305
  usage: chunk.usage,
10866
- cost: chunk.cost
11306
+ cost: chunk.cost,
11307
+ providerMetadata: chunk.providerMetadata,
11308
+ timing: chunk.timing
10867
11309
  };
10868
11310
  }
10869
11311
  }
11312
+ } catch (error) {
11313
+ if (iterator.return) {
11314
+ try {
11315
+ void Promise.resolve(iterator.return(void 0)).catch(() => {
11316
+ });
11317
+ } catch {
11318
+ }
11319
+ }
11320
+ throw error;
10870
11321
  }
11322
+ throwIfRequestStalled();
10871
11323
  for (const buffer of toolCallBuffers.values()) {
10872
11324
  toolCalls.push({
10873
11325
  id: buffer.id,
@@ -10885,39 +11337,91 @@ ${appendedSchemaText}`;
10885
11337
  if (streamProviderMetadata) {
10886
11338
  response.providerMetadata = streamProviderMetadata;
10887
11339
  }
11340
+ if (streamTiming) {
11341
+ response.timing = streamTiming;
11342
+ }
10888
11343
  if (thinkingContent) {
10889
11344
  response.thinking_content = thinkingContent;
10890
11345
  }
10891
11346
  } else {
10892
- response = await provider.chat(cloneMessagesForProvider(currentMessages), chatOptions);
11347
+ if (stallController && provider.reportsRequestLifecycle !== true) armStallTimer();
11348
+ response = await raceWithAbortSignal(
11349
+ provider.chat(cloneMessagesForProvider(currentMessages), chatOptions),
11350
+ providerSettlementSignal
11351
+ );
11352
+ throwIfRequestStalled();
10893
11353
  }
10894
11354
  } catch (err) {
10895
- if (richRequest) this.recordRichFailure(err);
11355
+ const callError = stallReason !== void 0 && providerSettlementSignal?.reason === stallReason ? new StallTimeoutError("ctx.ask()", stallTimeoutMs, agent2._name) : err;
11356
+ if (richRequest) this.recordRichFailure(callError);
11357
+ const failureTiming = callError instanceof ProviderError ? callError.timing : void 0;
10896
11358
  this.emitEvent({
10897
11359
  type: "agent_call_end",
10898
11360
  agent: agent2._name,
10899
11361
  model: effectiveModelUri,
10900
11362
  promptVersion: agent2._config.version,
11363
+ // A dispatched request abandoned at the runtime boundary may still
11364
+ // be billed even though a non-cooperative provider supplied no
11365
+ // usage. Preserve budget honesty by marking the known total as a
11366
+ // lower bound rather than presenting an exact $0.
11367
+ ...callError instanceof StallTimeoutError && requestDispatched ? { unpriced: true } : {},
10901
11368
  duration: Date.now() - turnStart,
11369
+ // Top-level beside `duration`, exactly as on the success path.
11370
+ ...failureTiming ? { timing: failureTiming } : {},
10902
11371
  data: {
10903
11372
  response: "",
10904
11373
  turn: turns,
10905
11374
  ...retryReason ? { retryReason } : {},
10906
- error: this._observerErrorProjection(err)?.message ?? legacyErrorMessage(err),
11375
+ error: this._observerErrorProjection(callError)?.message ?? legacyErrorMessage(callError),
10907
11376
  // Enrich with typed-error metadata when available. `body` is
10908
11377
  // deliberately NOT emitted — it's redaction-eligible (see
10909
11378
  // docs/security.md). `data.error` already carries the message.
10910
- ...providerErrorEventMetadata(err, richRequest)
11379
+ ...providerErrorEventMetadata(callError, richRequest)
10911
11380
  }
10912
11381
  });
10913
- throw err;
11382
+ throw callError;
11383
+ } finally {
11384
+ providerCallActive = false;
11385
+ clearStallTimer();
10914
11386
  }
10915
11387
  if (usageCapture && response.usage) {
10916
11388
  usageCapture.value = response.usage;
10917
11389
  }
11390
+ if (usageCapture && response.timing) {
11391
+ usageCapture.timing = response.timing;
11392
+ }
11393
+ if (chatOptions.promptCache && provider.realizesPromptCache?.(model) === true && response.usage && !this.promptCacheWarned) {
11394
+ const wrote = (response.usage.cache_write_tokens ?? 0) > 0;
11395
+ const read = (response.usage.cached_tokens ?? 0) > 0;
11396
+ if (read) {
11397
+ this.promptCacheColdWrites = 0;
11398
+ this.promptCacheInactive = 0;
11399
+ } else if (wrote) {
11400
+ this.promptCacheColdWrites += 1;
11401
+ this.promptCacheInactive = 0;
11402
+ } else {
11403
+ this.promptCacheInactive += 1;
11404
+ }
11405
+ let warning;
11406
+ if (this.promptCacheColdWrites >= PROMPT_CACHE_COLD_WRITE_WARN_AFTER) {
11407
+ 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.`;
11408
+ } else if (this.promptCacheInactive >= PROMPT_CACHE_COLD_WRITE_WARN_AFTER) {
11409
+ 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.`;
11410
+ }
11411
+ if (warning) {
11412
+ this.promptCacheWarned = true;
11413
+ this.emitEvent({ type: "log", agent: agent2._name, data: { warning } });
11414
+ }
11415
+ }
10918
11416
  if (response.cost) {
10919
11417
  this._accumulateBudgetCost(response.cost);
10920
11418
  }
11419
+ if (response.timing) {
11420
+ timingTotals.turns += 1;
11421
+ timingTotals.queuedMs += response.timing.queuedMs;
11422
+ timingTotals.retryMs += response.timing.retryMs;
11423
+ timingTotals.wireMs += response.timing.wireMs;
11424
+ }
10921
11425
  this.emitEvent({
10922
11426
  type: "agent_call_end",
10923
11427
  agent: agent2._name,
@@ -10932,6 +11436,8 @@ ${appendedSchemaText}`;
10932
11436
  cacheWrite: response.usage.cache_write_tokens
10933
11437
  } : void 0,
10934
11438
  duration: Date.now() - turnStart,
11439
+ // Additive beside `duration`, which keeps its turn-wall-clock meaning.
11440
+ ...response.timing ? { timing: response.timing } : {},
10935
11441
  data: {
10936
11442
  response: response.content,
10937
11443
  ...response.thinking_content ? { thinking: response.thinking_content } : {},
@@ -11650,6 +12156,7 @@ ${appendedSchemaText}`;
11650
12156
  if (this.summaryCache) {
11651
12157
  const summaryMsg2 = {
11652
12158
  role: "system",
12159
+ origin: "runtime",
11653
12160
  content: `Summary of earlier conversation:
11654
12161
  ${this.summaryCache}`
11655
12162
  };
@@ -11663,6 +12170,7 @@ ${this.summaryCache}`
11663
12170
  recentTokens2 += msgTokens;
11664
12171
  splitIdx2 = i;
11665
12172
  }
12173
+ splitIdx2 = nextUserTurn(history, splitIdx2) ?? history.length;
11666
12174
  if (splitIdx2 < history.length) {
11667
12175
  return [summaryMsg2, ...history.slice(splitIdx2)];
11668
12176
  }
@@ -11676,6 +12184,19 @@ ${this.summaryCache}`
11676
12184
  recentTokens += msgTokens;
11677
12185
  splitIdx = i;
11678
12186
  }
12187
+ const anchored = nextUserTurn(history, splitIdx);
12188
+ if (anchored !== void 0) {
12189
+ splitIdx = anchored;
12190
+ } else if (splitIdx === history.length && history.length > 1) {
12191
+ for (let i = history.length - 1; i > 0; i--) {
12192
+ if (history[i].role === "user") {
12193
+ splitIdx = i;
12194
+ break;
12195
+ }
12196
+ }
12197
+ } else {
12198
+ splitIdx = history.length;
12199
+ }
11679
12200
  if (splitIdx === 0) return history;
11680
12201
  const oldMessages = history.slice(0, splitIdx);
11681
12202
  const summaryModelUri = this.config.contextManagement?.summaryModel;
@@ -11707,6 +12228,7 @@ ${this.summaryCache}`
11707
12228
  }
11708
12229
  const summaryMsg = {
11709
12230
  role: "system",
12231
+ origin: "runtime",
11710
12232
  content: `Summary of earlier conversation:
11711
12233
  ${summaryResponse.content}`
11712
12234
  };
@@ -12385,46 +12907,52 @@ ${summaryResponse.content}`
12385
12907
  }
12386
12908
  // ── ctx.awaitHuman() ──────────────────────────────────────────────────
12387
12909
  async awaitHuman(options) {
12388
- if (this.spanManager) {
12389
- return this.spanManager.withSpanAsync(
12390
- "axl.ctx.awaitHuman",
12391
- {
12392
- "axl.awaitHuman.channel": options.channel
12393
- },
12394
- async (span) => {
12395
- const start = Date.now();
12396
- const result = await this._awaitHumanImpl(options);
12397
- span.setAttribute("axl.awaitHuman.wait_duration", Date.now() - start);
12398
- span.setAttribute("axl.awaitHuman.approved", result.approved);
12399
- return result;
12910
+ const trackers = timeoutTrackerStorage.getStore();
12911
+ const startedAt = Date.now();
12912
+ for (const tracker of trackers ?? []) {
12913
+ if (tracker.activeHumanWaits++ === 0) tracker.humanWaitStartedAt = startedAt;
12914
+ }
12915
+ try {
12916
+ if (this.spanManager) {
12917
+ return await this.spanManager.withSpanAsync(
12918
+ "axl.ctx.awaitHuman",
12919
+ {
12920
+ "axl.awaitHuman.channel": options.channel
12921
+ },
12922
+ async (span) => {
12923
+ const start = Date.now();
12924
+ const result = await this._awaitHumanImpl(options);
12925
+ span.setAttribute("axl.awaitHuman.wait_duration", Date.now() - start);
12926
+ span.setAttribute("axl.awaitHuman.approved", result.approved);
12927
+ return result;
12928
+ }
12929
+ );
12930
+ }
12931
+ return await this._awaitHumanImpl(options);
12932
+ } finally {
12933
+ const finishedAt = Date.now();
12934
+ for (const tracker of trackers ?? []) {
12935
+ tracker.activeHumanWaits--;
12936
+ if (tracker.activeHumanWaits === 0 && tracker.humanWaitStartedAt !== void 0) {
12937
+ tracker.humanWaitMs += finishedAt - tracker.humanWaitStartedAt;
12938
+ tracker.humanWaitStartedAt = void 0;
12400
12939
  }
12401
- );
12940
+ }
12402
12941
  }
12403
- return this._awaitHumanImpl(options);
12404
12942
  }
12405
12943
  async _awaitHumanImpl(options) {
12406
12944
  const branchSignal = this.currentSignal;
12407
- const makeAbortError = (reason) => {
12408
- if (typeof DOMException !== "undefined") {
12409
- return new DOMException(
12410
- typeof reason === "string" ? reason : "awaitHuman aborted",
12411
- "AbortError"
12412
- );
12413
- }
12414
- const err = new Error(typeof reason === "string" ? reason : "awaitHuman aborted");
12415
- err.name = "AbortError";
12416
- return err;
12417
- };
12418
12945
  const awaitWithAbort = async (promise, signal2 = branchSignal) => {
12419
12946
  if (!signal2) return promise;
12420
- if (signal2.aborted) throw makeAbortError(signal2.reason);
12947
+ signal2.throwIfAborted();
12421
12948
  let onAbort;
12422
12949
  try {
12423
12950
  return await Promise.race([
12424
12951
  promise,
12425
12952
  new Promise((_, reject) => {
12426
- onAbort = () => reject(makeAbortError(signal2.reason));
12953
+ onAbort = () => reject(signal2.reason);
12427
12954
  signal2.addEventListener("abort", onAbort, { once: true });
12955
+ if (signal2.aborted) onAbort();
12428
12956
  })
12429
12957
  ]);
12430
12958
  } finally {
@@ -12432,12 +12960,12 @@ ${summaryResponse.content}`
12432
12960
  }
12433
12961
  };
12434
12962
  if (this.awaitHumanHandler) {
12435
- if (branchSignal?.aborted) throw makeAbortError(branchSignal.reason);
12963
+ branchSignal?.throwIfAborted();
12436
12964
  this.emitEvent({
12437
12965
  type: "await_human",
12438
12966
  data: { channel: options.channel, prompt: options.prompt }
12439
12967
  });
12440
- if (branchSignal?.aborted) throw makeAbortError(branchSignal.reason);
12968
+ branchSignal?.throwIfAborted();
12441
12969
  const decision2 = parseHumanDecision(
12442
12970
  await awaitWithAbort(Promise.resolve(this.awaitHumanHandler(options)))
12443
12971
  );
@@ -12492,12 +13020,12 @@ ${summaryResponse.content}`
12492
13020
  const abortListener = () => {
12493
13021
  registeredResolver.cancelled = true;
12494
13022
  markReady();
12495
- rejectDecision(makeAbortError(signal.reason));
13023
+ rejectDecision(signal.reason);
12496
13024
  };
12497
13025
  signal.addEventListener("abort", abortListener, { once: true });
12498
13026
  let decision;
12499
13027
  try {
12500
- if (signal.aborted) throw makeAbortError(signal.reason);
13028
+ signal.throwIfAborted();
12501
13029
  if (this.stateStore) {
12502
13030
  saveAttempted = true;
12503
13031
  await this.stateStore.savePendingDecision(this.executionId, {
@@ -12508,7 +13036,7 @@ ${summaryResponse.content}`
12508
13036
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
12509
13037
  });
12510
13038
  }
12511
- if (signal.aborted) throw makeAbortError(signal.reason);
13039
+ signal.throwIfAborted();
12512
13040
  this.emitEvent({
12513
13041
  type: "await_human",
12514
13042
  data: { channel: options.channel, prompt: options.prompt }
@@ -12978,7 +13506,7 @@ ${summaryResponse.content}`
12978
13506
  data: partial.data
12979
13507
  });
12980
13508
  };
12981
- const abortError3 = () => new DOMException("The transcription operation was aborted", "AbortError");
13509
+ const abortError2 = () => new DOMException("The transcription operation was aborted", "AbortError");
12982
13510
  const cleanupStatus = (value) => value === "not_required" || value === "deleted" || value === "failed" || value === "timed_out" ? value : void 0;
12983
13511
  const safeProviderFailure = (error, provider, model) => {
12984
13512
  const details = error && typeof error === "object" ? error : void 0;
@@ -13029,7 +13557,7 @@ ${summaryResponse.content}`
13029
13557
  hasWork: false
13030
13558
  })
13031
13559
  });
13032
- throw abortError3();
13560
+ throw abortError2();
13033
13561
  }
13034
13562
  if (this.budgetContext?.exceeded) {
13035
13563
  const { limit, totalCost: spent, policy } = this.budgetContext;
@@ -13095,7 +13623,7 @@ ${summaryResponse.content}`
13095
13623
  ...failure.cleanup ? [{ cleanupStatus: failure.cleanup }] : []
13096
13624
  )
13097
13625
  });
13098
- throw abortError3();
13626
+ throw abortError2();
13099
13627
  }
13100
13628
  if (failure.accounting.cost !== void 0)
13101
13629
  this._accumulateBudgetCost(failure.accounting.cost);
@@ -13138,7 +13666,7 @@ ${summaryResponse.content}`
13138
13666
  ...cleanupStatus(result.cleanupStatus) ? [{ cleanupStatus: cleanupStatus(result.cleanupStatus) }] : []
13139
13667
  )
13140
13668
  });
13141
- throw abortError3();
13669
+ throw abortError2();
13142
13670
  }
13143
13671
  let transcript;
13144
13672
  try {
@@ -13224,9 +13752,9 @@ ${summaryResponse.content}`
13224
13752
  const cost = partial.cost;
13225
13753
  if (COST_BEARING_LEAF_TYPES.includes(partial.type)) {
13226
13754
  const usable = isUsableCost(cost);
13227
- const unpriced = !usable && hasPositiveBillableWork(
13755
+ const unpriced = !usable && (partial.unpriced === true || hasPositiveBillableWork(
13228
13756
  partial
13229
- );
13757
+ ));
13230
13758
  if (frame) {
13231
13759
  if (usable) frame.askCost.value += cost;
13232
13760
  else if (unpriced) frame.askUnpriced = true;
@@ -13237,7 +13765,7 @@ ${summaryResponse.content}`
13237
13765
  if (!this.budgetContext.unpricedWarned && Number.isFinite(this.budgetContext.limit)) {
13238
13766
  this.budgetContext.unpricedWarned = true;
13239
13767
  console.warn(
13240
- "Budget honesty: unpriced work detected \u2014 this budget's cost limit is a lower bound and is NOT enforced on unpriced models. See docs/observability.md#budget-honesty."
13768
+ "Budget honesty: unknown-cost work detected \u2014 this budget's cost limit is a lower bound and is NOT enforced on unknown spend. See docs/observability.md#budget-honesty."
13241
13769
  );
13242
13770
  }
13243
13771
  }
@@ -15206,12 +15734,13 @@ function forwardAbortSignal(external, internal) {
15206
15734
  if (!external) return () => {
15207
15735
  };
15208
15736
  if (external.aborted) {
15209
- internal.abort();
15737
+ internal.abort(external.reason);
15210
15738
  return () => {
15211
15739
  };
15212
15740
  }
15213
- const onAbort = () => internal.abort();
15741
+ const onAbort = () => internal.abort(external.reason);
15214
15742
  external.addEventListener("abort", onAbort, { once: true });
15743
+ if (external.aborted) onAbort();
15215
15744
  return () => external.removeEventListener("abort", onAbort);
15216
15745
  }
15217
15746
  var AxlRuntime = class extends import_node_events2.EventEmitter {
@@ -16482,6 +17011,8 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
16482
17011
  parent: parentScope
16483
17012
  };
16484
17013
  const modelCalls = /* @__PURE__ */ new Map();
17014
+ const modelTiming = /* @__PURE__ */ new Map();
17015
+ const captureTimingSamples = options?.captureTimingSamples === true;
16485
17016
  const workflowCalls = /* @__PURE__ */ new Map();
16486
17017
  const tokens = { input: 0, output: 0, reasoning: 0 };
16487
17018
  let agentCalls = 0;
@@ -16499,6 +17030,30 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
16499
17030
  tokens.output += event.tokens.output ?? 0;
16500
17031
  tokens.reasoning += event.tokens.reasoning ?? 0;
16501
17032
  }
17033
+ const failed2 = event.data?.error != null;
17034
+ if (event.model && event.timing && !failed2) {
17035
+ const t = event.timing;
17036
+ let bucket = modelTiming.get(event.model);
17037
+ if (!bucket) {
17038
+ bucket = {
17039
+ calls: 0,
17040
+ queuedMs: 0,
17041
+ retryMs: 0,
17042
+ wireMs: 0,
17043
+ ...captureTimingSamples ? { samples: [] } : {}
17044
+ };
17045
+ modelTiming.set(event.model, bucket);
17046
+ }
17047
+ bucket.calls++;
17048
+ bucket.samples?.push(t);
17049
+ bucket.queuedMs += t.queuedMs;
17050
+ bucket.retryMs += t.retryMs;
17051
+ bucket.wireMs += t.wireMs;
17052
+ if (t.firstTokenMs != null) {
17053
+ bucket.firstTokenMs = (bucket.firstTokenMs ?? 0) + t.firstTokenMs;
17054
+ bucket.firstTokenCalls = (bucket.firstTokenCalls ?? 0) + 1;
17055
+ }
17056
+ }
16502
17057
  }
16503
17058
  if (event.type === "workflow_start" && event.workflow) {
16504
17059
  workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
@@ -16529,6 +17084,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
16529
17084
  result,
16530
17085
  cost: scope.totalCost,
16531
17086
  unpriced,
17087
+ ...modelTiming.size > 0 ? { modelTiming: Object.fromEntries(modelTiming) } : {},
16532
17088
  ...capturedTraces ? { traces: capturedTraces } : {},
16533
17089
  metadata: {
16534
17090
  models: [...modelCalls.keys()],
@@ -17666,6 +18222,7 @@ function cosineSimilarity2(a, b) {
17666
18222
  SQLiteStore,
17667
18223
  Session,
17668
18224
  SqliteVectorStore,
18225
+ StallTimeoutError,
17669
18226
  ThinkTagScanner,
17670
18227
  TimeoutError,
17671
18228
  ToolFailure,