agentfootprint 11.2.0 → 11.4.0

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.
Files changed (259) hide show
  1. package/AGENTS.md +1 -1
  2. package/CAPABILITIES.md +4 -0
  3. package/ai-instructions/claude-code/SKILL.md +1 -1
  4. package/dist/adapters/identity/verify/claims.js +3 -3
  5. package/dist/adapters/mcp/agentcoreRuntime/invoke.js +3 -2
  6. package/dist/adapters/mcp/agentcoreRuntime/invoke.js.map +1 -1
  7. package/dist/adapters/mcp/agentcoreRuntime/sessionBusy.js +6 -5
  8. package/dist/adapters/mcp/agentcoreRuntime/sessionBusy.js.map +1 -1
  9. package/dist/adapters/mcp/agentcoreRuntime/transport.js +8 -6
  10. package/dist/adapters/mcp/agentcoreRuntime/transport.js.map +1 -1
  11. package/dist/answer-account/account.js +15 -3
  12. package/dist/answer-account/account.js.map +1 -1
  13. package/dist/answer-account/facts/howSure.js +10 -22
  14. package/dist/answer-account/facts/howSure.js.map +1 -1
  15. package/dist/core/Agent.js +26 -3
  16. package/dist/core/Agent.js.map +1 -1
  17. package/dist/core/agent/AgentBuilder.js +69 -12
  18. package/dist/core/agent/AgentBuilder.js.map +1 -1
  19. package/dist/core/agent/assessment/assess.js +15 -4
  20. package/dist/core/agent/assessment/assess.js.map +1 -1
  21. package/dist/core/agent/assessment/compose.js +153 -15
  22. package/dist/core/agent/assessment/compose.js.map +1 -1
  23. package/dist/core/agent/assessment/reasons.js +35 -1
  24. package/dist/core/agent/assessment/reasons.js.map +1 -1
  25. package/dist/core/agent/assessment/sources.js +205 -0
  26. package/dist/core/agent/assessment/sources.js.map +1 -0
  27. package/dist/core/agent/assessment/stage.js +15 -4
  28. package/dist/core/agent/assessment/stage.js.map +1 -1
  29. package/dist/core/agent/buildAgentChart.js.map +1 -1
  30. package/dist/core/agent/buildToolRegistry.js +4 -1
  31. package/dist/core/agent/buildToolRegistry.js.map +1 -1
  32. package/dist/core/agent/evidence/evidenceIndex.js +30 -2
  33. package/dist/core/agent/evidence/evidenceIndex.js.map +1 -1
  34. package/dist/core/agent/evidence/extract.js +34 -5
  35. package/dist/core/agent/evidence/extract.js.map +1 -1
  36. package/dist/core/agent/evidence/figures.js +31 -3
  37. package/dist/core/agent/evidence/figures.js.map +1 -1
  38. package/dist/core/agent/evidence/numberPrecision.js +58 -2
  39. package/dist/core/agent/evidence/numberPrecision.js.map +1 -1
  40. package/dist/core/agent/honesty/mounts.js.map +1 -1
  41. package/dist/core/agent/stages/prepareFinal.js +33 -17
  42. package/dist/core/agent/stages/prepareFinal.js.map +1 -1
  43. package/dist/core/agent/stages/route.js +2 -1
  44. package/dist/core/agent/stages/route.js.map +1 -1
  45. package/dist/core/agent/stages/toolCalls.js +14 -1
  46. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  47. package/dist/core/slots/buildToolsSlot.js +1 -1
  48. package/dist/core/slots/buildToolsSlot.js.map +1 -1
  49. package/dist/esm/adapters/identity/jwks.d.ts +2 -2
  50. package/dist/esm/adapters/identity/verify/claims.d.ts +3 -3
  51. package/dist/esm/adapters/identity/verify/claims.js +3 -3
  52. package/dist/esm/adapters/mcp/agentcoreRuntime/invoke.d.ts +3 -2
  53. package/dist/esm/adapters/mcp/agentcoreRuntime/invoke.js +3 -2
  54. package/dist/esm/adapters/mcp/agentcoreRuntime/invoke.js.map +1 -1
  55. package/dist/esm/adapters/mcp/agentcoreRuntime/sessionBusy.d.ts +6 -5
  56. package/dist/esm/adapters/mcp/agentcoreRuntime/sessionBusy.js +6 -5
  57. package/dist/esm/adapters/mcp/agentcoreRuntime/sessionBusy.js.map +1 -1
  58. package/dist/esm/adapters/mcp/agentcoreRuntime/transport.d.ts +3 -1
  59. package/dist/esm/adapters/mcp/agentcoreRuntime/transport.js +9 -7
  60. package/dist/esm/adapters/mcp/agentcoreRuntime/transport.js.map +1 -1
  61. package/dist/esm/answer-account/account.js +16 -4
  62. package/dist/esm/answer-account/account.js.map +1 -1
  63. package/dist/esm/answer-account/facts/howSure.js +10 -22
  64. package/dist/esm/answer-account/facts/howSure.js.map +1 -1
  65. package/dist/esm/core/Agent.d.ts +2 -1
  66. package/dist/esm/core/Agent.js +26 -3
  67. package/dist/esm/core/Agent.js.map +1 -1
  68. package/dist/esm/core/agent/AgentBuilder.d.ts +22 -1
  69. package/dist/esm/core/agent/AgentBuilder.js +69 -12
  70. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  71. package/dist/esm/core/agent/assessment/assess.js +15 -4
  72. package/dist/esm/core/agent/assessment/assess.js.map +1 -1
  73. package/dist/esm/core/agent/assessment/compose.d.ts +82 -5
  74. package/dist/esm/core/agent/assessment/compose.js +148 -14
  75. package/dist/esm/core/agent/assessment/compose.js.map +1 -1
  76. package/dist/esm/core/agent/assessment/reasons.d.ts +17 -1
  77. package/dist/esm/core/agent/assessment/reasons.js +35 -1
  78. package/dist/esm/core/agent/assessment/reasons.js.map +1 -1
  79. package/dist/esm/core/agent/assessment/sources.d.ts +66 -0
  80. package/dist/esm/core/agent/assessment/sources.js +201 -0
  81. package/dist/esm/core/agent/assessment/sources.js.map +1 -0
  82. package/dist/esm/core/agent/assessment/stage.d.ts +17 -4
  83. package/dist/esm/core/agent/assessment/stage.js +16 -5
  84. package/dist/esm/core/agent/assessment/stage.js.map +1 -1
  85. package/dist/esm/core/agent/assessment/types.d.ts +37 -0
  86. package/dist/esm/core/agent/buildAgentChart.d.ts +8 -0
  87. package/dist/esm/core/agent/buildAgentChart.js.map +1 -1
  88. package/dist/esm/core/agent/buildToolRegistry.js +5 -2
  89. package/dist/esm/core/agent/buildToolRegistry.js.map +1 -1
  90. package/dist/esm/core/agent/evidence/evidenceIndex.d.ts +12 -2
  91. package/dist/esm/core/agent/evidence/evidenceIndex.js +30 -2
  92. package/dist/esm/core/agent/evidence/evidenceIndex.js.map +1 -1
  93. package/dist/esm/core/agent/evidence/extract.js +35 -6
  94. package/dist/esm/core/agent/evidence/extract.js.map +1 -1
  95. package/dist/esm/core/agent/evidence/figures.d.ts +6 -0
  96. package/dist/esm/core/agent/evidence/figures.js +32 -4
  97. package/dist/esm/core/agent/evidence/figures.js.map +1 -1
  98. package/dist/esm/core/agent/evidence/numberPrecision.d.ts +29 -1
  99. package/dist/esm/core/agent/evidence/numberPrecision.js +56 -1
  100. package/dist/esm/core/agent/evidence/numberPrecision.js.map +1 -1
  101. package/dist/esm/core/agent/honesty/mounts.d.ts +11 -2
  102. package/dist/esm/core/agent/honesty/mounts.js.map +1 -1
  103. package/dist/esm/core/agent/stages/prepareFinal.d.ts +19 -3
  104. package/dist/esm/core/agent/stages/prepareFinal.js +33 -17
  105. package/dist/esm/core/agent/stages/prepareFinal.js.map +1 -1
  106. package/dist/esm/core/agent/stages/route.js +2 -1
  107. package/dist/esm/core/agent/stages/route.js.map +1 -1
  108. package/dist/esm/core/agent/stages/toolCalls.js +14 -1
  109. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  110. package/dist/esm/core/agent/types.d.ts +16 -2
  111. package/dist/esm/core/slots/buildToolsSlot.js +1 -1
  112. package/dist/esm/core/slots/buildToolsSlot.js.map +1 -1
  113. package/dist/esm/events/payloads.d.ts +194 -0
  114. package/dist/esm/events/registry.d.ts +3 -1
  115. package/dist/esm/events/registry.js +2 -0
  116. package/dist/esm/events/registry.js.map +1 -1
  117. package/dist/esm/hosting/doorGuard.d.ts +3 -3
  118. package/dist/esm/hosting/doorGuard.js +2 -2
  119. package/dist/esm/hosting/nodeHost.d.ts +1 -1
  120. package/dist/esm/lib/displayName.d.ts +81 -0
  121. package/dist/esm/lib/displayName.js +119 -0
  122. package/dist/esm/lib/displayName.js.map +1 -0
  123. package/dist/esm/mcp/callLifetime.d.ts +49 -0
  124. package/dist/esm/mcp/callLifetime.js +99 -0
  125. package/dist/esm/mcp/callLifetime.js.map +1 -0
  126. package/dist/esm/mcp/mcpClient.d.ts +4 -1
  127. package/dist/esm/mcp/mcpClient.js +70 -28
  128. package/dist/esm/mcp/mcpClient.js.map +1 -1
  129. package/dist/esm/mcp/requestLedger.d.ts +97 -0
  130. package/dist/esm/mcp/requestLedger.js +192 -0
  131. package/dist/esm/mcp/requestLedger.js.map +1 -0
  132. package/dist/esm/mcp/seriesEvidence.d.ts +102 -0
  133. package/dist/esm/mcp/seriesEvidence.js +246 -0
  134. package/dist/esm/mcp/seriesEvidence.js.map +1 -0
  135. package/dist/esm/mcp/throttleRetry.d.ts +70 -35
  136. package/dist/esm/mcp/throttleRetry.js +181 -54
  137. package/dist/esm/mcp/throttleRetry.js.map +1 -1
  138. package/dist/esm/mcp/types.d.ts +12 -7
  139. package/dist/esm/redaction/knownStrings.js +30 -0
  140. package/dist/esm/redaction/knownStrings.js.map +1 -1
  141. package/dist/esm/security/PermissionPolicy.d.ts +1 -1
  142. package/dist/esm/security/PermissionPolicy.js +1 -1
  143. package/dist/esm/semantics/coverage/answer.d.ts +9 -1
  144. package/dist/esm/semantics/coverage/answer.js +50 -10
  145. package/dist/esm/semantics/coverage/answer.js.map +1 -1
  146. package/dist/esm/strategies/defaults/chatBubbleLiveStatus.d.ts +1 -1
  147. package/dist/esm/strategies/defaults/chatBubbleLiveStatus.js +1 -1
  148. package/dist/esm/tool-lint/analyze.d.ts +1 -1
  149. package/dist/esm/tool-lint/analyze.js +1 -1
  150. package/dist/esm/tool-lint/rules.d.ts +3 -3
  151. package/dist/esm/tool-lint/rules.js +3 -3
  152. package/dist/esm/tools/requestEvidence.d.ts +78 -0
  153. package/dist/esm/tools/requestEvidence.js +331 -0
  154. package/dist/esm/tools/requestEvidence.js.map +1 -0
  155. package/dist/esm/tools/tools.d.ts +30 -0
  156. package/dist/esm/tools/tools.js +16 -0
  157. package/dist/esm/tools/tools.js.map +1 -1
  158. package/dist/events/registry.js +2 -0
  159. package/dist/events/registry.js.map +1 -1
  160. package/dist/hosting/doorGuard.js +2 -2
  161. package/dist/lib/displayName.js +125 -0
  162. package/dist/lib/displayName.js.map +1 -0
  163. package/dist/mcp/callLifetime.js +103 -0
  164. package/dist/mcp/callLifetime.js.map +1 -0
  165. package/dist/mcp/mcpClient.js +69 -27
  166. package/dist/mcp/mcpClient.js.map +1 -1
  167. package/dist/mcp/requestLedger.js +196 -0
  168. package/dist/mcp/requestLedger.js.map +1 -0
  169. package/dist/mcp/seriesEvidence.js +251 -0
  170. package/dist/mcp/seriesEvidence.js.map +1 -0
  171. package/dist/mcp/throttleRetry.js +184 -55
  172. package/dist/mcp/throttleRetry.js.map +1 -1
  173. package/dist/redaction/knownStrings.js +30 -0
  174. package/dist/redaction/knownStrings.js.map +1 -1
  175. package/dist/security/PermissionPolicy.js +1 -1
  176. package/dist/semantics/coverage/answer.js +49 -9
  177. package/dist/semantics/coverage/answer.js.map +1 -1
  178. package/dist/strategies/defaults/chatBubbleLiveStatus.js +1 -1
  179. package/dist/tool-lint/analyze.js +1 -1
  180. package/dist/tool-lint/rules.js +3 -3
  181. package/dist/tools/requestEvidence.js +337 -0
  182. package/dist/tools/requestEvidence.js.map +1 -0
  183. package/dist/tools/tools.js +18 -1
  184. package/dist/tools/tools.js.map +1 -1
  185. package/dist/types/adapters/identity/jwks.d.ts +2 -2
  186. package/dist/types/adapters/identity/verify/claims.d.ts +3 -3
  187. package/dist/types/adapters/mcp/agentcoreRuntime/invoke.d.ts +3 -2
  188. package/dist/types/adapters/mcp/agentcoreRuntime/invoke.d.ts.map +1 -1
  189. package/dist/types/adapters/mcp/agentcoreRuntime/sessionBusy.d.ts +6 -5
  190. package/dist/types/adapters/mcp/agentcoreRuntime/sessionBusy.d.ts.map +1 -1
  191. package/dist/types/adapters/mcp/agentcoreRuntime/transport.d.ts +3 -1
  192. package/dist/types/adapters/mcp/agentcoreRuntime/transport.d.ts.map +1 -1
  193. package/dist/types/answer-account/account.d.ts.map +1 -1
  194. package/dist/types/answer-account/facts/howSure.d.ts.map +1 -1
  195. package/dist/types/core/Agent.d.ts +2 -1
  196. package/dist/types/core/Agent.d.ts.map +1 -1
  197. package/dist/types/core/agent/AgentBuilder.d.ts +22 -1
  198. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  199. package/dist/types/core/agent/assessment/assess.d.ts.map +1 -1
  200. package/dist/types/core/agent/assessment/compose.d.ts +82 -5
  201. package/dist/types/core/agent/assessment/compose.d.ts.map +1 -1
  202. package/dist/types/core/agent/assessment/reasons.d.ts +17 -1
  203. package/dist/types/core/agent/assessment/reasons.d.ts.map +1 -1
  204. package/dist/types/core/agent/assessment/sources.d.ts +67 -0
  205. package/dist/types/core/agent/assessment/sources.d.ts.map +1 -0
  206. package/dist/types/core/agent/assessment/stage.d.ts +17 -4
  207. package/dist/types/core/agent/assessment/stage.d.ts.map +1 -1
  208. package/dist/types/core/agent/assessment/types.d.ts +37 -0
  209. package/dist/types/core/agent/assessment/types.d.ts.map +1 -1
  210. package/dist/types/core/agent/buildAgentChart.d.ts +8 -0
  211. package/dist/types/core/agent/buildAgentChart.d.ts.map +1 -1
  212. package/dist/types/core/agent/buildToolRegistry.d.ts.map +1 -1
  213. package/dist/types/core/agent/evidence/evidenceIndex.d.ts +12 -2
  214. package/dist/types/core/agent/evidence/evidenceIndex.d.ts.map +1 -1
  215. package/dist/types/core/agent/evidence/extract.d.ts.map +1 -1
  216. package/dist/types/core/agent/evidence/figures.d.ts +6 -0
  217. package/dist/types/core/agent/evidence/figures.d.ts.map +1 -1
  218. package/dist/types/core/agent/evidence/numberPrecision.d.ts +29 -1
  219. package/dist/types/core/agent/evidence/numberPrecision.d.ts.map +1 -1
  220. package/dist/types/core/agent/honesty/mounts.d.ts +11 -2
  221. package/dist/types/core/agent/honesty/mounts.d.ts.map +1 -1
  222. package/dist/types/core/agent/stages/prepareFinal.d.ts +19 -3
  223. package/dist/types/core/agent/stages/prepareFinal.d.ts.map +1 -1
  224. package/dist/types/core/agent/stages/route.d.ts.map +1 -1
  225. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  226. package/dist/types/core/agent/types.d.ts +16 -2
  227. package/dist/types/core/agent/types.d.ts.map +1 -1
  228. package/dist/types/events/payloads.d.ts +194 -0
  229. package/dist/types/events/payloads.d.ts.map +1 -1
  230. package/dist/types/events/registry.d.ts +3 -1
  231. package/dist/types/events/registry.d.ts.map +1 -1
  232. package/dist/types/hosting/doorGuard.d.ts +3 -3
  233. package/dist/types/hosting/nodeHost.d.ts +1 -1
  234. package/dist/types/lib/displayName.d.ts +82 -0
  235. package/dist/types/lib/displayName.d.ts.map +1 -0
  236. package/dist/types/mcp/callLifetime.d.ts +50 -0
  237. package/dist/types/mcp/callLifetime.d.ts.map +1 -0
  238. package/dist/types/mcp/mcpClient.d.ts +4 -1
  239. package/dist/types/mcp/mcpClient.d.ts.map +1 -1
  240. package/dist/types/mcp/requestLedger.d.ts +98 -0
  241. package/dist/types/mcp/requestLedger.d.ts.map +1 -0
  242. package/dist/types/mcp/seriesEvidence.d.ts +103 -0
  243. package/dist/types/mcp/seriesEvidence.d.ts.map +1 -0
  244. package/dist/types/mcp/throttleRetry.d.ts +70 -35
  245. package/dist/types/mcp/throttleRetry.d.ts.map +1 -1
  246. package/dist/types/mcp/types.d.ts +12 -7
  247. package/dist/types/mcp/types.d.ts.map +1 -1
  248. package/dist/types/redaction/knownStrings.d.ts.map +1 -1
  249. package/dist/types/security/PermissionPolicy.d.ts +1 -1
  250. package/dist/types/semantics/coverage/answer.d.ts +9 -1
  251. package/dist/types/semantics/coverage/answer.d.ts.map +1 -1
  252. package/dist/types/strategies/defaults/chatBubbleLiveStatus.d.ts +1 -1
  253. package/dist/types/tool-lint/analyze.d.ts +1 -1
  254. package/dist/types/tool-lint/rules.d.ts +3 -3
  255. package/dist/types/tools/requestEvidence.d.ts +79 -0
  256. package/dist/types/tools/requestEvidence.d.ts.map +1 -0
  257. package/dist/types/tools/tools.d.ts +30 -0
  258. package/dist/types/tools/tools.d.ts.map +1 -1
  259. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  /**
2
- * throttleRetry — when an MCP server refuses a request before running it, wait
3
- * and ask again.
2
+ * throttleRetry — when an MCP server refuses a request with a response the
3
+ * retry policy recognises (HTTP 429), wait and ask again.
4
4
  *
5
5
  * ── Why this exists ──────────────────────────────────────────────────────────
6
6
  * Managed MCP gateways enforce per-principal rate limits. A throttled
@@ -10,20 +10,24 @@
10
10
  * tool, or invents an answer. A designed, transient, self-clearing condition
11
11
  * was being turned into a wrong answer.
12
12
  *
13
- * ── Why retrying a 429 is safe, and why the policy is 429-ONLY ───────────────
14
- * **A 429 is a pre-execution rejection.** The rate limiter refused the request
15
- * at the edge; the server never ran the tool. Retrying therefore cannot
16
- * double-execute anything — which is exactly what is NOT true of a 500 or a
13
+ * ── What retrying a 429 assumes, and why the policy is 429-ONLY ─────────────
14
+ * **Automatic 429 retries assume the server rejected the request before
15
+ * executing it.** A rate limiter that refuses at the edge has not run the tool —
16
+ * but HTTP 429 says only that the request was rate-limited (RFC 6585 §4), so the
17
+ * saved response records the assumption's basis (the server's 429), not proof
18
+ * that nothing ran. There is no side-effect guard here: a tool with side
19
+ * effects is retried on 429 like any other today, and `retryOnThrottle: false`
20
+ * turns that off per server. What is certain is the contrast with a 500 or a
17
21
  * timeout, where the call may have half-run and a retry could charge a card
18
22
  * twice.
19
23
  *
20
- * That asymmetry is the whole license for this module, so the policy must never
21
- * widen. Only HTTP 429 retries here. Not 5xx, not network errors, not timeouts
24
+ * That assumption is the whole license for this module, so the policy must
25
+ * never widen. Only HTTP 429 retries here. Not 5xx, not network errors, not timeouts
22
26
  * — those belong to a caller who knows whether the specific tool is idempotent,
23
27
  * and this seam does not. A property test pins the boundary.
24
28
  *
25
29
  * The loop takes its question as a {@link RetryDecision}, so the one other
26
- * refusal a service DOCUMENTS as pre-execution — AgentCore Runtime's "session
30
+ * refusal a service documents as one to retry — AgentCore Runtime's "session
27
31
  * operation in progress" — reuses this loop rather than growing a second one.
28
32
  * That decision is applied only by that vendor's own transport; what
29
33
  * `mcpClient` and `retryingFetch` retry is still a 429 and nothing else.
@@ -46,10 +50,13 @@
46
50
  *
47
51
  * ── The loop is footprintjs's attempt runner, one series per call ────────────
48
52
  * Each call runs ONE `attempt()` series (footprintjs, used as it ships). Its
49
- * runner makes an attempt record per try; they stay inside this module for
50
- * now — only `onRetry`, before each wait, reaches a caller. An attempt sends
51
- * the request once, and an attempt that got a RESPONSE is not an operation
52
- * that succeeded: a 503 is delivered as the answer it was, never retried.
53
+ * runner makes an attempt record per try. `onRetry`, before each wait, is what
54
+ * reaches a caller; the RUN gets the records: for a call the agent dispatched,
55
+ * the series files every attempt and how the request ended into that call's
56
+ * run (`seriesEvidence.ts`, through the connection's `requestLedger.ts`). An
57
+ * attempt sends the request once, and an attempt that got a RESPONSE is not an
58
+ * operation that succeeded: a 503 is delivered as the answer it was, never
59
+ * retried — and written down as `answered` with its status, never success.
53
60
  * A response a rule's decision recognises is thrown as a refusal that names
54
61
  * its kind, and the policy's `retryOn` accepts those refusals and nothing
55
62
  * else — so a 5xx, a dropped connection or a timeout ends the series on the
@@ -83,6 +90,7 @@
83
90
  *
84
91
  * Pattern: Decorator over `fetch`. Role: Layer-3 tool transport.
85
92
  */
93
+ import type { RequestLedger } from './requestLedger.js';
86
94
  /**
87
95
  * The fetch shape the MCP SDK's `fetch` hook uses — deliberately NARROWER
88
96
  * than `gatewayTransport`'s `FetchLike` (no `Request` input), because that is
@@ -184,21 +192,24 @@ export interface ThrottleRetryOptions {
184
192
  /**
185
193
  * A name for this endpoint, declared by you, that `onRetry` reports as
186
194
  * `endpoint` — so a log line can say which server was retried without
187
- * carrying any part of its URL past the origin. 1–128 characters, none of
188
- * them control characters. Absent, `onRetry` has no `endpoint` field; the
189
- * library never makes one up from the URL.
195
+ * carrying any part of its URL past the origin. A dispatched call's saved
196
+ * attempts name the endpoint by it too, in place of the origin. 1–128
197
+ * characters, none of them control characters. Absent, `onRetry` has no
198
+ * `endpoint` field; the library never makes one up from the URL.
190
199
  */
191
200
  readonly endpointLabel?: string;
192
201
  /**
193
- * A notification before each wait — and, today, the ONLY retry report a
194
- * caller gets. It is told the refusal's kind, the attempt about to start,
195
- * the wait, the endpoint's origin and the label you declared for it
196
- * (`endpointLabel`) — never the URL's path, the request's
197
- * arguments or the server's answer. The series' final outcome and the
198
- * attempt records footprintjs's runner makes for each try are NOT passed on
199
- * yet. The contract matches what `withRetry` and `withCredentialRetry`
200
- * already use: a decorator at a transport boundary reports to its consumer,
201
- * and emits no events of its own.
202
+ * A notification before each wait. It is told the refusal's kind, the
203
+ * attempt about to start, the wait, the endpoint's origin and the label you
204
+ * declared for it (`endpointLabel`) — never the URL's path, the request's
205
+ * arguments or the server's answer. It is not told how the call ended. The
206
+ * contract matches what `withRetry` and `withCredentialRetry` already use: a
207
+ * decorator at a transport boundary reports to its consumer.
208
+ *
209
+ * What the RUN keeps is separate and needs no hook: for a call the agent
210
+ * dispatched, the series files every attempt — its outcome, its reason, its
211
+ * wait — and how the request ended, as `agentfootprint.tools.request_attempts`
212
+ * (the label is reported there as `endpoint`).
202
213
  *
203
214
  * A retried call is not invisible without this: the wait lands inside the
204
215
  * tool's `stream.tool_end` `durationMs`, and an exhausted retry still
@@ -229,9 +240,10 @@ export interface RetryVerdict {
229
240
  * The one question the loop asks of each response.
230
241
  *
231
242
  * ── The license every decision must hold ─────────────────────────────────────
232
- * A decision may answer "again" ONLY for a refusal the server made BEFORE it
233
- * ran anything — the property that makes a 429 safe to repeat (see the module
234
- * note). A response that could be the record of half-done work is never one.
243
+ * A decision may answer "again" ONLY for a response the library treats as a
244
+ * refusal made before anything ran — the assumption a 429 retry rests on (see
245
+ * the module note: the 429 is its basis, not proof). A response that could be
246
+ * the record of half-done work is never one.
235
247
  * The library holds exactly two such decisions: {@link throttled} below, and
236
248
  * AgentCore Runtime's documented "session operation in progress" refusal,
237
249
  * which lives with that vendor's adapter (`adapters/mcp/agentcoreRuntime/`).
@@ -333,20 +345,43 @@ export declare function retryRule(kind: RetryKind, decide: RetryDecision, config
333
345
  * is built, never at the first call.
334
346
  */
335
347
  export declare function retryingFetch(inner: ThrottleFetch | undefined, config: RetryOnThrottle | undefined): ThrottleFetch | undefined;
348
+ /**
349
+ * @internal What one call's series is built from: the rules it asks, in order,
350
+ * and the connection's ledger — which tool call each request belongs to, so the
351
+ * series can file its attempts into that call's run (`requestLedger.ts`).
352
+ */
353
+ export interface SeriesSpec {
354
+ readonly rules: readonly RetryRule[];
355
+ readonly ledger?: RequestLedger;
356
+ }
357
+ /** @internal What a joinable fetch's series can be rebuilt with: one more rule, or a ledger. */
358
+ export type SeriesJoin = {
359
+ readonly rule: RetryRule;
360
+ } | {
361
+ readonly ledger: RequestLedger;
362
+ };
363
+ /** @internal `spec` with `join` in it. */
364
+ export declare function joinedSpec(spec: SeriesSpec, join: SeriesJoin): SeriesSpec;
336
365
  /**
337
366
  * @internal Declare that `fetch` runs its retries in ONE series, and how to
338
- * rebuild it with one more rule in that same series. `retryingFetch` joins such
339
- * a fetch instead of wrapping it. A transport whose public fetch wraps its
340
- * series (a session applied around it, its answers rewritten after it) uses
341
- * this so the rule lands INSIDE that wrapping.
367
+ * rebuild it with one more rule — or with a ledger — in that same series.
368
+ * `retryingFetch` joins such a fetch instead of wrapping it. A transport whose
369
+ * public fetch wraps its series (a session applied around it, its answers
370
+ * rewritten after it) uses this so the rule lands INSIDE that wrapping.
371
+ */
372
+ export declare function joinableRetry(fetch: ThrottleFetch, rebuild: (join: SeriesJoin) => ThrottleFetch): ThrottleFetch;
373
+ /**
374
+ * @internal `fetch` rebuilt so its series files each request's attempts through
375
+ * `ledger` — or `undefined` when `fetch` runs no series (retry off), so there
376
+ * is nothing to file and the caller keeps `fetch` as it is.
342
377
  */
343
- export declare function joinableRetry(fetch: ThrottleFetch, join: (rule: RetryRule) => ThrottleFetch): ThrottleFetch;
378
+ export declare function ledgeredFetch(fetch: ThrottleFetch, ledger: RequestLedger): ThrottleFetch | undefined;
344
379
  /**
345
- * @internal The one series per call over `inner`, asking `rules` in order —
380
+ * @internal The one series per call over `inner`, asking `spec.rules` in order —
346
381
  * `inner` itself when there are none. The first rule holds the call's budget;
347
382
  * see the module note. The result is joinable.
348
383
  */
349
- export declare function retryingFetchFor(inner: ThrottleFetch, rules: readonly RetryRule[]): ThrottleFetch;
384
+ export declare function retryingFetchFor(inner: ThrottleFetch, spec: SeriesSpec): ThrottleFetch;
350
385
  /**
351
386
  * `Retry-After` in either legal form: delta-seconds (`"2"`) or an HTTP-date
352
387
  * (`"Wed, 21 Oct 2026 07:28:00 GMT"`). Anything else is treated as absent so a
@@ -1,6 +1,6 @@
1
1
  /**
2
- * throttleRetry — when an MCP server refuses a request before running it, wait
3
- * and ask again.
2
+ * throttleRetry — when an MCP server refuses a request with a response the
3
+ * retry policy recognises (HTTP 429), wait and ask again.
4
4
  *
5
5
  * ── Why this exists ──────────────────────────────────────────────────────────
6
6
  * Managed MCP gateways enforce per-principal rate limits. A throttled
@@ -10,20 +10,24 @@
10
10
  * tool, or invents an answer. A designed, transient, self-clearing condition
11
11
  * was being turned into a wrong answer.
12
12
  *
13
- * ── Why retrying a 429 is safe, and why the policy is 429-ONLY ───────────────
14
- * **A 429 is a pre-execution rejection.** The rate limiter refused the request
15
- * at the edge; the server never ran the tool. Retrying therefore cannot
16
- * double-execute anything — which is exactly what is NOT true of a 500 or a
13
+ * ── What retrying a 429 assumes, and why the policy is 429-ONLY ─────────────
14
+ * **Automatic 429 retries assume the server rejected the request before
15
+ * executing it.** A rate limiter that refuses at the edge has not run the tool —
16
+ * but HTTP 429 says only that the request was rate-limited (RFC 6585 §4), so the
17
+ * saved response records the assumption's basis (the server's 429), not proof
18
+ * that nothing ran. There is no side-effect guard here: a tool with side
19
+ * effects is retried on 429 like any other today, and `retryOnThrottle: false`
20
+ * turns that off per server. What is certain is the contrast with a 500 or a
17
21
  * timeout, where the call may have half-run and a retry could charge a card
18
22
  * twice.
19
23
  *
20
- * That asymmetry is the whole license for this module, so the policy must never
21
- * widen. Only HTTP 429 retries here. Not 5xx, not network errors, not timeouts
24
+ * That assumption is the whole license for this module, so the policy must
25
+ * never widen. Only HTTP 429 retries here. Not 5xx, not network errors, not timeouts
22
26
  * — those belong to a caller who knows whether the specific tool is idempotent,
23
27
  * and this seam does not. A property test pins the boundary.
24
28
  *
25
29
  * The loop takes its question as a {@link RetryDecision}, so the one other
26
- * refusal a service DOCUMENTS as pre-execution — AgentCore Runtime's "session
30
+ * refusal a service documents as one to retry — AgentCore Runtime's "session
27
31
  * operation in progress" — reuses this loop rather than growing a second one.
28
32
  * That decision is applied only by that vendor's own transport; what
29
33
  * `mcpClient` and `retryingFetch` retry is still a 429 and nothing else.
@@ -46,10 +50,13 @@
46
50
  *
47
51
  * ── The loop is footprintjs's attempt runner, one series per call ────────────
48
52
  * Each call runs ONE `attempt()` series (footprintjs, used as it ships). Its
49
- * runner makes an attempt record per try; they stay inside this module for
50
- * now — only `onRetry`, before each wait, reaches a caller. An attempt sends
51
- * the request once, and an attempt that got a RESPONSE is not an operation
52
- * that succeeded: a 503 is delivered as the answer it was, never retried.
53
+ * runner makes an attempt record per try. `onRetry`, before each wait, is what
54
+ * reaches a caller; the RUN gets the records: for a call the agent dispatched,
55
+ * the series files every attempt and how the request ended into that call's
56
+ * run (`seriesEvidence.ts`, through the connection's `requestLedger.ts`). An
57
+ * attempt sends the request once, and an attempt that got a RESPONSE is not an
58
+ * operation that succeeded: a 503 is delivered as the answer it was, never
59
+ * retried — and written down as `answered` with its status, never success.
53
60
  * A response a rule's decision recognises is thrown as a refusal that names
54
61
  * its kind, and the policy's `retryOn` accepts those refusals and nothing
55
62
  * else — so a 5xx, a dropped connection or a timeout ends the series on the
@@ -86,6 +93,7 @@
86
93
  import { attempt, } from 'footprintjs';
87
94
  import { abortErrorOf, eitherSignal, linkedController } from '../lib/abort.js';
88
95
  import { exponentialBackoff, statedWaitFirst } from './backoff.js';
96
+ import { SeriesEvidence, } from './seriesEvidence.js';
89
97
  /** HTTP 429, with the server's own `Retry-After` when it sent one. */
90
98
  export function throttled(response) {
91
99
  if (response.status !== 429)
@@ -177,37 +185,54 @@ export function retryingFetch(inner, config) {
177
185
  const rule = retryRule('throttle', throttled, config, THROTTLE_RETRY_DEFAULTS, 'retryOnThrottle');
178
186
  if (rule === undefined)
179
187
  return inner;
180
- const join = inner === undefined ? undefined : JOINS.get(inner);
181
- return join !== undefined ? join(rule) : retryingFetchFor(inner ?? platformFetch, [rule]);
188
+ const rebuild = inner === undefined ? undefined : JOINS.get(inner);
189
+ return rebuild !== undefined
190
+ ? rebuild({ rule })
191
+ : retryingFetchFor(inner ?? platformFetch, { rules: [rule] });
192
+ }
193
+ /** @internal `spec` with `join` in it. */
194
+ export function joinedSpec(spec, join) {
195
+ return 'rule' in join
196
+ ? { ...spec, rules: [...spec.rules, join.rule] }
197
+ : { ...spec, ledger: join.ledger };
182
198
  }
183
199
  /**
184
200
  * Fetches that run their retries in one series, each with the way to rebuild
185
- * it with one more rule. Module-private: only `joinableRetry` adds to it, so a
186
- * caller cannot make a fetch claim a series it does not run.
201
+ * it with one more rule or a ledger. Module-private: only `joinableRetry` adds
202
+ * to it, so a caller cannot make a fetch claim a series it does not run.
187
203
  */
188
204
  const JOINS = new WeakMap();
189
205
  /**
190
206
  * @internal Declare that `fetch` runs its retries in ONE series, and how to
191
- * rebuild it with one more rule in that same series. `retryingFetch` joins such
192
- * a fetch instead of wrapping it. A transport whose public fetch wraps its
193
- * series (a session applied around it, its answers rewritten after it) uses
194
- * this so the rule lands INSIDE that wrapping.
207
+ * rebuild it with one more rule — or with a ledger — in that same series.
208
+ * `retryingFetch` joins such a fetch instead of wrapping it. A transport whose
209
+ * public fetch wraps its series (a session applied around it, its answers
210
+ * rewritten after it) uses this so the rule lands INSIDE that wrapping.
195
211
  */
196
- export function joinableRetry(fetch, join) {
197
- JOINS.set(fetch, join);
212
+ export function joinableRetry(fetch, rebuild) {
213
+ JOINS.set(fetch, rebuild);
198
214
  return fetch;
199
215
  }
200
216
  /**
201
- * @internal The one series per call over `inner`, asking `rules` in order —
217
+ * @internal `fetch` rebuilt so its series files each request's attempts through
218
+ * `ledger` — or `undefined` when `fetch` runs no series (retry off), so there
219
+ * is nothing to file and the caller keeps `fetch` as it is.
220
+ */
221
+ export function ledgeredFetch(fetch, ledger) {
222
+ return JOINS.get(fetch)?.({ ledger });
223
+ }
224
+ /**
225
+ * @internal The one series per call over `inner`, asking `spec.rules` in order —
202
226
  * `inner` itself when there are none. The first rule holds the call's budget;
203
227
  * see the module note. The result is joinable.
204
228
  */
205
- export function retryingFetchFor(inner, rules) {
229
+ export function retryingFetchFor(inner, spec) {
230
+ const { rules, ledger } = spec;
206
231
  const [owner] = rules;
207
232
  if (owner === undefined)
208
233
  return inner;
209
234
  const bound = rules.map((rule) => boundRule(rule, owner));
210
- return joinableRetry((input, init) => new Series(bound, input, init).run(inner), (rule) => retryingFetchFor(inner, [...rules, rule]));
235
+ return joinableRetry((input, init) => new Series(bound, input, init, ledger).run(inner), (join) => retryingFetchFor(inner, joinedSpec(spec, join)));
211
236
  }
212
237
  /** The platform's fetch, looked up at call time so a later polyfill still wins. */
213
238
  const platformFetch = (input, init) => globalThis.fetch(input, init);
@@ -237,7 +262,8 @@ function retryAfterOf(error) {
237
262
  const SERIES_TARGET = 'mcp-request';
238
263
  /**
239
264
  * One call: one `attempt()` series, and what its policy's functions share while
240
- * it runs — the waiting so far, and the latest refusal.
265
+ * it runs — the waiting so far, the latest refusal, and, for a call the agent
266
+ * dispatched, where its attempts are filed.
241
267
  */
242
268
  class Series {
243
269
  rules;
@@ -246,13 +272,31 @@ class Series {
246
272
  owner;
247
273
  startedAt = Date.now();
248
274
  deadline;
275
+ /** The transport's own signal — what the request carried before this series existed. */
249
276
  signal;
277
+ /**
278
+ * What ends the series as a CANCELLATION: the transport's signal; the MCP
279
+ * SDK cancelling this request (`notifications/cancelled`, seen by the ledger,
280
+ * whether or not the request was matched to a call); and, for a call the
281
+ * agent dispatched, the call's own — its signal, the client's, its settling.
282
+ */
283
+ cancel;
284
+ /** Whether a request in flight is cut by more than the transport's own signal. */
285
+ cutsInFlight;
286
+ /** Where this request's attempts are filed — a call the agent dispatched. */
287
+ evidence;
250
288
  replayable;
251
289
  attemptNo = 0;
252
290
  waitedMs = 0;
253
291
  plannedWaitMs = 0;
254
292
  latest;
255
- constructor(rules, input, init) {
293
+ /** The current attempt's response status, before the runner records it. */
294
+ seen;
295
+ /** Why `retryOn` last said no. */
296
+ declined;
297
+ /** The attempt sent and not yet recorded — what a cancellation cuts in flight. */
298
+ inFlight;
299
+ constructor(rules, input, init, ledger) {
256
300
  this.rules = rules;
257
301
  this.input = input;
258
302
  this.init = init;
@@ -261,14 +305,50 @@ class Series {
261
305
  this.replayable = isReplayable(input, init);
262
306
  const { maxElapsedMs } = this.owner;
263
307
  this.deadline = Number.isFinite(maxElapsedMs) ? this.startedAt + maxElapsedMs : undefined;
308
+ const claimed = ledger?.claim(init?.body);
309
+ const ticket = claimed?.ticket;
310
+ this.cancel = eitherSignal(this.signal, ticket?.signal, claimed?.cancelled);
311
+ this.cutsInFlight = this.deadline !== undefined || claimed !== undefined;
312
+ this.evidence =
313
+ ticket === undefined ? undefined : new SeriesEvidence(ticket.sink, whereOf(rules, input));
264
314
  }
265
315
  async run(inner) {
316
+ const unwatch = this.watchCancel();
266
317
  try {
267
- return await attempt((t) => this.send(inner, t), this.policy(), this.options());
318
+ const response = await attempt((t) => this.send(inner, t), this.policy(), this.options());
319
+ this.evidence?.delivered();
320
+ return response;
268
321
  }
269
322
  catch (error) {
270
323
  return this.settle(error);
271
324
  }
325
+ finally {
326
+ unwatch();
327
+ }
328
+ }
329
+ /**
330
+ * File a cancellation the MOMENT it lands. A cancelled run stops waiting for
331
+ * its stage at once; a record filed when this series later unwinds would land
332
+ * after the run was saved. The runner records nothing for the attempt it
333
+ * abandons then, or for a wait it was cut out of, so this is also the only
334
+ * place those are written down.
335
+ */
336
+ watchCancel() {
337
+ const { cancel } = this;
338
+ if (cancel === undefined || this.evidence === undefined || cancel.aborted) {
339
+ return () => undefined;
340
+ }
341
+ const onAbort = () => this.fileCancellation();
342
+ cancel.addEventListener('abort', onAbort, { once: true });
343
+ return () => cancel.removeEventListener('abort', onAbort);
344
+ }
345
+ /** A cancellation, written down: the attempt it cut in flight, or the wait. */
346
+ fileCancellation() {
347
+ const { evidence, inFlight } = this;
348
+ if (inFlight !== undefined)
349
+ evidence?.cutInFlight(inFlight);
350
+ else
351
+ evidence?.cancelled();
272
352
  }
273
353
  /**
274
354
  * The owner's budget as footprintjs reads it; the rest is this series'
@@ -288,7 +368,7 @@ class Series {
288
368
  options() {
289
369
  return {
290
370
  target: SERIES_TARGET,
291
- ...(this.signal !== undefined && { signal: this.signal }),
371
+ ...(this.cancel !== undefined && { signal: this.cancel }),
292
372
  ...(this.deadline !== undefined && { deadline: this.deadline }),
293
373
  onAttempt: (record) => this.report(record),
294
374
  };
@@ -297,25 +377,30 @@ class Series {
297
377
  * One attempt: the request, sent once, and its answer classified. A refusal
298
378
  * a rule recognises is thrown for the runner to decide on.
299
379
  *
300
- * With no deadline of ours the request carries the caller's own `init`,
301
- * exactly as before this series existed. Under a deadline the runner can cut
302
- * the attempt before it is ANSWERED — and an answer is not in until it has
303
- * been classified, which can mean reading its body (a session-busy reply is
304
- * JSON in a 200). So the cut stays connected through the classification: a
305
- * stall in the request or in that body stops the request and releases the
306
- * body. Only then does it let go: an answer handed back to the caller keeps
380
+ * With no deadline of ours and no call to answer to, the request carries the
381
+ * caller's own `init`, exactly as before this series existed. Otherwise the
382
+ * runner can cut the attempt before it is ANSWERED — the deadline, or the
383
+ * dispatched call's cancellation — and an answer is not in until it has been
384
+ * classified, which can mean reading its body (a session-busy reply is JSON
385
+ * in a 200). So the cut stays connected through the classification: a stall
386
+ * in the request or in that body stops the request and releases the body.
387
+ * Only then does it let go: an answer handed back to the caller keeps
307
388
  * streaming for as long as the caller's own signal allows.
308
389
  */
309
390
  async send(inner, t) {
310
391
  this.attemptNo = t.attempt;
392
+ // From here the request is SENT — counted, as a row, however the attempt ends.
393
+ this.inFlight = t.attempt;
394
+ this.seen = undefined;
311
395
  // The previous attempt's refusal will not be delivered: release its body, so
312
396
  // a keep-alive connection is not pinned by an unread stream.
313
397
  this.supersede();
314
- const cut = this.deadline === undefined ? undefined : linkedController(t.signal);
398
+ const cut = this.cutsInFlight ? linkedController(t.signal) : undefined;
315
399
  const signal = eitherSignal(this.signal, cut?.controller.signal);
316
400
  try {
317
401
  const init = cut === undefined ? this.init : { ...this.init, ...(signal && { signal }) };
318
402
  const response = await inner(this.input, init);
403
+ this.seen = { attempt: t.attempt, status: response.status };
319
404
  const refused = await this.recognise(response, signal);
320
405
  if (refused === undefined)
321
406
  return response;
@@ -348,44 +433,59 @@ class Series {
348
433
  }
349
434
  /**
350
435
  * The runner's `retryOn`: is this attempt's error a refusal worth another
351
- * attempt? Plans the wait as it answers, because the ceilings need its length.
436
+ * attempt? Plans the wait as it answers, because the ceilings need its length,
437
+ * and keeps why it said no, for the record.
352
438
  */
353
439
  retryOn(error) {
354
440
  this.plannedWaitMs = 0;
355
441
  const refused = this.latest;
356
- // The license: only a refusal made before the server ran anything. A 5xx, a
357
- // dropped connection or a timeout may be the record of work that ran.
442
+ // The license: only a response a rule recognises as a refusal (the 429
443
+ // assumption — see the module note). A 5xx, a dropped connection or a
444
+ // timeout may be the record of work that ran.
358
445
  if (refused === undefined || error !== refused.refusal)
359
- return false;
446
+ return this.decline('not-retryable');
360
447
  // A body we cannot send twice makes a retry a different request. Rare for
361
448
  // MCP (JSON strings), but silently replaying half a stream would be worse
362
449
  // than surfacing the refusal.
363
450
  if (!this.replayable)
364
- return false;
451
+ return this.decline('not-replayable');
365
452
  const { rule } = refused;
366
453
  if (this.attemptNo >= rule.maxAttempts)
367
- return false;
454
+ return this.decline('exhausted');
368
455
  // The stated wait whole, else the schedule under `maxDelayMs`.
369
456
  const waitMs = rule.backoffMs(this.attemptNo, error);
370
457
  // The rail on Retry-After: never let a server talk us into a wait that
371
458
  // breaks the caller's budget. It stops the series; it never shortens the
372
459
  // wait. Past the deadline, the runner stops it the same way (`no-time-left`).
373
460
  if (this.waitedMs + waitMs > rule.maxWaitMs)
374
- return false;
461
+ return this.decline('wait-limit');
375
462
  // A joined rule's own, sooner deadline. The owner's is the runner's to hold.
376
463
  if (rule.maxElapsedMs < this.owner.maxElapsedMs &&
377
464
  this.elapsedAfter(waitMs) > rule.maxElapsedMs) {
378
- return false;
465
+ return this.decline('no-time-left');
379
466
  }
467
+ this.declined = undefined;
380
468
  this.plannedWaitMs = waitMs;
381
469
  return true;
382
470
  }
471
+ decline(reason) {
472
+ this.declined = reason;
473
+ return false;
474
+ }
383
475
  elapsedAfter(waitMs) {
384
476
  return Date.now() - this.startedAt + waitMs;
385
477
  }
386
- /** Each record as the runner ends an attempt: a retry is counted and reported to its rule. */
478
+ /**
479
+ * Each record as the runner ends an attempt: written down for the call's run
480
+ * first, then — for a retry — counted and reported to its rule. The record
481
+ * goes first because `onRetry` is the caller's, and a throw from it must not
482
+ * cost the run its evidence.
483
+ */
387
484
  report(record) {
388
485
  const refused = this.latest;
486
+ if (this.inFlight === record.attempt)
487
+ this.inFlight = undefined;
488
+ this.evidence?.attempt(record, this.seenOf(record, refused), this.declined);
389
489
  if (record.next !== 'retry' || refused === undefined)
390
490
  return;
391
491
  const waitMs = record.delayMs ?? 0;
@@ -401,18 +501,37 @@ class Series {
401
501
  ...(rule.endpointLabel !== undefined && { endpoint: rule.endpointLabel }),
402
502
  });
403
503
  }
504
+ /** What this series saw of the attempt `record` ends: its status, and the refusal it threw. */
505
+ seenOf(record, refused) {
506
+ const seen = this.seen;
507
+ if (seen === undefined || seen.attempt !== record.attempt)
508
+ return undefined;
509
+ // `latest` is cleared as each attempt starts, so a refusal here is this attempt's.
510
+ const { kind } = refused?.rule ?? {};
511
+ const retryAfterMs = refused?.verdict.retryAfterMs;
512
+ return {
513
+ status: seen.status,
514
+ ...(kind !== undefined && {
515
+ refusal: { kind, ...(retryAfterMs !== undefined && { retryAfterMs }) },
516
+ }),
517
+ };
518
+ }
404
519
  /** How the call ends when the series did not answer. */
405
520
  settle(error) {
406
521
  // The caller's cancellation wins: the call ends with their reason, and the
407
522
  // refusal nobody is waiting for any more is released.
408
- if (this.signal?.aborted) {
523
+ if (this.cancel?.aborted) {
524
+ this.fileCancellation();
409
525
  this.supersede();
410
- throw abortErrorOf(this.signal);
526
+ throw abortErrorOf(this.cancel);
411
527
  }
412
528
  // The series ended on a refusal — attempts spent, a ceiling reached, no
413
529
  // time left. The server's answer is delivered as it sent it.
414
- if (this.latest !== undefined && error === this.latest.refusal)
530
+ if (this.latest !== undefined && error === this.latest.refusal) {
531
+ this.evidence?.delivered();
415
532
  return this.latest.response;
533
+ }
534
+ this.evidence?.threw(this.deadline !== undefined && Date.now() >= this.deadline);
416
535
  throw error;
417
536
  }
418
537
  supersede() {
@@ -421,9 +540,17 @@ class Series {
421
540
  this.latest = undefined;
422
541
  }
423
542
  }
543
+ /** Where a series' requests go, as its evidence may say it: a label the app declared, or the origin. */
544
+ function whereOf(rules, input) {
545
+ const label = rules.find((rule) => rule.endpointLabel !== undefined)?.endpointLabel;
546
+ if (label !== undefined)
547
+ return { endpoint: label };
548
+ const origin = originOf(input);
549
+ return origin === '' ? {} : { origin };
550
+ }
424
551
  /**
425
- * A response a rule recognised as a refusal made before anything ran, thrown so
426
- * the attempt runner decides on it. It carries only what an attempt record may
552
+ * A response a rule recognised as a refusal (a 429, a session-busy reply), thrown
553
+ * so the attempt runner decides on it. It carries only what an attempt record may
427
554
  * keep: a fixed sentence, its kind as `code`, a 429's status and the wait the
428
555
  * server stated. Never the server's text, the request or the response — the
429
556
  * series holds the response, not the error.
@@ -442,8 +569,8 @@ class RetryableRefusal extends Error {
442
569
  }
443
570
  }
444
571
  const REFUSAL_SENTENCES = {
445
- throttle: 'The server refused the request with HTTP 429 before running it.',
446
- 'session-busy': 'AgentCore Runtime refused the request while its session was starting or stopping, before running it.',
572
+ throttle: 'The server answered with HTTP 429, a response the retry policy recognises as a refusal. It is recorded as sent, which is not proof that nothing ran.',
573
+ 'session-busy': 'AgentCore Runtime answered that its session was starting or stopping, a response the retry policy recognises as a refusal. It is recorded as sent, which is not proof that nothing ran.',
447
574
  };
448
575
  // ─── Helpers ─────────────────────────────────────────────────────────
449
576
  /** Reads one rule's numeric options, refusing by name what the schedule cannot mean. */