agentfootprint 9.83.0 → 9.84.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 (106) hide show
  1. package/CHANGELOG.md +635 -512
  2. package/dist/core/Agent.js +10 -0
  3. package/dist/core/Agent.js.map +1 -1
  4. package/dist/core/agent/AgentBuilder.js.map +1 -1
  5. package/dist/core/agent/evidence/frames.js +15 -6
  6. package/dist/core/agent/evidence/frames.js.map +1 -1
  7. package/dist/core/agent/outputEnforcement.js +7 -4
  8. package/dist/core/agent/outputEnforcement.js.map +1 -1
  9. package/dist/core/agent/selfCallNotice.js +174 -0
  10. package/dist/core/agent/selfCallNotice.js.map +1 -0
  11. package/dist/core/agent/stages/toolCalls.js +87 -2
  12. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  13. package/dist/core/agent/window/currentRequest.js +11 -13
  14. package/dist/core/agent/window/currentRequest.js.map +1 -1
  15. package/dist/core/agent/window/notice.js +9 -9
  16. package/dist/core/agent/window/notice.js.map +1 -1
  17. package/dist/core/agent/window/summarize.js +9 -9
  18. package/dist/core/agent/window/summarize.js.map +1 -1
  19. package/dist/doors/skill-graph.js +2 -1
  20. package/dist/doors/skill-graph.js.map +1 -1
  21. package/dist/esm/core/Agent.js +10 -0
  22. package/dist/esm/core/Agent.js.map +1 -1
  23. package/dist/esm/core/agent/AgentBuilder.d.ts +15 -0
  24. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  25. package/dist/esm/core/agent/evidence/frames.d.ts +9 -4
  26. package/dist/esm/core/agent/evidence/frames.js +13 -5
  27. package/dist/esm/core/agent/evidence/frames.js.map +1 -1
  28. package/dist/esm/core/agent/outputEnforcement.d.ts +1 -2
  29. package/dist/esm/core/agent/outputEnforcement.js +4 -2
  30. package/dist/esm/core/agent/outputEnforcement.js.map +1 -1
  31. package/dist/esm/core/agent/selfCallNotice.d.ts +140 -0
  32. package/dist/esm/core/agent/selfCallNotice.js +169 -0
  33. package/dist/esm/core/agent/selfCallNotice.js.map +1 -0
  34. package/dist/esm/core/agent/stages/toolCalls.d.ts +6 -0
  35. package/dist/esm/core/agent/stages/toolCalls.js +87 -2
  36. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  37. package/dist/esm/core/agent/window/currentRequest.d.ts +9 -3
  38. package/dist/esm/core/agent/window/currentRequest.js +11 -13
  39. package/dist/esm/core/agent/window/currentRequest.js.map +1 -1
  40. package/dist/esm/core/agent/window/notice.d.ts +1 -4
  41. package/dist/esm/core/agent/window/notice.js +5 -6
  42. package/dist/esm/core/agent/window/notice.js.map +1 -1
  43. package/dist/esm/core/agent/window/summarize.d.ts +1 -4
  44. package/dist/esm/core/agent/window/summarize.js +5 -6
  45. package/dist/esm/core/agent/window/summarize.js.map +1 -1
  46. package/dist/esm/doors/skill-graph.d.ts +1 -1
  47. package/dist/esm/doors/skill-graph.js +1 -1
  48. package/dist/esm/doors/skill-graph.js.map +1 -1
  49. package/dist/esm/events/payloads.d.ts +15 -0
  50. package/dist/esm/index.d.ts +1 -0
  51. package/dist/esm/index.js +8 -0
  52. package/dist/esm/index.js.map +1 -1
  53. package/dist/esm/lib/injection-engine/index.d.ts +1 -0
  54. package/dist/esm/lib/injection-engine/index.js +6 -0
  55. package/dist/esm/lib/injection-engine/index.js.map +1 -1
  56. package/dist/esm/lib/injection-engine/skillToolDescriptors.d.ts +29 -0
  57. package/dist/esm/lib/injection-engine/skillToolDescriptors.js +90 -7
  58. package/dist/esm/lib/injection-engine/skillToolDescriptors.js.map +1 -1
  59. package/dist/esm/lib/injection-engine/types.d.ts +54 -0
  60. package/dist/esm/lib/injection-engine/types.js +28 -0
  61. package/dist/esm/lib/injection-engine/types.js.map +1 -1
  62. package/dist/esm/lib/saidByPerson.d.ts +99 -0
  63. package/dist/esm/lib/saidByPerson.js +104 -0
  64. package/dist/esm/lib/saidByPerson.js.map +1 -0
  65. package/dist/index.js +14 -3
  66. package/dist/index.js.map +1 -1
  67. package/dist/lib/injection-engine/index.js +9 -2
  68. package/dist/lib/injection-engine/index.js.map +1 -1
  69. package/dist/lib/injection-engine/skillToolDescriptors.js +92 -8
  70. package/dist/lib/injection-engine/skillToolDescriptors.js.map +1 -1
  71. package/dist/lib/injection-engine/types.js +30 -1
  72. package/dist/lib/injection-engine/types.js.map +1 -1
  73. package/dist/lib/saidByPerson.js +111 -0
  74. package/dist/lib/saidByPerson.js.map +1 -0
  75. package/dist/types/core/Agent.d.ts.map +1 -1
  76. package/dist/types/core/agent/AgentBuilder.d.ts +15 -0
  77. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  78. package/dist/types/core/agent/evidence/frames.d.ts +9 -4
  79. package/dist/types/core/agent/evidence/frames.d.ts.map +1 -1
  80. package/dist/types/core/agent/outputEnforcement.d.ts +1 -2
  81. package/dist/types/core/agent/outputEnforcement.d.ts.map +1 -1
  82. package/dist/types/core/agent/selfCallNotice.d.ts +141 -0
  83. package/dist/types/core/agent/selfCallNotice.d.ts.map +1 -0
  84. package/dist/types/core/agent/stages/toolCalls.d.ts +6 -0
  85. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  86. package/dist/types/core/agent/window/currentRequest.d.ts +9 -3
  87. package/dist/types/core/agent/window/currentRequest.d.ts.map +1 -1
  88. package/dist/types/core/agent/window/notice.d.ts +1 -4
  89. package/dist/types/core/agent/window/notice.d.ts.map +1 -1
  90. package/dist/types/core/agent/window/summarize.d.ts +1 -4
  91. package/dist/types/core/agent/window/summarize.d.ts.map +1 -1
  92. package/dist/types/doors/skill-graph.d.ts +1 -1
  93. package/dist/types/doors/skill-graph.d.ts.map +1 -1
  94. package/dist/types/events/payloads.d.ts +15 -0
  95. package/dist/types/events/payloads.d.ts.map +1 -1
  96. package/dist/types/index.d.ts +1 -0
  97. package/dist/types/index.d.ts.map +1 -1
  98. package/dist/types/lib/injection-engine/index.d.ts +1 -0
  99. package/dist/types/lib/injection-engine/index.d.ts.map +1 -1
  100. package/dist/types/lib/injection-engine/skillToolDescriptors.d.ts +29 -0
  101. package/dist/types/lib/injection-engine/skillToolDescriptors.d.ts.map +1 -1
  102. package/dist/types/lib/injection-engine/types.d.ts +54 -0
  103. package/dist/types/lib/injection-engine/types.d.ts.map +1 -1
  104. package/dist/types/lib/saidByPerson.d.ts +100 -0
  105. package/dist/types/lib/saidByPerson.d.ts.map +1 -0
  106. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,112 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [9.84.0] - 2026-09-03
11
+
12
+ ### Fixed
13
+
14
+ - **`read_skill` refused the skill the model was already in.** A turn routed
15
+ decisively to `X`, the model called `read_skill("X")` to find out where it
16
+ stood, and the gate answered _"`read_skill("X")` is not reachable from here.
17
+ Reachable skills: …"_ — about the one skill whose body was in that call's
18
+ system prompt and whose tools were in that same call's tool list. The cursor
19
+ is in neither half of `hops ∪ open` by construction: `makeReachableSkills`
20
+ filters it out of its own successor set (a move to where you already are is
21
+ not a move) and `openSkillIds()` excludes every graph-wired skill. Nobody had
22
+ written the case for _"you asked for the room you are standing in."_ Read as a
23
+ claim about AVAILABILITY — which is how a model reads _"not reachable"_ — it
24
+ says the opposite of the request it arrived in. A field report recorded the
25
+ consequence three times in one day: the model concluded its capability was
26
+ gone and answered that it could not help, while the skill's tools sat on the
27
+ wire, loaded and callable.
28
+
29
+ A self-call now gets the truth instead of a refusal. It names where the model
30
+ stands and which tools it could call, taken from the merged wire list the LLM
31
+ stage actually sent, intersected with the skill's own declared tools — never
32
+ from the declaration alone. Every configuration that would make that false has
33
+ its own wording: a skill declaring no tools says so, a step or park hold-out
34
+ says the tools were withheld rather than naming them, and a call whose wire
35
+ cannot be established says nothing about tools at all. Mechanically it is still
36
+ a rejection — no activation, no cursor move, and the refusal budget still
37
+ counts it, because a self-call _loop_ is exactly the stuck model that budget
38
+ exists to escalate.
39
+
40
+ **Every clause is anchored to one named call.** A tool result is composed on
41
+ one iteration and re-read on every call after it, including the out-of-budget
42
+ wrap-up that carries no tools under _"Do not request tools."_ So the notice
43
+ makes no forward-looking claim at all: no exhortation to act, no offer of a
44
+ move, no clause conditioned on a budget or a posture that can change after the
45
+ sentence is written. Deixis counts as forward-looking — _"the call you just
46
+ made"_ denotes a different call on each re-read, so the anchor is named once
47
+ in the opening sentence and every later clause refers back to it.
48
+
49
+ - **The `read_skill` description said the same thing in the other channel.** The
50
+ current skill was listed under _"Not reachable from here (read_skill for these
51
+ will be refused)"_, where it appeared purely as an artefact of that same
52
+ filter. It is in neither column now, and the description names the cursor on
53
+ every call that has one — the positive signal whose absence was the root of the
54
+ field failure, since the system prompt carried the skill's body with nothing
55
+ saying which skill it was. A genuinely unreachable skill is still named there,
56
+ and a genuinely unreachable hop keeps its refusal word for word.
57
+
58
+ - **The description no longer predicts what `read_skill` will do.** Naming the
59
+ cursor is the fix; every sentence tried alongside it turned out false
60
+ somewhere. _"read_skill MOVES you to a DIFFERENT skill"_ is false at compose
61
+ time under `strictness: 'rails'` (every model hop refused) and under `'guard'`
62
+ off an outstanding menu, where the posture arm contradicts it head-on. Its
63
+ replacement — _"You do not need read_skill to go on using it"_ — was argued to
64
+ be a claim about necessity that no posture, budget or hold-out could falsify,
65
+ and the **park** falsifies it: a parked map member keeps the cursor, loses its
66
+ body and its tools, and `read_skill` is then the only door back. The
67
+ description is composed before the hold-outs run, so it cannot know when such a
68
+ claim would be lying. Outside an outstanding menu it now states the name and
69
+ stops. The menu's stay clause is unchanged.
70
+
71
+ - **Role visibility now covers the cursor.** The description read the cursor id
72
+ past the `hiddenIds` filter, so a role denied `skill_read` on the skill the
73
+ graph had routed to was still told _"You are in '\<that skill\>'"_ — leaking
74
+ the name of a capability no cursor move would ever grant it. A hidden cursor is
75
+ named nowhere: not as reachable, not as refusable, not as the cursor, and not
76
+ in the menu's stay clause. The security suite's _"a hidden skill is never
77
+ named"_ property is now driven on the `.skillGraph()` path as well as
78
+ `.skill()`; it stayed green through the leak because its agents used `.skill()`
79
+ only, so the leaking line never executed.
80
+
81
+ ### Added
82
+
83
+ - **`saidByPerson(ctx)` / `isSaidByPerson(msg)` — telling what a person said from
84
+ what the library wrote.** Five classes of `role: 'user'` message are authored
85
+ by this library, not by a person: the compaction frame, the drop notice (whose
86
+ text names tools), the schema-check and evidence-check corrections, and any
87
+ injection-delivered message. The window's own refusal engine has always applied
88
+ that rule; a `when` predicate could not, because `InjectionContext.history`
89
+ exposes only `{ role, content, toolName? }`. An author writing an entry rule
90
+ that reads history was silently matching on our own bookkeeping. One
91
+ implementation, reused by both — the rule cannot drift between routing and the
92
+ window.
93
+
94
+ - **`SkillRejectedPayload.reason`** — `'self-call' | 'unreachable' | 'posture'`,
95
+ optional and additive, so a consumer can tell a self-call from a genuine
96
+ unreachable hop without comparing two fields.
97
+
98
+ ### Documentation
99
+
100
+ - **`strictness` says what a posture governs, exactly.** A posture governs the
101
+ model's `read_skill` door and nothing else. Two doors stay open under all three
102
+ postures: OPEN skills, already stated, and a tool's `propose-transition`, now
103
+ stated with its reason and its reachability check. `'rails'` means _the model
104
+ never routes_ — never _"nothing but my declared edges routes"_; a tool of yours
105
+ that proposes is a route you declared in code instead of in the graph. No
106
+ behaviour changed: the exemption is recorded in three places and pinned by a
107
+ test whose title is the argument.
108
+
109
+ - **The injection-engine README's runtime picture matched an older engine.** Its
110
+ diagram drew one box that "evaluates triggers"; the engine is a four-stage
111
+ footprintjs subflow — Gather, Evaluate, Route, Delta — and the cursor, the step
112
+ pointer, the instruction leases and map engagement all advance inside Evaluate.
113
+ The events table placed `context.evaluated` at subflow exit; the code emits it
114
+ in stage 2 of 4. Both corrected, along with three counts that had drifted.
115
+
10
116
  ## [9.83.0] - 2026-09-03
11
117
 
12
118
  ### Fixed
@@ -14,13 +120,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
120
  - **The evidence gate claimed a boundary it did not measure.** Both of its
15
121
  user-facing sentences — the correction it sends the model
16
122
  (`buildEvidenceCorrection`) and the warning it prints an operator
17
- (`evidenceRefusalSentence`) — said the flagged values *"appear in NO tool
18
- result **from this turn**"*. The index behind them has never been turn-scoped:
123
+ (`evidenceRefusalSentence`) — said the flagged values _"appear in NO tool
124
+ result **from this turn**"_. The index behind them has never been turn-scoped:
19
125
  it walks every `role: 'tool'` turn in the history. The library was asserting a
20
126
  scope it could not honour, in the two places that assertion is read.
21
127
 
22
- Both now say what the check really reaches — *"appear in no tool result this
23
- run read"* — which is both true and the stronger claim, and the operator
128
+ Both now say what the check really reaches — _"appear in no tool result this
129
+ run read"_ — which is both true and the stronger claim, and the operator
24
130
  sentence adds the two facts a reader needs: that the corpus is the LIVE
25
131
  WINDOW (a window strategy rewrites `scope.history` in place, so a dropped
26
132
  result is not in it), and that `noticePriorTurnEvidence` is what answers the
@@ -34,8 +140,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
34
140
 
35
141
  The measured failure: a consumer's agent answered a data question with **zero
36
142
  tool calls**, and the gate approved it — `LLM calls 1 · Tool calls 0 ·
37
- Iterations 1`, then *"All 7 values in the answer were found in what the tools
38
- returned — the answer stands."* They were found: in an inventory result from
143
+ Iterations 1`, then _"All 7 values in the answer were found in what the tools
144
+ returned — the answer stands."_ They were found: in an inventory result from
39
145
  four turns earlier, fetched for a different question. The user had asked about
40
146
  array performance; the answer recommended enabling a collector that had been
41
147
  running for months. Two turns did it back to back. Every rail passed honestly
@@ -55,14 +161,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
55
161
  .namesAndNumbersFromEvidence() // ← the other half: it owns the extractor
56
162
  .build();
57
163
 
58
- await agent.run('what arrays are there?'); // fetches, answers, files nothing
164
+ await agent.run('what arrays are there?'); // fetches, answers, files nothing
59
165
  await agent.followUp('how is array performance?'); // no tool call, answers from turn 1
60
166
  // → prior-turn-evidence: 3 grounded value(s), all last served in turn 1,
61
167
  // and this turn called no tool at all.
62
168
  ```
63
169
 
64
170
  **The corpus is deliberately NOT narrowed to this turn.** That would have made
65
- the old sentence true and been the wrong fix: *"and what about that disk?"*
171
+ the old sentence true and been the wrong fix: _"and what about that disk?"_
66
172
  leans on the previous turn's rows legitimately, and a check that cries wolf is
67
173
  a check somebody switches off. ONE grounded value from this turn's own results
68
174
  files nothing — not a threshold to tune, but the falsification of the claim
@@ -106,7 +212,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
106
212
  is generated from what the run itself said: the branch descriptions the chart
107
213
  declared, and the rule labels this run's `decide()` evidence carried. For one
108
214
  branch, both sources are silent by construction — the DEFAULT. It is the
109
- branch chosen by *no rule* (it fires exactly when every rule failed, so no
215
+ branch chosen by _no rule_ (it fires exactly when every rule failed, so no
110
216
  `label` describes it), and when the decider lives inside a dynamically
111
217
  generated fan-out branch the branch chart does not exist at build time either,
112
218
  so there is no declared description to fall back on.
@@ -183,7 +289,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
183
289
  sdk: { Client, StreamableHTTPClientTransport },
184
290
  transport: { transport: 'http', url: '/py/mcp' },
185
291
  });
186
- const tools = await sidecar.tools(); // the same readonly Tool[], _meta and all
292
+ const tools = await sidecar.tools(); // the same readonly Tool[], _meta and all
187
293
  ```
188
294
 
189
295
  **`connection?: McpConnection`** — the full escape hatch: you connect the
@@ -257,8 +363,8 @@ arms through a real socket against the real SDK.
257
363
 
258
364
  **What none of that proves: a browser.** This repo has no browser test
259
365
  environment, so nobody has yet driven initialize/listTools/callTool from an
260
- actual page. The honest status is *proven in Node, fenced at the graph, not gated
261
- in a browser.* Three costs land on the app, not here: your server must send CORS
366
+ actual page. The honest status is _proven in Node, fenced at the graph, not gated
367
+ in a browser._ Three costs land on the app, not here: your server must send CORS
262
368
  headers (every MCP request preflights, and `Mcp-Session-Id` must be in
263
369
  `Access-Control-Expose-Headers`) — `mcpServe` sends none; SSE through a dev or
264
370
  production proxy is unproven; and the SDK's client path adds roughly 260 KB
@@ -277,8 +383,8 @@ first time a tool with an `outputSchema` is validated.
277
383
  had a standing rule against exactly that. The library was overruling it.
278
384
 
279
385
  The instruction was ours. Every verdict projection shipped `table`
280
- pre-rendered with `VERDICT_RENDER_NOTE`: *"table is PRE-RENDERED over the
281
- same rows as `verdicts` — output it VERBATIM."* That note is RIGHT wherever
386
+ pre-rendered with `VERDICT_RENDER_NOTE`: _"table is PRE-RENDERED over the
387
+ same rows as `verdicts` — output it VERBATIM."_ That note is RIGHT wherever
282
388
  the model's words are the rows' only surface — a chat client, a log line, an
283
389
  email — because the alternative there is retyping, and a retyped identifier
284
390
  that looks right and matches nothing is the failure the note exists to stop.
@@ -291,7 +397,7 @@ first time a tool with an `outputSchema` is validated.
291
397
  it is in. So the caller says, in one word:
292
398
 
293
399
  ```ts
294
- runbookAsTool({ /* … */ presentation: 'panel' }); // default: 'prose'
400
+ runbookAsTool({ /* … */ presentation: 'panel' }); // default: 'prose'
295
401
  ```
296
402
 
297
403
  - **`'prose'` (the default)** — today's envelope, key for key: `table`
@@ -357,7 +463,7 @@ first time a tool with an `outputSchema` is validated.
357
463
  `recording_note`. The wire ops that already redeem the walk redeem this with
358
464
  **zero new operations**.
359
465
 
360
- **OPT-IN, and that is the honest default.** A walk carries *sentences about*
466
+ **OPT-IN, and that is the honest default.** A walk carries _sentences about_
361
467
  what happened and no payload from it — values are off by construction
362
468
  (`narrative({ includeValues: false })`). A recording is the run: shared
363
469
  state, the whole commit log, every attached recorder's data — **whatever the
@@ -393,7 +499,7 @@ first time a tool with an `outputSchema` is validated.
393
499
  the spine exists to prevent.
394
500
 
395
501
  **`events` is empty by construction, and says so.** It is the typed
396
- *agentfootprint* stream, fired by an agent turn; what ran here is a
502
+ _agentfootprint_ stream, fired by an agent turn; what ran here is a
397
503
  footprintjs chart on its own executor, which fires none. All three keys are
398
504
  present (that is what a viewer reads), the empty array is the honest count,
399
505
  and the note states it so nobody reads it as a dropped stream — the walk's
@@ -412,8 +518,8 @@ first time a tool with an `outputSchema` is validated.
412
518
 
413
519
  - **`Tool.resultColumns` + `checkColumnTypes` — the column-type contract: a
414
520
  tool declares what its rows contain, and the library checks the rows against
415
- it at the boundary.** Three recorded failures, and they are one shape — *a
416
- number became something else, and nothing noticed at the seam*:
521
+ it at the boundary.** Three recorded failures, and they are one shape — _a
522
+ number became something else, and nothing noticed at the seam_:
417
523
 
418
524
  1. A mapping report wrote `str(m.get("logical_unit_number") or "")`. **LUN 0
419
525
  is falsy**, so LUN 0 was stored as an EMPTY STRING on 2,094 mappings, and
@@ -442,19 +548,19 @@ first time a tool with an `outputSchema` is validated.
442
548
  `CostBudget` two-spellings pattern, normalized once).
443
549
 
444
550
  **THE CEILING**, exported as `COLUMN_TYPE_CEILING` and quoted verbatim into
445
- every finding, the `EMPTY_LOOKUP_CEILING` law: *"This judges TYPE, never
551
+ every finding, the `EMPTY_LOOKUP_CEILING` law: _"This judges TYPE, never
446
552
  MEANING — it can see that a column declared `number` holds a string, and it
447
553
  can never see that the string should have been 0, or that a 0.0 should have
448
554
  been an 8; a column whose every value has its declared type passes here and
449
- can still be wrong."* Failures 1 and 3 are caught. **Failure 2 is not, and
555
+ can still be wrong."_ Failures 1 and 3 are caught. **Failure 2 is not, and
450
556
  never will be** — `0.0` is a perfectly good number — and the check says so
451
557
  out loud rather than letting a green row imply otherwise.
452
558
 
453
559
  **TWO finding kinds, because the field bug turned on the difference.** New
454
560
  `ContextErrorKind`s at the **write seam**: **`column-type-mismatch`** (the
455
561
  column is THERE and holds the wrong thing) and **`missing-column`** (the
456
- declared column is in NONE of the rows). *"The value is not what it should
457
- be"* sends a person to the mapping code; *"the column was never delivered"*
562
+ declared column is in NONE of the rows). _"The value is not what it should
563
+ be"_ sends a person to the mapping code; _"the column was never delivered"_
458
564
  sends them to the query. A checker that said only "something is off with
459
565
  logical_unit_number" would have helped with neither. Each finding names the
460
566
  column, the offending value quoted, the rows affected of the rows read, and
@@ -524,7 +630,7 @@ first time a tool with an `outputSchema` is validated.
524
630
  it came back empty.** A triage agent's reverse-lookup tool filtered a column
525
631
  before a pivot, so the column did not exist yet and EVERY reverse lookup
526
632
  returned an empty result — for every identifier, always. The tool then
527
- answered *successfully* with an empty list, and the agent reported in a
633
+ answered _successfully_ with an empty list, and the agent reported in a
528
634
  table, with confidence, that the device was not logged in to any port on any
529
635
  collected switch, advising a check of the physical cabling. It was logged in
530
636
  the whole time. Every rail passed, and passed honestly: nothing errored,
@@ -548,9 +654,9 @@ first time a tool with an `outputSchema` is validated.
548
654
  for the broken filter and for the honest absence. The bound ships as one
549
655
  exported string, `EMPTY_LOOKUP_CEILING`, quoted verbatim into every message
550
656
  so it cannot drift out of one doc and leave a reader thinking the library
551
- knows more than it does: *"An empty result can be perfectly true — the thing
657
+ knows more than it does: _"An empty result can be perfectly true — the thing
552
658
  may exist and simply have nothing to show right now — so this is a place to
553
- look, never a verdict that anything is wrong."*
659
+ look, never a verdict that anything is wrong."_
554
660
 
555
661
  Deliberately NOT `dangling-reference`, whose meaning is the opposite: there
556
662
  the ground has left reach; here the ground IS in reach and the lookup found
@@ -566,7 +672,7 @@ first time a tool with an `outputSchema` is validated.
566
672
  make impossible.
567
673
 
568
674
  **Armed by two halves**, and the second one is why: `noticeEmptyLookups:
569
- true` on `Agent.create` **and** at least one tool declaring `argumentsFrom`.
675
+ true` on `Agent.create` **and** at least one tool declaring `argumentsFrom`.
570
676
  The declaration alone is not enough — it already arms two other checks, and
571
677
  an advisory that armed itself off a declaration made for something else
572
678
  would not be opt-in at all. **Default off is byte-identical**: no finding, no
@@ -637,7 +743,7 @@ first time a tool with an `outputSchema` is validated.
637
743
  pre-rendered table, and the recorded walk that lets a reader CHECK the
638
744
  verdict instead of trusting it. `runbookAsTool` is that envelope as one
639
745
  declaration bag; the smallest legal call is `{ name, description,
640
- procedure }` and it still yields the honest spine.
746
+ procedure }` and it still yields the honest spine.
641
747
 
642
748
  **The mandatory honesty spine**, on every answer whatever the runbook's
643
749
  shape: `af_coverage` (three lists + a sentence naming the rule set and
@@ -673,7 +779,7 @@ first time a tool with an `outputSchema` is validated.
673
779
  tools are invisible — there is no build-time list, the stated 9.72.0
674
780
  caveat), so stages compose registered sources instead of importing modules
675
781
  and building a second query stack. Inner calls run with `hasArtifacts:
676
- false` (one answer, one ticket — never competing chips), a derived
782
+ false` (one answer, one ticket — never competing chips), a derived
677
783
  toolCallId naming the outer call, `needs` resolved on the fail-closed
678
784
  non-interactive path, and `checkIn`/`wants` tools refused BY NAME (an inner
679
785
  call cannot pause, and must never silently skip a consent gate).
@@ -697,8 +803,8 @@ first time a tool with an `outputSchema` is validated.
697
803
  run: four tool results carried real numbers, a compute tool that could sum
698
804
  them was registered — with `wants` declared over the staged dataset kind —
699
805
  and the app's prompt said to use it. The model summed the numbers in its
700
- head anyway and stated the total; the evidence gate recorded *"appears in no
701
- tool result"* and the answer shipped, because the posture only observed. The
806
+ head anyway and stated the total; the evidence gate recorded _"appears in no
807
+ tool result"_ and the answer shipped, because the posture only observed. The
702
808
  app patched it with more prose. The library-shaped fix is two mechanisms it
703
809
  already owns, on the one dial it already has:
704
810
 
@@ -732,9 +838,9 @@ first time a tool with an `outputSchema` is validated.
732
838
  `'guard'` allows the one bounded revision then delivers with both attempts
733
839
  on the record, `'rails'` refuses with `UnsupportedValuesError`. What the
734
840
  revision gains: when the flagged turn holds staged refs a served `wants`
735
- tool can spend, the correction now names them — *"pass 'art_…'
841
+ tool can spend, the correction now names them — _"pass 'art\_…'
736
842
  (dataset/rows) to `compute` — compute the number there and answer with what
737
- it returns"* — inside the authored frame, so the quoted values still come
843
+ it returns"_ — inside the authored frame, so the quoted values still come
738
844
  last and the exempt-corpus fence is untouched. The `revision-asked`
739
845
  `evidence_checked` event carries the same facts additively (`stagedRefs`,
740
846
  `spenderTools`). Absent everything — no gate, `nudge` unset, or no
@@ -762,7 +868,7 @@ first time a tool with an `outputSchema` is validated.
762
868
 
763
869
  **`foundry()` — the project-endpoint provider** (`agentfootprint/providers`).
764
870
  The JS answer to Microsoft's `FoundryChatClient(project_endpoint, model,
765
- credential)`: point it at a Foundry project endpoint (or let the hosted
871
+ credential)`: point it at a Foundry project endpoint (or let the hosted
766
872
  platform's auto-injected `FOUNDRY_PROJECT_ENDPOINT` supply it), name the
767
873
  deployment (`AZURE_AI_MODEL_DEPLOYMENT_NAME ?? MODEL_NAME`), and auth is an
768
874
  Entra `TokenCredential`, an api key, or — given neither — the platform's own
@@ -888,7 +994,6 @@ first time a tool with an `outputSchema` is validated.
888
994
  tests fail against the previous source (proven by restoring it); no public
889
995
  API change; `OllamaUnavailableError` is byte-identical.
890
996
 
891
-
892
997
  ## [9.73.0] - 2026-08-27
893
998
 
894
999
  ### Fixed
@@ -925,13 +1030,15 @@ first time a tool with an `outputSchema` is validated.
925
1030
 
926
1031
  ```ts
927
1032
  const agent = Agent.create({
928
- provider, model,
1033
+ provider,
1034
+ model,
929
1035
  // DECLARED, never ambient — this option is the only door.
930
- externalGrounds: () => viewerSelection.cells.map((cell) => ({
931
- value: cell.text, // verified by the app against the artifact
932
- source: 'viewer-selection', // the audit label that travels
933
- })),
934
- })
1036
+ externalGrounds: () =>
1037
+ viewerSelection.cells.map((cell) => ({
1038
+ value: cell.text, // verified by the app against the artifact
1039
+ source: 'viewer-selection', // the audit label that travels
1040
+ })),
1041
+ });
935
1042
  ```
936
1043
 
937
1044
  The provider is consulted once per LLM response that contains an armed call,
@@ -1008,25 +1115,25 @@ first time a tool with an `outputSchema` is validated.
1008
1115
  namespaced key (`MCP_TOOL_EXTRAS_KEY`, the string `agentfootprint`), and
1009
1116
  `mcpClient` / `mockMcpClient` read them back onto the registered `Tool`:
1010
1117
 
1011
- | field | what it arms on the consuming side |
1012
- |---|---|
1013
- | `argumentsFrom` | the dangling-reference and unsupported-argument checks |
1014
- | `resultKind` | placement's mint — a placed result a `wants` argument can spend |
1015
- | `owner` | the identity edge subject-joined checks read |
1016
- | `resultClass` | the per-class `check:semantics` rules |
1017
- | `resultCeiling` | the author's refusing ceiling on an oversized result |
1118
+ | field | what it arms on the consuming side |
1119
+ | --------------- | --------------------------------------------------------------- |
1120
+ | `argumentsFrom` | the dangling-reference and unsupported-argument checks |
1121
+ | `resultKind` | placement's mint — a placed result a `wants` argument can spend |
1122
+ | `owner` | the identity edge subject-joined checks read |
1123
+ | `resultClass` | the per-class `check:semantics` rules |
1124
+ | `resultCeiling` | the author's refusing ceiling on an oversized result |
1018
1125
 
1019
1126
  ```ts
1020
1127
  const fleet = await mcpClient({ name: 'fleet-mcp', transport });
1021
1128
  const agent = Agent.create({ provider, model })
1022
- .tools(await fleet.tools()) // backup_status declares argumentsFrom: ['fleet_report']
1129
+ .tools(await fleet.tools()) // backup_status declares argumentsFrom: ['fleet_report']
1023
1130
  .build();
1024
1131
  // …and the choice seam now files `unsupported-argument` for it, exactly as
1025
1132
  // it would for a local defineTool — pinned end to end, disposition row included.
1026
1133
  ```
1027
1134
 
1028
- **The inclusion bar, stated where the list lives:** *a declaration a
1029
- consumer-side check or rail reads; nothing that governs execution.* `needs`
1135
+ **The inclusion bar, stated where the list lives:** _a declaration a
1136
+ consumer-side check or rail reads; nothing that governs execution._ `needs`
1030
1137
  (credentials), `checkIn` (human consent) and the session hooks are excluded
1031
1138
  and always will be — they decide how a tool RUNS, and the tool runs on the
1032
1139
  server. A client holding a consent gate the only executor already held is
@@ -1088,7 +1195,7 @@ first time a tool with an `outputSchema` is validated.
1088
1195
  });
1089
1196
 
1090
1197
  // elsewhere — resolves now, and would have been a kind mismatch before
1091
- defineTool({ name: 'chart', wants: { dataset: 'dataset/rows' }, /* … */ });
1198
+ defineTool({ name: 'chart', wants: { dataset: 'dataset/rows' } /* … */ });
1092
1199
  ```
1093
1200
 
1094
1201
  - **The matcher is untouched.** Nothing here loosens `wants`; exact match is
@@ -1157,9 +1264,9 @@ first time a tool with an `outputSchema` is validated.
1157
1264
  that earns the port its keep —
1158
1265
 
1159
1266
  ```ts
1160
- await session.handControlTo('person'); // the automation stream stops
1267
+ await session.handControlTo('person'); // the automation stream stops
1161
1268
  // …they sign in, clear the CAPTCHA, approve the consent screen, watching live
1162
- await session.handControlTo('agent'); // and the agent carries on
1269
+ await session.handControlTo('agent'); // and the agent carries on
1163
1270
  ```
1164
1271
 
1165
1272
  Pair it with a check-in and the agent **pauses** rather than guesses: the
@@ -1177,7 +1284,7 @@ first time a tool with an `outputSchema` is validated.
1177
1284
  **What verification changed.** The `InvokeBrowser` action union, read off a
1178
1285
  real install of `@aws-sdk/client-bedrock-agentcore` 3.1118.0, is exactly
1179
1286
  `mouseClick | mouseMove | mouseDrag | mouseScroll | keyType | keyPress |
1180
- keyShortcut | screenshot` — with **no navigate member at all**. An adapter
1287
+ keyShortcut | screenshot` — with **no navigate member at all**. An adapter
1181
1288
  written from memory would have invented page verbs for a door that has none.
1182
1289
  The same pass fixed `MouseClickArguments` (`{ x, y, button?, clickCount? }`,
1183
1290
  buttons `LEFT|MIDDLE|RIGHT`), `KeyPressArguments` (`{ key, presses? }`) and
@@ -1213,6 +1320,7 @@ first time a tool with an `outputSchema` is validated.
1213
1320
  another agent (Strands, LangGraph, Google ADK, a Marketplace listing)
1214
1321
  discovers yours through its agent card and calls it. The same split as 9.65.0,
1215
1322
  for the third time:
1323
+
1216
1324
  - **`a2aWire()`** is the A2A PROTOCOL — JSON-RPC 2.0, `message/send`, text
1217
1325
  parts, artifacts, `A2A_PROTOCOL_VERSION` `0.3.0` — an open protocol with no
1218
1326
  vendor in it, exported in its own right along with
@@ -1252,8 +1360,8 @@ first time a tool with an `outputSchema` is validated.
1252
1360
  - **A capability this library claimed and could not honour.** `httpHost`
1253
1361
  declares `['streaming']` by default, so the A2A host inherited it while
1254
1362
  `message/send` has nowhere to put a chunk: `requireCapability(host,
1255
- 'streaming')` would have passed for a host that then delivered none. The
1256
- conformance suite caught it — it asserts chunks *if and only if* the
1363
+ 'streaming')` would have passed for a host that then delivered none. The
1364
+ conformance suite caught it — it asserts chunks _if and only if_ the
1257
1365
  capability is declared — and the adapter now declares `[]`.
1258
1366
 
1259
1367
  ### Changed
@@ -1271,6 +1379,7 @@ first time a tool with an `outputSchema` is validated.
1271
1379
  looking anything up** (`agentfootprint/providers`). `gatewayTransport` says of
1272
1380
  itself that nothing in it is vendor-specific, and that stays true because the
1273
1381
  four facts which ARE AgentCore's now live in one file beside it:
1382
+
1274
1383
  - **the endpoint** — `agentCoreGatewayUrl({ gatewayId, region })` builds
1275
1384
  `https://{gatewayId}.gateway.bedrock-agentcore.{region}.amazonaws.com/mcp`,
1276
1385
  a hostname nobody recalls correctly, which is why it is a function and not a
@@ -1336,6 +1445,7 @@ first time a tool with an `outputSchema` is validated.
1336
1445
  install of `@aws-sdk/client-bedrock-agentcore` **3.1118.0** — names, request
1337
1446
  shapes and enum values read off the package rather than remembered, which is
1338
1447
  the 9.4.0 law:
1448
+
1339
1449
  - **`userFlow: 'consent' | 'exchange'`** — `'exchange'` sends
1340
1450
  `ON_BEHALF_OF_TOKEN_EXCHANGE`, trading the person's existing login for a
1341
1451
  scoped downstream token with no consent screen at any point. Default stays
@@ -1492,7 +1602,7 @@ first time a tool with an `outputSchema` is validated.
1492
1602
 
1493
1603
  The field story: a tool result ended with an offer — "I can also map these
1494
1604
  ids to volume names" — and the person answered "yes please". The model bound
1495
- *that sentence* as the identifier argument and dispatched. The tool's schema
1605
+ _that sentence_ as the identifier argument and dispatched. The tool's schema
1496
1606
  DECLARED the identifier's shape, in a `pattern` that "yes please" could never
1497
1607
  match; the pre-dispatch validator simply did not read the keyword. So the
1498
1608
  call went out, failed downstream, and cost a round trip — and the consumer
@@ -1637,7 +1747,7 @@ first time a tool with an `outputSchema` is validated.
1637
1747
  check, so one busy check could carry the total while another check looked
1638
1748
  at nothing at all — the per-check rows said so underneath (`⚠ ran 0×`), but
1639
1749
  a reader who stops at the headline got the rosier story. The headline now
1640
- states coverage: either *all* registered checks ran, or it says COVERAGE IS
1750
+ states coverage: either _all_ registered checks ran, or it says COVERAGE IS
1641
1751
  PARTIAL and names the checks that checked nothing. Same law as the rows —
1642
1752
  a check that never saw a subject is silent about its seam, never a pass.
1643
1753
 
@@ -1667,38 +1777,40 @@ first time a tool with an `outputSchema` is validated.
1667
1777
  - **Six defects in the Context Integrity family, caught by an adversarial
1668
1778
  review before release** — each survived two independent attempts to refute
1669
1779
  it, and each is now pinned by a red-proved regression test.
1670
- - *A claim finding's identity ignored the FIELD*, so a contract naming two
1780
+
1781
+ - _A claim finding's identity ignored the FIELD_, so a contract naming two
1671
1782
  fields of one entity (the shape `.claims()` itself tells you to write)
1672
1783
  filed one event and swallowed the rest — while the disposition ledger
1673
1784
  counted them all, leaving the two accounts of one run disagreeing.
1674
1785
  `ContextError.predicate` now rides the identity, mirroring the
1675
1786
  substrate's own `assertionKey`; findings that never set it keep exactly
1676
1787
  the identity they had.
1677
- - *The dangling-reference check was DEAD under `reactMode:
1678
- 'dynamic-grouped'`* — `compactions` was threaded into the wrong mapper,
1788
+ - _The dangling-reference check was DEAD under `reactMode:
1789
+ 'dynamic-grouped'`_ — `compactions` was threaded into the wrong mapper,
1679
1790
  so the check saw an empty window ledger every pass and filed a healthy
1680
1791
  verdict. Chart-shape parity is now pinned by tests that run the same
1681
1792
  trap under both dynamic shapes.
1682
- - *The dev canary structurally disabled the wiring-rot theorem.* A minted
1793
+ - _The dev canary structurally disabled the wiring-rot theorem._ A minted
1683
1794
  canary proves the pure function still works; it says nothing about
1684
1795
  whether the pipeline ever calls it (`beginIntegrityRun` mints by calling
1685
1796
  the function directly). Masking theorem (i) with it meant the alarm this
1686
1797
  ledger exists for could never fire.
1687
- - *`workExisted` was hardcoded `true`* on every exit path, so a run that
1798
+ - _`workExisted` was hardcoded `true`_ on every exit path, so a run that
1688
1799
  died or paused before its first LLM call reported every registered
1689
1800
  checker as dead. It is now measured from a signal the integrity code
1690
1801
  does not itself write.
1691
- - *An answer agreeing with a settled non-reading was filed as an advisory*
1802
+ - _An answer agreeing with a settled non-reading was filed as an advisory_
1692
1803
  — `null` reported for a fact whose settled value is `null` is agreement,
1693
1804
  not doubt.
1694
- - *The claim ledger accumulated for agents that never declared a
1695
- contract*, and appended by whole-array spread. It is now gated on
1805
+ - _The claim ledger accumulated for agents that never declared a
1806
+ contract_, and appended by whole-array spread. It is now gated on
1696
1807
  `.claims()` and appends without the quadratic copy — restoring the
1697
1808
  zero-delta promise for every agent that configured none of this.
1698
1809
 
1699
1810
  - **Two honesty defects in `find_context_errors`**, both caught by the same
1700
1811
  adversarial review and both red-proved by a failing test first.
1701
- - *The green headline never read the counts it claimed to summarise.*
1812
+
1813
+ - _The green headline never read the counts it claimed to summarise._
1702
1814
  "The checkers below RAN; nothing they cover was violated" was printed on
1703
1815
  the mere absence of a finding, so a run whose every encounter was
1704
1816
  `unreachable` (the check could not see the evidence), whose checks all
@@ -1711,7 +1823,7 @@ first time a tool with an `outputSchema` is validated.
1711
1823
  all, each keep their own sentence. A checker ROW with zero `checked`
1712
1824
  stopped saying "the checker ran and found nothing at this seam" for the
1713
1825
  same reason.
1714
- - *The tool advertised defect classes no check can file.* The `kind` enum
1826
+ - _The tool advertised defect classes no check can file._ The `kind` enum
1715
1827
  was pinned to the whole `ContextErrorKind` union, so it offered
1716
1828
  `unsupported-argument` and `duplicate-execution` — classes no check in
1717
1829
  this build files — and answering one returned a negative verdict about
@@ -1776,7 +1888,7 @@ first time a tool with an `outputSchema` is validated.
1776
1888
  run's disposition rows ride along, so "the checkers ran and found nothing"
1777
1889
  and "no checker was registered for that seam" stay different answers, and
1778
1890
  a registered check with zero encounters is named as wiring rot. Honest
1779
- absence has its own sentences: no event tail is *no finding evidence*, a
1891
+ absence has its own sentences: no event tail is _no finding evidence_, a
1780
1892
  tail with no integrity events says the channel is empty and why, and rows
1781
1893
  reporting findings the tail no longer carries say **evidence missing** —
1782
1894
  none of them ever reads as "no context errors found". Synthetic canaries
@@ -1812,7 +1924,7 @@ first time a tool with an `outputSchema` is validated.
1812
1924
  - **`Tool.argumentsFrom` + the dangling-reference check** (the closure
1813
1925
  check's decidable fragment). A tool author can now declare where a tool's
1814
1926
  arguments come from — `defineTool({ name: 'screen_fire', argumentsFrom:
1815
- ['whats_here'] })` — and `callLLM` checks at request assembly that every
1927
+ ['whats_here'] })` — and `callLLM` checks at request assembly that every
1816
1928
  served tool's declared grounds still have results in the window: a ground
1817
1929
  the window ledger says was evicted (`droppedObservations`) with nothing
1818
1930
  re-established files a `dangling-reference` finding at seam `'compose'`,
@@ -1856,10 +1968,10 @@ first time a tool with an `outputSchema` is validated.
1856
1968
 
1857
1969
  - **The assertion algebra and the one visible finding type.** An
1858
1970
  `Assertion` is keyed by `(subject, predicate, epoch)` with two rules
1859
- that do the work: *serving is asserting; history is quotation* (checks
1971
+ that do the work: _serving is asserting; history is quotation_ (checks
1860
1972
  never fire across the quoted stratum — the whole stale-but-honest
1861
- false-positive class, closed structurally) and *single-valued by
1862
- default* (you declare exemptions, never rules). Unknown `Claim`s never
1973
+ false-positive class, closed structurally) and _single-valued by
1974
+ default_ (you declare exemptions, never rules). Unknown `Claim`s never
1863
1975
  participate in a comparison. `conflictsOf()` is the pure exclusion
1864
1976
  comparison; `ContextError` is the uniform finding — plain kinds
1865
1977
  (`invariant-violation`, `unsupported-argument`, `dangling-reference`,
@@ -1913,16 +2025,16 @@ when this patch was cut, and they are additive.
1913
2025
  ### Fixed
1914
2026
 
1915
2027
  - **The cache report no longer invents a cause for a turn it could not
1916
- measure.** *What changed:* when `cacheRecorder().report()` hands back an
2028
+ measure.** _What changed:_ when `cacheRecorder().report()` hands back an
1917
2029
  unknown hit rate, the sentence attached to it is now the one the calls
1918
2030
  themselves gave — most often "no CacheStrategy was given to
1919
2031
  `cacheRecorder()`, so nothing read the usage". When the calls disagree about
1920
2032
  why, the summary says they disagreed and lists the reasons (up to three, then
1921
- a count of the rest) rather than silently choosing one. *Why it was not
1922
- there:* the summary sentence was typed into the code as a fixed string, "the
2033
+ a count of the rest) rather than silently choosing one. _Why it was not
2034
+ there:_ the summary sentence was typed into the code as a fixed string, "the
1923
2035
  provider reported no cache fields" — true for the case the author had in
1924
2036
  mind, a guess for every other, and it overwrote what each call had already
1925
- stated. *How it improves:* run without a strategy against a provider that DID
2037
+ stated. _How it improves:_ run without a strategy against a provider that DID
1926
2038
  report cache traffic, and 9.59.0 told you your provider was reporting
1927
2039
  nothing — sending you off to debug a provider that was working fine, instead
1928
2040
  of naming the one line missing from your own setup. You now read the real
@@ -1945,113 +2057,113 @@ the published bytes and is unchanged.
1945
2057
 
1946
2058
  ### Fixed
1947
2059
 
1948
- - **A red gate could reach npm, and did.** *What changed:* the docs-truth
2060
+ - **A red gate could reach npm, and did.** _What changed:_ the docs-truth
1949
2061
  ratchet now runs inside the build job that the publishing job depends on,
1950
2062
  so a red gate fails the build and the publish never starts — however the
1951
- release was created. *Why it was not there:* the gate existed, but CI ran it
2063
+ release was created. _Why it was not there:_ the gate existed, but CI ran it
1952
2064
  on `push` while the publish workflow runs on `release: published`, and the
1953
2065
  release script fires the release seconds after the push. The two raced, and
1954
- npm never waited for a verdict. *How it improves:* 9.58.0 shipped with a red
2066
+ npm never waited for a verdict. _How it improves:_ 9.58.0 shipped with a red
1955
2067
  ratchet; that is now structurally impossible.
1956
- - **A generated report was being edited by hand.** *What changed:*
2068
+ - **A generated report was being edited by hand.** _What changed:_
1957
2069
  `npm run docs:truth:report` regenerates the report **without** touching the
1958
2070
  accepted-debt baseline, and a test fails if the committed report is not what
1959
- the generator produces. *Why it was not there:* the report could only be
2071
+ the generator produces. _Why it was not there:_ the report could only be
1960
2072
  regenerated by also re-recording the debt, so an author who merely wanted the
1961
2073
  numbers to match reality had to choose between accepting unrelated debt and
1962
2074
  editing the file. They edited the file: the 9.58.0 release commit changed
1963
2075
  "103 typed events" to "105" and nothing else, leaving the export count stale
1964
- and the ratchet red. *How it improves:* restating the truth and accepting
2076
+ and the ratchet red. _How it improves:_ restating the truth and accepting
1965
2077
  debt are now separate acts, and the file cannot silently drift.
1966
2078
  - **The cache meter reported 0% for turns that hit cache on every call.**
1967
- *What changed:* the strategies read the framework's normalised usage instead
2079
+ _What changed:_ the strategies read the framework's normalised usage instead
1968
2080
  of raw provider field names, and every number in the report is now a `Claim`
1969
- — a value that says how it knows itself. *Why it was not there:* the
2081
+ — a value that says how it knows itself. _Why it was not there:_ the
1970
2082
  strategies parsed `cache_read_input_tokens` off a value that has never
1971
2083
  carried it, so every field read as missing and nothing was recorded; and the
1972
2084
  report typed its totals as plain numbers, so "nobody measured" and "measured,
1973
2085
  and it was zero" looked identical. The test fixtures were themselves
1974
2086
  provider-shaped, which is how it survived a release with a green suite.
1975
- *How it improves:* a real hit rate, an unmeasured turn that renders as
2087
+ _How it improves:_ a real hit rate, an unmeasured turn that renders as
1976
2088
  unmeasured, and a rate that always states its own denominator ("3 of 20
1977
2089
  calls"). A **silent non-cache** — a prompt below the model's minimum
1978
2090
  cacheable size, which providers process without caching and without an error
1979
2091
  — is now visibly different from a turn nobody measured.
1980
- - **A meter was attached to a provider that cannot feed it.** *What changed:*
2092
+ - **A meter was attached to a provider that cannot feed it.** _What changed:_
1981
2093
  the Bedrock strategy declares itself disabled, passes requests through
1982
- untouched, and answers "not applicable" with the reason. *Why it was not
1983
- there:* it claimed full support and clamped cache markers onto a request
2094
+ untouched, and answers "not applicable" with the reason. _Why it was not
2095
+ there:_ it claimed full support and clamped cache markers onto a request
1984
2096
  field our Bedrock provider discards, reporting markers that never reached a
1985
- wire. *How it improves:* it stays registered, so a Bedrock user is told the
2097
+ wire. _How it improves:_ it stays registered, so a Bedrock user is told the
1986
2098
  truth by name instead of guessing. The same honesty is applied to OpenAI,
1987
2099
  which is the costlier gap because it caches automatically.
1988
2100
 
1989
2101
  ### Changed
1990
2102
 
1991
- - **A parked map now tells the model it is parked.** *What changed:* while
2103
+ - **A parked map now tells the model it is parked.** _What changed:_ while
1992
2104
  anything is parked, the model receives a short status card naming the cursor
1993
2105
  and the engagement as **separate** things, the reason, and the way back as a
1994
- concrete call. *Why it was not there:* every honesty signal landed on the
2106
+ concrete call. _Why it was not there:_ every honesty signal landed on the
1995
2107
  record, which the model never reads — so re-engagement was reachable in
1996
2108
  principle and unreachable in practice, because nothing told the model that
1997
- re-picking a skill it appears to already be in means anything. *How it
1998
- improves:* a door the model can see. It also learns the distinction the
2109
+ re-picking a skill it appears to already be in means anything. _How it
2110
+ improves:_ a door the model can see. It also learns the distinction the
1999
2111
  kernel is built on instead of inferring a contradiction.
2000
- - **A parked map's tools actually leave the wire.** *What changed:* parking now
2112
+ - **A parked map's tools actually leave the wire.** _What changed:_ parking now
2001
2113
  holds the parked map's tool schemas off the request on its own authority.
2002
- *Why it was not there:* `.maps()` promised parking stops "the prompt fragment
2114
+ _Why it was not there:_ `.maps()` promised parking stops "the prompt fragment
2003
2115
  and tools", but on the default posture for flat graphs only the fragment
2004
2116
  stopped — the model was shown tools for a skill whose instructions had just
2005
- vanished. *How it improves:* the promise is true on every posture. This is
2117
+ vanished. _How it improves:_ the promise is true on every posture. This is
2006
2118
  **not** a change to `scopeTools` and does not touch the 10.0.0 ledger: those
2007
2119
  dials answer different questions.
2008
2120
  - **A pick of the skill you are already on is now a legal re-engagement.**
2009
- *What changed:* a `read_skill` pick is routed by intent — of a parked map's
2010
- member it re-engages the map and does not move the cursor. *Why it was not
2011
- there:* the reachability gate refuses a pick of the node the cursor occupies,
2121
+ _What changed:_ a `read_skill` pick is routed by intent — of a parked map's
2122
+ member it re-engages the map and does not move the cursor. _Why it was not
2123
+ there:_ the reachability gate refuses a pick of the node the cursor occupies,
2012
2124
  which is right for a move and wrong for this; since parking never moves the
2013
2125
  cursor, a parked map is parked exactly where the model wants to return. For a
2014
- single-member map that made parking permanent. *How it improves:* the
2126
+ single-member map that made parking permanent. _How it improves:_ the
2015
2127
  documented recovery door is real.
2016
- - **An engagement's founding cause is never rewritten.** *What changed:* the
2128
+ - **An engagement's founding cause is never rewritten.** _What changed:_ the
2017
2129
  record now keeps three separate facts — why the map is participating at all,
2018
2130
  why the cursor is on this member, and why this contribution is being served
2019
- right now. *Why it was not there:* one field answered all three, so
2131
+ right now. _Why it was not there:_ one field answered all three, so
2020
2132
  confirming a guess silently overwrote the guess, and a record founded on a
2021
- keyword at iteration 1 later read as system-backed since iteration 1. *How it
2022
- improves:* an incident review can still ask "was this founded on a guess?"
2133
+ keyword at iteration 1 later read as system-backed since iteration 1. _How it
2134
+ improves:_ an incident review can still ask "was this founded on a guess?"
2023
2135
  and get the true answer, however much has happened since.
2024
- - **A declared route no longer forges a user request.** *What changed:* when a
2136
+ - **A declared route no longer forges a user request.** _What changed:_ when a
2025
2137
  declared edge moves the cursor to a different member, that member's
2026
2138
  eligibility is worked out from **its own** evidence rather than inherited.
2027
- *Why it was not there:* the next member inherited the previous one's
2139
+ _Why it was not there:_ the next member inherited the previous one's
2028
2140
  standing, and an explicit request never decays — so one explicit pick at the
2029
2141
  top of a turn silently warranted every skill the graph walked to afterwards.
2030
- *How it improves:* a member entered weakly can park, exactly as if it had
2142
+ _How it improves:_ a member entered weakly can park, exactly as if it had
2031
2143
  been entered that way from the start. The founding cause still says, forever,
2032
2144
  that the engagement began explicitly on the skill that was actually asked for.
2033
- - **A new turn is judged on the new turn's evidence.** *What changed:* a cursor
2145
+ - **A new turn is judged on the new turn's evidence.** _What changed:_ a cursor
2034
2146
  carried into a new turn with nothing explaining it is recorded as `assumed`
2035
2147
  — nobody said why — which is the weakest rung and expires like any guess.
2036
- *Why it was not there:* an absent explanation was recorded as system-backed,
2148
+ _Why it was not there:_ an absent explanation was recorded as system-backed,
2037
2149
  the strongest and non-decaying category, so turn one's mistaken keyword match
2038
- became a permanent warrant on turn two and every turn after. *How it
2039
- improves:* cursor continuity and engagement continuity have independent
2150
+ became a permanent warrant on turn two and every turn after. _How it
2151
+ improves:_ cursor continuity and engagement continuity have independent
2040
2152
  lifetimes, and both are now pinned by tests — half the defect was that
2041
2153
  nothing pinned either.
2042
- - **The idle test checks all three of the conditions it documents.** *What
2043
- changed:* a map only accrues idle when its contribution was actually served,
2044
- none of its tools was called, and the turn went elsewhere. *Why it was not
2045
- there:* only the last was checked, while the refusal text asserted all three.
2046
- *How it improves:* nothing claims a fact it did not check. (Making it honest
2154
+ - **The idle test checks all three of the conditions it documents.** _What
2155
+ changed:_ a map only accrues idle when its contribution was actually served,
2156
+ none of its tools was called, and the turn went elsewhere. _Why it was not
2157
+ there:_ only the last was checked, while the refusal text asserted all three.
2158
+ _How it improves:_ nothing claims a fact it did not check. (Making it honest
2047
2159
  needed the kernel to carry its own record of what was served — reading it
2048
2160
  from the existing delta machinery was silently empty in the grouped chart
2049
2161
  shape, which would have disabled parking there entirely.)
2050
- - **A map that cannot explain its cursor moves is refused at mount.** *What
2051
- changed:* `.maps()` requires a map that reports why the cursor moved. *Why it
2052
- was not there:* without it, no explanation ever arrives, and a kernel whose
2053
- whole job is weighing evidence had none to weigh — silently. *How it
2054
- improves:* the failure is named at build time instead of at 3am.
2162
+ - **A map that cannot explain its cursor moves is refused at mount.** _What
2163
+ changed:_ `.maps()` requires a map that reports why the cursor moved. _Why it
2164
+ was not there:_ without it, no explanation ever arrives, and a kernel whose
2165
+ whole job is weighing evidence had none to weigh — silently. _How it
2166
+ improves:_ the failure is named at build time instead of at 3am.
2055
2167
 
2056
2168
  ### Added
2057
2169
 
@@ -2061,9 +2173,9 @@ the published bytes and is unchanged.
2061
2173
  shipped and was unreachable: the kernel's data model carried the field and
2062
2174
  no option ever set it. It suppresses the park, not the measurement — the
2063
2175
  record still shows a map riding every call unused.
2064
- - **Two documentation pages**: *Mounted maps* (the kernel's why, the evidence
2065
- ladder, the idle test, the three facts, turns, and the park card) and *The
2066
- cache meter* (how to read a report, and exactly which providers can feed it).
2176
+ - **Two documentation pages**: _Mounted maps_ (the kernel's why, the evidence
2177
+ ladder, the idle test, the three facts, turns, and the park card) and _The
2178
+ cache meter_ (how to read a report, and exactly which providers can feed it).
2067
2179
  - **A measurement, not a fix, for the prompt-cache cost of `read_skill`.** The
2068
2180
  tool's description is rebuilt from the cursor every iteration, and changing a
2069
2181
  tool definition invalidates a provider's entire prompt cache. A shipped test
@@ -2081,7 +2193,6 @@ through `isKnown(...)`, or render them with `describeClaim(...)`. This is a
2081
2193
  signature change in a minor release, and deliberately so: the old bare numbers
2082
2194
  were not a contract worth keeping, because the value was zero on every turn.
2083
2195
 
2084
-
2085
2196
  ## [9.58.0] - 2026-08-20
2086
2197
 
2087
2198
  **A map's cursor says where it stands. Nothing said whether it had earned
@@ -2177,7 +2288,7 @@ to do, and no longer had the evidence to do it.
2177
2288
  What it did next was invent. It took an entity name it remembered plus the
2178
2289
  shape of an id it had used earlier, assembled one that has never existed, and
2179
2290
  was refused — a wasted action out of a small budget. In one archived run the
2180
- final answer to the *person* named a host that appears in no tool result at
2291
+ final answer to the _person_ named a host that appears in no tool result at
2181
2292
  all.
2182
2293
 
2183
2294
  Nothing in the conversation said the evidence had gone. That is the release.
@@ -2200,7 +2311,7 @@ Nothing in the conversation said the evidence had gone. That is the release.
2200
2311
  already inside `keepRecentTurns` costs nothing at all. Nothing at or before
2201
2312
  the current request is pinnable, so a new user turn releases the whole
2202
2313
  previous loop. The floor is `1 request + keepLastToolResults pins +
2203
- keepRecentTurns turns`, whatever your tool count, iteration count or run
2314
+ keepRecentTurns turns`, whatever your tool count, iteration count or run
2204
2315
  length.
2205
2316
 
2206
2317
  And a pin that BLOCKS is worse than a pin that misses: when two consecutive
@@ -2211,9 +2322,9 @@ Nothing in the conversation said the evidence had gone. That is the release.
2211
2322
  `keepLastToolResults: false` (or `0`) reproduces 9.56.0 byte for byte.
2212
2323
 
2213
2324
  - **A drop now says whose results it took.** The authored notice gains one
2214
- sentence: *"Tool results are among them (whats_here, pan_view) — call the
2325
+ sentence: _"Tool results are among them (whats_here, pan_view) — call the
2215
2326
  tool again if you need its output; do not reconstruct ids or values from
2216
- memory."* The drop is now STATED rather than silent — whether that sentence
2327
+ memory."_ The drop is now STATED rather than silent — whether that sentence
2217
2328
  changes what a model does next is **not measured here**: the five archived
2218
2329
  runs have not been re-run with it on, so it ships as an honesty fix and not
2219
2330
  as a performance claim. Tool names are the only caller data that reaches it, and they
@@ -2239,13 +2350,13 @@ Nothing in the conversation said the evidence had gone. That is the release.
2239
2350
  > You are on action 25 of 30; 5 remain. Finish what you have rather than
2240
2351
  > start something new.
2241
2352
 
2242
- Measured, not decorative: given its remaining budget a model wrote *"I have 5
2243
- steps left, enough to finish this properly"* and landed the task, where
2353
+ Measured, not decorative: given its remaining budget a model wrote _"I have 5
2354
+ steps left, enough to finish this properly"_ and landed the task, where
2244
2355
  before it spiralled and produced no answer at all.
2245
2356
 
2246
2357
  The vocabulary is closed rather than a `(ctx) => string` because of
2247
2358
  **absence**. Given a function, an author writes `${ctx.maxIterations}` and
2248
- ships *"23 of undefined"*, or writes `?? 0` and ships a fabricated
2359
+ ships _"23 of undefined"_, or writes `?? 0` and ships a fabricated
2249
2360
  denominator that nothing — and no model — can tell from a real zero. With
2250
2361
  named slots the library owns absence and applies one rule: if any named fact
2251
2362
  is unavailable, the whole instruction is skipped, by name, as
@@ -2269,7 +2380,7 @@ Nothing in the conversation said the evidence had gone. That is the release.
2269
2380
  message that would become the head was already a user turn (the pinned
2270
2381
  request, or an older turn of a restored conversation) no notice was owed at
2271
2382
  all. Its 245–358 characters were nevertheless allowed to veto a legitimate
2272
- drop; and because the removable span is the longest *contiguous* run, the
2383
+ drop; and because the removable span is the longest _contiguous_ run, the
2273
2384
  same verdict came back at every boundary while the window grew without
2274
2385
  bound.
2275
2386
 
@@ -2474,11 +2585,11 @@ they do.
2474
2585
  `unknown`, so a surface that guessed at it would put words in a tool's
2475
2586
  mouth. One rule, and it is the whole rule:
2476
2587
 
2477
- | your payload | the line a person reads |
2478
- | --- | --- |
2479
- | `{ message: 'Hop 3 of 12' }` | `Hop 3 of 12` — your sentence, verbatim |
2480
- | `{ done: 3, total: 12 }` | `` `walk_graph` reported progress (3 so far)… `` |
2481
- | `'a bare string'` | the same generic line |
2588
+ | your payload | the line a person reads |
2589
+ | ---------------------------- | ------------------------------------------------ |
2590
+ | `{ message: 'Hop 3 of 12' }` | `Hop 3 of 12` — your sentence, verbatim |
2591
+ | `{ done: 3, total: 12 }` | `` `walk_graph` reported progress (3 so far)… `` |
2592
+ | `'a bare string'` | the same generic line |
2482
2593
 
2483
2594
  A top-level string field named **`message`** is shown verbatim, trimmed, and
2484
2595
  cut at **120 characters** with the cut stated (`… (+N more)`) — `message` is
@@ -2504,8 +2615,8 @@ they do.
2504
2615
  `tool_end` used to be able to clear a SIBLING call's status, leaving the
2505
2616
  bubble blank while a tool was still working.
2506
2617
 
2507
- - **Commentary narrates the middle**, so recordings replay it: *"The
2508
- `walk_graph` tool reported progress while it was still running."* The
2618
+ - **Commentary narrates the middle**, so recordings replay it: _"The
2619
+ `walk_graph` tool reported progress while it was still running."_ The
2509
2620
  teaching voice states the fact and never the payload — the same split the
2510
2621
  Lens teaching view keeps, in the same words.
2511
2622
 
@@ -2559,8 +2670,10 @@ tool's name on it.
2559
2670
  series: [{ t: '2026-08-19T10:00:00Z', entity: 'fc1/3', metric: 'avg_iops', value: 18450 }],
2560
2671
  grain: { interval: '30m', aggregation: 'avg', is_counter: false },
2561
2672
  provenance: { measured_at: '2026-08-19T10:20:00Z', source: 'InfluxDB SwitchPortStats' },
2562
- coverage: { checked: ['fabric A: all 48 ports'],
2563
- notChecked: [{ what: 'the peer fabric', why: 'collector scoped to one fabric' }] },
2673
+ coverage: {
2674
+ checked: ['fabric A: all 48 ports'],
2675
+ notChecked: [{ what: 'the peer fabric', why: 'collector scoped to one fabric' }],
2676
+ },
2564
2677
  render: { default: 'table', columns: ['entity', 'value'], sort: 'value desc' },
2565
2678
  });
2566
2679
  ```
@@ -2612,7 +2725,7 @@ tool's name on it.
2612
2725
  - Zero-cost when unused: recognition is strict (`af_semantics: true` AND the
2613
2726
  whole rule set), so every value any tool has ever returned keeps its bytes.
2614
2727
  Runnable example: `examples/features/66-semantic-envelope.ts`. Guide:
2615
- docs *Build → Semantic tool results*.
2728
+ docs _Build → Semantic tool results_.
2616
2729
 
2617
2730
  ## [9.52.0] - 2026-08-19
2618
2731
 
@@ -2692,7 +2805,7 @@ opaque function on a route edge.
2692
2805
 
2693
2806
  - **`guard:` on route edges — guards as data (the `when` predicate's declared
2694
2807
  twin).** `.route(a, b, { guard: { riskLevel: { in: ['high','critical'] },
2695
- score: { gte: 0.7 } } })` — conditions over the hop (`toolName`, `result`,
2808
+ score: { gte: 0.7 } } })` — conditions over the hop (`toolName`, `result`,
2696
2809
  `status`, `iteration`, `userMessage`, `currentSkillId`) and over the tool
2697
2810
  result's own top-level JSON fields, operators `eq/ne/gt/gte/lt/lte/in/notIn`
2698
2811
  (deliberately footprintjs's `WhereFilter` grammar, mirrored door-locally —
@@ -2702,6 +2815,7 @@ opaque function on a route edge.
2702
2815
  outcome, AND these conditions"). ONE compilation produces the predicate
2703
2816
  that routes, the serializable `SkillGuardData`, and the evidence evaluator
2704
2817
  — so the three can never describe different guards. What being data buys:
2818
+
2705
2819
  - **the check-up proves contradictions** — new ERROR
2706
2820
  `guard-unsatisfiable`: crossed bounds (`gt: 5, lt: 3`), `eq` a same-key
2707
2821
  `ne`/`in`/`notIn` excludes, a `status` outside the closed result-status
@@ -2722,7 +2836,7 @@ opaque function on a route edge.
2722
2836
  evaluation: `guard` on the taken hop (verdict `true`) and `guardsClosed`
2723
2837
  for refusals (verdict `false`, at most one per edge per iteration, on
2724
2838
  whatever move resulted — a stay says `score gte 0.7 — saw "0.2" →
2725
- failed`). Agents without guards keep byte-identical events.
2839
+ failed`). Agents without guards keep byte-identical events.
2726
2840
 
2727
2841
  - **SkillMap & SkillWalker are now the official names.** You declare the
2728
2842
  **SkillMap**; the agent is the **SkillWalker**; the recording carries both.
@@ -2805,7 +2919,7 @@ is the whole release.
2805
2919
  ### Notes for recording consumers (the lens, triage platforms)
2806
2920
 
2807
2921
  - Draw the declared topology from `skill.graph_declared` (`declaredSource:
2808
- 'recording'` can now mean COMPLETE); filter `from !== null` for
2922
+ 'recording'` can now mean COMPLETE); filter `from !== null` for
2809
2923
  node-to-node edges, exactly as with a built graph's `edges`.
2810
2924
  - Fill per-beat reachability from `cursorMove.reachable` first; the refusal's
2811
2925
  `allowed` and declared-edge folds remain as fallbacks for older eras.
@@ -2864,7 +2978,7 @@ is the whole release.
2864
2978
  `recording.json`, the manifest carries a note naming the fact and the line
2865
2979
  that supplies it, and `BugReportUnit.enveloped` says per conversation which
2866
2980
  shape it got. Nothing is stamped that was not known.
2867
- - **The evidence is never packed twice** — an envelope *or* a bare recording,
2981
+ - **The evidence is never packed twice** — an envelope _or_ a bare recording,
2868
2982
  never both. The zip is store-only, so a duplicated recording is duplicated
2869
2983
  bytes against the size ceiling the trim hints exist to keep a reporter under.
2870
2984
  - The GitHub issue body names the file that is really in the bundle, including
@@ -3415,10 +3529,9 @@ answer for. These are ours, built from that evidence. The credit is the field's.
3415
3529
  ### Fixed
3416
3530
 
3417
3531
  - Two unanchored `coverage` rules in `.gitignore` matched `src/core/agent/
3418
- coverage/`, so a new source directory would have been absent from every clone.
3532
+ coverage/`, so a new source directory would have been absent from every clone.
3419
3533
  Anchored to the vitest report directory, with a comment naming why.
3420
3534
 
3421
-
3422
3535
  ## [9.42.0] - 2026-08-15
3423
3536
 
3424
3537
  **Comparing strategies with statistics, and retention on the session port.**
@@ -3504,7 +3617,6 @@ answer for. These are ours, built from that evidence. The credit is the field's.
3504
3617
  from a counter, so they differ in their prefix and the fold never shows.
3505
3618
  Fixing it re-keys stored sessions. Next release.
3506
3619
 
3507
-
3508
3620
  ## [9.41.0] - 2026-08-15
3509
3621
 
3510
3622
  **Comparing strategies, and proving a store.** An audit asked whether this
@@ -3574,7 +3686,6 @@ key, and the proof obligations for the second port.
3574
3686
  - `MemoryDefinition` gains optional declared `strategy`, `retrieval` and
3575
3687
  `embedderId` — additive, and what makes a memory row worth grouping on.
3576
3688
 
3577
-
3578
3689
  ## [9.40.0] - 2026-08-15
3579
3690
 
3580
3691
  **Two identity bugs, one of which had been paying out zeros since v2.8.**
@@ -3625,7 +3736,6 @@ key, and the proof obligations for the second port.
3625
3736
  `provider` now flows through to `CostTick` as well (optional; absent stays
3626
3737
  absent rather than becoming `'unknown'`).
3627
3738
 
3628
-
3629
3739
  ## [9.39.0] - 2026-08-14
3630
3740
 
3631
3741
  **Three false doors closed, and two promises the recording did not keep.** An
@@ -3664,7 +3774,7 @@ forking. Mostly yes — but the first thing such an author reads was wrong.
3664
3774
  was reserved. Not re-exported from any barrel, not an `exports` subpath, not
3665
3775
  in the shipped-surface baseline — so nothing public could reach it and its
3666
3776
  removal is not breaking. The by-instance door (`agent.enable.observability({
3667
- strategy })`) is real, first-class, and now the only one. The cache twin
3777
+ strategy })`) is real, first-class, and now the only one. The cache twin
3668
3778
  legitimately has two doors; this one had one door and a sign pointing at a
3669
3779
  wall.
3670
3780
 
@@ -3688,7 +3798,6 @@ forking. Mostly yes — but the first thing such an author reads was wrong.
3688
3798
  `enable.cost()` receives zeros. This release makes `model` resolve there; the
3689
3799
  numbers are still zero. Its own ticket.
3690
3800
 
3691
-
3692
3801
  ## [9.38.0] - 2026-08-14
3693
3802
 
3694
3803
  **Two behaviour changes that are bug fixes — a call that used to run now
@@ -4056,12 +4165,13 @@ labels." Ours had no rule at all. Now it does, and it is enforced, not
4056
4165
  promised.
4057
4166
 
4058
4167
  ### What it is, and — because the name invites the wrong reading — what it
4168
+
4059
4169
  ### is provably not
4060
4170
 
4061
4171
  It is a **fabrication detector, not a correctness judge.** If a value in
4062
4172
  the answer never appeared in anything a tool returned this turn, the model
4063
4173
  typed it rather than read it, and that is all this checks. It cannot catch
4064
- a false claim built entirely from real values — *"fc1/3 is healthy"* when
4174
+ a false claim built entirely from real values — _"fc1/3 is healthy"_ when
4065
4175
  the data says the port is down uses two grounded tokens and sails through.
4066
4176
  It cannot catch a real value attached to the wrong thing, a fabricated
4067
4177
  quantity under the digit threshold, or a fabricated name spelled in
@@ -4116,7 +4226,7 @@ is not judged at all, and a turn the iteration or cost limit just cut
4116
4226
  short does not get to spend a revision it can't afford. It composes with,
4117
4227
  rather than collides with, `.reliability()`: reliability governs whether a
4118
4228
  call is retried before anything is committed; this governs an answer
4119
- *after* it has already been committed to the transcript.
4229
+ _after_ it has already been committed to the transcript.
4120
4230
 
4121
4231
  ### Measured, not assumed
4122
4232
 
@@ -4195,7 +4305,7 @@ never touched `footprintjs` itself but imports the file that now did.
4195
4305
  - `isDevMode` (a `footprintjs` import) → a bound `devWarn()` / `devMode()`
4196
4306
  reader that the host supplies; every existing warning reads verbatim, and
4197
4307
  the existing `enableDevMode()` tests pass unchanged — the proof that
4198
- nothing about *what gets warned* moved, only *how it's asked*.
4308
+ nothing about _what gets warned_ moved, only _how it's asked_.
4199
4309
  - `ToolResultStatus` → pulled out to a zero-import leaf and re-exported from
4200
4310
  its old home, so the envelope grammar is unchanged for every existing
4201
4311
  caller. The fence caught four more inline crossings of this type that
@@ -4358,7 +4468,7 @@ was in the bundle — an empty bundle read as "nobody was turned away" when
4358
4468
  it only meant "nobody ran." `standingAgent({ onIngressDecision })` now
4359
4469
  hands your sink one `IngressRecord` per request, filed at the terminal the
4360
4470
  reply actually reached. The honest contract is stated in the type itself:
4361
- `'served'` means **delivered**, not *admitted* — a request the door let
4471
+ `'served'` means **delivered**, not _admitted_ — a request the door let
4362
4472
  through whose run, store or provider then broke files as `'failed'`, and
4363
4473
  the record carries the admission verdict (`allow` / `queue` / `refuse`)
4364
4474
  either way. It is a stream you chain into your own sink, not a join onto
@@ -4490,7 +4600,7 @@ read/write." Both, plus `decayPolicy`, are now carried under prefixed
4490
4600
  metadata keys and restored verbatim on read. A caller's own value under one
4491
4601
  of the three keys this adapter generates (`source`, `resourceName`,
4492
4602
  `distance`) is refused by name rather than silently shadowed — recognized by
4493
- *identity*, not shape, so a caller's own `distance: 12` cannot be mistaken
4603
+ _identity_, not shape, so a caller's own `distance: 12` cannot be mistaken
4494
4604
  for this adapter's. An oversized carried field is refused rather than
4495
4605
  truncated: provenance that came back shortened would be provenance nobody
4496
4606
  could tell was shortened.
@@ -4680,7 +4790,7 @@ effects. Every line follows the same rules as the rest of the layer:
4680
4790
  retry happened — no event attests one, so the words don't either.
4681
4791
  - **Sizes humanized, and the two units told apart.** `humanizeBytes` /
4682
4792
  `humanizeChars` — `41.0 KB` where the ceiling counts bytes, `240,000
4683
- characters` where it counts characters (`tools.result_refused` counts
4793
+ characters` where it counts characters (`tools.result_refused` counts
4684
4794
  characters, because that's what the limit does).
4685
4795
  - **Refs and digests stay out of prose.** They identify a row for the details
4686
4796
  panel; a reader doesn't read them. The repeated-call nudge's fingerprints
@@ -4734,8 +4844,8 @@ import { googleIdentity } from 'agentfootprint/security';
4734
4844
  now declares the same `ranksBy: 'server-text'` for the identical reason, so
4735
4845
  the two server-ranked stores no longer disagree on how they say what they
4736
4846
  are.)
4737
- - **`googleIdentity`** — a narrow `CredentialProvider`: it vends *Google*
4738
- access tokens for *Google* APIs from whatever credential the environment
4847
+ - **`googleIdentity`** — a narrow `CredentialProvider`: it vends _Google_
4848
+ access tokens for _Google_ APIs from whatever credential the environment
4739
4849
  already has (ADC, workload identity, an impersonated service account).
4740
4850
  `mode: 'user'` is refused by name rather than quietly served a machine
4741
4851
  token, since Google's user-token equivalent has no Node surface yet.
@@ -4810,7 +4920,9 @@ provider scopes a vault on:
4810
4920
 
4811
4921
  ```ts
4812
4922
  await standingAgent({
4813
- agent, sessions, host: nodeHost({ port: 8080 }),
4923
+ agent,
4924
+ sessions,
4925
+ host: nodeHost({ port: 8080 }),
4814
4926
  identity: {
4815
4927
  verify: jwksIdentity({
4816
4928
  jwksUrl: 'https://idp.example.com/.well-known/jwks.json',
@@ -4823,7 +4935,7 @@ await standingAgent({
4823
4935
 
4824
4936
  **Configured is closed-by-default.** A request with no `Authorization`
4825
4937
  header is refused (401) unless `allowAnonymous: true` is set, and a request
4826
- that *names* a `userId` without proving it is refused either way — a door
4938
+ that _names_ a `userId` without proving it is refused either way — a door
4827
4939
  that verifies a token when offered and waves the request through when it is
4828
4940
  not is a door anybody opens by sending less. `jwksIdentity` is the one
4829
4941
  adapter this release ships (`jose`, loaded lazily, pinned against a real
@@ -4842,7 +4954,9 @@ Nothing else reads them; they do not enter the run's own identity tuple.
4842
4954
 
4843
4955
  ```ts
4844
4956
  await standingAgent({
4845
- agent, sessions, host,
4957
+ agent,
4958
+ sessions,
4959
+ host,
4846
4960
  identity: { verify },
4847
4961
  admission: turnsPerHour({ limit: 60 }),
4848
4962
  });
@@ -5027,8 +5141,12 @@ wire, scoped to the session that asks.**
5027
5141
  ### Added — two wire operations on the existing invoke path
5028
5142
 
5029
5143
  ```ts
5030
- { op: 'artifact-head', ref } // → meta
5031
- { op: 'artifact-get', ref } // → meta + data
5144
+ {
5145
+ op: 'artifact-head', ref;
5146
+ } // → meta
5147
+ {
5148
+ op: 'artifact-get', ref;
5149
+ } // → meta + data
5032
5150
  ```
5033
5151
 
5034
5152
  Resolved under the requesting session's identity-composed scope — exactly
@@ -5079,7 +5197,9 @@ lines.**
5079
5197
  defineTool({
5080
5198
  name: 'summarize',
5081
5199
  wants: { dataset: 'dataset/rows' },
5082
- execute: async (args, ctx) => { /* args.dataset is the RESOLVED DATA */ },
5200
+ execute: async (args, ctx) => {
5201
+ /* args.dataset is the RESOLVED DATA */
5202
+ },
5083
5203
  });
5084
5204
  ```
5085
5205
 
@@ -5267,7 +5387,7 @@ on `llm_start`.
5267
5387
  Precedence is stated and enforced, most specific wins: **escalation
5268
5388
  brain > per-skill brain > `.configure()`'s run model > the build
5269
5389
  default.** A brain naming only a model inherits the agent's own provider;
5270
- a brain naming a *foreign* provider without a model is refused at
5390
+ a brain naming a _foreign_ provider without a model is refused at
5271
5391
  `Agent.build()` — the run's configured model belongs to another vendor's
5272
5392
  namespace and would fail mid-turn, on exactly the iteration the cursor
5273
5393
  enters the skill.
@@ -5304,12 +5424,12 @@ returns today — stay byte-identical; the envelope is recognized only by
5304
5424
  its own strict shape.
5305
5425
 
5306
5426
  - **`propose-transition`** — `{ kind: 'propose-transition', targetSkillId,
5307
- reason }`. The typed replacement for a string routing marker: the
5308
- *graph* decides. A same-batch declared edge still wins; an unreachable
5427
+ reason }`. The typed replacement for a string routing marker: the
5428
+ _graph_ decides. A same-batch declared edge still wins; an unreachable
5309
5429
  target is refused out loud, not silently dropped.
5310
5430
  - **`require-instruction`** — `{ kind: 'require-instruction',
5311
- instructionId, deliveryLease: 'next-call' | 'until-skill-exit' }`.
5312
- Pushes a *registered* instruction into the coming call(s) —
5431
+ instructionId, deliveryLease: 'next-call' | 'until-skill-exit' }`.
5432
+ Pushes a _registered_ instruction into the coming call(s) —
5313
5433
  `read_skill` stays the pull door for optional knowledge; this is the
5314
5434
  push door for mandatory procedure, and it only pushes what was
5315
5435
  registered at build. An unknown id is refused, never improvised.
@@ -5493,7 +5613,7 @@ unmatched, never as an uncontested winner.
5493
5613
  ### Zero-cost when unused
5494
5614
 
5495
5615
  Graphs that use none of the new options are byte-identical in behavior
5496
- *and* events to 9.16.0 — pinned by regression tests. 78+ new tests cover
5616
+ _and_ events to 9.16.0 — pinned by regression tests. 78+ new tests cover
5497
5617
  the cascade, the scorers, the strictness postures, and continuity.
5498
5618
 
5499
5619
  ### Deferred
@@ -5528,6 +5648,7 @@ ordering bug with no error, no log line, just a silently different cursor.
5528
5648
 
5529
5649
  On the record, so a trace answers "why didn't the second call route?"
5530
5650
  instead of leaving the reader to guess.
5651
+
5531
5652
  - Single-tool iterations are byte-identical to 9.15.0 — this only changes
5532
5653
  behavior when a batch actually contains more than one result.
5533
5654
  - New `AgentState.toolResults` / `InjectionContext.toolResults`: the full
@@ -5542,7 +5663,7 @@ ordering bug with no error, no log line, just a silently different cursor.
5542
5663
  Wiring a `.skillGraph(...)` onto a classic agent meant the graph would still
5543
5664
  route and the trace would still show an activation — but the model never saw
5544
5665
  the newly-active skill's prompt or tools, because the slot it would have
5545
- changed was already frozen. The configuration *looked* like it worked and
5666
+ changed was already frozen. The configuration _looked_ like it worked and
5546
5667
  didn't.
5547
5668
 
5548
5669
  `Agent.build()` now refuses this combination outright, naming both the
@@ -5596,7 +5717,7 @@ const graph = skillGraph({
5596
5717
 
5597
5718
  What previously required `autoActivate: 'currentSkill'` typed on every single
5598
5719
  skill is now one graph-level line. A skill's own explicit `autoActivate` always
5599
- wins — the graph sets a default, never an override. Only *wired* skills (named
5720
+ wins — the graph sets a default, never an override. Only _wired_ skills (named
5600
5721
  by an entry or a route) are stamped: an unwired skill's tools would otherwise
5601
5722
  never appear at all. With the dial absent or `false`, compiled skills are
5602
5723
  byte-identical to 9.14.0 (pinned by test). On a `tree()` graph the flat-arm
@@ -5677,10 +5798,10 @@ const memory = defineMemory({
5677
5798
  type: MEMORY_TYPES.EPISODIC,
5678
5799
  strategy: {
5679
5800
  kind: MEMORY_STRATEGIES.SUMMARIZE,
5680
- recent: 6, // the 6 newest entries stay verbatim
5681
- size: 20, // how much history to load per turn
5682
- llm: anthropic(), // its OWN instance, not the agent's
5683
- model: 'claude-haiku-4-5', // named explicitly — no fallback
5801
+ recent: 6, // the 6 newest entries stay verbatim
5802
+ size: 20, // how much history to load per turn
5803
+ llm: anthropic(), // its OWN instance, not the agent's
5804
+ model: 'claude-haiku-4-5', // named explicitly — no fallback
5684
5805
  },
5685
5806
  store,
5686
5807
  });
@@ -5710,8 +5831,8 @@ quietly bills your MAIN model for compression, and a different vendor is sent a
5710
5831
  model id it has never heard of, mid-conversation, on a paid run.
5711
5832
 
5712
5833
  `Agent.memory()` now also refuses a summarizer that is the agent's own provider
5713
- **instance** at the agent's own model (the narrow 8.14.0 rule; a *second
5714
- instance* of the same vendor at the same model is allowed and sometimes right).
5834
+ **instance** at the agent's own model (the narrow 8.14.0 rule; a _second
5835
+ instance_ of the same vendor at the same model is allowed and sometimes right).
5715
5836
  `defineMemory` cannot make that check — it has never heard of an agent — so a
5716
5837
  `MemoryDefinition` now declares `billing: { provider, model }` and the builder
5717
5838
  reads it, the same field and shape `WindowStrategy.billing` already used. One
@@ -5824,7 +5945,7 @@ why, with a date.
5824
5945
  import { gemini } from 'agentfootprint/providers';
5825
5946
 
5826
5947
  const vertex = gemini({ project: 'my-project', location: 'us-central1' }); // ADC
5827
- const studio = gemini({ apiKey: process.env.GEMINI_API_KEY! }); // one key
5948
+ const studio = gemini({ apiKey: process.env.GEMINI_API_KEY! }); // one key
5828
5949
  ```
5829
5950
 
5830
5951
  Two doors, one adapter, and neither is guessed: a project selects Vertex, a key
@@ -5877,8 +5998,8 @@ Three decisions worth reading before you rely on them:
5877
5998
  that arrives anyway is kept out of the visible answer on both paths.
5878
5999
  - **A stream that reports no usage reports ZERO, never an estimate.**
5879
6000
  `models.countTokens` is on the namespace, is not called, and is named in the pin
5880
- as not called: it answers what a request *tokenises to*, not what the call was
5881
- *billed for*. Same law as `openai()` and `ollama()`. (Usage is read off the
6001
+ as not called: it answers what a request _tokenises to_, not what the call was
6002
+ _billed for_. Same law as `openai()` and `ollama()`. (Usage is read off the
5882
6003
  closing chunk BEFORE any content guard — the bug that made streamed turns bill
5883
6004
  as zero on two earlier adapters.)
5884
6005
 
@@ -5950,8 +6071,8 @@ new and load-bearing.
5950
6071
 
5951
6072
  ### Added — Gemini's over-long-request sentence joins the typed error
5952
6073
 
5953
- `ContextWindowExceededError` (9.6.0) now translates *"The input token count
5954
- (1200293) exceeds the maximum number of tokens allowed (1048576)."* — a word order
6074
+ `ContextWindowExceededError` (9.6.0) now translates _"The input token count
6075
+ (1200293) exceeds the maximum number of tokens allowed (1048576)."_ — a word order
5955
6076
  the existing patterns did not match — and reads both numbers out of it, including
5956
6077
  the case where Google ships the first parenthesis empty. Detection stays
5957
6078
  conservative: "INPUT token count" is what keeps it off a `max_tokens` validation
@@ -6000,8 +6121,8 @@ concurrency-and-sessions section, and a status row per boundary.
6000
6121
  - A `gemini()` error never prints the API key it was constructed with. The
6001
6122
  redaction is narrow by design — the exact string you passed, removed from the
6002
6123
  message, the stack and the wrapped cause — and is not a heuristic scrubber:
6003
- a thrown provider error reaches the model as a tool result *and* the commit log
6004
- *and* every observability sink, so one interpolation would leak to all of them.
6124
+ a thrown provider error reaches the model as a tool result _and_ the commit log
6125
+ _and_ every observability sink, so one interpolation would leak to all of them.
6005
6126
 
6006
6127
  ## [9.12.0] - 2026-08-12
6007
6128
 
@@ -6022,7 +6143,7 @@ provider signed:
6022
6143
  const credentials = agentCoreIdentity({
6023
6144
  region: 'us-west-2',
6024
6145
  workloadName: 'workflow_assistant_agent',
6025
- requireUserToken: true, // optional — refuse a delegated call with no proof
6146
+ requireUserToken: true, // optional — refuse a delegated call with no proof
6026
6147
  });
6027
6148
 
6028
6149
  // inside a tool
@@ -6037,7 +6158,7 @@ await ctx.credentials.getCredential({ service: 'google', mode: 'user', userToken
6037
6158
  - **Nothing downstream changed.** `GetWorkloadAccessTokenForJWT` answers with the
6038
6159
  same `workloadAccessToken` the by-userId exchange does, so it feeds the same
6039
6160
  `GetResourceOauth2Token` call, the same `Credential`, the same `toHeaders()`.
6040
- The vault entry at the end belongs to the *person* rather than to the agent,
6161
+ The vault entry at the end belongs to the _person_ rather than to the agent,
6041
6162
  which is what makes revoking their access actually revoke it.
6042
6163
  - **Verified against the real SDK before it shipped**, names and shapes both:
6043
6164
  `{ workloadName, userToken }` in, `{ workloadAccessToken }` out. It joins the
@@ -6121,7 +6242,7 @@ refuses a call it used to allow, or emits a field it did not before.
6121
6242
  ### Added — `maxToolResultChars`: a ceiling on ONE tool result
6122
6243
 
6123
6244
  ```ts
6124
- Agent.create({ provider, model, maxToolResultChars: 20_000 })
6245
+ Agent.create({ provider, model, maxToolResultChars: 20_000 });
6125
6246
  ```
6126
6247
 
6127
6248
  Over the cap, the result is **replaced** by a marker that tells the model what
@@ -6161,8 +6282,8 @@ happened and what to do about it:
6161
6282
  ### Added — WHO the run was for, on every event
6162
6283
 
6163
6284
  `EventMeta.principal` and `EventMeta.tenant` (9.11.0) join `sessionId` (9.4.0) on
6164
- every event's meta. The stream has always said *what* happened and *when*; this
6165
- is the *who*, and the three together are an audit record rather than a debug log.
6285
+ every event's meta. The stream has always said _what_ happened and _when_; this
6286
+ is the _who_, and the three together are an audit record rather than a debug log.
6166
6287
 
6167
6288
  ```ts
6168
6289
  await agent.run(message, {
@@ -6183,7 +6304,7 @@ await agent.run(message, {
6183
6304
  and `sessionId` beside it is the fact the transport delivered.
6184
6305
  - **Which sinks carry it, checked rather than assumed.** `fileObservability`,
6185
6306
  `cloudwatchObservability`, `agentcoreObservability` and `auditExport` serialize
6186
- the whole envelope and inherit it for free — in `auditExport`'s case *inside*
6307
+ the whole envelope and inherit it for free — in `auditExport`'s case _inside_
6187
6308
  the hash chain, so editing who breaks the same verification as editing what.
6188
6309
  `otelObservability` maps signals onto spans rather than serializing, so the
6189
6310
  actor is PLACED there: `agentfootprint.principal.id` /
@@ -6209,7 +6330,7 @@ const policy = PermissionPolicy.fromRoles(roles, 'support', {
6209
6330
  ```
6210
6331
 
6211
6332
  - **`Tool.capabilities`** (`'memory_read' | 'memory_write' | 'external_net' |
6212
- 'user_data'`) is a declaration, never an inference. A tool's reach is not
6333
+ 'user_data'`) is a declaration, never an inference. A tool's reach is not
6213
6334
  knowable from its name, schema or description, and guessing would rest a policy
6214
6335
  decision on a heuristic.
6215
6336
  - **`PermissionChecker.governs`** is an optional, feature-detected member —
@@ -6244,8 +6365,8 @@ PermissionPolicy.fromRoles(roles, 'support', {
6244
6365
  - The refusal lands **before `execute`**, so a `surfaceMode: 'tool-only'` skill's
6245
6366
  body is never even computed.
6246
6367
  - **Hidden means unnamed.** The graph offer lists unreachable skills as "not
6247
- reachable from here" because a cursor can move; a hidden skill is about *who is
6248
- asking*, and naming it would tell one role about another role's capabilities.
6368
+ reachable from here" because a cursor can move; a hidden skill is about _who is
6369
+ asking_, and naming it would tell one role about another role's capabilities.
6249
6370
  - **The enum stays the full catalog.** `toolArgValidation` runs before the gate,
6250
6371
  so narrowing it would turn a policy refusal into a generic schema error and the
6251
6372
  model would never read the policy's own message — the reasoning 8.5.0 recorded
@@ -6295,7 +6416,7 @@ await standingAgent({
6295
6416
  agentFactory: () => Agent.create({ provider, model }).system('…').build(),
6296
6417
  sessions: sqliteSessions({ file: './sessions.db' }),
6297
6418
  host: nodeHost({ port: 8080 }),
6298
- maxActiveSessions: 200, // default 100
6419
+ maxActiveSessions: 200, // default 100
6299
6420
  });
6300
6421
  ```
6301
6422
 
@@ -6393,7 +6514,6 @@ nodeHost({ sessionCookie: 'af_session' }); // …or no client code at all
6393
6514
  - New example: `examples/deploy/multi-user.ts` — two people served at once,
6394
6515
  proving the overlap in wall clock and that neither saw the other's memory.
6395
6516
 
6396
-
6397
6517
  ## [9.9.0] - 2026-08-12
6398
6518
 
6399
6519
  **A bug report IS the evidence.**
@@ -6411,7 +6531,7 @@ with the prose attached.
6411
6531
  ```ts
6412
6532
  import { describeBugReport, exportBugReport } from 'agentfootprint/observe';
6413
6533
 
6414
- const offer = describeBugReport(recording); // measure — nothing has left yet
6534
+ const offer = describeBugReport(recording); // measure — nothing has left yet
6415
6535
  // …show offer.units to the human; they tick some…
6416
6536
  const report = exportBugReport(recording, {
6417
6537
  include: ['conv-1', 'file-conversation', 'file-environment'],
@@ -6420,7 +6540,7 @@ const report = exportBugReport(recording, {
6420
6540
  expected: 'the updated price',
6421
6541
  actual: 'the price from before the update',
6422
6542
  });
6423
- fs.writeFileSync(report.filename, report.zip); // a real .zip
6543
+ fs.writeFileSync(report.filename, report.zip); // a real .zip
6424
6544
  ```
6425
6545
 
6426
6546
  - **The manifest is SELECTABLE UNITS, not a blob.** Each conversation is a unit
@@ -6438,7 +6558,7 @@ fs.writeFileSync(report.filename, report.zip); // a real .zip
6438
6558
  - **What was left out is STATED.** `manifest.excluded` counts the conversations,
6439
6559
  files, events and turns that were withheld, names their unit ids, and the
6440
6560
  issue body repeats it. A maintainer reading turn 4 must be able to tell that
6441
- turns 1–3 were *withheld*, not *lost*.
6561
+ turns 1–3 were _withheld_, not _lost_.
6442
6562
  - **Redacted keys, BY NAME.** The recording arrives already redacted (footprintjs
6443
6563
  scrubs at commit time), so nothing here scrubs anything — it would be too late
6444
6564
  to matter and a second policy could only disagree with the first. Instead the
@@ -6474,9 +6594,9 @@ plain `fetch` with no SDK:
6474
6594
 
6475
6595
  ```ts
6476
6596
  const reporter = githubBugReporter({
6477
- issueRepo: 'footprintjs/agentfootprint', // public — the conversation
6478
- evidenceRepo: 'acme/af-bug-evidence', // private — the run
6479
- }); // token: GITHUB_TOKEN, or `token`
6597
+ issueRepo: 'footprintjs/agentfootprint', // public — the conversation
6598
+ evidenceRepo: 'acme/af-bug-evidence', // private — the run
6599
+ }); // token: GITHUB_TOKEN, or `token`
6480
6600
  const { issueUrl, zipUrl } = await reporter.file(report);
6481
6601
  ```
6482
6602
 
@@ -6517,13 +6637,13 @@ const { issueUrl, zipUrl } = await reporter.file(report);
6517
6637
  calls — browser-safe and server-safe, no client secret, no dependency:
6518
6638
 
6519
6639
  ```ts
6520
- const signIn = await githubDeviceSignIn({ clientId }); // returns at once
6640
+ const signIn = await githubDeviceSignIn({ clientId }); // returns at once
6521
6641
  show(`Open ${signIn.verificationUri} and enter ${signIn.userCode}`);
6522
- const { token, login } = await signIn.completed; // resolves on approve
6642
+ const { token, login } = await signIn.completed; // resolves on approve
6523
6643
  ```
6524
6644
 
6525
- A server PAT files every report as the *application*; this files it as the
6526
- *reporter*, which is what a field tester filing upstream needs. It honours
6645
+ A server PAT files every report as the _application_; this files it as the
6646
+ _reporter_, which is what a field tester filing upstream needs. It honours
6527
6647
  `slow_down`, respects the code's expiry, takes an `AbortSignal`, and fetches
6528
6648
  `/user` for attribution (a `/user` that refuses is not fatal — the token still
6529
6649
  works, the login is simply absent). The token it returns is handed to
@@ -6593,7 +6713,9 @@ agent.enable.observability({
6593
6713
  maxBytes: 64 * 1024 * 1024,
6594
6714
  }),
6595
6715
  });
6596
- process.on('SIGTERM', async () => { await agent.shutdown(); }); // flushes
6716
+ process.on('SIGTERM', async () => {
6717
+ await agent.shutdown();
6718
+ }); // flushes
6597
6719
  ```
6598
6720
 
6599
6721
  Four things it is deliberate about:
@@ -6637,8 +6759,8 @@ import { vaultCredentials } from 'agentfootprint/security';
6637
6759
 
6638
6760
  const credentials = vaultCredentials({
6639
6761
  address: 'https://vault.internal:8200',
6640
- paths: { github: 'ci/github' }, // …or resolve(service), or neither
6641
- }); // token: `token`, else VAULT_TOKEN
6762
+ paths: { github: 'ci/github' }, // …or resolve(service), or neither
6763
+ }); // token: `token`, else VAULT_TOKEN
6642
6764
  ```
6643
6765
 
6644
6766
  The tool code does not change from the `staticTokens` version — same port, same
@@ -6672,7 +6794,7 @@ service)` as the seam for a shop whose field names are its own.
6672
6794
  the service, the mount path and the HTTP status — and nothing from the
6673
6795
  response body, nothing from the token, not even the field names the secret
6674
6796
  carries. This is the 8.6.0 law applied one adapter down: a thrown message
6675
- reaches the model as a tool result *and* rides
6797
+ reaches the model as a tool result _and_ rides
6676
6798
  `agentfootprint.credential.failed` to every observer. It is pinned by a
6677
6799
  grep-shaped test that walks every failure path — unknown service, 401, 403,
6678
6800
  404, 503, a non-JSON reply, a KV v1 response, an unrecognised field set, and a
@@ -6685,21 +6807,21 @@ service)` as the seam for a shop whose field names are its own.
6685
6807
  - **`infrastructure/on-premises.mdx`** — the provider page beside AWS. The
6686
6808
  local-first ladder (mock → local model → your gateway → a paid API) as the
6687
6809
  opening frame, then a service-by-service map: LLM (`ollama`, `openai({
6688
- baseURL })` for vLLM / llama.cpp / a corporate gateway, or the two-method
6810
+ baseURL })` for vLLM / llama.cpp / a corporate gateway, or the two-method
6689
6811
  port), stores (`sqliteVectorStore`, `pgVectorStore`, `staticVectorStore`,
6690
6812
  `RedisStore`), embedders (`localEmbedder`, `staticEmbedder`), hosting
6691
6813
  (`httpHost` / `nodeHost` + `sqliteSessions` / `memorySessions`), code
6692
- execution (`localCodeRunner` — *isolation, not a sandbox*), telemetry
6814
+ execution (`localCodeRunner` — _isolation, not a sandbox_), telemetry
6693
6815
  (`otelObservability` to any OTLP collector, `fileObservability` when there is
6694
6816
  none, `auditExport` for evidence), credentials (`staticTokens`,
6695
6817
  `vaultCredentials`, and the port for everything else) and tools (`mcpClient`
6696
6818
  over stdio or Streamable HTTP). It ends with **what is NOT here** — no
6697
6819
  Kubernetes-native anything, no second secret-manager adapter, no metrics
6698
6820
  exporter, no retention policy, no air-gapped model distribution.
6699
- - **The status vocabulary gained one honest rung.** *Contract-shaped and tested;
6700
- awaiting field use* is what `fileObservability` and `vaultCredentials` carry:
6821
+ - **The status vocabulary gained one honest rung.** _Contract-shaped and tested;
6822
+ awaiting field use_ is what `fileObservability` and `vaultCredentials` carry:
6701
6823
  their ports and refusals are pinned by tests, and neither has met a real
6702
- production disk or vault. *Verified in a production field deployment* now
6824
+ production disk or vault. _Verified in a production field deployment_ now
6703
6825
  appears in exactly one place, describing a deployment **shape** — a standing
6704
6826
  agent over `httpHost` + `sqliteSessions` against an OpenAI-compatible gateway,
6705
6827
  the shape several past releases exist because of — and never an adapter. The
@@ -6730,14 +6852,14 @@ handed that said "this is over".
6730
6852
 
6731
6853
  Three optional fields, sourced from what the engine already stamps:
6732
6854
 
6733
- | field | source | absent when |
6734
- |---|---|---|
6735
- | `ctx.runId` | the run in flight | there is no run — a call served over `mcpServe` is one call, not a turn |
6736
- | `ctx.sessionId` | `run({ sessionId })` ← `HostRequest.sessionId` | the run is not session-bound |
6737
- | `ctx.identity` | the identity the CALLER passed | the caller passed none |
6855
+ | field | source | absent when |
6856
+ | --------------- | ---------------------------------------------- | ----------------------------------------------------------------------- |
6857
+ | `ctx.runId` | the run in flight | there is no run — a call served over `mcpServe` is one call, not a turn |
6858
+ | `ctx.sessionId` | `run({ sessionId })` ← `HostRequest.sessionId` | the run is not session-bound |
6859
+ | `ctx.identity` | the identity the CALLER passed | the caller passed none |
6738
6860
 
6739
6861
  Every one is **absent rather than invented**, which is the 9.4.0 rule applied one
6740
- layer down. `ctx.identity` is deliberately *not* the run's internal
6862
+ layer down. `ctx.identity` is deliberately _not_ the run's internal
6741
6863
  `runIdentity`: that is always populated, defaulting to
6742
6864
  `{ conversationId: '<runId>' }`, and handing a tool a synthesized conversation as
6743
6865
  "the identity" would let it isolate a live sandbox on a fiction.
@@ -6767,12 +6889,12 @@ tool that wants a run-scoped session needs to know it is at a door with no runs
6767
6889
  BEFORE it opens one. Asking for a scope a door cannot honour throws, naming the
6768
6890
  door.
6769
6891
 
6770
- | scope | fires |
6771
- |---|---|
6772
- | `'call'` | when `tool.execute` settles — resolve **or** throw. Every door, including `mcpServe`. |
6773
- | `'run'` | at a run terminal that is **not a pause**. |
6774
- | `'session'` | `agent.closeToolSessions({ sessionId })`. |
6775
- | `'shutdown'` | `agent.shutdown()`. |
6892
+ | scope | fires |
6893
+ | ------------ | ------------------------------------------------------------------------------------- |
6894
+ | `'call'` | when `tool.execute` settles — resolve **or** throw. Every door, including `mcpServe`. |
6895
+ | `'run'` | at a run terminal that is **not a pause**. |
6896
+ | `'session'` | `agent.closeToolSessions({ sessionId })`. |
6897
+ | `'shutdown'` | `agent.shutdown()`. |
6776
6898
 
6777
6899
  Seven laws, each pinned: at most once ever · idempotent by `(tool, scope, key)`
6778
6900
  with the FIRST registration winning (it holds the live handle) · reverse
@@ -6783,7 +6905,7 @@ tolerates "already gone" · nothing live is ever persisted into a checkpoint.
6783
6905
  **A pause is not a terminal.** `'run'` teardown deliberately does not hang off
6784
6906
  `finally`, which also runs on both pause shapes. A `checkIn` on a code
6785
6907
  interpreter stops the run so a person can approve the code; tearing the sandbox
6786
- down there destroys the exact state the resume needs, and it fails *quietly* — as
6908
+ down there destroys the exact state the resume needs, and it fails _quietly_ — as
6787
6909
  a resumed run that "just re-ran everything". An error IS a terminal.
6788
6910
 
6789
6911
  ### Added — `agent.closeToolSessions({ sessionId })`
@@ -6872,7 +6994,7 @@ the rows, and what comes back is the finding.
6872
6994
  `client.send(new Command(...))`, pinned in `test/adapters/aws/awsCommandPin.ts`
6873
6995
  and **verified against a real install of the SDK before shipping** — including
6874
6996
  two shapes a design could only have guessed at: `Invoke` answers with an EVENT
6875
- STREAM, and seven of its nine union members are modelled *exceptions* (folded
6997
+ STREAM, and seven of its nine union members are modelled _exceptions_ (folded
6876
6998
  in as empty output, an `AccessDenied` would have reported a clean run that
6877
6999
  "printed nothing"), and `Stop` takes the session id, not a URI.
6878
7000
  - **`codeRunnerTool({ runner, scope })`** (main barrel) — holds one session per
@@ -6937,8 +7059,8 @@ and now shares the paging with the general one.
6937
7059
  **Behaviour change, named loudly: multi-turn memory starts actually
6938
7060
  retaining.** A six-turn conversation stores twelve message entries where it
6939
7061
  stored two, and the window injects up to `size` of them instead of the last
6940
- exchange — so prompts get longer and stores get bigger *because the agent is
6941
- now remembering what it was asked to remember*. Turn it down deliberately
7062
+ exchange — so prompts get longer and stores get bigger _because the agent is
7063
+ now remembering what it was asked to remember_. Turn it down deliberately
6942
7064
  (`size`, `DECAY`, `.compaction()`) rather than by accident.
6943
7065
 
6944
7066
  Cost: one paged `list()` per store per run, and only when a memory actually
@@ -7187,7 +7309,7 @@ compaction that runs.
7187
7309
  **AWS adapters tell the truth.**
7188
7310
 
7189
7311
  A production field report tested 9.3.0 against a real account and found two
7190
- adapters *dispatching calls that were never made against AWS* — one sending a
7312
+ adapters _dispatching calls that were never made against AWS_ — one sending a
7191
7313
  command that does not exist, one calling a method that a command-based client
7192
7314
  does not have. Both compiled. Both had green tests. Every one of those tests
7193
7315
  injected a double past the SDK, which is exactly why the bug class survives: the
@@ -7284,9 +7406,9 @@ are untouched.
7284
7406
 
7285
7407
  ### Changed — a fail-closed refusal now READS final
7286
7408
 
7287
- When a `PermissionChecker` throws, the call is denied. What the model was *told*
7409
+ When a `PermissionChecker` throws, the call is denied. What the model was _told_
7288
7410
  was the checker's own thrown message — and those are written for operators:
7289
- *"not available right now"*, `ECONNREFUSED`, *"timed out"*. **Measured in
7411
+ _"not available right now"_, `ECONNREFUSED`, _"timed out"_. **Measured in
7290
7412
  production: a real model read that as weather and retried the same tool to
7291
7413
  `maxIterations`, then returned the empty string.** Against the local policy's
7292
7414
  long-standing bracketed form the same model adapted cleanly on the first
@@ -7327,8 +7449,8 @@ calls did so in a silence that read like health.
7327
7449
  ### Added — `EventMeta.sessionId`: which CONVERSATION an event belongs to
7328
7450
 
7329
7451
  `meta.runId` is per `run()` / `resume()`; a session spans many. A shipped
7330
- telemetry stream could answer *"what happened in this run?"* and not *"what
7331
- happened in this conversation?"* — the question a session-oriented host is built
7452
+ telemetry stream could answer _"what happened in this run?"_ and not _"what
7453
+ happened in this conversation?"_ — the question a session-oriented host is built
7332
7454
  around, and one the events alone cannot be joined back into afterwards.
7333
7455
 
7334
7456
  `standingAgent` now threads the caller's own session id onto every event the run
@@ -7348,13 +7470,13 @@ three preconditions it had no evidence for, all three of which were wrong at
7348
7470
  once:
7349
7471
 
7350
7472
  - **A euclidean index was accepted.** The construction refusal read
7351
- `options.distanceMetric`, a *claim by the caller* about an index the store did
7473
+ `options.distanceMetric`, a _claim by the caller_ about an index the store did
7352
7474
  not create, so the default (undeclared) sailed through. Measured live: a
7353
7475
  vector queried against itself returned **0.9991630113800056**, where a true
7354
7476
  cosine self-similarity is exactly `1.0` — the very "number that READS like a
7355
7477
  cosine and is not one" this adapter's own header warns about.
7356
7478
  - **A missing `nonFilterableMetadataKeys: ['af']`** surfaced as a raw AWS
7357
- `ValidationException` — *"Filterable metadata must have at most 2048 bytes"* —
7479
+ `ValidationException` — _"Filterable metadata must have at most 2048 bytes"_ —
7358
7480
  **mid-import**, with documents already written and success already reported
7359
7481
  for them.
7360
7482
  - **The index dimension was never compared with the embedder.**
@@ -7378,11 +7500,11 @@ index whose metric, layout and dimension it would be guessing at.
7378
7500
 
7379
7501
  **Three promises the code had already made, kept.**
7380
7502
 
7381
- `MemoryStore`'s own docstring has named its backends since 2.x — *"Every storage
7503
+ `MemoryStore`'s own docstring has named its backends since 2.x — _"Every storage
7382
7504
  backend (InMemory, Redis, DynamoDB, **Postgres**, Bedrock AgentCore) implements
7383
- this interface"* — and named the queries too, in the two places an implementer
7384
- would look: *"**Postgres**: multi-row INSERT … ON CONFLICT DO UPDATE"* for
7385
- `putMany`, and *"**pgvector**: `ORDER BY embedding <=> query LIMIT k`"* for
7505
+ this interface"_ — and named the queries too, in the two places an implementer
7506
+ would look: _"**Postgres**: multi-row INSERT … ON CONFLICT DO UPDATE"_ for
7507
+ `putMany`, and _"**pgvector**: `ORDER BY embedding <=> query LIMIT k`"_ for
7386
7508
  `search`. Every one of those sentences was true about the design and false about
7387
7509
  the shipped package. Two of the three items below are the same shape: a
7388
7510
  documented promise with nothing behind it.
@@ -7546,8 +7668,8 @@ are now pinned; the rest are below.
7546
7668
  ### The conversation has a door with its own name
7547
7669
 
7548
7670
  ```ts
7549
- await agent.run({ message: 'Book me a table for two.' }); // one turn
7550
- await agent.followUp('Make it three.'); // the next one
7671
+ await agent.run({ message: 'Book me a table for two.' }); // one turn
7672
+ await agent.followUp('Make it three.'); // the next one
7551
7673
 
7552
7674
  // …or hand the conversation around — plain JSON, any store, any machine:
7553
7675
  const conversation = agent.checkpoint();
@@ -7605,7 +7727,7 @@ already broken in effect, so the throw is a fix, not a new restriction.
7605
7727
  is what makes `checkpoint()`, `getLastSnapshot()` and `followUp()` possible.
7606
7728
  Two overlapping runs both finished, both returning plausible answers, and the
7607
7729
  state afterwards belonged to whichever finished last — so `checkpoint()` could
7608
- hand back the *other* run's conversation, with nothing in either recording
7730
+ hand back the _other_ run's conversation, with nothing in either recording
7609
7731
  saying so. That is corruption, not concurrency. `standingAgent` has serialized
7610
7732
  runs since it existed and calls it "a correctness requirement rather than a
7611
7733
  tuning choice"; the guarantee now lives in the primitive. Two turns at once:
@@ -7619,16 +7741,16 @@ a consent gate any later message could walk around. Answer it with
7619
7741
  `resume(checkpoint, decision)`, or say plainly that it is being dropped:
7620
7742
 
7621
7743
  ```ts
7622
- const dropped = agent.abandonPause(); // { toolName, toolCallId, question }
7744
+ const dropped = agent.abandonPause(); // { toolName, toolCallId, question }
7623
7745
  await agent.run({ message: 'never mind, different question' });
7624
7746
  ```
7625
7747
 
7626
- `abandonPause()` *returns* what it dropped, so the abandonment can be logged
7748
+ `abandonPause()` _returns_ what it dropped, so the abandonment can be logged
7627
7749
  rather than performed blind.
7628
7750
 
7629
7751
  **A pause belongs to a session, not to the instance.** `standingAgent` shares
7630
7752
  one `Agent` across every session, so the instance guard alone would have let
7631
- session A's unanswered question refuse session B's *first* message — a
7753
+ session A's unanswered question refuse session B's _first_ message — a
7632
7754
  different conversation, a different person, an answer they were never asked
7633
7755
  for. The composer now releases the instance at the moment ownership moves: once
7634
7756
  the pause is in the store, the store owns it, and a later request carrying a
@@ -7649,7 +7771,7 @@ No shipped example, test or doc called it twice.
7649
7771
  ### `agent.canExplain()`
7650
7772
 
7651
7773
  `.selfExplain()` was already honest to the **model** with no record bound — the
7652
- trace tools answer *"No completed run is available yet"* and the skill body says
7774
+ trace tools answer _"No completed run is available yet"_ and the skill body says
7653
7775
  to say so plainly. It had no answer for the **program**. `canExplain()` returns
7654
7776
  `false` for two honest reasons — not built with `.selfExplain()`, or built with
7655
7777
  it and no turn completed — so a caller can route a why-question before spending
@@ -7677,7 +7799,7 @@ of this class quietly. Three kinds of pin:
7677
7799
  bare `throw`.
7678
7800
  2. **Adapted** — pinned by what reaches the model / the store / the caller,
7679
7801
  observed on the wire, never by mocking internals.
7680
- 3. **Stated** — pinned **twice**: the behavior, *and* the sentence in the source
7802
+ 3. **Stated** — pinned **twice**: the behavior, _and_ the sentence in the source
7681
7803
  that states it. A stated behavior whose statement was deleted is back to
7682
7804
  being a silent success, and only the second assertion catches that.
7683
7805
 
@@ -7696,7 +7818,7 @@ decision somebody makes in a diff.
7696
7818
  prints the messages the provider actually received for each. The model's own
7697
7819
  "this is your first message" reply is the evidence.
7698
7820
  - `examples/features/49-self-explain-live.ts` — turn 2 now goes through
7699
- `followUp()`. It was a second `run()`, and the scripted mock read the *first*
7821
+ `followUp()`. It was a second `run()`, and the scripted mock read the _first_
7700
7822
  user message, so the demo's own scripting was masking the restart. Live, the
7701
7823
  why-question used to arrive with no subject.
7702
7824
  - `docs-next/content/docs/build/conversations.mdx` — the conversation, the two
@@ -7708,13 +7830,13 @@ decision somebody makes in a diff.
7708
7830
  Additive except for the three refusals, and all three fire on code that was
7709
7831
  already wrong:
7710
7832
 
7711
- | if you… | you now get | do this |
7712
- | --- | --- | --- |
7713
- | call `run()` twice expecting continuity | the same behavior as before (a new conversation) | `followUp(message)` or `run({ message, continueFrom })` |
7714
- | overlap two `run()` calls on one agent | `RunInFlightError` | await the first, or build a second agent |
7715
- | send a message while a pause is open | `PendingQuestionError` | `resume(checkpoint, decision)`, or `abandonPause()` first |
7716
- | call `.system()` twice | build-time throw | join the strings, or `.steering()` / `.configure()` |
7717
- | continue a stored conversation on a differently-**named** agent | `ConversationMismatchError` | continue it on the agent whose id recorded it |
7833
+ | if you… | you now get | do this |
7834
+ | --------------------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------- |
7835
+ | call `run()` twice expecting continuity | the same behavior as before (a new conversation) | `followUp(message)` or `run({ message, continueFrom })` |
7836
+ | overlap two `run()` calls on one agent | `RunInFlightError` | await the first, or build a second agent |
7837
+ | send a message while a pause is open | `PendingQuestionError` | `resume(checkpoint, decision)`, or `abandonPause()` first |
7838
+ | call `.system()` twice | build-time throw | join the strings, or `.steering()` / `.configure()` |
7839
+ | continue a stored conversation on a differently-**named** agent | `ConversationMismatchError` | continue it on the agent whose id recorded it |
7718
7840
 
7719
7841
  `checkpoint()` payloads written by 9.1.0 and earlier continue to work; they
7720
7842
  simply carry no `identity` or `agent`, which is the honest answer for a
@@ -7745,13 +7867,13 @@ embedder might have — which then cuts every larger embedder short.
7745
7867
  characters, that this embedder represents faithfully. Every shipped embedder
7746
7868
  fills it in:
7747
7869
 
7748
- | embedder | `maxInputChars` | where the number comes from |
7749
- |---|---|---|
7750
- | `localEmbedder()` | `2000` | measured — the default model's 512-wordpiece-token cliff |
7751
- | `openaiEmbedder()` | `32000` | the documented 8,191-token window, at 4 characters a token |
7752
- | `bedrockEmbedder()` | `32000` | Titan's documented 8,192-token window, same conversion |
7753
- | `staticEmbedder()` | `1000000` | no transformer, so no context window — nothing is ever clipped |
7754
- | `mockEmbedder()` | `1000000` | reads every character in a loop |
7870
+ | embedder | `maxInputChars` | where the number comes from |
7871
+ | ------------------- | --------------- | -------------------------------------------------------------- |
7872
+ | `localEmbedder()` | `2000` | measured — the default model's 512-wordpiece-token cliff |
7873
+ | `openaiEmbedder()` | `32000` | the documented 8,191-token window, at 4 characters a token |
7874
+ | `bedrockEmbedder()` | `32000` | Titan's documented 8,192-token window, same conversion |
7875
+ | `staticEmbedder()` | `1000000` | no transformer, so no context window — nothing is ever clipped |
7876
+ | `mockEmbedder()` | `1000000` | reads every character in a loop |
7755
7877
 
7756
7878
  `indexCorpus`, `indexFolder` and `indexDocuments` read the embedder's declared
7757
7879
  ceiling **in preference to** their own 2,000-character default. An explicit
@@ -7769,7 +7891,7 @@ is accepted because the cliff belongs to the **model**, not to the factory.
7769
7891
  ### Truncation became visible
7770
7892
 
7771
7893
  A run that clipped anything now says so — **once**, on `console.warn`, naming
7772
- the count, the ceiling in effect *and where that ceiling came from*, and the
7894
+ the count, the ceiling in effect _and where that ceiling came from_, and the
7773
7895
  two fixes (re-split smaller, or raise `maxChunkChars`). `IndexReport` gains
7774
7896
  **`truncatedCount`** beside the existing `truncated` list: the list is what you
7775
7897
  debug with, the count is what you assert on and what a dashboard row can hold.
@@ -7817,27 +7939,27 @@ deprecation warnings on 8.20.0, it compiles and runs unchanged on 9.0.0.**
7817
7939
 
7818
7940
  ### Removed — the sixteen door aliases
7819
7941
 
7820
- Each removed path re-exported the *same symbols* the door carries, never copies,
7942
+ Each removed path re-exported the _same symbols_ the door carries, never copies,
7821
7943
  so this is a find-and-replace on import lines. No name moved; no name was lost.
7822
7944
 
7823
- | you were importing from | import from |
7824
- |---|---|
7825
- | `agentfootprint/llm-providers` | `agentfootprint/providers` |
7826
- | `agentfootprint/embedders` | `agentfootprint/providers` |
7827
- | `agentfootprint/tool-providers` | `agentfootprint/providers` |
7828
- | `agentfootprint/thinking` | `agentfootprint/providers` |
7829
- | `agentfootprint/memory-providers` | `agentfootprint/memory` |
7830
- | `agentfootprint/observability-providers` | `agentfootprint/observe` |
7831
- | `agentfootprint/strategies` | `agentfootprint/observe` |
7832
- | `agentfootprint/stream` | `agentfootprint/observe` |
7833
- | `agentfootprint/status` | `agentfootprint/observe` |
7834
- | `agentfootprint/locales` | `agentfootprint/observe` |
7835
- | `agentfootprint/debug` | `agentfootprint/observe` |
7836
- | `agentfootprint/debug/finders` | `agentfootprint/observe` |
7837
- | `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
7838
- | `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
7839
- | `agentfootprint/injection-engine` | `agentfootprint/context` |
7840
- | `agentfootprint/identity` | `agentfootprint/security` |
7945
+ | you were importing from | import from |
7946
+ | --------------------------------------------------- | -------------------------- |
7947
+ | `agentfootprint/llm-providers` | `agentfootprint/providers` |
7948
+ | `agentfootprint/embedders` | `agentfootprint/providers` |
7949
+ | `agentfootprint/tool-providers` | `agentfootprint/providers` |
7950
+ | `agentfootprint/thinking` | `agentfootprint/providers` |
7951
+ | `agentfootprint/memory-providers` | `agentfootprint/memory` |
7952
+ | `agentfootprint/observability-providers` | `agentfootprint/observe` |
7953
+ | `agentfootprint/strategies` | `agentfootprint/observe` |
7954
+ | `agentfootprint/stream` | `agentfootprint/observe` |
7955
+ | `agentfootprint/status` | `agentfootprint/observe` |
7956
+ | `agentfootprint/locales` | `agentfootprint/observe` |
7957
+ | `agentfootprint/debug` | `agentfootprint/observe` |
7958
+ | `agentfootprint/debug/finders` | `agentfootprint/observe` |
7959
+ | `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
7960
+ | `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
7961
+ | `agentfootprint/injection-engine` | `agentfootprint/context` |
7962
+ | `agentfootprint/identity` | `agentfootprint/security` |
7841
7963
 
7842
7964
  What ships now is exactly: the root barrel, the ten doors (`/providers`,
7843
7965
  `/memory`, `/rag`, `/cache`, `/observe`, `/events`, `/context`, `/resilience`,
@@ -7853,23 +7975,23 @@ implementation barrel is served by its door as the same object.
7853
7975
 
7854
7976
  ### Removed — options, strings, methods, fields
7855
7977
 
7856
- | removed | replacement | since |
7857
- |---|---|---|
7858
- | `AgentBuilder.recorder(rec)` | `AgentBuilder.watch(rec)` — same list, same order, same attachment, and variadic | deprecated 8.0.0 |
7859
- | `defineSkill({ viaToolName })` | drop it — `'read_skill'` is the only activation tool the library builds; gate on a `rule` trigger or a `skillGraph()` edge | deprecated 8.7.0 |
7860
- | `skillsFromDir(dir, { viaToolName })` | drop it — same reason | deprecated 8.7.0 |
7861
- | `WindowRefusalReason` member `'summary-not-smaller'` | `'replacement-not-smaller'` | renamed 8.14.0 |
7862
- | type `FoldRefusal` | `WindowRefusal` | renamed 7.17 |
7863
- | type `FoldRefusalReason` | `WindowRefusalReason` | renamed 7.17 |
7864
- | `WindowStrategy` exported from `agentfootprint/memory` | `MemoryWindowStrategy` | renamed 7.27.1 |
7865
- | `CompactionRecord.foldedStageIds` | `WindowRecord.removedStageIds` | family name published 7.17 |
7866
- | `CompactionRecord.foldedMessageCount` | `WindowRecord.removedMessageCount` | family name published 7.17 |
7867
- | `ContextBudgetPressurePayload.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
7868
- | `ContextBudgetPressurePayload.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
7869
- | `BudgetPressureRecord.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
7870
- | `BudgetPressureRecord.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
7871
-
7872
- Three of those are worth a sentence each, because the *reason* is the migration:
7978
+ | removed | replacement | since |
7979
+ | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
7980
+ | `AgentBuilder.recorder(rec)` | `AgentBuilder.watch(rec)` — same list, same order, same attachment, and variadic | deprecated 8.0.0 |
7981
+ | `defineSkill({ viaToolName })` | drop it — `'read_skill'` is the only activation tool the library builds; gate on a `rule` trigger or a `skillGraph()` edge | deprecated 8.7.0 |
7982
+ | `skillsFromDir(dir, { viaToolName })` | drop it — same reason | deprecated 8.7.0 |
7983
+ | `WindowRefusalReason` member `'summary-not-smaller'` | `'replacement-not-smaller'` | renamed 8.14.0 |
7984
+ | type `FoldRefusal` | `WindowRefusal` | renamed 7.17 |
7985
+ | type `FoldRefusalReason` | `WindowRefusalReason` | renamed 7.17 |
7986
+ | `WindowStrategy` exported from `agentfootprint/memory` | `MemoryWindowStrategy` | renamed 7.27.1 |
7987
+ | `CompactionRecord.foldedStageIds` | `WindowRecord.removedStageIds` | family name published 7.17 |
7988
+ | `CompactionRecord.foldedMessageCount` | `WindowRecord.removedMessageCount` | family name published 7.17 |
7989
+ | `ContextBudgetPressurePayload.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
7990
+ | `ContextBudgetPressurePayload.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
7991
+ | `BudgetPressureRecord.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
7992
+ | `BudgetPressureRecord.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
7993
+
7994
+ Three of those are worth a sentence each, because the _reason_ is the migration:
7873
7995
 
7874
7996
  - **`viaToolName` named a door that was never built.** The evaluator activates
7875
7997
  an `llm-activated` skill by matching `ctx.activatedInjectionIds`, only
@@ -7908,7 +8030,7 @@ replacement and says when the signpost comes down.
7908
8030
  Deleting the type member alone would have been a silent DOWNGRADE for the two
7909
8031
  `viaToolName` cases: an object literal gets an excess-property error, but an
7910
8032
  options bag arriving through a variable does not, and the value would then be
7911
- *ignored* where 8.7.0 refused it. So the field is read at run time exactly once
8033
+ _ignored_ where 8.7.0 refused it. So the field is read at run time exactly once
7912
8034
  more, to say it is gone.
7913
8035
 
7914
8036
  ### Two things deliberately kept
@@ -7923,7 +8045,7 @@ more, to say it is gone.
7923
8045
  `CircuitState`, which is declared in both breaker files as
7924
8046
  `'closed' | 'open' | 'half-open'` — two declarations, one type, pinned as such.
7925
8047
  - **`buildRunSteps(events)` survives, still `@deprecated`.** Its deprecation is
7926
- a *preference*, not a migration: live consumers should attach
8048
+ a _preference_, not a migration: live consumers should attach
7927
8049
  `runStepRecorder()` and read `getSteps()` (O(N), the house pattern) instead of
7928
8050
  re-walking an event log (O(N²) across repeated calls). But the shim is the only
7929
8051
  way to build steps from a saved event list — replay, post-hoc analysis, tests —
@@ -8000,7 +8122,7 @@ The fix is one coherent rule across the structural splitters:
8000
8122
  alone. The heading is now glued to the first body paragraph; it can never
8001
8123
  again be a chunk by itself.
8002
8124
  - **The family was inspected, and two members are exempt by design.**
8003
- `fixedWithOverlap` cuts uniformly sized chunks *by request* — imposing a
8125
+ `fixedWithOverlap` cuts uniformly sized chunks _by request_ — imposing a
8004
8126
  250-char floor on `fixedWithOverlap({ chars: 120 })` would repeal the
8005
8127
  caller's own choice, and its only runt (the file tail) has always folded
8006
8128
  backward. `wholeDocument` is one chunk per document by definition.
@@ -8054,7 +8176,7 @@ any immutable or serverless runtime has this problem):
8054
8176
  - **`exportCorpus(store, identity?)`** (`agentfootprint/rag`) — every entry
8055
8177
  of a corpus namespace as one plain-JSON `CorpusBundle`:
8056
8178
  `{ entries: [{ id, text, vector, metadata }], embedder: { id, dimensions },
8057
- namespace }`. Plain JSON on purpose — the runtime that needs this is
8179
+ namespace }`. Plain JSON on purpose — the runtime that needs this is
8058
8180
  exactly the runtime that cannot open a database file. It refuses an empty
8059
8181
  namespace (naming the identity-mismatch cause), entries with no vector or
8060
8182
  no passage (a bundle never ships an unservable or uncitable entry), and a
@@ -8216,7 +8338,7 @@ it, before a byte is embedded. `InMemoryStore` and `sqliteVectorStore` declare
8216
8338
  **Behaviour change:** building a corpus into `AgentCoreStore` or `RedisStore` is
8217
8339
  refused where it used to run. Neither could ever serve those vectors back —
8218
8340
  `RedisStore` has no `search()` at all, so the same mistake already failed one
8219
- layer later, when `defineRAG` refused the store *after* the whole index had been
8341
+ layer later, when `defineRAG` refused the store _after_ the whole index had been
8220
8342
  embedded and billed. The refusal moved to the call that starts the spending.
8221
8343
 
8222
8344
  **Absence is not a `false`.** A store that declares nothing behaves exactly as it
@@ -8236,7 +8358,7 @@ it wrong.
8236
8358
  ```ts
8237
8359
  import { bedrockEmbedder } from 'agentfootprint/providers';
8238
8360
 
8239
- const embedder = bedrockEmbedder({ region: 'us-east-1' }); // 1024-d
8361
+ const embedder = bedrockEmbedder({ region: 'us-east-1' }); // 1024-d
8240
8362
  const small = bedrockEmbedder({ region: 'us-east-1', dimensions: 512 });
8241
8363
  ```
8242
8364
 
@@ -8409,7 +8531,7 @@ the run stops re-asking and names the rule — in the `outputAttempts` row
8409
8531
  (`brokenBy`), in the event, in `outputContractUnmet()`, and in the warning.
8410
8532
  Re-asking cannot fix a rule: a deterministic one breaks the next answer
8411
8533
  identically, so the retries buy a repeat of the same ending. An answer that was
8412
- *already* bad still spends its retries — the stop applies only when the
8534
+ _already_ bad still spends its retries — the stop applies only when the
8413
8535
  middleware is the cause.
8414
8536
 
8415
8537
  ### `.outputFallback()` says which door reaches its tiers
@@ -8496,8 +8618,8 @@ flowchartAsTool({
8496
8618
  name: 'weather_advice',
8497
8619
  description: 'Decide whether to bike tomorrow.',
8498
8620
  flowchart: adviceChart,
8499
- keepRecord: true, // ← off by default
8500
- keepRecordLimit: 20, // ← bounded LRU window (this is the default)
8621
+ keepRecord: true, // ← off by default
8622
+ keepRecordLimit: 20, // ← bounded LRU window (this is the default)
8501
8623
  redact: { keys: ['apiKey'] },
8502
8624
  });
8503
8625
  ```
@@ -8588,8 +8710,8 @@ artifacts by hand: `innerRunStore`, `innerRunsOf`, `mergeInnerRuns`,
8588
8710
  ### The demo
8589
8711
 
8590
8712
  `examples/features/50-through-the-tool-boundary.ts` — a weather-advice agent
8591
- whose ONE tool is a 4-stage footprintjs chart. Turn 1: *"Should I bike to work
8592
- in Chicago tomorrow?"* Turn 2: *"Why did you say it'll rain?"* — answered
8713
+ whose ONE tool is a 4-stage footprintjs chart. Turn 1: _"Should I bike to work
8714
+ in Chicago tomorrow?"_ Turn 2: _"Why did you say it'll rain?"_ — answered
8593
8715
  through visible `find_in_trace` → `inspect_tool_call` → `inspect_tool_run`
8594
8716
  calls that cite the inner stage (`validate-forecast#1`), the exact field
8595
8717
  (`rainChancePct = 82`) and the rule that consumed it. The chart's own stage
@@ -8610,8 +8732,8 @@ evidence existed, and the model had no way to reach it.
8610
8732
  ### `find_in_trace(query)` — free text in, step ids out
8611
8733
 
8612
8734
  Every other trace tool needs a name you already have: a step id, a state key, a
8613
- variable. But a follow-up question arrives in the user's words — *"why did you say
8614
- order 7712 was out of warranty?"* — and the model's only options were to guess a
8735
+ variable. But a follow-up question arrives in the user's words — _"why did you say
8736
+ order 7712 was out of warranty?"_ — and the model's only options were to guess a
8615
8737
  state key or read the whole narrative.
8616
8738
 
8617
8739
  This searches stage names and descriptions, state keys, every committed value and
@@ -8748,7 +8870,7 @@ await callTraceTool(tools, 'find_in_trace', { query: 'order 7712' });
8748
8870
 
8749
8871
  Pure: no engine, no agent, no I/O. Exported from `agentfootprint/observe` (and
8750
8872
  `agentfootprint/debug`). Honest about the two things a serialized run cannot carry
8751
- back — `controlDeps` is a lookup *function* and does not serialize (slices say
8873
+ back — `controlDeps` is a lookup _function_ and does not serialize (slices say
8752
8874
  `⚠ control edges unavailable`, the marker that already existed), and the narrative
8753
8875
  survives only if a narrative recorder was attached, since `recordRun` deliberately
8754
8876
  attaches none. Two teaching refusals name `recordRun` as the producer: a bundle with
@@ -8801,11 +8923,11 @@ cursor half and left the rule half standing, and the rule half is the bug: an en
8801
8923
  routed to `T`, `S`'s rule still matched and `S` and `T` were both active. Measured on
8802
8924
  a two-skill support graph:
8803
8925
 
8804
- | iteration | active | tool menu | skill bodies |
8805
- |---|---|---|---|
8806
- | 1 | `triage` | `read_skill`, `lookup_order` | triage |
8926
+ | iteration | active | tool menu | skill bodies |
8927
+ | --------------- | ------------------ | -------------------------------------------- | --------------- |
8928
+ | 1 | `triage` | `read_skill`, `lookup_order` | triage |
8807
8929
  | 2 — the handoff | `triage`, `refund` | `read_skill`, `lookup_order`, `issue_refund` | triage + refund |
8808
- | 3 | `triage`, `refund` | same | triage + refund |
8930
+ | 3 | `triage`, `refund` | same | triage + refund |
8809
8931
 
8810
8932
  Note iteration 3. This was never a one-iteration blip: with the cursor parked on
8811
8933
  `refund`, `triage`'s rule kept matching, so it came back and stayed. The overlap was
@@ -8813,8 +8935,8 @@ the steady state.
8813
8935
 
8814
8936
  **A conditional entry is now active exactly while the cursor is on it** — the same
8815
8937
  compiled expression a route target and an exclusive entry already used. One law for a
8816
- flat graph: *a skill is active iff the cursor is on it, or it declared itself
8817
- unconditional.* `when` chooses where a turn STARTS.
8938
+ flat graph: _a skill is active iff the cursor is on it, or it declared itself
8939
+ unconditional._ `when` chooses where a turn STARTS.
8818
8940
 
8819
8941
  This finishes 8.3.0 rather than reverting it. Both failures 8.3.0 named — a declared
8820
8942
  step INTO an entry skill, and a `read_skill` pick onto one — are carried by the cursor
@@ -8852,7 +8974,7 @@ superseded pick.
8852
8974
 
8853
8975
  `multi-entry-fanout` fired whenever a graph declared two or more entries, including
8854
8976
  when every one of them carried a `when` — a deterministic rule-router, which is a
8855
- taught shape. Worse, the advice it gave was *"give the extras a `when`"*, to entries
8977
+ taught shape. Worse, the advice it gave was _"give the extras a `when`"_, to entries
8856
8978
  that already had one. It computed which entries were unconditional and then used that
8857
8979
  only to soften the middle of the sentence.
8858
8980
 
@@ -8871,7 +8993,7 @@ why the check over-fired. Rewritten.
8871
8993
  A suppression the run cannot name is a silent drop. When a conditional entry's rule
8872
8994
  matched and the cursor law kept it off the wire, the entry's id is now reported on the
8873
8995
  per-iteration evaluation event, beside the `cursorMove` that says where the graph went
8874
- instead. Together they answer *"why isn't my entry loading?"* without anyone
8996
+ instead. Together they answer _"why isn't my entry loading?"_ without anyone
8875
8997
  re-running a predicate to guess.
8876
8998
 
8877
8999
  Omitted when nothing was suppressed, and for every non-skill-graph run — so an
@@ -8917,8 +9039,8 @@ build, and one changes a string on the wire. All eight are below.
8917
9039
  correct branch:
8918
9040
 
8919
9041
  - **same provider family** — it billed your MAIN model for every fold. The
8920
- refusal three lines above it in the same file promised *"the library will not
8921
- quietly bill your main model for compaction"*, and then did.
9042
+ refusal three lines above it in the same file promised _"the library will not
9043
+ quietly bill your main model for compaction"_, and then did.
8922
9044
  - **different provider** — it sent your agent's model id to a vendor that has
8923
9045
  never heard of it, so the fold died mid-run, on a paid run, in a file whose
8924
9046
  own header promises "everything fails at `.build()`, never mid-run".
@@ -8946,7 +9068,7 @@ Readings are now stamped with the iteration whose call produced them and expire
8946
9068
  one boundary later. An expired reading is `undefined`, which every strategy
8947
9069
  already treats as "do not act" — so a window strategy **stands down** instead of
8948
9070
  deciding on a number nobody took, and says so once on the console rather than
8949
- going quiet. "Counted, never guessed" has to mean counted *recently*.
9071
+ going quiet. "Counted, never guessed" has to mean counted _recently_.
8950
9072
 
8951
9073
  An agent whose provider reports usage reliably is unaffected.
8952
9074
 
@@ -8991,10 +9113,10 @@ comparison of two string lengths will not.
8991
9113
  Two emitters share this event name, this `slot: 'messages'` value, and — until
8992
9114
  now — one indistinguishable payload:
8993
9115
 
8994
- | emitter | counts | `unit` |
8995
- |---|---|---|
8996
- | the three context slots (`contextBudget`, **on by default**) | `String.length` | `'chars'` |
8997
- | a window strategy (`.window()` / `.compaction()`) | provider-reported input tokens | `'tokens'` |
9116
+ | emitter | counts | `unit` |
9117
+ | ------------------------------------------------------------ | ------------------------------ | ---------- |
9118
+ | the three context slots (`contextBudget`, **on by default**) | `String.length` | `'chars'` |
9119
+ | a window strategy (`.window()` / `.compaction()`) | provider-reported input tokens | `'tokens'` |
8998
9120
 
8999
9121
  So one subscriber routinely received both, and `cap 200, projected 258` could
9000
9122
  mean 258 characters or 258 tokens — a roughly 4× difference in the same field,
@@ -9018,8 +9140,8 @@ oversight — please do not "fix" them.
9018
9140
  ### `costBudget` can now stop the run
9019
9141
 
9020
9142
  `costBudget` was warn-only, while `commentaryTemplates.ts` narrated
9021
- *"{{appName}} hit a cost limit and stopped."* and `docs/monitor/deployment.mdx`
9022
- claimed the agent *"halts when the per-run USD budget is hit"*. It did neither.
9143
+ _"{{appName}} hit a cost limit and stopped."_ and `docs/monitor/deployment.mdx`
9144
+ claimed the agent _"halts when the per-run USD budget is hit"_. It did neither.
9023
9145
  `docs/monitor/observability.mdx`, on the same site, correctly said the library
9024
9146
  never auto-aborts.
9025
9147
 
@@ -9128,7 +9250,7 @@ version would make an older deployment refuse a session it can serve.
9128
9250
  **Governance never silently drops — and never silently invents.** Eight ways a
9129
9251
  rule you configured could decide nothing, and you could only find out by reading
9130
9252
  a quiet run. Seven are now refused at build time with a message that names the
9131
- fix; one was a rule that ran everywhere except the one path where a *person* had
9253
+ fix; one was a rule that ran everywhere except the one path where a _person_ had
9132
9254
  just typed the value.
9133
9255
 
9134
9256
  Two of these change what a run DOES. Both are called out below.
@@ -9200,7 +9322,7 @@ It now raises `DecisionRequiredError` (`code: 'ERR_DECISION_REQUIRED'`) at the
9200
9322
  API boundary. **Nothing executes and the checkpoint is unchanged**, so the same
9201
9323
  one can be answered properly and resumed again. The error names the gate
9202
9324
  (`gate: 'checkIn' | 'ask'`), the tool, the middleware that asked, and `received`
9203
- — the *shape* that arrived, never its contents, because a resume payload is
9325
+ — the _shape_ that arrived, never its contents, because a resume payload is
9204
9326
  caller data and an error message ends up in logs.
9205
9327
 
9206
9328
  Discriminated by the **pause**, never by the input, via the new
@@ -9282,9 +9404,9 @@ never stops your strategy. It now also carries two methods:
9282
9404
  ```ts
9283
9405
  const telemetry = agent.enable.observability({ strategy: cloudwatch });
9284
9406
 
9285
- await telemetry.flush(); // drain: driver queue first, then the buffer
9286
- telemetry(); // detach (unchanged)
9287
- telemetry.stop(); // release — timers, clients, buffers
9407
+ await telemetry.flush(); // drain: driver queue first, then the buffer
9408
+ telemetry(); // detach (unchanged)
9409
+ telemetry.stop(); // release — timers, clients, buffers
9288
9410
  ```
9289
9411
 
9290
9412
  `flush()` enforces the ORDER, which is the part no consumer could write from
@@ -9407,7 +9529,7 @@ of trying again. A drain that cannot finish must return, never retry forever.
9407
9529
 
9408
9530
  With `enable.observability({ detach })`, each export is scheduled onto a
9409
9531
  footprintjs detach driver. Scheduling happened inside a promise continuation,
9410
- so the detach handle reached footprintjs's registry a microtask *after* the
9532
+ so the detach handle reached footprintjs's registry a microtask _after_ the
9411
9533
  event was dispatched. `flushAllDetached()` drains until that registry is empty
9412
9534
  — and it was still empty when it looked.
9413
9535
 
@@ -9430,7 +9552,7 @@ the package either way.
9430
9552
  reach today (the declared type is still `Unsubscribe`); it exists so the
9431
9553
  no-subscription case is not the one path that breaks when that type widens.
9432
9554
  - Removed a dead loop condition in the CloudWatch drain (`lastFlushPromise !==
9433
- Promise.resolve()` compares against a freshly minted promise and is always
9555
+ Promise.resolve()` compares against a freshly minted promise and is always
9434
9556
  true).
9435
9557
 
9436
9558
  ## [8.11.0] - 2026-08-07
@@ -9467,7 +9589,7 @@ delivery — just an empty log group.
9467
9589
  The docstring for `logStreamName` had been promising `"Created on first put if
9468
9590
  it doesn't exist"` since the adapter shipped. It was never true. Worse, the
9469
9591
  convention the docs themselves recommended — `` `${HOSTNAME}/${Date.now()}` ``
9470
- — produces a name that *cannot* pre-exist, so following the documentation
9592
+ — produces a name that _cannot_ pre-exist, so following the documentation
9471
9593
  guaranteed the bug on every deploy. The only configuration that worked was the
9472
9594
  undocumented one.
9473
9595
 
@@ -9483,7 +9605,7 @@ instead of implying otherwise.
9483
9605
 
9484
9606
  The missing stream was one delivery failure. It turned out **every** delivery
9485
9607
  failure was silent: an IAM denial, a throttle, a rejected batch. Each adapter
9486
- installed its console fallback lazily *inside* its own `_onError` method — so
9608
+ installed its console fallback lazily _inside_ its own `_onError` method — so
9487
9609
  the delivery path, which read the hook rather than calling the method, found
9488
9610
  `undefined` and dropped the error on the floor. `cloudwatch`, `xray` and `otel`
9489
9611
  all had it.
@@ -9510,7 +9632,7 @@ fail. It is replaced by six that assert unconditionally.
9510
9632
 
9511
9633
  ### A knob the warning told you to turn, that did not exist
9512
9634
 
9513
- An over-budget context slot warned: *"Raise `budgetCap` on the slot config."*
9635
+ An over-budget context slot warned: _"Raise `budgetCap` on the slot config."_
9514
9636
  `budgetCap` was reachable from no public door. `buildMessagesSlot()` was called
9515
9637
  with no arguments at all four of its call sites, so its 10000-character cap was
9516
9638
  unreachable by construction. A warning you cannot act on is worse than no
@@ -9530,15 +9652,15 @@ budget is a signal, not a limiter. `LLMCallOptions` takes the same option
9530
9652
 
9531
9653
  A 429 is a **pre-execution rejection**: the rate limiter refused the request at
9532
9654
  the edge and the server never ran the tool, so a retry cannot double-execute
9533
- anything. That is exactly what is *not* true of a 500 or a timeout, where the
9655
+ anything. That is exactly what is _not_ true of a 500 or a timeout, where the
9534
9656
  call may have half-run and a retry could charge a card twice.
9535
9657
 
9536
9658
  That asymmetry is the entire license for this feature, so the policy is 429 and
9537
9659
  nothing else — pinned by a property test that walks twelve other statuses and a
9538
9660
  thrown transport error and asserts a single attempt for each. Managed gateways
9539
9661
  rate-limit per principal by design; without this, a designed and self-clearing
9540
- condition reached the model as a thrown tool error it reads as *"this tool is
9541
- broken"*, whereupon it apologises, picks another tool, or invents an answer.
9662
+ condition reached the model as a thrown tool error it reads as _"this tool is
9663
+ broken"_, whereupon it apologises, picks another tool, or invents an answer.
9542
9664
 
9543
9665
  It lives at the `fetch` seam because that is the only place `Retry-After` still
9544
9666
  exists — the MCP SDK reads the response, throws `StreamableHTTPError(status,
@@ -9565,18 +9687,22 @@ Per-attempt visibility is the `onRetry` callback — the contract `withRetry` an
9565
9687
  are consumer-called, the docs now say so, and a batching exporter loses its
9566
9688
  final batch and leaks its timer if you skip them:
9567
9689
  ```ts
9568
- process.on('SIGTERM', async () => { await telemetry.flush(); telemetry.stop(); stop(); });
9690
+ process.on('SIGTERM', async () => {
9691
+ await telemetry.flush();
9692
+ telemetry.stop();
9693
+ stop();
9694
+ });
9569
9695
  ```
9570
9696
  Wiring them into the framework lifecycle would change `run()` timing and
9571
9697
  misbehave for a strategy shared across two `enable` calls, so it is a design
9572
9698
  question on the ledger rather than a silent default.
9573
9699
  - **A Skill's tools are visible from iteration 1.** `DefineSkillOptions.tools`
9574
- said they were *"added to the tools slot once activated"*. They are added to
9700
+ said they were _"added to the tools slot once activated"_. They are added to
9575
9701
  the registry at build time; activation adds the Skill's **body**, not its
9576
9702
  tools. Gating is opt-in via `autoActivate: 'currentSkill'` (which
9577
9703
  `skillGraph().tree()` sets for you on every leaf) — and the docs said
9578
9704
  otherwise in twelve places, including a `process_refund` example claiming a
9579
- tool was *"locked away"*. That example now sets `autoActivate` and the prose
9705
+ tool was _"locked away"_. That example now sets `autoActivate` and the prose
9580
9706
  no longer implies a security boundary the default does not provide.
9581
9707
  - **`autoActivate` stopped calling itself a forward-compat marker** awaiting
9582
9708
  "v2.5 runtime wiring" — that wiring shipped in 2.5.0, six majors ago.
@@ -9586,7 +9712,7 @@ Per-attempt visibility is the `onRetry` callback — the contract `withRetry` an
9586
9712
  anything, and a lower tier is not a safer one: `'minimal'` still ships
9587
9713
  `agent.turn_start` (`userPrompt`), `agent.turn_end` (`finalContent`) and
9588
9714
  `agent.iteration_end` (the whole conversation `history[]`) — measured, it
9589
- carries user content in a *higher* share of its events than `'standard'` does.
9715
+ carries user content in a _higher_ share of its events than `'standard'` does.
9590
9716
  The docstring says this plainly now, points at `auditExport()` (bounded by
9591
9717
  default) and `otelObservability()` (omits `userPrompt`), and warns that
9592
9718
  `redactContent` does **not** apply to this channel — it operates on the
@@ -9680,12 +9806,12 @@ exists.
9680
9806
  `textLoader`, `markdownLoader` and `htmlLoader` need no dependency. `pdfLoader`
9681
9807
  needs one, and it was picked by measuring rather than by reputation:
9682
9808
 
9683
- | package | installed | packages | verdict |
9684
- |---|---|---|---|
9685
- | **`unpdf`** | **2.5 MB** | **1** | chosen — zero transitive deps, per-page text |
9686
- | `pdf-parse@2` | 86 MB | 3 | a native binary (`@napi-rs/canvas`), to read text |
9687
- | `pdf-parse@1` | 34 MB | 4 | unmaintained since 2018 |
9688
- | `pdfjs-dist@6` | 62 MB | 2 | 25× the size for the same engine |
9809
+ | package | installed | packages | verdict |
9810
+ | -------------- | ---------- | -------- | ------------------------------------------------- |
9811
+ | **`unpdf`** | **2.5 MB** | **1** | chosen — zero transitive deps, per-page text |
9812
+ | `pdf-parse@2` | 86 MB | 3 | a native binary (`@napi-rs/canvas`), to read text |
9813
+ | `pdf-parse@1` | 34 MB | 4 | unmaintained since 2018 |
9814
+ | `pdfjs-dist@6` | 62 MB | 2 | 25× the size for the same engine |
9689
9815
 
9690
9816
  It is an optional peer, lazily loaded, refusing with an install line when a PDF
9691
9817
  is actually met. Per-page text is why a PDF citation can name a page you can
@@ -9789,7 +9915,6 @@ the page its 150-character run-up borrowed from.
9789
9915
  - **`unpdf`** as a new optional peer. Only `pdfLoader` touches it, only when a
9790
9916
  PDF is actually read.
9791
9917
 
9792
-
9793
9918
  ## [8.9.0] - 2026-08-06
9794
9919
 
9795
9920
  **The durable index.** 8.8.0 made retrieval tell the truth about what it read.
@@ -9815,8 +9940,8 @@ await indexDocuments(store, embedder, docs, { embedderId: embedder.id });
9815
9940
  Embedding cost is not one number, and the split is the whole argument for a
9816
9941
  file. **Index time** embeds the corpus: once, scaling with how much you store.
9817
9942
  **Query time** embeds the user's question: per retrieval, scaling with traffic.
9818
- A 10,000-chunk corpus is 10,000 embeddings *once* and one per question
9819
- thereafter — with a `Map` it is 10,000 embeddings *per restart*.
9943
+ A 10,000-chunk corpus is 10,000 embeddings _once_ and one per question
9944
+ thereafter — with a `Map` it is 10,000 embeddings _per restart_.
9820
9945
 
9821
9946
  `agentfootprint.embedding.generated` has carried an `inputKind: 'document' |
9822
9947
  'query'` field since 2.x and nothing ever emitted it, so any dashboard built
@@ -9839,13 +9964,13 @@ top-K or it does not answer.
9839
9964
 
9840
9965
  Measured against this implementation on Node 22.16, Apple silicon:
9841
9966
 
9842
- | corpus | query | resident matrix | file | first search (hydration) |
9843
- |---|---|---|---|---|
9844
- | 10,000 × 384-d | 6 ms | 15 MB | 21 MB | 45 ms |
9845
- | 50,000 × 384-d | 31 ms | 77 MB | 105 MB | 251 ms |
9846
- | 100,000 × 384-d | 65 ms | 154 MB | 211 MB | 939 ms |
9847
- | 10,000 × 1536-d | 16 ms | 61 MB | 83 MB | 122 ms |
9848
- | 50,000 × 1536-d | 89 ms | 307 MB | 413 MB | **5.7 s** |
9967
+ | corpus | query | resident matrix | file | first search (hydration) |
9968
+ | --------------- | ----- | --------------- | ------ | ------------------------ |
9969
+ | 10,000 × 384-d | 6 ms | 15 MB | 21 MB | 45 ms |
9970
+ | 50,000 × 384-d | 31 ms | 77 MB | 105 MB | 251 ms |
9971
+ | 100,000 × 384-d | 65 ms | 154 MB | 211 MB | 939 ms |
9972
+ | 10,000 × 1536-d | 16 ms | 61 MB | 83 MB | 122 ms |
9973
+ | 50,000 × 1536-d | 89 ms | 307 MB | 413 MB | **5.7 s** |
9849
9974
 
9850
9975
  **The documented ceiling is 50,000 chunks** — under 100 ms per query at every
9851
9976
  embedder this library ships, under ~300 MB resident. It degrades linearly to
@@ -9928,13 +10053,12 @@ safe to answer with "no matches".**
9928
10053
  - The BLOB on disk keeps the **original** vector, so `get`/`list` round-trip
9929
10054
  exactly what was written; normalisation happens once, into the resident
9930
10055
  matrix, so search is a dot product without changing what is stored.
9931
- - Found while writing the schema-identity check: it originally ran *after* the
10056
+ - Found while writing the schema-identity check: it originally ran _after_ the
9932
10057
  indexes were created, so a foreign `af_vectors` table failed on a missing
9933
10058
  column and was reported as `'cannot-open'` — the right refusal for the wrong
9934
10059
  reason, telling the reader to check file permissions when the real problem was
9935
10060
  that the file belonged to something else. It runs before them now.
9936
10061
 
9937
-
9938
10062
  ## [8.8.0] - 2026-08-06
9939
10063
 
9940
10064
  **Retrieval tells the truth.** A retrieval computed a cosine score for every candidate
@@ -9969,12 +10093,14 @@ the two are registered separately, each with its own store:
9969
10093
  ```ts
9970
10094
  const agent = Agent.create({ provider })
9971
10095
  .rag(defineRAG({ id: 'product-docs', store: corpusStore, embedder }))
9972
- .memory(defineMemory({
9973
- id: 'chat',
9974
- type: MEMORY_TYPES.EPISODIC,
9975
- strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
9976
- store: conversationStore,
9977
- }))
10096
+ .memory(
10097
+ defineMemory({
10098
+ id: 'chat',
10099
+ type: MEMORY_TYPES.EPISODIC,
10100
+ strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
10101
+ store: conversationStore,
10102
+ }),
10103
+ )
9978
10104
  .build();
9979
10105
  ```
9980
10106
 
@@ -9996,7 +10122,7 @@ namespace it reads from — defaulting to the same `'_global'` the indexer write
9996
10122
  index with no options and retrieve with no options and the documents are found. Pass it
9997
10123
  explicitly for a per-tenant corpus, on both sides.
9998
10124
 
9999
- And a namespace that holds nothing is now *reported* rather than answered around:
10125
+ And a namespace that holds nothing is now _reported_ rather than answered around:
10000
10126
  `corpusEmpty: true` on the retrieval event, plus a once-per-process warning naming the
10001
10127
  namespace it searched and the usual cause.
10002
10128
 
@@ -10015,7 +10141,7 @@ namespace it searched and the usual cause.
10015
10141
  are admitted**: `search` returns score-descending, so either the whole pool clears the
10016
10142
  floor (admitted = first `k`, as before) or some entry fails it (every later entry fails
10017
10143
  too, so the pool already holds every entry that clears it). `rejectWindow` only controls
10018
- how many near-misses can be *shown*.
10144
+ how many near-misses can be _shown_.
10019
10145
  - **`agentfootprint.memory.retrieved`** (new, 72 typed events) — one per retrieval,
10020
10146
  carrying every candidate. `candidates: undefined` means the store ranked server-side
10021
10147
  and returned nothing comparable; it never means there were none.
@@ -10056,7 +10182,7 @@ recency ordering the best-scoring chunk can land last.
10056
10182
  ### Chunks the model can cite
10057
10183
 
10058
10184
  A retrieved page of a PDF rendered as `<memory role="unknown" turn="0">` under the header
10059
- *"Relevant context from prior conversations"* — three claims that were not true of a
10185
+ _"Relevant context from prior conversations"_ — three claims that were not true of a
10060
10186
  document, and no way to cite it. `defineRAG` renders a corpus as what it is:
10061
10187
 
10062
10188
  ```text
@@ -10108,13 +10234,12 @@ nothing to configure.
10108
10234
  `core/`; a second implementation is how two recordings of the same bytes end up
10109
10235
  disagreeing about their id.
10110
10236
 
10111
-
10112
10237
  ## [8.7.0] - 2026-08-06
10113
10238
 
10114
10239
  **The check-up stops being quiet, and a dead option stops pretending.** 8.4.0 stopped a
10115
10240
  skill graph from throwing away what the author declared; 8.5.0 stopped it telling the
10116
10241
  model things that were not so. This one is about the configurations the library
10117
- *watched you build and said nothing about* — an entry menu with no way to choose from
10242
+ _watched you build and said nothing about_ — an entry menu with no way to choose from
10118
10243
  it, a transition the cursor can never take, a tool name two sources claim, a scoped
10119
10244
  tool provider that returns nothing forever. Nine findings, one shape: the library knew,
10120
10245
  and did not say.
@@ -10122,7 +10247,7 @@ and did not say.
10122
10247
  ### An entry menu with no way to choose from it
10123
10248
 
10124
10249
  Declare two entries and no `.entryBy()` / `.entryByRead()`, and both of them load on
10125
- every call. An entry's compiled trigger is cursor-*independent* — no `when` compiles to
10250
+ every call. An entry's compiled trigger is cursor-_independent_ — no `when` compiles to
10126
10251
  `{ kind: 'always' }` — while exactly ONE of them can be the cursor: the first whose
10127
10252
  `when` passes. So the extras pay for their body and their tools on every iteration and
10128
10253
  route nothing, which is the opposite of what a skill graph is for.
@@ -10168,7 +10293,7 @@ all versus only bare edges incoming.
10168
10293
 
10169
10294
  ### `unreachable-skill` is told per trigger kind
10170
10295
 
10171
- The sentence *"it can only be reached by the model via read_skill"* is true for an
10296
+ The sentence _"it can only be reached by the model via read_skill"_ is true for an
10172
10297
  `llm-activated` trigger and for no other kind — `Agent.openSkillIds()` admits an open
10173
10298
  pick only for that one. But `deriveTrigger` returns null for an unwired skill, so a
10174
10299
  skill that arrived carrying a hand-authored `rule` trigger **kept it**, and the warning
@@ -10238,7 +10363,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
10238
10363
  - **`ToolDispatchContext.activeSkillIds`** — the real active set for this iteration.
10239
10364
  Optional, so a provider written before 8.7.0 sees `undefined` and behaves as it did.
10240
10365
  - **`agentfootprint.tools.shadowed`** (71 typed events now) — `{ toolName, iteration,
10241
- schemaFrom, schemaFromId?, dispatchTo, dispatchToId? }`. Names only: never args, never
10366
+ schemaFrom, schemaFromId?, dispatchTo, dispatchToId? }`. Names only: never args, never
10242
10367
  results, never a description body.
10243
10368
  - **`skillScopedToolsTarget` / `SKILL_SCOPED_TOOLS_ID_PREFIX`** — the provider-id
10244
10369
  convention, readable by anyone composing providers.
@@ -10256,7 +10381,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
10256
10381
  object-literal form since 8.4.0. **Behavior change.** A fluent graph with an
10257
10382
  error-level problem — `no-entry` or `unknown-skill`, i.e. a graph that cannot start a
10258
10383
  turn at all — built in silence outside dev mode and surfaced as a run that entered no
10259
- skill. What still builds: every graph whose check-up has no *error* (warnings never
10384
+ skill. What still builds: every graph whose check-up has no _error_ (warnings never
10260
10385
  throw, however many); every call passing `check: 'warn'` explicitly, which still never
10261
10386
  throws, so the mode keeps its name and its meaning; `check: 'off'` skips entirely.
10262
10387
  Only code that was already shipping a graph the library could not start is affected.
@@ -10265,7 +10390,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
10265
10390
  activates the skill — and no such tool has ever been built. The evaluator activates an
10266
10391
  `llm-activated` skill by matching `ctx.activatedInjectionIds`, which only `read_skill`
10267
10392
  writes, and it has never read the field. A skill declaring `viaToolName:
10268
- 'open_playbook'` activated through `read_skill` exactly like every other skill, so the
10393
+ 'open_playbook'` activated through `read_skill` exactly like every other skill, so the
10269
10394
  declaration described a door that does not exist. Nothing that worked stops working;
10270
10395
  a silent no-op becomes a named one, at `Agent.injection()` — the one funnel `.skill()`,
10271
10396
  `.skills()`, `.skillGraph()`, `skillsFromDir()` and a hand-built Injection all pass
@@ -10338,7 +10463,7 @@ it raises `CredentialConsentRequiredError`.
10338
10463
  back `authorization-required`.
10339
10464
  - **`CredentialConsentRequiredError`** (`ERR_CREDENTIAL_CONSENT_REQUIRED`, from
10340
10465
  `agentfootprint/identity`) — carries `service`, `sessionId`, `authorizationUrl`,
10341
- `tool` and `iteration`. The error *message* deliberately omits the URL, because
10466
+ `tool` and `iteration`. The error _message_ deliberately omits the URL, because
10342
10467
  a message is the one string that reliably reaches a log line.
10343
10468
  - **`pauseData.authorization`** — `{ service, authorizationUrl, sessionId }` on a
10344
10469
  consent pause, surfaced by `standingAgent` as `PendingAsk.pauseData`. The
@@ -10366,7 +10491,7 @@ it raises `CredentialConsentRequiredError`.
10366
10491
  This is the mirror image of a guarantee the library kept carefully everywhere
10367
10492
  else. `agentfootprint.credential.authorization_required` was designed to carry
10368
10493
  `{ service, sessionId }` and never the URL; OTel and X-Ray record the tool
10369
- result's *type* and never its value; the audit bundle's default `bounded` mode
10494
+ result's _type_ and never its value; the audit bundle's default `bounded` mode
10370
10495
  maps `tool_end.result` to `[type: string]`. Every observer channel was
10371
10496
  disciplined. The one channel nobody thought of as an observer — the
10372
10497
  conversation — was not, and it feeds all the others.
@@ -10506,7 +10631,7 @@ that gave the middleware-ask outcome union no `result` arm.
10506
10631
  ## [8.5.0] - 2026-08-06
10507
10632
 
10508
10633
  **`read_skill` tells the whole truth.** 8.4.0 stopped a skill graph from throwing
10509
- away what the author declared. This one stops it from telling the *model* things
10634
+ away what the author declared. This one stops it from telling the _model_ things
10510
10635
  that were not so. Five findings, all of the same shape: the library said a thing had
10511
10636
  happened, or offered a thing it would refuse, or recorded a cause that was not the
10512
10637
  cause. One is a build-time refusal, one is a gate refusal, three are fixes.
@@ -10515,8 +10640,8 @@ cause. One is a build-time refusal, one is a gate refusal, three are fixes.
10515
10640
 
10516
10641
  A `tree()` routes by predicate on every iteration. It has no cursor, so `read_skill`
10517
10642
  has nothing to move. But `graph.reachableSkills()` reported **all the leaves**, so
10518
- the gate accepted a leaf pick and `read_skill` answered *"Skill 'x' activated for the
10519
- next iteration"* — and nothing happened. A leaf compiles to a `rule` trigger; a
10643
+ the gate accepted a leaf pick and `read_skill` answered _"Skill 'x' activated for the
10644
+ next iteration"_ — and nothing happened. A leaf compiles to a `rule` trigger; a
10520
10645
  `read_skill` call writes only `activatedInjectionIds`; no `rule` trigger reads that.
10521
10646
  The leaf never activated, the tree re-decided by predicate, and the run then emitted
10522
10647
  `agentfootprint.skill.reroute_superseded` naming a winner that **did not exist** —
@@ -10542,7 +10667,7 @@ though the tool accepted the name. Answer with the skill the tree routed to, or
10542
10667
  **Behavior change:** `graph.reachableSkills()` now returns `[]` for a decision
10543
10668
  `tree()`, from every cursor. Its contract is "what `read_skill` may jump to", and
10544
10669
  all-leaves was the lie; use `graph.skills` to enumerate leaves, which is what it was
10545
- always for. `read_skill` is not dead under a tree — anything registered *beside* the
10670
+ always for. `read_skill` is not dead under a tree — anything registered _beside_ the
10546
10671
  graph (`.skill(x)`, `.skills(reg)`, `.selfExplain()`) is **open** and still admitted
10547
10672
  from anywhere, because those really do activate by `read_skill`. Two docstrings that
10548
10673
  promised "read_skill stays a full escape hatch there" are corrected.
@@ -10568,7 +10693,7 @@ Not reachable from here (read_skill for these will be refused):
10568
10693
  ```
10569
10694
 
10570
10695
  **The enum stays the full catalog, deliberately.** `toolArgValidation` defaults to
10571
- `'enforce'` and runs *before* the gate; an off-enum id is rejected with a generic
10696
+ `'enforce'` and runs _before_ the gate; an off-enum id is rejected with a generic
10572
10697
  schema error and never reaches it. Narrowing the enum would therefore have retired
10573
10698
  the gate's teaching refusal, the `agentfootprint.skill.rejected` event,
10574
10699
  `routeRecorder`'s rejection hops and the rejected-cap governor's only input — four
@@ -10602,7 +10727,7 @@ Use 'both' (system prompt AND tool result) or 'system-prompt'.
10602
10727
  ```
10603
10728
 
10604
10729
  Refusal rather than a quiet fall back to the system slot: the author wrote
10605
- `'tool-only'` to keep the body *out* of the system prompt, and silently putting it
10730
+ `'tool-only'` to keep the body _out_ of the system prompt, and silently putting it
10606
10731
  back would honour the activation while breaking the declaration — a different lie,
10607
10732
  not a fix. `'both'` already means "deliver it either way".
10608
10733
 
@@ -10630,7 +10755,7 @@ drift:
10630
10755
  - `routeRecorder()` reads it, and a `'model-pick'` hop carries **no** `edgeLabel`.
10631
10756
 
10632
10757
  This settles the one case no observer could reconstruct: an edge and a same-turn pick
10633
- naming the *same* skill resolves to `'route'` (`D1 > D2`), and only the resolver
10758
+ naming the _same_ skill resolves to `'route'` (`D1 > D2`), and only the resolver
10634
10759
  knows. Without `cursorMove` (an older graph, an older recording) the previous
10635
10760
  inference still stands.
10636
10761
 
@@ -10665,11 +10790,11 @@ refusal on the agent, and the gate one is a fix, not a refusal.
10665
10790
  cannot move the graph somewhere the graph doesn't go. That set is about the CURSOR,
10666
10791
  but it was being used as the whole catalog, so three shapes were dead:
10667
10792
 
10668
- | you wrote | before | now |
10669
- |---|---|---|
10670
- | `.skillGraph(g).selfExplain()` | `read_skill('self-explain')` rejected on every call — the debug skill and its six trace tools could never load | activates; the trace tools reach the model on the next iteration |
10671
- | `.skillGraph(g).skill(x)` / `.skills(reg)` | `x` was listed in `read_skill`'s own menu and refused every time; its body was unreachable | activates |
10672
- | `skillGraph({ skills: [..., x] })` with `x` wired to nothing | refused — while its check-up warning said *"it can only be reached by the model via read_skill"* | activates; the warning is true again |
10793
+ | you wrote | before | now |
10794
+ | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
10795
+ | `.skillGraph(g).selfExplain()` | `read_skill('self-explain')` rejected on every call — the debug skill and its six trace tools could never load | activates; the trace tools reach the model on the next iteration |
10796
+ | `.skillGraph(g).skill(x)` / `.skills(reg)` | `x` was listed in `read_skill`'s own menu and refused every time; its body was unreachable | activates |
10797
+ | `skillGraph({ skills: [..., x] })` with `x` wired to nothing | refused — while its check-up warning said _"it can only be reached by the model via read_skill"_ | activates; the warning is true again |
10673
10798
 
10674
10799
  A skill is **open** when its trigger is `llm-activated` (the trigger `read_skill`
10675
10800
  actually activates — a rule-gated injection is still refused, because admitting it
@@ -10794,7 +10919,7 @@ dropped pick is reported rather than swallowed — see the new event below.
10794
10919
  ### `agentfootprint.skill.reroute_superseded` (new typed event — 70 total)
10795
10920
 
10796
10921
  Fires in exactly one case: a `read_skill` the gate accepted did not end up active
10797
- because a declared edge won the same turn (the model emitted a domain tool *and*
10922
+ because a declared edge won the same turn (the model emitted a domain tool _and_
10798
10923
  `read_skill` in one message). Payload: `{ volunteeredId, wonId, fromSkillId,
10799
10924
  iteration }`. It is derived from the real active set, not from which clause won,
10800
10925
  so it cannot fire for a pick that did take effect.
@@ -10838,7 +10963,7 @@ never engages any of this.
10838
10963
 
10839
10964
  **Durable compaction.** An agent that has been up for a week folds week one
10840
10965
  into a summary. Then it gets deployed over. It comes back, is handed the same
10841
- conversation — and now it can still tell you what week one was about, *and*
10966
+ conversation — and now it can still tell you what week one was about, _and_
10842
10967
  show you week one, word for word.
10843
10968
 
10844
10969
  The window half of that already worked: a summary is an ordinary message, so
@@ -10909,14 +11034,14 @@ surprise.
10909
11034
 
10910
11035
  ### New exports
10911
11036
 
10912
- | export | what it is |
10913
- | --- | --- |
10914
- | `foldedSpanFor(conversation, message)` | The span behind one summary, joined by **content fingerprint** rather than index — a later fold swallows an earlier summary and every index after it moves. `undefined` means "no fold was recorded for this message", never "there were no originals". |
10915
- | `foldedMessages(conversation)` | Every retained message from every span, oldest fold first. |
10916
- | `FoldedSpan` · `CompactionRetention` · `FoldedConversation` | The types. |
11037
+ | export | what it is |
11038
+ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11039
+ | `foldedSpanFor(conversation, message)` | The span behind one summary, joined by **content fingerprint** rather than index — a later fold swallows an earlier summary and every index after it moves. `undefined` means "no fold was recorded for this message", never "there were no originals". |
11040
+ | `foldedMessages(conversation)` | Every retained message from every span, oldest fold first. |
11041
+ | `FoldedSpan` · `CompactionRetention` · `FoldedConversation` | The types. |
10917
11042
 
10918
11043
  The fingerprint is also what makes the join **forgery-proof**: `isCompactedSummary`
10919
- answers "this *looks* like a frame", which is all a prefix check can see, and a
11044
+ answers "this _looks_ like a frame", which is all a prefix check can see, and a
10920
11045
  model that copies the frame's opening words passes it. `foldedSpanFor` answers
10921
11046
  the stronger question — different content, different fingerprint, no match.
10922
11047
 
@@ -10977,14 +11102,14 @@ openai({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' });
10977
11102
 
10978
11103
  Two things go wrong with a local runtime, and each has a one-command answer. Both
10979
11104
  now raise a typed `OllamaUnavailableError` (discriminated by `reason`) whose
10980
- message *is* the instruction — never a raw `ECONNREFUSED`, never a bare `404`,
11105
+ message _is_ the instruction — never a raw `ECONNREFUSED`, never a bare `404`,
10981
11106
  and never a hang: a deadline bounds the wait for the daemon to answer (not
10982
11107
  generation, so a slow model is untouched).
10983
11108
 
10984
11109
  - **Daemon not running** — names the address it tried, `ollama serve`, the
10985
11110
  install link, and how to point somewhere else.
10986
11111
  - **Model not pulled** — names `ollama pull <model>`, and asks `/api/tags` so it
10987
- can also list what this machine *does* have.
11112
+ can also list what this machine _does_ have.
10988
11113
 
10989
11114
  ### Also in this release
10990
11115
 
@@ -10993,7 +11118,7 @@ generation, so a slow model is untouched).
10993
11118
  compaction and cost budgets work against a local model.
10994
11119
  - **Thinking blocks from local reasoning models.** `ollama('deepseek-r1', { think: true })`
10995
11120
  asks Ollama to lift reasoning out of the answer; the new `ollamaThinkingHandler`
10996
- auto-wires by provider name and normalizes it. When a model was *not* asked and
11121
+ auto-wires by provider name and normalizes it. When a model was _not_ asked and
10997
11122
  writes `<think>…</think>` into the answer instead, the library **recognizes the
10998
11123
  shape and surfaces the blocks, but does not edit the answer** — silently
10999
11124
  rewriting model output is a change of meaning, and that belongs to the
@@ -11039,32 +11164,32 @@ The doors are now named for the job:
11039
11164
 
11040
11165
  **Every old import path still works, unchanged, for all of 8.x.** They are
11041
11166
  marked `@deprecated` so your editor points at the new door; nothing is logged,
11042
- nothing breaks, and each one re-exports the *same symbols* the door carries —
11167
+ nothing breaks, and each one re-exports the _same symbols_ the door carries —
11043
11168
  not copies. `test/api-conformance/door-aliases.test.ts` drives the TypeScript
11044
11169
  checker over the shipped `.d.ts` files to prove it, name by name, so the
11045
11170
  aliases cannot drift. They are removed in 9.0.0.
11046
11171
 
11047
11172
  ### Migration
11048
11173
 
11049
- | you were importing from | import from |
11050
- |---|---|
11051
- | `agentfootprint/llm-providers` | `agentfootprint/providers` |
11052
- | `agentfootprint/embedders` | `agentfootprint/providers` |
11053
- | `agentfootprint/tool-providers` | `agentfootprint/providers` |
11054
- | `agentfootprint/thinking` | `agentfootprint/providers` |
11055
- | `agentfootprint/memory-providers` | `agentfootprint/memory` |
11056
- | `agentfootprint/observability-providers` | `agentfootprint/observe` |
11057
- | `agentfootprint/strategies` | `agentfootprint/observe` |
11058
- | `agentfootprint/stream` | `agentfootprint/observe` |
11059
- | `agentfootprint/status` | `agentfootprint/observe` |
11060
- | `agentfootprint/locales` | `agentfootprint/observe` |
11061
- | `agentfootprint/debug` | `agentfootprint/observe` |
11062
- | `agentfootprint/debug/finders` | `agentfootprint/observe` |
11063
- | `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
11064
- | `agentfootprint/reliability` | `agentfootprint/resilience` |
11065
- | `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
11066
- | `agentfootprint/injection-engine` | `agentfootprint/context` |
11067
- | `agentfootprint/identity` | `agentfootprint/security` |
11174
+ | you were importing from | import from |
11175
+ | --------------------------------------------------- | --------------------------- |
11176
+ | `agentfootprint/llm-providers` | `agentfootprint/providers` |
11177
+ | `agentfootprint/embedders` | `agentfootprint/providers` |
11178
+ | `agentfootprint/tool-providers` | `agentfootprint/providers` |
11179
+ | `agentfootprint/thinking` | `agentfootprint/providers` |
11180
+ | `agentfootprint/memory-providers` | `agentfootprint/memory` |
11181
+ | `agentfootprint/observability-providers` | `agentfootprint/observe` |
11182
+ | `agentfootprint/strategies` | `agentfootprint/observe` |
11183
+ | `agentfootprint/stream` | `agentfootprint/observe` |
11184
+ | `agentfootprint/status` | `agentfootprint/observe` |
11185
+ | `agentfootprint/locales` | `agentfootprint/observe` |
11186
+ | `agentfootprint/debug` | `agentfootprint/observe` |
11187
+ | `agentfootprint/debug/finders` | `agentfootprint/observe` |
11188
+ | `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
11189
+ | `agentfootprint/reliability` | `agentfootprint/resilience` |
11190
+ | `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
11191
+ | `agentfootprint/injection-engine` | `agentfootprint/context` |
11192
+ | `agentfootprint/identity` | `agentfootprint/security` |
11068
11193
 
11069
11194
  `agentfootprint`, `agentfootprint/memory`, `agentfootprint/observe`,
11070
11195
  `agentfootprint/security`, `agentfootprint/hosting` and
@@ -11078,7 +11203,7 @@ aliases cannot drift. They are removed in 9.0.0.
11078
11203
  executing those registrations and carrying them in every bundle. Side-effectful
11079
11204
  code stays behind its own plainly-named door.
11080
11205
  - **`agentfootprint/events`** is not folded into `/observe`. It is the typed
11081
- wire vocabulary observers *read*, not a tool for watching — and concretely,
11206
+ wire vocabulary observers _read_, not a tool for watching — and concretely,
11082
11207
  its `ContextSource` (the injection-flavour union: `'rag' | 'skill' | …`) is a
11083
11208
  completely different type from the `ContextSource` `/observe` already carries
11084
11209
  (the context-bisect record). Two incompatible shapes cannot share a door.
@@ -11123,7 +11248,7 @@ runtime door, and it still returns the `Unsubscribe` you own.
11123
11248
  `CombinedRecorder` keeps its export too.
11124
11249
 
11125
11250
  There is deliberately **no** `WATCH_MOMENTS`. `.act()`'s keys are a closed,
11126
- compiler-pinned list because a rule has to be *told* where it may speak; an
11251
+ compiler-pinned list because a rule has to be _told_ where it may speak; an
11127
11252
  observer attends the whole stream, and a list we published would be a
11128
11253
  vocabulary we then had to keep true against every event ever added.
11129
11254
 
@@ -11159,7 +11284,7 @@ vocabulary we then had to keep true against every event ever added.
11159
11284
  ## [7.28.0] - 2026-08-05
11160
11285
 
11161
11286
  A paused agent is a promise you made to a person. Until this release the library
11162
- handed you that promise as JSON and wished you luck: *store it anywhere.*
11287
+ handed you that promise as JSON and wished you luck: _store it anywhere._
11163
11288
  Anywhere was the whole of the offer.
11164
11289
 
11165
11290
  `sqliteSessions({ file })` is the first battery included — the same
@@ -11182,7 +11307,7 @@ and the next step up was "bring a Redis" — a service to run, secure, back up a
11182
11307
  pay for, to keep a few kilobytes of chat. Everyone in between wrote the same
11183
11308
  little file store themselves and each one re-decided what a half-written file
11184
11309
  means. A pause had no home at all: a question outstanding is the one piece of
11185
- agent state that *must* outlive the process, because the answer arrives on human
11310
+ agent state that _must_ outlive the process, because the answer arrives on human
11186
11311
  time — after lunch, after the deploy, tomorrow. Both land in one table here,
11187
11312
  because `CheckpointEnvelope` was already a union of the two and a session store
11188
11313
  has no business caring which half it is holding.
@@ -11192,8 +11317,7 @@ process (or a few) on ONE machine, writing ONE file. It survives anything that
11192
11317
  ends the process and leaves the disk alone. It is **not** a distributed store:
11193
11318
  two machines do not share a session by both opening a file over a network
11194
11319
  filesystem. WAL gives many readers plus **one writer at a time**, and that is
11195
- the ceiling — a second writer waits for the lock up to `busyTimeoutMs` (default
11196
- 5000) and then fails loudly rather than queueing forever. When you outgrow it,
11320
+ the ceiling — a second writer waits for the lock up to `busyTimeoutMs` (default 5000) and then fails loudly rather than queueing forever. When you outgrow it,
11197
11321
  one argument to `standingAgent` changes and nothing above it moves.
11198
11322
 
11199
11323
  **A refusal where a fallback would have been easier.** `node:sqlite` ships with
@@ -11209,7 +11333,7 @@ user.
11209
11333
  **"Unreadable is not absent", one level up.** The envelope law already said an
11210
11334
  unreadable stored conversation and an absent one are different facts, and only
11211
11335
  one is safe to answer with a fresh start. A file store can break that promise
11212
- higher up — point it at a log file and a careless adapter opens it as an *empty*
11336
+ higher up — point it at a log file and a careless adapter opens it as an _empty_
11213
11337
  store. So the file is checked at construction and refused with
11214
11338
  `UnreadableSessionFileError`, whose `problem` field is the fact to branch on:
11215
11339
  `'cannot-open'`, `'not-our-schema'` (somebody else's table of that name), or
@@ -11220,7 +11344,7 @@ session that was never written hydrates as `undefined`.
11220
11344
  well as fields inside the JSON, so during an incident `sqlite3` answers "which
11221
11345
  sessions are waiting on a person, and since when?" with no JSON parser and
11222
11346
  without this library. `journalMode` on the returned store reports what the file
11223
- *actually got* rather than what was asked for — a silent downgrade from WAL on a
11347
+ _actually got_ rather than what was asked for — a silent downgrade from WAL on a
11224
11348
  network filesystem is the kind of thing only ever discovered under load.
11225
11349
 
11226
11350
  Added, all on the existing `agentfootprint/hosting` door — no new subpath:
@@ -11252,7 +11376,7 @@ the machine took, and the machine is shared: the suite runs beside a build, a
11252
11376
  coverage pass, and two other vitest workers. Identical code takes three to five
11253
11377
  times longer under that load with nothing about the code having changed. So the
11254
11378
  assertion cannot tell "we got slower" from "the box was busy", and it fires
11255
- *exactly* when CI is busiest. Five of them had already been logged as flakes —
11379
+ _exactly_ when CI is busiest. Five of them had already been logged as flakes —
11256
11380
  `xray` P6, `withCircuitBreaker` P6, `locales/messages` Block D,
11257
11381
  `consumer-domain-events`, `SkillRegistryOptions` — always under concurrent
11258
11382
  build load, always passing in isolation. The failure mode of a guard nobody
@@ -11296,7 +11420,7 @@ still flaked when actually run under the reproduction condition. Three things
11296
11420
  had to be added, each because the proof run said so:
11297
11421
 
11298
11422
  - **Repeat until the sample is worth timing.** Below a scheduler quantum, one
11299
- preemption *is* the measurement: a 0.2ms operation that gets descheduled
11423
+ preemption _is_ the measurement: a 0.2ms operation that gets descheduled
11300
11424
  reads as a hundred times its real cost, while the 100ms operation beside it
11301
11425
  absorbs the same theft as a rounding error — and load stops cancelling. Each
11302
11426
  operation is now repeated inside one sample until the sample clears 20ms, and
@@ -11328,8 +11452,8 @@ load, which is what makes them worth asserting.
11328
11452
  **No perf claim was deleted. The form changed; the meaning stayed** — and in
11329
11453
  four places the meaning got sharper, because writing the claim down properly
11330
11454
  exposed what it had actually been asserting. Three sites keep a millisecond
11331
- ceiling on purpose and say so at the site: they are stated against a *configured
11332
- delay* (a mock's own thinking band, a slow branch's own timeout, a strategy's
11455
+ ceiling on purpose and say so at the site: they are stated against a _configured
11456
+ delay_ (a mock's own thinking band, a slow branch's own timeout, a strategy's
11333
11457
  own per-event block) rather than against a guess about the machine, because
11334
11458
  "did not sleep longer than it was told to" has no cheaper form.
11335
11459
 
@@ -11485,8 +11609,8 @@ each of those is now the failure of the thing that caused it. A 400, a 500, one
11485
11609
  refused upgrade, one ended conversation. Never the process.
11486
11610
 
11487
11611
  One of them was not even a throw. `serveOne`'s promise is held in a Set and
11488
- voided at the call site, so anything that escaped it was an *unhandled
11489
- rejection* — which on node's defaults is the same dead container reached by a
11612
+ voided at the call site, so anything that escaped it was an _unhandled
11613
+ rejection_ — which on node's defaults is the same dead container reached by a
11490
11614
  different road. It is total by construction now, and says so.
11491
11615
 
11492
11616
  **The audit's other finding is that the conversation door was already safe, and
@@ -11503,17 +11627,16 @@ serve. It is a great deal of ceremony when all you wanted was a `/debug/trace`
11503
11627
  beside the agent on the one port the container was given.
11504
11628
 
11505
11629
  ```ts
11506
- nodeHost({ port: 8080, onUnhandled: (req, res) => myRouter(req, res) })
11630
+ nodeHost({ port: 8080, onUnhandled: (req, res) => myRouter(req, res) });
11507
11631
  ```
11508
11632
 
11509
11633
  Same single port, opposite direction: the host binds the socket as it always
11510
- did, and every path it does not own is handed to your code **instead of** its
11511
- 404. The host still never answers for your application — with this hook it no
11634
+ did, and every path it does not own is handed to your code **instead of** its 404. The host still never answers for your application — with this hook it no
11512
11635
  longer has to 404 for it either.
11513
11636
 
11514
11637
  What it never receives is the interesting half. The paths the host owns —
11515
- `invokePath`, `healthPath`, `conversationPath` — never reach it, *including a
11516
- wrong method on one of them*, because a hook that could claim `POST /invoke`
11638
+ `invokePath`, `healthPath`, `conversationPath` — never reach it, _including a
11639
+ wrong method on one of them_, because a hook that could claim `POST /invoke`
11517
11640
  would be a second door wearing the first one's name. And it is refused at
11518
11641
  construction beside `{ server }`, by name: there, unmatched paths already fall
11519
11642
  through to your own `'request'` listeners, so a second way to answer them would
@@ -11560,7 +11683,7 @@ hosts rather than by care.
11560
11683
  chunks are coerced back to bytes, and no bytes are lost doing it: `setEncoding`
11561
11684
  decodes through a `StringDecoder`, which holds a partial multi-byte sequence
11562
11685
  across a chunk boundary rather than splitting it. Pinned by writing a body in
11563
- two TCP writes with the split placed *inside* a four-byte character and
11686
+ two TCP writes with the split placed _inside_ a four-byte character and
11564
11687
  asserting it round-trips. Reachable only through `{ server }` — the mode built
11565
11688
  for co-listeners — and reproduced there, with a real second listener on a real
11566
11689
  shared socket rather than a stubbed request.
@@ -11649,7 +11772,7 @@ avoid admitting the cost would have been an accounting trick.
11649
11772
  **The corrective message is an authored frame with the validator's error as
11650
11773
  DATA.** The library's own words come first and say that what follows is a
11651
11774
  report about the answer rather than an instruction; the error is quoted
11652
- verbatim; and *nothing authored follows it*, so there is no trailing sentence
11775
+ verbatim; and _nothing authored follows it_, so there is no trailing sentence
11653
11776
  for injected text to pre-empt. A schema whose error message reads "IGNORE ALL
11654
11777
  PREVIOUS INSTRUCTIONS" produces a message that still says, first and in the
11655
11778
  library's voice, what it is. This is exactly the compaction frame's rule
@@ -11728,7 +11851,7 @@ bytes — pinned against 7.25 by test, not by care.
11728
11851
 
11729
11852
  - **`agentfootprint.agent.output_schema_retry`** — one per failed attempt,
11730
11853
  carrying `{ attempt, retriesRemaining, iteration, stage, error, path?,
11731
- correctiveMessageHash }`. 69 typed events across 20 domains. It sits in the
11854
+ correctiveMessageHash }`. 69 typed events across 20 domains. It sits in the
11732
11855
  `agent` domain beside `output_schema_validation_failed`, its in-stage
11733
11856
  sibling; a new domain for one event that has a family home would have been
11734
11857
  taxonomy for its own sake.
@@ -11789,8 +11912,9 @@ side can call. So this release ships a second port beside the first.
11789
11912
  ```ts
11790
11913
  const host = nodeHost({ port: 8080 });
11791
11914
 
11792
- await standingAgent({ agent, sessions, host }); // POST /invoke
11793
- await host.serveConversations((conversation) => { // WS /conversation
11915
+ await standingAgent({ agent, sessions, host }); // POST /invoke
11916
+ await host.serveConversations((conversation) => {
11917
+ // WS /conversation
11794
11918
  conversation.onFrame((frame) => conversation.send(answer(frame)));
11795
11919
  conversation.onClose(({ by, reason }) => log(by, reason));
11796
11920
  });
@@ -11959,11 +12083,11 @@ So this release does two things, and the second is the reason for the first.
11959
12083
  ```ts
11960
12084
  Agent.create({ provider, model })
11961
12085
  .act({
11962
- input: [scrubSSNs], // the message, before the run commits it
11963
- beforeTool: [refundCeiling, fourEyes], // every call, before it is dispatched
11964
- afterTool: [stripPII], // every result, before the model reads it
11965
- window: slidingWindow({ keepRecentTurns: 12 }), // what the live window keeps
11966
- output: [noCodenames], // the answer, before the caller gets it
12086
+ input: [scrubSSNs], // the message, before the run commits it
12087
+ beforeTool: [refundCeiling, fourEyes], // every call, before it is dispatched
12088
+ afterTool: [stripPII], // every result, before the model reads it
12089
+ window: slidingWindow({ keepRecentTurns: 12 }), // what the live window keeps
12090
+ output: [noCodenames], // the answer, before the caller gets it
11967
12091
  })
11968
12092
  .build();
11969
12093
  ```
@@ -11977,8 +12101,8 @@ wire, same rows in the ledger.
11977
12101
 
11978
12102
  The canonical path is preserved by **demoting the doors, not deleting them**.
11979
12103
  `.toolMiddleware()`, `.messageMiddleware()`, `.window()` and `.compaction()`
11980
- are unchanged and stay open, and they are now documented under *Composing
11981
- incrementally* — because adding one rule to an agent somebody else built is a
12104
+ are unchanged and stay open, and they are now documented under _Composing
12105
+ incrementally_ — because adding one rule to an agent somebody else built is a
11982
12106
  real job, and a bundle that must be written all at once cannot do it. That
11983
12107
  division is the one-sentence answer to "which spelling": **`.act()` for an
11984
12108
  agent you own, a door for a piece you are adding to somebody else's.** A second
@@ -12227,8 +12351,8 @@ the honest pick is the one whose bytes come back unchanged.
12227
12351
  **The law, which is the part that outlives this vendor.** In the words of the
12228
12352
  field report that bought it:
12229
12353
 
12230
- > *An unreadable stored conversation and an absent one are different facts, and
12231
- > only one of them is safe to answer with a fresh start.*
12354
+ > _An unreadable stored conversation and an absent one are different facts, and
12355
+ > only one of them is safe to answer with a fresh start._
12232
12356
 
12233
12357
  A session nobody has used is absent, and answering it fresh is right. A session
12234
12358
  whose bytes are present and unreadable is not, and answering THAT fresh is
@@ -12246,7 +12370,7 @@ through to the fresh-start path.
12246
12370
  meant reading how this repo writes an event blob, and `AgentCoreStore` — the
12247
12371
  `MemoryStore` adapter — wrote them identically: `payload: [{ blob: entry }]`,
12248
12372
  read back as objects only. Same service, same mangling, same silence, different
12249
- loss: an entry that decodes to nothing was *skipped*, so `list()` came back one
12373
+ loss: an entry that decodes to nothing was _skipped_, so `list()` came back one
12250
12374
  memory short and `get()` came back `null`. Memory that silently stays empty is
12251
12375
  indistinguishable from memory that works, until somebody notices the assistant
12252
12376
  has forgotten a customer's address. Shipping the cure for one organ while the
@@ -12289,8 +12413,8 @@ and are still described that way.
12289
12413
  refused loudly rather than decoded to `undefined`.
12290
12414
 
12291
12415
  - **`hydrate` no longer answers "no session" for a session that HAS one.** The
12292
- adapter's decode step now distinguishes *no blob at all* (an absence, which
12293
- hydrates as `undefined`) from *a blob it cannot read* (which travels on to the
12416
+ adapter's decode step now distinguishes _no blob at all_ (an absence, which
12417
+ hydrates as `undefined`) from _a blob it cannot read_ (which travels on to the
12294
12418
  shared reading law and is refused by name). Both file and event modes pass the
12295
12419
  session id into `checkEnvelope`, so a refusal names the conversation.
12296
12420
 
@@ -12439,7 +12563,7 @@ than by an opinion.** See below.
12439
12563
  satisfy the container contract this repo documents for a managed agent
12440
12564
  runtime, and is not trying to.** It serves MCP — statelessly (it neither
12441
12565
  issues nor demands a session id, so replicas are interchangeable), on the path
12442
- and port you choose — and it answers *neither* of the contract's two routes:
12566
+ and port you choose — and it answers _neither_ of the contract's two routes:
12443
12567
  `GET /ping` and `POST /invocations` are 404s from it. Two protocols, two
12444
12568
  paths, two adapters: serve the container contract with
12445
12569
  `agentCoreRuntimeHost` and MCP with `mcpServe`. The one thing that is NOT
@@ -12454,7 +12578,7 @@ The refusal becomes acceptance.
12454
12578
  Three releases ago the messages slot was a lie: content declared for it was
12455
12579
  recorded as injected, counted in the slot composition, routed by the engine, and
12456
12580
  never sent. 7.19.1 refused the declaration by name rather than deliver it badly,
12457
- and said why in the same breath — the wire has no system role *inside* the
12581
+ and said why in the same breath — the wire has no system role _inside_ the
12458
12582
  message list on the Anthropic family (system is a separate top-level field)
12459
12583
  while the OpenAI family carries it, so wiring the slot straight through would
12460
12584
  have replaced one uniform gap with a **provider-dependent** one that nothing in
@@ -12479,7 +12603,7 @@ refused when the run starts, naming the provider and the roles it does. The role
12479
12603
  is never rewritten to one that fits — changing who appears to speak is a meaning
12480
12604
  change the app must make, not the library. **Position**: a delivered message goes
12481
12605
  at the end of the window, and if its role would repeat the turn already there, it
12482
- is *deferred* to the next boundary with a sentence on
12606
+ is _deferred_ to the next boundary with a sentence on
12483
12607
  `messagesDelivery.deferred`, never dropped and never reordered, and never
12484
12608
  inserted between a tool call and its result.
12485
12609
 
@@ -12535,7 +12659,7 @@ end. An honest limitation stated loudly beats a clever one hidden.
12535
12659
  ### Fixed
12536
12660
 
12537
12661
  - **The cache marker for `field: 'messages'` pointed at the wrong message.** It
12538
- counted entries in a per-slot list of *injections* and handed that count to
12662
+ counted entries in a per-slot list of _injections_ and handed that count to
12539
12663
  providers who read it as a position in `request.messages` — two index spaces
12540
12664
  under one name. It was unreachable while nothing could target the slot, and
12541
12665
  delivery makes it reachable, so it is recomputed against the actual wire array
@@ -12623,7 +12747,6 @@ end. An honest limitation stated loudly beats a clever one hidden.
12623
12747
  request's tool array. It is out of this release's scope, and it is now the only
12624
12748
  one of the pair left.
12625
12749
 
12626
-
12627
12750
  ## [7.20.0] - 2026-08-03
12628
12751
 
12629
12752
  Three small honesty fixes. No new machinery, no delivery change, no wire bytes
@@ -12733,7 +12856,7 @@ message-sequence rule; it is queued as a feature, with this gap as its evidence.
12733
12856
  / `.instruction` / `.fact` all pass through — refuses a hand-built `Injection`
12734
12857
  carrying `inject.messages`, so the refusal cannot be walked around. The
12735
12858
  message names the limitation and the working alternatives: `slot:
12736
- 'system-prompt'` (the default, delivered by every provider), a tool's return
12859
+ 'system-prompt'` (the default, delivered by every provider), a tool's return
12737
12860
  value (a tool result IS a recent message, at the recency the option was
12738
12861
  reaching for), and the text passed to `agent.run({ message })`.
12739
12862
 
@@ -12941,7 +13064,7 @@ Example: `examples/deploy/durable-sessions.ts`.
12941
13064
  ## [7.18.0] - 2026-08-03
12942
13065
 
12943
13066
  Every agent framework lets you wrap a tool call. Most of them let the wrapper
12944
- *answer* — return a canned string, a cached value, a "simulated" result — and
13067
+ _answer_ — return a canned string, a cached value, a "simulated" result — and
12945
13068
  the moment one does, the trace is fiction. The model was told a tool ran.
12946
13069
  Nothing ran.
12947
13070
 
@@ -13000,7 +13123,7 @@ taken afterwards: the trace would show text nobody ever sent.
13000
13123
 
13001
13124
  - **`ask` suspends on the SHIPPED pause machinery.** `isAskPause(outcome)`
13002
13125
  narrows a paused run and `outcome.ask` carries `{ question, detail?,
13003
- middleware }`. Resume with `checkInApproved` / `checkInDeclined` — the same
13126
+ middleware }`. Resume with `checkInApproved` / `checkInDeclined` — the same
13004
13127
  human-answer vocabulary check-ins use, deliberately, because a person
13005
13128
  approving is a person approving and one word for one thing beats a synonym.
13006
13129
  A malformed resume DECLINES, so a governed call can never execute because a
@@ -13137,7 +13260,7 @@ measured lifetime. Removing is not forgetting.
13137
13260
  token budget, and reporting a `capTokens` nobody configured would be exactly
13138
13261
  the invented number this family refuses.
13139
13262
 
13140
- `keepRecentTurns` is required and has no default. It *is* the policy.
13263
+ `keepRecentTurns` is required and has no default. It _is_ the policy.
13141
13264
 
13142
13265
  - **`tokenBudget({ thresholdTokens, keepRecentTurns? })` — counted, then
13143
13266
  dropped.** Reads the input tokens the adapter reported for the last call and
@@ -13185,7 +13308,7 @@ measured lifetime. Removing is not forgetting.
13185
13308
  It appears only at the head (a removal in the middle leaves the opening turn
13186
13309
  in place, so there is nothing to fix and a spliced `user` message is its own
13187
13310
  risk); it never accumulates (the next drop absorbs it); and if it would not
13188
- be *smaller* than the span it replaces, the whole drop is abandoned under
13311
+ be _smaller_ than the span it replaces, the whole drop is abandoned under
13189
13312
  `summary-not-smaller`, whose meaning generalizes to "the replacement came
13190
13313
  back no smaller than the span" rather than growing the closed reason union.
13191
13314
 
@@ -13279,7 +13402,7 @@ arrived yet.
13279
13402
  Example: `examples/context-engineering/11-compaction.ts`.
13280
13403
 
13281
13404
  - **`CompactionRecord` on `scope.compactions` — the fold's half of the law.**
13282
- One record per over-budget visit, *including the visits that folded nothing*,
13405
+ One record per over-budget visit, _including the visits that folded nothing_,
13283
13406
  which are the interesting ones. It carries `foldedStageIds` (real
13284
13407
  `runtimeStageId`s, resolvable in the commit log), `foldedMessageCount`,
13285
13408
  `measuredTokens` vs `thresholdTokens`, exact `windowCharsBefore` /
@@ -13303,7 +13426,7 @@ arrived yet.
13303
13426
  - **`COMPACTED_FRAME_PREFIX` / `isCompactedSummary(msg)`** — the authored frame
13304
13427
  is a library constant and the summarizer's text is appended after it as data.
13305
13428
  A summarizer returning `IGNORE ALL PREVIOUS INSTRUCTIONS` still arrives
13306
- *inside* a message that says, first and in the library's own words, that what
13429
+ _inside_ a message that says, first and in the library's own words, that what
13307
13430
  follows is a summary written by a model and not the conversation. A test pins
13308
13431
  exactly that, with a hostile summarizer. The boundary points both ways: the
13309
13432
  folded transcript reaches the summarizer between markers the authored
@@ -13333,7 +13456,7 @@ arrived yet.
13333
13456
  - **With `.compaction()` configured, the compaction stage becomes the ReAct
13334
13457
  loop target** (`compact`), mounted immediately before the previous one. The
13335
13458
  loop is branch-sourced, so anything ahead of the target runs once and is
13336
- never seen again — and being the target puts the fold *before* the injection
13459
+ never seen again — and being the target puts the fold _before_ the injection
13337
13460
  engine and the three context slots, which is the point: the triggers, the
13338
13461
  slots and the wire then all see one window, and no part of the run reasons
13339
13462
  over a past the model was not shown. Without `.compaction()` the loop target
@@ -13350,9 +13473,9 @@ arrived yet.
13350
13473
  ## [7.15.0] - 2026-08-02
13351
13474
 
13352
13475
  7.14.0 shipped two hosting ports that name no cloud, plus a conformance suite,
13353
- and made a promise: *a cloud adapter is vendor paths and a header mapping on a
13476
+ and made a promise: _a cloud adapter is vendor paths and a header mapping on a
13354
13477
  port that already worked; if writing one needs a change to a port, the port was
13355
- wrong.* A promise like that is worth nothing until somebody writes the adapter.
13478
+ wrong._ A promise like that is worth nothing until somebody writes the adapter.
13356
13479
 
13357
13480
  This release writes it. `agentCoreRuntimeHost` is a real cloud runtime's
13358
13481
  container contract — different paths, different body fields, the conversation id
@@ -13366,8 +13489,8 @@ policy store behind the existing permission port, per-request credential vending
13366
13489
  for Gateway tools, and the memory adapter's `search()` finally wired.
13367
13490
 
13368
13491
  Three seams did have to move, and none of them was a port. They are listed under
13369
- "Changed" rather than buried, because *where an adapter needs more than paths
13370
- and headers* is the interesting result of an exercise like this — and two of the
13492
+ "Changed" rather than buried, because _where an adapter needs more than paths
13493
+ and headers_ is the interesting result of an exercise like this — and two of the
13371
13494
  three turned out not to be about this vendor at all.
13372
13495
 
13373
13496
  ### Added
@@ -13399,7 +13522,7 @@ three turned out not to be about this vendor at all.
13399
13522
  Example: `examples/deploy/agentcore-runtime.ts`.
13400
13523
 
13401
13524
  - **`httpHost({ name, wire, invokePath, healthPath, port?, hostname?,
13402
- capabilities? })` — the HTTP work, parameterised by the JSON dialect it
13525
+ capabilities? })` — the HTTP work, parameterised by the JSON dialect it
13403
13526
  speaks.** Draining on close, aborting when the caller hangs up, failing a
13404
13527
  handler that throws, failing a handler that answers nothing, mapping refusal
13405
13528
  codes to status codes, and choosing between one JSON body and Server-Sent
@@ -13414,7 +13537,7 @@ three turned out not to be about this vendor at all.
13414
13537
  matching and gets it subtly wrong in exactly one deployment.
13415
13538
 
13416
13539
  - **`agentCorePolicy({ policyStoreId, region?, onUnavailable?, onWarning?,
13417
- principalFor?, name?, cacheSize? })` — an AgentCore policy store behind the
13540
+ principalFor?, name?, cacheSize? })` — an AgentCore policy store behind the
13418
13541
  existing `PermissionChecker` port** (`agentfootprint/security`). Every
13419
13542
  attempted tool call becomes one evaluation.
13420
13543
 
@@ -13462,7 +13585,7 @@ three turned out not to be about this vendor at all.
13462
13585
 
13463
13586
  Results are marked `metadata.source: 'agentcore-memory-record'`, because
13464
13587
  `search` reads a genuinely different population than `list`: the records
13465
- AgentCore's extraction strategies *derived from* your events, whose ids belong
13588
+ AgentCore's extraction strategies _derived from_ your events, whose ids belong
13466
13589
  to AgentCore, so `store.get(result.entry.id)` will not find them.
13467
13590
 
13468
13591
  There is **no `stream()`**. AgentCore Memory has no streaming data-plane
@@ -13579,7 +13702,7 @@ and if writing one ever needs a change to a port, the port was wrong.
13579
13702
  - **`standingAgent({ agent, sessions, host, onConcurrentInvoke? })` — the
13580
13703
  composer.** Per request: wake and hydrate the session, resume that
13581
13704
  conversation or start a fresh one, persist what the run left behind, reply.
13582
- Persist happens *before* the answer goes out, so a queued next turn can never
13705
+ Persist happens _before_ the answer goes out, so a queued next turn can never
13583
13706
  read state older than the answer already given.
13584
13707
 
13585
13708
  It restates the `resumeOnError` tool re-execution caveat **verbatim** in its
@@ -13593,7 +13716,7 @@ and if writing one ever needs a change to a port, the port was wrong.
13593
13716
  afterwards belongs to whichever started last, so one session's envelope can end
13594
13717
  up holding another session's conversation with nothing in the recording to say
13595
13718
  so. `ConcurrentInvokePolicy` is the separate question of a second turn of the
13596
- *same* conversation: `'reject'` (default) refuses with a `ConcurrentRunError`
13719
+ _same_ conversation: `'reject'` (default) refuses with a `ConcurrentRunError`
13597
13720
  naming the active run (`409`), `'enqueue'` queues it FIFO behind the run whose
13598
13721
  state it will then read. A request for a **different** session is never
13599
13722
  refused — it waits its turn.
@@ -13634,7 +13757,7 @@ person something, `standingAgent` answers with a `PauseNotCarriedError` and
13634
13757
  writes **nothing** — the session keeps exactly the conversation it had before the
13635
13758
  request. Over HTTP that is a `409`, not a `500`, because the agent did not break
13636
13759
  and every dashboard that sees a 500 will conclude otherwise. `'conversation-v1'`
13637
- stores a conversation; a paused run is a conversation *plus* an engine
13760
+ stores a conversation; a paused run is a conversation _plus_ an engine
13638
13761
  checkpoint, and storing half of it would be worse than storing none. Carrying a
13639
13762
  pause would be a NEW format name in the same envelope — which is precisely what
13640
13763
  the version in the format is for.
@@ -13649,7 +13772,7 @@ host can send any string there, including someone else's.
13649
13772
 
13650
13773
  **The conformance suite is the deliverable, not the tests for it.** One handler
13651
13774
  constant, served by `nodeHost` and by a minimal in-process host that declares
13652
- *no* capabilities so the buffering path is exercised rather than assumed, with a
13775
+ _no_ capabilities so the buffering path is exercised rather than assumed, with a
13653
13776
  final pair of cases invoking both and comparing directly. A future adapter —
13654
13777
  including a cloud one — is measured against that file.
13655
13778
 
@@ -13677,7 +13800,7 @@ other way, and a value committed where run-level values already commit.
13677
13800
  and the body still arrives only after it does. What changes is who can edit a
13678
13801
  playbook, and whether changing the refund policy shows up as a reviewable diff.
13679
13802
 
13680
- A skill body is *instructions to a model*, so where it came from is a security
13803
+ A skill body is _instructions to a model_, so where it came from is a security
13681
13804
  property rather than a convenience — content fetched at run time is content
13682
13805
  someone else can change after you reviewed it. The loader therefore accepts a
13683
13806
  local directory and nothing else: a URL is **refused by name**, not fetched.
@@ -13748,7 +13871,7 @@ other way, and a value committed where run-level values already commit.
13748
13871
 
13749
13872
  - **`McpClientOptions.signal` cancels a hung MCP tool call again — it never
13750
13873
  did.** The signal was being sent as part of the `tools/call` request
13751
- *params*, where an `AbortSignal` JSON-serializes to `{}`: the server received
13874
+ _params_, where an `AbortSignal` JSON-serializes to `{}`: the server received
13752
13875
  a meaningless field and the caller received no cancellation. The SDK takes
13753
13876
  per-request options in a separate trailing argument, which is where the signal
13754
13877
  now goes; it is threaded to `connect()` and `listTools()` for the same reason,
@@ -13787,7 +13910,7 @@ synchronous, and making it async to accommodate a resolver would shift every
13787
13910
  agent's timing for a feature most agents do not use. A resolver that needs I/O
13788
13911
  can do it before `run()` and close over the result.
13789
13912
 
13790
- Every debugging session starts at a *variable* — "where did that instruction come
13913
+ Every debugging session starts at a _variable_ — "where did that instruction come
13791
13914
  from?", "which loop wrote the history it answered from?" — and both halves of the
13792
13915
  answer already existed, in vocabularies that did not meet. footprintjs 9.13 records
13793
13916
  a variable's whole life in commit indices and runtimeStageIds; the localizer thinks
@@ -13798,7 +13921,7 @@ Joining them turned out to buy something bigger than a nicer read-out. The backw
13798
13921
  walk narrows each loop with embedding similarity — a proxy that points at a
13799
13922
  neighbourhood and cannot separate a planted instruction from an innocent same-topic
13800
13923
  sibling. But where the recording carries per-write provenance, one part of that guess
13801
- is unnecessary: the commit log *says* which write produced the value this loop read.
13924
+ is unnecessary: the commit log _says_ which write produced the value this loop read.
13802
13925
  So the walk stops guessing exactly there — and keeps saying so everywhere else.
13803
13926
 
13804
13927
  ### Added
@@ -13826,7 +13949,7 @@ So the walk stops guessing exactly there — and keeps saying so everywhere else
13826
13949
  same hops, same order, same verdicts (pinned by a deep-equal test).
13827
13950
 
13828
13951
  The proxy still picks WHO; dataflow picks WHERE. A stage-level edge never becomes
13829
- an exact hop, and a recorded edge outranks the *inferred* proximate-tool hop —
13952
+ an exact hop, and a recorded edge outranks the _inferred_ proximate-tool hop —
13830
13953
  better evidence wins, and the hop record says which kind it used.
13831
13954
 
13832
13955
  - **`AgentOptions.writeProvenance`** (`'off'` default, `'reads-prefix'` to enable) —
@@ -13843,7 +13966,7 @@ So the walk stops guessing exactly there — and keeps saying so everywhere else
13843
13966
  `coverage: 'exact'` requires **positive** evidence — at least one recorded per-write
13844
13967
  edge — not merely the absence of a conservative one. A key nothing ever reads back
13845
13968
  (the agent's `lastToolResult`: written by tool-calls, never read by `call-llm`) has
13846
- an empty edge set, so "no conservative edges" is *vacuously* true; scoring that as
13969
+ an empty edge set, so "no conservative edges" is _vacuously_ true; scoring that as
13847
13970
  exact would hand the walk its most confident hop on its least-evidenced key. Absence
13848
13971
  of dataflow is `'unknown'`, never exactness.
13849
13972
 
@@ -13867,7 +13990,7 @@ it with measurements instead of re-deriving why it was strict.
13867
13990
 
13868
13991
  ## [7.11.0] - 2026-08-02
13869
13992
 
13870
- A pipeline whose steps form a *shape* rather than a line — one step feeding two
13993
+ A pipeline whose steps form a _shape_ rather than a line — one step feeding two
13871
13994
  independent lookups, a third waiting for both — had no home here. You could nest
13872
13995
  a `Parallel` inside a `Sequence`, but then you were scheduling it by hand, and
13873
13996
  the values did not survive the trip.
@@ -13939,7 +14062,7 @@ switches keep compiling. Same reasoning as `workflow()` in 7.10.0.
13939
14062
  ## [7.10.0] - 2026-08-02
13940
14063
 
13941
14064
  Two routing-shaped gaps closed. Both were things the docs told you to hand-roll,
13942
- and both were fiddly in the same way: the wiring is easy to get *nearly* right,
14065
+ and both were fiddly in the same way: the wiring is easy to get _nearly_ right,
13943
14066
  and nearly right fails quietly — at run time, several steps away from the
13944
14067
  mistake.
13945
14068
 
@@ -13948,7 +14071,7 @@ mistake.
13948
14071
  - **`llmRouter` — the classic Swarm decision, packaged.** `swarm()` asks for a
13949
14072
  `route()` that is sync and pure, and it means it: the `Conditional` evaluates
13950
14073
  it once per branch predicate and the loop's exit guard evaluates it again
13951
- after every turn. So the LLM decision has to happen *somewhere else*, before
14074
+ after every turn. So the LLM decision has to happen _somewhere else_, before
13952
14075
  the message reaches `route` — and that placement is the part everyone
13953
14076
  re-invented, along with the prompt, the parsing, and a second copy of the
13954
14077
  agent roster that drifts from the first.
@@ -13962,8 +14085,8 @@ mistake.
13962
14085
  with a stale decision.
13963
14086
 
13964
14087
  The decision is validated JSON — `RoutingDecision` = `{ agentId?, message,
13965
- reason? }`. No `agentId` means "done", and the swarm halts through its own
13966
- halt sentinel. An id that isn't in the roster is kept verbatim, *not* quietly
14088
+ reason? }`. No `agentId` means "done", and the swarm halts through its own
14089
+ halt sentinel. An id that isn't in the roster is kept verbatim, _not_ quietly
13967
14090
  swapped for a plausible one: `swarm()`'s existing done/fallback law then ends
13968
14091
  the run, so a hallucinated agent shows up as a halt instead of a wrong answer.
13969
14092
  Unusable output throws `RoutingDecisionError` with the model's raw text
@@ -13998,7 +14121,7 @@ mistake.
13998
14121
  feeds the next step's `{ message }`, the house convention every LLM runner
13999
14122
  speaks; anything else must match exactly) — a chain that doesn't line up is a
14000
14123
  compile error, pinned by `@ts-expect-error` fixtures under `npm run
14001
- test:types`. At run time, values are handed over **unchanged**: objects stay
14124
+ test:types`. At run time, values are handed over **unchanged**: objects stay
14002
14125
  objects. `workflow(draft, edit)` over two `LLMCall`s reads exactly as it
14003
14126
  always did.
14004
14127
 
@@ -14028,7 +14151,7 @@ mistake.
14028
14151
 
14029
14152
  **If you pass `dimensions` today, your vectors change length.** They were the
14030
14153
  model's native length all along; now they are the length you asked for. Anything
14031
- you have already embedded and stored was written at the *old* length, so a store
14154
+ you have already embedded and stored was written at the _old_ length, so a store
14032
14155
  built with `openaiEmbedder({ dimensions: 256 })` on 7.8 holds 1536-long vectors
14033
14156
  and will not match new 256-long queries. **Re-embed, or drop `dimensions` to
14034
14157
  keep the old lengths.**
@@ -14046,7 +14169,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
14046
14169
  1536 for everything, so `text-embedding-3-large` under-reported by half.
14047
14170
  - An **unknown model with no `dimensions` is now a construction-time error**
14048
14171
  instead of a silent 1536. This is the breaking edge: `openaiEmbedder({
14049
- baseURL, model: 'nomic-embed-text' })` against a gateway, an Ollama server or
14172
+ baseURL, model: 'nomic-embed-text' })` against a gateway, an Ollama server or
14050
14173
  an Azure deployment name now throws until you state the length. That
14051
14174
  population is exactly the one that was being lied to. One option fixes it:
14052
14175
  `{ dimensions: 768 }`.
@@ -14062,7 +14185,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
14062
14185
  - **`localEmbedder({ backend })` / `staticEmbedder({ backend })` — pass an
14063
14186
  already-imported module, and the on-device embedders work in a browser.** To
14064
14187
  keep the heavy peer deps optional, both factories import them through a
14065
- *variable* specifier — which no bundler can see through. The bare name
14188
+ _variable_ specifier — which no bundler can see through. The bare name
14066
14189
  survived a production build and reached the browser unresolved:
14067
14190
  `TypeError: Failed to resolve module specifier '@huggingface/transformers'`.
14068
14191
  The capability was there all along; only the packaging blocked it. Now the
@@ -14099,7 +14222,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
14099
14222
 
14100
14223
  - **Docs-truth check — an ongoing, honest answer to "do the docs describe what
14101
14224
  the code actually does?"** `npm run docs:truth` (new CI job `docs-truth`)
14102
- answers three *separate* questions for every capability the package exposes,
14225
+ answers three _separate_ questions for every capability the package exposes,
14103
14226
  because their combinations are different bugs: DECLARED (in the published
14104
14227
  surface), DOCUMENTED (described in prose on the site), EXERCISED (a real run
14105
14228
  produces it). Declared/documented/never-exercised is the shape a dead or
@@ -14113,7 +14236,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
14113
14236
  therefore cannot see a single `agentfootprint/<subpath>` symbol. The
14114
14237
  surface is reported per subpath, since root-barrel-vs-subpath is itself a
14115
14238
  known source of user confusion. Events come from `ALL_EVENT_TYPES`.
14116
- - The DOCUMENTED column counts *only* prose on the 63 hand-written pages
14239
+ - The DOCUMENTED column counts _only_ prose on the 63 hand-written pages
14117
14240
  under `docs-next/content/docs`. Both TypeDoc trees
14118
14241
  (`docs-next/content/docs/api/`, `docs/api-reference/`) are excluded — they
14119
14242
  are generated from source, so every symbol appears in them by construction