agentfootprint 9.83.0 → 9.85.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.
- package/CHANGELOG.md +713 -512
- package/dist/artifacts/present.js +21 -3
- package/dist/artifacts/present.js.map +1 -1
- package/dist/artifacts/wants.js +19 -4
- package/dist/artifacts/wants.js.map +1 -1
- package/dist/core/Agent.js +10 -0
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/agent/AgentBuilder.js.map +1 -1
- package/dist/core/agent/buildToolRegistry.js +137 -0
- package/dist/core/agent/buildToolRegistry.js.map +1 -1
- package/dist/core/agent/evidence/frames.js +15 -6
- package/dist/core/agent/evidence/frames.js.map +1 -1
- package/dist/core/agent/outputEnforcement.js +7 -4
- package/dist/core/agent/outputEnforcement.js.map +1 -1
- package/dist/core/agent/selfCallNotice.js +174 -0
- package/dist/core/agent/selfCallNotice.js.map +1 -0
- package/dist/core/agent/stages/routeTurn.js +28 -3
- package/dist/core/agent/stages/routeTurn.js.map +1 -1
- package/dist/core/agent/stages/toolCalls.js +87 -2
- package/dist/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/core/agent/window/currentRequest.js +11 -13
- package/dist/core/agent/window/currentRequest.js.map +1 -1
- package/dist/core/agent/window/notice.js +9 -9
- package/dist/core/agent/window/notice.js.map +1 -1
- package/dist/core/agent/window/summarize.js +9 -9
- package/dist/core/agent/window/summarize.js.map +1 -1
- package/dist/core/codeRunnerTool.js +25 -6
- package/dist/core/codeRunnerTool.js.map +1 -1
- package/dist/doors/skill-graph.js +2 -1
- package/dist/doors/skill-graph.js.map +1 -1
- package/dist/esm/artifacts/present.js +21 -3
- package/dist/esm/artifacts/present.js.map +1 -1
- package/dist/esm/artifacts/wants.js +19 -4
- package/dist/esm/artifacts/wants.js.map +1 -1
- package/dist/esm/core/Agent.js +10 -0
- package/dist/esm/core/Agent.js.map +1 -1
- package/dist/esm/core/agent/AgentBuilder.d.ts +15 -0
- package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
- package/dist/esm/core/agent/buildToolRegistry.d.ts +137 -0
- package/dist/esm/core/agent/buildToolRegistry.js +137 -0
- package/dist/esm/core/agent/buildToolRegistry.js.map +1 -1
- package/dist/esm/core/agent/evidence/frames.d.ts +9 -4
- package/dist/esm/core/agent/evidence/frames.js +13 -5
- package/dist/esm/core/agent/evidence/frames.js.map +1 -1
- package/dist/esm/core/agent/outputEnforcement.d.ts +1 -2
- package/dist/esm/core/agent/outputEnforcement.js +4 -2
- package/dist/esm/core/agent/outputEnforcement.js.map +1 -1
- package/dist/esm/core/agent/selfCallNotice.d.ts +140 -0
- package/dist/esm/core/agent/selfCallNotice.js +169 -0
- package/dist/esm/core/agent/selfCallNotice.js.map +1 -0
- package/dist/esm/core/agent/stages/routeTurn.js +28 -3
- package/dist/esm/core/agent/stages/routeTurn.js.map +1 -1
- package/dist/esm/core/agent/stages/toolCalls.d.ts +6 -0
- package/dist/esm/core/agent/stages/toolCalls.js +87 -2
- package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/esm/core/agent/window/currentRequest.d.ts +9 -3
- package/dist/esm/core/agent/window/currentRequest.js +11 -13
- package/dist/esm/core/agent/window/currentRequest.js.map +1 -1
- package/dist/esm/core/agent/window/notice.d.ts +1 -4
- package/dist/esm/core/agent/window/notice.js +5 -6
- package/dist/esm/core/agent/window/notice.js.map +1 -1
- package/dist/esm/core/agent/window/summarize.d.ts +1 -4
- package/dist/esm/core/agent/window/summarize.js +5 -6
- package/dist/esm/core/agent/window/summarize.js.map +1 -1
- package/dist/esm/core/codeRunnerTool.js +25 -6
- package/dist/esm/core/codeRunnerTool.js.map +1 -1
- package/dist/esm/doors/skill-graph.d.ts +1 -1
- package/dist/esm/doors/skill-graph.js +1 -1
- package/dist/esm/doors/skill-graph.js.map +1 -1
- package/dist/esm/events/payloads.d.ts +15 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/injection-engine/index.d.ts +1 -0
- package/dist/esm/lib/injection-engine/index.js +6 -0
- package/dist/esm/lib/injection-engine/index.js.map +1 -1
- package/dist/esm/lib/injection-engine/skillToolDescriptors.d.ts +29 -0
- package/dist/esm/lib/injection-engine/skillToolDescriptors.js +138 -8
- package/dist/esm/lib/injection-engine/skillToolDescriptors.js.map +1 -1
- package/dist/esm/lib/injection-engine/types.d.ts +54 -0
- package/dist/esm/lib/injection-engine/types.js +28 -0
- package/dist/esm/lib/injection-engine/types.js.map +1 -1
- package/dist/esm/lib/saidByPerson.d.ts +99 -0
- package/dist/esm/lib/saidByPerson.js +104 -0
- package/dist/esm/lib/saidByPerson.js.map +1 -0
- package/dist/esm/maps/engagement/parkCard.js +7 -1
- package/dist/esm/maps/engagement/parkCard.js.map +1 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/injection-engine/index.js +9 -2
- package/dist/lib/injection-engine/index.js.map +1 -1
- package/dist/lib/injection-engine/skillToolDescriptors.js +140 -9
- package/dist/lib/injection-engine/skillToolDescriptors.js.map +1 -1
- package/dist/lib/injection-engine/types.js +30 -1
- package/dist/lib/injection-engine/types.js.map +1 -1
- package/dist/lib/saidByPerson.js +111 -0
- package/dist/lib/saidByPerson.js.map +1 -0
- package/dist/maps/engagement/parkCard.js +7 -1
- package/dist/maps/engagement/parkCard.js.map +1 -1
- package/dist/types/artifacts/present.d.ts.map +1 -1
- package/dist/types/artifacts/wants.d.ts.map +1 -1
- package/dist/types/core/Agent.d.ts.map +1 -1
- package/dist/types/core/agent/AgentBuilder.d.ts +15 -0
- package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
- package/dist/types/core/agent/buildToolRegistry.d.ts +137 -0
- package/dist/types/core/agent/buildToolRegistry.d.ts.map +1 -1
- package/dist/types/core/agent/evidence/frames.d.ts +9 -4
- package/dist/types/core/agent/evidence/frames.d.ts.map +1 -1
- package/dist/types/core/agent/outputEnforcement.d.ts +1 -2
- package/dist/types/core/agent/outputEnforcement.d.ts.map +1 -1
- package/dist/types/core/agent/selfCallNotice.d.ts +141 -0
- package/dist/types/core/agent/selfCallNotice.d.ts.map +1 -0
- package/dist/types/core/agent/stages/routeTurn.d.ts.map +1 -1
- package/dist/types/core/agent/stages/toolCalls.d.ts +6 -0
- package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
- package/dist/types/core/agent/window/currentRequest.d.ts +9 -3
- package/dist/types/core/agent/window/currentRequest.d.ts.map +1 -1
- package/dist/types/core/agent/window/notice.d.ts +1 -4
- package/dist/types/core/agent/window/notice.d.ts.map +1 -1
- package/dist/types/core/agent/window/summarize.d.ts +1 -4
- package/dist/types/core/agent/window/summarize.d.ts.map +1 -1
- package/dist/types/doors/skill-graph.d.ts +1 -1
- package/dist/types/doors/skill-graph.d.ts.map +1 -1
- package/dist/types/events/payloads.d.ts +15 -0
- package/dist/types/events/payloads.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/injection-engine/index.d.ts +1 -0
- package/dist/types/lib/injection-engine/index.d.ts.map +1 -1
- package/dist/types/lib/injection-engine/skillToolDescriptors.d.ts +29 -0
- package/dist/types/lib/injection-engine/skillToolDescriptors.d.ts.map +1 -1
- package/dist/types/lib/injection-engine/types.d.ts +54 -0
- package/dist/types/lib/injection-engine/types.d.ts.map +1 -1
- package/dist/types/lib/saidByPerson.d.ts +100 -0
- package/dist/types/lib/saidByPerson.d.ts.map +1 -0
- package/dist/types/maps/engagement/parkCard.d.ts.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,190 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [9.85.0] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
A sentence composed once and read many times is not a fact — it is a prediction.
|
|
13
|
+
|
|
14
|
+
9.84.0 fixed one such sentence and shipped a checker for the class. The checker
|
|
15
|
+
covered two surfaces. Five more sentences matching its own existing rule were
|
|
16
|
+
live elsewhere in the tree, unread by it, because coverage was decided by which
|
|
17
|
+
suite happened to import the helper. That is the same defect one level up: a
|
|
18
|
+
guard asserting a boundary it cannot verify.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Five model-facing sentences that outlive the moment they were true.** Each is
|
|
23
|
+
now anchored to one named call, in the past tense, after tracing it to its
|
|
24
|
+
delivery point to confirm it really is re-read:
|
|
25
|
+
|
|
26
|
+
- `artifacts/present.ts` and `artifacts/wants.ts` — _"Nothing is live in this
|
|
27
|
+
run's scope right now"_ and its sibling inventory, _"Live refs in scope: …"_.
|
|
28
|
+
A census goes wrong in BOTH directions on re-read: entries sweep, new ones
|
|
29
|
+
appear.
|
|
30
|
+
- `core/codeRunnerTool.ts` — _"staged into this session"_. The anchor resolves,
|
|
31
|
+
and goes on resolving; the defect is that it resolves to a scope holding many
|
|
32
|
+
calls, so a per-call report cannot say which call it describes.
|
|
33
|
+
- `maps/engagement/parkCard.ts` — _"Its instructions and its tools are not being
|
|
34
|
+
sent right now"_, on a card that rides every call while a map is parked. Its
|
|
35
|
+
falsifier is compose order, not staleness: the card is written in the
|
|
36
|
+
injection-engine pass and the tools slot that acts on the park runs after it.
|
|
37
|
+
- The `read_skill` description no longer predicts what `read_skill` will do.
|
|
38
|
+
Naming the cursor is the fix; every sentence tried beside it turned out false
|
|
39
|
+
somewhere. The last one — _"You do not need read_skill to go on using it"_ —
|
|
40
|
+
was argued to be a necessity claim no posture, budget or hold-out could
|
|
41
|
+
falsify. The PARK falsifies it: a parked member keeps the cursor, loses its
|
|
42
|
+
body and its tools, and `read_skill` is then the only door back.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **Surfaces carry `channel` and `lifetime` separately.** Where a string is
|
|
47
|
+
delivered and how long it lives do not correlate: system text is rebuilt every
|
|
48
|
+
request, so a present-tense clause in it is a fact; a tool result persists, so
|
|
49
|
+
the same clause is a forecast. The rules judge lifetime. Both existing
|
|
50
|
+
exemptions turned out to be lifetime claims wearing channel clothes and are now
|
|
51
|
+
derivable rather than asserted.
|
|
52
|
+
|
|
53
|
+
- **A producer registry that fails when a surface is not exercised.** Coverage
|
|
54
|
+
decided by which suite imports a helper is a habit, not a guarantee. Its header
|
|
55
|
+
states what a green run does not prove: hand-maintained rows cannot see a
|
|
56
|
+
producer nobody registered.
|
|
57
|
+
|
|
58
|
+
- **The offer/dispatch divergence list is walked, not written.**
|
|
59
|
+
`test/core/agent/toolDivergenceWalk.test.ts` crosses every source that can put
|
|
60
|
+
a name on the wire or answer to one — static, provider, MCP, always-on skill,
|
|
61
|
+
active skill, inactive skill, stepped skill — against six narrowing states and
|
|
62
|
+
the framework's auto-attach names, and drives a real run per configuration.
|
|
63
|
+
Sixty configurations, thirty-six divergences, each with a mechanically derived
|
|
64
|
+
cause and a stated reason it is tolerated. New fails. Disappeared fails.
|
|
65
|
+
Vacuous fails, and is unbaselineable.
|
|
66
|
+
|
|
67
|
+
It replaced a hand-written enumeration that claimed completeness and was
|
|
68
|
+
falsified three rounds running. It then found three classes nobody seeded: a
|
|
69
|
+
provider tool whose name a registry holder already owns is dead in both
|
|
70
|
+
directions and the shadow report cannot see it; the auto-attach names disagree
|
|
71
|
+
about what they refuse; and `selfExplain` is a fourth family whose reservation
|
|
72
|
+
reads only the static registry.
|
|
73
|
+
|
|
74
|
+
### Documentation
|
|
75
|
+
|
|
76
|
+
- **Three laws stated where the code lives**, epoch-scoped, after two earlier
|
|
77
|
+
phrasings were false in shipped configurations. Every offered capability
|
|
78
|
+
resolves to a dispatchable implementation with stable identity for that epoch;
|
|
79
|
+
attention may alter the offer, but omission from the offer must not be
|
|
80
|
+
presented as proof of permanent capability loss. Only static skill-registry
|
|
81
|
+
tools are known to remain dispatchable after leaving the offer.
|
|
82
|
+
|
|
83
|
+
- **`docs/design/2026-09-recorded-not-built.md`** — three real defects with
|
|
84
|
+
reproductions, deliberately not fixed: an inactive skill's tool shadows in
|
|
85
|
+
silence, the shadow report names the wrong source, and `skip_step` is
|
|
86
|
+
claimable by a provider. Each names what deciding to fix it would cost.
|
|
87
|
+
|
|
88
|
+
## [9.84.0] - 2026-09-03
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
|
|
92
|
+
- **`read_skill` refused the skill the model was already in.** A turn routed
|
|
93
|
+
decisively to `X`, the model called `read_skill("X")` to find out where it
|
|
94
|
+
stood, and the gate answered _"`read_skill("X")` is not reachable from here.
|
|
95
|
+
Reachable skills: …"_ — about the one skill whose body was in that call's
|
|
96
|
+
system prompt and whose tools were in that same call's tool list. The cursor
|
|
97
|
+
is in neither half of `hops ∪ open` by construction: `makeReachableSkills`
|
|
98
|
+
filters it out of its own successor set (a move to where you already are is
|
|
99
|
+
not a move) and `openSkillIds()` excludes every graph-wired skill. Nobody had
|
|
100
|
+
written the case for _"you asked for the room you are standing in."_ Read as a
|
|
101
|
+
claim about AVAILABILITY — which is how a model reads _"not reachable"_ — it
|
|
102
|
+
says the opposite of the request it arrived in. A field report recorded the
|
|
103
|
+
consequence three times in one day: the model concluded its capability was
|
|
104
|
+
gone and answered that it could not help, while the skill's tools sat on the
|
|
105
|
+
wire, loaded and callable.
|
|
106
|
+
|
|
107
|
+
A self-call now gets the truth instead of a refusal. It names where the model
|
|
108
|
+
stands and which tools it could call, taken from the merged wire list the LLM
|
|
109
|
+
stage actually sent, intersected with the skill's own declared tools — never
|
|
110
|
+
from the declaration alone. Every configuration that would make that false has
|
|
111
|
+
its own wording: a skill declaring no tools says so, a step or park hold-out
|
|
112
|
+
says the tools were withheld rather than naming them, and a call whose wire
|
|
113
|
+
cannot be established says nothing about tools at all. Mechanically it is still
|
|
114
|
+
a rejection — no activation, no cursor move, and the refusal budget still
|
|
115
|
+
counts it, because a self-call _loop_ is exactly the stuck model that budget
|
|
116
|
+
exists to escalate.
|
|
117
|
+
|
|
118
|
+
**Every clause is anchored to one named call.** A tool result is composed on
|
|
119
|
+
one iteration and re-read on every call after it, including the out-of-budget
|
|
120
|
+
wrap-up that carries no tools under _"Do not request tools."_ So the notice
|
|
121
|
+
makes no forward-looking claim at all: no exhortation to act, no offer of a
|
|
122
|
+
move, no clause conditioned on a budget or a posture that can change after the
|
|
123
|
+
sentence is written. Deixis counts as forward-looking — _"the call you just
|
|
124
|
+
made"_ denotes a different call on each re-read, so the anchor is named once
|
|
125
|
+
in the opening sentence and every later clause refers back to it.
|
|
126
|
+
|
|
127
|
+
- **The `read_skill` description said the same thing in the other channel.** The
|
|
128
|
+
current skill was listed under _"Not reachable from here (read_skill for these
|
|
129
|
+
will be refused)"_, where it appeared purely as an artefact of that same
|
|
130
|
+
filter. It is in neither column now, and the description names the cursor on
|
|
131
|
+
every call that has one — the positive signal whose absence was the root of the
|
|
132
|
+
field failure, since the system prompt carried the skill's body with nothing
|
|
133
|
+
saying which skill it was. A genuinely unreachable skill is still named there,
|
|
134
|
+
and a genuinely unreachable hop keeps its refusal word for word.
|
|
135
|
+
|
|
136
|
+
- **The description no longer predicts what `read_skill` will do.** Naming the
|
|
137
|
+
cursor is the fix; every sentence tried alongside it turned out false
|
|
138
|
+
somewhere. _"read_skill MOVES you to a DIFFERENT skill"_ is false at compose
|
|
139
|
+
time under `strictness: 'rails'` (every model hop refused) and under `'guard'`
|
|
140
|
+
off an outstanding menu, where the posture arm contradicts it head-on. Its
|
|
141
|
+
replacement — _"You do not need read_skill to go on using it"_ — was argued to
|
|
142
|
+
be a claim about necessity that no posture, budget or hold-out could falsify,
|
|
143
|
+
and the **park** falsifies it: a parked map member keeps the cursor, loses its
|
|
144
|
+
body and its tools, and `read_skill` is then the only door back. The
|
|
145
|
+
description is composed before the hold-outs run, so it cannot know when such a
|
|
146
|
+
claim would be lying. Outside an outstanding menu it now states the name and
|
|
147
|
+
stops. The menu's stay clause is unchanged.
|
|
148
|
+
|
|
149
|
+
- **Role visibility now covers the cursor.** The description read the cursor id
|
|
150
|
+
past the `hiddenIds` filter, so a role denied `skill_read` on the skill the
|
|
151
|
+
graph had routed to was still told _"You are in '\<that skill\>'"_ — leaking
|
|
152
|
+
the name of a capability no cursor move would ever grant it. A hidden cursor is
|
|
153
|
+
named nowhere: not as reachable, not as refusable, not as the cursor, and not
|
|
154
|
+
in the menu's stay clause. The security suite's _"a hidden skill is never
|
|
155
|
+
named"_ property is now driven on the `.skillGraph()` path as well as
|
|
156
|
+
`.skill()`; it stayed green through the leak because its agents used `.skill()`
|
|
157
|
+
only, so the leaking line never executed.
|
|
158
|
+
|
|
159
|
+
### Added
|
|
160
|
+
|
|
161
|
+
- **`saidByPerson(ctx)` / `isSaidByPerson(msg)` — telling what a person said from
|
|
162
|
+
what the library wrote.** Five classes of `role: 'user'` message are authored
|
|
163
|
+
by this library, not by a person: the compaction frame, the drop notice (whose
|
|
164
|
+
text names tools), the schema-check and evidence-check corrections, and any
|
|
165
|
+
injection-delivered message. The window's own refusal engine has always applied
|
|
166
|
+
that rule; a `when` predicate could not, because `InjectionContext.history`
|
|
167
|
+
exposes only `{ role, content, toolName? }`. An author writing an entry rule
|
|
168
|
+
that reads history was silently matching on our own bookkeeping. One
|
|
169
|
+
implementation, reused by both — the rule cannot drift between routing and the
|
|
170
|
+
window.
|
|
171
|
+
|
|
172
|
+
- **`SkillRejectedPayload.reason`** — `'self-call' | 'unreachable' | 'posture'`,
|
|
173
|
+
optional and additive, so a consumer can tell a self-call from a genuine
|
|
174
|
+
unreachable hop without comparing two fields.
|
|
175
|
+
|
|
176
|
+
### Documentation
|
|
177
|
+
|
|
178
|
+
- **`strictness` says what a posture governs, exactly.** A posture governs the
|
|
179
|
+
model's `read_skill` door and nothing else. Two doors stay open under all three
|
|
180
|
+
postures: OPEN skills, already stated, and a tool's `propose-transition`, now
|
|
181
|
+
stated with its reason and its reachability check. `'rails'` means _the model
|
|
182
|
+
never routes_ — never _"nothing but my declared edges routes"_; a tool of yours
|
|
183
|
+
that proposes is a route you declared in code instead of in the graph. No
|
|
184
|
+
behaviour changed: the exemption is recorded in three places and pinned by a
|
|
185
|
+
test whose title is the argument.
|
|
186
|
+
|
|
187
|
+
- **The injection-engine README's runtime picture matched an older engine.** Its
|
|
188
|
+
diagram drew one box that "evaluates triggers"; the engine is a four-stage
|
|
189
|
+
footprintjs subflow — Gather, Evaluate, Route, Delta — and the cursor, the step
|
|
190
|
+
pointer, the instruction leases and map engagement all advance inside Evaluate.
|
|
191
|
+
The events table placed `context.evaluated` at subflow exit; the code emits it
|
|
192
|
+
in stage 2 of 4. Both corrected, along with three counts that had drifted.
|
|
193
|
+
|
|
10
194
|
## [9.83.0] - 2026-09-03
|
|
11
195
|
|
|
12
196
|
### Fixed
|
|
@@ -14,13 +198,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
14
198
|
- **The evidence gate claimed a boundary it did not measure.** Both of its
|
|
15
199
|
user-facing sentences — the correction it sends the model
|
|
16
200
|
(`buildEvidenceCorrection`) and the warning it prints an operator
|
|
17
|
-
(`evidenceRefusalSentence`) — said the flagged values
|
|
18
|
-
result **from this turn**"
|
|
201
|
+
(`evidenceRefusalSentence`) — said the flagged values _"appear in NO tool
|
|
202
|
+
result **from this turn**"_. The index behind them has never been turn-scoped:
|
|
19
203
|
it walks every `role: 'tool'` turn in the history. The library was asserting a
|
|
20
204
|
scope it could not honour, in the two places that assertion is read.
|
|
21
205
|
|
|
22
|
-
Both now say what the check really reaches —
|
|
23
|
-
run read"
|
|
206
|
+
Both now say what the check really reaches — _"appear in no tool result this
|
|
207
|
+
run read"_ — which is both true and the stronger claim, and the operator
|
|
24
208
|
sentence adds the two facts a reader needs: that the corpus is the LIVE
|
|
25
209
|
WINDOW (a window strategy rewrites `scope.history` in place, so a dropped
|
|
26
210
|
result is not in it), and that `noticePriorTurnEvidence` is what answers the
|
|
@@ -34,8 +218,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
34
218
|
|
|
35
219
|
The measured failure: a consumer's agent answered a data question with **zero
|
|
36
220
|
tool calls**, and the gate approved it — `LLM calls 1 · Tool calls 0 ·
|
|
37
|
-
|
|
38
|
-
returned — the answer stands."
|
|
221
|
+
Iterations 1`, then _"All 7 values in the answer were found in what the tools
|
|
222
|
+
returned — the answer stands."_ They were found: in an inventory result from
|
|
39
223
|
four turns earlier, fetched for a different question. The user had asked about
|
|
40
224
|
array performance; the answer recommended enabling a collector that had been
|
|
41
225
|
running for months. Two turns did it back to back. Every rail passed honestly
|
|
@@ -55,14 +239,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
55
239
|
.namesAndNumbersFromEvidence() // ← the other half: it owns the extractor
|
|
56
240
|
.build();
|
|
57
241
|
|
|
58
|
-
await agent.run('what arrays are there?');
|
|
242
|
+
await agent.run('what arrays are there?'); // fetches, answers, files nothing
|
|
59
243
|
await agent.followUp('how is array performance?'); // no tool call, answers from turn 1
|
|
60
244
|
// → prior-turn-evidence: 3 grounded value(s), all last served in turn 1,
|
|
61
245
|
// and this turn called no tool at all.
|
|
62
246
|
```
|
|
63
247
|
|
|
64
248
|
**The corpus is deliberately NOT narrowed to this turn.** That would have made
|
|
65
|
-
the old sentence true and been the wrong fix:
|
|
249
|
+
the old sentence true and been the wrong fix: _"and what about that disk?"_
|
|
66
250
|
leans on the previous turn's rows legitimately, and a check that cries wolf is
|
|
67
251
|
a check somebody switches off. ONE grounded value from this turn's own results
|
|
68
252
|
files nothing — not a threshold to tune, but the falsification of the claim
|
|
@@ -106,7 +290,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
106
290
|
is generated from what the run itself said: the branch descriptions the chart
|
|
107
291
|
declared, and the rule labels this run's `decide()` evidence carried. For one
|
|
108
292
|
branch, both sources are silent by construction — the DEFAULT. It is the
|
|
109
|
-
branch chosen by
|
|
293
|
+
branch chosen by _no rule_ (it fires exactly when every rule failed, so no
|
|
110
294
|
`label` describes it), and when the decider lives inside a dynamically
|
|
111
295
|
generated fan-out branch the branch chart does not exist at build time either,
|
|
112
296
|
so there is no declared description to fall back on.
|
|
@@ -183,7 +367,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
183
367
|
sdk: { Client, StreamableHTTPClientTransport },
|
|
184
368
|
transport: { transport: 'http', url: '/py/mcp' },
|
|
185
369
|
});
|
|
186
|
-
const tools = await sidecar.tools();
|
|
370
|
+
const tools = await sidecar.tools(); // the same readonly Tool[], _meta and all
|
|
187
371
|
```
|
|
188
372
|
|
|
189
373
|
**`connection?: McpConnection`** — the full escape hatch: you connect the
|
|
@@ -257,8 +441,8 @@ arms through a real socket against the real SDK.
|
|
|
257
441
|
|
|
258
442
|
**What none of that proves: a browser.** This repo has no browser test
|
|
259
443
|
environment, so nobody has yet driven initialize/listTools/callTool from an
|
|
260
|
-
actual page. The honest status is
|
|
261
|
-
in a browser
|
|
444
|
+
actual page. The honest status is _proven in Node, fenced at the graph, not gated
|
|
445
|
+
in a browser._ Three costs land on the app, not here: your server must send CORS
|
|
262
446
|
headers (every MCP request preflights, and `Mcp-Session-Id` must be in
|
|
263
447
|
`Access-Control-Expose-Headers`) — `mcpServe` sends none; SSE through a dev or
|
|
264
448
|
production proxy is unproven; and the SDK's client path adds roughly 260 KB
|
|
@@ -277,8 +461,8 @@ first time a tool with an `outputSchema` is validated.
|
|
|
277
461
|
had a standing rule against exactly that. The library was overruling it.
|
|
278
462
|
|
|
279
463
|
The instruction was ours. Every verdict projection shipped `table`
|
|
280
|
-
pre-rendered with `VERDICT_RENDER_NOTE`:
|
|
281
|
-
same rows as `verdicts` — output it VERBATIM."
|
|
464
|
+
pre-rendered with `VERDICT_RENDER_NOTE`: _"table is PRE-RENDERED over the
|
|
465
|
+
same rows as `verdicts` — output it VERBATIM."_ That note is RIGHT wherever
|
|
282
466
|
the model's words are the rows' only surface — a chat client, a log line, an
|
|
283
467
|
email — because the alternative there is retyping, and a retyped identifier
|
|
284
468
|
that looks right and matches nothing is the failure the note exists to stop.
|
|
@@ -291,7 +475,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
291
475
|
it is in. So the caller says, in one word:
|
|
292
476
|
|
|
293
477
|
```ts
|
|
294
|
-
runbookAsTool({ /* … */ presentation: 'panel' });
|
|
478
|
+
runbookAsTool({ /* … */ presentation: 'panel' }); // default: 'prose'
|
|
295
479
|
```
|
|
296
480
|
|
|
297
481
|
- **`'prose'` (the default)** — today's envelope, key for key: `table`
|
|
@@ -357,7 +541,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
357
541
|
`recording_note`. The wire ops that already redeem the walk redeem this with
|
|
358
542
|
**zero new operations**.
|
|
359
543
|
|
|
360
|
-
**OPT-IN, and that is the honest default.** A walk carries
|
|
544
|
+
**OPT-IN, and that is the honest default.** A walk carries _sentences about_
|
|
361
545
|
what happened and no payload from it — values are off by construction
|
|
362
546
|
(`narrative({ includeValues: false })`). A recording is the run: shared
|
|
363
547
|
state, the whole commit log, every attached recorder's data — **whatever the
|
|
@@ -393,7 +577,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
393
577
|
the spine exists to prevent.
|
|
394
578
|
|
|
395
579
|
**`events` is empty by construction, and says so.** It is the typed
|
|
396
|
-
|
|
580
|
+
_agentfootprint_ stream, fired by an agent turn; what ran here is a
|
|
397
581
|
footprintjs chart on its own executor, which fires none. All three keys are
|
|
398
582
|
present (that is what a viewer reads), the empty array is the honest count,
|
|
399
583
|
and the note states it so nobody reads it as a dropped stream — the walk's
|
|
@@ -412,8 +596,8 @@ first time a tool with an `outputSchema` is validated.
|
|
|
412
596
|
|
|
413
597
|
- **`Tool.resultColumns` + `checkColumnTypes` — the column-type contract: a
|
|
414
598
|
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 —
|
|
416
|
-
number became something else, and nothing noticed at the
|
|
599
|
+
it at the boundary.** Three recorded failures, and they are one shape — _a
|
|
600
|
+
number became something else, and nothing noticed at the seam_:
|
|
417
601
|
|
|
418
602
|
1. A mapping report wrote `str(m.get("logical_unit_number") or "")`. **LUN 0
|
|
419
603
|
is falsy**, so LUN 0 was stored as an EMPTY STRING on 2,094 mappings, and
|
|
@@ -442,19 +626,19 @@ first time a tool with an `outputSchema` is validated.
|
|
|
442
626
|
`CostBudget` two-spellings pattern, normalized once).
|
|
443
627
|
|
|
444
628
|
**THE CEILING**, exported as `COLUMN_TYPE_CEILING` and quoted verbatim into
|
|
445
|
-
every finding, the `EMPTY_LOOKUP_CEILING` law:
|
|
629
|
+
every finding, the `EMPTY_LOOKUP_CEILING` law: _"This judges TYPE, never
|
|
446
630
|
MEANING — it can see that a column declared `number` holds a string, and it
|
|
447
631
|
can never see that the string should have been 0, or that a 0.0 should have
|
|
448
632
|
been an 8; a column whose every value has its declared type passes here and
|
|
449
|
-
can still be wrong."
|
|
633
|
+
can still be wrong."_ Failures 1 and 3 are caught. **Failure 2 is not, and
|
|
450
634
|
never will be** — `0.0` is a perfectly good number — and the check says so
|
|
451
635
|
out loud rather than letting a green row imply otherwise.
|
|
452
636
|
|
|
453
637
|
**TWO finding kinds, because the field bug turned on the difference.** New
|
|
454
638
|
`ContextErrorKind`s at the **write seam**: **`column-type-mismatch`** (the
|
|
455
639
|
column is THERE and holds the wrong thing) and **`missing-column`** (the
|
|
456
|
-
declared column is in NONE of the rows).
|
|
457
|
-
be"
|
|
640
|
+
declared column is in NONE of the rows). _"The value is not what it should
|
|
641
|
+
be"_ sends a person to the mapping code; _"the column was never delivered"_
|
|
458
642
|
sends them to the query. A checker that said only "something is off with
|
|
459
643
|
logical_unit_number" would have helped with neither. Each finding names the
|
|
460
644
|
column, the offending value quoted, the rows affected of the rows read, and
|
|
@@ -524,7 +708,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
524
708
|
it came back empty.** A triage agent's reverse-lookup tool filtered a column
|
|
525
709
|
before a pivot, so the column did not exist yet and EVERY reverse lookup
|
|
526
710
|
returned an empty result — for every identifier, always. The tool then
|
|
527
|
-
answered
|
|
711
|
+
answered _successfully_ with an empty list, and the agent reported in a
|
|
528
712
|
table, with confidence, that the device was not logged in to any port on any
|
|
529
713
|
collected switch, advising a check of the physical cabling. It was logged in
|
|
530
714
|
the whole time. Every rail passed, and passed honestly: nothing errored,
|
|
@@ -548,9 +732,9 @@ first time a tool with an `outputSchema` is validated.
|
|
|
548
732
|
for the broken filter and for the honest absence. The bound ships as one
|
|
549
733
|
exported string, `EMPTY_LOOKUP_CEILING`, quoted verbatim into every message
|
|
550
734
|
so it cannot drift out of one doc and leave a reader thinking the library
|
|
551
|
-
knows more than it does:
|
|
735
|
+
knows more than it does: _"An empty result can be perfectly true — the thing
|
|
552
736
|
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."
|
|
737
|
+
look, never a verdict that anything is wrong."_
|
|
554
738
|
|
|
555
739
|
Deliberately NOT `dangling-reference`, whose meaning is the opposite: there
|
|
556
740
|
the ground has left reach; here the ground IS in reach and the lookup found
|
|
@@ -566,7 +750,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
566
750
|
make impossible.
|
|
567
751
|
|
|
568
752
|
**Armed by two halves**, and the second one is why: `noticeEmptyLookups:
|
|
569
|
-
|
|
753
|
+
true` on `Agent.create` **and** at least one tool declaring `argumentsFrom`.
|
|
570
754
|
The declaration alone is not enough — it already arms two other checks, and
|
|
571
755
|
an advisory that armed itself off a declaration made for something else
|
|
572
756
|
would not be opt-in at all. **Default off is byte-identical**: no finding, no
|
|
@@ -637,7 +821,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
637
821
|
pre-rendered table, and the recorded walk that lets a reader CHECK the
|
|
638
822
|
verdict instead of trusting it. `runbookAsTool` is that envelope as one
|
|
639
823
|
declaration bag; the smallest legal call is `{ name, description,
|
|
640
|
-
|
|
824
|
+
procedure }` and it still yields the honest spine.
|
|
641
825
|
|
|
642
826
|
**The mandatory honesty spine**, on every answer whatever the runbook's
|
|
643
827
|
shape: `af_coverage` (three lists + a sentence naming the rule set and
|
|
@@ -673,7 +857,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
673
857
|
tools are invisible — there is no build-time list, the stated 9.72.0
|
|
674
858
|
caveat), so stages compose registered sources instead of importing modules
|
|
675
859
|
and building a second query stack. Inner calls run with `hasArtifacts:
|
|
676
|
-
|
|
860
|
+
false` (one answer, one ticket — never competing chips), a derived
|
|
677
861
|
toolCallId naming the outer call, `needs` resolved on the fail-closed
|
|
678
862
|
non-interactive path, and `checkIn`/`wants` tools refused BY NAME (an inner
|
|
679
863
|
call cannot pause, and must never silently skip a consent gate).
|
|
@@ -697,8 +881,8 @@ first time a tool with an `outputSchema` is validated.
|
|
|
697
881
|
run: four tool results carried real numbers, a compute tool that could sum
|
|
698
882
|
them was registered — with `wants` declared over the staged dataset kind —
|
|
699
883
|
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
|
|
701
|
-
tool result"
|
|
884
|
+
head anyway and stated the total; the evidence gate recorded _"appears in no
|
|
885
|
+
tool result"_ and the answer shipped, because the posture only observed. The
|
|
702
886
|
app patched it with more prose. The library-shaped fix is two mechanisms it
|
|
703
887
|
already owns, on the one dial it already has:
|
|
704
888
|
|
|
@@ -732,9 +916,9 @@ first time a tool with an `outputSchema` is validated.
|
|
|
732
916
|
`'guard'` allows the one bounded revision then delivers with both attempts
|
|
733
917
|
on the record, `'rails'` refuses with `UnsupportedValuesError`. What the
|
|
734
918
|
revision gains: when the flagged turn holds staged refs a served `wants`
|
|
735
|
-
tool can spend, the correction now names them —
|
|
919
|
+
tool can spend, the correction now names them — _"pass 'art\_…'
|
|
736
920
|
(dataset/rows) to `compute` — compute the number there and answer with what
|
|
737
|
-
it returns"
|
|
921
|
+
it returns"_ — inside the authored frame, so the quoted values still come
|
|
738
922
|
last and the exempt-corpus fence is untouched. The `revision-asked`
|
|
739
923
|
`evidence_checked` event carries the same facts additively (`stagedRefs`,
|
|
740
924
|
`spenderTools`). Absent everything — no gate, `nudge` unset, or no
|
|
@@ -762,7 +946,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
762
946
|
|
|
763
947
|
**`foundry()` — the project-endpoint provider** (`agentfootprint/providers`).
|
|
764
948
|
The JS answer to Microsoft's `FoundryChatClient(project_endpoint, model,
|
|
765
|
-
|
|
949
|
+
credential)`: point it at a Foundry project endpoint (or let the hosted
|
|
766
950
|
platform's auto-injected `FOUNDRY_PROJECT_ENDPOINT` supply it), name the
|
|
767
951
|
deployment (`AZURE_AI_MODEL_DEPLOYMENT_NAME ?? MODEL_NAME`), and auth is an
|
|
768
952
|
Entra `TokenCredential`, an api key, or — given neither — the platform's own
|
|
@@ -888,7 +1072,6 @@ first time a tool with an `outputSchema` is validated.
|
|
|
888
1072
|
tests fail against the previous source (proven by restoring it); no public
|
|
889
1073
|
API change; `OllamaUnavailableError` is byte-identical.
|
|
890
1074
|
|
|
891
|
-
|
|
892
1075
|
## [9.73.0] - 2026-08-27
|
|
893
1076
|
|
|
894
1077
|
### Fixed
|
|
@@ -925,13 +1108,15 @@ first time a tool with an `outputSchema` is validated.
|
|
|
925
1108
|
|
|
926
1109
|
```ts
|
|
927
1110
|
const agent = Agent.create({
|
|
928
|
-
provider,
|
|
1111
|
+
provider,
|
|
1112
|
+
model,
|
|
929
1113
|
// DECLARED, never ambient — this option is the only door.
|
|
930
|
-
externalGrounds: () =>
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1114
|
+
externalGrounds: () =>
|
|
1115
|
+
viewerSelection.cells.map((cell) => ({
|
|
1116
|
+
value: cell.text, // verified by the app against the artifact
|
|
1117
|
+
source: 'viewer-selection', // the audit label that travels
|
|
1118
|
+
})),
|
|
1119
|
+
});
|
|
935
1120
|
```
|
|
936
1121
|
|
|
937
1122
|
The provider is consulted once per LLM response that contains an armed call,
|
|
@@ -1008,25 +1193,25 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1008
1193
|
namespaced key (`MCP_TOOL_EXTRAS_KEY`, the string `agentfootprint`), and
|
|
1009
1194
|
`mcpClient` / `mockMcpClient` read them back onto the registered `Tool`:
|
|
1010
1195
|
|
|
1011
|
-
| field
|
|
1012
|
-
|
|
1013
|
-
| `argumentsFrom` | the dangling-reference and unsupported-argument checks
|
|
1014
|
-
| `resultKind`
|
|
1015
|
-
| `owner`
|
|
1016
|
-
| `resultClass`
|
|
1017
|
-
| `resultCeiling` | the author's refusing ceiling on an oversized result
|
|
1196
|
+
| field | what it arms on the consuming side |
|
|
1197
|
+
| --------------- | --------------------------------------------------------------- |
|
|
1198
|
+
| `argumentsFrom` | the dangling-reference and unsupported-argument checks |
|
|
1199
|
+
| `resultKind` | placement's mint — a placed result a `wants` argument can spend |
|
|
1200
|
+
| `owner` | the identity edge subject-joined checks read |
|
|
1201
|
+
| `resultClass` | the per-class `check:semantics` rules |
|
|
1202
|
+
| `resultCeiling` | the author's refusing ceiling on an oversized result |
|
|
1018
1203
|
|
|
1019
1204
|
```ts
|
|
1020
1205
|
const fleet = await mcpClient({ name: 'fleet-mcp', transport });
|
|
1021
1206
|
const agent = Agent.create({ provider, model })
|
|
1022
|
-
.tools(await fleet.tools())
|
|
1207
|
+
.tools(await fleet.tools()) // backup_status declares argumentsFrom: ['fleet_report']
|
|
1023
1208
|
.build();
|
|
1024
1209
|
// …and the choice seam now files `unsupported-argument` for it, exactly as
|
|
1025
1210
|
// it would for a local defineTool — pinned end to end, disposition row included.
|
|
1026
1211
|
```
|
|
1027
1212
|
|
|
1028
|
-
**The inclusion bar, stated where the list lives:**
|
|
1029
|
-
consumer-side check or rail reads; nothing that governs execution
|
|
1213
|
+
**The inclusion bar, stated where the list lives:** _a declaration a
|
|
1214
|
+
consumer-side check or rail reads; nothing that governs execution._ `needs`
|
|
1030
1215
|
(credentials), `checkIn` (human consent) and the session hooks are excluded
|
|
1031
1216
|
and always will be — they decide how a tool RUNS, and the tool runs on the
|
|
1032
1217
|
server. A client holding a consent gate the only executor already held is
|
|
@@ -1088,7 +1273,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1088
1273
|
});
|
|
1089
1274
|
|
|
1090
1275
|
// elsewhere — resolves now, and would have been a kind mismatch before
|
|
1091
|
-
defineTool({ name: 'chart', wants: { dataset: 'dataset/rows' }
|
|
1276
|
+
defineTool({ name: 'chart', wants: { dataset: 'dataset/rows' } /* … */ });
|
|
1092
1277
|
```
|
|
1093
1278
|
|
|
1094
1279
|
- **The matcher is untouched.** Nothing here loosens `wants`; exact match is
|
|
@@ -1157,9 +1342,9 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1157
1342
|
that earns the port its keep —
|
|
1158
1343
|
|
|
1159
1344
|
```ts
|
|
1160
|
-
await session.handControlTo('person');
|
|
1345
|
+
await session.handControlTo('person'); // the automation stream stops
|
|
1161
1346
|
// …they sign in, clear the CAPTCHA, approve the consent screen, watching live
|
|
1162
|
-
await session.handControlTo('agent');
|
|
1347
|
+
await session.handControlTo('agent'); // and the agent carries on
|
|
1163
1348
|
```
|
|
1164
1349
|
|
|
1165
1350
|
Pair it with a check-in and the agent **pauses** rather than guesses: the
|
|
@@ -1177,7 +1362,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1177
1362
|
**What verification changed.** The `InvokeBrowser` action union, read off a
|
|
1178
1363
|
real install of `@aws-sdk/client-bedrock-agentcore` 3.1118.0, is exactly
|
|
1179
1364
|
`mouseClick | mouseMove | mouseDrag | mouseScroll | keyType | keyPress |
|
|
1180
|
-
|
|
1365
|
+
keyShortcut | screenshot` — with **no navigate member at all**. An adapter
|
|
1181
1366
|
written from memory would have invented page verbs for a door that has none.
|
|
1182
1367
|
The same pass fixed `MouseClickArguments` (`{ x, y, button?, clickCount? }`,
|
|
1183
1368
|
buttons `LEFT|MIDDLE|RIGHT`), `KeyPressArguments` (`{ key, presses? }`) and
|
|
@@ -1213,6 +1398,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1213
1398
|
another agent (Strands, LangGraph, Google ADK, a Marketplace listing)
|
|
1214
1399
|
discovers yours through its agent card and calls it. The same split as 9.65.0,
|
|
1215
1400
|
for the third time:
|
|
1401
|
+
|
|
1216
1402
|
- **`a2aWire()`** is the A2A PROTOCOL — JSON-RPC 2.0, `message/send`, text
|
|
1217
1403
|
parts, artifacts, `A2A_PROTOCOL_VERSION` `0.3.0` — an open protocol with no
|
|
1218
1404
|
vendor in it, exported in its own right along with
|
|
@@ -1252,8 +1438,8 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1252
1438
|
- **A capability this library claimed and could not honour.** `httpHost`
|
|
1253
1439
|
declares `['streaming']` by default, so the A2A host inherited it while
|
|
1254
1440
|
`message/send` has nowhere to put a chunk: `requireCapability(host,
|
|
1255
|
-
|
|
1256
|
-
conformance suite caught it — it asserts chunks
|
|
1441
|
+
'streaming')` would have passed for a host that then delivered none. The
|
|
1442
|
+
conformance suite caught it — it asserts chunks _if and only if_ the
|
|
1257
1443
|
capability is declared — and the adapter now declares `[]`.
|
|
1258
1444
|
|
|
1259
1445
|
### Changed
|
|
@@ -1271,6 +1457,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1271
1457
|
looking anything up** (`agentfootprint/providers`). `gatewayTransport` says of
|
|
1272
1458
|
itself that nothing in it is vendor-specific, and that stays true because the
|
|
1273
1459
|
four facts which ARE AgentCore's now live in one file beside it:
|
|
1460
|
+
|
|
1274
1461
|
- **the endpoint** — `agentCoreGatewayUrl({ gatewayId, region })` builds
|
|
1275
1462
|
`https://{gatewayId}.gateway.bedrock-agentcore.{region}.amazonaws.com/mcp`,
|
|
1276
1463
|
a hostname nobody recalls correctly, which is why it is a function and not a
|
|
@@ -1336,6 +1523,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1336
1523
|
install of `@aws-sdk/client-bedrock-agentcore` **3.1118.0** — names, request
|
|
1337
1524
|
shapes and enum values read off the package rather than remembered, which is
|
|
1338
1525
|
the 9.4.0 law:
|
|
1526
|
+
|
|
1339
1527
|
- **`userFlow: 'consent' | 'exchange'`** — `'exchange'` sends
|
|
1340
1528
|
`ON_BEHALF_OF_TOKEN_EXCHANGE`, trading the person's existing login for a
|
|
1341
1529
|
scoped downstream token with no consent screen at any point. Default stays
|
|
@@ -1492,7 +1680,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1492
1680
|
|
|
1493
1681
|
The field story: a tool result ended with an offer — "I can also map these
|
|
1494
1682
|
ids to volume names" — and the person answered "yes please". The model bound
|
|
1495
|
-
|
|
1683
|
+
_that sentence_ as the identifier argument and dispatched. The tool's schema
|
|
1496
1684
|
DECLARED the identifier's shape, in a `pattern` that "yes please" could never
|
|
1497
1685
|
match; the pre-dispatch validator simply did not read the keyword. So the
|
|
1498
1686
|
call went out, failed downstream, and cost a round trip — and the consumer
|
|
@@ -1637,7 +1825,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1637
1825
|
check, so one busy check could carry the total while another check looked
|
|
1638
1826
|
at nothing at all — the per-check rows said so underneath (`⚠ ran 0×`), but
|
|
1639
1827
|
a reader who stops at the headline got the rosier story. The headline now
|
|
1640
|
-
states coverage: either
|
|
1828
|
+
states coverage: either _all_ registered checks ran, or it says COVERAGE IS
|
|
1641
1829
|
PARTIAL and names the checks that checked nothing. Same law as the rows —
|
|
1642
1830
|
a check that never saw a subject is silent about its seam, never a pass.
|
|
1643
1831
|
|
|
@@ -1667,38 +1855,40 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1667
1855
|
- **Six defects in the Context Integrity family, caught by an adversarial
|
|
1668
1856
|
review before release** — each survived two independent attempts to refute
|
|
1669
1857
|
it, and each is now pinned by a red-proved regression test.
|
|
1670
|
-
|
|
1858
|
+
|
|
1859
|
+
- _A claim finding's identity ignored the FIELD_, so a contract naming two
|
|
1671
1860
|
fields of one entity (the shape `.claims()` itself tells you to write)
|
|
1672
1861
|
filed one event and swallowed the rest — while the disposition ledger
|
|
1673
1862
|
counted them all, leaving the two accounts of one run disagreeing.
|
|
1674
1863
|
`ContextError.predicate` now rides the identity, mirroring the
|
|
1675
1864
|
substrate's own `assertionKey`; findings that never set it keep exactly
|
|
1676
1865
|
the identity they had.
|
|
1677
|
-
-
|
|
1678
|
-
|
|
1866
|
+
- _The dangling-reference check was DEAD under `reactMode:
|
|
1867
|
+
'dynamic-grouped'`_ — `compactions` was threaded into the wrong mapper,
|
|
1679
1868
|
so the check saw an empty window ledger every pass and filed a healthy
|
|
1680
1869
|
verdict. Chart-shape parity is now pinned by tests that run the same
|
|
1681
1870
|
trap under both dynamic shapes.
|
|
1682
|
-
-
|
|
1871
|
+
- _The dev canary structurally disabled the wiring-rot theorem._ A minted
|
|
1683
1872
|
canary proves the pure function still works; it says nothing about
|
|
1684
1873
|
whether the pipeline ever calls it (`beginIntegrityRun` mints by calling
|
|
1685
1874
|
the function directly). Masking theorem (i) with it meant the alarm this
|
|
1686
1875
|
ledger exists for could never fire.
|
|
1687
|
-
-
|
|
1876
|
+
- _`workExisted` was hardcoded `true`_ on every exit path, so a run that
|
|
1688
1877
|
died or paused before its first LLM call reported every registered
|
|
1689
1878
|
checker as dead. It is now measured from a signal the integrity code
|
|
1690
1879
|
does not itself write.
|
|
1691
|
-
-
|
|
1880
|
+
- _An answer agreeing with a settled non-reading was filed as an advisory_
|
|
1692
1881
|
— `null` reported for a fact whose settled value is `null` is agreement,
|
|
1693
1882
|
not doubt.
|
|
1694
|
-
-
|
|
1695
|
-
|
|
1883
|
+
- _The claim ledger accumulated for agents that never declared a
|
|
1884
|
+
contract_, and appended by whole-array spread. It is now gated on
|
|
1696
1885
|
`.claims()` and appends without the quadratic copy — restoring the
|
|
1697
1886
|
zero-delta promise for every agent that configured none of this.
|
|
1698
1887
|
|
|
1699
1888
|
- **Two honesty defects in `find_context_errors`**, both caught by the same
|
|
1700
1889
|
adversarial review and both red-proved by a failing test first.
|
|
1701
|
-
|
|
1890
|
+
|
|
1891
|
+
- _The green headline never read the counts it claimed to summarise._
|
|
1702
1892
|
"The checkers below RAN; nothing they cover was violated" was printed on
|
|
1703
1893
|
the mere absence of a finding, so a run whose every encounter was
|
|
1704
1894
|
`unreachable` (the check could not see the evidence), whose checks all
|
|
@@ -1711,7 +1901,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1711
1901
|
all, each keep their own sentence. A checker ROW with zero `checked`
|
|
1712
1902
|
stopped saying "the checker ran and found nothing at this seam" for the
|
|
1713
1903
|
same reason.
|
|
1714
|
-
-
|
|
1904
|
+
- _The tool advertised defect classes no check can file._ The `kind` enum
|
|
1715
1905
|
was pinned to the whole `ContextErrorKind` union, so it offered
|
|
1716
1906
|
`unsupported-argument` and `duplicate-execution` — classes no check in
|
|
1717
1907
|
this build files — and answering one returned a negative verdict about
|
|
@@ -1776,7 +1966,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1776
1966
|
run's disposition rows ride along, so "the checkers ran and found nothing"
|
|
1777
1967
|
and "no checker was registered for that seam" stay different answers, and
|
|
1778
1968
|
a registered check with zero encounters is named as wiring rot. Honest
|
|
1779
|
-
absence has its own sentences: no event tail is
|
|
1969
|
+
absence has its own sentences: no event tail is _no finding evidence_, a
|
|
1780
1970
|
tail with no integrity events says the channel is empty and why, and rows
|
|
1781
1971
|
reporting findings the tail no longer carries say **evidence missing** —
|
|
1782
1972
|
none of them ever reads as "no context errors found". Synthetic canaries
|
|
@@ -1812,7 +2002,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1812
2002
|
- **`Tool.argumentsFrom` + the dangling-reference check** (the closure
|
|
1813
2003
|
check's decidable fragment). A tool author can now declare where a tool's
|
|
1814
2004
|
arguments come from — `defineTool({ name: 'screen_fire', argumentsFrom:
|
|
1815
|
-
|
|
2005
|
+
['whats_here'] })` — and `callLLM` checks at request assembly that every
|
|
1816
2006
|
served tool's declared grounds still have results in the window: a ground
|
|
1817
2007
|
the window ledger says was evicted (`droppedObservations`) with nothing
|
|
1818
2008
|
re-established files a `dangling-reference` finding at seam `'compose'`,
|
|
@@ -1856,10 +2046,10 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1856
2046
|
|
|
1857
2047
|
- **The assertion algebra and the one visible finding type.** An
|
|
1858
2048
|
`Assertion` is keyed by `(subject, predicate, epoch)` with two rules
|
|
1859
|
-
that do the work:
|
|
2049
|
+
that do the work: _serving is asserting; history is quotation_ (checks
|
|
1860
2050
|
never fire across the quoted stratum — the whole stale-but-honest
|
|
1861
|
-
false-positive class, closed structurally) and
|
|
1862
|
-
|
|
2051
|
+
false-positive class, closed structurally) and _single-valued by
|
|
2052
|
+
default_ (you declare exemptions, never rules). Unknown `Claim`s never
|
|
1863
2053
|
participate in a comparison. `conflictsOf()` is the pure exclusion
|
|
1864
2054
|
comparison; `ContextError` is the uniform finding — plain kinds
|
|
1865
2055
|
(`invariant-violation`, `unsupported-argument`, `dangling-reference`,
|
|
@@ -1913,16 +2103,16 @@ when this patch was cut, and they are additive.
|
|
|
1913
2103
|
### Fixed
|
|
1914
2104
|
|
|
1915
2105
|
- **The cache report no longer invents a cause for a turn it could not
|
|
1916
|
-
measure.**
|
|
2106
|
+
measure.** _What changed:_ when `cacheRecorder().report()` hands back an
|
|
1917
2107
|
unknown hit rate, the sentence attached to it is now the one the calls
|
|
1918
2108
|
themselves gave — most often "no CacheStrategy was given to
|
|
1919
2109
|
`cacheRecorder()`, so nothing read the usage". When the calls disagree about
|
|
1920
2110
|
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.
|
|
1922
|
-
there
|
|
2111
|
+
a count of the rest) rather than silently choosing one. _Why it was not
|
|
2112
|
+
there:_ the summary sentence was typed into the code as a fixed string, "the
|
|
1923
2113
|
provider reported no cache fields" — true for the case the author had in
|
|
1924
2114
|
mind, a guess for every other, and it overwrote what each call had already
|
|
1925
|
-
stated.
|
|
2115
|
+
stated. _How it improves:_ run without a strategy against a provider that DID
|
|
1926
2116
|
report cache traffic, and 9.59.0 told you your provider was reporting
|
|
1927
2117
|
nothing — sending you off to debug a provider that was working fine, instead
|
|
1928
2118
|
of naming the one line missing from your own setup. You now read the real
|
|
@@ -1945,113 +2135,113 @@ the published bytes and is unchanged.
|
|
|
1945
2135
|
|
|
1946
2136
|
### Fixed
|
|
1947
2137
|
|
|
1948
|
-
- **A red gate could reach npm, and did.**
|
|
2138
|
+
- **A red gate could reach npm, and did.** _What changed:_ the docs-truth
|
|
1949
2139
|
ratchet now runs inside the build job that the publishing job depends on,
|
|
1950
2140
|
so a red gate fails the build and the publish never starts — however the
|
|
1951
|
-
release was created.
|
|
2141
|
+
release was created. _Why it was not there:_ the gate existed, but CI ran it
|
|
1952
2142
|
on `push` while the publish workflow runs on `release: published`, and the
|
|
1953
2143
|
release script fires the release seconds after the push. The two raced, and
|
|
1954
|
-
npm never waited for a verdict.
|
|
2144
|
+
npm never waited for a verdict. _How it improves:_ 9.58.0 shipped with a red
|
|
1955
2145
|
ratchet; that is now structurally impossible.
|
|
1956
|
-
- **A generated report was being edited by hand.**
|
|
2146
|
+
- **A generated report was being edited by hand.** _What changed:_
|
|
1957
2147
|
`npm run docs:truth:report` regenerates the report **without** touching the
|
|
1958
2148
|
accepted-debt baseline, and a test fails if the committed report is not what
|
|
1959
|
-
the generator produces.
|
|
2149
|
+
the generator produces. _Why it was not there:_ the report could only be
|
|
1960
2150
|
regenerated by also re-recording the debt, so an author who merely wanted the
|
|
1961
2151
|
numbers to match reality had to choose between accepting unrelated debt and
|
|
1962
2152
|
editing the file. They edited the file: the 9.58.0 release commit changed
|
|
1963
2153
|
"103 typed events" to "105" and nothing else, leaving the export count stale
|
|
1964
|
-
and the ratchet red.
|
|
2154
|
+
and the ratchet red. _How it improves:_ restating the truth and accepting
|
|
1965
2155
|
debt are now separate acts, and the file cannot silently drift.
|
|
1966
2156
|
- **The cache meter reported 0% for turns that hit cache on every call.**
|
|
1967
|
-
|
|
2157
|
+
_What changed:_ the strategies read the framework's normalised usage instead
|
|
1968
2158
|
of raw provider field names, and every number in the report is now a `Claim`
|
|
1969
|
-
— a value that says how it knows itself.
|
|
2159
|
+
— a value that says how it knows itself. _Why it was not there:_ the
|
|
1970
2160
|
strategies parsed `cache_read_input_tokens` off a value that has never
|
|
1971
2161
|
carried it, so every field read as missing and nothing was recorded; and the
|
|
1972
2162
|
report typed its totals as plain numbers, so "nobody measured" and "measured,
|
|
1973
2163
|
and it was zero" looked identical. The test fixtures were themselves
|
|
1974
2164
|
provider-shaped, which is how it survived a release with a green suite.
|
|
1975
|
-
|
|
2165
|
+
_How it improves:_ a real hit rate, an unmeasured turn that renders as
|
|
1976
2166
|
unmeasured, and a rate that always states its own denominator ("3 of 20
|
|
1977
2167
|
calls"). A **silent non-cache** — a prompt below the model's minimum
|
|
1978
2168
|
cacheable size, which providers process without caching and without an error
|
|
1979
2169
|
— is now visibly different from a turn nobody measured.
|
|
1980
|
-
- **A meter was attached to a provider that cannot feed it.**
|
|
2170
|
+
- **A meter was attached to a provider that cannot feed it.** _What changed:_
|
|
1981
2171
|
the Bedrock strategy declares itself disabled, passes requests through
|
|
1982
|
-
untouched, and answers "not applicable" with the reason.
|
|
1983
|
-
there
|
|
2172
|
+
untouched, and answers "not applicable" with the reason. _Why it was not
|
|
2173
|
+
there:_ it claimed full support and clamped cache markers onto a request
|
|
1984
2174
|
field our Bedrock provider discards, reporting markers that never reached a
|
|
1985
|
-
wire.
|
|
2175
|
+
wire. _How it improves:_ it stays registered, so a Bedrock user is told the
|
|
1986
2176
|
truth by name instead of guessing. The same honesty is applied to OpenAI,
|
|
1987
2177
|
which is the costlier gap because it caches automatically.
|
|
1988
2178
|
|
|
1989
2179
|
### Changed
|
|
1990
2180
|
|
|
1991
|
-
- **A parked map now tells the model it is parked.**
|
|
2181
|
+
- **A parked map now tells the model it is parked.** _What changed:_ while
|
|
1992
2182
|
anything is parked, the model receives a short status card naming the cursor
|
|
1993
2183
|
and the engagement as **separate** things, the reason, and the way back as a
|
|
1994
|
-
concrete call.
|
|
2184
|
+
concrete call. _Why it was not there:_ every honesty signal landed on the
|
|
1995
2185
|
record, which the model never reads — so re-engagement was reachable in
|
|
1996
2186
|
principle and unreachable in practice, because nothing told the model that
|
|
1997
|
-
re-picking a skill it appears to already be in means anything.
|
|
1998
|
-
improves
|
|
2187
|
+
re-picking a skill it appears to already be in means anything. _How it
|
|
2188
|
+
improves:_ a door the model can see. It also learns the distinction the
|
|
1999
2189
|
kernel is built on instead of inferring a contradiction.
|
|
2000
|
-
- **A parked map's tools actually leave the wire.**
|
|
2190
|
+
- **A parked map's tools actually leave the wire.** _What changed:_ parking now
|
|
2001
2191
|
holds the parked map's tool schemas off the request on its own authority.
|
|
2002
|
-
|
|
2192
|
+
_Why it was not there:_ `.maps()` promised parking stops "the prompt fragment
|
|
2003
2193
|
and tools", but on the default posture for flat graphs only the fragment
|
|
2004
2194
|
stopped — the model was shown tools for a skill whose instructions had just
|
|
2005
|
-
vanished.
|
|
2195
|
+
vanished. _How it improves:_ the promise is true on every posture. This is
|
|
2006
2196
|
**not** a change to `scopeTools` and does not touch the 10.0.0 ledger: those
|
|
2007
2197
|
dials answer different questions.
|
|
2008
2198
|
- **A pick of the skill you are already on is now a legal re-engagement.**
|
|
2009
|
-
|
|
2010
|
-
member it re-engages the map and does not move the cursor.
|
|
2011
|
-
there
|
|
2199
|
+
_What changed:_ a `read_skill` pick is routed by intent — of a parked map's
|
|
2200
|
+
member it re-engages the map and does not move the cursor. _Why it was not
|
|
2201
|
+
there:_ the reachability gate refuses a pick of the node the cursor occupies,
|
|
2012
2202
|
which is right for a move and wrong for this; since parking never moves the
|
|
2013
2203
|
cursor, a parked map is parked exactly where the model wants to return. For a
|
|
2014
|
-
single-member map that made parking permanent.
|
|
2204
|
+
single-member map that made parking permanent. _How it improves:_ the
|
|
2015
2205
|
documented recovery door is real.
|
|
2016
|
-
- **An engagement's founding cause is never rewritten.**
|
|
2206
|
+
- **An engagement's founding cause is never rewritten.** _What changed:_ the
|
|
2017
2207
|
record now keeps three separate facts — why the map is participating at all,
|
|
2018
2208
|
why the cursor is on this member, and why this contribution is being served
|
|
2019
|
-
right now.
|
|
2209
|
+
right now. _Why it was not there:_ one field answered all three, so
|
|
2020
2210
|
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.
|
|
2022
|
-
improves
|
|
2211
|
+
keyword at iteration 1 later read as system-backed since iteration 1. _How it
|
|
2212
|
+
improves:_ an incident review can still ask "was this founded on a guess?"
|
|
2023
2213
|
and get the true answer, however much has happened since.
|
|
2024
|
-
- **A declared route no longer forges a user request.**
|
|
2214
|
+
- **A declared route no longer forges a user request.** _What changed:_ when a
|
|
2025
2215
|
declared edge moves the cursor to a different member, that member's
|
|
2026
2216
|
eligibility is worked out from **its own** evidence rather than inherited.
|
|
2027
|
-
|
|
2217
|
+
_Why it was not there:_ the next member inherited the previous one's
|
|
2028
2218
|
standing, and an explicit request never decays — so one explicit pick at the
|
|
2029
2219
|
top of a turn silently warranted every skill the graph walked to afterwards.
|
|
2030
|
-
|
|
2220
|
+
_How it improves:_ a member entered weakly can park, exactly as if it had
|
|
2031
2221
|
been entered that way from the start. The founding cause still says, forever,
|
|
2032
2222
|
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.**
|
|
2223
|
+
- **A new turn is judged on the new turn's evidence.** _What changed:_ a cursor
|
|
2034
2224
|
carried into a new turn with nothing explaining it is recorded as `assumed`
|
|
2035
2225
|
— nobody said why — which is the weakest rung and expires like any guess.
|
|
2036
|
-
|
|
2226
|
+
_Why it was not there:_ an absent explanation was recorded as system-backed,
|
|
2037
2227
|
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.
|
|
2039
|
-
improves
|
|
2228
|
+
became a permanent warrant on turn two and every turn after. _How it
|
|
2229
|
+
improves:_ cursor continuity and engagement continuity have independent
|
|
2040
2230
|
lifetimes, and both are now pinned by tests — half the defect was that
|
|
2041
2231
|
nothing pinned either.
|
|
2042
|
-
- **The idle test checks all three of the conditions it documents.**
|
|
2043
|
-
changed
|
|
2044
|
-
none of its tools was called, and the turn went elsewhere.
|
|
2045
|
-
there
|
|
2046
|
-
|
|
2232
|
+
- **The idle test checks all three of the conditions it documents.** _What
|
|
2233
|
+
changed:_ a map only accrues idle when its contribution was actually served,
|
|
2234
|
+
none of its tools was called, and the turn went elsewhere. _Why it was not
|
|
2235
|
+
there:_ only the last was checked, while the refusal text asserted all three.
|
|
2236
|
+
_How it improves:_ nothing claims a fact it did not check. (Making it honest
|
|
2047
2237
|
needed the kernel to carry its own record of what was served — reading it
|
|
2048
2238
|
from the existing delta machinery was silently empty in the grouped chart
|
|
2049
2239
|
shape, which would have disabled parking there entirely.)
|
|
2050
|
-
- **A map that cannot explain its cursor moves is refused at mount.**
|
|
2051
|
-
changed
|
|
2052
|
-
was not there
|
|
2053
|
-
whole job is weighing evidence had none to weigh — silently.
|
|
2054
|
-
improves
|
|
2240
|
+
- **A map that cannot explain its cursor moves is refused at mount.** _What
|
|
2241
|
+
changed:_ `.maps()` requires a map that reports why the cursor moved. _Why it
|
|
2242
|
+
was not there:_ without it, no explanation ever arrives, and a kernel whose
|
|
2243
|
+
whole job is weighing evidence had none to weigh — silently. _How it
|
|
2244
|
+
improves:_ the failure is named at build time instead of at 3am.
|
|
2055
2245
|
|
|
2056
2246
|
### Added
|
|
2057
2247
|
|
|
@@ -2061,9 +2251,9 @@ the published bytes and is unchanged.
|
|
|
2061
2251
|
shipped and was unreachable: the kernel's data model carried the field and
|
|
2062
2252
|
no option ever set it. It suppresses the park, not the measurement — the
|
|
2063
2253
|
record still shows a map riding every call unused.
|
|
2064
|
-
- **Two documentation pages**:
|
|
2065
|
-
ladder, the idle test, the three facts, turns, and the park card) and
|
|
2066
|
-
cache
|
|
2254
|
+
- **Two documentation pages**: _Mounted maps_ (the kernel's why, the evidence
|
|
2255
|
+
ladder, the idle test, the three facts, turns, and the park card) and _The
|
|
2256
|
+
cache meter_ (how to read a report, and exactly which providers can feed it).
|
|
2067
2257
|
- **A measurement, not a fix, for the prompt-cache cost of `read_skill`.** The
|
|
2068
2258
|
tool's description is rebuilt from the cursor every iteration, and changing a
|
|
2069
2259
|
tool definition invalidates a provider's entire prompt cache. A shipped test
|
|
@@ -2081,7 +2271,6 @@ through `isKnown(...)`, or render them with `describeClaim(...)`. This is a
|
|
|
2081
2271
|
signature change in a minor release, and deliberately so: the old bare numbers
|
|
2082
2272
|
were not a contract worth keeping, because the value was zero on every turn.
|
|
2083
2273
|
|
|
2084
|
-
|
|
2085
2274
|
## [9.58.0] - 2026-08-20
|
|
2086
2275
|
|
|
2087
2276
|
**A map's cursor says where it stands. Nothing said whether it had earned
|
|
@@ -2177,7 +2366,7 @@ to do, and no longer had the evidence to do it.
|
|
|
2177
2366
|
What it did next was invent. It took an entity name it remembered plus the
|
|
2178
2367
|
shape of an id it had used earlier, assembled one that has never existed, and
|
|
2179
2368
|
was refused — a wasted action out of a small budget. In one archived run the
|
|
2180
|
-
final answer to the
|
|
2369
|
+
final answer to the _person_ named a host that appears in no tool result at
|
|
2181
2370
|
all.
|
|
2182
2371
|
|
|
2183
2372
|
Nothing in the conversation said the evidence had gone. That is the release.
|
|
@@ -2200,7 +2389,7 @@ Nothing in the conversation said the evidence had gone. That is the release.
|
|
|
2200
2389
|
already inside `keepRecentTurns` costs nothing at all. Nothing at or before
|
|
2201
2390
|
the current request is pinnable, so a new user turn releases the whole
|
|
2202
2391
|
previous loop. The floor is `1 request + keepLastToolResults pins +
|
|
2203
|
-
|
|
2392
|
+
keepRecentTurns turns`, whatever your tool count, iteration count or run
|
|
2204
2393
|
length.
|
|
2205
2394
|
|
|
2206
2395
|
And a pin that BLOCKS is worse than a pin that misses: when two consecutive
|
|
@@ -2211,9 +2400,9 @@ Nothing in the conversation said the evidence had gone. That is the release.
|
|
|
2211
2400
|
`keepLastToolResults: false` (or `0`) reproduces 9.56.0 byte for byte.
|
|
2212
2401
|
|
|
2213
2402
|
- **A drop now says whose results it took.** The authored notice gains one
|
|
2214
|
-
sentence:
|
|
2403
|
+
sentence: _"Tool results are among them (whats_here, pan_view) — call the
|
|
2215
2404
|
tool again if you need its output; do not reconstruct ids or values from
|
|
2216
|
-
memory."
|
|
2405
|
+
memory."_ The drop is now STATED rather than silent — whether that sentence
|
|
2217
2406
|
changes what a model does next is **not measured here**: the five archived
|
|
2218
2407
|
runs have not been re-run with it on, so it ships as an honesty fix and not
|
|
2219
2408
|
as a performance claim. Tool names are the only caller data that reaches it, and they
|
|
@@ -2239,13 +2428,13 @@ Nothing in the conversation said the evidence had gone. That is the release.
|
|
|
2239
2428
|
> You are on action 25 of 30; 5 remain. Finish what you have rather than
|
|
2240
2429
|
> start something new.
|
|
2241
2430
|
|
|
2242
|
-
Measured, not decorative: given its remaining budget a model wrote
|
|
2243
|
-
steps left, enough to finish this properly"
|
|
2431
|
+
Measured, not decorative: given its remaining budget a model wrote _"I have 5
|
|
2432
|
+
steps left, enough to finish this properly"_ and landed the task, where
|
|
2244
2433
|
before it spiralled and produced no answer at all.
|
|
2245
2434
|
|
|
2246
2435
|
The vocabulary is closed rather than a `(ctx) => string` because of
|
|
2247
2436
|
**absence**. Given a function, an author writes `${ctx.maxIterations}` and
|
|
2248
|
-
ships
|
|
2437
|
+
ships _"23 of undefined"_, or writes `?? 0` and ships a fabricated
|
|
2249
2438
|
denominator that nothing — and no model — can tell from a real zero. With
|
|
2250
2439
|
named slots the library owns absence and applies one rule: if any named fact
|
|
2251
2440
|
is unavailable, the whole instruction is skipped, by name, as
|
|
@@ -2269,7 +2458,7 @@ Nothing in the conversation said the evidence had gone. That is the release.
|
|
|
2269
2458
|
message that would become the head was already a user turn (the pinned
|
|
2270
2459
|
request, or an older turn of a restored conversation) no notice was owed at
|
|
2271
2460
|
all. Its 245–358 characters were nevertheless allowed to veto a legitimate
|
|
2272
|
-
drop; and because the removable span is the longest
|
|
2461
|
+
drop; and because the removable span is the longest _contiguous_ run, the
|
|
2273
2462
|
same verdict came back at every boundary while the window grew without
|
|
2274
2463
|
bound.
|
|
2275
2464
|
|
|
@@ -2474,11 +2663,11 @@ they do.
|
|
|
2474
2663
|
`unknown`, so a surface that guessed at it would put words in a tool's
|
|
2475
2664
|
mouth. One rule, and it is the whole rule:
|
|
2476
2665
|
|
|
2477
|
-
| your payload
|
|
2478
|
-
|
|
|
2479
|
-
| `{ message: 'Hop 3 of 12' }` | `Hop 3 of 12` — your sentence, verbatim
|
|
2480
|
-
| `{ done: 3, total: 12 }`
|
|
2481
|
-
| `'a bare string'`
|
|
2666
|
+
| your payload | the line a person reads |
|
|
2667
|
+
| ---------------------------- | ------------------------------------------------ |
|
|
2668
|
+
| `{ message: 'Hop 3 of 12' }` | `Hop 3 of 12` — your sentence, verbatim |
|
|
2669
|
+
| `{ done: 3, total: 12 }` | `` `walk_graph` reported progress (3 so far)… `` |
|
|
2670
|
+
| `'a bare string'` | the same generic line |
|
|
2482
2671
|
|
|
2483
2672
|
A top-level string field named **`message`** is shown verbatim, trimmed, and
|
|
2484
2673
|
cut at **120 characters** with the cut stated (`… (+N more)`) — `message` is
|
|
@@ -2504,8 +2693,8 @@ they do.
|
|
|
2504
2693
|
`tool_end` used to be able to clear a SIBLING call's status, leaving the
|
|
2505
2694
|
bubble blank while a tool was still working.
|
|
2506
2695
|
|
|
2507
|
-
- **Commentary narrates the middle**, so recordings replay it:
|
|
2508
|
-
`walk_graph` tool reported progress while it was still running."
|
|
2696
|
+
- **Commentary narrates the middle**, so recordings replay it: _"The
|
|
2697
|
+
`walk_graph` tool reported progress while it was still running."_ The
|
|
2509
2698
|
teaching voice states the fact and never the payload — the same split the
|
|
2510
2699
|
Lens teaching view keeps, in the same words.
|
|
2511
2700
|
|
|
@@ -2559,8 +2748,10 @@ tool's name on it.
|
|
|
2559
2748
|
series: [{ t: '2026-08-19T10:00:00Z', entity: 'fc1/3', metric: 'avg_iops', value: 18450 }],
|
|
2560
2749
|
grain: { interval: '30m', aggregation: 'avg', is_counter: false },
|
|
2561
2750
|
provenance: { measured_at: '2026-08-19T10:20:00Z', source: 'InfluxDB SwitchPortStats' },
|
|
2562
|
-
coverage: {
|
|
2563
|
-
|
|
2751
|
+
coverage: {
|
|
2752
|
+
checked: ['fabric A: all 48 ports'],
|
|
2753
|
+
notChecked: [{ what: 'the peer fabric', why: 'collector scoped to one fabric' }],
|
|
2754
|
+
},
|
|
2564
2755
|
render: { default: 'table', columns: ['entity', 'value'], sort: 'value desc' },
|
|
2565
2756
|
});
|
|
2566
2757
|
```
|
|
@@ -2612,7 +2803,7 @@ tool's name on it.
|
|
|
2612
2803
|
- Zero-cost when unused: recognition is strict (`af_semantics: true` AND the
|
|
2613
2804
|
whole rule set), so every value any tool has ever returned keeps its bytes.
|
|
2614
2805
|
Runnable example: `examples/features/66-semantic-envelope.ts`. Guide:
|
|
2615
|
-
docs
|
|
2806
|
+
docs _Build → Semantic tool results_.
|
|
2616
2807
|
|
|
2617
2808
|
## [9.52.0] - 2026-08-19
|
|
2618
2809
|
|
|
@@ -2692,7 +2883,7 @@ opaque function on a route edge.
|
|
|
2692
2883
|
|
|
2693
2884
|
- **`guard:` on route edges — guards as data (the `when` predicate's declared
|
|
2694
2885
|
twin).** `.route(a, b, { guard: { riskLevel: { in: ['high','critical'] },
|
|
2695
|
-
|
|
2886
|
+
score: { gte: 0.7 } } })` — conditions over the hop (`toolName`, `result`,
|
|
2696
2887
|
`status`, `iteration`, `userMessage`, `currentSkillId`) and over the tool
|
|
2697
2888
|
result's own top-level JSON fields, operators `eq/ne/gt/gte/lt/lte/in/notIn`
|
|
2698
2889
|
(deliberately footprintjs's `WhereFilter` grammar, mirrored door-locally —
|
|
@@ -2702,6 +2893,7 @@ opaque function on a route edge.
|
|
|
2702
2893
|
outcome, AND these conditions"). ONE compilation produces the predicate
|
|
2703
2894
|
that routes, the serializable `SkillGuardData`, and the evidence evaluator
|
|
2704
2895
|
— so the three can never describe different guards. What being data buys:
|
|
2896
|
+
|
|
2705
2897
|
- **the check-up proves contradictions** — new ERROR
|
|
2706
2898
|
`guard-unsatisfiable`: crossed bounds (`gt: 5, lt: 3`), `eq` a same-key
|
|
2707
2899
|
`ne`/`in`/`notIn` excludes, a `status` outside the closed result-status
|
|
@@ -2722,7 +2914,7 @@ opaque function on a route edge.
|
|
|
2722
2914
|
evaluation: `guard` on the taken hop (verdict `true`) and `guardsClosed`
|
|
2723
2915
|
for refusals (verdict `false`, at most one per edge per iteration, on
|
|
2724
2916
|
whatever move resulted — a stay says `score gte 0.7 — saw "0.2" →
|
|
2725
|
-
|
|
2917
|
+
failed`). Agents without guards keep byte-identical events.
|
|
2726
2918
|
|
|
2727
2919
|
- **SkillMap & SkillWalker are now the official names.** You declare the
|
|
2728
2920
|
**SkillMap**; the agent is the **SkillWalker**; the recording carries both.
|
|
@@ -2805,7 +2997,7 @@ is the whole release.
|
|
|
2805
2997
|
### Notes for recording consumers (the lens, triage platforms)
|
|
2806
2998
|
|
|
2807
2999
|
- Draw the declared topology from `skill.graph_declared` (`declaredSource:
|
|
2808
|
-
|
|
3000
|
+
'recording'` can now mean COMPLETE); filter `from !== null` for
|
|
2809
3001
|
node-to-node edges, exactly as with a built graph's `edges`.
|
|
2810
3002
|
- Fill per-beat reachability from `cursorMove.reachable` first; the refusal's
|
|
2811
3003
|
`allowed` and declared-edge folds remain as fallbacks for older eras.
|
|
@@ -2864,7 +3056,7 @@ is the whole release.
|
|
|
2864
3056
|
`recording.json`, the manifest carries a note naming the fact and the line
|
|
2865
3057
|
that supplies it, and `BugReportUnit.enveloped` says per conversation which
|
|
2866
3058
|
shape it got. Nothing is stamped that was not known.
|
|
2867
|
-
- **The evidence is never packed twice** — an envelope
|
|
3059
|
+
- **The evidence is never packed twice** — an envelope _or_ a bare recording,
|
|
2868
3060
|
never both. The zip is store-only, so a duplicated recording is duplicated
|
|
2869
3061
|
bytes against the size ceiling the trim hints exist to keep a reporter under.
|
|
2870
3062
|
- The GitHub issue body names the file that is really in the bundle, including
|
|
@@ -3415,10 +3607,9 @@ answer for. These are ours, built from that evidence. The credit is the field's.
|
|
|
3415
3607
|
### Fixed
|
|
3416
3608
|
|
|
3417
3609
|
- Two unanchored `coverage` rules in `.gitignore` matched `src/core/agent/
|
|
3418
|
-
|
|
3610
|
+
coverage/`, so a new source directory would have been absent from every clone.
|
|
3419
3611
|
Anchored to the vitest report directory, with a comment naming why.
|
|
3420
3612
|
|
|
3421
|
-
|
|
3422
3613
|
## [9.42.0] - 2026-08-15
|
|
3423
3614
|
|
|
3424
3615
|
**Comparing strategies with statistics, and retention on the session port.**
|
|
@@ -3504,7 +3695,6 @@ answer for. These are ours, built from that evidence. The credit is the field's.
|
|
|
3504
3695
|
from a counter, so they differ in their prefix and the fold never shows.
|
|
3505
3696
|
Fixing it re-keys stored sessions. Next release.
|
|
3506
3697
|
|
|
3507
|
-
|
|
3508
3698
|
## [9.41.0] - 2026-08-15
|
|
3509
3699
|
|
|
3510
3700
|
**Comparing strategies, and proving a store.** An audit asked whether this
|
|
@@ -3574,7 +3764,6 @@ key, and the proof obligations for the second port.
|
|
|
3574
3764
|
- `MemoryDefinition` gains optional declared `strategy`, `retrieval` and
|
|
3575
3765
|
`embedderId` — additive, and what makes a memory row worth grouping on.
|
|
3576
3766
|
|
|
3577
|
-
|
|
3578
3767
|
## [9.40.0] - 2026-08-15
|
|
3579
3768
|
|
|
3580
3769
|
**Two identity bugs, one of which had been paying out zeros since v2.8.**
|
|
@@ -3625,7 +3814,6 @@ key, and the proof obligations for the second port.
|
|
|
3625
3814
|
`provider` now flows through to `CostTick` as well (optional; absent stays
|
|
3626
3815
|
absent rather than becoming `'unknown'`).
|
|
3627
3816
|
|
|
3628
|
-
|
|
3629
3817
|
## [9.39.0] - 2026-08-14
|
|
3630
3818
|
|
|
3631
3819
|
**Three false doors closed, and two promises the recording did not keep.** An
|
|
@@ -3664,7 +3852,7 @@ forking. Mostly yes — but the first thing such an author reads was wrong.
|
|
|
3664
3852
|
was reserved. Not re-exported from any barrel, not an `exports` subpath, not
|
|
3665
3853
|
in the shipped-surface baseline — so nothing public could reach it and its
|
|
3666
3854
|
removal is not breaking. The by-instance door (`agent.enable.observability({
|
|
3667
|
-
|
|
3855
|
+
strategy })`) is real, first-class, and now the only one. The cache twin
|
|
3668
3856
|
legitimately has two doors; this one had one door and a sign pointing at a
|
|
3669
3857
|
wall.
|
|
3670
3858
|
|
|
@@ -3688,7 +3876,6 @@ forking. Mostly yes — but the first thing such an author reads was wrong.
|
|
|
3688
3876
|
`enable.cost()` receives zeros. This release makes `model` resolve there; the
|
|
3689
3877
|
numbers are still zero. Its own ticket.
|
|
3690
3878
|
|
|
3691
|
-
|
|
3692
3879
|
## [9.38.0] - 2026-08-14
|
|
3693
3880
|
|
|
3694
3881
|
**Two behaviour changes that are bug fixes — a call that used to run now
|
|
@@ -4056,12 +4243,13 @@ labels." Ours had no rule at all. Now it does, and it is enforced, not
|
|
|
4056
4243
|
promised.
|
|
4057
4244
|
|
|
4058
4245
|
### What it is, and — because the name invites the wrong reading — what it
|
|
4246
|
+
|
|
4059
4247
|
### is provably not
|
|
4060
4248
|
|
|
4061
4249
|
It is a **fabrication detector, not a correctness judge.** If a value in
|
|
4062
4250
|
the answer never appeared in anything a tool returned this turn, the model
|
|
4063
4251
|
typed it rather than read it, and that is all this checks. It cannot catch
|
|
4064
|
-
a false claim built entirely from real values —
|
|
4252
|
+
a false claim built entirely from real values — _"fc1/3 is healthy"_ when
|
|
4065
4253
|
the data says the port is down uses two grounded tokens and sails through.
|
|
4066
4254
|
It cannot catch a real value attached to the wrong thing, a fabricated
|
|
4067
4255
|
quantity under the digit threshold, or a fabricated name spelled in
|
|
@@ -4116,7 +4304,7 @@ is not judged at all, and a turn the iteration or cost limit just cut
|
|
|
4116
4304
|
short does not get to spend a revision it can't afford. It composes with,
|
|
4117
4305
|
rather than collides with, `.reliability()`: reliability governs whether a
|
|
4118
4306
|
call is retried before anything is committed; this governs an answer
|
|
4119
|
-
|
|
4307
|
+
_after_ it has already been committed to the transcript.
|
|
4120
4308
|
|
|
4121
4309
|
### Measured, not assumed
|
|
4122
4310
|
|
|
@@ -4195,7 +4383,7 @@ never touched `footprintjs` itself but imports the file that now did.
|
|
|
4195
4383
|
- `isDevMode` (a `footprintjs` import) → a bound `devWarn()` / `devMode()`
|
|
4196
4384
|
reader that the host supplies; every existing warning reads verbatim, and
|
|
4197
4385
|
the existing `enableDevMode()` tests pass unchanged — the proof that
|
|
4198
|
-
nothing about
|
|
4386
|
+
nothing about _what gets warned_ moved, only _how it's asked_.
|
|
4199
4387
|
- `ToolResultStatus` → pulled out to a zero-import leaf and re-exported from
|
|
4200
4388
|
its old home, so the envelope grammar is unchanged for every existing
|
|
4201
4389
|
caller. The fence caught four more inline crossings of this type that
|
|
@@ -4358,7 +4546,7 @@ was in the bundle — an empty bundle read as "nobody was turned away" when
|
|
|
4358
4546
|
it only meant "nobody ran." `standingAgent({ onIngressDecision })` now
|
|
4359
4547
|
hands your sink one `IngressRecord` per request, filed at the terminal the
|
|
4360
4548
|
reply actually reached. The honest contract is stated in the type itself:
|
|
4361
|
-
`'served'` means **delivered**, not
|
|
4549
|
+
`'served'` means **delivered**, not _admitted_ — a request the door let
|
|
4362
4550
|
through whose run, store or provider then broke files as `'failed'`, and
|
|
4363
4551
|
the record carries the admission verdict (`allow` / `queue` / `refuse`)
|
|
4364
4552
|
either way. It is a stream you chain into your own sink, not a join onto
|
|
@@ -4490,7 +4678,7 @@ read/write." Both, plus `decayPolicy`, are now carried under prefixed
|
|
|
4490
4678
|
metadata keys and restored verbatim on read. A caller's own value under one
|
|
4491
4679
|
of the three keys this adapter generates (`source`, `resourceName`,
|
|
4492
4680
|
`distance`) is refused by name rather than silently shadowed — recognized by
|
|
4493
|
-
|
|
4681
|
+
_identity_, not shape, so a caller's own `distance: 12` cannot be mistaken
|
|
4494
4682
|
for this adapter's. An oversized carried field is refused rather than
|
|
4495
4683
|
truncated: provenance that came back shortened would be provenance nobody
|
|
4496
4684
|
could tell was shortened.
|
|
@@ -4680,7 +4868,7 @@ effects. Every line follows the same rules as the rest of the layer:
|
|
|
4680
4868
|
retry happened — no event attests one, so the words don't either.
|
|
4681
4869
|
- **Sizes humanized, and the two units told apart.** `humanizeBytes` /
|
|
4682
4870
|
`humanizeChars` — `41.0 KB` where the ceiling counts bytes, `240,000
|
|
4683
|
-
|
|
4871
|
+
characters` where it counts characters (`tools.result_refused` counts
|
|
4684
4872
|
characters, because that's what the limit does).
|
|
4685
4873
|
- **Refs and digests stay out of prose.** They identify a row for the details
|
|
4686
4874
|
panel; a reader doesn't read them. The repeated-call nudge's fingerprints
|
|
@@ -4734,8 +4922,8 @@ import { googleIdentity } from 'agentfootprint/security';
|
|
|
4734
4922
|
now declares the same `ranksBy: 'server-text'` for the identical reason, so
|
|
4735
4923
|
the two server-ranked stores no longer disagree on how they say what they
|
|
4736
4924
|
are.)
|
|
4737
|
-
- **`googleIdentity`** — a narrow `CredentialProvider`: it vends
|
|
4738
|
-
access tokens for
|
|
4925
|
+
- **`googleIdentity`** — a narrow `CredentialProvider`: it vends _Google_
|
|
4926
|
+
access tokens for _Google_ APIs from whatever credential the environment
|
|
4739
4927
|
already has (ADC, workload identity, an impersonated service account).
|
|
4740
4928
|
`mode: 'user'` is refused by name rather than quietly served a machine
|
|
4741
4929
|
token, since Google's user-token equivalent has no Node surface yet.
|
|
@@ -4810,7 +4998,9 @@ provider scopes a vault on:
|
|
|
4810
4998
|
|
|
4811
4999
|
```ts
|
|
4812
5000
|
await standingAgent({
|
|
4813
|
-
agent,
|
|
5001
|
+
agent,
|
|
5002
|
+
sessions,
|
|
5003
|
+
host: nodeHost({ port: 8080 }),
|
|
4814
5004
|
identity: {
|
|
4815
5005
|
verify: jwksIdentity({
|
|
4816
5006
|
jwksUrl: 'https://idp.example.com/.well-known/jwks.json',
|
|
@@ -4823,7 +5013,7 @@ await standingAgent({
|
|
|
4823
5013
|
|
|
4824
5014
|
**Configured is closed-by-default.** A request with no `Authorization`
|
|
4825
5015
|
header is refused (401) unless `allowAnonymous: true` is set, and a request
|
|
4826
|
-
that
|
|
5016
|
+
that _names_ a `userId` without proving it is refused either way — a door
|
|
4827
5017
|
that verifies a token when offered and waves the request through when it is
|
|
4828
5018
|
not is a door anybody opens by sending less. `jwksIdentity` is the one
|
|
4829
5019
|
adapter this release ships (`jose`, loaded lazily, pinned against a real
|
|
@@ -4842,7 +5032,9 @@ Nothing else reads them; they do not enter the run's own identity tuple.
|
|
|
4842
5032
|
|
|
4843
5033
|
```ts
|
|
4844
5034
|
await standingAgent({
|
|
4845
|
-
agent,
|
|
5035
|
+
agent,
|
|
5036
|
+
sessions,
|
|
5037
|
+
host,
|
|
4846
5038
|
identity: { verify },
|
|
4847
5039
|
admission: turnsPerHour({ limit: 60 }),
|
|
4848
5040
|
});
|
|
@@ -5027,8 +5219,12 @@ wire, scoped to the session that asks.**
|
|
|
5027
5219
|
### Added — two wire operations on the existing invoke path
|
|
5028
5220
|
|
|
5029
5221
|
```ts
|
|
5030
|
-
{
|
|
5031
|
-
|
|
5222
|
+
{
|
|
5223
|
+
op: 'artifact-head', ref;
|
|
5224
|
+
} // → meta
|
|
5225
|
+
{
|
|
5226
|
+
op: 'artifact-get', ref;
|
|
5227
|
+
} // → meta + data
|
|
5032
5228
|
```
|
|
5033
5229
|
|
|
5034
5230
|
Resolved under the requesting session's identity-composed scope — exactly
|
|
@@ -5079,7 +5275,9 @@ lines.**
|
|
|
5079
5275
|
defineTool({
|
|
5080
5276
|
name: 'summarize',
|
|
5081
5277
|
wants: { dataset: 'dataset/rows' },
|
|
5082
|
-
execute: async (args, ctx) => {
|
|
5278
|
+
execute: async (args, ctx) => {
|
|
5279
|
+
/* args.dataset is the RESOLVED DATA */
|
|
5280
|
+
},
|
|
5083
5281
|
});
|
|
5084
5282
|
```
|
|
5085
5283
|
|
|
@@ -5267,7 +5465,7 @@ on `llm_start`.
|
|
|
5267
5465
|
Precedence is stated and enforced, most specific wins: **escalation
|
|
5268
5466
|
brain > per-skill brain > `.configure()`'s run model > the build
|
|
5269
5467
|
default.** A brain naming only a model inherits the agent's own provider;
|
|
5270
|
-
a brain naming a
|
|
5468
|
+
a brain naming a _foreign_ provider without a model is refused at
|
|
5271
5469
|
`Agent.build()` — the run's configured model belongs to another vendor's
|
|
5272
5470
|
namespace and would fail mid-turn, on exactly the iteration the cursor
|
|
5273
5471
|
enters the skill.
|
|
@@ -5304,12 +5502,12 @@ returns today — stay byte-identical; the envelope is recognized only by
|
|
|
5304
5502
|
its own strict shape.
|
|
5305
5503
|
|
|
5306
5504
|
- **`propose-transition`** — `{ kind: 'propose-transition', targetSkillId,
|
|
5307
|
-
|
|
5308
|
-
|
|
5505
|
+
reason }`. The typed replacement for a string routing marker: the
|
|
5506
|
+
_graph_ decides. A same-batch declared edge still wins; an unreachable
|
|
5309
5507
|
target is refused out loud, not silently dropped.
|
|
5310
5508
|
- **`require-instruction`** — `{ kind: 'require-instruction',
|
|
5311
|
-
|
|
5312
|
-
Pushes a
|
|
5509
|
+
instructionId, deliveryLease: 'next-call' | 'until-skill-exit' }`.
|
|
5510
|
+
Pushes a _registered_ instruction into the coming call(s) —
|
|
5313
5511
|
`read_skill` stays the pull door for optional knowledge; this is the
|
|
5314
5512
|
push door for mandatory procedure, and it only pushes what was
|
|
5315
5513
|
registered at build. An unknown id is refused, never improvised.
|
|
@@ -5493,7 +5691,7 @@ unmatched, never as an uncontested winner.
|
|
|
5493
5691
|
### Zero-cost when unused
|
|
5494
5692
|
|
|
5495
5693
|
Graphs that use none of the new options are byte-identical in behavior
|
|
5496
|
-
|
|
5694
|
+
_and_ events to 9.16.0 — pinned by regression tests. 78+ new tests cover
|
|
5497
5695
|
the cascade, the scorers, the strictness postures, and continuity.
|
|
5498
5696
|
|
|
5499
5697
|
### Deferred
|
|
@@ -5528,6 +5726,7 @@ ordering bug with no error, no log line, just a silently different cursor.
|
|
|
5528
5726
|
|
|
5529
5727
|
On the record, so a trace answers "why didn't the second call route?"
|
|
5530
5728
|
instead of leaving the reader to guess.
|
|
5729
|
+
|
|
5531
5730
|
- Single-tool iterations are byte-identical to 9.15.0 — this only changes
|
|
5532
5731
|
behavior when a batch actually contains more than one result.
|
|
5533
5732
|
- New `AgentState.toolResults` / `InjectionContext.toolResults`: the full
|
|
@@ -5542,7 +5741,7 @@ ordering bug with no error, no log line, just a silently different cursor.
|
|
|
5542
5741
|
Wiring a `.skillGraph(...)` onto a classic agent meant the graph would still
|
|
5543
5742
|
route and the trace would still show an activation — but the model never saw
|
|
5544
5743
|
the newly-active skill's prompt or tools, because the slot it would have
|
|
5545
|
-
changed was already frozen. The configuration
|
|
5744
|
+
changed was already frozen. The configuration _looked_ like it worked and
|
|
5546
5745
|
didn't.
|
|
5547
5746
|
|
|
5548
5747
|
`Agent.build()` now refuses this combination outright, naming both the
|
|
@@ -5596,7 +5795,7 @@ const graph = skillGraph({
|
|
|
5596
5795
|
|
|
5597
5796
|
What previously required `autoActivate: 'currentSkill'` typed on every single
|
|
5598
5797
|
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
|
|
5798
|
+
wins — the graph sets a default, never an override. Only _wired_ skills (named
|
|
5600
5799
|
by an entry or a route) are stamped: an unwired skill's tools would otherwise
|
|
5601
5800
|
never appear at all. With the dial absent or `false`, compiled skills are
|
|
5602
5801
|
byte-identical to 9.14.0 (pinned by test). On a `tree()` graph the flat-arm
|
|
@@ -5677,10 +5876,10 @@ const memory = defineMemory({
|
|
|
5677
5876
|
type: MEMORY_TYPES.EPISODIC,
|
|
5678
5877
|
strategy: {
|
|
5679
5878
|
kind: MEMORY_STRATEGIES.SUMMARIZE,
|
|
5680
|
-
recent: 6,
|
|
5681
|
-
size: 20,
|
|
5682
|
-
llm: anthropic(),
|
|
5683
|
-
model: 'claude-haiku-4-5',
|
|
5879
|
+
recent: 6, // the 6 newest entries stay verbatim
|
|
5880
|
+
size: 20, // how much history to load per turn
|
|
5881
|
+
llm: anthropic(), // its OWN instance, not the agent's
|
|
5882
|
+
model: 'claude-haiku-4-5', // named explicitly — no fallback
|
|
5684
5883
|
},
|
|
5685
5884
|
store,
|
|
5686
5885
|
});
|
|
@@ -5710,8 +5909,8 @@ quietly bills your MAIN model for compression, and a different vendor is sent a
|
|
|
5710
5909
|
model id it has never heard of, mid-conversation, on a paid run.
|
|
5711
5910
|
|
|
5712
5911
|
`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
|
|
5714
|
-
|
|
5912
|
+
**instance** at the agent's own model (the narrow 8.14.0 rule; a _second
|
|
5913
|
+
instance_ of the same vendor at the same model is allowed and sometimes right).
|
|
5715
5914
|
`defineMemory` cannot make that check — it has never heard of an agent — so a
|
|
5716
5915
|
`MemoryDefinition` now declares `billing: { provider, model }` and the builder
|
|
5717
5916
|
reads it, the same field and shape `WindowStrategy.billing` already used. One
|
|
@@ -5824,7 +6023,7 @@ why, with a date.
|
|
|
5824
6023
|
import { gemini } from 'agentfootprint/providers';
|
|
5825
6024
|
|
|
5826
6025
|
const vertex = gemini({ project: 'my-project', location: 'us-central1' }); // ADC
|
|
5827
|
-
const studio = gemini({ apiKey: process.env.GEMINI_API_KEY! });
|
|
6026
|
+
const studio = gemini({ apiKey: process.env.GEMINI_API_KEY! }); // one key
|
|
5828
6027
|
```
|
|
5829
6028
|
|
|
5830
6029
|
Two doors, one adapter, and neither is guessed: a project selects Vertex, a key
|
|
@@ -5877,8 +6076,8 @@ Three decisions worth reading before you rely on them:
|
|
|
5877
6076
|
that arrives anyway is kept out of the visible answer on both paths.
|
|
5878
6077
|
- **A stream that reports no usage reports ZERO, never an estimate.**
|
|
5879
6078
|
`models.countTokens` is on the namespace, is not called, and is named in the pin
|
|
5880
|
-
as not called: it answers what a request
|
|
5881
|
-
|
|
6079
|
+
as not called: it answers what a request _tokenises to_, not what the call was
|
|
6080
|
+
_billed for_. Same law as `openai()` and `ollama()`. (Usage is read off the
|
|
5882
6081
|
closing chunk BEFORE any content guard — the bug that made streamed turns bill
|
|
5883
6082
|
as zero on two earlier adapters.)
|
|
5884
6083
|
|
|
@@ -5950,8 +6149,8 @@ new and load-bearing.
|
|
|
5950
6149
|
|
|
5951
6150
|
### Added — Gemini's over-long-request sentence joins the typed error
|
|
5952
6151
|
|
|
5953
|
-
`ContextWindowExceededError` (9.6.0) now translates
|
|
5954
|
-
(1200293) exceeds the maximum number of tokens allowed (1048576)."
|
|
6152
|
+
`ContextWindowExceededError` (9.6.0) now translates _"The input token count
|
|
6153
|
+
(1200293) exceeds the maximum number of tokens allowed (1048576)."_ — a word order
|
|
5955
6154
|
the existing patterns did not match — and reads both numbers out of it, including
|
|
5956
6155
|
the case where Google ships the first parenthesis empty. Detection stays
|
|
5957
6156
|
conservative: "INPUT token count" is what keeps it off a `max_tokens` validation
|
|
@@ -6000,8 +6199,8 @@ concurrency-and-sessions section, and a status row per boundary.
|
|
|
6000
6199
|
- A `gemini()` error never prints the API key it was constructed with. The
|
|
6001
6200
|
redaction is narrow by design — the exact string you passed, removed from the
|
|
6002
6201
|
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
|
|
6004
|
-
|
|
6202
|
+
a thrown provider error reaches the model as a tool result _and_ the commit log
|
|
6203
|
+
_and_ every observability sink, so one interpolation would leak to all of them.
|
|
6005
6204
|
|
|
6006
6205
|
## [9.12.0] - 2026-08-12
|
|
6007
6206
|
|
|
@@ -6022,7 +6221,7 @@ provider signed:
|
|
|
6022
6221
|
const credentials = agentCoreIdentity({
|
|
6023
6222
|
region: 'us-west-2',
|
|
6024
6223
|
workloadName: 'workflow_assistant_agent',
|
|
6025
|
-
requireUserToken: true,
|
|
6224
|
+
requireUserToken: true, // optional — refuse a delegated call with no proof
|
|
6026
6225
|
});
|
|
6027
6226
|
|
|
6028
6227
|
// inside a tool
|
|
@@ -6037,7 +6236,7 @@ await ctx.credentials.getCredential({ service: 'google', mode: 'user', userToken
|
|
|
6037
6236
|
- **Nothing downstream changed.** `GetWorkloadAccessTokenForJWT` answers with the
|
|
6038
6237
|
same `workloadAccessToken` the by-userId exchange does, so it feeds the same
|
|
6039
6238
|
`GetResourceOauth2Token` call, the same `Credential`, the same `toHeaders()`.
|
|
6040
|
-
The vault entry at the end belongs to the
|
|
6239
|
+
The vault entry at the end belongs to the _person_ rather than to the agent,
|
|
6041
6240
|
which is what makes revoking their access actually revoke it.
|
|
6042
6241
|
- **Verified against the real SDK before it shipped**, names and shapes both:
|
|
6043
6242
|
`{ workloadName, userToken }` in, `{ workloadAccessToken }` out. It joins the
|
|
@@ -6121,7 +6320,7 @@ refuses a call it used to allow, or emits a field it did not before.
|
|
|
6121
6320
|
### Added — `maxToolResultChars`: a ceiling on ONE tool result
|
|
6122
6321
|
|
|
6123
6322
|
```ts
|
|
6124
|
-
Agent.create({ provider, model, maxToolResultChars: 20_000 })
|
|
6323
|
+
Agent.create({ provider, model, maxToolResultChars: 20_000 });
|
|
6125
6324
|
```
|
|
6126
6325
|
|
|
6127
6326
|
Over the cap, the result is **replaced** by a marker that tells the model what
|
|
@@ -6161,8 +6360,8 @@ happened and what to do about it:
|
|
|
6161
6360
|
### Added — WHO the run was for, on every event
|
|
6162
6361
|
|
|
6163
6362
|
`EventMeta.principal` and `EventMeta.tenant` (9.11.0) join `sessionId` (9.4.0) on
|
|
6164
|
-
every event's meta. The stream has always said
|
|
6165
|
-
is the
|
|
6363
|
+
every event's meta. The stream has always said _what_ happened and _when_; this
|
|
6364
|
+
is the _who_, and the three together are an audit record rather than a debug log.
|
|
6166
6365
|
|
|
6167
6366
|
```ts
|
|
6168
6367
|
await agent.run(message, {
|
|
@@ -6183,7 +6382,7 @@ await agent.run(message, {
|
|
|
6183
6382
|
and `sessionId` beside it is the fact the transport delivered.
|
|
6184
6383
|
- **Which sinks carry it, checked rather than assumed.** `fileObservability`,
|
|
6185
6384
|
`cloudwatchObservability`, `agentcoreObservability` and `auditExport` serialize
|
|
6186
|
-
the whole envelope and inherit it for free — in `auditExport`'s case
|
|
6385
|
+
the whole envelope and inherit it for free — in `auditExport`'s case _inside_
|
|
6187
6386
|
the hash chain, so editing who breaks the same verification as editing what.
|
|
6188
6387
|
`otelObservability` maps signals onto spans rather than serializing, so the
|
|
6189
6388
|
actor is PLACED there: `agentfootprint.principal.id` /
|
|
@@ -6209,7 +6408,7 @@ const policy = PermissionPolicy.fromRoles(roles, 'support', {
|
|
|
6209
6408
|
```
|
|
6210
6409
|
|
|
6211
6410
|
- **`Tool.capabilities`** (`'memory_read' | 'memory_write' | 'external_net' |
|
|
6212
|
-
|
|
6411
|
+
'user_data'`) is a declaration, never an inference. A tool's reach is not
|
|
6213
6412
|
knowable from its name, schema or description, and guessing would rest a policy
|
|
6214
6413
|
decision on a heuristic.
|
|
6215
6414
|
- **`PermissionChecker.governs`** is an optional, feature-detected member —
|
|
@@ -6244,8 +6443,8 @@ PermissionPolicy.fromRoles(roles, 'support', {
|
|
|
6244
6443
|
- The refusal lands **before `execute`**, so a `surfaceMode: 'tool-only'` skill's
|
|
6245
6444
|
body is never even computed.
|
|
6246
6445
|
- **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
|
|
6248
|
-
|
|
6446
|
+
reachable from here" because a cursor can move; a hidden skill is about _who is
|
|
6447
|
+
asking_, and naming it would tell one role about another role's capabilities.
|
|
6249
6448
|
- **The enum stays the full catalog.** `toolArgValidation` runs before the gate,
|
|
6250
6449
|
so narrowing it would turn a policy refusal into a generic schema error and the
|
|
6251
6450
|
model would never read the policy's own message — the reasoning 8.5.0 recorded
|
|
@@ -6295,7 +6494,7 @@ await standingAgent({
|
|
|
6295
6494
|
agentFactory: () => Agent.create({ provider, model }).system('…').build(),
|
|
6296
6495
|
sessions: sqliteSessions({ file: './sessions.db' }),
|
|
6297
6496
|
host: nodeHost({ port: 8080 }),
|
|
6298
|
-
maxActiveSessions: 200,
|
|
6497
|
+
maxActiveSessions: 200, // default 100
|
|
6299
6498
|
});
|
|
6300
6499
|
```
|
|
6301
6500
|
|
|
@@ -6393,7 +6592,6 @@ nodeHost({ sessionCookie: 'af_session' }); // …or no client code at all
|
|
|
6393
6592
|
- New example: `examples/deploy/multi-user.ts` — two people served at once,
|
|
6394
6593
|
proving the overlap in wall clock and that neither saw the other's memory.
|
|
6395
6594
|
|
|
6396
|
-
|
|
6397
6595
|
## [9.9.0] - 2026-08-12
|
|
6398
6596
|
|
|
6399
6597
|
**A bug report IS the evidence.**
|
|
@@ -6411,7 +6609,7 @@ with the prose attached.
|
|
|
6411
6609
|
```ts
|
|
6412
6610
|
import { describeBugReport, exportBugReport } from 'agentfootprint/observe';
|
|
6413
6611
|
|
|
6414
|
-
const offer = describeBugReport(recording);
|
|
6612
|
+
const offer = describeBugReport(recording); // measure — nothing has left yet
|
|
6415
6613
|
// …show offer.units to the human; they tick some…
|
|
6416
6614
|
const report = exportBugReport(recording, {
|
|
6417
6615
|
include: ['conv-1', 'file-conversation', 'file-environment'],
|
|
@@ -6420,7 +6618,7 @@ const report = exportBugReport(recording, {
|
|
|
6420
6618
|
expected: 'the updated price',
|
|
6421
6619
|
actual: 'the price from before the update',
|
|
6422
6620
|
});
|
|
6423
|
-
fs.writeFileSync(report.filename, report.zip);
|
|
6621
|
+
fs.writeFileSync(report.filename, report.zip); // a real .zip
|
|
6424
6622
|
```
|
|
6425
6623
|
|
|
6426
6624
|
- **The manifest is SELECTABLE UNITS, not a blob.** Each conversation is a unit
|
|
@@ -6438,7 +6636,7 @@ fs.writeFileSync(report.filename, report.zip); // a real .zip
|
|
|
6438
6636
|
- **What was left out is STATED.** `manifest.excluded` counts the conversations,
|
|
6439
6637
|
files, events and turns that were withheld, names their unit ids, and the
|
|
6440
6638
|
issue body repeats it. A maintainer reading turn 4 must be able to tell that
|
|
6441
|
-
turns 1–3 were
|
|
6639
|
+
turns 1–3 were _withheld_, not _lost_.
|
|
6442
6640
|
- **Redacted keys, BY NAME.** The recording arrives already redacted (footprintjs
|
|
6443
6641
|
scrubs at commit time), so nothing here scrubs anything — it would be too late
|
|
6444
6642
|
to matter and a second policy could only disagree with the first. Instead the
|
|
@@ -6474,9 +6672,9 @@ plain `fetch` with no SDK:
|
|
|
6474
6672
|
|
|
6475
6673
|
```ts
|
|
6476
6674
|
const reporter = githubBugReporter({
|
|
6477
|
-
issueRepo: 'footprintjs/agentfootprint',
|
|
6478
|
-
evidenceRepo: 'acme/af-bug-evidence',
|
|
6479
|
-
});
|
|
6675
|
+
issueRepo: 'footprintjs/agentfootprint', // public — the conversation
|
|
6676
|
+
evidenceRepo: 'acme/af-bug-evidence', // private — the run
|
|
6677
|
+
}); // token: GITHUB_TOKEN, or `token`
|
|
6480
6678
|
const { issueUrl, zipUrl } = await reporter.file(report);
|
|
6481
6679
|
```
|
|
6482
6680
|
|
|
@@ -6517,13 +6715,13 @@ const { issueUrl, zipUrl } = await reporter.file(report);
|
|
|
6517
6715
|
calls — browser-safe and server-safe, no client secret, no dependency:
|
|
6518
6716
|
|
|
6519
6717
|
```ts
|
|
6520
|
-
const signIn = await githubDeviceSignIn({ clientId });
|
|
6718
|
+
const signIn = await githubDeviceSignIn({ clientId }); // returns at once
|
|
6521
6719
|
show(`Open ${signIn.verificationUri} and enter ${signIn.userCode}`);
|
|
6522
|
-
const { token, login } = await signIn.completed;
|
|
6720
|
+
const { token, login } = await signIn.completed; // resolves on approve
|
|
6523
6721
|
```
|
|
6524
6722
|
|
|
6525
|
-
A server PAT files every report as the
|
|
6526
|
-
|
|
6723
|
+
A server PAT files every report as the _application_; this files it as the
|
|
6724
|
+
_reporter_, which is what a field tester filing upstream needs. It honours
|
|
6527
6725
|
`slow_down`, respects the code's expiry, takes an `AbortSignal`, and fetches
|
|
6528
6726
|
`/user` for attribution (a `/user` that refuses is not fatal — the token still
|
|
6529
6727
|
works, the login is simply absent). The token it returns is handed to
|
|
@@ -6593,7 +6791,9 @@ agent.enable.observability({
|
|
|
6593
6791
|
maxBytes: 64 * 1024 * 1024,
|
|
6594
6792
|
}),
|
|
6595
6793
|
});
|
|
6596
|
-
process.on('SIGTERM', async () => {
|
|
6794
|
+
process.on('SIGTERM', async () => {
|
|
6795
|
+
await agent.shutdown();
|
|
6796
|
+
}); // flushes
|
|
6597
6797
|
```
|
|
6598
6798
|
|
|
6599
6799
|
Four things it is deliberate about:
|
|
@@ -6637,8 +6837,8 @@ import { vaultCredentials } from 'agentfootprint/security';
|
|
|
6637
6837
|
|
|
6638
6838
|
const credentials = vaultCredentials({
|
|
6639
6839
|
address: 'https://vault.internal:8200',
|
|
6640
|
-
paths: { github: 'ci/github' },
|
|
6641
|
-
});
|
|
6840
|
+
paths: { github: 'ci/github' }, // …or resolve(service), or neither
|
|
6841
|
+
}); // token: `token`, else VAULT_TOKEN
|
|
6642
6842
|
```
|
|
6643
6843
|
|
|
6644
6844
|
The tool code does not change from the `staticTokens` version — same port, same
|
|
@@ -6672,7 +6872,7 @@ service)` as the seam for a shop whose field names are its own.
|
|
|
6672
6872
|
the service, the mount path and the HTTP status — and nothing from the
|
|
6673
6873
|
response body, nothing from the token, not even the field names the secret
|
|
6674
6874
|
carries. This is the 8.6.0 law applied one adapter down: a thrown message
|
|
6675
|
-
reaches the model as a tool result
|
|
6875
|
+
reaches the model as a tool result _and_ rides
|
|
6676
6876
|
`agentfootprint.credential.failed` to every observer. It is pinned by a
|
|
6677
6877
|
grep-shaped test that walks every failure path — unknown service, 401, 403,
|
|
6678
6878
|
404, 503, a non-JSON reply, a KV v1 response, an unrecognised field set, and a
|
|
@@ -6685,21 +6885,21 @@ service)` as the seam for a shop whose field names are its own.
|
|
|
6685
6885
|
- **`infrastructure/on-premises.mdx`** — the provider page beside AWS. The
|
|
6686
6886
|
local-first ladder (mock → local model → your gateway → a paid API) as the
|
|
6687
6887
|
opening frame, then a service-by-service map: LLM (`ollama`, `openai({
|
|
6688
|
-
|
|
6888
|
+
baseURL })` for vLLM / llama.cpp / a corporate gateway, or the two-method
|
|
6689
6889
|
port), stores (`sqliteVectorStore`, `pgVectorStore`, `staticVectorStore`,
|
|
6690
6890
|
`RedisStore`), embedders (`localEmbedder`, `staticEmbedder`), hosting
|
|
6691
6891
|
(`httpHost` / `nodeHost` + `sqliteSessions` / `memorySessions`), code
|
|
6692
|
-
execution (`localCodeRunner` —
|
|
6892
|
+
execution (`localCodeRunner` — _isolation, not a sandbox_), telemetry
|
|
6693
6893
|
(`otelObservability` to any OTLP collector, `fileObservability` when there is
|
|
6694
6894
|
none, `auditExport` for evidence), credentials (`staticTokens`,
|
|
6695
6895
|
`vaultCredentials`, and the port for everything else) and tools (`mcpClient`
|
|
6696
6896
|
over stdio or Streamable HTTP). It ends with **what is NOT here** — no
|
|
6697
6897
|
Kubernetes-native anything, no second secret-manager adapter, no metrics
|
|
6698
6898
|
exporter, no retention policy, no air-gapped model distribution.
|
|
6699
|
-
- **The status vocabulary gained one honest rung.**
|
|
6700
|
-
awaiting field
|
|
6899
|
+
- **The status vocabulary gained one honest rung.** _Contract-shaped and tested;
|
|
6900
|
+
awaiting field use_ is what `fileObservability` and `vaultCredentials` carry:
|
|
6701
6901
|
their ports and refusals are pinned by tests, and neither has met a real
|
|
6702
|
-
production disk or vault.
|
|
6902
|
+
production disk or vault. _Verified in a production field deployment_ now
|
|
6703
6903
|
appears in exactly one place, describing a deployment **shape** — a standing
|
|
6704
6904
|
agent over `httpHost` + `sqliteSessions` against an OpenAI-compatible gateway,
|
|
6705
6905
|
the shape several past releases exist because of — and never an adapter. The
|
|
@@ -6730,14 +6930,14 @@ handed that said "this is over".
|
|
|
6730
6930
|
|
|
6731
6931
|
Three optional fields, sourced from what the engine already stamps:
|
|
6732
6932
|
|
|
6733
|
-
| field
|
|
6734
|
-
|
|
6735
|
-
| `ctx.runId`
|
|
6736
|
-
| `ctx.sessionId` | `run({ sessionId })` ← `HostRequest.sessionId` | the run is not session-bound
|
|
6737
|
-
| `ctx.identity`
|
|
6933
|
+
| field | source | absent when |
|
|
6934
|
+
| --------------- | ---------------------------------------------- | ----------------------------------------------------------------------- |
|
|
6935
|
+
| `ctx.runId` | the run in flight | there is no run — a call served over `mcpServe` is one call, not a turn |
|
|
6936
|
+
| `ctx.sessionId` | `run({ sessionId })` ← `HostRequest.sessionId` | the run is not session-bound |
|
|
6937
|
+
| `ctx.identity` | the identity the CALLER passed | the caller passed none |
|
|
6738
6938
|
|
|
6739
6939
|
Every one is **absent rather than invented**, which is the 9.4.0 rule applied one
|
|
6740
|
-
layer down. `ctx.identity` is deliberately
|
|
6940
|
+
layer down. `ctx.identity` is deliberately _not_ the run's internal
|
|
6741
6941
|
`runIdentity`: that is always populated, defaulting to
|
|
6742
6942
|
`{ conversationId: '<runId>' }`, and handing a tool a synthesized conversation as
|
|
6743
6943
|
"the identity" would let it isolate a live sandbox on a fiction.
|
|
@@ -6767,12 +6967,12 @@ tool that wants a run-scoped session needs to know it is at a door with no runs
|
|
|
6767
6967
|
BEFORE it opens one. Asking for a scope a door cannot honour throws, naming the
|
|
6768
6968
|
door.
|
|
6769
6969
|
|
|
6770
|
-
| scope
|
|
6771
|
-
|
|
6772
|
-
| `'call'`
|
|
6773
|
-
| `'run'`
|
|
6774
|
-
| `'session'`
|
|
6775
|
-
| `'shutdown'` | `agent.shutdown()`.
|
|
6970
|
+
| scope | fires |
|
|
6971
|
+
| ------------ | ------------------------------------------------------------------------------------- |
|
|
6972
|
+
| `'call'` | when `tool.execute` settles — resolve **or** throw. Every door, including `mcpServe`. |
|
|
6973
|
+
| `'run'` | at a run terminal that is **not a pause**. |
|
|
6974
|
+
| `'session'` | `agent.closeToolSessions({ sessionId })`. |
|
|
6975
|
+
| `'shutdown'` | `agent.shutdown()`. |
|
|
6776
6976
|
|
|
6777
6977
|
Seven laws, each pinned: at most once ever · idempotent by `(tool, scope, key)`
|
|
6778
6978
|
with the FIRST registration winning (it holds the live handle) · reverse
|
|
@@ -6783,7 +6983,7 @@ tolerates "already gone" · nothing live is ever persisted into a checkpoint.
|
|
|
6783
6983
|
**A pause is not a terminal.** `'run'` teardown deliberately does not hang off
|
|
6784
6984
|
`finally`, which also runs on both pause shapes. A `checkIn` on a code
|
|
6785
6985
|
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
|
|
6986
|
+
down there destroys the exact state the resume needs, and it fails _quietly_ — as
|
|
6787
6987
|
a resumed run that "just re-ran everything". An error IS a terminal.
|
|
6788
6988
|
|
|
6789
6989
|
### Added — `agent.closeToolSessions({ sessionId })`
|
|
@@ -6872,7 +7072,7 @@ the rows, and what comes back is the finding.
|
|
|
6872
7072
|
`client.send(new Command(...))`, pinned in `test/adapters/aws/awsCommandPin.ts`
|
|
6873
7073
|
and **verified against a real install of the SDK before shipping** — including
|
|
6874
7074
|
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
|
|
7075
|
+
STREAM, and seven of its nine union members are modelled _exceptions_ (folded
|
|
6876
7076
|
in as empty output, an `AccessDenied` would have reported a clean run that
|
|
6877
7077
|
"printed nothing"), and `Stop` takes the session id, not a URI.
|
|
6878
7078
|
- **`codeRunnerTool({ runner, scope })`** (main barrel) — holds one session per
|
|
@@ -6937,8 +7137,8 @@ and now shares the paging with the general one.
|
|
|
6937
7137
|
**Behaviour change, named loudly: multi-turn memory starts actually
|
|
6938
7138
|
retaining.** A six-turn conversation stores twelve message entries where it
|
|
6939
7139
|
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
|
|
6941
|
-
now remembering what it was asked to
|
|
7140
|
+
exchange — so prompts get longer and stores get bigger _because the agent is
|
|
7141
|
+
now remembering what it was asked to remember_. Turn it down deliberately
|
|
6942
7142
|
(`size`, `DECAY`, `.compaction()`) rather than by accident.
|
|
6943
7143
|
|
|
6944
7144
|
Cost: one paged `list()` per store per run, and only when a memory actually
|
|
@@ -7187,7 +7387,7 @@ compaction that runs.
|
|
|
7187
7387
|
**AWS adapters tell the truth.**
|
|
7188
7388
|
|
|
7189
7389
|
A production field report tested 9.3.0 against a real account and found two
|
|
7190
|
-
adapters
|
|
7390
|
+
adapters _dispatching calls that were never made against AWS_ — one sending a
|
|
7191
7391
|
command that does not exist, one calling a method that a command-based client
|
|
7192
7392
|
does not have. Both compiled. Both had green tests. Every one of those tests
|
|
7193
7393
|
injected a double past the SDK, which is exactly why the bug class survives: the
|
|
@@ -7284,9 +7484,9 @@ are untouched.
|
|
|
7284
7484
|
|
|
7285
7485
|
### Changed — a fail-closed refusal now READS final
|
|
7286
7486
|
|
|
7287
|
-
When a `PermissionChecker` throws, the call is denied. What the model was
|
|
7487
|
+
When a `PermissionChecker` throws, the call is denied. What the model was _told_
|
|
7288
7488
|
was the checker's own thrown message — and those are written for operators:
|
|
7289
|
-
|
|
7489
|
+
_"not available right now"_, `ECONNREFUSED`, _"timed out"_. **Measured in
|
|
7290
7490
|
production: a real model read that as weather and retried the same tool to
|
|
7291
7491
|
`maxIterations`, then returned the empty string.** Against the local policy's
|
|
7292
7492
|
long-standing bracketed form the same model adapted cleanly on the first
|
|
@@ -7327,8 +7527,8 @@ calls did so in a silence that read like health.
|
|
|
7327
7527
|
### Added — `EventMeta.sessionId`: which CONVERSATION an event belongs to
|
|
7328
7528
|
|
|
7329
7529
|
`meta.runId` is per `run()` / `resume()`; a session spans many. A shipped
|
|
7330
|
-
telemetry stream could answer
|
|
7331
|
-
happened in this conversation?"
|
|
7530
|
+
telemetry stream could answer _"what happened in this run?"_ and not _"what
|
|
7531
|
+
happened in this conversation?"_ — the question a session-oriented host is built
|
|
7332
7532
|
around, and one the events alone cannot be joined back into afterwards.
|
|
7333
7533
|
|
|
7334
7534
|
`standingAgent` now threads the caller's own session id onto every event the run
|
|
@@ -7348,13 +7548,13 @@ three preconditions it had no evidence for, all three of which were wrong at
|
|
|
7348
7548
|
once:
|
|
7349
7549
|
|
|
7350
7550
|
- **A euclidean index was accepted.** The construction refusal read
|
|
7351
|
-
`options.distanceMetric`, a
|
|
7551
|
+
`options.distanceMetric`, a _claim by the caller_ about an index the store did
|
|
7352
7552
|
not create, so the default (undeclared) sailed through. Measured live: a
|
|
7353
7553
|
vector queried against itself returned **0.9991630113800056**, where a true
|
|
7354
7554
|
cosine self-similarity is exactly `1.0` — the very "number that READS like a
|
|
7355
7555
|
cosine and is not one" this adapter's own header warns about.
|
|
7356
7556
|
- **A missing `nonFilterableMetadataKeys: ['af']`** surfaced as a raw AWS
|
|
7357
|
-
`ValidationException` —
|
|
7557
|
+
`ValidationException` — _"Filterable metadata must have at most 2048 bytes"_ —
|
|
7358
7558
|
**mid-import**, with documents already written and success already reported
|
|
7359
7559
|
for them.
|
|
7360
7560
|
- **The index dimension was never compared with the embedder.**
|
|
@@ -7378,11 +7578,11 @@ index whose metric, layout and dimension it would be guessing at.
|
|
|
7378
7578
|
|
|
7379
7579
|
**Three promises the code had already made, kept.**
|
|
7380
7580
|
|
|
7381
|
-
`MemoryStore`'s own docstring has named its backends since 2.x —
|
|
7581
|
+
`MemoryStore`'s own docstring has named its backends since 2.x — _"Every storage
|
|
7382
7582
|
backend (InMemory, Redis, DynamoDB, **Postgres**, Bedrock AgentCore) implements
|
|
7383
|
-
this interface"
|
|
7384
|
-
would look:
|
|
7385
|
-
`putMany`, and
|
|
7583
|
+
this interface"_ — and named the queries too, in the two places an implementer
|
|
7584
|
+
would look: _"**Postgres**: multi-row INSERT … ON CONFLICT DO UPDATE"_ for
|
|
7585
|
+
`putMany`, and _"**pgvector**: `ORDER BY embedding <=> query LIMIT k`"_ for
|
|
7386
7586
|
`search`. Every one of those sentences was true about the design and false about
|
|
7387
7587
|
the shipped package. Two of the three items below are the same shape: a
|
|
7388
7588
|
documented promise with nothing behind it.
|
|
@@ -7546,8 +7746,8 @@ are now pinned; the rest are below.
|
|
|
7546
7746
|
### The conversation has a door with its own name
|
|
7547
7747
|
|
|
7548
7748
|
```ts
|
|
7549
|
-
await agent.run({ message: 'Book me a table for two.' });
|
|
7550
|
-
await agent.followUp('Make it three.');
|
|
7749
|
+
await agent.run({ message: 'Book me a table for two.' }); // one turn
|
|
7750
|
+
await agent.followUp('Make it three.'); // the next one
|
|
7551
7751
|
|
|
7552
7752
|
// …or hand the conversation around — plain JSON, any store, any machine:
|
|
7553
7753
|
const conversation = agent.checkpoint();
|
|
@@ -7605,7 +7805,7 @@ already broken in effect, so the throw is a fix, not a new restriction.
|
|
|
7605
7805
|
is what makes `checkpoint()`, `getLastSnapshot()` and `followUp()` possible.
|
|
7606
7806
|
Two overlapping runs both finished, both returning plausible answers, and the
|
|
7607
7807
|
state afterwards belonged to whichever finished last — so `checkpoint()` could
|
|
7608
|
-
hand back the
|
|
7808
|
+
hand back the _other_ run's conversation, with nothing in either recording
|
|
7609
7809
|
saying so. That is corruption, not concurrency. `standingAgent` has serialized
|
|
7610
7810
|
runs since it existed and calls it "a correctness requirement rather than a
|
|
7611
7811
|
tuning choice"; the guarantee now lives in the primitive. Two turns at once:
|
|
@@ -7619,16 +7819,16 @@ a consent gate any later message could walk around. Answer it with
|
|
|
7619
7819
|
`resume(checkpoint, decision)`, or say plainly that it is being dropped:
|
|
7620
7820
|
|
|
7621
7821
|
```ts
|
|
7622
|
-
const dropped = agent.abandonPause();
|
|
7822
|
+
const dropped = agent.abandonPause(); // { toolName, toolCallId, question }
|
|
7623
7823
|
await agent.run({ message: 'never mind, different question' });
|
|
7624
7824
|
```
|
|
7625
7825
|
|
|
7626
|
-
`abandonPause()`
|
|
7826
|
+
`abandonPause()` _returns_ what it dropped, so the abandonment can be logged
|
|
7627
7827
|
rather than performed blind.
|
|
7628
7828
|
|
|
7629
7829
|
**A pause belongs to a session, not to the instance.** `standingAgent` shares
|
|
7630
7830
|
one `Agent` across every session, so the instance guard alone would have let
|
|
7631
|
-
session A's unanswered question refuse session B's
|
|
7831
|
+
session A's unanswered question refuse session B's _first_ message — a
|
|
7632
7832
|
different conversation, a different person, an answer they were never asked
|
|
7633
7833
|
for. The composer now releases the instance at the moment ownership moves: once
|
|
7634
7834
|
the pause is in the store, the store owns it, and a later request carrying a
|
|
@@ -7649,7 +7849,7 @@ No shipped example, test or doc called it twice.
|
|
|
7649
7849
|
### `agent.canExplain()`
|
|
7650
7850
|
|
|
7651
7851
|
`.selfExplain()` was already honest to the **model** with no record bound — the
|
|
7652
|
-
trace tools answer
|
|
7852
|
+
trace tools answer _"No completed run is available yet"_ and the skill body says
|
|
7653
7853
|
to say so plainly. It had no answer for the **program**. `canExplain()` returns
|
|
7654
7854
|
`false` for two honest reasons — not built with `.selfExplain()`, or built with
|
|
7655
7855
|
it and no turn completed — so a caller can route a why-question before spending
|
|
@@ -7677,7 +7877,7 @@ of this class quietly. Three kinds of pin:
|
|
|
7677
7877
|
bare `throw`.
|
|
7678
7878
|
2. **Adapted** — pinned by what reaches the model / the store / the caller,
|
|
7679
7879
|
observed on the wire, never by mocking internals.
|
|
7680
|
-
3. **Stated** — pinned **twice**: the behavior,
|
|
7880
|
+
3. **Stated** — pinned **twice**: the behavior, _and_ the sentence in the source
|
|
7681
7881
|
that states it. A stated behavior whose statement was deleted is back to
|
|
7682
7882
|
being a silent success, and only the second assertion catches that.
|
|
7683
7883
|
|
|
@@ -7696,7 +7896,7 @@ decision somebody makes in a diff.
|
|
|
7696
7896
|
prints the messages the provider actually received for each. The model's own
|
|
7697
7897
|
"this is your first message" reply is the evidence.
|
|
7698
7898
|
- `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
|
|
7899
|
+
`followUp()`. It was a second `run()`, and the scripted mock read the _first_
|
|
7700
7900
|
user message, so the demo's own scripting was masking the restart. Live, the
|
|
7701
7901
|
why-question used to arrive with no subject.
|
|
7702
7902
|
- `docs-next/content/docs/build/conversations.mdx` — the conversation, the two
|
|
@@ -7708,13 +7908,13 @@ decision somebody makes in a diff.
|
|
|
7708
7908
|
Additive except for the three refusals, and all three fire on code that was
|
|
7709
7909
|
already wrong:
|
|
7710
7910
|
|
|
7711
|
-
| if you…
|
|
7712
|
-
|
|
|
7713
|
-
| call `run()` twice expecting continuity
|
|
7714
|
-
| overlap two `run()` calls on one agent
|
|
7715
|
-
| send a message while a pause is open
|
|
7716
|
-
| call `.system()` twice
|
|
7717
|
-
| continue a stored conversation on a differently-**named** agent | `ConversationMismatchError`
|
|
7911
|
+
| if you… | you now get | do this |
|
|
7912
|
+
| --------------------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------- |
|
|
7913
|
+
| call `run()` twice expecting continuity | the same behavior as before (a new conversation) | `followUp(message)` or `run({ message, continueFrom })` |
|
|
7914
|
+
| overlap two `run()` calls on one agent | `RunInFlightError` | await the first, or build a second agent |
|
|
7915
|
+
| send a message while a pause is open | `PendingQuestionError` | `resume(checkpoint, decision)`, or `abandonPause()` first |
|
|
7916
|
+
| call `.system()` twice | build-time throw | join the strings, or `.steering()` / `.configure()` |
|
|
7917
|
+
| continue a stored conversation on a differently-**named** agent | `ConversationMismatchError` | continue it on the agent whose id recorded it |
|
|
7718
7918
|
|
|
7719
7919
|
`checkpoint()` payloads written by 9.1.0 and earlier continue to work; they
|
|
7720
7920
|
simply carry no `identity` or `agent`, which is the honest answer for a
|
|
@@ -7745,13 +7945,13 @@ embedder might have — which then cuts every larger embedder short.
|
|
|
7745
7945
|
characters, that this embedder represents faithfully. Every shipped embedder
|
|
7746
7946
|
fills it in:
|
|
7747
7947
|
|
|
7748
|
-
| embedder
|
|
7749
|
-
|
|
7750
|
-
| `localEmbedder()`
|
|
7751
|
-
| `openaiEmbedder()`
|
|
7752
|
-
| `bedrockEmbedder()` | `32000`
|
|
7753
|
-
| `staticEmbedder()`
|
|
7754
|
-
| `mockEmbedder()`
|
|
7948
|
+
| embedder | `maxInputChars` | where the number comes from |
|
|
7949
|
+
| ------------------- | --------------- | -------------------------------------------------------------- |
|
|
7950
|
+
| `localEmbedder()` | `2000` | measured — the default model's 512-wordpiece-token cliff |
|
|
7951
|
+
| `openaiEmbedder()` | `32000` | the documented 8,191-token window, at 4 characters a token |
|
|
7952
|
+
| `bedrockEmbedder()` | `32000` | Titan's documented 8,192-token window, same conversion |
|
|
7953
|
+
| `staticEmbedder()` | `1000000` | no transformer, so no context window — nothing is ever clipped |
|
|
7954
|
+
| `mockEmbedder()` | `1000000` | reads every character in a loop |
|
|
7755
7955
|
|
|
7756
7956
|
`indexCorpus`, `indexFolder` and `indexDocuments` read the embedder's declared
|
|
7757
7957
|
ceiling **in preference to** their own 2,000-character default. An explicit
|
|
@@ -7769,7 +7969,7 @@ is accepted because the cliff belongs to the **model**, not to the factory.
|
|
|
7769
7969
|
### Truncation became visible
|
|
7770
7970
|
|
|
7771
7971
|
A run that clipped anything now says so — **once**, on `console.warn`, naming
|
|
7772
|
-
the count, the ceiling in effect
|
|
7972
|
+
the count, the ceiling in effect _and where that ceiling came from_, and the
|
|
7773
7973
|
two fixes (re-split smaller, or raise `maxChunkChars`). `IndexReport` gains
|
|
7774
7974
|
**`truncatedCount`** beside the existing `truncated` list: the list is what you
|
|
7775
7975
|
debug with, the count is what you assert on and what a dashboard row can hold.
|
|
@@ -7817,27 +8017,27 @@ deprecation warnings on 8.20.0, it compiles and runs unchanged on 9.0.0.**
|
|
|
7817
8017
|
|
|
7818
8018
|
### Removed — the sixteen door aliases
|
|
7819
8019
|
|
|
7820
|
-
Each removed path re-exported the
|
|
8020
|
+
Each removed path re-exported the _same symbols_ the door carries, never copies,
|
|
7821
8021
|
so this is a find-and-replace on import lines. No name moved; no name was lost.
|
|
7822
8022
|
|
|
7823
|
-
| you were importing from
|
|
7824
|
-
|
|
7825
|
-
| `agentfootprint/llm-providers`
|
|
7826
|
-
| `agentfootprint/embedders`
|
|
7827
|
-
| `agentfootprint/tool-providers`
|
|
7828
|
-
| `agentfootprint/thinking`
|
|
7829
|
-
| `agentfootprint/memory-providers`
|
|
7830
|
-
| `agentfootprint/observability-providers`
|
|
7831
|
-
| `agentfootprint/strategies`
|
|
7832
|
-
| `agentfootprint/stream`
|
|
7833
|
-
| `agentfootprint/status`
|
|
7834
|
-
| `agentfootprint/locales`
|
|
7835
|
-
| `agentfootprint/debug`
|
|
7836
|
-
| `agentfootprint/debug/finders`
|
|
7837
|
-
| `agentfootprint/observability/contextError/finders` | `agentfootprint/observe`
|
|
7838
|
-
| `agentfootprint/hosting-providers`
|
|
7839
|
-
| `agentfootprint/injection-engine`
|
|
7840
|
-
| `agentfootprint/identity`
|
|
8023
|
+
| you were importing from | import from |
|
|
8024
|
+
| --------------------------------------------------- | -------------------------- |
|
|
8025
|
+
| `agentfootprint/llm-providers` | `agentfootprint/providers` |
|
|
8026
|
+
| `agentfootprint/embedders` | `agentfootprint/providers` |
|
|
8027
|
+
| `agentfootprint/tool-providers` | `agentfootprint/providers` |
|
|
8028
|
+
| `agentfootprint/thinking` | `agentfootprint/providers` |
|
|
8029
|
+
| `agentfootprint/memory-providers` | `agentfootprint/memory` |
|
|
8030
|
+
| `agentfootprint/observability-providers` | `agentfootprint/observe` |
|
|
8031
|
+
| `agentfootprint/strategies` | `agentfootprint/observe` |
|
|
8032
|
+
| `agentfootprint/stream` | `agentfootprint/observe` |
|
|
8033
|
+
| `agentfootprint/status` | `agentfootprint/observe` |
|
|
8034
|
+
| `agentfootprint/locales` | `agentfootprint/observe` |
|
|
8035
|
+
| `agentfootprint/debug` | `agentfootprint/observe` |
|
|
8036
|
+
| `agentfootprint/debug/finders` | `agentfootprint/observe` |
|
|
8037
|
+
| `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
|
|
8038
|
+
| `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
|
|
8039
|
+
| `agentfootprint/injection-engine` | `agentfootprint/context` |
|
|
8040
|
+
| `agentfootprint/identity` | `agentfootprint/security` |
|
|
7841
8041
|
|
|
7842
8042
|
What ships now is exactly: the root barrel, the ten doors (`/providers`,
|
|
7843
8043
|
`/memory`, `/rag`, `/cache`, `/observe`, `/events`, `/context`, `/resilience`,
|
|
@@ -7853,23 +8053,23 @@ implementation barrel is served by its door as the same object.
|
|
|
7853
8053
|
|
|
7854
8054
|
### Removed — options, strings, methods, fields
|
|
7855
8055
|
|
|
7856
|
-
| removed
|
|
7857
|
-
|
|
7858
|
-
| `AgentBuilder.recorder(rec)`
|
|
7859
|
-
| `defineSkill({ viaToolName })`
|
|
7860
|
-
| `skillsFromDir(dir, { viaToolName })`
|
|
7861
|
-
| `WindowRefusalReason` member `'summary-not-smaller'`
|
|
7862
|
-
| type `FoldRefusal`
|
|
7863
|
-
| type `FoldRefusalReason`
|
|
7864
|
-
| `WindowStrategy` exported from `agentfootprint/memory` | `MemoryWindowStrategy`
|
|
7865
|
-
| `CompactionRecord.foldedStageIds`
|
|
7866
|
-
| `CompactionRecord.foldedMessageCount`
|
|
7867
|
-
| `ContextBudgetPressurePayload.capTokens`
|
|
7868
|
-
| `ContextBudgetPressurePayload.projectedTokens`
|
|
7869
|
-
| `BudgetPressureRecord.capTokens`
|
|
7870
|
-
| `BudgetPressureRecord.projectedTokens`
|
|
7871
|
-
|
|
7872
|
-
Three of those are worth a sentence each, because the
|
|
8056
|
+
| removed | replacement | since |
|
|
8057
|
+
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
|
|
8058
|
+
| `AgentBuilder.recorder(rec)` | `AgentBuilder.watch(rec)` — same list, same order, same attachment, and variadic | deprecated 8.0.0 |
|
|
8059
|
+
| `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 |
|
|
8060
|
+
| `skillsFromDir(dir, { viaToolName })` | drop it — same reason | deprecated 8.7.0 |
|
|
8061
|
+
| `WindowRefusalReason` member `'summary-not-smaller'` | `'replacement-not-smaller'` | renamed 8.14.0 |
|
|
8062
|
+
| type `FoldRefusal` | `WindowRefusal` | renamed 7.17 |
|
|
8063
|
+
| type `FoldRefusalReason` | `WindowRefusalReason` | renamed 7.17 |
|
|
8064
|
+
| `WindowStrategy` exported from `agentfootprint/memory` | `MemoryWindowStrategy` | renamed 7.27.1 |
|
|
8065
|
+
| `CompactionRecord.foldedStageIds` | `WindowRecord.removedStageIds` | family name published 7.17 |
|
|
8066
|
+
| `CompactionRecord.foldedMessageCount` | `WindowRecord.removedMessageCount` | family name published 7.17 |
|
|
8067
|
+
| `ContextBudgetPressurePayload.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
|
|
8068
|
+
| `ContextBudgetPressurePayload.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
|
|
8069
|
+
| `BudgetPressureRecord.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
|
|
8070
|
+
| `BudgetPressureRecord.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
|
|
8071
|
+
|
|
8072
|
+
Three of those are worth a sentence each, because the _reason_ is the migration:
|
|
7873
8073
|
|
|
7874
8074
|
- **`viaToolName` named a door that was never built.** The evaluator activates
|
|
7875
8075
|
an `llm-activated` skill by matching `ctx.activatedInjectionIds`, only
|
|
@@ -7908,7 +8108,7 @@ replacement and says when the signpost comes down.
|
|
|
7908
8108
|
Deleting the type member alone would have been a silent DOWNGRADE for the two
|
|
7909
8109
|
`viaToolName` cases: an object literal gets an excess-property error, but an
|
|
7910
8110
|
options bag arriving through a variable does not, and the value would then be
|
|
7911
|
-
|
|
8111
|
+
_ignored_ where 8.7.0 refused it. So the field is read at run time exactly once
|
|
7912
8112
|
more, to say it is gone.
|
|
7913
8113
|
|
|
7914
8114
|
### Two things deliberately kept
|
|
@@ -7923,7 +8123,7 @@ more, to say it is gone.
|
|
|
7923
8123
|
`CircuitState`, which is declared in both breaker files as
|
|
7924
8124
|
`'closed' | 'open' | 'half-open'` — two declarations, one type, pinned as such.
|
|
7925
8125
|
- **`buildRunSteps(events)` survives, still `@deprecated`.** Its deprecation is
|
|
7926
|
-
a
|
|
8126
|
+
a _preference_, not a migration: live consumers should attach
|
|
7927
8127
|
`runStepRecorder()` and read `getSteps()` (O(N), the house pattern) instead of
|
|
7928
8128
|
re-walking an event log (O(N²) across repeated calls). But the shim is the only
|
|
7929
8129
|
way to build steps from a saved event list — replay, post-hoc analysis, tests —
|
|
@@ -8000,7 +8200,7 @@ The fix is one coherent rule across the structural splitters:
|
|
|
8000
8200
|
alone. The heading is now glued to the first body paragraph; it can never
|
|
8001
8201
|
again be a chunk by itself.
|
|
8002
8202
|
- **The family was inspected, and two members are exempt by design.**
|
|
8003
|
-
`fixedWithOverlap` cuts uniformly sized chunks
|
|
8203
|
+
`fixedWithOverlap` cuts uniformly sized chunks _by request_ — imposing a
|
|
8004
8204
|
250-char floor on `fixedWithOverlap({ chars: 120 })` would repeal the
|
|
8005
8205
|
caller's own choice, and its only runt (the file tail) has always folded
|
|
8006
8206
|
backward. `wholeDocument` is one chunk per document by definition.
|
|
@@ -8054,7 +8254,7 @@ any immutable or serverless runtime has this problem):
|
|
|
8054
8254
|
- **`exportCorpus(store, identity?)`** (`agentfootprint/rag`) — every entry
|
|
8055
8255
|
of a corpus namespace as one plain-JSON `CorpusBundle`:
|
|
8056
8256
|
`{ entries: [{ id, text, vector, metadata }], embedder: { id, dimensions },
|
|
8057
|
-
|
|
8257
|
+
namespace }`. Plain JSON on purpose — the runtime that needs this is
|
|
8058
8258
|
exactly the runtime that cannot open a database file. It refuses an empty
|
|
8059
8259
|
namespace (naming the identity-mismatch cause), entries with no vector or
|
|
8060
8260
|
no passage (a bundle never ships an unservable or uncitable entry), and a
|
|
@@ -8216,7 +8416,7 @@ it, before a byte is embedded. `InMemoryStore` and `sqliteVectorStore` declare
|
|
|
8216
8416
|
**Behaviour change:** building a corpus into `AgentCoreStore` or `RedisStore` is
|
|
8217
8417
|
refused where it used to run. Neither could ever serve those vectors back —
|
|
8218
8418
|
`RedisStore` has no `search()` at all, so the same mistake already failed one
|
|
8219
|
-
layer later, when `defineRAG` refused the store
|
|
8419
|
+
layer later, when `defineRAG` refused the store _after_ the whole index had been
|
|
8220
8420
|
embedded and billed. The refusal moved to the call that starts the spending.
|
|
8221
8421
|
|
|
8222
8422
|
**Absence is not a `false`.** A store that declares nothing behaves exactly as it
|
|
@@ -8236,7 +8436,7 @@ it wrong.
|
|
|
8236
8436
|
```ts
|
|
8237
8437
|
import { bedrockEmbedder } from 'agentfootprint/providers';
|
|
8238
8438
|
|
|
8239
|
-
const embedder = bedrockEmbedder({ region: 'us-east-1' });
|
|
8439
|
+
const embedder = bedrockEmbedder({ region: 'us-east-1' }); // 1024-d
|
|
8240
8440
|
const small = bedrockEmbedder({ region: 'us-east-1', dimensions: 512 });
|
|
8241
8441
|
```
|
|
8242
8442
|
|
|
@@ -8409,7 +8609,7 @@ the run stops re-asking and names the rule — in the `outputAttempts` row
|
|
|
8409
8609
|
(`brokenBy`), in the event, in `outputContractUnmet()`, and in the warning.
|
|
8410
8610
|
Re-asking cannot fix a rule: a deterministic one breaks the next answer
|
|
8411
8611
|
identically, so the retries buy a repeat of the same ending. An answer that was
|
|
8412
|
-
|
|
8612
|
+
_already_ bad still spends its retries — the stop applies only when the
|
|
8413
8613
|
middleware is the cause.
|
|
8414
8614
|
|
|
8415
8615
|
### `.outputFallback()` says which door reaches its tiers
|
|
@@ -8496,8 +8696,8 @@ flowchartAsTool({
|
|
|
8496
8696
|
name: 'weather_advice',
|
|
8497
8697
|
description: 'Decide whether to bike tomorrow.',
|
|
8498
8698
|
flowchart: adviceChart,
|
|
8499
|
-
keepRecord: true,
|
|
8500
|
-
keepRecordLimit: 20,
|
|
8699
|
+
keepRecord: true, // ← off by default
|
|
8700
|
+
keepRecordLimit: 20, // ← bounded LRU window (this is the default)
|
|
8501
8701
|
redact: { keys: ['apiKey'] },
|
|
8502
8702
|
});
|
|
8503
8703
|
```
|
|
@@ -8588,8 +8788,8 @@ artifacts by hand: `innerRunStore`, `innerRunsOf`, `mergeInnerRuns`,
|
|
|
8588
8788
|
### The demo
|
|
8589
8789
|
|
|
8590
8790
|
`examples/features/50-through-the-tool-boundary.ts` — a weather-advice agent
|
|
8591
|
-
whose ONE tool is a 4-stage footprintjs chart. Turn 1:
|
|
8592
|
-
in Chicago tomorrow?"
|
|
8791
|
+
whose ONE tool is a 4-stage footprintjs chart. Turn 1: _"Should I bike to work
|
|
8792
|
+
in Chicago tomorrow?"_ Turn 2: _"Why did you say it'll rain?"_ — answered
|
|
8593
8793
|
through visible `find_in_trace` → `inspect_tool_call` → `inspect_tool_run`
|
|
8594
8794
|
calls that cite the inner stage (`validate-forecast#1`), the exact field
|
|
8595
8795
|
(`rainChancePct = 82`) and the rule that consumed it. The chart's own stage
|
|
@@ -8610,8 +8810,8 @@ evidence existed, and the model had no way to reach it.
|
|
|
8610
8810
|
### `find_in_trace(query)` — free text in, step ids out
|
|
8611
8811
|
|
|
8612
8812
|
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 —
|
|
8614
|
-
order 7712 was out of warranty?"
|
|
8813
|
+
variable. But a follow-up question arrives in the user's words — _"why did you say
|
|
8814
|
+
order 7712 was out of warranty?"_ — and the model's only options were to guess a
|
|
8615
8815
|
state key or read the whole narrative.
|
|
8616
8816
|
|
|
8617
8817
|
This searches stage names and descriptions, state keys, every committed value and
|
|
@@ -8748,7 +8948,7 @@ await callTraceTool(tools, 'find_in_trace', { query: 'order 7712' });
|
|
|
8748
8948
|
|
|
8749
8949
|
Pure: no engine, no agent, no I/O. Exported from `agentfootprint/observe` (and
|
|
8750
8950
|
`agentfootprint/debug`). Honest about the two things a serialized run cannot carry
|
|
8751
|
-
back — `controlDeps` is a lookup
|
|
8951
|
+
back — `controlDeps` is a lookup _function_ and does not serialize (slices say
|
|
8752
8952
|
`⚠ control edges unavailable`, the marker that already existed), and the narrative
|
|
8753
8953
|
survives only if a narrative recorder was attached, since `recordRun` deliberately
|
|
8754
8954
|
attaches none. Two teaching refusals name `recordRun` as the producer: a bundle with
|
|
@@ -8801,11 +9001,11 @@ cursor half and left the rule half standing, and the rule half is the bug: an en
|
|
|
8801
9001
|
routed to `T`, `S`'s rule still matched and `S` and `T` were both active. Measured on
|
|
8802
9002
|
a two-skill support graph:
|
|
8803
9003
|
|
|
8804
|
-
| iteration
|
|
8805
|
-
|
|
8806
|
-
| 1
|
|
9004
|
+
| iteration | active | tool menu | skill bodies |
|
|
9005
|
+
| --------------- | ------------------ | -------------------------------------------- | --------------- |
|
|
9006
|
+
| 1 | `triage` | `read_skill`, `lookup_order` | triage |
|
|
8807
9007
|
| 2 — the handoff | `triage`, `refund` | `read_skill`, `lookup_order`, `issue_refund` | triage + refund |
|
|
8808
|
-
| 3
|
|
9008
|
+
| 3 | `triage`, `refund` | same | triage + refund |
|
|
8809
9009
|
|
|
8810
9010
|
Note iteration 3. This was never a one-iteration blip: with the cursor parked on
|
|
8811
9011
|
`refund`, `triage`'s rule kept matching, so it came back and stayed. The overlap was
|
|
@@ -8813,8 +9013,8 @@ the steady state.
|
|
|
8813
9013
|
|
|
8814
9014
|
**A conditional entry is now active exactly while the cursor is on it** — the same
|
|
8815
9015
|
compiled expression a route target and an exclusive entry already used. One law for a
|
|
8816
|
-
flat graph:
|
|
8817
|
-
unconditional
|
|
9016
|
+
flat graph: _a skill is active iff the cursor is on it, or it declared itself
|
|
9017
|
+
unconditional._ `when` chooses where a turn STARTS.
|
|
8818
9018
|
|
|
8819
9019
|
This finishes 8.3.0 rather than reverting it. Both failures 8.3.0 named — a declared
|
|
8820
9020
|
step INTO an entry skill, and a `read_skill` pick onto one — are carried by the cursor
|
|
@@ -8852,7 +9052,7 @@ superseded pick.
|
|
|
8852
9052
|
|
|
8853
9053
|
`multi-entry-fanout` fired whenever a graph declared two or more entries, including
|
|
8854
9054
|
when every one of them carried a `when` — a deterministic rule-router, which is a
|
|
8855
|
-
taught shape. Worse, the advice it gave was
|
|
9055
|
+
taught shape. Worse, the advice it gave was _"give the extras a `when`"_, to entries
|
|
8856
9056
|
that already had one. It computed which entries were unconditional and then used that
|
|
8857
9057
|
only to soften the middle of the sentence.
|
|
8858
9058
|
|
|
@@ -8871,7 +9071,7 @@ why the check over-fired. Rewritten.
|
|
|
8871
9071
|
A suppression the run cannot name is a silent drop. When a conditional entry's rule
|
|
8872
9072
|
matched and the cursor law kept it off the wire, the entry's id is now reported on the
|
|
8873
9073
|
per-iteration evaluation event, beside the `cursorMove` that says where the graph went
|
|
8874
|
-
instead. Together they answer
|
|
9074
|
+
instead. Together they answer _"why isn't my entry loading?"_ without anyone
|
|
8875
9075
|
re-running a predicate to guess.
|
|
8876
9076
|
|
|
8877
9077
|
Omitted when nothing was suppressed, and for every non-skill-graph run — so an
|
|
@@ -8917,8 +9117,8 @@ build, and one changes a string on the wire. All eight are below.
|
|
|
8917
9117
|
correct branch:
|
|
8918
9118
|
|
|
8919
9119
|
- **same provider family** — it billed your MAIN model for every fold. The
|
|
8920
|
-
refusal three lines above it in the same file promised
|
|
8921
|
-
quietly bill your main model for compaction"
|
|
9120
|
+
refusal three lines above it in the same file promised _"the library will not
|
|
9121
|
+
quietly bill your main model for compaction"_, and then did.
|
|
8922
9122
|
- **different provider** — it sent your agent's model id to a vendor that has
|
|
8923
9123
|
never heard of it, so the fold died mid-run, on a paid run, in a file whose
|
|
8924
9124
|
own header promises "everything fails at `.build()`, never mid-run".
|
|
@@ -8946,7 +9146,7 @@ Readings are now stamped with the iteration whose call produced them and expire
|
|
|
8946
9146
|
one boundary later. An expired reading is `undefined`, which every strategy
|
|
8947
9147
|
already treats as "do not act" — so a window strategy **stands down** instead of
|
|
8948
9148
|
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
|
|
9149
|
+
going quiet. "Counted, never guessed" has to mean counted _recently_.
|
|
8950
9150
|
|
|
8951
9151
|
An agent whose provider reports usage reliably is unaffected.
|
|
8952
9152
|
|
|
@@ -8991,10 +9191,10 @@ comparison of two string lengths will not.
|
|
|
8991
9191
|
Two emitters share this event name, this `slot: 'messages'` value, and — until
|
|
8992
9192
|
now — one indistinguishable payload:
|
|
8993
9193
|
|
|
8994
|
-
| emitter
|
|
8995
|
-
|
|
8996
|
-
| the three context slots (`contextBudget`, **on by default**) | `String.length`
|
|
8997
|
-
| a window strategy (`.window()` / `.compaction()`)
|
|
9194
|
+
| emitter | counts | `unit` |
|
|
9195
|
+
| ------------------------------------------------------------ | ------------------------------ | ---------- |
|
|
9196
|
+
| the three context slots (`contextBudget`, **on by default**) | `String.length` | `'chars'` |
|
|
9197
|
+
| a window strategy (`.window()` / `.compaction()`) | provider-reported input tokens | `'tokens'` |
|
|
8998
9198
|
|
|
8999
9199
|
So one subscriber routinely received both, and `cap 200, projected 258` could
|
|
9000
9200
|
mean 258 characters or 258 tokens — a roughly 4× difference in the same field,
|
|
@@ -9018,8 +9218,8 @@ oversight — please do not "fix" them.
|
|
|
9018
9218
|
### `costBudget` can now stop the run
|
|
9019
9219
|
|
|
9020
9220
|
`costBudget` was warn-only, while `commentaryTemplates.ts` narrated
|
|
9021
|
-
|
|
9022
|
-
claimed the agent
|
|
9221
|
+
_"{{appName}} hit a cost limit and stopped."_ and `docs/monitor/deployment.mdx`
|
|
9222
|
+
claimed the agent _"halts when the per-run USD budget is hit"_. It did neither.
|
|
9023
9223
|
`docs/monitor/observability.mdx`, on the same site, correctly said the library
|
|
9024
9224
|
never auto-aborts.
|
|
9025
9225
|
|
|
@@ -9128,7 +9328,7 @@ version would make an older deployment refuse a session it can serve.
|
|
|
9128
9328
|
**Governance never silently drops — and never silently invents.** Eight ways a
|
|
9129
9329
|
rule you configured could decide nothing, and you could only find out by reading
|
|
9130
9330
|
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
|
|
9331
|
+
fix; one was a rule that ran everywhere except the one path where a _person_ had
|
|
9132
9332
|
just typed the value.
|
|
9133
9333
|
|
|
9134
9334
|
Two of these change what a run DOES. Both are called out below.
|
|
@@ -9200,7 +9400,7 @@ It now raises `DecisionRequiredError` (`code: 'ERR_DECISION_REQUIRED'`) at the
|
|
|
9200
9400
|
API boundary. **Nothing executes and the checkpoint is unchanged**, so the same
|
|
9201
9401
|
one can be answered properly and resumed again. The error names the gate
|
|
9202
9402
|
(`gate: 'checkIn' | 'ask'`), the tool, the middleware that asked, and `received`
|
|
9203
|
-
— the
|
|
9403
|
+
— the _shape_ that arrived, never its contents, because a resume payload is
|
|
9204
9404
|
caller data and an error message ends up in logs.
|
|
9205
9405
|
|
|
9206
9406
|
Discriminated by the **pause**, never by the input, via the new
|
|
@@ -9282,9 +9482,9 @@ never stops your strategy. It now also carries two methods:
|
|
|
9282
9482
|
```ts
|
|
9283
9483
|
const telemetry = agent.enable.observability({ strategy: cloudwatch });
|
|
9284
9484
|
|
|
9285
|
-
await telemetry.flush();
|
|
9286
|
-
telemetry();
|
|
9287
|
-
telemetry.stop();
|
|
9485
|
+
await telemetry.flush(); // drain: driver queue first, then the buffer
|
|
9486
|
+
telemetry(); // detach (unchanged)
|
|
9487
|
+
telemetry.stop(); // release — timers, clients, buffers
|
|
9288
9488
|
```
|
|
9289
9489
|
|
|
9290
9490
|
`flush()` enforces the ORDER, which is the part no consumer could write from
|
|
@@ -9407,7 +9607,7 @@ of trying again. A drain that cannot finish must return, never retry forever.
|
|
|
9407
9607
|
|
|
9408
9608
|
With `enable.observability({ detach })`, each export is scheduled onto a
|
|
9409
9609
|
footprintjs detach driver. Scheduling happened inside a promise continuation,
|
|
9410
|
-
so the detach handle reached footprintjs's registry a microtask
|
|
9610
|
+
so the detach handle reached footprintjs's registry a microtask _after_ the
|
|
9411
9611
|
event was dispatched. `flushAllDetached()` drains until that registry is empty
|
|
9412
9612
|
— and it was still empty when it looked.
|
|
9413
9613
|
|
|
@@ -9430,7 +9630,7 @@ the package either way.
|
|
|
9430
9630
|
reach today (the declared type is still `Unsubscribe`); it exists so the
|
|
9431
9631
|
no-subscription case is not the one path that breaks when that type widens.
|
|
9432
9632
|
- Removed a dead loop condition in the CloudWatch drain (`lastFlushPromise !==
|
|
9433
|
-
|
|
9633
|
+
Promise.resolve()` compares against a freshly minted promise and is always
|
|
9434
9634
|
true).
|
|
9435
9635
|
|
|
9436
9636
|
## [8.11.0] - 2026-08-07
|
|
@@ -9467,7 +9667,7 @@ delivery — just an empty log group.
|
|
|
9467
9667
|
The docstring for `logStreamName` had been promising `"Created on first put if
|
|
9468
9668
|
it doesn't exist"` since the adapter shipped. It was never true. Worse, the
|
|
9469
9669
|
convention the docs themselves recommended — `` `${HOSTNAME}/${Date.now()}` ``
|
|
9470
|
-
— produces a name that
|
|
9670
|
+
— produces a name that _cannot_ pre-exist, so following the documentation
|
|
9471
9671
|
guaranteed the bug on every deploy. The only configuration that worked was the
|
|
9472
9672
|
undocumented one.
|
|
9473
9673
|
|
|
@@ -9483,7 +9683,7 @@ instead of implying otherwise.
|
|
|
9483
9683
|
|
|
9484
9684
|
The missing stream was one delivery failure. It turned out **every** delivery
|
|
9485
9685
|
failure was silent: an IAM denial, a throttle, a rejected batch. Each adapter
|
|
9486
|
-
installed its console fallback lazily
|
|
9686
|
+
installed its console fallback lazily _inside_ its own `_onError` method — so
|
|
9487
9687
|
the delivery path, which read the hook rather than calling the method, found
|
|
9488
9688
|
`undefined` and dropped the error on the floor. `cloudwatch`, `xray` and `otel`
|
|
9489
9689
|
all had it.
|
|
@@ -9510,7 +9710,7 @@ fail. It is replaced by six that assert unconditionally.
|
|
|
9510
9710
|
|
|
9511
9711
|
### A knob the warning told you to turn, that did not exist
|
|
9512
9712
|
|
|
9513
|
-
An over-budget context slot warned:
|
|
9713
|
+
An over-budget context slot warned: _"Raise `budgetCap` on the slot config."_
|
|
9514
9714
|
`budgetCap` was reachable from no public door. `buildMessagesSlot()` was called
|
|
9515
9715
|
with no arguments at all four of its call sites, so its 10000-character cap was
|
|
9516
9716
|
unreachable by construction. A warning you cannot act on is worse than no
|
|
@@ -9530,15 +9730,15 @@ budget is a signal, not a limiter. `LLMCallOptions` takes the same option
|
|
|
9530
9730
|
|
|
9531
9731
|
A 429 is a **pre-execution rejection**: the rate limiter refused the request at
|
|
9532
9732
|
the edge and the server never ran the tool, so a retry cannot double-execute
|
|
9533
|
-
anything. That is exactly what is
|
|
9733
|
+
anything. That is exactly what is _not_ true of a 500 or a timeout, where the
|
|
9534
9734
|
call may have half-run and a retry could charge a card twice.
|
|
9535
9735
|
|
|
9536
9736
|
That asymmetry is the entire license for this feature, so the policy is 429 and
|
|
9537
9737
|
nothing else — pinned by a property test that walks twelve other statuses and a
|
|
9538
9738
|
thrown transport error and asserts a single attempt for each. Managed gateways
|
|
9539
9739
|
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
|
|
9541
|
-
broken"
|
|
9740
|
+
condition reached the model as a thrown tool error it reads as _"this tool is
|
|
9741
|
+
broken"_, whereupon it apologises, picks another tool, or invents an answer.
|
|
9542
9742
|
|
|
9543
9743
|
It lives at the `fetch` seam because that is the only place `Retry-After` still
|
|
9544
9744
|
exists — the MCP SDK reads the response, throws `StreamableHTTPError(status,
|
|
@@ -9565,18 +9765,22 @@ Per-attempt visibility is the `onRetry` callback — the contract `withRetry` an
|
|
|
9565
9765
|
are consumer-called, the docs now say so, and a batching exporter loses its
|
|
9566
9766
|
final batch and leaks its timer if you skip them:
|
|
9567
9767
|
```ts
|
|
9568
|
-
process.on('SIGTERM', async () => {
|
|
9768
|
+
process.on('SIGTERM', async () => {
|
|
9769
|
+
await telemetry.flush();
|
|
9770
|
+
telemetry.stop();
|
|
9771
|
+
stop();
|
|
9772
|
+
});
|
|
9569
9773
|
```
|
|
9570
9774
|
Wiring them into the framework lifecycle would change `run()` timing and
|
|
9571
9775
|
misbehave for a strategy shared across two `enable` calls, so it is a design
|
|
9572
9776
|
question on the ledger rather than a silent default.
|
|
9573
9777
|
- **A Skill's tools are visible from iteration 1.** `DefineSkillOptions.tools`
|
|
9574
|
-
said they were
|
|
9778
|
+
said they were _"added to the tools slot once activated"_. They are added to
|
|
9575
9779
|
the registry at build time; activation adds the Skill's **body**, not its
|
|
9576
9780
|
tools. Gating is opt-in via `autoActivate: 'currentSkill'` (which
|
|
9577
9781
|
`skillGraph().tree()` sets for you on every leaf) — and the docs said
|
|
9578
9782
|
otherwise in twelve places, including a `process_refund` example claiming a
|
|
9579
|
-
tool was
|
|
9783
|
+
tool was _"locked away"_. That example now sets `autoActivate` and the prose
|
|
9580
9784
|
no longer implies a security boundary the default does not provide.
|
|
9581
9785
|
- **`autoActivate` stopped calling itself a forward-compat marker** awaiting
|
|
9582
9786
|
"v2.5 runtime wiring" — that wiring shipped in 2.5.0, six majors ago.
|
|
@@ -9586,7 +9790,7 @@ Per-attempt visibility is the `onRetry` callback — the contract `withRetry` an
|
|
|
9586
9790
|
anything, and a lower tier is not a safer one: `'minimal'` still ships
|
|
9587
9791
|
`agent.turn_start` (`userPrompt`), `agent.turn_end` (`finalContent`) and
|
|
9588
9792
|
`agent.iteration_end` (the whole conversation `history[]`) — measured, it
|
|
9589
|
-
carries user content in a
|
|
9793
|
+
carries user content in a _higher_ share of its events than `'standard'` does.
|
|
9590
9794
|
The docstring says this plainly now, points at `auditExport()` (bounded by
|
|
9591
9795
|
default) and `otelObservability()` (omits `userPrompt`), and warns that
|
|
9592
9796
|
`redactContent` does **not** apply to this channel — it operates on the
|
|
@@ -9680,12 +9884,12 @@ exists.
|
|
|
9680
9884
|
`textLoader`, `markdownLoader` and `htmlLoader` need no dependency. `pdfLoader`
|
|
9681
9885
|
needs one, and it was picked by measuring rather than by reputation:
|
|
9682
9886
|
|
|
9683
|
-
| package
|
|
9684
|
-
|
|
9685
|
-
| **`unpdf`**
|
|
9686
|
-
| `pdf-parse@2`
|
|
9687
|
-
| `pdf-parse@1`
|
|
9688
|
-
| `pdfjs-dist@6` | 62 MB
|
|
9887
|
+
| package | installed | packages | verdict |
|
|
9888
|
+
| -------------- | ---------- | -------- | ------------------------------------------------- |
|
|
9889
|
+
| **`unpdf`** | **2.5 MB** | **1** | chosen — zero transitive deps, per-page text |
|
|
9890
|
+
| `pdf-parse@2` | 86 MB | 3 | a native binary (`@napi-rs/canvas`), to read text |
|
|
9891
|
+
| `pdf-parse@1` | 34 MB | 4 | unmaintained since 2018 |
|
|
9892
|
+
| `pdfjs-dist@6` | 62 MB | 2 | 25× the size for the same engine |
|
|
9689
9893
|
|
|
9690
9894
|
It is an optional peer, lazily loaded, refusing with an install line when a PDF
|
|
9691
9895
|
is actually met. Per-page text is why a PDF citation can name a page you can
|
|
@@ -9789,7 +9993,6 @@ the page its 150-character run-up borrowed from.
|
|
|
9789
9993
|
- **`unpdf`** as a new optional peer. Only `pdfLoader` touches it, only when a
|
|
9790
9994
|
PDF is actually read.
|
|
9791
9995
|
|
|
9792
|
-
|
|
9793
9996
|
## [8.9.0] - 2026-08-06
|
|
9794
9997
|
|
|
9795
9998
|
**The durable index.** 8.8.0 made retrieval tell the truth about what it read.
|
|
@@ -9815,8 +10018,8 @@ await indexDocuments(store, embedder, docs, { embedderId: embedder.id });
|
|
|
9815
10018
|
Embedding cost is not one number, and the split is the whole argument for a
|
|
9816
10019
|
file. **Index time** embeds the corpus: once, scaling with how much you store.
|
|
9817
10020
|
**Query time** embeds the user's question: per retrieval, scaling with traffic.
|
|
9818
|
-
A 10,000-chunk corpus is 10,000 embeddings
|
|
9819
|
-
thereafter — with a `Map` it is 10,000 embeddings
|
|
10021
|
+
A 10,000-chunk corpus is 10,000 embeddings _once_ and one per question
|
|
10022
|
+
thereafter — with a `Map` it is 10,000 embeddings _per restart_.
|
|
9820
10023
|
|
|
9821
10024
|
`agentfootprint.embedding.generated` has carried an `inputKind: 'document' |
|
|
9822
10025
|
'query'` field since 2.x and nothing ever emitted it, so any dashboard built
|
|
@@ -9839,13 +10042,13 @@ top-K or it does not answer.
|
|
|
9839
10042
|
|
|
9840
10043
|
Measured against this implementation on Node 22.16, Apple silicon:
|
|
9841
10044
|
|
|
9842
|
-
| corpus
|
|
9843
|
-
|
|
9844
|
-
| 10,000 × 384-d
|
|
9845
|
-
| 50,000 × 384-d
|
|
9846
|
-
| 100,000 × 384-d | 65 ms | 154 MB
|
|
9847
|
-
| 10,000 × 1536-d | 16 ms | 61 MB
|
|
9848
|
-
| 50,000 × 1536-d | 89 ms | 307 MB
|
|
10045
|
+
| corpus | query | resident matrix | file | first search (hydration) |
|
|
10046
|
+
| --------------- | ----- | --------------- | ------ | ------------------------ |
|
|
10047
|
+
| 10,000 × 384-d | 6 ms | 15 MB | 21 MB | 45 ms |
|
|
10048
|
+
| 50,000 × 384-d | 31 ms | 77 MB | 105 MB | 251 ms |
|
|
10049
|
+
| 100,000 × 384-d | 65 ms | 154 MB | 211 MB | 939 ms |
|
|
10050
|
+
| 10,000 × 1536-d | 16 ms | 61 MB | 83 MB | 122 ms |
|
|
10051
|
+
| 50,000 × 1536-d | 89 ms | 307 MB | 413 MB | **5.7 s** |
|
|
9849
10052
|
|
|
9850
10053
|
**The documented ceiling is 50,000 chunks** — under 100 ms per query at every
|
|
9851
10054
|
embedder this library ships, under ~300 MB resident. It degrades linearly to
|
|
@@ -9928,13 +10131,12 @@ safe to answer with "no matches".**
|
|
|
9928
10131
|
- The BLOB on disk keeps the **original** vector, so `get`/`list` round-trip
|
|
9929
10132
|
exactly what was written; normalisation happens once, into the resident
|
|
9930
10133
|
matrix, so search is a dot product without changing what is stored.
|
|
9931
|
-
- Found while writing the schema-identity check: it originally ran
|
|
10134
|
+
- Found while writing the schema-identity check: it originally ran _after_ the
|
|
9932
10135
|
indexes were created, so a foreign `af_vectors` table failed on a missing
|
|
9933
10136
|
column and was reported as `'cannot-open'` — the right refusal for the wrong
|
|
9934
10137
|
reason, telling the reader to check file permissions when the real problem was
|
|
9935
10138
|
that the file belonged to something else. It runs before them now.
|
|
9936
10139
|
|
|
9937
|
-
|
|
9938
10140
|
## [8.8.0] - 2026-08-06
|
|
9939
10141
|
|
|
9940
10142
|
**Retrieval tells the truth.** A retrieval computed a cosine score for every candidate
|
|
@@ -9969,12 +10171,14 @@ the two are registered separately, each with its own store:
|
|
|
9969
10171
|
```ts
|
|
9970
10172
|
const agent = Agent.create({ provider })
|
|
9971
10173
|
.rag(defineRAG({ id: 'product-docs', store: corpusStore, embedder }))
|
|
9972
|
-
.memory(
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
10174
|
+
.memory(
|
|
10175
|
+
defineMemory({
|
|
10176
|
+
id: 'chat',
|
|
10177
|
+
type: MEMORY_TYPES.EPISODIC,
|
|
10178
|
+
strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
|
|
10179
|
+
store: conversationStore,
|
|
10180
|
+
}),
|
|
10181
|
+
)
|
|
9978
10182
|
.build();
|
|
9979
10183
|
```
|
|
9980
10184
|
|
|
@@ -9996,7 +10200,7 @@ namespace it reads from — defaulting to the same `'_global'` the indexer write
|
|
|
9996
10200
|
index with no options and retrieve with no options and the documents are found. Pass it
|
|
9997
10201
|
explicitly for a per-tenant corpus, on both sides.
|
|
9998
10202
|
|
|
9999
|
-
And a namespace that holds nothing is now
|
|
10203
|
+
And a namespace that holds nothing is now _reported_ rather than answered around:
|
|
10000
10204
|
`corpusEmpty: true` on the retrieval event, plus a once-per-process warning naming the
|
|
10001
10205
|
namespace it searched and the usual cause.
|
|
10002
10206
|
|
|
@@ -10015,7 +10219,7 @@ namespace it searched and the usual cause.
|
|
|
10015
10219
|
are admitted**: `search` returns score-descending, so either the whole pool clears the
|
|
10016
10220
|
floor (admitted = first `k`, as before) or some entry fails it (every later entry fails
|
|
10017
10221
|
too, so the pool already holds every entry that clears it). `rejectWindow` only controls
|
|
10018
|
-
how many near-misses can be
|
|
10222
|
+
how many near-misses can be _shown_.
|
|
10019
10223
|
- **`agentfootprint.memory.retrieved`** (new, 72 typed events) — one per retrieval,
|
|
10020
10224
|
carrying every candidate. `candidates: undefined` means the store ranked server-side
|
|
10021
10225
|
and returned nothing comparable; it never means there were none.
|
|
@@ -10056,7 +10260,7 @@ recency ordering the best-scoring chunk can land last.
|
|
|
10056
10260
|
### Chunks the model can cite
|
|
10057
10261
|
|
|
10058
10262
|
A retrieved page of a PDF rendered as `<memory role="unknown" turn="0">` under the header
|
|
10059
|
-
|
|
10263
|
+
_"Relevant context from prior conversations"_ — three claims that were not true of a
|
|
10060
10264
|
document, and no way to cite it. `defineRAG` renders a corpus as what it is:
|
|
10061
10265
|
|
|
10062
10266
|
```text
|
|
@@ -10108,13 +10312,12 @@ nothing to configure.
|
|
|
10108
10312
|
`core/`; a second implementation is how two recordings of the same bytes end up
|
|
10109
10313
|
disagreeing about their id.
|
|
10110
10314
|
|
|
10111
|
-
|
|
10112
10315
|
## [8.7.0] - 2026-08-06
|
|
10113
10316
|
|
|
10114
10317
|
**The check-up stops being quiet, and a dead option stops pretending.** 8.4.0 stopped a
|
|
10115
10318
|
skill graph from throwing away what the author declared; 8.5.0 stopped it telling the
|
|
10116
10319
|
model things that were not so. This one is about the configurations the library
|
|
10117
|
-
|
|
10320
|
+
_watched you build and said nothing about_ — an entry menu with no way to choose from
|
|
10118
10321
|
it, a transition the cursor can never take, a tool name two sources claim, a scoped
|
|
10119
10322
|
tool provider that returns nothing forever. Nine findings, one shape: the library knew,
|
|
10120
10323
|
and did not say.
|
|
@@ -10122,7 +10325,7 @@ and did not say.
|
|
|
10122
10325
|
### An entry menu with no way to choose from it
|
|
10123
10326
|
|
|
10124
10327
|
Declare two entries and no `.entryBy()` / `.entryByRead()`, and both of them load on
|
|
10125
|
-
every call. An entry's compiled trigger is cursor
|
|
10328
|
+
every call. An entry's compiled trigger is cursor-_independent_ — no `when` compiles to
|
|
10126
10329
|
`{ kind: 'always' }` — while exactly ONE of them can be the cursor: the first whose
|
|
10127
10330
|
`when` passes. So the extras pay for their body and their tools on every iteration and
|
|
10128
10331
|
route nothing, which is the opposite of what a skill graph is for.
|
|
@@ -10168,7 +10371,7 @@ all versus only bare edges incoming.
|
|
|
10168
10371
|
|
|
10169
10372
|
### `unreachable-skill` is told per trigger kind
|
|
10170
10373
|
|
|
10171
|
-
The sentence
|
|
10374
|
+
The sentence _"it can only be reached by the model via read_skill"_ is true for an
|
|
10172
10375
|
`llm-activated` trigger and for no other kind — `Agent.openSkillIds()` admits an open
|
|
10173
10376
|
pick only for that one. But `deriveTrigger` returns null for an unwired skill, so a
|
|
10174
10377
|
skill that arrived carrying a hand-authored `rule` trigger **kept it**, and the warning
|
|
@@ -10238,7 +10441,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
|
|
|
10238
10441
|
- **`ToolDispatchContext.activeSkillIds`** — the real active set for this iteration.
|
|
10239
10442
|
Optional, so a provider written before 8.7.0 sees `undefined` and behaves as it did.
|
|
10240
10443
|
- **`agentfootprint.tools.shadowed`** (71 typed events now) — `{ toolName, iteration,
|
|
10241
|
-
|
|
10444
|
+
schemaFrom, schemaFromId?, dispatchTo, dispatchToId? }`. Names only: never args, never
|
|
10242
10445
|
results, never a description body.
|
|
10243
10446
|
- **`skillScopedToolsTarget` / `SKILL_SCOPED_TOOLS_ID_PREFIX`** — the provider-id
|
|
10244
10447
|
convention, readable by anyone composing providers.
|
|
@@ -10256,7 +10459,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
|
|
|
10256
10459
|
object-literal form since 8.4.0. **Behavior change.** A fluent graph with an
|
|
10257
10460
|
error-level problem — `no-entry` or `unknown-skill`, i.e. a graph that cannot start a
|
|
10258
10461
|
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
|
|
10462
|
+
skill. What still builds: every graph whose check-up has no _error_ (warnings never
|
|
10260
10463
|
throw, however many); every call passing `check: 'warn'` explicitly, which still never
|
|
10261
10464
|
throws, so the mode keeps its name and its meaning; `check: 'off'` skips entirely.
|
|
10262
10465
|
Only code that was already shipping a graph the library could not start is affected.
|
|
@@ -10265,7 +10468,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
|
|
|
10265
10468
|
activates the skill — and no such tool has ever been built. The evaluator activates an
|
|
10266
10469
|
`llm-activated` skill by matching `ctx.activatedInjectionIds`, which only `read_skill`
|
|
10267
10470
|
writes, and it has never read the field. A skill declaring `viaToolName:
|
|
10268
|
-
|
|
10471
|
+
'open_playbook'` activated through `read_skill` exactly like every other skill, so the
|
|
10269
10472
|
declaration described a door that does not exist. Nothing that worked stops working;
|
|
10270
10473
|
a silent no-op becomes a named one, at `Agent.injection()` — the one funnel `.skill()`,
|
|
10271
10474
|
`.skills()`, `.skillGraph()`, `skillsFromDir()` and a hand-built Injection all pass
|
|
@@ -10338,7 +10541,7 @@ it raises `CredentialConsentRequiredError`.
|
|
|
10338
10541
|
back `authorization-required`.
|
|
10339
10542
|
- **`CredentialConsentRequiredError`** (`ERR_CREDENTIAL_CONSENT_REQUIRED`, from
|
|
10340
10543
|
`agentfootprint/identity`) — carries `service`, `sessionId`, `authorizationUrl`,
|
|
10341
|
-
`tool` and `iteration`. The error
|
|
10544
|
+
`tool` and `iteration`. The error _message_ deliberately omits the URL, because
|
|
10342
10545
|
a message is the one string that reliably reaches a log line.
|
|
10343
10546
|
- **`pauseData.authorization`** — `{ service, authorizationUrl, sessionId }` on a
|
|
10344
10547
|
consent pause, surfaced by `standingAgent` as `PendingAsk.pauseData`. The
|
|
@@ -10366,7 +10569,7 @@ it raises `CredentialConsentRequiredError`.
|
|
|
10366
10569
|
This is the mirror image of a guarantee the library kept carefully everywhere
|
|
10367
10570
|
else. `agentfootprint.credential.authorization_required` was designed to carry
|
|
10368
10571
|
`{ service, sessionId }` and never the URL; OTel and X-Ray record the tool
|
|
10369
|
-
result's
|
|
10572
|
+
result's _type_ and never its value; the audit bundle's default `bounded` mode
|
|
10370
10573
|
maps `tool_end.result` to `[type: string]`. Every observer channel was
|
|
10371
10574
|
disciplined. The one channel nobody thought of as an observer — the
|
|
10372
10575
|
conversation — was not, and it feeds all the others.
|
|
@@ -10506,7 +10709,7 @@ that gave the middleware-ask outcome union no `result` arm.
|
|
|
10506
10709
|
## [8.5.0] - 2026-08-06
|
|
10507
10710
|
|
|
10508
10711
|
**`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
|
|
10712
|
+
away what the author declared. This one stops it from telling the _model_ things
|
|
10510
10713
|
that were not so. Five findings, all of the same shape: the library said a thing had
|
|
10511
10714
|
happened, or offered a thing it would refuse, or recorded a cause that was not the
|
|
10512
10715
|
cause. One is a build-time refusal, one is a gate refusal, three are fixes.
|
|
@@ -10515,8 +10718,8 @@ cause. One is a build-time refusal, one is a gate refusal, three are fixes.
|
|
|
10515
10718
|
|
|
10516
10719
|
A `tree()` routes by predicate on every iteration. It has no cursor, so `read_skill`
|
|
10517
10720
|
has nothing to move. But `graph.reachableSkills()` reported **all the leaves**, so
|
|
10518
|
-
the gate accepted a leaf pick and `read_skill` answered
|
|
10519
|
-
next iteration"
|
|
10721
|
+
the gate accepted a leaf pick and `read_skill` answered _"Skill 'x' activated for the
|
|
10722
|
+
next iteration"_ — and nothing happened. A leaf compiles to a `rule` trigger; a
|
|
10520
10723
|
`read_skill` call writes only `activatedInjectionIds`; no `rule` trigger reads that.
|
|
10521
10724
|
The leaf never activated, the tree re-decided by predicate, and the run then emitted
|
|
10522
10725
|
`agentfootprint.skill.reroute_superseded` naming a winner that **did not exist** —
|
|
@@ -10542,7 +10745,7 @@ though the tool accepted the name. Answer with the skill the tree routed to, or
|
|
|
10542
10745
|
**Behavior change:** `graph.reachableSkills()` now returns `[]` for a decision
|
|
10543
10746
|
`tree()`, from every cursor. Its contract is "what `read_skill` may jump to", and
|
|
10544
10747
|
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
|
|
10748
|
+
always for. `read_skill` is not dead under a tree — anything registered _beside_ the
|
|
10546
10749
|
graph (`.skill(x)`, `.skills(reg)`, `.selfExplain()`) is **open** and still admitted
|
|
10547
10750
|
from anywhere, because those really do activate by `read_skill`. Two docstrings that
|
|
10548
10751
|
promised "read_skill stays a full escape hatch there" are corrected.
|
|
@@ -10568,7 +10771,7 @@ Not reachable from here (read_skill for these will be refused):
|
|
|
10568
10771
|
```
|
|
10569
10772
|
|
|
10570
10773
|
**The enum stays the full catalog, deliberately.** `toolArgValidation` defaults to
|
|
10571
|
-
`'enforce'` and runs
|
|
10774
|
+
`'enforce'` and runs _before_ the gate; an off-enum id is rejected with a generic
|
|
10572
10775
|
schema error and never reaches it. Narrowing the enum would therefore have retired
|
|
10573
10776
|
the gate's teaching refusal, the `agentfootprint.skill.rejected` event,
|
|
10574
10777
|
`routeRecorder`'s rejection hops and the rejected-cap governor's only input — four
|
|
@@ -10602,7 +10805,7 @@ Use 'both' (system prompt AND tool result) or 'system-prompt'.
|
|
|
10602
10805
|
```
|
|
10603
10806
|
|
|
10604
10807
|
Refusal rather than a quiet fall back to the system slot: the author wrote
|
|
10605
|
-
`'tool-only'` to keep the body
|
|
10808
|
+
`'tool-only'` to keep the body _out_ of the system prompt, and silently putting it
|
|
10606
10809
|
back would honour the activation while breaking the declaration — a different lie,
|
|
10607
10810
|
not a fix. `'both'` already means "deliver it either way".
|
|
10608
10811
|
|
|
@@ -10630,7 +10833,7 @@ drift:
|
|
|
10630
10833
|
- `routeRecorder()` reads it, and a `'model-pick'` hop carries **no** `edgeLabel`.
|
|
10631
10834
|
|
|
10632
10835
|
This settles the one case no observer could reconstruct: an edge and a same-turn pick
|
|
10633
|
-
naming the
|
|
10836
|
+
naming the _same_ skill resolves to `'route'` (`D1 > D2`), and only the resolver
|
|
10634
10837
|
knows. Without `cursorMove` (an older graph, an older recording) the previous
|
|
10635
10838
|
inference still stands.
|
|
10636
10839
|
|
|
@@ -10665,11 +10868,11 @@ refusal on the agent, and the gate one is a fix, not a refusal.
|
|
|
10665
10868
|
cannot move the graph somewhere the graph doesn't go. That set is about the CURSOR,
|
|
10666
10869
|
but it was being used as the whole catalog, so three shapes were dead:
|
|
10667
10870
|
|
|
10668
|
-
| you wrote
|
|
10669
|
-
|
|
10670
|
-
| `.skillGraph(g).selfExplain()`
|
|
10671
|
-
| `.skillGraph(g).skill(x)` / `.skills(reg)`
|
|
10672
|
-
| `skillGraph({ skills: [..., x] })` with `x` wired to nothing | refused — while its check-up warning said
|
|
10871
|
+
| you wrote | before | now |
|
|
10872
|
+
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
10873
|
+
| `.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 |
|
|
10874
|
+
| `.skillGraph(g).skill(x)` / `.skills(reg)` | `x` was listed in `read_skill`'s own menu and refused every time; its body was unreachable | activates |
|
|
10875
|
+
| `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
10876
|
|
|
10674
10877
|
A skill is **open** when its trigger is `llm-activated` (the trigger `read_skill`
|
|
10675
10878
|
actually activates — a rule-gated injection is still refused, because admitting it
|
|
@@ -10794,7 +10997,7 @@ dropped pick is reported rather than swallowed — see the new event below.
|
|
|
10794
10997
|
### `agentfootprint.skill.reroute_superseded` (new typed event — 70 total)
|
|
10795
10998
|
|
|
10796
10999
|
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
|
|
11000
|
+
because a declared edge won the same turn (the model emitted a domain tool _and_
|
|
10798
11001
|
`read_skill` in one message). Payload: `{ volunteeredId, wonId, fromSkillId,
|
|
10799
11002
|
iteration }`. It is derived from the real active set, not from which clause won,
|
|
10800
11003
|
so it cannot fire for a pick that did take effect.
|
|
@@ -10838,7 +11041,7 @@ never engages any of this.
|
|
|
10838
11041
|
|
|
10839
11042
|
**Durable compaction.** An agent that has been up for a week folds week one
|
|
10840
11043
|
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,
|
|
11044
|
+
conversation — and now it can still tell you what week one was about, _and_
|
|
10842
11045
|
show you week one, word for word.
|
|
10843
11046
|
|
|
10844
11047
|
The window half of that already worked: a summary is an ordinary message, so
|
|
@@ -10909,14 +11112,14 @@ surprise.
|
|
|
10909
11112
|
|
|
10910
11113
|
### New exports
|
|
10911
11114
|
|
|
10912
|
-
| export
|
|
10913
|
-
|
|
|
10914
|
-
| `foldedSpanFor(conversation, message)`
|
|
10915
|
-
| `foldedMessages(conversation)`
|
|
10916
|
-
| `FoldedSpan` · `CompactionRetention` · `FoldedConversation` | The types.
|
|
11115
|
+
| export | what it is |
|
|
11116
|
+
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11117
|
+
| `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". |
|
|
11118
|
+
| `foldedMessages(conversation)` | Every retained message from every span, oldest fold first. |
|
|
11119
|
+
| `FoldedSpan` · `CompactionRetention` · `FoldedConversation` | The types. |
|
|
10917
11120
|
|
|
10918
11121
|
The fingerprint is also what makes the join **forgery-proof**: `isCompactedSummary`
|
|
10919
|
-
answers "this
|
|
11122
|
+
answers "this _looks_ like a frame", which is all a prefix check can see, and a
|
|
10920
11123
|
model that copies the frame's opening words passes it. `foldedSpanFor` answers
|
|
10921
11124
|
the stronger question — different content, different fingerprint, no match.
|
|
10922
11125
|
|
|
@@ -10977,14 +11180,14 @@ openai({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' });
|
|
|
10977
11180
|
|
|
10978
11181
|
Two things go wrong with a local runtime, and each has a one-command answer. Both
|
|
10979
11182
|
now raise a typed `OllamaUnavailableError` (discriminated by `reason`) whose
|
|
10980
|
-
message
|
|
11183
|
+
message _is_ the instruction — never a raw `ECONNREFUSED`, never a bare `404`,
|
|
10981
11184
|
and never a hang: a deadline bounds the wait for the daemon to answer (not
|
|
10982
11185
|
generation, so a slow model is untouched).
|
|
10983
11186
|
|
|
10984
11187
|
- **Daemon not running** — names the address it tried, `ollama serve`, the
|
|
10985
11188
|
install link, and how to point somewhere else.
|
|
10986
11189
|
- **Model not pulled** — names `ollama pull <model>`, and asks `/api/tags` so it
|
|
10987
|
-
can also list what this machine
|
|
11190
|
+
can also list what this machine _does_ have.
|
|
10988
11191
|
|
|
10989
11192
|
### Also in this release
|
|
10990
11193
|
|
|
@@ -10993,7 +11196,7 @@ generation, so a slow model is untouched).
|
|
|
10993
11196
|
compaction and cost budgets work against a local model.
|
|
10994
11197
|
- **Thinking blocks from local reasoning models.** `ollama('deepseek-r1', { think: true })`
|
|
10995
11198
|
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
|
|
11199
|
+
auto-wires by provider name and normalizes it. When a model was _not_ asked and
|
|
10997
11200
|
writes `<think>…</think>` into the answer instead, the library **recognizes the
|
|
10998
11201
|
shape and surfaces the blocks, but does not edit the answer** — silently
|
|
10999
11202
|
rewriting model output is a change of meaning, and that belongs to the
|
|
@@ -11039,32 +11242,32 @@ The doors are now named for the job:
|
|
|
11039
11242
|
|
|
11040
11243
|
**Every old import path still works, unchanged, for all of 8.x.** They are
|
|
11041
11244
|
marked `@deprecated` so your editor points at the new door; nothing is logged,
|
|
11042
|
-
nothing breaks, and each one re-exports the
|
|
11245
|
+
nothing breaks, and each one re-exports the _same symbols_ the door carries —
|
|
11043
11246
|
not copies. `test/api-conformance/door-aliases.test.ts` drives the TypeScript
|
|
11044
11247
|
checker over the shipped `.d.ts` files to prove it, name by name, so the
|
|
11045
11248
|
aliases cannot drift. They are removed in 9.0.0.
|
|
11046
11249
|
|
|
11047
11250
|
### Migration
|
|
11048
11251
|
|
|
11049
|
-
| you were importing from
|
|
11050
|
-
|
|
11051
|
-
| `agentfootprint/llm-providers`
|
|
11052
|
-
| `agentfootprint/embedders`
|
|
11053
|
-
| `agentfootprint/tool-providers`
|
|
11054
|
-
| `agentfootprint/thinking`
|
|
11055
|
-
| `agentfootprint/memory-providers`
|
|
11056
|
-
| `agentfootprint/observability-providers`
|
|
11057
|
-
| `agentfootprint/strategies`
|
|
11058
|
-
| `agentfootprint/stream`
|
|
11059
|
-
| `agentfootprint/status`
|
|
11060
|
-
| `agentfootprint/locales`
|
|
11061
|
-
| `agentfootprint/debug`
|
|
11062
|
-
| `agentfootprint/debug/finders`
|
|
11063
|
-
| `agentfootprint/observability/contextError/finders` | `agentfootprint/observe`
|
|
11064
|
-
| `agentfootprint/reliability`
|
|
11065
|
-
| `agentfootprint/hosting-providers`
|
|
11066
|
-
| `agentfootprint/injection-engine`
|
|
11067
|
-
| `agentfootprint/identity`
|
|
11252
|
+
| you were importing from | import from |
|
|
11253
|
+
| --------------------------------------------------- | --------------------------- |
|
|
11254
|
+
| `agentfootprint/llm-providers` | `agentfootprint/providers` |
|
|
11255
|
+
| `agentfootprint/embedders` | `agentfootprint/providers` |
|
|
11256
|
+
| `agentfootprint/tool-providers` | `agentfootprint/providers` |
|
|
11257
|
+
| `agentfootprint/thinking` | `agentfootprint/providers` |
|
|
11258
|
+
| `agentfootprint/memory-providers` | `agentfootprint/memory` |
|
|
11259
|
+
| `agentfootprint/observability-providers` | `agentfootprint/observe` |
|
|
11260
|
+
| `agentfootprint/strategies` | `agentfootprint/observe` |
|
|
11261
|
+
| `agentfootprint/stream` | `agentfootprint/observe` |
|
|
11262
|
+
| `agentfootprint/status` | `agentfootprint/observe` |
|
|
11263
|
+
| `agentfootprint/locales` | `agentfootprint/observe` |
|
|
11264
|
+
| `agentfootprint/debug` | `agentfootprint/observe` |
|
|
11265
|
+
| `agentfootprint/debug/finders` | `agentfootprint/observe` |
|
|
11266
|
+
| `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
|
|
11267
|
+
| `agentfootprint/reliability` | `agentfootprint/resilience` |
|
|
11268
|
+
| `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
|
|
11269
|
+
| `agentfootprint/injection-engine` | `agentfootprint/context` |
|
|
11270
|
+
| `agentfootprint/identity` | `agentfootprint/security` |
|
|
11068
11271
|
|
|
11069
11272
|
`agentfootprint`, `agentfootprint/memory`, `agentfootprint/observe`,
|
|
11070
11273
|
`agentfootprint/security`, `agentfootprint/hosting` and
|
|
@@ -11078,7 +11281,7 @@ aliases cannot drift. They are removed in 9.0.0.
|
|
|
11078
11281
|
executing those registrations and carrying them in every bundle. Side-effectful
|
|
11079
11282
|
code stays behind its own plainly-named door.
|
|
11080
11283
|
- **`agentfootprint/events`** is not folded into `/observe`. It is the typed
|
|
11081
|
-
wire vocabulary observers
|
|
11284
|
+
wire vocabulary observers _read_, not a tool for watching — and concretely,
|
|
11082
11285
|
its `ContextSource` (the injection-flavour union: `'rag' | 'skill' | …`) is a
|
|
11083
11286
|
completely different type from the `ContextSource` `/observe` already carries
|
|
11084
11287
|
(the context-bisect record). Two incompatible shapes cannot share a door.
|
|
@@ -11123,7 +11326,7 @@ runtime door, and it still returns the `Unsubscribe` you own.
|
|
|
11123
11326
|
`CombinedRecorder` keeps its export too.
|
|
11124
11327
|
|
|
11125
11328
|
There is deliberately **no** `WATCH_MOMENTS`. `.act()`'s keys are a closed,
|
|
11126
|
-
compiler-pinned list because a rule has to be
|
|
11329
|
+
compiler-pinned list because a rule has to be _told_ where it may speak; an
|
|
11127
11330
|
observer attends the whole stream, and a list we published would be a
|
|
11128
11331
|
vocabulary we then had to keep true against every event ever added.
|
|
11129
11332
|
|
|
@@ -11159,7 +11362,7 @@ vocabulary we then had to keep true against every event ever added.
|
|
|
11159
11362
|
## [7.28.0] - 2026-08-05
|
|
11160
11363
|
|
|
11161
11364
|
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:
|
|
11365
|
+
handed you that promise as JSON and wished you luck: _store it anywhere._
|
|
11163
11366
|
Anywhere was the whole of the offer.
|
|
11164
11367
|
|
|
11165
11368
|
`sqliteSessions({ file })` is the first battery included — the same
|
|
@@ -11182,7 +11385,7 @@ and the next step up was "bring a Redis" — a service to run, secure, back up a
|
|
|
11182
11385
|
pay for, to keep a few kilobytes of chat. Everyone in between wrote the same
|
|
11183
11386
|
little file store themselves and each one re-decided what a half-written file
|
|
11184
11387
|
means. A pause had no home at all: a question outstanding is the one piece of
|
|
11185
|
-
agent state that
|
|
11388
|
+
agent state that _must_ outlive the process, because the answer arrives on human
|
|
11186
11389
|
time — after lunch, after the deploy, tomorrow. Both land in one table here,
|
|
11187
11390
|
because `CheckpointEnvelope` was already a union of the two and a session store
|
|
11188
11391
|
has no business caring which half it is holding.
|
|
@@ -11192,8 +11395,7 @@ process (or a few) on ONE machine, writing ONE file. It survives anything that
|
|
|
11192
11395
|
ends the process and leaves the disk alone. It is **not** a distributed store:
|
|
11193
11396
|
two machines do not share a session by both opening a file over a network
|
|
11194
11397
|
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,
|
|
11398
|
+
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
11399
|
one argument to `standingAgent` changes and nothing above it moves.
|
|
11198
11400
|
|
|
11199
11401
|
**A refusal where a fallback would have been easier.** `node:sqlite` ships with
|
|
@@ -11209,7 +11411,7 @@ user.
|
|
|
11209
11411
|
**"Unreadable is not absent", one level up.** The envelope law already said an
|
|
11210
11412
|
unreadable stored conversation and an absent one are different facts, and only
|
|
11211
11413
|
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
|
|
11414
|
+
higher up — point it at a log file and a careless adapter opens it as an _empty_
|
|
11213
11415
|
store. So the file is checked at construction and refused with
|
|
11214
11416
|
`UnreadableSessionFileError`, whose `problem` field is the fact to branch on:
|
|
11215
11417
|
`'cannot-open'`, `'not-our-schema'` (somebody else's table of that name), or
|
|
@@ -11220,7 +11422,7 @@ session that was never written hydrates as `undefined`.
|
|
|
11220
11422
|
well as fields inside the JSON, so during an incident `sqlite3` answers "which
|
|
11221
11423
|
sessions are waiting on a person, and since when?" with no JSON parser and
|
|
11222
11424
|
without this library. `journalMode` on the returned store reports what the file
|
|
11223
|
-
|
|
11425
|
+
_actually got_ rather than what was asked for — a silent downgrade from WAL on a
|
|
11224
11426
|
network filesystem is the kind of thing only ever discovered under load.
|
|
11225
11427
|
|
|
11226
11428
|
Added, all on the existing `agentfootprint/hosting` door — no new subpath:
|
|
@@ -11252,7 +11454,7 @@ the machine took, and the machine is shared: the suite runs beside a build, a
|
|
|
11252
11454
|
coverage pass, and two other vitest workers. Identical code takes three to five
|
|
11253
11455
|
times longer under that load with nothing about the code having changed. So the
|
|
11254
11456
|
assertion cannot tell "we got slower" from "the box was busy", and it fires
|
|
11255
|
-
|
|
11457
|
+
_exactly_ when CI is busiest. Five of them had already been logged as flakes —
|
|
11256
11458
|
`xray` P6, `withCircuitBreaker` P6, `locales/messages` Block D,
|
|
11257
11459
|
`consumer-domain-events`, `SkillRegistryOptions` — always under concurrent
|
|
11258
11460
|
build load, always passing in isolation. The failure mode of a guard nobody
|
|
@@ -11296,7 +11498,7 @@ still flaked when actually run under the reproduction condition. Three things
|
|
|
11296
11498
|
had to be added, each because the proof run said so:
|
|
11297
11499
|
|
|
11298
11500
|
- **Repeat until the sample is worth timing.** Below a scheduler quantum, one
|
|
11299
|
-
preemption
|
|
11501
|
+
preemption _is_ the measurement: a 0.2ms operation that gets descheduled
|
|
11300
11502
|
reads as a hundred times its real cost, while the 100ms operation beside it
|
|
11301
11503
|
absorbs the same theft as a rounding error — and load stops cancelling. Each
|
|
11302
11504
|
operation is now repeated inside one sample until the sample clears 20ms, and
|
|
@@ -11328,8 +11530,8 @@ load, which is what makes them worth asserting.
|
|
|
11328
11530
|
**No perf claim was deleted. The form changed; the meaning stayed** — and in
|
|
11329
11531
|
four places the meaning got sharper, because writing the claim down properly
|
|
11330
11532
|
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
|
|
11332
|
-
|
|
11533
|
+
ceiling on purpose and say so at the site: they are stated against a _configured
|
|
11534
|
+
delay_ (a mock's own thinking band, a slow branch's own timeout, a strategy's
|
|
11333
11535
|
own per-event block) rather than against a guess about the machine, because
|
|
11334
11536
|
"did not sleep longer than it was told to" has no cheaper form.
|
|
11335
11537
|
|
|
@@ -11485,8 +11687,8 @@ each of those is now the failure of the thing that caused it. A 400, a 500, one
|
|
|
11485
11687
|
refused upgrade, one ended conversation. Never the process.
|
|
11486
11688
|
|
|
11487
11689
|
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
|
|
11489
|
-
|
|
11690
|
+
voided at the call site, so anything that escaped it was an _unhandled
|
|
11691
|
+
rejection_ — which on node's defaults is the same dead container reached by a
|
|
11490
11692
|
different road. It is total by construction now, and says so.
|
|
11491
11693
|
|
|
11492
11694
|
**The audit's other finding is that the conversation door was already safe, and
|
|
@@ -11503,17 +11705,16 @@ serve. It is a great deal of ceremony when all you wanted was a `/debug/trace`
|
|
|
11503
11705
|
beside the agent on the one port the container was given.
|
|
11504
11706
|
|
|
11505
11707
|
```ts
|
|
11506
|
-
nodeHost({ port: 8080, onUnhandled: (req, res) => myRouter(req, res) })
|
|
11708
|
+
nodeHost({ port: 8080, onUnhandled: (req, res) => myRouter(req, res) });
|
|
11507
11709
|
```
|
|
11508
11710
|
|
|
11509
11711
|
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
|
|
11712
|
+
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
11713
|
longer has to 404 for it either.
|
|
11513
11714
|
|
|
11514
11715
|
What it never receives is the interesting half. The paths the host owns —
|
|
11515
|
-
`invokePath`, `healthPath`, `conversationPath` — never reach it,
|
|
11516
|
-
wrong method on one of
|
|
11716
|
+
`invokePath`, `healthPath`, `conversationPath` — never reach it, _including a
|
|
11717
|
+
wrong method on one of them_, because a hook that could claim `POST /invoke`
|
|
11517
11718
|
would be a second door wearing the first one's name. And it is refused at
|
|
11518
11719
|
construction beside `{ server }`, by name: there, unmatched paths already fall
|
|
11519
11720
|
through to your own `'request'` listeners, so a second way to answer them would
|
|
@@ -11560,7 +11761,7 @@ hosts rather than by care.
|
|
|
11560
11761
|
chunks are coerced back to bytes, and no bytes are lost doing it: `setEncoding`
|
|
11561
11762
|
decodes through a `StringDecoder`, which holds a partial multi-byte sequence
|
|
11562
11763
|
across a chunk boundary rather than splitting it. Pinned by writing a body in
|
|
11563
|
-
two TCP writes with the split placed
|
|
11764
|
+
two TCP writes with the split placed _inside_ a four-byte character and
|
|
11564
11765
|
asserting it round-trips. Reachable only through `{ server }` — the mode built
|
|
11565
11766
|
for co-listeners — and reproduced there, with a real second listener on a real
|
|
11566
11767
|
shared socket rather than a stubbed request.
|
|
@@ -11649,7 +11850,7 @@ avoid admitting the cost would have been an accounting trick.
|
|
|
11649
11850
|
**The corrective message is an authored frame with the validator's error as
|
|
11650
11851
|
DATA.** The library's own words come first and say that what follows is a
|
|
11651
11852
|
report about the answer rather than an instruction; the error is quoted
|
|
11652
|
-
verbatim; and
|
|
11853
|
+
verbatim; and _nothing authored follows it_, so there is no trailing sentence
|
|
11653
11854
|
for injected text to pre-empt. A schema whose error message reads "IGNORE ALL
|
|
11654
11855
|
PREVIOUS INSTRUCTIONS" produces a message that still says, first and in the
|
|
11655
11856
|
library's voice, what it is. This is exactly the compaction frame's rule
|
|
@@ -11728,7 +11929,7 @@ bytes — pinned against 7.25 by test, not by care.
|
|
|
11728
11929
|
|
|
11729
11930
|
- **`agentfootprint.agent.output_schema_retry`** — one per failed attempt,
|
|
11730
11931
|
carrying `{ attempt, retriesRemaining, iteration, stage, error, path?,
|
|
11731
|
-
|
|
11932
|
+
correctiveMessageHash }`. 69 typed events across 20 domains. It sits in the
|
|
11732
11933
|
`agent` domain beside `output_schema_validation_failed`, its in-stage
|
|
11733
11934
|
sibling; a new domain for one event that has a family home would have been
|
|
11734
11935
|
taxonomy for its own sake.
|
|
@@ -11789,8 +11990,9 @@ side can call. So this release ships a second port beside the first.
|
|
|
11789
11990
|
```ts
|
|
11790
11991
|
const host = nodeHost({ port: 8080 });
|
|
11791
11992
|
|
|
11792
|
-
await standingAgent({ agent, sessions, host });
|
|
11793
|
-
await host.serveConversations((conversation) => {
|
|
11993
|
+
await standingAgent({ agent, sessions, host }); // POST /invoke
|
|
11994
|
+
await host.serveConversations((conversation) => {
|
|
11995
|
+
// WS /conversation
|
|
11794
11996
|
conversation.onFrame((frame) => conversation.send(answer(frame)));
|
|
11795
11997
|
conversation.onClose(({ by, reason }) => log(by, reason));
|
|
11796
11998
|
});
|
|
@@ -11959,11 +12161,11 @@ So this release does two things, and the second is the reason for the first.
|
|
|
11959
12161
|
```ts
|
|
11960
12162
|
Agent.create({ provider, model })
|
|
11961
12163
|
.act({
|
|
11962
|
-
input:
|
|
11963
|
-
beforeTool: [refundCeiling, fourEyes],
|
|
11964
|
-
afterTool:
|
|
11965
|
-
window:
|
|
11966
|
-
output:
|
|
12164
|
+
input: [scrubSSNs], // the message, before the run commits it
|
|
12165
|
+
beforeTool: [refundCeiling, fourEyes], // every call, before it is dispatched
|
|
12166
|
+
afterTool: [stripPII], // every result, before the model reads it
|
|
12167
|
+
window: slidingWindow({ keepRecentTurns: 12 }), // what the live window keeps
|
|
12168
|
+
output: [noCodenames], // the answer, before the caller gets it
|
|
11967
12169
|
})
|
|
11968
12170
|
.build();
|
|
11969
12171
|
```
|
|
@@ -11977,8 +12179,8 @@ wire, same rows in the ledger.
|
|
|
11977
12179
|
|
|
11978
12180
|
The canonical path is preserved by **demoting the doors, not deleting them**.
|
|
11979
12181
|
`.toolMiddleware()`, `.messageMiddleware()`, `.window()` and `.compaction()`
|
|
11980
|
-
are unchanged and stay open, and they are now documented under
|
|
11981
|
-
|
|
12182
|
+
are unchanged and stay open, and they are now documented under _Composing
|
|
12183
|
+
incrementally_ — because adding one rule to an agent somebody else built is a
|
|
11982
12184
|
real job, and a bundle that must be written all at once cannot do it. That
|
|
11983
12185
|
division is the one-sentence answer to "which spelling": **`.act()` for an
|
|
11984
12186
|
agent you own, a door for a piece you are adding to somebody else's.** A second
|
|
@@ -12227,8 +12429,8 @@ the honest pick is the one whose bytes come back unchanged.
|
|
|
12227
12429
|
**The law, which is the part that outlives this vendor.** In the words of the
|
|
12228
12430
|
field report that bought it:
|
|
12229
12431
|
|
|
12230
|
-
>
|
|
12231
|
-
> only one of them is safe to answer with a fresh start
|
|
12432
|
+
> _An unreadable stored conversation and an absent one are different facts, and
|
|
12433
|
+
> only one of them is safe to answer with a fresh start._
|
|
12232
12434
|
|
|
12233
12435
|
A session nobody has used is absent, and answering it fresh is right. A session
|
|
12234
12436
|
whose bytes are present and unreadable is not, and answering THAT fresh is
|
|
@@ -12246,7 +12448,7 @@ through to the fresh-start path.
|
|
|
12246
12448
|
meant reading how this repo writes an event blob, and `AgentCoreStore` — the
|
|
12247
12449
|
`MemoryStore` adapter — wrote them identically: `payload: [{ blob: entry }]`,
|
|
12248
12450
|
read back as objects only. Same service, same mangling, same silence, different
|
|
12249
|
-
loss: an entry that decodes to nothing was
|
|
12451
|
+
loss: an entry that decodes to nothing was _skipped_, so `list()` came back one
|
|
12250
12452
|
memory short and `get()` came back `null`. Memory that silently stays empty is
|
|
12251
12453
|
indistinguishable from memory that works, until somebody notices the assistant
|
|
12252
12454
|
has forgotten a customer's address. Shipping the cure for one organ while the
|
|
@@ -12289,8 +12491,8 @@ and are still described that way.
|
|
|
12289
12491
|
refused loudly rather than decoded to `undefined`.
|
|
12290
12492
|
|
|
12291
12493
|
- **`hydrate` no longer answers "no session" for a session that HAS one.** The
|
|
12292
|
-
adapter's decode step now distinguishes
|
|
12293
|
-
hydrates as `undefined`) from
|
|
12494
|
+
adapter's decode step now distinguishes _no blob at all_ (an absence, which
|
|
12495
|
+
hydrates as `undefined`) from _a blob it cannot read_ (which travels on to the
|
|
12294
12496
|
shared reading law and is refused by name). Both file and event modes pass the
|
|
12295
12497
|
session id into `checkEnvelope`, so a refusal names the conversation.
|
|
12296
12498
|
|
|
@@ -12439,7 +12641,7 @@ than by an opinion.** See below.
|
|
|
12439
12641
|
satisfy the container contract this repo documents for a managed agent
|
|
12440
12642
|
runtime, and is not trying to.** It serves MCP — statelessly (it neither
|
|
12441
12643
|
issues nor demands a session id, so replicas are interchangeable), on the path
|
|
12442
|
-
and port you choose — and it answers
|
|
12644
|
+
and port you choose — and it answers _neither_ of the contract's two routes:
|
|
12443
12645
|
`GET /ping` and `POST /invocations` are 404s from it. Two protocols, two
|
|
12444
12646
|
paths, two adapters: serve the container contract with
|
|
12445
12647
|
`agentCoreRuntimeHost` and MCP with `mcpServe`. The one thing that is NOT
|
|
@@ -12454,7 +12656,7 @@ The refusal becomes acceptance.
|
|
|
12454
12656
|
Three releases ago the messages slot was a lie: content declared for it was
|
|
12455
12657
|
recorded as injected, counted in the slot composition, routed by the engine, and
|
|
12456
12658
|
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
|
|
12659
|
+
and said why in the same breath — the wire has no system role _inside_ the
|
|
12458
12660
|
message list on the Anthropic family (system is a separate top-level field)
|
|
12459
12661
|
while the OpenAI family carries it, so wiring the slot straight through would
|
|
12460
12662
|
have replaced one uniform gap with a **provider-dependent** one that nothing in
|
|
@@ -12479,7 +12681,7 @@ refused when the run starts, naming the provider and the roles it does. The role
|
|
|
12479
12681
|
is never rewritten to one that fits — changing who appears to speak is a meaning
|
|
12480
12682
|
change the app must make, not the library. **Position**: a delivered message goes
|
|
12481
12683
|
at the end of the window, and if its role would repeat the turn already there, it
|
|
12482
|
-
is
|
|
12684
|
+
is _deferred_ to the next boundary with a sentence on
|
|
12483
12685
|
`messagesDelivery.deferred`, never dropped and never reordered, and never
|
|
12484
12686
|
inserted between a tool call and its result.
|
|
12485
12687
|
|
|
@@ -12535,7 +12737,7 @@ end. An honest limitation stated loudly beats a clever one hidden.
|
|
|
12535
12737
|
### Fixed
|
|
12536
12738
|
|
|
12537
12739
|
- **The cache marker for `field: 'messages'` pointed at the wrong message.** It
|
|
12538
|
-
counted entries in a per-slot list of
|
|
12740
|
+
counted entries in a per-slot list of _injections_ and handed that count to
|
|
12539
12741
|
providers who read it as a position in `request.messages` — two index spaces
|
|
12540
12742
|
under one name. It was unreachable while nothing could target the slot, and
|
|
12541
12743
|
delivery makes it reachable, so it is recomputed against the actual wire array
|
|
@@ -12623,7 +12825,6 @@ end. An honest limitation stated loudly beats a clever one hidden.
|
|
|
12623
12825
|
request's tool array. It is out of this release's scope, and it is now the only
|
|
12624
12826
|
one of the pair left.
|
|
12625
12827
|
|
|
12626
|
-
|
|
12627
12828
|
## [7.20.0] - 2026-08-03
|
|
12628
12829
|
|
|
12629
12830
|
Three small honesty fixes. No new machinery, no delivery change, no wire bytes
|
|
@@ -12733,7 +12934,7 @@ message-sequence rule; it is queued as a feature, with this gap as its evidence.
|
|
|
12733
12934
|
/ `.instruction` / `.fact` all pass through — refuses a hand-built `Injection`
|
|
12734
12935
|
carrying `inject.messages`, so the refusal cannot be walked around. The
|
|
12735
12936
|
message names the limitation and the working alternatives: `slot:
|
|
12736
|
-
|
|
12937
|
+
'system-prompt'` (the default, delivered by every provider), a tool's return
|
|
12737
12938
|
value (a tool result IS a recent message, at the recency the option was
|
|
12738
12939
|
reaching for), and the text passed to `agent.run({ message })`.
|
|
12739
12940
|
|
|
@@ -12941,7 +13142,7 @@ Example: `examples/deploy/durable-sessions.ts`.
|
|
|
12941
13142
|
## [7.18.0] - 2026-08-03
|
|
12942
13143
|
|
|
12943
13144
|
Every agent framework lets you wrap a tool call. Most of them let the wrapper
|
|
12944
|
-
|
|
13145
|
+
_answer_ — return a canned string, a cached value, a "simulated" result — and
|
|
12945
13146
|
the moment one does, the trace is fiction. The model was told a tool ran.
|
|
12946
13147
|
Nothing ran.
|
|
12947
13148
|
|
|
@@ -13000,7 +13201,7 @@ taken afterwards: the trace would show text nobody ever sent.
|
|
|
13000
13201
|
|
|
13001
13202
|
- **`ask` suspends on the SHIPPED pause machinery.** `isAskPause(outcome)`
|
|
13002
13203
|
narrows a paused run and `outcome.ask` carries `{ question, detail?,
|
|
13003
|
-
|
|
13204
|
+
middleware }`. Resume with `checkInApproved` / `checkInDeclined` — the same
|
|
13004
13205
|
human-answer vocabulary check-ins use, deliberately, because a person
|
|
13005
13206
|
approving is a person approving and one word for one thing beats a synonym.
|
|
13006
13207
|
A malformed resume DECLINES, so a governed call can never execute because a
|
|
@@ -13137,7 +13338,7 @@ measured lifetime. Removing is not forgetting.
|
|
|
13137
13338
|
token budget, and reporting a `capTokens` nobody configured would be exactly
|
|
13138
13339
|
the invented number this family refuses.
|
|
13139
13340
|
|
|
13140
|
-
`keepRecentTurns` is required and has no default. It
|
|
13341
|
+
`keepRecentTurns` is required and has no default. It _is_ the policy.
|
|
13141
13342
|
|
|
13142
13343
|
- **`tokenBudget({ thresholdTokens, keepRecentTurns? })` — counted, then
|
|
13143
13344
|
dropped.** Reads the input tokens the adapter reported for the last call and
|
|
@@ -13185,7 +13386,7 @@ measured lifetime. Removing is not forgetting.
|
|
|
13185
13386
|
It appears only at the head (a removal in the middle leaves the opening turn
|
|
13186
13387
|
in place, so there is nothing to fix and a spliced `user` message is its own
|
|
13187
13388
|
risk); it never accumulates (the next drop absorbs it); and if it would not
|
|
13188
|
-
be
|
|
13389
|
+
be _smaller_ than the span it replaces, the whole drop is abandoned under
|
|
13189
13390
|
`summary-not-smaller`, whose meaning generalizes to "the replacement came
|
|
13190
13391
|
back no smaller than the span" rather than growing the closed reason union.
|
|
13191
13392
|
|
|
@@ -13279,7 +13480,7 @@ arrived yet.
|
|
|
13279
13480
|
Example: `examples/context-engineering/11-compaction.ts`.
|
|
13280
13481
|
|
|
13281
13482
|
- **`CompactionRecord` on `scope.compactions` — the fold's half of the law.**
|
|
13282
|
-
One record per over-budget visit,
|
|
13483
|
+
One record per over-budget visit, _including the visits that folded nothing_,
|
|
13283
13484
|
which are the interesting ones. It carries `foldedStageIds` (real
|
|
13284
13485
|
`runtimeStageId`s, resolvable in the commit log), `foldedMessageCount`,
|
|
13285
13486
|
`measuredTokens` vs `thresholdTokens`, exact `windowCharsBefore` /
|
|
@@ -13303,7 +13504,7 @@ arrived yet.
|
|
|
13303
13504
|
- **`COMPACTED_FRAME_PREFIX` / `isCompactedSummary(msg)`** — the authored frame
|
|
13304
13505
|
is a library constant and the summarizer's text is appended after it as data.
|
|
13305
13506
|
A summarizer returning `IGNORE ALL PREVIOUS INSTRUCTIONS` still arrives
|
|
13306
|
-
|
|
13507
|
+
_inside_ a message that says, first and in the library's own words, that what
|
|
13307
13508
|
follows is a summary written by a model and not the conversation. A test pins
|
|
13308
13509
|
exactly that, with a hostile summarizer. The boundary points both ways: the
|
|
13309
13510
|
folded transcript reaches the summarizer between markers the authored
|
|
@@ -13333,7 +13534,7 @@ arrived yet.
|
|
|
13333
13534
|
- **With `.compaction()` configured, the compaction stage becomes the ReAct
|
|
13334
13535
|
loop target** (`compact`), mounted immediately before the previous one. The
|
|
13335
13536
|
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
|
|
13537
|
+
never seen again — and being the target puts the fold _before_ the injection
|
|
13337
13538
|
engine and the three context slots, which is the point: the triggers, the
|
|
13338
13539
|
slots and the wire then all see one window, and no part of the run reasons
|
|
13339
13540
|
over a past the model was not shown. Without `.compaction()` the loop target
|
|
@@ -13350,9 +13551,9 @@ arrived yet.
|
|
|
13350
13551
|
## [7.15.0] - 2026-08-02
|
|
13351
13552
|
|
|
13352
13553
|
7.14.0 shipped two hosting ports that name no cloud, plus a conformance suite,
|
|
13353
|
-
and made a promise:
|
|
13554
|
+
and made a promise: _a cloud adapter is vendor paths and a header mapping on a
|
|
13354
13555
|
port that already worked; if writing one needs a change to a port, the port was
|
|
13355
|
-
wrong
|
|
13556
|
+
wrong._ A promise like that is worth nothing until somebody writes the adapter.
|
|
13356
13557
|
|
|
13357
13558
|
This release writes it. `agentCoreRuntimeHost` is a real cloud runtime's
|
|
13358
13559
|
container contract — different paths, different body fields, the conversation id
|
|
@@ -13366,8 +13567,8 @@ policy store behind the existing permission port, per-request credential vending
|
|
|
13366
13567
|
for Gateway tools, and the memory adapter's `search()` finally wired.
|
|
13367
13568
|
|
|
13368
13569
|
Three seams did have to move, and none of them was a port. They are listed under
|
|
13369
|
-
"Changed" rather than buried, because
|
|
13370
|
-
and
|
|
13570
|
+
"Changed" rather than buried, because _where an adapter needs more than paths
|
|
13571
|
+
and headers_ is the interesting result of an exercise like this — and two of the
|
|
13371
13572
|
three turned out not to be about this vendor at all.
|
|
13372
13573
|
|
|
13373
13574
|
### Added
|
|
@@ -13399,7 +13600,7 @@ three turned out not to be about this vendor at all.
|
|
|
13399
13600
|
Example: `examples/deploy/agentcore-runtime.ts`.
|
|
13400
13601
|
|
|
13401
13602
|
- **`httpHost({ name, wire, invokePath, healthPath, port?, hostname?,
|
|
13402
|
-
|
|
13603
|
+
capabilities? })` — the HTTP work, parameterised by the JSON dialect it
|
|
13403
13604
|
speaks.** Draining on close, aborting when the caller hangs up, failing a
|
|
13404
13605
|
handler that throws, failing a handler that answers nothing, mapping refusal
|
|
13405
13606
|
codes to status codes, and choosing between one JSON body and Server-Sent
|
|
@@ -13414,7 +13615,7 @@ three turned out not to be about this vendor at all.
|
|
|
13414
13615
|
matching and gets it subtly wrong in exactly one deployment.
|
|
13415
13616
|
|
|
13416
13617
|
- **`agentCorePolicy({ policyStoreId, region?, onUnavailable?, onWarning?,
|
|
13417
|
-
|
|
13618
|
+
principalFor?, name?, cacheSize? })` — an AgentCore policy store behind the
|
|
13418
13619
|
existing `PermissionChecker` port** (`agentfootprint/security`). Every
|
|
13419
13620
|
attempted tool call becomes one evaluation.
|
|
13420
13621
|
|
|
@@ -13462,7 +13663,7 @@ three turned out not to be about this vendor at all.
|
|
|
13462
13663
|
|
|
13463
13664
|
Results are marked `metadata.source: 'agentcore-memory-record'`, because
|
|
13464
13665
|
`search` reads a genuinely different population than `list`: the records
|
|
13465
|
-
AgentCore's extraction strategies
|
|
13666
|
+
AgentCore's extraction strategies _derived from_ your events, whose ids belong
|
|
13466
13667
|
to AgentCore, so `store.get(result.entry.id)` will not find them.
|
|
13467
13668
|
|
|
13468
13669
|
There is **no `stream()`**. AgentCore Memory has no streaming data-plane
|
|
@@ -13579,7 +13780,7 @@ and if writing one ever needs a change to a port, the port was wrong.
|
|
|
13579
13780
|
- **`standingAgent({ agent, sessions, host, onConcurrentInvoke? })` — the
|
|
13580
13781
|
composer.** Per request: wake and hydrate the session, resume that
|
|
13581
13782
|
conversation or start a fresh one, persist what the run left behind, reply.
|
|
13582
|
-
Persist happens
|
|
13783
|
+
Persist happens _before_ the answer goes out, so a queued next turn can never
|
|
13583
13784
|
read state older than the answer already given.
|
|
13584
13785
|
|
|
13585
13786
|
It restates the `resumeOnError` tool re-execution caveat **verbatim** in its
|
|
@@ -13593,7 +13794,7 @@ and if writing one ever needs a change to a port, the port was wrong.
|
|
|
13593
13794
|
afterwards belongs to whichever started last, so one session's envelope can end
|
|
13594
13795
|
up holding another session's conversation with nothing in the recording to say
|
|
13595
13796
|
so. `ConcurrentInvokePolicy` is the separate question of a second turn of the
|
|
13596
|
-
|
|
13797
|
+
_same_ conversation: `'reject'` (default) refuses with a `ConcurrentRunError`
|
|
13597
13798
|
naming the active run (`409`), `'enqueue'` queues it FIFO behind the run whose
|
|
13598
13799
|
state it will then read. A request for a **different** session is never
|
|
13599
13800
|
refused — it waits its turn.
|
|
@@ -13634,7 +13835,7 @@ person something, `standingAgent` answers with a `PauseNotCarriedError` and
|
|
|
13634
13835
|
writes **nothing** — the session keeps exactly the conversation it had before the
|
|
13635
13836
|
request. Over HTTP that is a `409`, not a `500`, because the agent did not break
|
|
13636
13837
|
and every dashboard that sees a 500 will conclude otherwise. `'conversation-v1'`
|
|
13637
|
-
stores a conversation; a paused run is a conversation
|
|
13838
|
+
stores a conversation; a paused run is a conversation _plus_ an engine
|
|
13638
13839
|
checkpoint, and storing half of it would be worse than storing none. Carrying a
|
|
13639
13840
|
pause would be a NEW format name in the same envelope — which is precisely what
|
|
13640
13841
|
the version in the format is for.
|
|
@@ -13649,7 +13850,7 @@ host can send any string there, including someone else's.
|
|
|
13649
13850
|
|
|
13650
13851
|
**The conformance suite is the deliverable, not the tests for it.** One handler
|
|
13651
13852
|
constant, served by `nodeHost` and by a minimal in-process host that declares
|
|
13652
|
-
|
|
13853
|
+
_no_ capabilities so the buffering path is exercised rather than assumed, with a
|
|
13653
13854
|
final pair of cases invoking both and comparing directly. A future adapter —
|
|
13654
13855
|
including a cloud one — is measured against that file.
|
|
13655
13856
|
|
|
@@ -13677,7 +13878,7 @@ other way, and a value committed where run-level values already commit.
|
|
|
13677
13878
|
and the body still arrives only after it does. What changes is who can edit a
|
|
13678
13879
|
playbook, and whether changing the refund policy shows up as a reviewable diff.
|
|
13679
13880
|
|
|
13680
|
-
A skill body is
|
|
13881
|
+
A skill body is _instructions to a model_, so where it came from is a security
|
|
13681
13882
|
property rather than a convenience — content fetched at run time is content
|
|
13682
13883
|
someone else can change after you reviewed it. The loader therefore accepts a
|
|
13683
13884
|
local directory and nothing else: a URL is **refused by name**, not fetched.
|
|
@@ -13748,7 +13949,7 @@ other way, and a value committed where run-level values already commit.
|
|
|
13748
13949
|
|
|
13749
13950
|
- **`McpClientOptions.signal` cancels a hung MCP tool call again — it never
|
|
13750
13951
|
did.** The signal was being sent as part of the `tools/call` request
|
|
13751
|
-
|
|
13952
|
+
_params_, where an `AbortSignal` JSON-serializes to `{}`: the server received
|
|
13752
13953
|
a meaningless field and the caller received no cancellation. The SDK takes
|
|
13753
13954
|
per-request options in a separate trailing argument, which is where the signal
|
|
13754
13955
|
now goes; it is threaded to `connect()` and `listTools()` for the same reason,
|
|
@@ -13787,7 +13988,7 @@ synchronous, and making it async to accommodate a resolver would shift every
|
|
|
13787
13988
|
agent's timing for a feature most agents do not use. A resolver that needs I/O
|
|
13788
13989
|
can do it before `run()` and close over the result.
|
|
13789
13990
|
|
|
13790
|
-
Every debugging session starts at a
|
|
13991
|
+
Every debugging session starts at a _variable_ — "where did that instruction come
|
|
13791
13992
|
from?", "which loop wrote the history it answered from?" — and both halves of the
|
|
13792
13993
|
answer already existed, in vocabularies that did not meet. footprintjs 9.13 records
|
|
13793
13994
|
a variable's whole life in commit indices and runtimeStageIds; the localizer thinks
|
|
@@ -13798,7 +13999,7 @@ Joining them turned out to buy something bigger than a nicer read-out. The backw
|
|
|
13798
13999
|
walk narrows each loop with embedding similarity — a proxy that points at a
|
|
13799
14000
|
neighbourhood and cannot separate a planted instruction from an innocent same-topic
|
|
13800
14001
|
sibling. But where the recording carries per-write provenance, one part of that guess
|
|
13801
|
-
is unnecessary: the commit log
|
|
14002
|
+
is unnecessary: the commit log _says_ which write produced the value this loop read.
|
|
13802
14003
|
So the walk stops guessing exactly there — and keeps saying so everywhere else.
|
|
13803
14004
|
|
|
13804
14005
|
### Added
|
|
@@ -13826,7 +14027,7 @@ So the walk stops guessing exactly there — and keeps saying so everywhere else
|
|
|
13826
14027
|
same hops, same order, same verdicts (pinned by a deep-equal test).
|
|
13827
14028
|
|
|
13828
14029
|
The proxy still picks WHO; dataflow picks WHERE. A stage-level edge never becomes
|
|
13829
|
-
an exact hop, and a recorded edge outranks the
|
|
14030
|
+
an exact hop, and a recorded edge outranks the _inferred_ proximate-tool hop —
|
|
13830
14031
|
better evidence wins, and the hop record says which kind it used.
|
|
13831
14032
|
|
|
13832
14033
|
- **`AgentOptions.writeProvenance`** (`'off'` default, `'reads-prefix'` to enable) —
|
|
@@ -13843,7 +14044,7 @@ So the walk stops guessing exactly there — and keeps saying so everywhere else
|
|
|
13843
14044
|
`coverage: 'exact'` requires **positive** evidence — at least one recorded per-write
|
|
13844
14045
|
edge — not merely the absence of a conservative one. A key nothing ever reads back
|
|
13845
14046
|
(the agent's `lastToolResult`: written by tool-calls, never read by `call-llm`) has
|
|
13846
|
-
an empty edge set, so "no conservative edges" is
|
|
14047
|
+
an empty edge set, so "no conservative edges" is _vacuously_ true; scoring that as
|
|
13847
14048
|
exact would hand the walk its most confident hop on its least-evidenced key. Absence
|
|
13848
14049
|
of dataflow is `'unknown'`, never exactness.
|
|
13849
14050
|
|
|
@@ -13867,7 +14068,7 @@ it with measurements instead of re-deriving why it was strict.
|
|
|
13867
14068
|
|
|
13868
14069
|
## [7.11.0] - 2026-08-02
|
|
13869
14070
|
|
|
13870
|
-
A pipeline whose steps form a
|
|
14071
|
+
A pipeline whose steps form a _shape_ rather than a line — one step feeding two
|
|
13871
14072
|
independent lookups, a third waiting for both — had no home here. You could nest
|
|
13872
14073
|
a `Parallel` inside a `Sequence`, but then you were scheduling it by hand, and
|
|
13873
14074
|
the values did not survive the trip.
|
|
@@ -13939,7 +14140,7 @@ switches keep compiling. Same reasoning as `workflow()` in 7.10.0.
|
|
|
13939
14140
|
## [7.10.0] - 2026-08-02
|
|
13940
14141
|
|
|
13941
14142
|
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
|
|
14143
|
+
and both were fiddly in the same way: the wiring is easy to get _nearly_ right,
|
|
13943
14144
|
and nearly right fails quietly — at run time, several steps away from the
|
|
13944
14145
|
mistake.
|
|
13945
14146
|
|
|
@@ -13948,7 +14149,7 @@ mistake.
|
|
|
13948
14149
|
- **`llmRouter` — the classic Swarm decision, packaged.** `swarm()` asks for a
|
|
13949
14150
|
`route()` that is sync and pure, and it means it: the `Conditional` evaluates
|
|
13950
14151
|
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
|
|
14152
|
+
after every turn. So the LLM decision has to happen _somewhere else_, before
|
|
13952
14153
|
the message reaches `route` — and that placement is the part everyone
|
|
13953
14154
|
re-invented, along with the prompt, the parsing, and a second copy of the
|
|
13954
14155
|
agent roster that drifts from the first.
|
|
@@ -13962,8 +14163,8 @@ mistake.
|
|
|
13962
14163
|
with a stale decision.
|
|
13963
14164
|
|
|
13964
14165
|
The decision is validated JSON — `RoutingDecision` = `{ agentId?, message,
|
|
13965
|
-
|
|
13966
|
-
halt sentinel. An id that isn't in the roster is kept verbatim,
|
|
14166
|
+
reason? }`. No `agentId` means "done", and the swarm halts through its own
|
|
14167
|
+
halt sentinel. An id that isn't in the roster is kept verbatim, _not_ quietly
|
|
13967
14168
|
swapped for a plausible one: `swarm()`'s existing done/fallback law then ends
|
|
13968
14169
|
the run, so a hallucinated agent shows up as a halt instead of a wrong answer.
|
|
13969
14170
|
Unusable output throws `RoutingDecisionError` with the model's raw text
|
|
@@ -13998,7 +14199,7 @@ mistake.
|
|
|
13998
14199
|
feeds the next step's `{ message }`, the house convention every LLM runner
|
|
13999
14200
|
speaks; anything else must match exactly) — a chain that doesn't line up is a
|
|
14000
14201
|
compile error, pinned by `@ts-expect-error` fixtures under `npm run
|
|
14001
|
-
|
|
14202
|
+
test:types`. At run time, values are handed over **unchanged**: objects stay
|
|
14002
14203
|
objects. `workflow(draft, edit)` over two `LLMCall`s reads exactly as it
|
|
14003
14204
|
always did.
|
|
14004
14205
|
|
|
@@ -14028,7 +14229,7 @@ mistake.
|
|
|
14028
14229
|
|
|
14029
14230
|
**If you pass `dimensions` today, your vectors change length.** They were the
|
|
14030
14231
|
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
|
|
14232
|
+
you have already embedded and stored was written at the _old_ length, so a store
|
|
14032
14233
|
built with `openaiEmbedder({ dimensions: 256 })` on 7.8 holds 1536-long vectors
|
|
14033
14234
|
and will not match new 256-long queries. **Re-embed, or drop `dimensions` to
|
|
14034
14235
|
keep the old lengths.**
|
|
@@ -14046,7 +14247,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
|
|
|
14046
14247
|
1536 for everything, so `text-embedding-3-large` under-reported by half.
|
|
14047
14248
|
- An **unknown model with no `dimensions` is now a construction-time error**
|
|
14048
14249
|
instead of a silent 1536. This is the breaking edge: `openaiEmbedder({
|
|
14049
|
-
|
|
14250
|
+
baseURL, model: 'nomic-embed-text' })` against a gateway, an Ollama server or
|
|
14050
14251
|
an Azure deployment name now throws until you state the length. That
|
|
14051
14252
|
population is exactly the one that was being lied to. One option fixes it:
|
|
14052
14253
|
`{ dimensions: 768 }`.
|
|
@@ -14062,7 +14263,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
|
|
|
14062
14263
|
- **`localEmbedder({ backend })` / `staticEmbedder({ backend })` — pass an
|
|
14063
14264
|
already-imported module, and the on-device embedders work in a browser.** To
|
|
14064
14265
|
keep the heavy peer deps optional, both factories import them through a
|
|
14065
|
-
|
|
14266
|
+
_variable_ specifier — which no bundler can see through. The bare name
|
|
14066
14267
|
survived a production build and reached the browser unresolved:
|
|
14067
14268
|
`TypeError: Failed to resolve module specifier '@huggingface/transformers'`.
|
|
14068
14269
|
The capability was there all along; only the packaging blocked it. Now the
|
|
@@ -14099,7 +14300,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
|
|
|
14099
14300
|
|
|
14100
14301
|
- **Docs-truth check — an ongoing, honest answer to "do the docs describe what
|
|
14101
14302
|
the code actually does?"** `npm run docs:truth` (new CI job `docs-truth`)
|
|
14102
|
-
answers three
|
|
14303
|
+
answers three _separate_ questions for every capability the package exposes,
|
|
14103
14304
|
because their combinations are different bugs: DECLARED (in the published
|
|
14104
14305
|
surface), DOCUMENTED (described in prose on the site), EXERCISED (a real run
|
|
14105
14306
|
produces it). Declared/documented/never-exercised is the shape a dead or
|
|
@@ -14113,7 +14314,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
|
|
|
14113
14314
|
therefore cannot see a single `agentfootprint/<subpath>` symbol. The
|
|
14114
14315
|
surface is reported per subpath, since root-barrel-vs-subpath is itself a
|
|
14115
14316
|
known source of user confusion. Events come from `ALL_EVENT_TYPES`.
|
|
14116
|
-
- The DOCUMENTED column counts
|
|
14317
|
+
- The DOCUMENTED column counts _only_ prose on the 63 hand-written pages
|
|
14117
14318
|
under `docs-next/content/docs`. Both TypeDoc trees
|
|
14118
14319
|
(`docs-next/content/docs/api/`, `docs/api-reference/`) are excluded — they
|
|
14119
14320
|
are generated from source, so every symbol appears in them by construction
|