agentfootprint 9.82.0 → 9.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +718 -504
- package/CLAUDE.md +1 -0
- package/README.md +15 -15
- package/ai-instructions/claude-code/SKILL.md +1 -1
- package/dist/core/Agent.js +43 -1
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/agent/AgentBuilder.js.map +1 -1
- package/dist/core/agent/evidence/evidenceIndex.js +96 -11
- package/dist/core/agent/evidence/evidenceIndex.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/evidence/gate.js +66 -6
- package/dist/core/agent/evidence/gate.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/route.js +71 -12
- package/dist/core/agent/stages/route.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/doors/skill-graph.js +2 -1
- package/dist/doors/skill-graph.js.map +1 -1
- package/dist/esm/core/Agent.d.ts +4 -0
- package/dist/esm/core/Agent.js +43 -1
- 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/evidence/evidenceIndex.d.ts +91 -5
- package/dist/esm/core/agent/evidence/evidenceIndex.js +96 -11
- package/dist/esm/core/agent/evidence/evidenceIndex.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/evidence/gate.d.ts +27 -0
- package/dist/esm/core/agent/evidence/gate.js +66 -6
- package/dist/esm/core/agent/evidence/gate.js.map +1 -1
- package/dist/esm/core/agent/evidence/types.d.ts +12 -0
- 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/route.d.ts +6 -1
- package/dist/esm/core/agent/stages/route.js +69 -10
- package/dist/esm/core/agent/stages/route.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/types.d.ts +66 -0
- 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/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 +2 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/integrity/disposition/lifecycle.d.ts +17 -1
- package/dist/esm/integrity/disposition/lifecycle.js +26 -1
- package/dist/esm/integrity/disposition/lifecycle.js.map +1 -1
- package/dist/esm/integrity/finding/types.d.ts +12 -2
- package/dist/esm/integrity/finding/types.js.map +1 -1
- package/dist/esm/integrity/prior-turn-evidence/check.d.ts +145 -0
- package/dist/esm/integrity/prior-turn-evidence/check.js +200 -0
- package/dist/esm/integrity/prior-turn-evidence/check.js.map +1 -0
- 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 +90 -7
- 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/lib/trace-toolpack/traceToolpack.js +3 -2
- package/dist/esm/lib/trace-toolpack/traceToolpack.js.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/integrity/disposition/lifecycle.js +26 -1
- package/dist/integrity/disposition/lifecycle.js.map +1 -1
- package/dist/integrity/finding/types.js.map +1 -1
- package/dist/integrity/prior-turn-evidence/check.js +204 -0
- package/dist/integrity/prior-turn-evidence/check.js.map +1 -0
- 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 +92 -8
- 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/lib/trace-toolpack/traceToolpack.js +3 -2
- package/dist/lib/trace-toolpack/traceToolpack.js.map +1 -1
- package/dist/types/core/Agent.d.ts +4 -0
- 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/evidence/evidenceIndex.d.ts +91 -5
- package/dist/types/core/agent/evidence/evidenceIndex.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/evidence/gate.d.ts +27 -0
- package/dist/types/core/agent/evidence/gate.d.ts.map +1 -1
- package/dist/types/core/agent/evidence/types.d.ts +12 -0
- package/dist/types/core/agent/evidence/types.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/route.d.ts +6 -1
- package/dist/types/core/agent/stages/route.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/types.d.ts +66 -0
- package/dist/types/core/agent/types.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 +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/integrity/disposition/lifecycle.d.ts +17 -1
- package/dist/types/integrity/disposition/lifecycle.d.ts.map +1 -1
- package/dist/types/integrity/finding/types.d.ts +12 -2
- package/dist/types/integrity/finding/types.d.ts.map +1 -1
- package/dist/types/integrity/prior-turn-evidence/check.d.ts +146 -0
- package/dist/types/integrity/prior-turn-evidence/check.d.ts.map +1 -0
- 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/lib/trace-toolpack/traceToolpack.d.ts.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,203 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [9.84.0] - 2026-09-03
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`read_skill` refused the skill the model was already in.** A turn routed
|
|
15
|
+
decisively to `X`, the model called `read_skill("X")` to find out where it
|
|
16
|
+
stood, and the gate answered _"`read_skill("X")` is not reachable from here.
|
|
17
|
+
Reachable skills: …"_ — about the one skill whose body was in that call's
|
|
18
|
+
system prompt and whose tools were in that same call's tool list. The cursor
|
|
19
|
+
is in neither half of `hops ∪ open` by construction: `makeReachableSkills`
|
|
20
|
+
filters it out of its own successor set (a move to where you already are is
|
|
21
|
+
not a move) and `openSkillIds()` excludes every graph-wired skill. Nobody had
|
|
22
|
+
written the case for _"you asked for the room you are standing in."_ Read as a
|
|
23
|
+
claim about AVAILABILITY — which is how a model reads _"not reachable"_ — it
|
|
24
|
+
says the opposite of the request it arrived in. A field report recorded the
|
|
25
|
+
consequence three times in one day: the model concluded its capability was
|
|
26
|
+
gone and answered that it could not help, while the skill's tools sat on the
|
|
27
|
+
wire, loaded and callable.
|
|
28
|
+
|
|
29
|
+
A self-call now gets the truth instead of a refusal. It names where the model
|
|
30
|
+
stands and which tools it could call, taken from the merged wire list the LLM
|
|
31
|
+
stage actually sent, intersected with the skill's own declared tools — never
|
|
32
|
+
from the declaration alone. Every configuration that would make that false has
|
|
33
|
+
its own wording: a skill declaring no tools says so, a step or park hold-out
|
|
34
|
+
says the tools were withheld rather than naming them, and a call whose wire
|
|
35
|
+
cannot be established says nothing about tools at all. Mechanically it is still
|
|
36
|
+
a rejection — no activation, no cursor move, and the refusal budget still
|
|
37
|
+
counts it, because a self-call _loop_ is exactly the stuck model that budget
|
|
38
|
+
exists to escalate.
|
|
39
|
+
|
|
40
|
+
**Every clause is anchored to one named call.** A tool result is composed on
|
|
41
|
+
one iteration and re-read on every call after it, including the out-of-budget
|
|
42
|
+
wrap-up that carries no tools under _"Do not request tools."_ So the notice
|
|
43
|
+
makes no forward-looking claim at all: no exhortation to act, no offer of a
|
|
44
|
+
move, no clause conditioned on a budget or a posture that can change after the
|
|
45
|
+
sentence is written. Deixis counts as forward-looking — _"the call you just
|
|
46
|
+
made"_ denotes a different call on each re-read, so the anchor is named once
|
|
47
|
+
in the opening sentence and every later clause refers back to it.
|
|
48
|
+
|
|
49
|
+
- **The `read_skill` description said the same thing in the other channel.** The
|
|
50
|
+
current skill was listed under _"Not reachable from here (read_skill for these
|
|
51
|
+
will be refused)"_, where it appeared purely as an artefact of that same
|
|
52
|
+
filter. It is in neither column now, and the description names the cursor on
|
|
53
|
+
every call that has one — the positive signal whose absence was the root of the
|
|
54
|
+
field failure, since the system prompt carried the skill's body with nothing
|
|
55
|
+
saying which skill it was. A genuinely unreachable skill is still named there,
|
|
56
|
+
and a genuinely unreachable hop keeps its refusal word for word.
|
|
57
|
+
|
|
58
|
+
- **The description no longer predicts what `read_skill` will do.** Naming the
|
|
59
|
+
cursor is the fix; every sentence tried alongside it turned out false
|
|
60
|
+
somewhere. _"read_skill MOVES you to a DIFFERENT skill"_ is false at compose
|
|
61
|
+
time under `strictness: 'rails'` (every model hop refused) and under `'guard'`
|
|
62
|
+
off an outstanding menu, where the posture arm contradicts it head-on. Its
|
|
63
|
+
replacement — _"You do not need read_skill to go on using it"_ — was argued to
|
|
64
|
+
be a claim about necessity that no posture, budget or hold-out could falsify,
|
|
65
|
+
and the **park** falsifies it: a parked map member keeps the cursor, loses its
|
|
66
|
+
body and its tools, and `read_skill` is then the only door back. The
|
|
67
|
+
description is composed before the hold-outs run, so it cannot know when such a
|
|
68
|
+
claim would be lying. Outside an outstanding menu it now states the name and
|
|
69
|
+
stops. The menu's stay clause is unchanged.
|
|
70
|
+
|
|
71
|
+
- **Role visibility now covers the cursor.** The description read the cursor id
|
|
72
|
+
past the `hiddenIds` filter, so a role denied `skill_read` on the skill the
|
|
73
|
+
graph had routed to was still told _"You are in '\<that skill\>'"_ — leaking
|
|
74
|
+
the name of a capability no cursor move would ever grant it. A hidden cursor is
|
|
75
|
+
named nowhere: not as reachable, not as refusable, not as the cursor, and not
|
|
76
|
+
in the menu's stay clause. The security suite's _"a hidden skill is never
|
|
77
|
+
named"_ property is now driven on the `.skillGraph()` path as well as
|
|
78
|
+
`.skill()`; it stayed green through the leak because its agents used `.skill()`
|
|
79
|
+
only, so the leaking line never executed.
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **`saidByPerson(ctx)` / `isSaidByPerson(msg)` — telling what a person said from
|
|
84
|
+
what the library wrote.** Five classes of `role: 'user'` message are authored
|
|
85
|
+
by this library, not by a person: the compaction frame, the drop notice (whose
|
|
86
|
+
text names tools), the schema-check and evidence-check corrections, and any
|
|
87
|
+
injection-delivered message. The window's own refusal engine has always applied
|
|
88
|
+
that rule; a `when` predicate could not, because `InjectionContext.history`
|
|
89
|
+
exposes only `{ role, content, toolName? }`. An author writing an entry rule
|
|
90
|
+
that reads history was silently matching on our own bookkeeping. One
|
|
91
|
+
implementation, reused by both — the rule cannot drift between routing and the
|
|
92
|
+
window.
|
|
93
|
+
|
|
94
|
+
- **`SkillRejectedPayload.reason`** — `'self-call' | 'unreachable' | 'posture'`,
|
|
95
|
+
optional and additive, so a consumer can tell a self-call from a genuine
|
|
96
|
+
unreachable hop without comparing two fields.
|
|
97
|
+
|
|
98
|
+
### Documentation
|
|
99
|
+
|
|
100
|
+
- **`strictness` says what a posture governs, exactly.** A posture governs the
|
|
101
|
+
model's `read_skill` door and nothing else. Two doors stay open under all three
|
|
102
|
+
postures: OPEN skills, already stated, and a tool's `propose-transition`, now
|
|
103
|
+
stated with its reason and its reachability check. `'rails'` means _the model
|
|
104
|
+
never routes_ — never _"nothing but my declared edges routes"_; a tool of yours
|
|
105
|
+
that proposes is a route you declared in code instead of in the graph. No
|
|
106
|
+
behaviour changed: the exemption is recorded in three places and pinned by a
|
|
107
|
+
test whose title is the argument.
|
|
108
|
+
|
|
109
|
+
- **The injection-engine README's runtime picture matched an older engine.** Its
|
|
110
|
+
diagram drew one box that "evaluates triggers"; the engine is a four-stage
|
|
111
|
+
footprintjs subflow — Gather, Evaluate, Route, Delta — and the cursor, the step
|
|
112
|
+
pointer, the instruction leases and map engagement all advance inside Evaluate.
|
|
113
|
+
The events table placed `context.evaluated` at subflow exit; the code emits it
|
|
114
|
+
in stage 2 of 4. Both corrected, along with three counts that had drifted.
|
|
115
|
+
|
|
116
|
+
## [9.83.0] - 2026-09-03
|
|
117
|
+
|
|
118
|
+
### Fixed
|
|
119
|
+
|
|
120
|
+
- **The evidence gate claimed a boundary it did not measure.** Both of its
|
|
121
|
+
user-facing sentences — the correction it sends the model
|
|
122
|
+
(`buildEvidenceCorrection`) and the warning it prints an operator
|
|
123
|
+
(`evidenceRefusalSentence`) — said the flagged values _"appear in NO tool
|
|
124
|
+
result **from this turn**"_. The index behind them has never been turn-scoped:
|
|
125
|
+
it walks every `role: 'tool'` turn in the history. The library was asserting a
|
|
126
|
+
scope it could not honour, in the two places that assertion is read.
|
|
127
|
+
|
|
128
|
+
Both now say what the check really reaches — _"appear in no tool result this
|
|
129
|
+
run read"_ — which is both true and the stronger claim, and the operator
|
|
130
|
+
sentence adds the two facts a reader needs: that the corpus is the LIVE
|
|
131
|
+
WINDOW (a window strategy rewrites `scope.history` in place, so a dropped
|
|
132
|
+
result is not in it), and that `noticePriorTurnEvidence` is what answers the
|
|
133
|
+
recency question. The frame PREFIX is unchanged, so
|
|
134
|
+
`isLibraryAuthoredTurn` and every consumer matching on it are untouched.
|
|
135
|
+
|
|
136
|
+
### Added
|
|
137
|
+
|
|
138
|
+
- **`noticePriorTurnEvidence` — the answer is grounded, and nothing this turn
|
|
139
|
+
fetched grounds it.** Default off.
|
|
140
|
+
|
|
141
|
+
The measured failure: a consumer's agent answered a data question with **zero
|
|
142
|
+
tool calls**, and the gate approved it — `LLM calls 1 · Tool calls 0 ·
|
|
143
|
+
Iterations 1`, then _"All 7 values in the answer were found in what the tools
|
|
144
|
+
returned — the answer stands."_ They were found: in an inventory result from
|
|
145
|
+
four turns earlier, fetched for a different question. The user had asked about
|
|
146
|
+
array performance; the answer recommended enabling a collector that had been
|
|
147
|
+
running for months. Two turns did it back to back. Every rail passed honestly
|
|
148
|
+
— the gate measures GROUNDEDNESS and had no notion of WHEN a value was
|
|
149
|
+
grounded.
|
|
150
|
+
|
|
151
|
+
Every indexed form now carries the turn that last served it — one number,
|
|
152
|
+
stamped during the walk the index was already doing (`EvidenceCorpus.values`
|
|
153
|
+
became a `Map<form, turn>`; a TURN starts at each `role: 'user'` message the
|
|
154
|
+
library did not author). When at least one value in the answer is grounded and
|
|
155
|
+
**not one of them** came from the turn being answered, one `advisory` finding
|
|
156
|
+
is filed at the claim seam:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const agent = Agent.create({ provider, model, noticePriorTurnEvidence: true })
|
|
160
|
+
.tool(arrayInventory)
|
|
161
|
+
.namesAndNumbersFromEvidence() // ← the other half: it owns the extractor
|
|
162
|
+
.build();
|
|
163
|
+
|
|
164
|
+
await agent.run('what arrays are there?'); // fetches, answers, files nothing
|
|
165
|
+
await agent.followUp('how is array performance?'); // no tool call, answers from turn 1
|
|
166
|
+
// → prior-turn-evidence: 3 grounded value(s), all last served in turn 1,
|
|
167
|
+
// and this turn called no tool at all.
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**The corpus is deliberately NOT narrowed to this turn.** That would have made
|
|
171
|
+
the old sentence true and been the wrong fix: _"and what about that disk?"_
|
|
172
|
+
leans on the previous turn's rows legitimately, and a check that cries wolf is
|
|
173
|
+
a check somebody switches off. ONE grounded value from this turn's own results
|
|
174
|
+
files nothing — not a threshold to tune, but the falsification of the claim
|
|
175
|
+
being tested. A follow-up that calls a tool usually gets that for free,
|
|
176
|
+
because a lookup keyed on an earlier identifier echoes it back.
|
|
177
|
+
|
|
178
|
+
A turn that served no tool results at all is the SAME finding with a stronger
|
|
179
|
+
witness, not a second kind: it is a cheaper proof of the identical fact.
|
|
180
|
+
|
|
181
|
+
**The ceiling** ships as `PRIOR_TURN_EVIDENCE_CEILING`, exported and quoted
|
|
182
|
+
verbatim into every message: referring back is indistinguishable, by evidence
|
|
183
|
+
alone, from going stale; the ordinals count only the turns still in the live
|
|
184
|
+
window, so the distance is a FLOOR (the boundary itself is exact — the
|
|
185
|
+
current request is un-droppable); and values that reached the model through
|
|
186
|
+
`.memory()` recall or RAG are exempt from grounding and invisible to it, so it
|
|
187
|
+
can under-report and never over-report.
|
|
188
|
+
|
|
189
|
+
**Two halves arm it**, and the second is structural rather than a policy
|
|
190
|
+
companion: the dial AND `.namesAndNumbersFromEvidence()`, whose extractor
|
|
191
|
+
decides which tokens in an answer are values at all. It REPORTS — whether an
|
|
192
|
+
answer is advised or refused stays the gate's own `posture` decision, and
|
|
193
|
+
nothing here blocks, revises or rewrites anything. Absent, a run is
|
|
194
|
+
byte-identical save the registered `prior-turn-evidence` row filed
|
|
195
|
+
`not-applicable`, which is the family's law rather than an exception to it.
|
|
196
|
+
|
|
197
|
+
Three terminal exits reach a caller without the gate ever producing a
|
|
198
|
+
grounding reading — an empty answer, a middleware denial, and an answer the
|
|
199
|
+
output schema rejected — and each files its disposition rather than leaving
|
|
200
|
+
the armed row untouched. An untouched armed row is what `assertAlive` reads
|
|
201
|
+
as wiring rot, so without this an empty answer under
|
|
202
|
+
`integrityPosture: 'dev'` would have failed a healthy run with
|
|
203
|
+
`CheckerDeadError`.
|
|
204
|
+
|
|
205
|
+
Docs: [Prior-turn evidence](https://agentfootprint.dev/docs/monitor/prior-turn-evidence).
|
|
206
|
+
|
|
10
207
|
## [9.82.0] - 2026-08-30
|
|
11
208
|
|
|
12
209
|
### Added
|
|
@@ -15,7 +212,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
212
|
is generated from what the run itself said: the branch descriptions the chart
|
|
16
213
|
declared, and the rule labels this run's `decide()` evidence carried. For one
|
|
17
214
|
branch, both sources are silent by construction — the DEFAULT. It is the
|
|
18
|
-
branch chosen by
|
|
215
|
+
branch chosen by _no rule_ (it fires exactly when every rule failed, so no
|
|
19
216
|
`label` describes it), and when the decider lives inside a dynamically
|
|
20
217
|
generated fan-out branch the branch chart does not exist at build time either,
|
|
21
218
|
so there is no declared description to fall back on.
|
|
@@ -92,7 +289,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
92
289
|
sdk: { Client, StreamableHTTPClientTransport },
|
|
93
290
|
transport: { transport: 'http', url: '/py/mcp' },
|
|
94
291
|
});
|
|
95
|
-
const tools = await sidecar.tools();
|
|
292
|
+
const tools = await sidecar.tools(); // the same readonly Tool[], _meta and all
|
|
96
293
|
```
|
|
97
294
|
|
|
98
295
|
**`connection?: McpConnection`** — the full escape hatch: you connect the
|
|
@@ -166,8 +363,8 @@ arms through a real socket against the real SDK.
|
|
|
166
363
|
|
|
167
364
|
**What none of that proves: a browser.** This repo has no browser test
|
|
168
365
|
environment, so nobody has yet driven initialize/listTools/callTool from an
|
|
169
|
-
actual page. The honest status is
|
|
170
|
-
in a browser
|
|
366
|
+
actual page. The honest status is _proven in Node, fenced at the graph, not gated
|
|
367
|
+
in a browser._ Three costs land on the app, not here: your server must send CORS
|
|
171
368
|
headers (every MCP request preflights, and `Mcp-Session-Id` must be in
|
|
172
369
|
`Access-Control-Expose-Headers`) — `mcpServe` sends none; SSE through a dev or
|
|
173
370
|
production proxy is unproven; and the SDK's client path adds roughly 260 KB
|
|
@@ -186,8 +383,8 @@ first time a tool with an `outputSchema` is validated.
|
|
|
186
383
|
had a standing rule against exactly that. The library was overruling it.
|
|
187
384
|
|
|
188
385
|
The instruction was ours. Every verdict projection shipped `table`
|
|
189
|
-
pre-rendered with `VERDICT_RENDER_NOTE`:
|
|
190
|
-
same rows as `verdicts` — output it VERBATIM."
|
|
386
|
+
pre-rendered with `VERDICT_RENDER_NOTE`: _"table is PRE-RENDERED over the
|
|
387
|
+
same rows as `verdicts` — output it VERBATIM."_ That note is RIGHT wherever
|
|
191
388
|
the model's words are the rows' only surface — a chat client, a log line, an
|
|
192
389
|
email — because the alternative there is retyping, and a retyped identifier
|
|
193
390
|
that looks right and matches nothing is the failure the note exists to stop.
|
|
@@ -200,7 +397,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
200
397
|
it is in. So the caller says, in one word:
|
|
201
398
|
|
|
202
399
|
```ts
|
|
203
|
-
runbookAsTool({ /* … */ presentation: 'panel' });
|
|
400
|
+
runbookAsTool({ /* … */ presentation: 'panel' }); // default: 'prose'
|
|
204
401
|
```
|
|
205
402
|
|
|
206
403
|
- **`'prose'` (the default)** — today's envelope, key for key: `table`
|
|
@@ -266,7 +463,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
266
463
|
`recording_note`. The wire ops that already redeem the walk redeem this with
|
|
267
464
|
**zero new operations**.
|
|
268
465
|
|
|
269
|
-
**OPT-IN, and that is the honest default.** A walk carries
|
|
466
|
+
**OPT-IN, and that is the honest default.** A walk carries _sentences about_
|
|
270
467
|
what happened and no payload from it — values are off by construction
|
|
271
468
|
(`narrative({ includeValues: false })`). A recording is the run: shared
|
|
272
469
|
state, the whole commit log, every attached recorder's data — **whatever the
|
|
@@ -302,7 +499,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
302
499
|
the spine exists to prevent.
|
|
303
500
|
|
|
304
501
|
**`events` is empty by construction, and says so.** It is the typed
|
|
305
|
-
|
|
502
|
+
_agentfootprint_ stream, fired by an agent turn; what ran here is a
|
|
306
503
|
footprintjs chart on its own executor, which fires none. All three keys are
|
|
307
504
|
present (that is what a viewer reads), the empty array is the honest count,
|
|
308
505
|
and the note states it so nobody reads it as a dropped stream — the walk's
|
|
@@ -321,8 +518,8 @@ first time a tool with an `outputSchema` is validated.
|
|
|
321
518
|
|
|
322
519
|
- **`Tool.resultColumns` + `checkColumnTypes` — the column-type contract: a
|
|
323
520
|
tool declares what its rows contain, and the library checks the rows against
|
|
324
|
-
it at the boundary.** Three recorded failures, and they are one shape —
|
|
325
|
-
number became something else, and nothing noticed at the
|
|
521
|
+
it at the boundary.** Three recorded failures, and they are one shape — _a
|
|
522
|
+
number became something else, and nothing noticed at the seam_:
|
|
326
523
|
|
|
327
524
|
1. A mapping report wrote `str(m.get("logical_unit_number") or "")`. **LUN 0
|
|
328
525
|
is falsy**, so LUN 0 was stored as an EMPTY STRING on 2,094 mappings, and
|
|
@@ -351,19 +548,19 @@ first time a tool with an `outputSchema` is validated.
|
|
|
351
548
|
`CostBudget` two-spellings pattern, normalized once).
|
|
352
549
|
|
|
353
550
|
**THE CEILING**, exported as `COLUMN_TYPE_CEILING` and quoted verbatim into
|
|
354
|
-
every finding, the `EMPTY_LOOKUP_CEILING` law:
|
|
551
|
+
every finding, the `EMPTY_LOOKUP_CEILING` law: _"This judges TYPE, never
|
|
355
552
|
MEANING — it can see that a column declared `number` holds a string, and it
|
|
356
553
|
can never see that the string should have been 0, or that a 0.0 should have
|
|
357
554
|
been an 8; a column whose every value has its declared type passes here and
|
|
358
|
-
can still be wrong."
|
|
555
|
+
can still be wrong."_ Failures 1 and 3 are caught. **Failure 2 is not, and
|
|
359
556
|
never will be** — `0.0` is a perfectly good number — and the check says so
|
|
360
557
|
out loud rather than letting a green row imply otherwise.
|
|
361
558
|
|
|
362
559
|
**TWO finding kinds, because the field bug turned on the difference.** New
|
|
363
560
|
`ContextErrorKind`s at the **write seam**: **`column-type-mismatch`** (the
|
|
364
561
|
column is THERE and holds the wrong thing) and **`missing-column`** (the
|
|
365
|
-
declared column is in NONE of the rows).
|
|
366
|
-
be"
|
|
562
|
+
declared column is in NONE of the rows). _"The value is not what it should
|
|
563
|
+
be"_ sends a person to the mapping code; _"the column was never delivered"_
|
|
367
564
|
sends them to the query. A checker that said only "something is off with
|
|
368
565
|
logical_unit_number" would have helped with neither. Each finding names the
|
|
369
566
|
column, the offending value quoted, the rows affected of the rows read, and
|
|
@@ -433,7 +630,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
433
630
|
it came back empty.** A triage agent's reverse-lookup tool filtered a column
|
|
434
631
|
before a pivot, so the column did not exist yet and EVERY reverse lookup
|
|
435
632
|
returned an empty result — for every identifier, always. The tool then
|
|
436
|
-
answered
|
|
633
|
+
answered _successfully_ with an empty list, and the agent reported in a
|
|
437
634
|
table, with confidence, that the device was not logged in to any port on any
|
|
438
635
|
collected switch, advising a check of the physical cabling. It was logged in
|
|
439
636
|
the whole time. Every rail passed, and passed honestly: nothing errored,
|
|
@@ -457,9 +654,9 @@ first time a tool with an `outputSchema` is validated.
|
|
|
457
654
|
for the broken filter and for the honest absence. The bound ships as one
|
|
458
655
|
exported string, `EMPTY_LOOKUP_CEILING`, quoted verbatim into every message
|
|
459
656
|
so it cannot drift out of one doc and leave a reader thinking the library
|
|
460
|
-
knows more than it does:
|
|
657
|
+
knows more than it does: _"An empty result can be perfectly true — the thing
|
|
461
658
|
may exist and simply have nothing to show right now — so this is a place to
|
|
462
|
-
look, never a verdict that anything is wrong."
|
|
659
|
+
look, never a verdict that anything is wrong."_
|
|
463
660
|
|
|
464
661
|
Deliberately NOT `dangling-reference`, whose meaning is the opposite: there
|
|
465
662
|
the ground has left reach; here the ground IS in reach and the lookup found
|
|
@@ -475,7 +672,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
475
672
|
make impossible.
|
|
476
673
|
|
|
477
674
|
**Armed by two halves**, and the second one is why: `noticeEmptyLookups:
|
|
478
|
-
|
|
675
|
+
true` on `Agent.create` **and** at least one tool declaring `argumentsFrom`.
|
|
479
676
|
The declaration alone is not enough — it already arms two other checks, and
|
|
480
677
|
an advisory that armed itself off a declaration made for something else
|
|
481
678
|
would not be opt-in at all. **Default off is byte-identical**: no finding, no
|
|
@@ -546,7 +743,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
546
743
|
pre-rendered table, and the recorded walk that lets a reader CHECK the
|
|
547
744
|
verdict instead of trusting it. `runbookAsTool` is that envelope as one
|
|
548
745
|
declaration bag; the smallest legal call is `{ name, description,
|
|
549
|
-
|
|
746
|
+
procedure }` and it still yields the honest spine.
|
|
550
747
|
|
|
551
748
|
**The mandatory honesty spine**, on every answer whatever the runbook's
|
|
552
749
|
shape: `af_coverage` (three lists + a sentence naming the rule set and
|
|
@@ -582,7 +779,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
582
779
|
tools are invisible — there is no build-time list, the stated 9.72.0
|
|
583
780
|
caveat), so stages compose registered sources instead of importing modules
|
|
584
781
|
and building a second query stack. Inner calls run with `hasArtifacts:
|
|
585
|
-
|
|
782
|
+
false` (one answer, one ticket — never competing chips), a derived
|
|
586
783
|
toolCallId naming the outer call, `needs` resolved on the fail-closed
|
|
587
784
|
non-interactive path, and `checkIn`/`wants` tools refused BY NAME (an inner
|
|
588
785
|
call cannot pause, and must never silently skip a consent gate).
|
|
@@ -606,8 +803,8 @@ first time a tool with an `outputSchema` is validated.
|
|
|
606
803
|
run: four tool results carried real numbers, a compute tool that could sum
|
|
607
804
|
them was registered — with `wants` declared over the staged dataset kind —
|
|
608
805
|
and the app's prompt said to use it. The model summed the numbers in its
|
|
609
|
-
head anyway and stated the total; the evidence gate recorded
|
|
610
|
-
tool result"
|
|
806
|
+
head anyway and stated the total; the evidence gate recorded _"appears in no
|
|
807
|
+
tool result"_ and the answer shipped, because the posture only observed. The
|
|
611
808
|
app patched it with more prose. The library-shaped fix is two mechanisms it
|
|
612
809
|
already owns, on the one dial it already has:
|
|
613
810
|
|
|
@@ -641,9 +838,9 @@ first time a tool with an `outputSchema` is validated.
|
|
|
641
838
|
`'guard'` allows the one bounded revision then delivers with both attempts
|
|
642
839
|
on the record, `'rails'` refuses with `UnsupportedValuesError`. What the
|
|
643
840
|
revision gains: when the flagged turn holds staged refs a served `wants`
|
|
644
|
-
tool can spend, the correction now names them —
|
|
841
|
+
tool can spend, the correction now names them — _"pass 'art\_…'
|
|
645
842
|
(dataset/rows) to `compute` — compute the number there and answer with what
|
|
646
|
-
it returns"
|
|
843
|
+
it returns"_ — inside the authored frame, so the quoted values still come
|
|
647
844
|
last and the exempt-corpus fence is untouched. The `revision-asked`
|
|
648
845
|
`evidence_checked` event carries the same facts additively (`stagedRefs`,
|
|
649
846
|
`spenderTools`). Absent everything — no gate, `nudge` unset, or no
|
|
@@ -671,7 +868,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
671
868
|
|
|
672
869
|
**`foundry()` — the project-endpoint provider** (`agentfootprint/providers`).
|
|
673
870
|
The JS answer to Microsoft's `FoundryChatClient(project_endpoint, model,
|
|
674
|
-
|
|
871
|
+
credential)`: point it at a Foundry project endpoint (or let the hosted
|
|
675
872
|
platform's auto-injected `FOUNDRY_PROJECT_ENDPOINT` supply it), name the
|
|
676
873
|
deployment (`AZURE_AI_MODEL_DEPLOYMENT_NAME ?? MODEL_NAME`), and auth is an
|
|
677
874
|
Entra `TokenCredential`, an api key, or — given neither — the platform's own
|
|
@@ -797,7 +994,6 @@ first time a tool with an `outputSchema` is validated.
|
|
|
797
994
|
tests fail against the previous source (proven by restoring it); no public
|
|
798
995
|
API change; `OllamaUnavailableError` is byte-identical.
|
|
799
996
|
|
|
800
|
-
|
|
801
997
|
## [9.73.0] - 2026-08-27
|
|
802
998
|
|
|
803
999
|
### Fixed
|
|
@@ -834,13 +1030,15 @@ first time a tool with an `outputSchema` is validated.
|
|
|
834
1030
|
|
|
835
1031
|
```ts
|
|
836
1032
|
const agent = Agent.create({
|
|
837
|
-
provider,
|
|
1033
|
+
provider,
|
|
1034
|
+
model,
|
|
838
1035
|
// DECLARED, never ambient — this option is the only door.
|
|
839
|
-
externalGrounds: () =>
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1036
|
+
externalGrounds: () =>
|
|
1037
|
+
viewerSelection.cells.map((cell) => ({
|
|
1038
|
+
value: cell.text, // verified by the app against the artifact
|
|
1039
|
+
source: 'viewer-selection', // the audit label that travels
|
|
1040
|
+
})),
|
|
1041
|
+
});
|
|
844
1042
|
```
|
|
845
1043
|
|
|
846
1044
|
The provider is consulted once per LLM response that contains an armed call,
|
|
@@ -917,25 +1115,25 @@ first time a tool with an `outputSchema` is validated.
|
|
|
917
1115
|
namespaced key (`MCP_TOOL_EXTRAS_KEY`, the string `agentfootprint`), and
|
|
918
1116
|
`mcpClient` / `mockMcpClient` read them back onto the registered `Tool`:
|
|
919
1117
|
|
|
920
|
-
| field
|
|
921
|
-
|
|
922
|
-
| `argumentsFrom` | the dangling-reference and unsupported-argument checks
|
|
923
|
-
| `resultKind`
|
|
924
|
-
| `owner`
|
|
925
|
-
| `resultClass`
|
|
926
|
-
| `resultCeiling` | the author's refusing ceiling on an oversized result
|
|
1118
|
+
| field | what it arms on the consuming side |
|
|
1119
|
+
| --------------- | --------------------------------------------------------------- |
|
|
1120
|
+
| `argumentsFrom` | the dangling-reference and unsupported-argument checks |
|
|
1121
|
+
| `resultKind` | placement's mint — a placed result a `wants` argument can spend |
|
|
1122
|
+
| `owner` | the identity edge subject-joined checks read |
|
|
1123
|
+
| `resultClass` | the per-class `check:semantics` rules |
|
|
1124
|
+
| `resultCeiling` | the author's refusing ceiling on an oversized result |
|
|
927
1125
|
|
|
928
1126
|
```ts
|
|
929
1127
|
const fleet = await mcpClient({ name: 'fleet-mcp', transport });
|
|
930
1128
|
const agent = Agent.create({ provider, model })
|
|
931
|
-
.tools(await fleet.tools())
|
|
1129
|
+
.tools(await fleet.tools()) // backup_status declares argumentsFrom: ['fleet_report']
|
|
932
1130
|
.build();
|
|
933
1131
|
// …and the choice seam now files `unsupported-argument` for it, exactly as
|
|
934
1132
|
// it would for a local defineTool — pinned end to end, disposition row included.
|
|
935
1133
|
```
|
|
936
1134
|
|
|
937
|
-
**The inclusion bar, stated where the list lives:**
|
|
938
|
-
consumer-side check or rail reads; nothing that governs execution
|
|
1135
|
+
**The inclusion bar, stated where the list lives:** _a declaration a
|
|
1136
|
+
consumer-side check or rail reads; nothing that governs execution._ `needs`
|
|
939
1137
|
(credentials), `checkIn` (human consent) and the session hooks are excluded
|
|
940
1138
|
and always will be — they decide how a tool RUNS, and the tool runs on the
|
|
941
1139
|
server. A client holding a consent gate the only executor already held is
|
|
@@ -997,7 +1195,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
997
1195
|
});
|
|
998
1196
|
|
|
999
1197
|
// elsewhere — resolves now, and would have been a kind mismatch before
|
|
1000
|
-
defineTool({ name: 'chart', wants: { dataset: 'dataset/rows' }
|
|
1198
|
+
defineTool({ name: 'chart', wants: { dataset: 'dataset/rows' } /* … */ });
|
|
1001
1199
|
```
|
|
1002
1200
|
|
|
1003
1201
|
- **The matcher is untouched.** Nothing here loosens `wants`; exact match is
|
|
@@ -1066,9 +1264,9 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1066
1264
|
that earns the port its keep —
|
|
1067
1265
|
|
|
1068
1266
|
```ts
|
|
1069
|
-
await session.handControlTo('person');
|
|
1267
|
+
await session.handControlTo('person'); // the automation stream stops
|
|
1070
1268
|
// …they sign in, clear the CAPTCHA, approve the consent screen, watching live
|
|
1071
|
-
await session.handControlTo('agent');
|
|
1269
|
+
await session.handControlTo('agent'); // and the agent carries on
|
|
1072
1270
|
```
|
|
1073
1271
|
|
|
1074
1272
|
Pair it with a check-in and the agent **pauses** rather than guesses: the
|
|
@@ -1086,7 +1284,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1086
1284
|
**What verification changed.** The `InvokeBrowser` action union, read off a
|
|
1087
1285
|
real install of `@aws-sdk/client-bedrock-agentcore` 3.1118.0, is exactly
|
|
1088
1286
|
`mouseClick | mouseMove | mouseDrag | mouseScroll | keyType | keyPress |
|
|
1089
|
-
|
|
1287
|
+
keyShortcut | screenshot` — with **no navigate member at all**. An adapter
|
|
1090
1288
|
written from memory would have invented page verbs for a door that has none.
|
|
1091
1289
|
The same pass fixed `MouseClickArguments` (`{ x, y, button?, clickCount? }`,
|
|
1092
1290
|
buttons `LEFT|MIDDLE|RIGHT`), `KeyPressArguments` (`{ key, presses? }`) and
|
|
@@ -1122,6 +1320,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1122
1320
|
another agent (Strands, LangGraph, Google ADK, a Marketplace listing)
|
|
1123
1321
|
discovers yours through its agent card and calls it. The same split as 9.65.0,
|
|
1124
1322
|
for the third time:
|
|
1323
|
+
|
|
1125
1324
|
- **`a2aWire()`** is the A2A PROTOCOL — JSON-RPC 2.0, `message/send`, text
|
|
1126
1325
|
parts, artifacts, `A2A_PROTOCOL_VERSION` `0.3.0` — an open protocol with no
|
|
1127
1326
|
vendor in it, exported in its own right along with
|
|
@@ -1161,8 +1360,8 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1161
1360
|
- **A capability this library claimed and could not honour.** `httpHost`
|
|
1162
1361
|
declares `['streaming']` by default, so the A2A host inherited it while
|
|
1163
1362
|
`message/send` has nowhere to put a chunk: `requireCapability(host,
|
|
1164
|
-
|
|
1165
|
-
conformance suite caught it — it asserts chunks
|
|
1363
|
+
'streaming')` would have passed for a host that then delivered none. The
|
|
1364
|
+
conformance suite caught it — it asserts chunks _if and only if_ the
|
|
1166
1365
|
capability is declared — and the adapter now declares `[]`.
|
|
1167
1366
|
|
|
1168
1367
|
### Changed
|
|
@@ -1180,6 +1379,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1180
1379
|
looking anything up** (`agentfootprint/providers`). `gatewayTransport` says of
|
|
1181
1380
|
itself that nothing in it is vendor-specific, and that stays true because the
|
|
1182
1381
|
four facts which ARE AgentCore's now live in one file beside it:
|
|
1382
|
+
|
|
1183
1383
|
- **the endpoint** — `agentCoreGatewayUrl({ gatewayId, region })` builds
|
|
1184
1384
|
`https://{gatewayId}.gateway.bedrock-agentcore.{region}.amazonaws.com/mcp`,
|
|
1185
1385
|
a hostname nobody recalls correctly, which is why it is a function and not a
|
|
@@ -1245,6 +1445,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1245
1445
|
install of `@aws-sdk/client-bedrock-agentcore` **3.1118.0** — names, request
|
|
1246
1446
|
shapes and enum values read off the package rather than remembered, which is
|
|
1247
1447
|
the 9.4.0 law:
|
|
1448
|
+
|
|
1248
1449
|
- **`userFlow: 'consent' | 'exchange'`** — `'exchange'` sends
|
|
1249
1450
|
`ON_BEHALF_OF_TOKEN_EXCHANGE`, trading the person's existing login for a
|
|
1250
1451
|
scoped downstream token with no consent screen at any point. Default stays
|
|
@@ -1401,7 +1602,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1401
1602
|
|
|
1402
1603
|
The field story: a tool result ended with an offer — "I can also map these
|
|
1403
1604
|
ids to volume names" — and the person answered "yes please". The model bound
|
|
1404
|
-
|
|
1605
|
+
_that sentence_ as the identifier argument and dispatched. The tool's schema
|
|
1405
1606
|
DECLARED the identifier's shape, in a `pattern` that "yes please" could never
|
|
1406
1607
|
match; the pre-dispatch validator simply did not read the keyword. So the
|
|
1407
1608
|
call went out, failed downstream, and cost a round trip — and the consumer
|
|
@@ -1546,7 +1747,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1546
1747
|
check, so one busy check could carry the total while another check looked
|
|
1547
1748
|
at nothing at all — the per-check rows said so underneath (`⚠ ran 0×`), but
|
|
1548
1749
|
a reader who stops at the headline got the rosier story. The headline now
|
|
1549
|
-
states coverage: either
|
|
1750
|
+
states coverage: either _all_ registered checks ran, or it says COVERAGE IS
|
|
1550
1751
|
PARTIAL and names the checks that checked nothing. Same law as the rows —
|
|
1551
1752
|
a check that never saw a subject is silent about its seam, never a pass.
|
|
1552
1753
|
|
|
@@ -1576,38 +1777,40 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1576
1777
|
- **Six defects in the Context Integrity family, caught by an adversarial
|
|
1577
1778
|
review before release** — each survived two independent attempts to refute
|
|
1578
1779
|
it, and each is now pinned by a red-proved regression test.
|
|
1579
|
-
|
|
1780
|
+
|
|
1781
|
+
- _A claim finding's identity ignored the FIELD_, so a contract naming two
|
|
1580
1782
|
fields of one entity (the shape `.claims()` itself tells you to write)
|
|
1581
1783
|
filed one event and swallowed the rest — while the disposition ledger
|
|
1582
1784
|
counted them all, leaving the two accounts of one run disagreeing.
|
|
1583
1785
|
`ContextError.predicate` now rides the identity, mirroring the
|
|
1584
1786
|
substrate's own `assertionKey`; findings that never set it keep exactly
|
|
1585
1787
|
the identity they had.
|
|
1586
|
-
-
|
|
1587
|
-
|
|
1788
|
+
- _The dangling-reference check was DEAD under `reactMode:
|
|
1789
|
+
'dynamic-grouped'`_ — `compactions` was threaded into the wrong mapper,
|
|
1588
1790
|
so the check saw an empty window ledger every pass and filed a healthy
|
|
1589
1791
|
verdict. Chart-shape parity is now pinned by tests that run the same
|
|
1590
1792
|
trap under both dynamic shapes.
|
|
1591
|
-
-
|
|
1793
|
+
- _The dev canary structurally disabled the wiring-rot theorem._ A minted
|
|
1592
1794
|
canary proves the pure function still works; it says nothing about
|
|
1593
1795
|
whether the pipeline ever calls it (`beginIntegrityRun` mints by calling
|
|
1594
1796
|
the function directly). Masking theorem (i) with it meant the alarm this
|
|
1595
1797
|
ledger exists for could never fire.
|
|
1596
|
-
-
|
|
1798
|
+
- _`workExisted` was hardcoded `true`_ on every exit path, so a run that
|
|
1597
1799
|
died or paused before its first LLM call reported every registered
|
|
1598
1800
|
checker as dead. It is now measured from a signal the integrity code
|
|
1599
1801
|
does not itself write.
|
|
1600
|
-
-
|
|
1802
|
+
- _An answer agreeing with a settled non-reading was filed as an advisory_
|
|
1601
1803
|
— `null` reported for a fact whose settled value is `null` is agreement,
|
|
1602
1804
|
not doubt.
|
|
1603
|
-
-
|
|
1604
|
-
|
|
1805
|
+
- _The claim ledger accumulated for agents that never declared a
|
|
1806
|
+
contract_, and appended by whole-array spread. It is now gated on
|
|
1605
1807
|
`.claims()` and appends without the quadratic copy — restoring the
|
|
1606
1808
|
zero-delta promise for every agent that configured none of this.
|
|
1607
1809
|
|
|
1608
1810
|
- **Two honesty defects in `find_context_errors`**, both caught by the same
|
|
1609
1811
|
adversarial review and both red-proved by a failing test first.
|
|
1610
|
-
|
|
1812
|
+
|
|
1813
|
+
- _The green headline never read the counts it claimed to summarise._
|
|
1611
1814
|
"The checkers below RAN; nothing they cover was violated" was printed on
|
|
1612
1815
|
the mere absence of a finding, so a run whose every encounter was
|
|
1613
1816
|
`unreachable` (the check could not see the evidence), whose checks all
|
|
@@ -1620,7 +1823,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1620
1823
|
all, each keep their own sentence. A checker ROW with zero `checked`
|
|
1621
1824
|
stopped saying "the checker ran and found nothing at this seam" for the
|
|
1622
1825
|
same reason.
|
|
1623
|
-
-
|
|
1826
|
+
- _The tool advertised defect classes no check can file._ The `kind` enum
|
|
1624
1827
|
was pinned to the whole `ContextErrorKind` union, so it offered
|
|
1625
1828
|
`unsupported-argument` and `duplicate-execution` — classes no check in
|
|
1626
1829
|
this build files — and answering one returned a negative verdict about
|
|
@@ -1685,7 +1888,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1685
1888
|
run's disposition rows ride along, so "the checkers ran and found nothing"
|
|
1686
1889
|
and "no checker was registered for that seam" stay different answers, and
|
|
1687
1890
|
a registered check with zero encounters is named as wiring rot. Honest
|
|
1688
|
-
absence has its own sentences: no event tail is
|
|
1891
|
+
absence has its own sentences: no event tail is _no finding evidence_, a
|
|
1689
1892
|
tail with no integrity events says the channel is empty and why, and rows
|
|
1690
1893
|
reporting findings the tail no longer carries say **evidence missing** —
|
|
1691
1894
|
none of them ever reads as "no context errors found". Synthetic canaries
|
|
@@ -1721,7 +1924,7 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1721
1924
|
- **`Tool.argumentsFrom` + the dangling-reference check** (the closure
|
|
1722
1925
|
check's decidable fragment). A tool author can now declare where a tool's
|
|
1723
1926
|
arguments come from — `defineTool({ name: 'screen_fire', argumentsFrom:
|
|
1724
|
-
|
|
1927
|
+
['whats_here'] })` — and `callLLM` checks at request assembly that every
|
|
1725
1928
|
served tool's declared grounds still have results in the window: a ground
|
|
1726
1929
|
the window ledger says was evicted (`droppedObservations`) with nothing
|
|
1727
1930
|
re-established files a `dangling-reference` finding at seam `'compose'`,
|
|
@@ -1765,10 +1968,10 @@ first time a tool with an `outputSchema` is validated.
|
|
|
1765
1968
|
|
|
1766
1969
|
- **The assertion algebra and the one visible finding type.** An
|
|
1767
1970
|
`Assertion` is keyed by `(subject, predicate, epoch)` with two rules
|
|
1768
|
-
that do the work:
|
|
1971
|
+
that do the work: _serving is asserting; history is quotation_ (checks
|
|
1769
1972
|
never fire across the quoted stratum — the whole stale-but-honest
|
|
1770
|
-
false-positive class, closed structurally) and
|
|
1771
|
-
|
|
1973
|
+
false-positive class, closed structurally) and _single-valued by
|
|
1974
|
+
default_ (you declare exemptions, never rules). Unknown `Claim`s never
|
|
1772
1975
|
participate in a comparison. `conflictsOf()` is the pure exclusion
|
|
1773
1976
|
comparison; `ContextError` is the uniform finding — plain kinds
|
|
1774
1977
|
(`invariant-violation`, `unsupported-argument`, `dangling-reference`,
|
|
@@ -1822,16 +2025,16 @@ when this patch was cut, and they are additive.
|
|
|
1822
2025
|
### Fixed
|
|
1823
2026
|
|
|
1824
2027
|
- **The cache report no longer invents a cause for a turn it could not
|
|
1825
|
-
measure.**
|
|
2028
|
+
measure.** _What changed:_ when `cacheRecorder().report()` hands back an
|
|
1826
2029
|
unknown hit rate, the sentence attached to it is now the one the calls
|
|
1827
2030
|
themselves gave — most often "no CacheStrategy was given to
|
|
1828
2031
|
`cacheRecorder()`, so nothing read the usage". When the calls disagree about
|
|
1829
2032
|
why, the summary says they disagreed and lists the reasons (up to three, then
|
|
1830
|
-
a count of the rest) rather than silently choosing one.
|
|
1831
|
-
there
|
|
2033
|
+
a count of the rest) rather than silently choosing one. _Why it was not
|
|
2034
|
+
there:_ the summary sentence was typed into the code as a fixed string, "the
|
|
1832
2035
|
provider reported no cache fields" — true for the case the author had in
|
|
1833
2036
|
mind, a guess for every other, and it overwrote what each call had already
|
|
1834
|
-
stated.
|
|
2037
|
+
stated. _How it improves:_ run without a strategy against a provider that DID
|
|
1835
2038
|
report cache traffic, and 9.59.0 told you your provider was reporting
|
|
1836
2039
|
nothing — sending you off to debug a provider that was working fine, instead
|
|
1837
2040
|
of naming the one line missing from your own setup. You now read the real
|
|
@@ -1854,113 +2057,113 @@ the published bytes and is unchanged.
|
|
|
1854
2057
|
|
|
1855
2058
|
### Fixed
|
|
1856
2059
|
|
|
1857
|
-
- **A red gate could reach npm, and did.**
|
|
2060
|
+
- **A red gate could reach npm, and did.** _What changed:_ the docs-truth
|
|
1858
2061
|
ratchet now runs inside the build job that the publishing job depends on,
|
|
1859
2062
|
so a red gate fails the build and the publish never starts — however the
|
|
1860
|
-
release was created.
|
|
2063
|
+
release was created. _Why it was not there:_ the gate existed, but CI ran it
|
|
1861
2064
|
on `push` while the publish workflow runs on `release: published`, and the
|
|
1862
2065
|
release script fires the release seconds after the push. The two raced, and
|
|
1863
|
-
npm never waited for a verdict.
|
|
2066
|
+
npm never waited for a verdict. _How it improves:_ 9.58.0 shipped with a red
|
|
1864
2067
|
ratchet; that is now structurally impossible.
|
|
1865
|
-
- **A generated report was being edited by hand.**
|
|
2068
|
+
- **A generated report was being edited by hand.** _What changed:_
|
|
1866
2069
|
`npm run docs:truth:report` regenerates the report **without** touching the
|
|
1867
2070
|
accepted-debt baseline, and a test fails if the committed report is not what
|
|
1868
|
-
the generator produces.
|
|
2071
|
+
the generator produces. _Why it was not there:_ the report could only be
|
|
1869
2072
|
regenerated by also re-recording the debt, so an author who merely wanted the
|
|
1870
2073
|
numbers to match reality had to choose between accepting unrelated debt and
|
|
1871
2074
|
editing the file. They edited the file: the 9.58.0 release commit changed
|
|
1872
2075
|
"103 typed events" to "105" and nothing else, leaving the export count stale
|
|
1873
|
-
and the ratchet red.
|
|
2076
|
+
and the ratchet red. _How it improves:_ restating the truth and accepting
|
|
1874
2077
|
debt are now separate acts, and the file cannot silently drift.
|
|
1875
2078
|
- **The cache meter reported 0% for turns that hit cache on every call.**
|
|
1876
|
-
|
|
2079
|
+
_What changed:_ the strategies read the framework's normalised usage instead
|
|
1877
2080
|
of raw provider field names, and every number in the report is now a `Claim`
|
|
1878
|
-
— a value that says how it knows itself.
|
|
2081
|
+
— a value that says how it knows itself. _Why it was not there:_ the
|
|
1879
2082
|
strategies parsed `cache_read_input_tokens` off a value that has never
|
|
1880
2083
|
carried it, so every field read as missing and nothing was recorded; and the
|
|
1881
2084
|
report typed its totals as plain numbers, so "nobody measured" and "measured,
|
|
1882
2085
|
and it was zero" looked identical. The test fixtures were themselves
|
|
1883
2086
|
provider-shaped, which is how it survived a release with a green suite.
|
|
1884
|
-
|
|
2087
|
+
_How it improves:_ a real hit rate, an unmeasured turn that renders as
|
|
1885
2088
|
unmeasured, and a rate that always states its own denominator ("3 of 20
|
|
1886
2089
|
calls"). A **silent non-cache** — a prompt below the model's minimum
|
|
1887
2090
|
cacheable size, which providers process without caching and without an error
|
|
1888
2091
|
— is now visibly different from a turn nobody measured.
|
|
1889
|
-
- **A meter was attached to a provider that cannot feed it.**
|
|
2092
|
+
- **A meter was attached to a provider that cannot feed it.** _What changed:_
|
|
1890
2093
|
the Bedrock strategy declares itself disabled, passes requests through
|
|
1891
|
-
untouched, and answers "not applicable" with the reason.
|
|
1892
|
-
there
|
|
2094
|
+
untouched, and answers "not applicable" with the reason. _Why it was not
|
|
2095
|
+
there:_ it claimed full support and clamped cache markers onto a request
|
|
1893
2096
|
field our Bedrock provider discards, reporting markers that never reached a
|
|
1894
|
-
wire.
|
|
2097
|
+
wire. _How it improves:_ it stays registered, so a Bedrock user is told the
|
|
1895
2098
|
truth by name instead of guessing. The same honesty is applied to OpenAI,
|
|
1896
2099
|
which is the costlier gap because it caches automatically.
|
|
1897
2100
|
|
|
1898
2101
|
### Changed
|
|
1899
2102
|
|
|
1900
|
-
- **A parked map now tells the model it is parked.**
|
|
2103
|
+
- **A parked map now tells the model it is parked.** _What changed:_ while
|
|
1901
2104
|
anything is parked, the model receives a short status card naming the cursor
|
|
1902
2105
|
and the engagement as **separate** things, the reason, and the way back as a
|
|
1903
|
-
concrete call.
|
|
2106
|
+
concrete call. _Why it was not there:_ every honesty signal landed on the
|
|
1904
2107
|
record, which the model never reads — so re-engagement was reachable in
|
|
1905
2108
|
principle and unreachable in practice, because nothing told the model that
|
|
1906
|
-
re-picking a skill it appears to already be in means anything.
|
|
1907
|
-
improves
|
|
2109
|
+
re-picking a skill it appears to already be in means anything. _How it
|
|
2110
|
+
improves:_ a door the model can see. It also learns the distinction the
|
|
1908
2111
|
kernel is built on instead of inferring a contradiction.
|
|
1909
|
-
- **A parked map's tools actually leave the wire.**
|
|
2112
|
+
- **A parked map's tools actually leave the wire.** _What changed:_ parking now
|
|
1910
2113
|
holds the parked map's tool schemas off the request on its own authority.
|
|
1911
|
-
|
|
2114
|
+
_Why it was not there:_ `.maps()` promised parking stops "the prompt fragment
|
|
1912
2115
|
and tools", but on the default posture for flat graphs only the fragment
|
|
1913
2116
|
stopped — the model was shown tools for a skill whose instructions had just
|
|
1914
|
-
vanished.
|
|
2117
|
+
vanished. _How it improves:_ the promise is true on every posture. This is
|
|
1915
2118
|
**not** a change to `scopeTools` and does not touch the 10.0.0 ledger: those
|
|
1916
2119
|
dials answer different questions.
|
|
1917
2120
|
- **A pick of the skill you are already on is now a legal re-engagement.**
|
|
1918
|
-
|
|
1919
|
-
member it re-engages the map and does not move the cursor.
|
|
1920
|
-
there
|
|
2121
|
+
_What changed:_ a `read_skill` pick is routed by intent — of a parked map's
|
|
2122
|
+
member it re-engages the map and does not move the cursor. _Why it was not
|
|
2123
|
+
there:_ the reachability gate refuses a pick of the node the cursor occupies,
|
|
1921
2124
|
which is right for a move and wrong for this; since parking never moves the
|
|
1922
2125
|
cursor, a parked map is parked exactly where the model wants to return. For a
|
|
1923
|
-
single-member map that made parking permanent.
|
|
2126
|
+
single-member map that made parking permanent. _How it improves:_ the
|
|
1924
2127
|
documented recovery door is real.
|
|
1925
|
-
- **An engagement's founding cause is never rewritten.**
|
|
2128
|
+
- **An engagement's founding cause is never rewritten.** _What changed:_ the
|
|
1926
2129
|
record now keeps three separate facts — why the map is participating at all,
|
|
1927
2130
|
why the cursor is on this member, and why this contribution is being served
|
|
1928
|
-
right now.
|
|
2131
|
+
right now. _Why it was not there:_ one field answered all three, so
|
|
1929
2132
|
confirming a guess silently overwrote the guess, and a record founded on a
|
|
1930
|
-
keyword at iteration 1 later read as system-backed since iteration 1.
|
|
1931
|
-
improves
|
|
2133
|
+
keyword at iteration 1 later read as system-backed since iteration 1. _How it
|
|
2134
|
+
improves:_ an incident review can still ask "was this founded on a guess?"
|
|
1932
2135
|
and get the true answer, however much has happened since.
|
|
1933
|
-
- **A declared route no longer forges a user request.**
|
|
2136
|
+
- **A declared route no longer forges a user request.** _What changed:_ when a
|
|
1934
2137
|
declared edge moves the cursor to a different member, that member's
|
|
1935
2138
|
eligibility is worked out from **its own** evidence rather than inherited.
|
|
1936
|
-
|
|
2139
|
+
_Why it was not there:_ the next member inherited the previous one's
|
|
1937
2140
|
standing, and an explicit request never decays — so one explicit pick at the
|
|
1938
2141
|
top of a turn silently warranted every skill the graph walked to afterwards.
|
|
1939
|
-
|
|
2142
|
+
_How it improves:_ a member entered weakly can park, exactly as if it had
|
|
1940
2143
|
been entered that way from the start. The founding cause still says, forever,
|
|
1941
2144
|
that the engagement began explicitly on the skill that was actually asked for.
|
|
1942
|
-
- **A new turn is judged on the new turn's evidence.**
|
|
2145
|
+
- **A new turn is judged on the new turn's evidence.** _What changed:_ a cursor
|
|
1943
2146
|
carried into a new turn with nothing explaining it is recorded as `assumed`
|
|
1944
2147
|
— nobody said why — which is the weakest rung and expires like any guess.
|
|
1945
|
-
|
|
2148
|
+
_Why it was not there:_ an absent explanation was recorded as system-backed,
|
|
1946
2149
|
the strongest and non-decaying category, so turn one's mistaken keyword match
|
|
1947
|
-
became a permanent warrant on turn two and every turn after.
|
|
1948
|
-
improves
|
|
2150
|
+
became a permanent warrant on turn two and every turn after. _How it
|
|
2151
|
+
improves:_ cursor continuity and engagement continuity have independent
|
|
1949
2152
|
lifetimes, and both are now pinned by tests — half the defect was that
|
|
1950
2153
|
nothing pinned either.
|
|
1951
|
-
- **The idle test checks all three of the conditions it documents.**
|
|
1952
|
-
changed
|
|
1953
|
-
none of its tools was called, and the turn went elsewhere.
|
|
1954
|
-
there
|
|
1955
|
-
|
|
2154
|
+
- **The idle test checks all three of the conditions it documents.** _What
|
|
2155
|
+
changed:_ a map only accrues idle when its contribution was actually served,
|
|
2156
|
+
none of its tools was called, and the turn went elsewhere. _Why it was not
|
|
2157
|
+
there:_ only the last was checked, while the refusal text asserted all three.
|
|
2158
|
+
_How it improves:_ nothing claims a fact it did not check. (Making it honest
|
|
1956
2159
|
needed the kernel to carry its own record of what was served — reading it
|
|
1957
2160
|
from the existing delta machinery was silently empty in the grouped chart
|
|
1958
2161
|
shape, which would have disabled parking there entirely.)
|
|
1959
|
-
- **A map that cannot explain its cursor moves is refused at mount.**
|
|
1960
|
-
changed
|
|
1961
|
-
was not there
|
|
1962
|
-
whole job is weighing evidence had none to weigh — silently.
|
|
1963
|
-
improves
|
|
2162
|
+
- **A map that cannot explain its cursor moves is refused at mount.** _What
|
|
2163
|
+
changed:_ `.maps()` requires a map that reports why the cursor moved. _Why it
|
|
2164
|
+
was not there:_ without it, no explanation ever arrives, and a kernel whose
|
|
2165
|
+
whole job is weighing evidence had none to weigh — silently. _How it
|
|
2166
|
+
improves:_ the failure is named at build time instead of at 3am.
|
|
1964
2167
|
|
|
1965
2168
|
### Added
|
|
1966
2169
|
|
|
@@ -1970,9 +2173,9 @@ the published bytes and is unchanged.
|
|
|
1970
2173
|
shipped and was unreachable: the kernel's data model carried the field and
|
|
1971
2174
|
no option ever set it. It suppresses the park, not the measurement — the
|
|
1972
2175
|
record still shows a map riding every call unused.
|
|
1973
|
-
- **Two documentation pages**:
|
|
1974
|
-
ladder, the idle test, the three facts, turns, and the park card) and
|
|
1975
|
-
cache
|
|
2176
|
+
- **Two documentation pages**: _Mounted maps_ (the kernel's why, the evidence
|
|
2177
|
+
ladder, the idle test, the three facts, turns, and the park card) and _The
|
|
2178
|
+
cache meter_ (how to read a report, and exactly which providers can feed it).
|
|
1976
2179
|
- **A measurement, not a fix, for the prompt-cache cost of `read_skill`.** The
|
|
1977
2180
|
tool's description is rebuilt from the cursor every iteration, and changing a
|
|
1978
2181
|
tool definition invalidates a provider's entire prompt cache. A shipped test
|
|
@@ -1990,7 +2193,6 @@ through `isKnown(...)`, or render them with `describeClaim(...)`. This is a
|
|
|
1990
2193
|
signature change in a minor release, and deliberately so: the old bare numbers
|
|
1991
2194
|
were not a contract worth keeping, because the value was zero on every turn.
|
|
1992
2195
|
|
|
1993
|
-
|
|
1994
2196
|
## [9.58.0] - 2026-08-20
|
|
1995
2197
|
|
|
1996
2198
|
**A map's cursor says where it stands. Nothing said whether it had earned
|
|
@@ -2086,7 +2288,7 @@ to do, and no longer had the evidence to do it.
|
|
|
2086
2288
|
What it did next was invent. It took an entity name it remembered plus the
|
|
2087
2289
|
shape of an id it had used earlier, assembled one that has never existed, and
|
|
2088
2290
|
was refused — a wasted action out of a small budget. In one archived run the
|
|
2089
|
-
final answer to the
|
|
2291
|
+
final answer to the _person_ named a host that appears in no tool result at
|
|
2090
2292
|
all.
|
|
2091
2293
|
|
|
2092
2294
|
Nothing in the conversation said the evidence had gone. That is the release.
|
|
@@ -2109,7 +2311,7 @@ Nothing in the conversation said the evidence had gone. That is the release.
|
|
|
2109
2311
|
already inside `keepRecentTurns` costs nothing at all. Nothing at or before
|
|
2110
2312
|
the current request is pinnable, so a new user turn releases the whole
|
|
2111
2313
|
previous loop. The floor is `1 request + keepLastToolResults pins +
|
|
2112
|
-
|
|
2314
|
+
keepRecentTurns turns`, whatever your tool count, iteration count or run
|
|
2113
2315
|
length.
|
|
2114
2316
|
|
|
2115
2317
|
And a pin that BLOCKS is worse than a pin that misses: when two consecutive
|
|
@@ -2120,9 +2322,9 @@ Nothing in the conversation said the evidence had gone. That is the release.
|
|
|
2120
2322
|
`keepLastToolResults: false` (or `0`) reproduces 9.56.0 byte for byte.
|
|
2121
2323
|
|
|
2122
2324
|
- **A drop now says whose results it took.** The authored notice gains one
|
|
2123
|
-
sentence:
|
|
2325
|
+
sentence: _"Tool results are among them (whats_here, pan_view) — call the
|
|
2124
2326
|
tool again if you need its output; do not reconstruct ids or values from
|
|
2125
|
-
memory."
|
|
2327
|
+
memory."_ The drop is now STATED rather than silent — whether that sentence
|
|
2126
2328
|
changes what a model does next is **not measured here**: the five archived
|
|
2127
2329
|
runs have not been re-run with it on, so it ships as an honesty fix and not
|
|
2128
2330
|
as a performance claim. Tool names are the only caller data that reaches it, and they
|
|
@@ -2148,13 +2350,13 @@ Nothing in the conversation said the evidence had gone. That is the release.
|
|
|
2148
2350
|
> You are on action 25 of 30; 5 remain. Finish what you have rather than
|
|
2149
2351
|
> start something new.
|
|
2150
2352
|
|
|
2151
|
-
Measured, not decorative: given its remaining budget a model wrote
|
|
2152
|
-
steps left, enough to finish this properly"
|
|
2353
|
+
Measured, not decorative: given its remaining budget a model wrote _"I have 5
|
|
2354
|
+
steps left, enough to finish this properly"_ and landed the task, where
|
|
2153
2355
|
before it spiralled and produced no answer at all.
|
|
2154
2356
|
|
|
2155
2357
|
The vocabulary is closed rather than a `(ctx) => string` because of
|
|
2156
2358
|
**absence**. Given a function, an author writes `${ctx.maxIterations}` and
|
|
2157
|
-
ships
|
|
2359
|
+
ships _"23 of undefined"_, or writes `?? 0` and ships a fabricated
|
|
2158
2360
|
denominator that nothing — and no model — can tell from a real zero. With
|
|
2159
2361
|
named slots the library owns absence and applies one rule: if any named fact
|
|
2160
2362
|
is unavailable, the whole instruction is skipped, by name, as
|
|
@@ -2178,7 +2380,7 @@ Nothing in the conversation said the evidence had gone. That is the release.
|
|
|
2178
2380
|
message that would become the head was already a user turn (the pinned
|
|
2179
2381
|
request, or an older turn of a restored conversation) no notice was owed at
|
|
2180
2382
|
all. Its 245–358 characters were nevertheless allowed to veto a legitimate
|
|
2181
|
-
drop; and because the removable span is the longest
|
|
2383
|
+
drop; and because the removable span is the longest _contiguous_ run, the
|
|
2182
2384
|
same verdict came back at every boundary while the window grew without
|
|
2183
2385
|
bound.
|
|
2184
2386
|
|
|
@@ -2383,11 +2585,11 @@ they do.
|
|
|
2383
2585
|
`unknown`, so a surface that guessed at it would put words in a tool's
|
|
2384
2586
|
mouth. One rule, and it is the whole rule:
|
|
2385
2587
|
|
|
2386
|
-
| your payload
|
|
2387
|
-
|
|
|
2388
|
-
| `{ message: 'Hop 3 of 12' }` | `Hop 3 of 12` — your sentence, verbatim
|
|
2389
|
-
| `{ done: 3, total: 12 }`
|
|
2390
|
-
| `'a bare string'`
|
|
2588
|
+
| your payload | the line a person reads |
|
|
2589
|
+
| ---------------------------- | ------------------------------------------------ |
|
|
2590
|
+
| `{ message: 'Hop 3 of 12' }` | `Hop 3 of 12` — your sentence, verbatim |
|
|
2591
|
+
| `{ done: 3, total: 12 }` | `` `walk_graph` reported progress (3 so far)… `` |
|
|
2592
|
+
| `'a bare string'` | the same generic line |
|
|
2391
2593
|
|
|
2392
2594
|
A top-level string field named **`message`** is shown verbatim, trimmed, and
|
|
2393
2595
|
cut at **120 characters** with the cut stated (`… (+N more)`) — `message` is
|
|
@@ -2413,8 +2615,8 @@ they do.
|
|
|
2413
2615
|
`tool_end` used to be able to clear a SIBLING call's status, leaving the
|
|
2414
2616
|
bubble blank while a tool was still working.
|
|
2415
2617
|
|
|
2416
|
-
- **Commentary narrates the middle**, so recordings replay it:
|
|
2417
|
-
`walk_graph` tool reported progress while it was still running."
|
|
2618
|
+
- **Commentary narrates the middle**, so recordings replay it: _"The
|
|
2619
|
+
`walk_graph` tool reported progress while it was still running."_ The
|
|
2418
2620
|
teaching voice states the fact and never the payload — the same split the
|
|
2419
2621
|
Lens teaching view keeps, in the same words.
|
|
2420
2622
|
|
|
@@ -2468,8 +2670,10 @@ tool's name on it.
|
|
|
2468
2670
|
series: [{ t: '2026-08-19T10:00:00Z', entity: 'fc1/3', metric: 'avg_iops', value: 18450 }],
|
|
2469
2671
|
grain: { interval: '30m', aggregation: 'avg', is_counter: false },
|
|
2470
2672
|
provenance: { measured_at: '2026-08-19T10:20:00Z', source: 'InfluxDB SwitchPortStats' },
|
|
2471
|
-
coverage: {
|
|
2472
|
-
|
|
2673
|
+
coverage: {
|
|
2674
|
+
checked: ['fabric A: all 48 ports'],
|
|
2675
|
+
notChecked: [{ what: 'the peer fabric', why: 'collector scoped to one fabric' }],
|
|
2676
|
+
},
|
|
2473
2677
|
render: { default: 'table', columns: ['entity', 'value'], sort: 'value desc' },
|
|
2474
2678
|
});
|
|
2475
2679
|
```
|
|
@@ -2521,7 +2725,7 @@ tool's name on it.
|
|
|
2521
2725
|
- Zero-cost when unused: recognition is strict (`af_semantics: true` AND the
|
|
2522
2726
|
whole rule set), so every value any tool has ever returned keeps its bytes.
|
|
2523
2727
|
Runnable example: `examples/features/66-semantic-envelope.ts`. Guide:
|
|
2524
|
-
docs
|
|
2728
|
+
docs _Build → Semantic tool results_.
|
|
2525
2729
|
|
|
2526
2730
|
## [9.52.0] - 2026-08-19
|
|
2527
2731
|
|
|
@@ -2601,7 +2805,7 @@ opaque function on a route edge.
|
|
|
2601
2805
|
|
|
2602
2806
|
- **`guard:` on route edges — guards as data (the `when` predicate's declared
|
|
2603
2807
|
twin).** `.route(a, b, { guard: { riskLevel: { in: ['high','critical'] },
|
|
2604
|
-
|
|
2808
|
+
score: { gte: 0.7 } } })` — conditions over the hop (`toolName`, `result`,
|
|
2605
2809
|
`status`, `iteration`, `userMessage`, `currentSkillId`) and over the tool
|
|
2606
2810
|
result's own top-level JSON fields, operators `eq/ne/gt/gte/lt/lte/in/notIn`
|
|
2607
2811
|
(deliberately footprintjs's `WhereFilter` grammar, mirrored door-locally —
|
|
@@ -2611,6 +2815,7 @@ opaque function on a route edge.
|
|
|
2611
2815
|
outcome, AND these conditions"). ONE compilation produces the predicate
|
|
2612
2816
|
that routes, the serializable `SkillGuardData`, and the evidence evaluator
|
|
2613
2817
|
— so the three can never describe different guards. What being data buys:
|
|
2818
|
+
|
|
2614
2819
|
- **the check-up proves contradictions** — new ERROR
|
|
2615
2820
|
`guard-unsatisfiable`: crossed bounds (`gt: 5, lt: 3`), `eq` a same-key
|
|
2616
2821
|
`ne`/`in`/`notIn` excludes, a `status` outside the closed result-status
|
|
@@ -2631,7 +2836,7 @@ opaque function on a route edge.
|
|
|
2631
2836
|
evaluation: `guard` on the taken hop (verdict `true`) and `guardsClosed`
|
|
2632
2837
|
for refusals (verdict `false`, at most one per edge per iteration, on
|
|
2633
2838
|
whatever move resulted — a stay says `score gte 0.7 — saw "0.2" →
|
|
2634
|
-
|
|
2839
|
+
failed`). Agents without guards keep byte-identical events.
|
|
2635
2840
|
|
|
2636
2841
|
- **SkillMap & SkillWalker are now the official names.** You declare the
|
|
2637
2842
|
**SkillMap**; the agent is the **SkillWalker**; the recording carries both.
|
|
@@ -2714,7 +2919,7 @@ is the whole release.
|
|
|
2714
2919
|
### Notes for recording consumers (the lens, triage platforms)
|
|
2715
2920
|
|
|
2716
2921
|
- Draw the declared topology from `skill.graph_declared` (`declaredSource:
|
|
2717
|
-
|
|
2922
|
+
'recording'` can now mean COMPLETE); filter `from !== null` for
|
|
2718
2923
|
node-to-node edges, exactly as with a built graph's `edges`.
|
|
2719
2924
|
- Fill per-beat reachability from `cursorMove.reachable` first; the refusal's
|
|
2720
2925
|
`allowed` and declared-edge folds remain as fallbacks for older eras.
|
|
@@ -2773,7 +2978,7 @@ is the whole release.
|
|
|
2773
2978
|
`recording.json`, the manifest carries a note naming the fact and the line
|
|
2774
2979
|
that supplies it, and `BugReportUnit.enveloped` says per conversation which
|
|
2775
2980
|
shape it got. Nothing is stamped that was not known.
|
|
2776
|
-
- **The evidence is never packed twice** — an envelope
|
|
2981
|
+
- **The evidence is never packed twice** — an envelope _or_ a bare recording,
|
|
2777
2982
|
never both. The zip is store-only, so a duplicated recording is duplicated
|
|
2778
2983
|
bytes against the size ceiling the trim hints exist to keep a reporter under.
|
|
2779
2984
|
- The GitHub issue body names the file that is really in the bundle, including
|
|
@@ -3324,10 +3529,9 @@ answer for. These are ours, built from that evidence. The credit is the field's.
|
|
|
3324
3529
|
### Fixed
|
|
3325
3530
|
|
|
3326
3531
|
- Two unanchored `coverage` rules in `.gitignore` matched `src/core/agent/
|
|
3327
|
-
|
|
3532
|
+
coverage/`, so a new source directory would have been absent from every clone.
|
|
3328
3533
|
Anchored to the vitest report directory, with a comment naming why.
|
|
3329
3534
|
|
|
3330
|
-
|
|
3331
3535
|
## [9.42.0] - 2026-08-15
|
|
3332
3536
|
|
|
3333
3537
|
**Comparing strategies with statistics, and retention on the session port.**
|
|
@@ -3413,7 +3617,6 @@ answer for. These are ours, built from that evidence. The credit is the field's.
|
|
|
3413
3617
|
from a counter, so they differ in their prefix and the fold never shows.
|
|
3414
3618
|
Fixing it re-keys stored sessions. Next release.
|
|
3415
3619
|
|
|
3416
|
-
|
|
3417
3620
|
## [9.41.0] - 2026-08-15
|
|
3418
3621
|
|
|
3419
3622
|
**Comparing strategies, and proving a store.** An audit asked whether this
|
|
@@ -3483,7 +3686,6 @@ key, and the proof obligations for the second port.
|
|
|
3483
3686
|
- `MemoryDefinition` gains optional declared `strategy`, `retrieval` and
|
|
3484
3687
|
`embedderId` — additive, and what makes a memory row worth grouping on.
|
|
3485
3688
|
|
|
3486
|
-
|
|
3487
3689
|
## [9.40.0] - 2026-08-15
|
|
3488
3690
|
|
|
3489
3691
|
**Two identity bugs, one of which had been paying out zeros since v2.8.**
|
|
@@ -3534,7 +3736,6 @@ key, and the proof obligations for the second port.
|
|
|
3534
3736
|
`provider` now flows through to `CostTick` as well (optional; absent stays
|
|
3535
3737
|
absent rather than becoming `'unknown'`).
|
|
3536
3738
|
|
|
3537
|
-
|
|
3538
3739
|
## [9.39.0] - 2026-08-14
|
|
3539
3740
|
|
|
3540
3741
|
**Three false doors closed, and two promises the recording did not keep.** An
|
|
@@ -3573,7 +3774,7 @@ forking. Mostly yes — but the first thing such an author reads was wrong.
|
|
|
3573
3774
|
was reserved. Not re-exported from any barrel, not an `exports` subpath, not
|
|
3574
3775
|
in the shipped-surface baseline — so nothing public could reach it and its
|
|
3575
3776
|
removal is not breaking. The by-instance door (`agent.enable.observability({
|
|
3576
|
-
|
|
3777
|
+
strategy })`) is real, first-class, and now the only one. The cache twin
|
|
3577
3778
|
legitimately has two doors; this one had one door and a sign pointing at a
|
|
3578
3779
|
wall.
|
|
3579
3780
|
|
|
@@ -3597,7 +3798,6 @@ forking. Mostly yes — but the first thing such an author reads was wrong.
|
|
|
3597
3798
|
`enable.cost()` receives zeros. This release makes `model` resolve there; the
|
|
3598
3799
|
numbers are still zero. Its own ticket.
|
|
3599
3800
|
|
|
3600
|
-
|
|
3601
3801
|
## [9.38.0] - 2026-08-14
|
|
3602
3802
|
|
|
3603
3803
|
**Two behaviour changes that are bug fixes — a call that used to run now
|
|
@@ -3965,12 +4165,13 @@ labels." Ours had no rule at all. Now it does, and it is enforced, not
|
|
|
3965
4165
|
promised.
|
|
3966
4166
|
|
|
3967
4167
|
### What it is, and — because the name invites the wrong reading — what it
|
|
4168
|
+
|
|
3968
4169
|
### is provably not
|
|
3969
4170
|
|
|
3970
4171
|
It is a **fabrication detector, not a correctness judge.** If a value in
|
|
3971
4172
|
the answer never appeared in anything a tool returned this turn, the model
|
|
3972
4173
|
typed it rather than read it, and that is all this checks. It cannot catch
|
|
3973
|
-
a false claim built entirely from real values —
|
|
4174
|
+
a false claim built entirely from real values — _"fc1/3 is healthy"_ when
|
|
3974
4175
|
the data says the port is down uses two grounded tokens and sails through.
|
|
3975
4176
|
It cannot catch a real value attached to the wrong thing, a fabricated
|
|
3976
4177
|
quantity under the digit threshold, or a fabricated name spelled in
|
|
@@ -4025,7 +4226,7 @@ is not judged at all, and a turn the iteration or cost limit just cut
|
|
|
4025
4226
|
short does not get to spend a revision it can't afford. It composes with,
|
|
4026
4227
|
rather than collides with, `.reliability()`: reliability governs whether a
|
|
4027
4228
|
call is retried before anything is committed; this governs an answer
|
|
4028
|
-
|
|
4229
|
+
_after_ it has already been committed to the transcript.
|
|
4029
4230
|
|
|
4030
4231
|
### Measured, not assumed
|
|
4031
4232
|
|
|
@@ -4104,7 +4305,7 @@ never touched `footprintjs` itself but imports the file that now did.
|
|
|
4104
4305
|
- `isDevMode` (a `footprintjs` import) → a bound `devWarn()` / `devMode()`
|
|
4105
4306
|
reader that the host supplies; every existing warning reads verbatim, and
|
|
4106
4307
|
the existing `enableDevMode()` tests pass unchanged — the proof that
|
|
4107
|
-
nothing about
|
|
4308
|
+
nothing about _what gets warned_ moved, only _how it's asked_.
|
|
4108
4309
|
- `ToolResultStatus` → pulled out to a zero-import leaf and re-exported from
|
|
4109
4310
|
its old home, so the envelope grammar is unchanged for every existing
|
|
4110
4311
|
caller. The fence caught four more inline crossings of this type that
|
|
@@ -4267,7 +4468,7 @@ was in the bundle — an empty bundle read as "nobody was turned away" when
|
|
|
4267
4468
|
it only meant "nobody ran." `standingAgent({ onIngressDecision })` now
|
|
4268
4469
|
hands your sink one `IngressRecord` per request, filed at the terminal the
|
|
4269
4470
|
reply actually reached. The honest contract is stated in the type itself:
|
|
4270
|
-
`'served'` means **delivered**, not
|
|
4471
|
+
`'served'` means **delivered**, not _admitted_ — a request the door let
|
|
4271
4472
|
through whose run, store or provider then broke files as `'failed'`, and
|
|
4272
4473
|
the record carries the admission verdict (`allow` / `queue` / `refuse`)
|
|
4273
4474
|
either way. It is a stream you chain into your own sink, not a join onto
|
|
@@ -4399,7 +4600,7 @@ read/write." Both, plus `decayPolicy`, are now carried under prefixed
|
|
|
4399
4600
|
metadata keys and restored verbatim on read. A caller's own value under one
|
|
4400
4601
|
of the three keys this adapter generates (`source`, `resourceName`,
|
|
4401
4602
|
`distance`) is refused by name rather than silently shadowed — recognized by
|
|
4402
|
-
|
|
4603
|
+
_identity_, not shape, so a caller's own `distance: 12` cannot be mistaken
|
|
4403
4604
|
for this adapter's. An oversized carried field is refused rather than
|
|
4404
4605
|
truncated: provenance that came back shortened would be provenance nobody
|
|
4405
4606
|
could tell was shortened.
|
|
@@ -4589,7 +4790,7 @@ effects. Every line follows the same rules as the rest of the layer:
|
|
|
4589
4790
|
retry happened — no event attests one, so the words don't either.
|
|
4590
4791
|
- **Sizes humanized, and the two units told apart.** `humanizeBytes` /
|
|
4591
4792
|
`humanizeChars` — `41.0 KB` where the ceiling counts bytes, `240,000
|
|
4592
|
-
|
|
4793
|
+
characters` where it counts characters (`tools.result_refused` counts
|
|
4593
4794
|
characters, because that's what the limit does).
|
|
4594
4795
|
- **Refs and digests stay out of prose.** They identify a row for the details
|
|
4595
4796
|
panel; a reader doesn't read them. The repeated-call nudge's fingerprints
|
|
@@ -4643,8 +4844,8 @@ import { googleIdentity } from 'agentfootprint/security';
|
|
|
4643
4844
|
now declares the same `ranksBy: 'server-text'` for the identical reason, so
|
|
4644
4845
|
the two server-ranked stores no longer disagree on how they say what they
|
|
4645
4846
|
are.)
|
|
4646
|
-
- **`googleIdentity`** — a narrow `CredentialProvider`: it vends
|
|
4647
|
-
access tokens for
|
|
4847
|
+
- **`googleIdentity`** — a narrow `CredentialProvider`: it vends _Google_
|
|
4848
|
+
access tokens for _Google_ APIs from whatever credential the environment
|
|
4648
4849
|
already has (ADC, workload identity, an impersonated service account).
|
|
4649
4850
|
`mode: 'user'` is refused by name rather than quietly served a machine
|
|
4650
4851
|
token, since Google's user-token equivalent has no Node surface yet.
|
|
@@ -4719,7 +4920,9 @@ provider scopes a vault on:
|
|
|
4719
4920
|
|
|
4720
4921
|
```ts
|
|
4721
4922
|
await standingAgent({
|
|
4722
|
-
agent,
|
|
4923
|
+
agent,
|
|
4924
|
+
sessions,
|
|
4925
|
+
host: nodeHost({ port: 8080 }),
|
|
4723
4926
|
identity: {
|
|
4724
4927
|
verify: jwksIdentity({
|
|
4725
4928
|
jwksUrl: 'https://idp.example.com/.well-known/jwks.json',
|
|
@@ -4732,7 +4935,7 @@ await standingAgent({
|
|
|
4732
4935
|
|
|
4733
4936
|
**Configured is closed-by-default.** A request with no `Authorization`
|
|
4734
4937
|
header is refused (401) unless `allowAnonymous: true` is set, and a request
|
|
4735
|
-
that
|
|
4938
|
+
that _names_ a `userId` without proving it is refused either way — a door
|
|
4736
4939
|
that verifies a token when offered and waves the request through when it is
|
|
4737
4940
|
not is a door anybody opens by sending less. `jwksIdentity` is the one
|
|
4738
4941
|
adapter this release ships (`jose`, loaded lazily, pinned against a real
|
|
@@ -4751,7 +4954,9 @@ Nothing else reads them; they do not enter the run's own identity tuple.
|
|
|
4751
4954
|
|
|
4752
4955
|
```ts
|
|
4753
4956
|
await standingAgent({
|
|
4754
|
-
agent,
|
|
4957
|
+
agent,
|
|
4958
|
+
sessions,
|
|
4959
|
+
host,
|
|
4755
4960
|
identity: { verify },
|
|
4756
4961
|
admission: turnsPerHour({ limit: 60 }),
|
|
4757
4962
|
});
|
|
@@ -4936,8 +5141,12 @@ wire, scoped to the session that asks.**
|
|
|
4936
5141
|
### Added — two wire operations on the existing invoke path
|
|
4937
5142
|
|
|
4938
5143
|
```ts
|
|
4939
|
-
{
|
|
4940
|
-
|
|
5144
|
+
{
|
|
5145
|
+
op: 'artifact-head', ref;
|
|
5146
|
+
} // → meta
|
|
5147
|
+
{
|
|
5148
|
+
op: 'artifact-get', ref;
|
|
5149
|
+
} // → meta + data
|
|
4941
5150
|
```
|
|
4942
5151
|
|
|
4943
5152
|
Resolved under the requesting session's identity-composed scope — exactly
|
|
@@ -4988,7 +5197,9 @@ lines.**
|
|
|
4988
5197
|
defineTool({
|
|
4989
5198
|
name: 'summarize',
|
|
4990
5199
|
wants: { dataset: 'dataset/rows' },
|
|
4991
|
-
execute: async (args, ctx) => {
|
|
5200
|
+
execute: async (args, ctx) => {
|
|
5201
|
+
/* args.dataset is the RESOLVED DATA */
|
|
5202
|
+
},
|
|
4992
5203
|
});
|
|
4993
5204
|
```
|
|
4994
5205
|
|
|
@@ -5176,7 +5387,7 @@ on `llm_start`.
|
|
|
5176
5387
|
Precedence is stated and enforced, most specific wins: **escalation
|
|
5177
5388
|
brain > per-skill brain > `.configure()`'s run model > the build
|
|
5178
5389
|
default.** A brain naming only a model inherits the agent's own provider;
|
|
5179
|
-
a brain naming a
|
|
5390
|
+
a brain naming a _foreign_ provider without a model is refused at
|
|
5180
5391
|
`Agent.build()` — the run's configured model belongs to another vendor's
|
|
5181
5392
|
namespace and would fail mid-turn, on exactly the iteration the cursor
|
|
5182
5393
|
enters the skill.
|
|
@@ -5213,12 +5424,12 @@ returns today — stay byte-identical; the envelope is recognized only by
|
|
|
5213
5424
|
its own strict shape.
|
|
5214
5425
|
|
|
5215
5426
|
- **`propose-transition`** — `{ kind: 'propose-transition', targetSkillId,
|
|
5216
|
-
|
|
5217
|
-
|
|
5427
|
+
reason }`. The typed replacement for a string routing marker: the
|
|
5428
|
+
_graph_ decides. A same-batch declared edge still wins; an unreachable
|
|
5218
5429
|
target is refused out loud, not silently dropped.
|
|
5219
5430
|
- **`require-instruction`** — `{ kind: 'require-instruction',
|
|
5220
|
-
|
|
5221
|
-
Pushes a
|
|
5431
|
+
instructionId, deliveryLease: 'next-call' | 'until-skill-exit' }`.
|
|
5432
|
+
Pushes a _registered_ instruction into the coming call(s) —
|
|
5222
5433
|
`read_skill` stays the pull door for optional knowledge; this is the
|
|
5223
5434
|
push door for mandatory procedure, and it only pushes what was
|
|
5224
5435
|
registered at build. An unknown id is refused, never improvised.
|
|
@@ -5402,7 +5613,7 @@ unmatched, never as an uncontested winner.
|
|
|
5402
5613
|
### Zero-cost when unused
|
|
5403
5614
|
|
|
5404
5615
|
Graphs that use none of the new options are byte-identical in behavior
|
|
5405
|
-
|
|
5616
|
+
_and_ events to 9.16.0 — pinned by regression tests. 78+ new tests cover
|
|
5406
5617
|
the cascade, the scorers, the strictness postures, and continuity.
|
|
5407
5618
|
|
|
5408
5619
|
### Deferred
|
|
@@ -5437,6 +5648,7 @@ ordering bug with no error, no log line, just a silently different cursor.
|
|
|
5437
5648
|
|
|
5438
5649
|
On the record, so a trace answers "why didn't the second call route?"
|
|
5439
5650
|
instead of leaving the reader to guess.
|
|
5651
|
+
|
|
5440
5652
|
- Single-tool iterations are byte-identical to 9.15.0 — this only changes
|
|
5441
5653
|
behavior when a batch actually contains more than one result.
|
|
5442
5654
|
- New `AgentState.toolResults` / `InjectionContext.toolResults`: the full
|
|
@@ -5451,7 +5663,7 @@ ordering bug with no error, no log line, just a silently different cursor.
|
|
|
5451
5663
|
Wiring a `.skillGraph(...)` onto a classic agent meant the graph would still
|
|
5452
5664
|
route and the trace would still show an activation — but the model never saw
|
|
5453
5665
|
the newly-active skill's prompt or tools, because the slot it would have
|
|
5454
|
-
changed was already frozen. The configuration
|
|
5666
|
+
changed was already frozen. The configuration _looked_ like it worked and
|
|
5455
5667
|
didn't.
|
|
5456
5668
|
|
|
5457
5669
|
`Agent.build()` now refuses this combination outright, naming both the
|
|
@@ -5505,7 +5717,7 @@ const graph = skillGraph({
|
|
|
5505
5717
|
|
|
5506
5718
|
What previously required `autoActivate: 'currentSkill'` typed on every single
|
|
5507
5719
|
skill is now one graph-level line. A skill's own explicit `autoActivate` always
|
|
5508
|
-
wins — the graph sets a default, never an override. Only
|
|
5720
|
+
wins — the graph sets a default, never an override. Only _wired_ skills (named
|
|
5509
5721
|
by an entry or a route) are stamped: an unwired skill's tools would otherwise
|
|
5510
5722
|
never appear at all. With the dial absent or `false`, compiled skills are
|
|
5511
5723
|
byte-identical to 9.14.0 (pinned by test). On a `tree()` graph the flat-arm
|
|
@@ -5586,10 +5798,10 @@ const memory = defineMemory({
|
|
|
5586
5798
|
type: MEMORY_TYPES.EPISODIC,
|
|
5587
5799
|
strategy: {
|
|
5588
5800
|
kind: MEMORY_STRATEGIES.SUMMARIZE,
|
|
5589
|
-
recent: 6,
|
|
5590
|
-
size: 20,
|
|
5591
|
-
llm: anthropic(),
|
|
5592
|
-
model: 'claude-haiku-4-5',
|
|
5801
|
+
recent: 6, // the 6 newest entries stay verbatim
|
|
5802
|
+
size: 20, // how much history to load per turn
|
|
5803
|
+
llm: anthropic(), // its OWN instance, not the agent's
|
|
5804
|
+
model: 'claude-haiku-4-5', // named explicitly — no fallback
|
|
5593
5805
|
},
|
|
5594
5806
|
store,
|
|
5595
5807
|
});
|
|
@@ -5619,8 +5831,8 @@ quietly bills your MAIN model for compression, and a different vendor is sent a
|
|
|
5619
5831
|
model id it has never heard of, mid-conversation, on a paid run.
|
|
5620
5832
|
|
|
5621
5833
|
`Agent.memory()` now also refuses a summarizer that is the agent's own provider
|
|
5622
|
-
**instance** at the agent's own model (the narrow 8.14.0 rule; a
|
|
5623
|
-
|
|
5834
|
+
**instance** at the agent's own model (the narrow 8.14.0 rule; a _second
|
|
5835
|
+
instance_ of the same vendor at the same model is allowed and sometimes right).
|
|
5624
5836
|
`defineMemory` cannot make that check — it has never heard of an agent — so a
|
|
5625
5837
|
`MemoryDefinition` now declares `billing: { provider, model }` and the builder
|
|
5626
5838
|
reads it, the same field and shape `WindowStrategy.billing` already used. One
|
|
@@ -5733,7 +5945,7 @@ why, with a date.
|
|
|
5733
5945
|
import { gemini } from 'agentfootprint/providers';
|
|
5734
5946
|
|
|
5735
5947
|
const vertex = gemini({ project: 'my-project', location: 'us-central1' }); // ADC
|
|
5736
|
-
const studio = gemini({ apiKey: process.env.GEMINI_API_KEY! });
|
|
5948
|
+
const studio = gemini({ apiKey: process.env.GEMINI_API_KEY! }); // one key
|
|
5737
5949
|
```
|
|
5738
5950
|
|
|
5739
5951
|
Two doors, one adapter, and neither is guessed: a project selects Vertex, a key
|
|
@@ -5786,8 +5998,8 @@ Three decisions worth reading before you rely on them:
|
|
|
5786
5998
|
that arrives anyway is kept out of the visible answer on both paths.
|
|
5787
5999
|
- **A stream that reports no usage reports ZERO, never an estimate.**
|
|
5788
6000
|
`models.countTokens` is on the namespace, is not called, and is named in the pin
|
|
5789
|
-
as not called: it answers what a request
|
|
5790
|
-
|
|
6001
|
+
as not called: it answers what a request _tokenises to_, not what the call was
|
|
6002
|
+
_billed for_. Same law as `openai()` and `ollama()`. (Usage is read off the
|
|
5791
6003
|
closing chunk BEFORE any content guard — the bug that made streamed turns bill
|
|
5792
6004
|
as zero on two earlier adapters.)
|
|
5793
6005
|
|
|
@@ -5859,8 +6071,8 @@ new and load-bearing.
|
|
|
5859
6071
|
|
|
5860
6072
|
### Added — Gemini's over-long-request sentence joins the typed error
|
|
5861
6073
|
|
|
5862
|
-
`ContextWindowExceededError` (9.6.0) now translates
|
|
5863
|
-
(1200293) exceeds the maximum number of tokens allowed (1048576)."
|
|
6074
|
+
`ContextWindowExceededError` (9.6.0) now translates _"The input token count
|
|
6075
|
+
(1200293) exceeds the maximum number of tokens allowed (1048576)."_ — a word order
|
|
5864
6076
|
the existing patterns did not match — and reads both numbers out of it, including
|
|
5865
6077
|
the case where Google ships the first parenthesis empty. Detection stays
|
|
5866
6078
|
conservative: "INPUT token count" is what keeps it off a `max_tokens` validation
|
|
@@ -5909,8 +6121,8 @@ concurrency-and-sessions section, and a status row per boundary.
|
|
|
5909
6121
|
- A `gemini()` error never prints the API key it was constructed with. The
|
|
5910
6122
|
redaction is narrow by design — the exact string you passed, removed from the
|
|
5911
6123
|
message, the stack and the wrapped cause — and is not a heuristic scrubber:
|
|
5912
|
-
a thrown provider error reaches the model as a tool result
|
|
5913
|
-
|
|
6124
|
+
a thrown provider error reaches the model as a tool result _and_ the commit log
|
|
6125
|
+
_and_ every observability sink, so one interpolation would leak to all of them.
|
|
5914
6126
|
|
|
5915
6127
|
## [9.12.0] - 2026-08-12
|
|
5916
6128
|
|
|
@@ -5931,7 +6143,7 @@ provider signed:
|
|
|
5931
6143
|
const credentials = agentCoreIdentity({
|
|
5932
6144
|
region: 'us-west-2',
|
|
5933
6145
|
workloadName: 'workflow_assistant_agent',
|
|
5934
|
-
requireUserToken: true,
|
|
6146
|
+
requireUserToken: true, // optional — refuse a delegated call with no proof
|
|
5935
6147
|
});
|
|
5936
6148
|
|
|
5937
6149
|
// inside a tool
|
|
@@ -5946,7 +6158,7 @@ await ctx.credentials.getCredential({ service: 'google', mode: 'user', userToken
|
|
|
5946
6158
|
- **Nothing downstream changed.** `GetWorkloadAccessTokenForJWT` answers with the
|
|
5947
6159
|
same `workloadAccessToken` the by-userId exchange does, so it feeds the same
|
|
5948
6160
|
`GetResourceOauth2Token` call, the same `Credential`, the same `toHeaders()`.
|
|
5949
|
-
The vault entry at the end belongs to the
|
|
6161
|
+
The vault entry at the end belongs to the _person_ rather than to the agent,
|
|
5950
6162
|
which is what makes revoking their access actually revoke it.
|
|
5951
6163
|
- **Verified against the real SDK before it shipped**, names and shapes both:
|
|
5952
6164
|
`{ workloadName, userToken }` in, `{ workloadAccessToken }` out. It joins the
|
|
@@ -6030,7 +6242,7 @@ refuses a call it used to allow, or emits a field it did not before.
|
|
|
6030
6242
|
### Added — `maxToolResultChars`: a ceiling on ONE tool result
|
|
6031
6243
|
|
|
6032
6244
|
```ts
|
|
6033
|
-
Agent.create({ provider, model, maxToolResultChars: 20_000 })
|
|
6245
|
+
Agent.create({ provider, model, maxToolResultChars: 20_000 });
|
|
6034
6246
|
```
|
|
6035
6247
|
|
|
6036
6248
|
Over the cap, the result is **replaced** by a marker that tells the model what
|
|
@@ -6070,8 +6282,8 @@ happened and what to do about it:
|
|
|
6070
6282
|
### Added — WHO the run was for, on every event
|
|
6071
6283
|
|
|
6072
6284
|
`EventMeta.principal` and `EventMeta.tenant` (9.11.0) join `sessionId` (9.4.0) on
|
|
6073
|
-
every event's meta. The stream has always said
|
|
6074
|
-
is the
|
|
6285
|
+
every event's meta. The stream has always said _what_ happened and _when_; this
|
|
6286
|
+
is the _who_, and the three together are an audit record rather than a debug log.
|
|
6075
6287
|
|
|
6076
6288
|
```ts
|
|
6077
6289
|
await agent.run(message, {
|
|
@@ -6092,7 +6304,7 @@ await agent.run(message, {
|
|
|
6092
6304
|
and `sessionId` beside it is the fact the transport delivered.
|
|
6093
6305
|
- **Which sinks carry it, checked rather than assumed.** `fileObservability`,
|
|
6094
6306
|
`cloudwatchObservability`, `agentcoreObservability` and `auditExport` serialize
|
|
6095
|
-
the whole envelope and inherit it for free — in `auditExport`'s case
|
|
6307
|
+
the whole envelope and inherit it for free — in `auditExport`'s case _inside_
|
|
6096
6308
|
the hash chain, so editing who breaks the same verification as editing what.
|
|
6097
6309
|
`otelObservability` maps signals onto spans rather than serializing, so the
|
|
6098
6310
|
actor is PLACED there: `agentfootprint.principal.id` /
|
|
@@ -6118,7 +6330,7 @@ const policy = PermissionPolicy.fromRoles(roles, 'support', {
|
|
|
6118
6330
|
```
|
|
6119
6331
|
|
|
6120
6332
|
- **`Tool.capabilities`** (`'memory_read' | 'memory_write' | 'external_net' |
|
|
6121
|
-
|
|
6333
|
+
'user_data'`) is a declaration, never an inference. A tool's reach is not
|
|
6122
6334
|
knowable from its name, schema or description, and guessing would rest a policy
|
|
6123
6335
|
decision on a heuristic.
|
|
6124
6336
|
- **`PermissionChecker.governs`** is an optional, feature-detected member —
|
|
@@ -6153,8 +6365,8 @@ PermissionPolicy.fromRoles(roles, 'support', {
|
|
|
6153
6365
|
- The refusal lands **before `execute`**, so a `surfaceMode: 'tool-only'` skill's
|
|
6154
6366
|
body is never even computed.
|
|
6155
6367
|
- **Hidden means unnamed.** The graph offer lists unreachable skills as "not
|
|
6156
|
-
reachable from here" because a cursor can move; a hidden skill is about
|
|
6157
|
-
|
|
6368
|
+
reachable from here" because a cursor can move; a hidden skill is about _who is
|
|
6369
|
+
asking_, and naming it would tell one role about another role's capabilities.
|
|
6158
6370
|
- **The enum stays the full catalog.** `toolArgValidation` runs before the gate,
|
|
6159
6371
|
so narrowing it would turn a policy refusal into a generic schema error and the
|
|
6160
6372
|
model would never read the policy's own message — the reasoning 8.5.0 recorded
|
|
@@ -6204,7 +6416,7 @@ await standingAgent({
|
|
|
6204
6416
|
agentFactory: () => Agent.create({ provider, model }).system('…').build(),
|
|
6205
6417
|
sessions: sqliteSessions({ file: './sessions.db' }),
|
|
6206
6418
|
host: nodeHost({ port: 8080 }),
|
|
6207
|
-
maxActiveSessions: 200,
|
|
6419
|
+
maxActiveSessions: 200, // default 100
|
|
6208
6420
|
});
|
|
6209
6421
|
```
|
|
6210
6422
|
|
|
@@ -6302,7 +6514,6 @@ nodeHost({ sessionCookie: 'af_session' }); // …or no client code at all
|
|
|
6302
6514
|
- New example: `examples/deploy/multi-user.ts` — two people served at once,
|
|
6303
6515
|
proving the overlap in wall clock and that neither saw the other's memory.
|
|
6304
6516
|
|
|
6305
|
-
|
|
6306
6517
|
## [9.9.0] - 2026-08-12
|
|
6307
6518
|
|
|
6308
6519
|
**A bug report IS the evidence.**
|
|
@@ -6320,7 +6531,7 @@ with the prose attached.
|
|
|
6320
6531
|
```ts
|
|
6321
6532
|
import { describeBugReport, exportBugReport } from 'agentfootprint/observe';
|
|
6322
6533
|
|
|
6323
|
-
const offer = describeBugReport(recording);
|
|
6534
|
+
const offer = describeBugReport(recording); // measure — nothing has left yet
|
|
6324
6535
|
// …show offer.units to the human; they tick some…
|
|
6325
6536
|
const report = exportBugReport(recording, {
|
|
6326
6537
|
include: ['conv-1', 'file-conversation', 'file-environment'],
|
|
@@ -6329,7 +6540,7 @@ const report = exportBugReport(recording, {
|
|
|
6329
6540
|
expected: 'the updated price',
|
|
6330
6541
|
actual: 'the price from before the update',
|
|
6331
6542
|
});
|
|
6332
|
-
fs.writeFileSync(report.filename, report.zip);
|
|
6543
|
+
fs.writeFileSync(report.filename, report.zip); // a real .zip
|
|
6333
6544
|
```
|
|
6334
6545
|
|
|
6335
6546
|
- **The manifest is SELECTABLE UNITS, not a blob.** Each conversation is a unit
|
|
@@ -6347,7 +6558,7 @@ fs.writeFileSync(report.filename, report.zip); // a real .zip
|
|
|
6347
6558
|
- **What was left out is STATED.** `manifest.excluded` counts the conversations,
|
|
6348
6559
|
files, events and turns that were withheld, names their unit ids, and the
|
|
6349
6560
|
issue body repeats it. A maintainer reading turn 4 must be able to tell that
|
|
6350
|
-
turns 1–3 were
|
|
6561
|
+
turns 1–3 were _withheld_, not _lost_.
|
|
6351
6562
|
- **Redacted keys, BY NAME.** The recording arrives already redacted (footprintjs
|
|
6352
6563
|
scrubs at commit time), so nothing here scrubs anything — it would be too late
|
|
6353
6564
|
to matter and a second policy could only disagree with the first. Instead the
|
|
@@ -6383,9 +6594,9 @@ plain `fetch` with no SDK:
|
|
|
6383
6594
|
|
|
6384
6595
|
```ts
|
|
6385
6596
|
const reporter = githubBugReporter({
|
|
6386
|
-
issueRepo: 'footprintjs/agentfootprint',
|
|
6387
|
-
evidenceRepo: 'acme/af-bug-evidence',
|
|
6388
|
-
});
|
|
6597
|
+
issueRepo: 'footprintjs/agentfootprint', // public — the conversation
|
|
6598
|
+
evidenceRepo: 'acme/af-bug-evidence', // private — the run
|
|
6599
|
+
}); // token: GITHUB_TOKEN, or `token`
|
|
6389
6600
|
const { issueUrl, zipUrl } = await reporter.file(report);
|
|
6390
6601
|
```
|
|
6391
6602
|
|
|
@@ -6426,13 +6637,13 @@ const { issueUrl, zipUrl } = await reporter.file(report);
|
|
|
6426
6637
|
calls — browser-safe and server-safe, no client secret, no dependency:
|
|
6427
6638
|
|
|
6428
6639
|
```ts
|
|
6429
|
-
const signIn = await githubDeviceSignIn({ clientId });
|
|
6640
|
+
const signIn = await githubDeviceSignIn({ clientId }); // returns at once
|
|
6430
6641
|
show(`Open ${signIn.verificationUri} and enter ${signIn.userCode}`);
|
|
6431
|
-
const { token, login } = await signIn.completed;
|
|
6642
|
+
const { token, login } = await signIn.completed; // resolves on approve
|
|
6432
6643
|
```
|
|
6433
6644
|
|
|
6434
|
-
A server PAT files every report as the
|
|
6435
|
-
|
|
6645
|
+
A server PAT files every report as the _application_; this files it as the
|
|
6646
|
+
_reporter_, which is what a field tester filing upstream needs. It honours
|
|
6436
6647
|
`slow_down`, respects the code's expiry, takes an `AbortSignal`, and fetches
|
|
6437
6648
|
`/user` for attribution (a `/user` that refuses is not fatal — the token still
|
|
6438
6649
|
works, the login is simply absent). The token it returns is handed to
|
|
@@ -6502,7 +6713,9 @@ agent.enable.observability({
|
|
|
6502
6713
|
maxBytes: 64 * 1024 * 1024,
|
|
6503
6714
|
}),
|
|
6504
6715
|
});
|
|
6505
|
-
process.on('SIGTERM', async () => {
|
|
6716
|
+
process.on('SIGTERM', async () => {
|
|
6717
|
+
await agent.shutdown();
|
|
6718
|
+
}); // flushes
|
|
6506
6719
|
```
|
|
6507
6720
|
|
|
6508
6721
|
Four things it is deliberate about:
|
|
@@ -6546,8 +6759,8 @@ import { vaultCredentials } from 'agentfootprint/security';
|
|
|
6546
6759
|
|
|
6547
6760
|
const credentials = vaultCredentials({
|
|
6548
6761
|
address: 'https://vault.internal:8200',
|
|
6549
|
-
paths: { github: 'ci/github' },
|
|
6550
|
-
});
|
|
6762
|
+
paths: { github: 'ci/github' }, // …or resolve(service), or neither
|
|
6763
|
+
}); // token: `token`, else VAULT_TOKEN
|
|
6551
6764
|
```
|
|
6552
6765
|
|
|
6553
6766
|
The tool code does not change from the `staticTokens` version — same port, same
|
|
@@ -6581,7 +6794,7 @@ service)` as the seam for a shop whose field names are its own.
|
|
|
6581
6794
|
the service, the mount path and the HTTP status — and nothing from the
|
|
6582
6795
|
response body, nothing from the token, not even the field names the secret
|
|
6583
6796
|
carries. This is the 8.6.0 law applied one adapter down: a thrown message
|
|
6584
|
-
reaches the model as a tool result
|
|
6797
|
+
reaches the model as a tool result _and_ rides
|
|
6585
6798
|
`agentfootprint.credential.failed` to every observer. It is pinned by a
|
|
6586
6799
|
grep-shaped test that walks every failure path — unknown service, 401, 403,
|
|
6587
6800
|
404, 503, a non-JSON reply, a KV v1 response, an unrecognised field set, and a
|
|
@@ -6594,21 +6807,21 @@ service)` as the seam for a shop whose field names are its own.
|
|
|
6594
6807
|
- **`infrastructure/on-premises.mdx`** — the provider page beside AWS. The
|
|
6595
6808
|
local-first ladder (mock → local model → your gateway → a paid API) as the
|
|
6596
6809
|
opening frame, then a service-by-service map: LLM (`ollama`, `openai({
|
|
6597
|
-
|
|
6810
|
+
baseURL })` for vLLM / llama.cpp / a corporate gateway, or the two-method
|
|
6598
6811
|
port), stores (`sqliteVectorStore`, `pgVectorStore`, `staticVectorStore`,
|
|
6599
6812
|
`RedisStore`), embedders (`localEmbedder`, `staticEmbedder`), hosting
|
|
6600
6813
|
(`httpHost` / `nodeHost` + `sqliteSessions` / `memorySessions`), code
|
|
6601
|
-
execution (`localCodeRunner` —
|
|
6814
|
+
execution (`localCodeRunner` — _isolation, not a sandbox_), telemetry
|
|
6602
6815
|
(`otelObservability` to any OTLP collector, `fileObservability` when there is
|
|
6603
6816
|
none, `auditExport` for evidence), credentials (`staticTokens`,
|
|
6604
6817
|
`vaultCredentials`, and the port for everything else) and tools (`mcpClient`
|
|
6605
6818
|
over stdio or Streamable HTTP). It ends with **what is NOT here** — no
|
|
6606
6819
|
Kubernetes-native anything, no second secret-manager adapter, no metrics
|
|
6607
6820
|
exporter, no retention policy, no air-gapped model distribution.
|
|
6608
|
-
- **The status vocabulary gained one honest rung.**
|
|
6609
|
-
awaiting field
|
|
6821
|
+
- **The status vocabulary gained one honest rung.** _Contract-shaped and tested;
|
|
6822
|
+
awaiting field use_ is what `fileObservability` and `vaultCredentials` carry:
|
|
6610
6823
|
their ports and refusals are pinned by tests, and neither has met a real
|
|
6611
|
-
production disk or vault.
|
|
6824
|
+
production disk or vault. _Verified in a production field deployment_ now
|
|
6612
6825
|
appears in exactly one place, describing a deployment **shape** — a standing
|
|
6613
6826
|
agent over `httpHost` + `sqliteSessions` against an OpenAI-compatible gateway,
|
|
6614
6827
|
the shape several past releases exist because of — and never an adapter. The
|
|
@@ -6639,14 +6852,14 @@ handed that said "this is over".
|
|
|
6639
6852
|
|
|
6640
6853
|
Three optional fields, sourced from what the engine already stamps:
|
|
6641
6854
|
|
|
6642
|
-
| field
|
|
6643
|
-
|
|
6644
|
-
| `ctx.runId`
|
|
6645
|
-
| `ctx.sessionId` | `run({ sessionId })` ← `HostRequest.sessionId` | the run is not session-bound
|
|
6646
|
-
| `ctx.identity`
|
|
6855
|
+
| field | source | absent when |
|
|
6856
|
+
| --------------- | ---------------------------------------------- | ----------------------------------------------------------------------- |
|
|
6857
|
+
| `ctx.runId` | the run in flight | there is no run — a call served over `mcpServe` is one call, not a turn |
|
|
6858
|
+
| `ctx.sessionId` | `run({ sessionId })` ← `HostRequest.sessionId` | the run is not session-bound |
|
|
6859
|
+
| `ctx.identity` | the identity the CALLER passed | the caller passed none |
|
|
6647
6860
|
|
|
6648
6861
|
Every one is **absent rather than invented**, which is the 9.4.0 rule applied one
|
|
6649
|
-
layer down. `ctx.identity` is deliberately
|
|
6862
|
+
layer down. `ctx.identity` is deliberately _not_ the run's internal
|
|
6650
6863
|
`runIdentity`: that is always populated, defaulting to
|
|
6651
6864
|
`{ conversationId: '<runId>' }`, and handing a tool a synthesized conversation as
|
|
6652
6865
|
"the identity" would let it isolate a live sandbox on a fiction.
|
|
@@ -6676,12 +6889,12 @@ tool that wants a run-scoped session needs to know it is at a door with no runs
|
|
|
6676
6889
|
BEFORE it opens one. Asking for a scope a door cannot honour throws, naming the
|
|
6677
6890
|
door.
|
|
6678
6891
|
|
|
6679
|
-
| scope
|
|
6680
|
-
|
|
6681
|
-
| `'call'`
|
|
6682
|
-
| `'run'`
|
|
6683
|
-
| `'session'`
|
|
6684
|
-
| `'shutdown'` | `agent.shutdown()`.
|
|
6892
|
+
| scope | fires |
|
|
6893
|
+
| ------------ | ------------------------------------------------------------------------------------- |
|
|
6894
|
+
| `'call'` | when `tool.execute` settles — resolve **or** throw. Every door, including `mcpServe`. |
|
|
6895
|
+
| `'run'` | at a run terminal that is **not a pause**. |
|
|
6896
|
+
| `'session'` | `agent.closeToolSessions({ sessionId })`. |
|
|
6897
|
+
| `'shutdown'` | `agent.shutdown()`. |
|
|
6685
6898
|
|
|
6686
6899
|
Seven laws, each pinned: at most once ever · idempotent by `(tool, scope, key)`
|
|
6687
6900
|
with the FIRST registration winning (it holds the live handle) · reverse
|
|
@@ -6692,7 +6905,7 @@ tolerates "already gone" · nothing live is ever persisted into a checkpoint.
|
|
|
6692
6905
|
**A pause is not a terminal.** `'run'` teardown deliberately does not hang off
|
|
6693
6906
|
`finally`, which also runs on both pause shapes. A `checkIn` on a code
|
|
6694
6907
|
interpreter stops the run so a person can approve the code; tearing the sandbox
|
|
6695
|
-
down there destroys the exact state the resume needs, and it fails
|
|
6908
|
+
down there destroys the exact state the resume needs, and it fails _quietly_ — as
|
|
6696
6909
|
a resumed run that "just re-ran everything". An error IS a terminal.
|
|
6697
6910
|
|
|
6698
6911
|
### Added — `agent.closeToolSessions({ sessionId })`
|
|
@@ -6781,7 +6994,7 @@ the rows, and what comes back is the finding.
|
|
|
6781
6994
|
`client.send(new Command(...))`, pinned in `test/adapters/aws/awsCommandPin.ts`
|
|
6782
6995
|
and **verified against a real install of the SDK before shipping** — including
|
|
6783
6996
|
two shapes a design could only have guessed at: `Invoke` answers with an EVENT
|
|
6784
|
-
STREAM, and seven of its nine union members are modelled
|
|
6997
|
+
STREAM, and seven of its nine union members are modelled _exceptions_ (folded
|
|
6785
6998
|
in as empty output, an `AccessDenied` would have reported a clean run that
|
|
6786
6999
|
"printed nothing"), and `Stop` takes the session id, not a URI.
|
|
6787
7000
|
- **`codeRunnerTool({ runner, scope })`** (main barrel) — holds one session per
|
|
@@ -6846,8 +7059,8 @@ and now shares the paging with the general one.
|
|
|
6846
7059
|
**Behaviour change, named loudly: multi-turn memory starts actually
|
|
6847
7060
|
retaining.** A six-turn conversation stores twelve message entries where it
|
|
6848
7061
|
stored two, and the window injects up to `size` of them instead of the last
|
|
6849
|
-
exchange — so prompts get longer and stores get bigger
|
|
6850
|
-
now remembering what it was asked to
|
|
7062
|
+
exchange — so prompts get longer and stores get bigger _because the agent is
|
|
7063
|
+
now remembering what it was asked to remember_. Turn it down deliberately
|
|
6851
7064
|
(`size`, `DECAY`, `.compaction()`) rather than by accident.
|
|
6852
7065
|
|
|
6853
7066
|
Cost: one paged `list()` per store per run, and only when a memory actually
|
|
@@ -7096,7 +7309,7 @@ compaction that runs.
|
|
|
7096
7309
|
**AWS adapters tell the truth.**
|
|
7097
7310
|
|
|
7098
7311
|
A production field report tested 9.3.0 against a real account and found two
|
|
7099
|
-
adapters
|
|
7312
|
+
adapters _dispatching calls that were never made against AWS_ — one sending a
|
|
7100
7313
|
command that does not exist, one calling a method that a command-based client
|
|
7101
7314
|
does not have. Both compiled. Both had green tests. Every one of those tests
|
|
7102
7315
|
injected a double past the SDK, which is exactly why the bug class survives: the
|
|
@@ -7193,9 +7406,9 @@ are untouched.
|
|
|
7193
7406
|
|
|
7194
7407
|
### Changed — a fail-closed refusal now READS final
|
|
7195
7408
|
|
|
7196
|
-
When a `PermissionChecker` throws, the call is denied. What the model was
|
|
7409
|
+
When a `PermissionChecker` throws, the call is denied. What the model was _told_
|
|
7197
7410
|
was the checker's own thrown message — and those are written for operators:
|
|
7198
|
-
|
|
7411
|
+
_"not available right now"_, `ECONNREFUSED`, _"timed out"_. **Measured in
|
|
7199
7412
|
production: a real model read that as weather and retried the same tool to
|
|
7200
7413
|
`maxIterations`, then returned the empty string.** Against the local policy's
|
|
7201
7414
|
long-standing bracketed form the same model adapted cleanly on the first
|
|
@@ -7236,8 +7449,8 @@ calls did so in a silence that read like health.
|
|
|
7236
7449
|
### Added — `EventMeta.sessionId`: which CONVERSATION an event belongs to
|
|
7237
7450
|
|
|
7238
7451
|
`meta.runId` is per `run()` / `resume()`; a session spans many. A shipped
|
|
7239
|
-
telemetry stream could answer
|
|
7240
|
-
happened in this conversation?"
|
|
7452
|
+
telemetry stream could answer _"what happened in this run?"_ and not _"what
|
|
7453
|
+
happened in this conversation?"_ — the question a session-oriented host is built
|
|
7241
7454
|
around, and one the events alone cannot be joined back into afterwards.
|
|
7242
7455
|
|
|
7243
7456
|
`standingAgent` now threads the caller's own session id onto every event the run
|
|
@@ -7257,13 +7470,13 @@ three preconditions it had no evidence for, all three of which were wrong at
|
|
|
7257
7470
|
once:
|
|
7258
7471
|
|
|
7259
7472
|
- **A euclidean index was accepted.** The construction refusal read
|
|
7260
|
-
`options.distanceMetric`, a
|
|
7473
|
+
`options.distanceMetric`, a _claim by the caller_ about an index the store did
|
|
7261
7474
|
not create, so the default (undeclared) sailed through. Measured live: a
|
|
7262
7475
|
vector queried against itself returned **0.9991630113800056**, where a true
|
|
7263
7476
|
cosine self-similarity is exactly `1.0` — the very "number that READS like a
|
|
7264
7477
|
cosine and is not one" this adapter's own header warns about.
|
|
7265
7478
|
- **A missing `nonFilterableMetadataKeys: ['af']`** surfaced as a raw AWS
|
|
7266
|
-
`ValidationException` —
|
|
7479
|
+
`ValidationException` — _"Filterable metadata must have at most 2048 bytes"_ —
|
|
7267
7480
|
**mid-import**, with documents already written and success already reported
|
|
7268
7481
|
for them.
|
|
7269
7482
|
- **The index dimension was never compared with the embedder.**
|
|
@@ -7287,11 +7500,11 @@ index whose metric, layout and dimension it would be guessing at.
|
|
|
7287
7500
|
|
|
7288
7501
|
**Three promises the code had already made, kept.**
|
|
7289
7502
|
|
|
7290
|
-
`MemoryStore`'s own docstring has named its backends since 2.x —
|
|
7503
|
+
`MemoryStore`'s own docstring has named its backends since 2.x — _"Every storage
|
|
7291
7504
|
backend (InMemory, Redis, DynamoDB, **Postgres**, Bedrock AgentCore) implements
|
|
7292
|
-
this interface"
|
|
7293
|
-
would look:
|
|
7294
|
-
`putMany`, and
|
|
7505
|
+
this interface"_ — and named the queries too, in the two places an implementer
|
|
7506
|
+
would look: _"**Postgres**: multi-row INSERT … ON CONFLICT DO UPDATE"_ for
|
|
7507
|
+
`putMany`, and _"**pgvector**: `ORDER BY embedding <=> query LIMIT k`"_ for
|
|
7295
7508
|
`search`. Every one of those sentences was true about the design and false about
|
|
7296
7509
|
the shipped package. Two of the three items below are the same shape: a
|
|
7297
7510
|
documented promise with nothing behind it.
|
|
@@ -7455,8 +7668,8 @@ are now pinned; the rest are below.
|
|
|
7455
7668
|
### The conversation has a door with its own name
|
|
7456
7669
|
|
|
7457
7670
|
```ts
|
|
7458
|
-
await agent.run({ message: 'Book me a table for two.' });
|
|
7459
|
-
await agent.followUp('Make it three.');
|
|
7671
|
+
await agent.run({ message: 'Book me a table for two.' }); // one turn
|
|
7672
|
+
await agent.followUp('Make it three.'); // the next one
|
|
7460
7673
|
|
|
7461
7674
|
// …or hand the conversation around — plain JSON, any store, any machine:
|
|
7462
7675
|
const conversation = agent.checkpoint();
|
|
@@ -7514,7 +7727,7 @@ already broken in effect, so the throw is a fix, not a new restriction.
|
|
|
7514
7727
|
is what makes `checkpoint()`, `getLastSnapshot()` and `followUp()` possible.
|
|
7515
7728
|
Two overlapping runs both finished, both returning plausible answers, and the
|
|
7516
7729
|
state afterwards belonged to whichever finished last — so `checkpoint()` could
|
|
7517
|
-
hand back the
|
|
7730
|
+
hand back the _other_ run's conversation, with nothing in either recording
|
|
7518
7731
|
saying so. That is corruption, not concurrency. `standingAgent` has serialized
|
|
7519
7732
|
runs since it existed and calls it "a correctness requirement rather than a
|
|
7520
7733
|
tuning choice"; the guarantee now lives in the primitive. Two turns at once:
|
|
@@ -7528,16 +7741,16 @@ a consent gate any later message could walk around. Answer it with
|
|
|
7528
7741
|
`resume(checkpoint, decision)`, or say plainly that it is being dropped:
|
|
7529
7742
|
|
|
7530
7743
|
```ts
|
|
7531
|
-
const dropped = agent.abandonPause();
|
|
7744
|
+
const dropped = agent.abandonPause(); // { toolName, toolCallId, question }
|
|
7532
7745
|
await agent.run({ message: 'never mind, different question' });
|
|
7533
7746
|
```
|
|
7534
7747
|
|
|
7535
|
-
`abandonPause()`
|
|
7748
|
+
`abandonPause()` _returns_ what it dropped, so the abandonment can be logged
|
|
7536
7749
|
rather than performed blind.
|
|
7537
7750
|
|
|
7538
7751
|
**A pause belongs to a session, not to the instance.** `standingAgent` shares
|
|
7539
7752
|
one `Agent` across every session, so the instance guard alone would have let
|
|
7540
|
-
session A's unanswered question refuse session B's
|
|
7753
|
+
session A's unanswered question refuse session B's _first_ message — a
|
|
7541
7754
|
different conversation, a different person, an answer they were never asked
|
|
7542
7755
|
for. The composer now releases the instance at the moment ownership moves: once
|
|
7543
7756
|
the pause is in the store, the store owns it, and a later request carrying a
|
|
@@ -7558,7 +7771,7 @@ No shipped example, test or doc called it twice.
|
|
|
7558
7771
|
### `agent.canExplain()`
|
|
7559
7772
|
|
|
7560
7773
|
`.selfExplain()` was already honest to the **model** with no record bound — the
|
|
7561
|
-
trace tools answer
|
|
7774
|
+
trace tools answer _"No completed run is available yet"_ and the skill body says
|
|
7562
7775
|
to say so plainly. It had no answer for the **program**. `canExplain()` returns
|
|
7563
7776
|
`false` for two honest reasons — not built with `.selfExplain()`, or built with
|
|
7564
7777
|
it and no turn completed — so a caller can route a why-question before spending
|
|
@@ -7586,7 +7799,7 @@ of this class quietly. Three kinds of pin:
|
|
|
7586
7799
|
bare `throw`.
|
|
7587
7800
|
2. **Adapted** — pinned by what reaches the model / the store / the caller,
|
|
7588
7801
|
observed on the wire, never by mocking internals.
|
|
7589
|
-
3. **Stated** — pinned **twice**: the behavior,
|
|
7802
|
+
3. **Stated** — pinned **twice**: the behavior, _and_ the sentence in the source
|
|
7590
7803
|
that states it. A stated behavior whose statement was deleted is back to
|
|
7591
7804
|
being a silent success, and only the second assertion catches that.
|
|
7592
7805
|
|
|
@@ -7605,7 +7818,7 @@ decision somebody makes in a diff.
|
|
|
7605
7818
|
prints the messages the provider actually received for each. The model's own
|
|
7606
7819
|
"this is your first message" reply is the evidence.
|
|
7607
7820
|
- `examples/features/49-self-explain-live.ts` — turn 2 now goes through
|
|
7608
|
-
`followUp()`. It was a second `run()`, and the scripted mock read the
|
|
7821
|
+
`followUp()`. It was a second `run()`, and the scripted mock read the _first_
|
|
7609
7822
|
user message, so the demo's own scripting was masking the restart. Live, the
|
|
7610
7823
|
why-question used to arrive with no subject.
|
|
7611
7824
|
- `docs-next/content/docs/build/conversations.mdx` — the conversation, the two
|
|
@@ -7617,13 +7830,13 @@ decision somebody makes in a diff.
|
|
|
7617
7830
|
Additive except for the three refusals, and all three fire on code that was
|
|
7618
7831
|
already wrong:
|
|
7619
7832
|
|
|
7620
|
-
| if you…
|
|
7621
|
-
|
|
|
7622
|
-
| call `run()` twice expecting continuity
|
|
7623
|
-
| overlap two `run()` calls on one agent
|
|
7624
|
-
| send a message while a pause is open
|
|
7625
|
-
| call `.system()` twice
|
|
7626
|
-
| continue a stored conversation on a differently-**named** agent | `ConversationMismatchError`
|
|
7833
|
+
| if you… | you now get | do this |
|
|
7834
|
+
| --------------------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------- |
|
|
7835
|
+
| call `run()` twice expecting continuity | the same behavior as before (a new conversation) | `followUp(message)` or `run({ message, continueFrom })` |
|
|
7836
|
+
| overlap two `run()` calls on one agent | `RunInFlightError` | await the first, or build a second agent |
|
|
7837
|
+
| send a message while a pause is open | `PendingQuestionError` | `resume(checkpoint, decision)`, or `abandonPause()` first |
|
|
7838
|
+
| call `.system()` twice | build-time throw | join the strings, or `.steering()` / `.configure()` |
|
|
7839
|
+
| continue a stored conversation on a differently-**named** agent | `ConversationMismatchError` | continue it on the agent whose id recorded it |
|
|
7627
7840
|
|
|
7628
7841
|
`checkpoint()` payloads written by 9.1.0 and earlier continue to work; they
|
|
7629
7842
|
simply carry no `identity` or `agent`, which is the honest answer for a
|
|
@@ -7654,13 +7867,13 @@ embedder might have — which then cuts every larger embedder short.
|
|
|
7654
7867
|
characters, that this embedder represents faithfully. Every shipped embedder
|
|
7655
7868
|
fills it in:
|
|
7656
7869
|
|
|
7657
|
-
| embedder
|
|
7658
|
-
|
|
7659
|
-
| `localEmbedder()`
|
|
7660
|
-
| `openaiEmbedder()`
|
|
7661
|
-
| `bedrockEmbedder()` | `32000`
|
|
7662
|
-
| `staticEmbedder()`
|
|
7663
|
-
| `mockEmbedder()`
|
|
7870
|
+
| embedder | `maxInputChars` | where the number comes from |
|
|
7871
|
+
| ------------------- | --------------- | -------------------------------------------------------------- |
|
|
7872
|
+
| `localEmbedder()` | `2000` | measured — the default model's 512-wordpiece-token cliff |
|
|
7873
|
+
| `openaiEmbedder()` | `32000` | the documented 8,191-token window, at 4 characters a token |
|
|
7874
|
+
| `bedrockEmbedder()` | `32000` | Titan's documented 8,192-token window, same conversion |
|
|
7875
|
+
| `staticEmbedder()` | `1000000` | no transformer, so no context window — nothing is ever clipped |
|
|
7876
|
+
| `mockEmbedder()` | `1000000` | reads every character in a loop |
|
|
7664
7877
|
|
|
7665
7878
|
`indexCorpus`, `indexFolder` and `indexDocuments` read the embedder's declared
|
|
7666
7879
|
ceiling **in preference to** their own 2,000-character default. An explicit
|
|
@@ -7678,7 +7891,7 @@ is accepted because the cliff belongs to the **model**, not to the factory.
|
|
|
7678
7891
|
### Truncation became visible
|
|
7679
7892
|
|
|
7680
7893
|
A run that clipped anything now says so — **once**, on `console.warn`, naming
|
|
7681
|
-
the count, the ceiling in effect
|
|
7894
|
+
the count, the ceiling in effect _and where that ceiling came from_, and the
|
|
7682
7895
|
two fixes (re-split smaller, or raise `maxChunkChars`). `IndexReport` gains
|
|
7683
7896
|
**`truncatedCount`** beside the existing `truncated` list: the list is what you
|
|
7684
7897
|
debug with, the count is what you assert on and what a dashboard row can hold.
|
|
@@ -7726,27 +7939,27 @@ deprecation warnings on 8.20.0, it compiles and runs unchanged on 9.0.0.**
|
|
|
7726
7939
|
|
|
7727
7940
|
### Removed — the sixteen door aliases
|
|
7728
7941
|
|
|
7729
|
-
Each removed path re-exported the
|
|
7942
|
+
Each removed path re-exported the _same symbols_ the door carries, never copies,
|
|
7730
7943
|
so this is a find-and-replace on import lines. No name moved; no name was lost.
|
|
7731
7944
|
|
|
7732
|
-
| you were importing from
|
|
7733
|
-
|
|
7734
|
-
| `agentfootprint/llm-providers`
|
|
7735
|
-
| `agentfootprint/embedders`
|
|
7736
|
-
| `agentfootprint/tool-providers`
|
|
7737
|
-
| `agentfootprint/thinking`
|
|
7738
|
-
| `agentfootprint/memory-providers`
|
|
7739
|
-
| `agentfootprint/observability-providers`
|
|
7740
|
-
| `agentfootprint/strategies`
|
|
7741
|
-
| `agentfootprint/stream`
|
|
7742
|
-
| `agentfootprint/status`
|
|
7743
|
-
| `agentfootprint/locales`
|
|
7744
|
-
| `agentfootprint/debug`
|
|
7745
|
-
| `agentfootprint/debug/finders`
|
|
7746
|
-
| `agentfootprint/observability/contextError/finders` | `agentfootprint/observe`
|
|
7747
|
-
| `agentfootprint/hosting-providers`
|
|
7748
|
-
| `agentfootprint/injection-engine`
|
|
7749
|
-
| `agentfootprint/identity`
|
|
7945
|
+
| you were importing from | import from |
|
|
7946
|
+
| --------------------------------------------------- | -------------------------- |
|
|
7947
|
+
| `agentfootprint/llm-providers` | `agentfootprint/providers` |
|
|
7948
|
+
| `agentfootprint/embedders` | `agentfootprint/providers` |
|
|
7949
|
+
| `agentfootprint/tool-providers` | `agentfootprint/providers` |
|
|
7950
|
+
| `agentfootprint/thinking` | `agentfootprint/providers` |
|
|
7951
|
+
| `agentfootprint/memory-providers` | `agentfootprint/memory` |
|
|
7952
|
+
| `agentfootprint/observability-providers` | `agentfootprint/observe` |
|
|
7953
|
+
| `agentfootprint/strategies` | `agentfootprint/observe` |
|
|
7954
|
+
| `agentfootprint/stream` | `agentfootprint/observe` |
|
|
7955
|
+
| `agentfootprint/status` | `agentfootprint/observe` |
|
|
7956
|
+
| `agentfootprint/locales` | `agentfootprint/observe` |
|
|
7957
|
+
| `agentfootprint/debug` | `agentfootprint/observe` |
|
|
7958
|
+
| `agentfootprint/debug/finders` | `agentfootprint/observe` |
|
|
7959
|
+
| `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
|
|
7960
|
+
| `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
|
|
7961
|
+
| `agentfootprint/injection-engine` | `agentfootprint/context` |
|
|
7962
|
+
| `agentfootprint/identity` | `agentfootprint/security` |
|
|
7750
7963
|
|
|
7751
7964
|
What ships now is exactly: the root barrel, the ten doors (`/providers`,
|
|
7752
7965
|
`/memory`, `/rag`, `/cache`, `/observe`, `/events`, `/context`, `/resilience`,
|
|
@@ -7762,23 +7975,23 @@ implementation barrel is served by its door as the same object.
|
|
|
7762
7975
|
|
|
7763
7976
|
### Removed — options, strings, methods, fields
|
|
7764
7977
|
|
|
7765
|
-
| removed
|
|
7766
|
-
|
|
7767
|
-
| `AgentBuilder.recorder(rec)`
|
|
7768
|
-
| `defineSkill({ viaToolName })`
|
|
7769
|
-
| `skillsFromDir(dir, { viaToolName })`
|
|
7770
|
-
| `WindowRefusalReason` member `'summary-not-smaller'`
|
|
7771
|
-
| type `FoldRefusal`
|
|
7772
|
-
| type `FoldRefusalReason`
|
|
7773
|
-
| `WindowStrategy` exported from `agentfootprint/memory` | `MemoryWindowStrategy`
|
|
7774
|
-
| `CompactionRecord.foldedStageIds`
|
|
7775
|
-
| `CompactionRecord.foldedMessageCount`
|
|
7776
|
-
| `ContextBudgetPressurePayload.capTokens`
|
|
7777
|
-
| `ContextBudgetPressurePayload.projectedTokens`
|
|
7778
|
-
| `BudgetPressureRecord.capTokens`
|
|
7779
|
-
| `BudgetPressureRecord.projectedTokens`
|
|
7780
|
-
|
|
7781
|
-
Three of those are worth a sentence each, because the
|
|
7978
|
+
| removed | replacement | since |
|
|
7979
|
+
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
|
|
7980
|
+
| `AgentBuilder.recorder(rec)` | `AgentBuilder.watch(rec)` — same list, same order, same attachment, and variadic | deprecated 8.0.0 |
|
|
7981
|
+
| `defineSkill({ viaToolName })` | drop it — `'read_skill'` is the only activation tool the library builds; gate on a `rule` trigger or a `skillGraph()` edge | deprecated 8.7.0 |
|
|
7982
|
+
| `skillsFromDir(dir, { viaToolName })` | drop it — same reason | deprecated 8.7.0 |
|
|
7983
|
+
| `WindowRefusalReason` member `'summary-not-smaller'` | `'replacement-not-smaller'` | renamed 8.14.0 |
|
|
7984
|
+
| type `FoldRefusal` | `WindowRefusal` | renamed 7.17 |
|
|
7985
|
+
| type `FoldRefusalReason` | `WindowRefusalReason` | renamed 7.17 |
|
|
7986
|
+
| `WindowStrategy` exported from `agentfootprint/memory` | `MemoryWindowStrategy` | renamed 7.27.1 |
|
|
7987
|
+
| `CompactionRecord.foldedStageIds` | `WindowRecord.removedStageIds` | family name published 7.17 |
|
|
7988
|
+
| `CompactionRecord.foldedMessageCount` | `WindowRecord.removedMessageCount` | family name published 7.17 |
|
|
7989
|
+
| `ContextBudgetPressurePayload.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
|
|
7990
|
+
| `ContextBudgetPressurePayload.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
|
|
7991
|
+
| `BudgetPressureRecord.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
|
|
7992
|
+
| `BudgetPressureRecord.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
|
|
7993
|
+
|
|
7994
|
+
Three of those are worth a sentence each, because the _reason_ is the migration:
|
|
7782
7995
|
|
|
7783
7996
|
- **`viaToolName` named a door that was never built.** The evaluator activates
|
|
7784
7997
|
an `llm-activated` skill by matching `ctx.activatedInjectionIds`, only
|
|
@@ -7817,7 +8030,7 @@ replacement and says when the signpost comes down.
|
|
|
7817
8030
|
Deleting the type member alone would have been a silent DOWNGRADE for the two
|
|
7818
8031
|
`viaToolName` cases: an object literal gets an excess-property error, but an
|
|
7819
8032
|
options bag arriving through a variable does not, and the value would then be
|
|
7820
|
-
|
|
8033
|
+
_ignored_ where 8.7.0 refused it. So the field is read at run time exactly once
|
|
7821
8034
|
more, to say it is gone.
|
|
7822
8035
|
|
|
7823
8036
|
### Two things deliberately kept
|
|
@@ -7832,7 +8045,7 @@ more, to say it is gone.
|
|
|
7832
8045
|
`CircuitState`, which is declared in both breaker files as
|
|
7833
8046
|
`'closed' | 'open' | 'half-open'` — two declarations, one type, pinned as such.
|
|
7834
8047
|
- **`buildRunSteps(events)` survives, still `@deprecated`.** Its deprecation is
|
|
7835
|
-
a
|
|
8048
|
+
a _preference_, not a migration: live consumers should attach
|
|
7836
8049
|
`runStepRecorder()` and read `getSteps()` (O(N), the house pattern) instead of
|
|
7837
8050
|
re-walking an event log (O(N²) across repeated calls). But the shim is the only
|
|
7838
8051
|
way to build steps from a saved event list — replay, post-hoc analysis, tests —
|
|
@@ -7909,7 +8122,7 @@ The fix is one coherent rule across the structural splitters:
|
|
|
7909
8122
|
alone. The heading is now glued to the first body paragraph; it can never
|
|
7910
8123
|
again be a chunk by itself.
|
|
7911
8124
|
- **The family was inspected, and two members are exempt by design.**
|
|
7912
|
-
`fixedWithOverlap` cuts uniformly sized chunks
|
|
8125
|
+
`fixedWithOverlap` cuts uniformly sized chunks _by request_ — imposing a
|
|
7913
8126
|
250-char floor on `fixedWithOverlap({ chars: 120 })` would repeal the
|
|
7914
8127
|
caller's own choice, and its only runt (the file tail) has always folded
|
|
7915
8128
|
backward. `wholeDocument` is one chunk per document by definition.
|
|
@@ -7963,7 +8176,7 @@ any immutable or serverless runtime has this problem):
|
|
|
7963
8176
|
- **`exportCorpus(store, identity?)`** (`agentfootprint/rag`) — every entry
|
|
7964
8177
|
of a corpus namespace as one plain-JSON `CorpusBundle`:
|
|
7965
8178
|
`{ entries: [{ id, text, vector, metadata }], embedder: { id, dimensions },
|
|
7966
|
-
|
|
8179
|
+
namespace }`. Plain JSON on purpose — the runtime that needs this is
|
|
7967
8180
|
exactly the runtime that cannot open a database file. It refuses an empty
|
|
7968
8181
|
namespace (naming the identity-mismatch cause), entries with no vector or
|
|
7969
8182
|
no passage (a bundle never ships an unservable or uncitable entry), and a
|
|
@@ -8125,7 +8338,7 @@ it, before a byte is embedded. `InMemoryStore` and `sqliteVectorStore` declare
|
|
|
8125
8338
|
**Behaviour change:** building a corpus into `AgentCoreStore` or `RedisStore` is
|
|
8126
8339
|
refused where it used to run. Neither could ever serve those vectors back —
|
|
8127
8340
|
`RedisStore` has no `search()` at all, so the same mistake already failed one
|
|
8128
|
-
layer later, when `defineRAG` refused the store
|
|
8341
|
+
layer later, when `defineRAG` refused the store _after_ the whole index had been
|
|
8129
8342
|
embedded and billed. The refusal moved to the call that starts the spending.
|
|
8130
8343
|
|
|
8131
8344
|
**Absence is not a `false`.** A store that declares nothing behaves exactly as it
|
|
@@ -8145,7 +8358,7 @@ it wrong.
|
|
|
8145
8358
|
```ts
|
|
8146
8359
|
import { bedrockEmbedder } from 'agentfootprint/providers';
|
|
8147
8360
|
|
|
8148
|
-
const embedder = bedrockEmbedder({ region: 'us-east-1' });
|
|
8361
|
+
const embedder = bedrockEmbedder({ region: 'us-east-1' }); // 1024-d
|
|
8149
8362
|
const small = bedrockEmbedder({ region: 'us-east-1', dimensions: 512 });
|
|
8150
8363
|
```
|
|
8151
8364
|
|
|
@@ -8318,7 +8531,7 @@ the run stops re-asking and names the rule — in the `outputAttempts` row
|
|
|
8318
8531
|
(`brokenBy`), in the event, in `outputContractUnmet()`, and in the warning.
|
|
8319
8532
|
Re-asking cannot fix a rule: a deterministic one breaks the next answer
|
|
8320
8533
|
identically, so the retries buy a repeat of the same ending. An answer that was
|
|
8321
|
-
|
|
8534
|
+
_already_ bad still spends its retries — the stop applies only when the
|
|
8322
8535
|
middleware is the cause.
|
|
8323
8536
|
|
|
8324
8537
|
### `.outputFallback()` says which door reaches its tiers
|
|
@@ -8405,8 +8618,8 @@ flowchartAsTool({
|
|
|
8405
8618
|
name: 'weather_advice',
|
|
8406
8619
|
description: 'Decide whether to bike tomorrow.',
|
|
8407
8620
|
flowchart: adviceChart,
|
|
8408
|
-
keepRecord: true,
|
|
8409
|
-
keepRecordLimit: 20,
|
|
8621
|
+
keepRecord: true, // ← off by default
|
|
8622
|
+
keepRecordLimit: 20, // ← bounded LRU window (this is the default)
|
|
8410
8623
|
redact: { keys: ['apiKey'] },
|
|
8411
8624
|
});
|
|
8412
8625
|
```
|
|
@@ -8497,8 +8710,8 @@ artifacts by hand: `innerRunStore`, `innerRunsOf`, `mergeInnerRuns`,
|
|
|
8497
8710
|
### The demo
|
|
8498
8711
|
|
|
8499
8712
|
`examples/features/50-through-the-tool-boundary.ts` — a weather-advice agent
|
|
8500
|
-
whose ONE tool is a 4-stage footprintjs chart. Turn 1:
|
|
8501
|
-
in Chicago tomorrow?"
|
|
8713
|
+
whose ONE tool is a 4-stage footprintjs chart. Turn 1: _"Should I bike to work
|
|
8714
|
+
in Chicago tomorrow?"_ Turn 2: _"Why did you say it'll rain?"_ — answered
|
|
8502
8715
|
through visible `find_in_trace` → `inspect_tool_call` → `inspect_tool_run`
|
|
8503
8716
|
calls that cite the inner stage (`validate-forecast#1`), the exact field
|
|
8504
8717
|
(`rainChancePct = 82`) and the rule that consumed it. The chart's own stage
|
|
@@ -8519,8 +8732,8 @@ evidence existed, and the model had no way to reach it.
|
|
|
8519
8732
|
### `find_in_trace(query)` — free text in, step ids out
|
|
8520
8733
|
|
|
8521
8734
|
Every other trace tool needs a name you already have: a step id, a state key, a
|
|
8522
|
-
variable. But a follow-up question arrives in the user's words —
|
|
8523
|
-
order 7712 was out of warranty?"
|
|
8735
|
+
variable. But a follow-up question arrives in the user's words — _"why did you say
|
|
8736
|
+
order 7712 was out of warranty?"_ — and the model's only options were to guess a
|
|
8524
8737
|
state key or read the whole narrative.
|
|
8525
8738
|
|
|
8526
8739
|
This searches stage names and descriptions, state keys, every committed value and
|
|
@@ -8657,7 +8870,7 @@ await callTraceTool(tools, 'find_in_trace', { query: 'order 7712' });
|
|
|
8657
8870
|
|
|
8658
8871
|
Pure: no engine, no agent, no I/O. Exported from `agentfootprint/observe` (and
|
|
8659
8872
|
`agentfootprint/debug`). Honest about the two things a serialized run cannot carry
|
|
8660
|
-
back — `controlDeps` is a lookup
|
|
8873
|
+
back — `controlDeps` is a lookup _function_ and does not serialize (slices say
|
|
8661
8874
|
`⚠ control edges unavailable`, the marker that already existed), and the narrative
|
|
8662
8875
|
survives only if a narrative recorder was attached, since `recordRun` deliberately
|
|
8663
8876
|
attaches none. Two teaching refusals name `recordRun` as the producer: a bundle with
|
|
@@ -8710,11 +8923,11 @@ cursor half and left the rule half standing, and the rule half is the bug: an en
|
|
|
8710
8923
|
routed to `T`, `S`'s rule still matched and `S` and `T` were both active. Measured on
|
|
8711
8924
|
a two-skill support graph:
|
|
8712
8925
|
|
|
8713
|
-
| iteration
|
|
8714
|
-
|
|
8715
|
-
| 1
|
|
8926
|
+
| iteration | active | tool menu | skill bodies |
|
|
8927
|
+
| --------------- | ------------------ | -------------------------------------------- | --------------- |
|
|
8928
|
+
| 1 | `triage` | `read_skill`, `lookup_order` | triage |
|
|
8716
8929
|
| 2 — the handoff | `triage`, `refund` | `read_skill`, `lookup_order`, `issue_refund` | triage + refund |
|
|
8717
|
-
| 3
|
|
8930
|
+
| 3 | `triage`, `refund` | same | triage + refund |
|
|
8718
8931
|
|
|
8719
8932
|
Note iteration 3. This was never a one-iteration blip: with the cursor parked on
|
|
8720
8933
|
`refund`, `triage`'s rule kept matching, so it came back and stayed. The overlap was
|
|
@@ -8722,8 +8935,8 @@ the steady state.
|
|
|
8722
8935
|
|
|
8723
8936
|
**A conditional entry is now active exactly while the cursor is on it** — the same
|
|
8724
8937
|
compiled expression a route target and an exclusive entry already used. One law for a
|
|
8725
|
-
flat graph:
|
|
8726
|
-
unconditional
|
|
8938
|
+
flat graph: _a skill is active iff the cursor is on it, or it declared itself
|
|
8939
|
+
unconditional._ `when` chooses where a turn STARTS.
|
|
8727
8940
|
|
|
8728
8941
|
This finishes 8.3.0 rather than reverting it. Both failures 8.3.0 named — a declared
|
|
8729
8942
|
step INTO an entry skill, and a `read_skill` pick onto one — are carried by the cursor
|
|
@@ -8761,7 +8974,7 @@ superseded pick.
|
|
|
8761
8974
|
|
|
8762
8975
|
`multi-entry-fanout` fired whenever a graph declared two or more entries, including
|
|
8763
8976
|
when every one of them carried a `when` — a deterministic rule-router, which is a
|
|
8764
|
-
taught shape. Worse, the advice it gave was
|
|
8977
|
+
taught shape. Worse, the advice it gave was _"give the extras a `when`"_, to entries
|
|
8765
8978
|
that already had one. It computed which entries were unconditional and then used that
|
|
8766
8979
|
only to soften the middle of the sentence.
|
|
8767
8980
|
|
|
@@ -8780,7 +8993,7 @@ why the check over-fired. Rewritten.
|
|
|
8780
8993
|
A suppression the run cannot name is a silent drop. When a conditional entry's rule
|
|
8781
8994
|
matched and the cursor law kept it off the wire, the entry's id is now reported on the
|
|
8782
8995
|
per-iteration evaluation event, beside the `cursorMove` that says where the graph went
|
|
8783
|
-
instead. Together they answer
|
|
8996
|
+
instead. Together they answer _"why isn't my entry loading?"_ without anyone
|
|
8784
8997
|
re-running a predicate to guess.
|
|
8785
8998
|
|
|
8786
8999
|
Omitted when nothing was suppressed, and for every non-skill-graph run — so an
|
|
@@ -8826,8 +9039,8 @@ build, and one changes a string on the wire. All eight are below.
|
|
|
8826
9039
|
correct branch:
|
|
8827
9040
|
|
|
8828
9041
|
- **same provider family** — it billed your MAIN model for every fold. The
|
|
8829
|
-
refusal three lines above it in the same file promised
|
|
8830
|
-
quietly bill your main model for compaction"
|
|
9042
|
+
refusal three lines above it in the same file promised _"the library will not
|
|
9043
|
+
quietly bill your main model for compaction"_, and then did.
|
|
8831
9044
|
- **different provider** — it sent your agent's model id to a vendor that has
|
|
8832
9045
|
never heard of it, so the fold died mid-run, on a paid run, in a file whose
|
|
8833
9046
|
own header promises "everything fails at `.build()`, never mid-run".
|
|
@@ -8855,7 +9068,7 @@ Readings are now stamped with the iteration whose call produced them and expire
|
|
|
8855
9068
|
one boundary later. An expired reading is `undefined`, which every strategy
|
|
8856
9069
|
already treats as "do not act" — so a window strategy **stands down** instead of
|
|
8857
9070
|
deciding on a number nobody took, and says so once on the console rather than
|
|
8858
|
-
going quiet. "Counted, never guessed" has to mean counted
|
|
9071
|
+
going quiet. "Counted, never guessed" has to mean counted _recently_.
|
|
8859
9072
|
|
|
8860
9073
|
An agent whose provider reports usage reliably is unaffected.
|
|
8861
9074
|
|
|
@@ -8900,10 +9113,10 @@ comparison of two string lengths will not.
|
|
|
8900
9113
|
Two emitters share this event name, this `slot: 'messages'` value, and — until
|
|
8901
9114
|
now — one indistinguishable payload:
|
|
8902
9115
|
|
|
8903
|
-
| emitter
|
|
8904
|
-
|
|
8905
|
-
| the three context slots (`contextBudget`, **on by default**) | `String.length`
|
|
8906
|
-
| a window strategy (`.window()` / `.compaction()`)
|
|
9116
|
+
| emitter | counts | `unit` |
|
|
9117
|
+
| ------------------------------------------------------------ | ------------------------------ | ---------- |
|
|
9118
|
+
| the three context slots (`contextBudget`, **on by default**) | `String.length` | `'chars'` |
|
|
9119
|
+
| a window strategy (`.window()` / `.compaction()`) | provider-reported input tokens | `'tokens'` |
|
|
8907
9120
|
|
|
8908
9121
|
So one subscriber routinely received both, and `cap 200, projected 258` could
|
|
8909
9122
|
mean 258 characters or 258 tokens — a roughly 4× difference in the same field,
|
|
@@ -8927,8 +9140,8 @@ oversight — please do not "fix" them.
|
|
|
8927
9140
|
### `costBudget` can now stop the run
|
|
8928
9141
|
|
|
8929
9142
|
`costBudget` was warn-only, while `commentaryTemplates.ts` narrated
|
|
8930
|
-
|
|
8931
|
-
claimed the agent
|
|
9143
|
+
_"{{appName}} hit a cost limit and stopped."_ and `docs/monitor/deployment.mdx`
|
|
9144
|
+
claimed the agent _"halts when the per-run USD budget is hit"_. It did neither.
|
|
8932
9145
|
`docs/monitor/observability.mdx`, on the same site, correctly said the library
|
|
8933
9146
|
never auto-aborts.
|
|
8934
9147
|
|
|
@@ -9037,7 +9250,7 @@ version would make an older deployment refuse a session it can serve.
|
|
|
9037
9250
|
**Governance never silently drops — and never silently invents.** Eight ways a
|
|
9038
9251
|
rule you configured could decide nothing, and you could only find out by reading
|
|
9039
9252
|
a quiet run. Seven are now refused at build time with a message that names the
|
|
9040
|
-
fix; one was a rule that ran everywhere except the one path where a
|
|
9253
|
+
fix; one was a rule that ran everywhere except the one path where a _person_ had
|
|
9041
9254
|
just typed the value.
|
|
9042
9255
|
|
|
9043
9256
|
Two of these change what a run DOES. Both are called out below.
|
|
@@ -9109,7 +9322,7 @@ It now raises `DecisionRequiredError` (`code: 'ERR_DECISION_REQUIRED'`) at the
|
|
|
9109
9322
|
API boundary. **Nothing executes and the checkpoint is unchanged**, so the same
|
|
9110
9323
|
one can be answered properly and resumed again. The error names the gate
|
|
9111
9324
|
(`gate: 'checkIn' | 'ask'`), the tool, the middleware that asked, and `received`
|
|
9112
|
-
— the
|
|
9325
|
+
— the _shape_ that arrived, never its contents, because a resume payload is
|
|
9113
9326
|
caller data and an error message ends up in logs.
|
|
9114
9327
|
|
|
9115
9328
|
Discriminated by the **pause**, never by the input, via the new
|
|
@@ -9191,9 +9404,9 @@ never stops your strategy. It now also carries two methods:
|
|
|
9191
9404
|
```ts
|
|
9192
9405
|
const telemetry = agent.enable.observability({ strategy: cloudwatch });
|
|
9193
9406
|
|
|
9194
|
-
await telemetry.flush();
|
|
9195
|
-
telemetry();
|
|
9196
|
-
telemetry.stop();
|
|
9407
|
+
await telemetry.flush(); // drain: driver queue first, then the buffer
|
|
9408
|
+
telemetry(); // detach (unchanged)
|
|
9409
|
+
telemetry.stop(); // release — timers, clients, buffers
|
|
9197
9410
|
```
|
|
9198
9411
|
|
|
9199
9412
|
`flush()` enforces the ORDER, which is the part no consumer could write from
|
|
@@ -9316,7 +9529,7 @@ of trying again. A drain that cannot finish must return, never retry forever.
|
|
|
9316
9529
|
|
|
9317
9530
|
With `enable.observability({ detach })`, each export is scheduled onto a
|
|
9318
9531
|
footprintjs detach driver. Scheduling happened inside a promise continuation,
|
|
9319
|
-
so the detach handle reached footprintjs's registry a microtask
|
|
9532
|
+
so the detach handle reached footprintjs's registry a microtask _after_ the
|
|
9320
9533
|
event was dispatched. `flushAllDetached()` drains until that registry is empty
|
|
9321
9534
|
— and it was still empty when it looked.
|
|
9322
9535
|
|
|
@@ -9339,7 +9552,7 @@ the package either way.
|
|
|
9339
9552
|
reach today (the declared type is still `Unsubscribe`); it exists so the
|
|
9340
9553
|
no-subscription case is not the one path that breaks when that type widens.
|
|
9341
9554
|
- Removed a dead loop condition in the CloudWatch drain (`lastFlushPromise !==
|
|
9342
|
-
|
|
9555
|
+
Promise.resolve()` compares against a freshly minted promise and is always
|
|
9343
9556
|
true).
|
|
9344
9557
|
|
|
9345
9558
|
## [8.11.0] - 2026-08-07
|
|
@@ -9376,7 +9589,7 @@ delivery — just an empty log group.
|
|
|
9376
9589
|
The docstring for `logStreamName` had been promising `"Created on first put if
|
|
9377
9590
|
it doesn't exist"` since the adapter shipped. It was never true. Worse, the
|
|
9378
9591
|
convention the docs themselves recommended — `` `${HOSTNAME}/${Date.now()}` ``
|
|
9379
|
-
— produces a name that
|
|
9592
|
+
— produces a name that _cannot_ pre-exist, so following the documentation
|
|
9380
9593
|
guaranteed the bug on every deploy. The only configuration that worked was the
|
|
9381
9594
|
undocumented one.
|
|
9382
9595
|
|
|
@@ -9392,7 +9605,7 @@ instead of implying otherwise.
|
|
|
9392
9605
|
|
|
9393
9606
|
The missing stream was one delivery failure. It turned out **every** delivery
|
|
9394
9607
|
failure was silent: an IAM denial, a throttle, a rejected batch. Each adapter
|
|
9395
|
-
installed its console fallback lazily
|
|
9608
|
+
installed its console fallback lazily _inside_ its own `_onError` method — so
|
|
9396
9609
|
the delivery path, which read the hook rather than calling the method, found
|
|
9397
9610
|
`undefined` and dropped the error on the floor. `cloudwatch`, `xray` and `otel`
|
|
9398
9611
|
all had it.
|
|
@@ -9419,7 +9632,7 @@ fail. It is replaced by six that assert unconditionally.
|
|
|
9419
9632
|
|
|
9420
9633
|
### A knob the warning told you to turn, that did not exist
|
|
9421
9634
|
|
|
9422
|
-
An over-budget context slot warned:
|
|
9635
|
+
An over-budget context slot warned: _"Raise `budgetCap` on the slot config."_
|
|
9423
9636
|
`budgetCap` was reachable from no public door. `buildMessagesSlot()` was called
|
|
9424
9637
|
with no arguments at all four of its call sites, so its 10000-character cap was
|
|
9425
9638
|
unreachable by construction. A warning you cannot act on is worse than no
|
|
@@ -9439,15 +9652,15 @@ budget is a signal, not a limiter. `LLMCallOptions` takes the same option
|
|
|
9439
9652
|
|
|
9440
9653
|
A 429 is a **pre-execution rejection**: the rate limiter refused the request at
|
|
9441
9654
|
the edge and the server never ran the tool, so a retry cannot double-execute
|
|
9442
|
-
anything. That is exactly what is
|
|
9655
|
+
anything. That is exactly what is _not_ true of a 500 or a timeout, where the
|
|
9443
9656
|
call may have half-run and a retry could charge a card twice.
|
|
9444
9657
|
|
|
9445
9658
|
That asymmetry is the entire license for this feature, so the policy is 429 and
|
|
9446
9659
|
nothing else — pinned by a property test that walks twelve other statuses and a
|
|
9447
9660
|
thrown transport error and asserts a single attempt for each. Managed gateways
|
|
9448
9661
|
rate-limit per principal by design; without this, a designed and self-clearing
|
|
9449
|
-
condition reached the model as a thrown tool error it reads as
|
|
9450
|
-
broken"
|
|
9662
|
+
condition reached the model as a thrown tool error it reads as _"this tool is
|
|
9663
|
+
broken"_, whereupon it apologises, picks another tool, or invents an answer.
|
|
9451
9664
|
|
|
9452
9665
|
It lives at the `fetch` seam because that is the only place `Retry-After` still
|
|
9453
9666
|
exists — the MCP SDK reads the response, throws `StreamableHTTPError(status,
|
|
@@ -9474,18 +9687,22 @@ Per-attempt visibility is the `onRetry` callback — the contract `withRetry` an
|
|
|
9474
9687
|
are consumer-called, the docs now say so, and a batching exporter loses its
|
|
9475
9688
|
final batch and leaks its timer if you skip them:
|
|
9476
9689
|
```ts
|
|
9477
|
-
process.on('SIGTERM', async () => {
|
|
9690
|
+
process.on('SIGTERM', async () => {
|
|
9691
|
+
await telemetry.flush();
|
|
9692
|
+
telemetry.stop();
|
|
9693
|
+
stop();
|
|
9694
|
+
});
|
|
9478
9695
|
```
|
|
9479
9696
|
Wiring them into the framework lifecycle would change `run()` timing and
|
|
9480
9697
|
misbehave for a strategy shared across two `enable` calls, so it is a design
|
|
9481
9698
|
question on the ledger rather than a silent default.
|
|
9482
9699
|
- **A Skill's tools are visible from iteration 1.** `DefineSkillOptions.tools`
|
|
9483
|
-
said they were
|
|
9700
|
+
said they were _"added to the tools slot once activated"_. They are added to
|
|
9484
9701
|
the registry at build time; activation adds the Skill's **body**, not its
|
|
9485
9702
|
tools. Gating is opt-in via `autoActivate: 'currentSkill'` (which
|
|
9486
9703
|
`skillGraph().tree()` sets for you on every leaf) — and the docs said
|
|
9487
9704
|
otherwise in twelve places, including a `process_refund` example claiming a
|
|
9488
|
-
tool was
|
|
9705
|
+
tool was _"locked away"_. That example now sets `autoActivate` and the prose
|
|
9489
9706
|
no longer implies a security boundary the default does not provide.
|
|
9490
9707
|
- **`autoActivate` stopped calling itself a forward-compat marker** awaiting
|
|
9491
9708
|
"v2.5 runtime wiring" — that wiring shipped in 2.5.0, six majors ago.
|
|
@@ -9495,7 +9712,7 @@ Per-attempt visibility is the `onRetry` callback — the contract `withRetry` an
|
|
|
9495
9712
|
anything, and a lower tier is not a safer one: `'minimal'` still ships
|
|
9496
9713
|
`agent.turn_start` (`userPrompt`), `agent.turn_end` (`finalContent`) and
|
|
9497
9714
|
`agent.iteration_end` (the whole conversation `history[]`) — measured, it
|
|
9498
|
-
carries user content in a
|
|
9715
|
+
carries user content in a _higher_ share of its events than `'standard'` does.
|
|
9499
9716
|
The docstring says this plainly now, points at `auditExport()` (bounded by
|
|
9500
9717
|
default) and `otelObservability()` (omits `userPrompt`), and warns that
|
|
9501
9718
|
`redactContent` does **not** apply to this channel — it operates on the
|
|
@@ -9589,12 +9806,12 @@ exists.
|
|
|
9589
9806
|
`textLoader`, `markdownLoader` and `htmlLoader` need no dependency. `pdfLoader`
|
|
9590
9807
|
needs one, and it was picked by measuring rather than by reputation:
|
|
9591
9808
|
|
|
9592
|
-
| package
|
|
9593
|
-
|
|
9594
|
-
| **`unpdf`**
|
|
9595
|
-
| `pdf-parse@2`
|
|
9596
|
-
| `pdf-parse@1`
|
|
9597
|
-
| `pdfjs-dist@6` | 62 MB
|
|
9809
|
+
| package | installed | packages | verdict |
|
|
9810
|
+
| -------------- | ---------- | -------- | ------------------------------------------------- |
|
|
9811
|
+
| **`unpdf`** | **2.5 MB** | **1** | chosen — zero transitive deps, per-page text |
|
|
9812
|
+
| `pdf-parse@2` | 86 MB | 3 | a native binary (`@napi-rs/canvas`), to read text |
|
|
9813
|
+
| `pdf-parse@1` | 34 MB | 4 | unmaintained since 2018 |
|
|
9814
|
+
| `pdfjs-dist@6` | 62 MB | 2 | 25× the size for the same engine |
|
|
9598
9815
|
|
|
9599
9816
|
It is an optional peer, lazily loaded, refusing with an install line when a PDF
|
|
9600
9817
|
is actually met. Per-page text is why a PDF citation can name a page you can
|
|
@@ -9698,7 +9915,6 @@ the page its 150-character run-up borrowed from.
|
|
|
9698
9915
|
- **`unpdf`** as a new optional peer. Only `pdfLoader` touches it, only when a
|
|
9699
9916
|
PDF is actually read.
|
|
9700
9917
|
|
|
9701
|
-
|
|
9702
9918
|
## [8.9.0] - 2026-08-06
|
|
9703
9919
|
|
|
9704
9920
|
**The durable index.** 8.8.0 made retrieval tell the truth about what it read.
|
|
@@ -9724,8 +9940,8 @@ await indexDocuments(store, embedder, docs, { embedderId: embedder.id });
|
|
|
9724
9940
|
Embedding cost is not one number, and the split is the whole argument for a
|
|
9725
9941
|
file. **Index time** embeds the corpus: once, scaling with how much you store.
|
|
9726
9942
|
**Query time** embeds the user's question: per retrieval, scaling with traffic.
|
|
9727
|
-
A 10,000-chunk corpus is 10,000 embeddings
|
|
9728
|
-
thereafter — with a `Map` it is 10,000 embeddings
|
|
9943
|
+
A 10,000-chunk corpus is 10,000 embeddings _once_ and one per question
|
|
9944
|
+
thereafter — with a `Map` it is 10,000 embeddings _per restart_.
|
|
9729
9945
|
|
|
9730
9946
|
`agentfootprint.embedding.generated` has carried an `inputKind: 'document' |
|
|
9731
9947
|
'query'` field since 2.x and nothing ever emitted it, so any dashboard built
|
|
@@ -9748,13 +9964,13 @@ top-K or it does not answer.
|
|
|
9748
9964
|
|
|
9749
9965
|
Measured against this implementation on Node 22.16, Apple silicon:
|
|
9750
9966
|
|
|
9751
|
-
| corpus
|
|
9752
|
-
|
|
9753
|
-
| 10,000 × 384-d
|
|
9754
|
-
| 50,000 × 384-d
|
|
9755
|
-
| 100,000 × 384-d | 65 ms | 154 MB
|
|
9756
|
-
| 10,000 × 1536-d | 16 ms | 61 MB
|
|
9757
|
-
| 50,000 × 1536-d | 89 ms | 307 MB
|
|
9967
|
+
| corpus | query | resident matrix | file | first search (hydration) |
|
|
9968
|
+
| --------------- | ----- | --------------- | ------ | ------------------------ |
|
|
9969
|
+
| 10,000 × 384-d | 6 ms | 15 MB | 21 MB | 45 ms |
|
|
9970
|
+
| 50,000 × 384-d | 31 ms | 77 MB | 105 MB | 251 ms |
|
|
9971
|
+
| 100,000 × 384-d | 65 ms | 154 MB | 211 MB | 939 ms |
|
|
9972
|
+
| 10,000 × 1536-d | 16 ms | 61 MB | 83 MB | 122 ms |
|
|
9973
|
+
| 50,000 × 1536-d | 89 ms | 307 MB | 413 MB | **5.7 s** |
|
|
9758
9974
|
|
|
9759
9975
|
**The documented ceiling is 50,000 chunks** — under 100 ms per query at every
|
|
9760
9976
|
embedder this library ships, under ~300 MB resident. It degrades linearly to
|
|
@@ -9837,13 +10053,12 @@ safe to answer with "no matches".**
|
|
|
9837
10053
|
- The BLOB on disk keeps the **original** vector, so `get`/`list` round-trip
|
|
9838
10054
|
exactly what was written; normalisation happens once, into the resident
|
|
9839
10055
|
matrix, so search is a dot product without changing what is stored.
|
|
9840
|
-
- Found while writing the schema-identity check: it originally ran
|
|
10056
|
+
- Found while writing the schema-identity check: it originally ran _after_ the
|
|
9841
10057
|
indexes were created, so a foreign `af_vectors` table failed on a missing
|
|
9842
10058
|
column and was reported as `'cannot-open'` — the right refusal for the wrong
|
|
9843
10059
|
reason, telling the reader to check file permissions when the real problem was
|
|
9844
10060
|
that the file belonged to something else. It runs before them now.
|
|
9845
10061
|
|
|
9846
|
-
|
|
9847
10062
|
## [8.8.0] - 2026-08-06
|
|
9848
10063
|
|
|
9849
10064
|
**Retrieval tells the truth.** A retrieval computed a cosine score for every candidate
|
|
@@ -9878,12 +10093,14 @@ the two are registered separately, each with its own store:
|
|
|
9878
10093
|
```ts
|
|
9879
10094
|
const agent = Agent.create({ provider })
|
|
9880
10095
|
.rag(defineRAG({ id: 'product-docs', store: corpusStore, embedder }))
|
|
9881
|
-
.memory(
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
10096
|
+
.memory(
|
|
10097
|
+
defineMemory({
|
|
10098
|
+
id: 'chat',
|
|
10099
|
+
type: MEMORY_TYPES.EPISODIC,
|
|
10100
|
+
strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
|
|
10101
|
+
store: conversationStore,
|
|
10102
|
+
}),
|
|
10103
|
+
)
|
|
9887
10104
|
.build();
|
|
9888
10105
|
```
|
|
9889
10106
|
|
|
@@ -9905,7 +10122,7 @@ namespace it reads from — defaulting to the same `'_global'` the indexer write
|
|
|
9905
10122
|
index with no options and retrieve with no options and the documents are found. Pass it
|
|
9906
10123
|
explicitly for a per-tenant corpus, on both sides.
|
|
9907
10124
|
|
|
9908
|
-
And a namespace that holds nothing is now
|
|
10125
|
+
And a namespace that holds nothing is now _reported_ rather than answered around:
|
|
9909
10126
|
`corpusEmpty: true` on the retrieval event, plus a once-per-process warning naming the
|
|
9910
10127
|
namespace it searched and the usual cause.
|
|
9911
10128
|
|
|
@@ -9924,7 +10141,7 @@ namespace it searched and the usual cause.
|
|
|
9924
10141
|
are admitted**: `search` returns score-descending, so either the whole pool clears the
|
|
9925
10142
|
floor (admitted = first `k`, as before) or some entry fails it (every later entry fails
|
|
9926
10143
|
too, so the pool already holds every entry that clears it). `rejectWindow` only controls
|
|
9927
|
-
how many near-misses can be
|
|
10144
|
+
how many near-misses can be _shown_.
|
|
9928
10145
|
- **`agentfootprint.memory.retrieved`** (new, 72 typed events) — one per retrieval,
|
|
9929
10146
|
carrying every candidate. `candidates: undefined` means the store ranked server-side
|
|
9930
10147
|
and returned nothing comparable; it never means there were none.
|
|
@@ -9965,7 +10182,7 @@ recency ordering the best-scoring chunk can land last.
|
|
|
9965
10182
|
### Chunks the model can cite
|
|
9966
10183
|
|
|
9967
10184
|
A retrieved page of a PDF rendered as `<memory role="unknown" turn="0">` under the header
|
|
9968
|
-
|
|
10185
|
+
_"Relevant context from prior conversations"_ — three claims that were not true of a
|
|
9969
10186
|
document, and no way to cite it. `defineRAG` renders a corpus as what it is:
|
|
9970
10187
|
|
|
9971
10188
|
```text
|
|
@@ -10017,13 +10234,12 @@ nothing to configure.
|
|
|
10017
10234
|
`core/`; a second implementation is how two recordings of the same bytes end up
|
|
10018
10235
|
disagreeing about their id.
|
|
10019
10236
|
|
|
10020
|
-
|
|
10021
10237
|
## [8.7.0] - 2026-08-06
|
|
10022
10238
|
|
|
10023
10239
|
**The check-up stops being quiet, and a dead option stops pretending.** 8.4.0 stopped a
|
|
10024
10240
|
skill graph from throwing away what the author declared; 8.5.0 stopped it telling the
|
|
10025
10241
|
model things that were not so. This one is about the configurations the library
|
|
10026
|
-
|
|
10242
|
+
_watched you build and said nothing about_ — an entry menu with no way to choose from
|
|
10027
10243
|
it, a transition the cursor can never take, a tool name two sources claim, a scoped
|
|
10028
10244
|
tool provider that returns nothing forever. Nine findings, one shape: the library knew,
|
|
10029
10245
|
and did not say.
|
|
@@ -10031,7 +10247,7 @@ and did not say.
|
|
|
10031
10247
|
### An entry menu with no way to choose from it
|
|
10032
10248
|
|
|
10033
10249
|
Declare two entries and no `.entryBy()` / `.entryByRead()`, and both of them load on
|
|
10034
|
-
every call. An entry's compiled trigger is cursor
|
|
10250
|
+
every call. An entry's compiled trigger is cursor-_independent_ — no `when` compiles to
|
|
10035
10251
|
`{ kind: 'always' }` — while exactly ONE of them can be the cursor: the first whose
|
|
10036
10252
|
`when` passes. So the extras pay for their body and their tools on every iteration and
|
|
10037
10253
|
route nothing, which is the opposite of what a skill graph is for.
|
|
@@ -10077,7 +10293,7 @@ all versus only bare edges incoming.
|
|
|
10077
10293
|
|
|
10078
10294
|
### `unreachable-skill` is told per trigger kind
|
|
10079
10295
|
|
|
10080
|
-
The sentence
|
|
10296
|
+
The sentence _"it can only be reached by the model via read_skill"_ is true for an
|
|
10081
10297
|
`llm-activated` trigger and for no other kind — `Agent.openSkillIds()` admits an open
|
|
10082
10298
|
pick only for that one. But `deriveTrigger` returns null for an unwired skill, so a
|
|
10083
10299
|
skill that arrived carrying a hand-authored `rule` trigger **kept it**, and the warning
|
|
@@ -10147,7 +10363,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
|
|
|
10147
10363
|
- **`ToolDispatchContext.activeSkillIds`** — the real active set for this iteration.
|
|
10148
10364
|
Optional, so a provider written before 8.7.0 sees `undefined` and behaves as it did.
|
|
10149
10365
|
- **`agentfootprint.tools.shadowed`** (71 typed events now) — `{ toolName, iteration,
|
|
10150
|
-
|
|
10366
|
+
schemaFrom, schemaFromId?, dispatchTo, dispatchToId? }`. Names only: never args, never
|
|
10151
10367
|
results, never a description body.
|
|
10152
10368
|
- **`skillScopedToolsTarget` / `SKILL_SCOPED_TOOLS_ID_PREFIX`** — the provider-id
|
|
10153
10369
|
convention, readable by anyone composing providers.
|
|
@@ -10165,7 +10381,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
|
|
|
10165
10381
|
object-literal form since 8.4.0. **Behavior change.** A fluent graph with an
|
|
10166
10382
|
error-level problem — `no-entry` or `unknown-skill`, i.e. a graph that cannot start a
|
|
10167
10383
|
turn at all — built in silence outside dev mode and surfaced as a run that entered no
|
|
10168
|
-
skill. What still builds: every graph whose check-up has no
|
|
10384
|
+
skill. What still builds: every graph whose check-up has no _error_ (warnings never
|
|
10169
10385
|
throw, however many); every call passing `check: 'warn'` explicitly, which still never
|
|
10170
10386
|
throws, so the mode keeps its name and its meaning; `check: 'off'` skips entirely.
|
|
10171
10387
|
Only code that was already shipping a graph the library could not start is affected.
|
|
@@ -10174,7 +10390,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
|
|
|
10174
10390
|
activates the skill — and no such tool has ever been built. The evaluator activates an
|
|
10175
10391
|
`llm-activated` skill by matching `ctx.activatedInjectionIds`, which only `read_skill`
|
|
10176
10392
|
writes, and it has never read the field. A skill declaring `viaToolName:
|
|
10177
|
-
|
|
10393
|
+
'open_playbook'` activated through `read_skill` exactly like every other skill, so the
|
|
10178
10394
|
declaration described a door that does not exist. Nothing that worked stops working;
|
|
10179
10395
|
a silent no-op becomes a named one, at `Agent.injection()` — the one funnel `.skill()`,
|
|
10180
10396
|
`.skills()`, `.skillGraph()`, `skillsFromDir()` and a hand-built Injection all pass
|
|
@@ -10247,7 +10463,7 @@ it raises `CredentialConsentRequiredError`.
|
|
|
10247
10463
|
back `authorization-required`.
|
|
10248
10464
|
- **`CredentialConsentRequiredError`** (`ERR_CREDENTIAL_CONSENT_REQUIRED`, from
|
|
10249
10465
|
`agentfootprint/identity`) — carries `service`, `sessionId`, `authorizationUrl`,
|
|
10250
|
-
`tool` and `iteration`. The error
|
|
10466
|
+
`tool` and `iteration`. The error _message_ deliberately omits the URL, because
|
|
10251
10467
|
a message is the one string that reliably reaches a log line.
|
|
10252
10468
|
- **`pauseData.authorization`** — `{ service, authorizationUrl, sessionId }` on a
|
|
10253
10469
|
consent pause, surfaced by `standingAgent` as `PendingAsk.pauseData`. The
|
|
@@ -10275,7 +10491,7 @@ it raises `CredentialConsentRequiredError`.
|
|
|
10275
10491
|
This is the mirror image of a guarantee the library kept carefully everywhere
|
|
10276
10492
|
else. `agentfootprint.credential.authorization_required` was designed to carry
|
|
10277
10493
|
`{ service, sessionId }` and never the URL; OTel and X-Ray record the tool
|
|
10278
|
-
result's
|
|
10494
|
+
result's _type_ and never its value; the audit bundle's default `bounded` mode
|
|
10279
10495
|
maps `tool_end.result` to `[type: string]`. Every observer channel was
|
|
10280
10496
|
disciplined. The one channel nobody thought of as an observer — the
|
|
10281
10497
|
conversation — was not, and it feeds all the others.
|
|
@@ -10415,7 +10631,7 @@ that gave the middleware-ask outcome union no `result` arm.
|
|
|
10415
10631
|
## [8.5.0] - 2026-08-06
|
|
10416
10632
|
|
|
10417
10633
|
**`read_skill` tells the whole truth.** 8.4.0 stopped a skill graph from throwing
|
|
10418
|
-
away what the author declared. This one stops it from telling the
|
|
10634
|
+
away what the author declared. This one stops it from telling the _model_ things
|
|
10419
10635
|
that were not so. Five findings, all of the same shape: the library said a thing had
|
|
10420
10636
|
happened, or offered a thing it would refuse, or recorded a cause that was not the
|
|
10421
10637
|
cause. One is a build-time refusal, one is a gate refusal, three are fixes.
|
|
@@ -10424,8 +10640,8 @@ cause. One is a build-time refusal, one is a gate refusal, three are fixes.
|
|
|
10424
10640
|
|
|
10425
10641
|
A `tree()` routes by predicate on every iteration. It has no cursor, so `read_skill`
|
|
10426
10642
|
has nothing to move. But `graph.reachableSkills()` reported **all the leaves**, so
|
|
10427
|
-
the gate accepted a leaf pick and `read_skill` answered
|
|
10428
|
-
next iteration"
|
|
10643
|
+
the gate accepted a leaf pick and `read_skill` answered _"Skill 'x' activated for the
|
|
10644
|
+
next iteration"_ — and nothing happened. A leaf compiles to a `rule` trigger; a
|
|
10429
10645
|
`read_skill` call writes only `activatedInjectionIds`; no `rule` trigger reads that.
|
|
10430
10646
|
The leaf never activated, the tree re-decided by predicate, and the run then emitted
|
|
10431
10647
|
`agentfootprint.skill.reroute_superseded` naming a winner that **did not exist** —
|
|
@@ -10451,7 +10667,7 @@ though the tool accepted the name. Answer with the skill the tree routed to, or
|
|
|
10451
10667
|
**Behavior change:** `graph.reachableSkills()` now returns `[]` for a decision
|
|
10452
10668
|
`tree()`, from every cursor. Its contract is "what `read_skill` may jump to", and
|
|
10453
10669
|
all-leaves was the lie; use `graph.skills` to enumerate leaves, which is what it was
|
|
10454
|
-
always for. `read_skill` is not dead under a tree — anything registered
|
|
10670
|
+
always for. `read_skill` is not dead under a tree — anything registered _beside_ the
|
|
10455
10671
|
graph (`.skill(x)`, `.skills(reg)`, `.selfExplain()`) is **open** and still admitted
|
|
10456
10672
|
from anywhere, because those really do activate by `read_skill`. Two docstrings that
|
|
10457
10673
|
promised "read_skill stays a full escape hatch there" are corrected.
|
|
@@ -10477,7 +10693,7 @@ Not reachable from here (read_skill for these will be refused):
|
|
|
10477
10693
|
```
|
|
10478
10694
|
|
|
10479
10695
|
**The enum stays the full catalog, deliberately.** `toolArgValidation` defaults to
|
|
10480
|
-
`'enforce'` and runs
|
|
10696
|
+
`'enforce'` and runs _before_ the gate; an off-enum id is rejected with a generic
|
|
10481
10697
|
schema error and never reaches it. Narrowing the enum would therefore have retired
|
|
10482
10698
|
the gate's teaching refusal, the `agentfootprint.skill.rejected` event,
|
|
10483
10699
|
`routeRecorder`'s rejection hops and the rejected-cap governor's only input — four
|
|
@@ -10511,7 +10727,7 @@ Use 'both' (system prompt AND tool result) or 'system-prompt'.
|
|
|
10511
10727
|
```
|
|
10512
10728
|
|
|
10513
10729
|
Refusal rather than a quiet fall back to the system slot: the author wrote
|
|
10514
|
-
`'tool-only'` to keep the body
|
|
10730
|
+
`'tool-only'` to keep the body _out_ of the system prompt, and silently putting it
|
|
10515
10731
|
back would honour the activation while breaking the declaration — a different lie,
|
|
10516
10732
|
not a fix. `'both'` already means "deliver it either way".
|
|
10517
10733
|
|
|
@@ -10539,7 +10755,7 @@ drift:
|
|
|
10539
10755
|
- `routeRecorder()` reads it, and a `'model-pick'` hop carries **no** `edgeLabel`.
|
|
10540
10756
|
|
|
10541
10757
|
This settles the one case no observer could reconstruct: an edge and a same-turn pick
|
|
10542
|
-
naming the
|
|
10758
|
+
naming the _same_ skill resolves to `'route'` (`D1 > D2`), and only the resolver
|
|
10543
10759
|
knows. Without `cursorMove` (an older graph, an older recording) the previous
|
|
10544
10760
|
inference still stands.
|
|
10545
10761
|
|
|
@@ -10574,11 +10790,11 @@ refusal on the agent, and the gate one is a fix, not a refusal.
|
|
|
10574
10790
|
cannot move the graph somewhere the graph doesn't go. That set is about the CURSOR,
|
|
10575
10791
|
but it was being used as the whole catalog, so three shapes were dead:
|
|
10576
10792
|
|
|
10577
|
-
| you wrote
|
|
10578
|
-
|
|
10579
|
-
| `.skillGraph(g).selfExplain()`
|
|
10580
|
-
| `.skillGraph(g).skill(x)` / `.skills(reg)`
|
|
10581
|
-
| `skillGraph({ skills: [..., x] })` with `x` wired to nothing | refused — while its check-up warning said
|
|
10793
|
+
| you wrote | before | now |
|
|
10794
|
+
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
10795
|
+
| `.skillGraph(g).selfExplain()` | `read_skill('self-explain')` rejected on every call — the debug skill and its six trace tools could never load | activates; the trace tools reach the model on the next iteration |
|
|
10796
|
+
| `.skillGraph(g).skill(x)` / `.skills(reg)` | `x` was listed in `read_skill`'s own menu and refused every time; its body was unreachable | activates |
|
|
10797
|
+
| `skillGraph({ skills: [..., x] })` with `x` wired to nothing | refused — while its check-up warning said _"it can only be reached by the model via read_skill"_ | activates; the warning is true again |
|
|
10582
10798
|
|
|
10583
10799
|
A skill is **open** when its trigger is `llm-activated` (the trigger `read_skill`
|
|
10584
10800
|
actually activates — a rule-gated injection is still refused, because admitting it
|
|
@@ -10703,7 +10919,7 @@ dropped pick is reported rather than swallowed — see the new event below.
|
|
|
10703
10919
|
### `agentfootprint.skill.reroute_superseded` (new typed event — 70 total)
|
|
10704
10920
|
|
|
10705
10921
|
Fires in exactly one case: a `read_skill` the gate accepted did not end up active
|
|
10706
|
-
because a declared edge won the same turn (the model emitted a domain tool
|
|
10922
|
+
because a declared edge won the same turn (the model emitted a domain tool _and_
|
|
10707
10923
|
`read_skill` in one message). Payload: `{ volunteeredId, wonId, fromSkillId,
|
|
10708
10924
|
iteration }`. It is derived from the real active set, not from which clause won,
|
|
10709
10925
|
so it cannot fire for a pick that did take effect.
|
|
@@ -10747,7 +10963,7 @@ never engages any of this.
|
|
|
10747
10963
|
|
|
10748
10964
|
**Durable compaction.** An agent that has been up for a week folds week one
|
|
10749
10965
|
into a summary. Then it gets deployed over. It comes back, is handed the same
|
|
10750
|
-
conversation — and now it can still tell you what week one was about,
|
|
10966
|
+
conversation — and now it can still tell you what week one was about, _and_
|
|
10751
10967
|
show you week one, word for word.
|
|
10752
10968
|
|
|
10753
10969
|
The window half of that already worked: a summary is an ordinary message, so
|
|
@@ -10818,14 +11034,14 @@ surprise.
|
|
|
10818
11034
|
|
|
10819
11035
|
### New exports
|
|
10820
11036
|
|
|
10821
|
-
| export
|
|
10822
|
-
|
|
|
10823
|
-
| `foldedSpanFor(conversation, message)`
|
|
10824
|
-
| `foldedMessages(conversation)`
|
|
10825
|
-
| `FoldedSpan` · `CompactionRetention` · `FoldedConversation` | The types.
|
|
11037
|
+
| export | what it is |
|
|
11038
|
+
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11039
|
+
| `foldedSpanFor(conversation, message)` | The span behind one summary, joined by **content fingerprint** rather than index — a later fold swallows an earlier summary and every index after it moves. `undefined` means "no fold was recorded for this message", never "there were no originals". |
|
|
11040
|
+
| `foldedMessages(conversation)` | Every retained message from every span, oldest fold first. |
|
|
11041
|
+
| `FoldedSpan` · `CompactionRetention` · `FoldedConversation` | The types. |
|
|
10826
11042
|
|
|
10827
11043
|
The fingerprint is also what makes the join **forgery-proof**: `isCompactedSummary`
|
|
10828
|
-
answers "this
|
|
11044
|
+
answers "this _looks_ like a frame", which is all a prefix check can see, and a
|
|
10829
11045
|
model that copies the frame's opening words passes it. `foldedSpanFor` answers
|
|
10830
11046
|
the stronger question — different content, different fingerprint, no match.
|
|
10831
11047
|
|
|
@@ -10886,14 +11102,14 @@ openai({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' });
|
|
|
10886
11102
|
|
|
10887
11103
|
Two things go wrong with a local runtime, and each has a one-command answer. Both
|
|
10888
11104
|
now raise a typed `OllamaUnavailableError` (discriminated by `reason`) whose
|
|
10889
|
-
message
|
|
11105
|
+
message _is_ the instruction — never a raw `ECONNREFUSED`, never a bare `404`,
|
|
10890
11106
|
and never a hang: a deadline bounds the wait for the daemon to answer (not
|
|
10891
11107
|
generation, so a slow model is untouched).
|
|
10892
11108
|
|
|
10893
11109
|
- **Daemon not running** — names the address it tried, `ollama serve`, the
|
|
10894
11110
|
install link, and how to point somewhere else.
|
|
10895
11111
|
- **Model not pulled** — names `ollama pull <model>`, and asks `/api/tags` so it
|
|
10896
|
-
can also list what this machine
|
|
11112
|
+
can also list what this machine _does_ have.
|
|
10897
11113
|
|
|
10898
11114
|
### Also in this release
|
|
10899
11115
|
|
|
@@ -10902,7 +11118,7 @@ generation, so a slow model is untouched).
|
|
|
10902
11118
|
compaction and cost budgets work against a local model.
|
|
10903
11119
|
- **Thinking blocks from local reasoning models.** `ollama('deepseek-r1', { think: true })`
|
|
10904
11120
|
asks Ollama to lift reasoning out of the answer; the new `ollamaThinkingHandler`
|
|
10905
|
-
auto-wires by provider name and normalizes it. When a model was
|
|
11121
|
+
auto-wires by provider name and normalizes it. When a model was _not_ asked and
|
|
10906
11122
|
writes `<think>…</think>` into the answer instead, the library **recognizes the
|
|
10907
11123
|
shape and surfaces the blocks, but does not edit the answer** — silently
|
|
10908
11124
|
rewriting model output is a change of meaning, and that belongs to the
|
|
@@ -10948,32 +11164,32 @@ The doors are now named for the job:
|
|
|
10948
11164
|
|
|
10949
11165
|
**Every old import path still works, unchanged, for all of 8.x.** They are
|
|
10950
11166
|
marked `@deprecated` so your editor points at the new door; nothing is logged,
|
|
10951
|
-
nothing breaks, and each one re-exports the
|
|
11167
|
+
nothing breaks, and each one re-exports the _same symbols_ the door carries —
|
|
10952
11168
|
not copies. `test/api-conformance/door-aliases.test.ts` drives the TypeScript
|
|
10953
11169
|
checker over the shipped `.d.ts` files to prove it, name by name, so the
|
|
10954
11170
|
aliases cannot drift. They are removed in 9.0.0.
|
|
10955
11171
|
|
|
10956
11172
|
### Migration
|
|
10957
11173
|
|
|
10958
|
-
| you were importing from
|
|
10959
|
-
|
|
10960
|
-
| `agentfootprint/llm-providers`
|
|
10961
|
-
| `agentfootprint/embedders`
|
|
10962
|
-
| `agentfootprint/tool-providers`
|
|
10963
|
-
| `agentfootprint/thinking`
|
|
10964
|
-
| `agentfootprint/memory-providers`
|
|
10965
|
-
| `agentfootprint/observability-providers`
|
|
10966
|
-
| `agentfootprint/strategies`
|
|
10967
|
-
| `agentfootprint/stream`
|
|
10968
|
-
| `agentfootprint/status`
|
|
10969
|
-
| `agentfootprint/locales`
|
|
10970
|
-
| `agentfootprint/debug`
|
|
10971
|
-
| `agentfootprint/debug/finders`
|
|
10972
|
-
| `agentfootprint/observability/contextError/finders` | `agentfootprint/observe`
|
|
10973
|
-
| `agentfootprint/reliability`
|
|
10974
|
-
| `agentfootprint/hosting-providers`
|
|
10975
|
-
| `agentfootprint/injection-engine`
|
|
10976
|
-
| `agentfootprint/identity`
|
|
11174
|
+
| you were importing from | import from |
|
|
11175
|
+
| --------------------------------------------------- | --------------------------- |
|
|
11176
|
+
| `agentfootprint/llm-providers` | `agentfootprint/providers` |
|
|
11177
|
+
| `agentfootprint/embedders` | `agentfootprint/providers` |
|
|
11178
|
+
| `agentfootprint/tool-providers` | `agentfootprint/providers` |
|
|
11179
|
+
| `agentfootprint/thinking` | `agentfootprint/providers` |
|
|
11180
|
+
| `agentfootprint/memory-providers` | `agentfootprint/memory` |
|
|
11181
|
+
| `agentfootprint/observability-providers` | `agentfootprint/observe` |
|
|
11182
|
+
| `agentfootprint/strategies` | `agentfootprint/observe` |
|
|
11183
|
+
| `agentfootprint/stream` | `agentfootprint/observe` |
|
|
11184
|
+
| `agentfootprint/status` | `agentfootprint/observe` |
|
|
11185
|
+
| `agentfootprint/locales` | `agentfootprint/observe` |
|
|
11186
|
+
| `agentfootprint/debug` | `agentfootprint/observe` |
|
|
11187
|
+
| `agentfootprint/debug/finders` | `agentfootprint/observe` |
|
|
11188
|
+
| `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
|
|
11189
|
+
| `agentfootprint/reliability` | `agentfootprint/resilience` |
|
|
11190
|
+
| `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
|
|
11191
|
+
| `agentfootprint/injection-engine` | `agentfootprint/context` |
|
|
11192
|
+
| `agentfootprint/identity` | `agentfootprint/security` |
|
|
10977
11193
|
|
|
10978
11194
|
`agentfootprint`, `agentfootprint/memory`, `agentfootprint/observe`,
|
|
10979
11195
|
`agentfootprint/security`, `agentfootprint/hosting` and
|
|
@@ -10987,7 +11203,7 @@ aliases cannot drift. They are removed in 9.0.0.
|
|
|
10987
11203
|
executing those registrations and carrying them in every bundle. Side-effectful
|
|
10988
11204
|
code stays behind its own plainly-named door.
|
|
10989
11205
|
- **`agentfootprint/events`** is not folded into `/observe`. It is the typed
|
|
10990
|
-
wire vocabulary observers
|
|
11206
|
+
wire vocabulary observers _read_, not a tool for watching — and concretely,
|
|
10991
11207
|
its `ContextSource` (the injection-flavour union: `'rag' | 'skill' | …`) is a
|
|
10992
11208
|
completely different type from the `ContextSource` `/observe` already carries
|
|
10993
11209
|
(the context-bisect record). Two incompatible shapes cannot share a door.
|
|
@@ -11032,7 +11248,7 @@ runtime door, and it still returns the `Unsubscribe` you own.
|
|
|
11032
11248
|
`CombinedRecorder` keeps its export too.
|
|
11033
11249
|
|
|
11034
11250
|
There is deliberately **no** `WATCH_MOMENTS`. `.act()`'s keys are a closed,
|
|
11035
|
-
compiler-pinned list because a rule has to be
|
|
11251
|
+
compiler-pinned list because a rule has to be _told_ where it may speak; an
|
|
11036
11252
|
observer attends the whole stream, and a list we published would be a
|
|
11037
11253
|
vocabulary we then had to keep true against every event ever added.
|
|
11038
11254
|
|
|
@@ -11068,7 +11284,7 @@ vocabulary we then had to keep true against every event ever added.
|
|
|
11068
11284
|
## [7.28.0] - 2026-08-05
|
|
11069
11285
|
|
|
11070
11286
|
A paused agent is a promise you made to a person. Until this release the library
|
|
11071
|
-
handed you that promise as JSON and wished you luck:
|
|
11287
|
+
handed you that promise as JSON and wished you luck: _store it anywhere._
|
|
11072
11288
|
Anywhere was the whole of the offer.
|
|
11073
11289
|
|
|
11074
11290
|
`sqliteSessions({ file })` is the first battery included — the same
|
|
@@ -11091,7 +11307,7 @@ and the next step up was "bring a Redis" — a service to run, secure, back up a
|
|
|
11091
11307
|
pay for, to keep a few kilobytes of chat. Everyone in between wrote the same
|
|
11092
11308
|
little file store themselves and each one re-decided what a half-written file
|
|
11093
11309
|
means. A pause had no home at all: a question outstanding is the one piece of
|
|
11094
|
-
agent state that
|
|
11310
|
+
agent state that _must_ outlive the process, because the answer arrives on human
|
|
11095
11311
|
time — after lunch, after the deploy, tomorrow. Both land in one table here,
|
|
11096
11312
|
because `CheckpointEnvelope` was already a union of the two and a session store
|
|
11097
11313
|
has no business caring which half it is holding.
|
|
@@ -11101,8 +11317,7 @@ process (or a few) on ONE machine, writing ONE file. It survives anything that
|
|
|
11101
11317
|
ends the process and leaves the disk alone. It is **not** a distributed store:
|
|
11102
11318
|
two machines do not share a session by both opening a file over a network
|
|
11103
11319
|
filesystem. WAL gives many readers plus **one writer at a time**, and that is
|
|
11104
|
-
the ceiling — a second writer waits for the lock up to `busyTimeoutMs` (default
|
|
11105
|
-
5000) and then fails loudly rather than queueing forever. When you outgrow it,
|
|
11320
|
+
the ceiling — a second writer waits for the lock up to `busyTimeoutMs` (default 5000) and then fails loudly rather than queueing forever. When you outgrow it,
|
|
11106
11321
|
one argument to `standingAgent` changes and nothing above it moves.
|
|
11107
11322
|
|
|
11108
11323
|
**A refusal where a fallback would have been easier.** `node:sqlite` ships with
|
|
@@ -11118,7 +11333,7 @@ user.
|
|
|
11118
11333
|
**"Unreadable is not absent", one level up.** The envelope law already said an
|
|
11119
11334
|
unreadable stored conversation and an absent one are different facts, and only
|
|
11120
11335
|
one is safe to answer with a fresh start. A file store can break that promise
|
|
11121
|
-
higher up — point it at a log file and a careless adapter opens it as an
|
|
11336
|
+
higher up — point it at a log file and a careless adapter opens it as an _empty_
|
|
11122
11337
|
store. So the file is checked at construction and refused with
|
|
11123
11338
|
`UnreadableSessionFileError`, whose `problem` field is the fact to branch on:
|
|
11124
11339
|
`'cannot-open'`, `'not-our-schema'` (somebody else's table of that name), or
|
|
@@ -11129,7 +11344,7 @@ session that was never written hydrates as `undefined`.
|
|
|
11129
11344
|
well as fields inside the JSON, so during an incident `sqlite3` answers "which
|
|
11130
11345
|
sessions are waiting on a person, and since when?" with no JSON parser and
|
|
11131
11346
|
without this library. `journalMode` on the returned store reports what the file
|
|
11132
|
-
|
|
11347
|
+
_actually got_ rather than what was asked for — a silent downgrade from WAL on a
|
|
11133
11348
|
network filesystem is the kind of thing only ever discovered under load.
|
|
11134
11349
|
|
|
11135
11350
|
Added, all on the existing `agentfootprint/hosting` door — no new subpath:
|
|
@@ -11161,7 +11376,7 @@ the machine took, and the machine is shared: the suite runs beside a build, a
|
|
|
11161
11376
|
coverage pass, and two other vitest workers. Identical code takes three to five
|
|
11162
11377
|
times longer under that load with nothing about the code having changed. So the
|
|
11163
11378
|
assertion cannot tell "we got slower" from "the box was busy", and it fires
|
|
11164
|
-
|
|
11379
|
+
_exactly_ when CI is busiest. Five of them had already been logged as flakes —
|
|
11165
11380
|
`xray` P6, `withCircuitBreaker` P6, `locales/messages` Block D,
|
|
11166
11381
|
`consumer-domain-events`, `SkillRegistryOptions` — always under concurrent
|
|
11167
11382
|
build load, always passing in isolation. The failure mode of a guard nobody
|
|
@@ -11205,7 +11420,7 @@ still flaked when actually run under the reproduction condition. Three things
|
|
|
11205
11420
|
had to be added, each because the proof run said so:
|
|
11206
11421
|
|
|
11207
11422
|
- **Repeat until the sample is worth timing.** Below a scheduler quantum, one
|
|
11208
|
-
preemption
|
|
11423
|
+
preemption _is_ the measurement: a 0.2ms operation that gets descheduled
|
|
11209
11424
|
reads as a hundred times its real cost, while the 100ms operation beside it
|
|
11210
11425
|
absorbs the same theft as a rounding error — and load stops cancelling. Each
|
|
11211
11426
|
operation is now repeated inside one sample until the sample clears 20ms, and
|
|
@@ -11237,8 +11452,8 @@ load, which is what makes them worth asserting.
|
|
|
11237
11452
|
**No perf claim was deleted. The form changed; the meaning stayed** — and in
|
|
11238
11453
|
four places the meaning got sharper, because writing the claim down properly
|
|
11239
11454
|
exposed what it had actually been asserting. Three sites keep a millisecond
|
|
11240
|
-
ceiling on purpose and say so at the site: they are stated against a
|
|
11241
|
-
|
|
11455
|
+
ceiling on purpose and say so at the site: they are stated against a _configured
|
|
11456
|
+
delay_ (a mock's own thinking band, a slow branch's own timeout, a strategy's
|
|
11242
11457
|
own per-event block) rather than against a guess about the machine, because
|
|
11243
11458
|
"did not sleep longer than it was told to" has no cheaper form.
|
|
11244
11459
|
|
|
@@ -11394,8 +11609,8 @@ each of those is now the failure of the thing that caused it. A 400, a 500, one
|
|
|
11394
11609
|
refused upgrade, one ended conversation. Never the process.
|
|
11395
11610
|
|
|
11396
11611
|
One of them was not even a throw. `serveOne`'s promise is held in a Set and
|
|
11397
|
-
voided at the call site, so anything that escaped it was an
|
|
11398
|
-
|
|
11612
|
+
voided at the call site, so anything that escaped it was an _unhandled
|
|
11613
|
+
rejection_ — which on node's defaults is the same dead container reached by a
|
|
11399
11614
|
different road. It is total by construction now, and says so.
|
|
11400
11615
|
|
|
11401
11616
|
**The audit's other finding is that the conversation door was already safe, and
|
|
@@ -11412,17 +11627,16 @@ serve. It is a great deal of ceremony when all you wanted was a `/debug/trace`
|
|
|
11412
11627
|
beside the agent on the one port the container was given.
|
|
11413
11628
|
|
|
11414
11629
|
```ts
|
|
11415
|
-
nodeHost({ port: 8080, onUnhandled: (req, res) => myRouter(req, res) })
|
|
11630
|
+
nodeHost({ port: 8080, onUnhandled: (req, res) => myRouter(req, res) });
|
|
11416
11631
|
```
|
|
11417
11632
|
|
|
11418
11633
|
Same single port, opposite direction: the host binds the socket as it always
|
|
11419
|
-
did, and every path it does not own is handed to your code **instead of** its
|
|
11420
|
-
404. The host still never answers for your application — with this hook it no
|
|
11634
|
+
did, and every path it does not own is handed to your code **instead of** its 404. The host still never answers for your application — with this hook it no
|
|
11421
11635
|
longer has to 404 for it either.
|
|
11422
11636
|
|
|
11423
11637
|
What it never receives is the interesting half. The paths the host owns —
|
|
11424
|
-
`invokePath`, `healthPath`, `conversationPath` — never reach it,
|
|
11425
|
-
wrong method on one of
|
|
11638
|
+
`invokePath`, `healthPath`, `conversationPath` — never reach it, _including a
|
|
11639
|
+
wrong method on one of them_, because a hook that could claim `POST /invoke`
|
|
11426
11640
|
would be a second door wearing the first one's name. And it is refused at
|
|
11427
11641
|
construction beside `{ server }`, by name: there, unmatched paths already fall
|
|
11428
11642
|
through to your own `'request'` listeners, so a second way to answer them would
|
|
@@ -11469,7 +11683,7 @@ hosts rather than by care.
|
|
|
11469
11683
|
chunks are coerced back to bytes, and no bytes are lost doing it: `setEncoding`
|
|
11470
11684
|
decodes through a `StringDecoder`, which holds a partial multi-byte sequence
|
|
11471
11685
|
across a chunk boundary rather than splitting it. Pinned by writing a body in
|
|
11472
|
-
two TCP writes with the split placed
|
|
11686
|
+
two TCP writes with the split placed _inside_ a four-byte character and
|
|
11473
11687
|
asserting it round-trips. Reachable only through `{ server }` — the mode built
|
|
11474
11688
|
for co-listeners — and reproduced there, with a real second listener on a real
|
|
11475
11689
|
shared socket rather than a stubbed request.
|
|
@@ -11558,7 +11772,7 @@ avoid admitting the cost would have been an accounting trick.
|
|
|
11558
11772
|
**The corrective message is an authored frame with the validator's error as
|
|
11559
11773
|
DATA.** The library's own words come first and say that what follows is a
|
|
11560
11774
|
report about the answer rather than an instruction; the error is quoted
|
|
11561
|
-
verbatim; and
|
|
11775
|
+
verbatim; and _nothing authored follows it_, so there is no trailing sentence
|
|
11562
11776
|
for injected text to pre-empt. A schema whose error message reads "IGNORE ALL
|
|
11563
11777
|
PREVIOUS INSTRUCTIONS" produces a message that still says, first and in the
|
|
11564
11778
|
library's voice, what it is. This is exactly the compaction frame's rule
|
|
@@ -11637,7 +11851,7 @@ bytes — pinned against 7.25 by test, not by care.
|
|
|
11637
11851
|
|
|
11638
11852
|
- **`agentfootprint.agent.output_schema_retry`** — one per failed attempt,
|
|
11639
11853
|
carrying `{ attempt, retriesRemaining, iteration, stage, error, path?,
|
|
11640
|
-
|
|
11854
|
+
correctiveMessageHash }`. 69 typed events across 20 domains. It sits in the
|
|
11641
11855
|
`agent` domain beside `output_schema_validation_failed`, its in-stage
|
|
11642
11856
|
sibling; a new domain for one event that has a family home would have been
|
|
11643
11857
|
taxonomy for its own sake.
|
|
@@ -11698,8 +11912,9 @@ side can call. So this release ships a second port beside the first.
|
|
|
11698
11912
|
```ts
|
|
11699
11913
|
const host = nodeHost({ port: 8080 });
|
|
11700
11914
|
|
|
11701
|
-
await standingAgent({ agent, sessions, host });
|
|
11702
|
-
await host.serveConversations((conversation) => {
|
|
11915
|
+
await standingAgent({ agent, sessions, host }); // POST /invoke
|
|
11916
|
+
await host.serveConversations((conversation) => {
|
|
11917
|
+
// WS /conversation
|
|
11703
11918
|
conversation.onFrame((frame) => conversation.send(answer(frame)));
|
|
11704
11919
|
conversation.onClose(({ by, reason }) => log(by, reason));
|
|
11705
11920
|
});
|
|
@@ -11868,11 +12083,11 @@ So this release does two things, and the second is the reason for the first.
|
|
|
11868
12083
|
```ts
|
|
11869
12084
|
Agent.create({ provider, model })
|
|
11870
12085
|
.act({
|
|
11871
|
-
input:
|
|
11872
|
-
beforeTool: [refundCeiling, fourEyes],
|
|
11873
|
-
afterTool:
|
|
11874
|
-
window:
|
|
11875
|
-
output:
|
|
12086
|
+
input: [scrubSSNs], // the message, before the run commits it
|
|
12087
|
+
beforeTool: [refundCeiling, fourEyes], // every call, before it is dispatched
|
|
12088
|
+
afterTool: [stripPII], // every result, before the model reads it
|
|
12089
|
+
window: slidingWindow({ keepRecentTurns: 12 }), // what the live window keeps
|
|
12090
|
+
output: [noCodenames], // the answer, before the caller gets it
|
|
11876
12091
|
})
|
|
11877
12092
|
.build();
|
|
11878
12093
|
```
|
|
@@ -11886,8 +12101,8 @@ wire, same rows in the ledger.
|
|
|
11886
12101
|
|
|
11887
12102
|
The canonical path is preserved by **demoting the doors, not deleting them**.
|
|
11888
12103
|
`.toolMiddleware()`, `.messageMiddleware()`, `.window()` and `.compaction()`
|
|
11889
|
-
are unchanged and stay open, and they are now documented under
|
|
11890
|
-
|
|
12104
|
+
are unchanged and stay open, and they are now documented under _Composing
|
|
12105
|
+
incrementally_ — because adding one rule to an agent somebody else built is a
|
|
11891
12106
|
real job, and a bundle that must be written all at once cannot do it. That
|
|
11892
12107
|
division is the one-sentence answer to "which spelling": **`.act()` for an
|
|
11893
12108
|
agent you own, a door for a piece you are adding to somebody else's.** A second
|
|
@@ -12136,8 +12351,8 @@ the honest pick is the one whose bytes come back unchanged.
|
|
|
12136
12351
|
**The law, which is the part that outlives this vendor.** In the words of the
|
|
12137
12352
|
field report that bought it:
|
|
12138
12353
|
|
|
12139
|
-
>
|
|
12140
|
-
> only one of them is safe to answer with a fresh start
|
|
12354
|
+
> _An unreadable stored conversation and an absent one are different facts, and
|
|
12355
|
+
> only one of them is safe to answer with a fresh start._
|
|
12141
12356
|
|
|
12142
12357
|
A session nobody has used is absent, and answering it fresh is right. A session
|
|
12143
12358
|
whose bytes are present and unreadable is not, and answering THAT fresh is
|
|
@@ -12155,7 +12370,7 @@ through to the fresh-start path.
|
|
|
12155
12370
|
meant reading how this repo writes an event blob, and `AgentCoreStore` — the
|
|
12156
12371
|
`MemoryStore` adapter — wrote them identically: `payload: [{ blob: entry }]`,
|
|
12157
12372
|
read back as objects only. Same service, same mangling, same silence, different
|
|
12158
|
-
loss: an entry that decodes to nothing was
|
|
12373
|
+
loss: an entry that decodes to nothing was _skipped_, so `list()` came back one
|
|
12159
12374
|
memory short and `get()` came back `null`. Memory that silently stays empty is
|
|
12160
12375
|
indistinguishable from memory that works, until somebody notices the assistant
|
|
12161
12376
|
has forgotten a customer's address. Shipping the cure for one organ while the
|
|
@@ -12198,8 +12413,8 @@ and are still described that way.
|
|
|
12198
12413
|
refused loudly rather than decoded to `undefined`.
|
|
12199
12414
|
|
|
12200
12415
|
- **`hydrate` no longer answers "no session" for a session that HAS one.** The
|
|
12201
|
-
adapter's decode step now distinguishes
|
|
12202
|
-
hydrates as `undefined`) from
|
|
12416
|
+
adapter's decode step now distinguishes _no blob at all_ (an absence, which
|
|
12417
|
+
hydrates as `undefined`) from _a blob it cannot read_ (which travels on to the
|
|
12203
12418
|
shared reading law and is refused by name). Both file and event modes pass the
|
|
12204
12419
|
session id into `checkEnvelope`, so a refusal names the conversation.
|
|
12205
12420
|
|
|
@@ -12348,7 +12563,7 @@ than by an opinion.** See below.
|
|
|
12348
12563
|
satisfy the container contract this repo documents for a managed agent
|
|
12349
12564
|
runtime, and is not trying to.** It serves MCP — statelessly (it neither
|
|
12350
12565
|
issues nor demands a session id, so replicas are interchangeable), on the path
|
|
12351
|
-
and port you choose — and it answers
|
|
12566
|
+
and port you choose — and it answers _neither_ of the contract's two routes:
|
|
12352
12567
|
`GET /ping` and `POST /invocations` are 404s from it. Two protocols, two
|
|
12353
12568
|
paths, two adapters: serve the container contract with
|
|
12354
12569
|
`agentCoreRuntimeHost` and MCP with `mcpServe`. The one thing that is NOT
|
|
@@ -12363,7 +12578,7 @@ The refusal becomes acceptance.
|
|
|
12363
12578
|
Three releases ago the messages slot was a lie: content declared for it was
|
|
12364
12579
|
recorded as injected, counted in the slot composition, routed by the engine, and
|
|
12365
12580
|
never sent. 7.19.1 refused the declaration by name rather than deliver it badly,
|
|
12366
|
-
and said why in the same breath — the wire has no system role
|
|
12581
|
+
and said why in the same breath — the wire has no system role _inside_ the
|
|
12367
12582
|
message list on the Anthropic family (system is a separate top-level field)
|
|
12368
12583
|
while the OpenAI family carries it, so wiring the slot straight through would
|
|
12369
12584
|
have replaced one uniform gap with a **provider-dependent** one that nothing in
|
|
@@ -12388,7 +12603,7 @@ refused when the run starts, naming the provider and the roles it does. The role
|
|
|
12388
12603
|
is never rewritten to one that fits — changing who appears to speak is a meaning
|
|
12389
12604
|
change the app must make, not the library. **Position**: a delivered message goes
|
|
12390
12605
|
at the end of the window, and if its role would repeat the turn already there, it
|
|
12391
|
-
is
|
|
12606
|
+
is _deferred_ to the next boundary with a sentence on
|
|
12392
12607
|
`messagesDelivery.deferred`, never dropped and never reordered, and never
|
|
12393
12608
|
inserted between a tool call and its result.
|
|
12394
12609
|
|
|
@@ -12444,7 +12659,7 @@ end. An honest limitation stated loudly beats a clever one hidden.
|
|
|
12444
12659
|
### Fixed
|
|
12445
12660
|
|
|
12446
12661
|
- **The cache marker for `field: 'messages'` pointed at the wrong message.** It
|
|
12447
|
-
counted entries in a per-slot list of
|
|
12662
|
+
counted entries in a per-slot list of _injections_ and handed that count to
|
|
12448
12663
|
providers who read it as a position in `request.messages` — two index spaces
|
|
12449
12664
|
under one name. It was unreachable while nothing could target the slot, and
|
|
12450
12665
|
delivery makes it reachable, so it is recomputed against the actual wire array
|
|
@@ -12532,7 +12747,6 @@ end. An honest limitation stated loudly beats a clever one hidden.
|
|
|
12532
12747
|
request's tool array. It is out of this release's scope, and it is now the only
|
|
12533
12748
|
one of the pair left.
|
|
12534
12749
|
|
|
12535
|
-
|
|
12536
12750
|
## [7.20.0] - 2026-08-03
|
|
12537
12751
|
|
|
12538
12752
|
Three small honesty fixes. No new machinery, no delivery change, no wire bytes
|
|
@@ -12642,7 +12856,7 @@ message-sequence rule; it is queued as a feature, with this gap as its evidence.
|
|
|
12642
12856
|
/ `.instruction` / `.fact` all pass through — refuses a hand-built `Injection`
|
|
12643
12857
|
carrying `inject.messages`, so the refusal cannot be walked around. The
|
|
12644
12858
|
message names the limitation and the working alternatives: `slot:
|
|
12645
|
-
|
|
12859
|
+
'system-prompt'` (the default, delivered by every provider), a tool's return
|
|
12646
12860
|
value (a tool result IS a recent message, at the recency the option was
|
|
12647
12861
|
reaching for), and the text passed to `agent.run({ message })`.
|
|
12648
12862
|
|
|
@@ -12850,7 +13064,7 @@ Example: `examples/deploy/durable-sessions.ts`.
|
|
|
12850
13064
|
## [7.18.0] - 2026-08-03
|
|
12851
13065
|
|
|
12852
13066
|
Every agent framework lets you wrap a tool call. Most of them let the wrapper
|
|
12853
|
-
|
|
13067
|
+
_answer_ — return a canned string, a cached value, a "simulated" result — and
|
|
12854
13068
|
the moment one does, the trace is fiction. The model was told a tool ran.
|
|
12855
13069
|
Nothing ran.
|
|
12856
13070
|
|
|
@@ -12909,7 +13123,7 @@ taken afterwards: the trace would show text nobody ever sent.
|
|
|
12909
13123
|
|
|
12910
13124
|
- **`ask` suspends on the SHIPPED pause machinery.** `isAskPause(outcome)`
|
|
12911
13125
|
narrows a paused run and `outcome.ask` carries `{ question, detail?,
|
|
12912
|
-
|
|
13126
|
+
middleware }`. Resume with `checkInApproved` / `checkInDeclined` — the same
|
|
12913
13127
|
human-answer vocabulary check-ins use, deliberately, because a person
|
|
12914
13128
|
approving is a person approving and one word for one thing beats a synonym.
|
|
12915
13129
|
A malformed resume DECLINES, so a governed call can never execute because a
|
|
@@ -13046,7 +13260,7 @@ measured lifetime. Removing is not forgetting.
|
|
|
13046
13260
|
token budget, and reporting a `capTokens` nobody configured would be exactly
|
|
13047
13261
|
the invented number this family refuses.
|
|
13048
13262
|
|
|
13049
|
-
`keepRecentTurns` is required and has no default. It
|
|
13263
|
+
`keepRecentTurns` is required and has no default. It _is_ the policy.
|
|
13050
13264
|
|
|
13051
13265
|
- **`tokenBudget({ thresholdTokens, keepRecentTurns? })` — counted, then
|
|
13052
13266
|
dropped.** Reads the input tokens the adapter reported for the last call and
|
|
@@ -13094,7 +13308,7 @@ measured lifetime. Removing is not forgetting.
|
|
|
13094
13308
|
It appears only at the head (a removal in the middle leaves the opening turn
|
|
13095
13309
|
in place, so there is nothing to fix and a spliced `user` message is its own
|
|
13096
13310
|
risk); it never accumulates (the next drop absorbs it); and if it would not
|
|
13097
|
-
be
|
|
13311
|
+
be _smaller_ than the span it replaces, the whole drop is abandoned under
|
|
13098
13312
|
`summary-not-smaller`, whose meaning generalizes to "the replacement came
|
|
13099
13313
|
back no smaller than the span" rather than growing the closed reason union.
|
|
13100
13314
|
|
|
@@ -13188,7 +13402,7 @@ arrived yet.
|
|
|
13188
13402
|
Example: `examples/context-engineering/11-compaction.ts`.
|
|
13189
13403
|
|
|
13190
13404
|
- **`CompactionRecord` on `scope.compactions` — the fold's half of the law.**
|
|
13191
|
-
One record per over-budget visit,
|
|
13405
|
+
One record per over-budget visit, _including the visits that folded nothing_,
|
|
13192
13406
|
which are the interesting ones. It carries `foldedStageIds` (real
|
|
13193
13407
|
`runtimeStageId`s, resolvable in the commit log), `foldedMessageCount`,
|
|
13194
13408
|
`measuredTokens` vs `thresholdTokens`, exact `windowCharsBefore` /
|
|
@@ -13212,7 +13426,7 @@ arrived yet.
|
|
|
13212
13426
|
- **`COMPACTED_FRAME_PREFIX` / `isCompactedSummary(msg)`** — the authored frame
|
|
13213
13427
|
is a library constant and the summarizer's text is appended after it as data.
|
|
13214
13428
|
A summarizer returning `IGNORE ALL PREVIOUS INSTRUCTIONS` still arrives
|
|
13215
|
-
|
|
13429
|
+
_inside_ a message that says, first and in the library's own words, that what
|
|
13216
13430
|
follows is a summary written by a model and not the conversation. A test pins
|
|
13217
13431
|
exactly that, with a hostile summarizer. The boundary points both ways: the
|
|
13218
13432
|
folded transcript reaches the summarizer between markers the authored
|
|
@@ -13242,7 +13456,7 @@ arrived yet.
|
|
|
13242
13456
|
- **With `.compaction()` configured, the compaction stage becomes the ReAct
|
|
13243
13457
|
loop target** (`compact`), mounted immediately before the previous one. The
|
|
13244
13458
|
loop is branch-sourced, so anything ahead of the target runs once and is
|
|
13245
|
-
never seen again — and being the target puts the fold
|
|
13459
|
+
never seen again — and being the target puts the fold _before_ the injection
|
|
13246
13460
|
engine and the three context slots, which is the point: the triggers, the
|
|
13247
13461
|
slots and the wire then all see one window, and no part of the run reasons
|
|
13248
13462
|
over a past the model was not shown. Without `.compaction()` the loop target
|
|
@@ -13259,9 +13473,9 @@ arrived yet.
|
|
|
13259
13473
|
## [7.15.0] - 2026-08-02
|
|
13260
13474
|
|
|
13261
13475
|
7.14.0 shipped two hosting ports that name no cloud, plus a conformance suite,
|
|
13262
|
-
and made a promise:
|
|
13476
|
+
and made a promise: _a cloud adapter is vendor paths and a header mapping on a
|
|
13263
13477
|
port that already worked; if writing one needs a change to a port, the port was
|
|
13264
|
-
wrong
|
|
13478
|
+
wrong._ A promise like that is worth nothing until somebody writes the adapter.
|
|
13265
13479
|
|
|
13266
13480
|
This release writes it. `agentCoreRuntimeHost` is a real cloud runtime's
|
|
13267
13481
|
container contract — different paths, different body fields, the conversation id
|
|
@@ -13275,8 +13489,8 @@ policy store behind the existing permission port, per-request credential vending
|
|
|
13275
13489
|
for Gateway tools, and the memory adapter's `search()` finally wired.
|
|
13276
13490
|
|
|
13277
13491
|
Three seams did have to move, and none of them was a port. They are listed under
|
|
13278
|
-
"Changed" rather than buried, because
|
|
13279
|
-
and
|
|
13492
|
+
"Changed" rather than buried, because _where an adapter needs more than paths
|
|
13493
|
+
and headers_ is the interesting result of an exercise like this — and two of the
|
|
13280
13494
|
three turned out not to be about this vendor at all.
|
|
13281
13495
|
|
|
13282
13496
|
### Added
|
|
@@ -13308,7 +13522,7 @@ three turned out not to be about this vendor at all.
|
|
|
13308
13522
|
Example: `examples/deploy/agentcore-runtime.ts`.
|
|
13309
13523
|
|
|
13310
13524
|
- **`httpHost({ name, wire, invokePath, healthPath, port?, hostname?,
|
|
13311
|
-
|
|
13525
|
+
capabilities? })` — the HTTP work, parameterised by the JSON dialect it
|
|
13312
13526
|
speaks.** Draining on close, aborting when the caller hangs up, failing a
|
|
13313
13527
|
handler that throws, failing a handler that answers nothing, mapping refusal
|
|
13314
13528
|
codes to status codes, and choosing between one JSON body and Server-Sent
|
|
@@ -13323,7 +13537,7 @@ three turned out not to be about this vendor at all.
|
|
|
13323
13537
|
matching and gets it subtly wrong in exactly one deployment.
|
|
13324
13538
|
|
|
13325
13539
|
- **`agentCorePolicy({ policyStoreId, region?, onUnavailable?, onWarning?,
|
|
13326
|
-
|
|
13540
|
+
principalFor?, name?, cacheSize? })` — an AgentCore policy store behind the
|
|
13327
13541
|
existing `PermissionChecker` port** (`agentfootprint/security`). Every
|
|
13328
13542
|
attempted tool call becomes one evaluation.
|
|
13329
13543
|
|
|
@@ -13371,7 +13585,7 @@ three turned out not to be about this vendor at all.
|
|
|
13371
13585
|
|
|
13372
13586
|
Results are marked `metadata.source: 'agentcore-memory-record'`, because
|
|
13373
13587
|
`search` reads a genuinely different population than `list`: the records
|
|
13374
|
-
AgentCore's extraction strategies
|
|
13588
|
+
AgentCore's extraction strategies _derived from_ your events, whose ids belong
|
|
13375
13589
|
to AgentCore, so `store.get(result.entry.id)` will not find them.
|
|
13376
13590
|
|
|
13377
13591
|
There is **no `stream()`**. AgentCore Memory has no streaming data-plane
|
|
@@ -13488,7 +13702,7 @@ and if writing one ever needs a change to a port, the port was wrong.
|
|
|
13488
13702
|
- **`standingAgent({ agent, sessions, host, onConcurrentInvoke? })` — the
|
|
13489
13703
|
composer.** Per request: wake and hydrate the session, resume that
|
|
13490
13704
|
conversation or start a fresh one, persist what the run left behind, reply.
|
|
13491
|
-
Persist happens
|
|
13705
|
+
Persist happens _before_ the answer goes out, so a queued next turn can never
|
|
13492
13706
|
read state older than the answer already given.
|
|
13493
13707
|
|
|
13494
13708
|
It restates the `resumeOnError` tool re-execution caveat **verbatim** in its
|
|
@@ -13502,7 +13716,7 @@ and if writing one ever needs a change to a port, the port was wrong.
|
|
|
13502
13716
|
afterwards belongs to whichever started last, so one session's envelope can end
|
|
13503
13717
|
up holding another session's conversation with nothing in the recording to say
|
|
13504
13718
|
so. `ConcurrentInvokePolicy` is the separate question of a second turn of the
|
|
13505
|
-
|
|
13719
|
+
_same_ conversation: `'reject'` (default) refuses with a `ConcurrentRunError`
|
|
13506
13720
|
naming the active run (`409`), `'enqueue'` queues it FIFO behind the run whose
|
|
13507
13721
|
state it will then read. A request for a **different** session is never
|
|
13508
13722
|
refused — it waits its turn.
|
|
@@ -13543,7 +13757,7 @@ person something, `standingAgent` answers with a `PauseNotCarriedError` and
|
|
|
13543
13757
|
writes **nothing** — the session keeps exactly the conversation it had before the
|
|
13544
13758
|
request. Over HTTP that is a `409`, not a `500`, because the agent did not break
|
|
13545
13759
|
and every dashboard that sees a 500 will conclude otherwise. `'conversation-v1'`
|
|
13546
|
-
stores a conversation; a paused run is a conversation
|
|
13760
|
+
stores a conversation; a paused run is a conversation _plus_ an engine
|
|
13547
13761
|
checkpoint, and storing half of it would be worse than storing none. Carrying a
|
|
13548
13762
|
pause would be a NEW format name in the same envelope — which is precisely what
|
|
13549
13763
|
the version in the format is for.
|
|
@@ -13558,7 +13772,7 @@ host can send any string there, including someone else's.
|
|
|
13558
13772
|
|
|
13559
13773
|
**The conformance suite is the deliverable, not the tests for it.** One handler
|
|
13560
13774
|
constant, served by `nodeHost` and by a minimal in-process host that declares
|
|
13561
|
-
|
|
13775
|
+
_no_ capabilities so the buffering path is exercised rather than assumed, with a
|
|
13562
13776
|
final pair of cases invoking both and comparing directly. A future adapter —
|
|
13563
13777
|
including a cloud one — is measured against that file.
|
|
13564
13778
|
|
|
@@ -13586,7 +13800,7 @@ other way, and a value committed where run-level values already commit.
|
|
|
13586
13800
|
and the body still arrives only after it does. What changes is who can edit a
|
|
13587
13801
|
playbook, and whether changing the refund policy shows up as a reviewable diff.
|
|
13588
13802
|
|
|
13589
|
-
A skill body is
|
|
13803
|
+
A skill body is _instructions to a model_, so where it came from is a security
|
|
13590
13804
|
property rather than a convenience — content fetched at run time is content
|
|
13591
13805
|
someone else can change after you reviewed it. The loader therefore accepts a
|
|
13592
13806
|
local directory and nothing else: a URL is **refused by name**, not fetched.
|
|
@@ -13657,7 +13871,7 @@ other way, and a value committed where run-level values already commit.
|
|
|
13657
13871
|
|
|
13658
13872
|
- **`McpClientOptions.signal` cancels a hung MCP tool call again — it never
|
|
13659
13873
|
did.** The signal was being sent as part of the `tools/call` request
|
|
13660
|
-
|
|
13874
|
+
_params_, where an `AbortSignal` JSON-serializes to `{}`: the server received
|
|
13661
13875
|
a meaningless field and the caller received no cancellation. The SDK takes
|
|
13662
13876
|
per-request options in a separate trailing argument, which is where the signal
|
|
13663
13877
|
now goes; it is threaded to `connect()` and `listTools()` for the same reason,
|
|
@@ -13696,7 +13910,7 @@ synchronous, and making it async to accommodate a resolver would shift every
|
|
|
13696
13910
|
agent's timing for a feature most agents do not use. A resolver that needs I/O
|
|
13697
13911
|
can do it before `run()` and close over the result.
|
|
13698
13912
|
|
|
13699
|
-
Every debugging session starts at a
|
|
13913
|
+
Every debugging session starts at a _variable_ — "where did that instruction come
|
|
13700
13914
|
from?", "which loop wrote the history it answered from?" — and both halves of the
|
|
13701
13915
|
answer already existed, in vocabularies that did not meet. footprintjs 9.13 records
|
|
13702
13916
|
a variable's whole life in commit indices and runtimeStageIds; the localizer thinks
|
|
@@ -13707,7 +13921,7 @@ Joining them turned out to buy something bigger than a nicer read-out. The backw
|
|
|
13707
13921
|
walk narrows each loop with embedding similarity — a proxy that points at a
|
|
13708
13922
|
neighbourhood and cannot separate a planted instruction from an innocent same-topic
|
|
13709
13923
|
sibling. But where the recording carries per-write provenance, one part of that guess
|
|
13710
|
-
is unnecessary: the commit log
|
|
13924
|
+
is unnecessary: the commit log _says_ which write produced the value this loop read.
|
|
13711
13925
|
So the walk stops guessing exactly there — and keeps saying so everywhere else.
|
|
13712
13926
|
|
|
13713
13927
|
### Added
|
|
@@ -13735,7 +13949,7 @@ So the walk stops guessing exactly there — and keeps saying so everywhere else
|
|
|
13735
13949
|
same hops, same order, same verdicts (pinned by a deep-equal test).
|
|
13736
13950
|
|
|
13737
13951
|
The proxy still picks WHO; dataflow picks WHERE. A stage-level edge never becomes
|
|
13738
|
-
an exact hop, and a recorded edge outranks the
|
|
13952
|
+
an exact hop, and a recorded edge outranks the _inferred_ proximate-tool hop —
|
|
13739
13953
|
better evidence wins, and the hop record says which kind it used.
|
|
13740
13954
|
|
|
13741
13955
|
- **`AgentOptions.writeProvenance`** (`'off'` default, `'reads-prefix'` to enable) —
|
|
@@ -13752,7 +13966,7 @@ So the walk stops guessing exactly there — and keeps saying so everywhere else
|
|
|
13752
13966
|
`coverage: 'exact'` requires **positive** evidence — at least one recorded per-write
|
|
13753
13967
|
edge — not merely the absence of a conservative one. A key nothing ever reads back
|
|
13754
13968
|
(the agent's `lastToolResult`: written by tool-calls, never read by `call-llm`) has
|
|
13755
|
-
an empty edge set, so "no conservative edges" is
|
|
13969
|
+
an empty edge set, so "no conservative edges" is _vacuously_ true; scoring that as
|
|
13756
13970
|
exact would hand the walk its most confident hop on its least-evidenced key. Absence
|
|
13757
13971
|
of dataflow is `'unknown'`, never exactness.
|
|
13758
13972
|
|
|
@@ -13776,7 +13990,7 @@ it with measurements instead of re-deriving why it was strict.
|
|
|
13776
13990
|
|
|
13777
13991
|
## [7.11.0] - 2026-08-02
|
|
13778
13992
|
|
|
13779
|
-
A pipeline whose steps form a
|
|
13993
|
+
A pipeline whose steps form a _shape_ rather than a line — one step feeding two
|
|
13780
13994
|
independent lookups, a third waiting for both — had no home here. You could nest
|
|
13781
13995
|
a `Parallel` inside a `Sequence`, but then you were scheduling it by hand, and
|
|
13782
13996
|
the values did not survive the trip.
|
|
@@ -13848,7 +14062,7 @@ switches keep compiling. Same reasoning as `workflow()` in 7.10.0.
|
|
|
13848
14062
|
## [7.10.0] - 2026-08-02
|
|
13849
14063
|
|
|
13850
14064
|
Two routing-shaped gaps closed. Both were things the docs told you to hand-roll,
|
|
13851
|
-
and both were fiddly in the same way: the wiring is easy to get
|
|
14065
|
+
and both were fiddly in the same way: the wiring is easy to get _nearly_ right,
|
|
13852
14066
|
and nearly right fails quietly — at run time, several steps away from the
|
|
13853
14067
|
mistake.
|
|
13854
14068
|
|
|
@@ -13857,7 +14071,7 @@ mistake.
|
|
|
13857
14071
|
- **`llmRouter` — the classic Swarm decision, packaged.** `swarm()` asks for a
|
|
13858
14072
|
`route()` that is sync and pure, and it means it: the `Conditional` evaluates
|
|
13859
14073
|
it once per branch predicate and the loop's exit guard evaluates it again
|
|
13860
|
-
after every turn. So the LLM decision has to happen
|
|
14074
|
+
after every turn. So the LLM decision has to happen _somewhere else_, before
|
|
13861
14075
|
the message reaches `route` — and that placement is the part everyone
|
|
13862
14076
|
re-invented, along with the prompt, the parsing, and a second copy of the
|
|
13863
14077
|
agent roster that drifts from the first.
|
|
@@ -13871,8 +14085,8 @@ mistake.
|
|
|
13871
14085
|
with a stale decision.
|
|
13872
14086
|
|
|
13873
14087
|
The decision is validated JSON — `RoutingDecision` = `{ agentId?, message,
|
|
13874
|
-
|
|
13875
|
-
halt sentinel. An id that isn't in the roster is kept verbatim,
|
|
14088
|
+
reason? }`. No `agentId` means "done", and the swarm halts through its own
|
|
14089
|
+
halt sentinel. An id that isn't in the roster is kept verbatim, _not_ quietly
|
|
13876
14090
|
swapped for a plausible one: `swarm()`'s existing done/fallback law then ends
|
|
13877
14091
|
the run, so a hallucinated agent shows up as a halt instead of a wrong answer.
|
|
13878
14092
|
Unusable output throws `RoutingDecisionError` with the model's raw text
|
|
@@ -13907,7 +14121,7 @@ mistake.
|
|
|
13907
14121
|
feeds the next step's `{ message }`, the house convention every LLM runner
|
|
13908
14122
|
speaks; anything else must match exactly) — a chain that doesn't line up is a
|
|
13909
14123
|
compile error, pinned by `@ts-expect-error` fixtures under `npm run
|
|
13910
|
-
|
|
14124
|
+
test:types`. At run time, values are handed over **unchanged**: objects stay
|
|
13911
14125
|
objects. `workflow(draft, edit)` over two `LLMCall`s reads exactly as it
|
|
13912
14126
|
always did.
|
|
13913
14127
|
|
|
@@ -13937,7 +14151,7 @@ mistake.
|
|
|
13937
14151
|
|
|
13938
14152
|
**If you pass `dimensions` today, your vectors change length.** They were the
|
|
13939
14153
|
model's native length all along; now they are the length you asked for. Anything
|
|
13940
|
-
you have already embedded and stored was written at the
|
|
14154
|
+
you have already embedded and stored was written at the _old_ length, so a store
|
|
13941
14155
|
built with `openaiEmbedder({ dimensions: 256 })` on 7.8 holds 1536-long vectors
|
|
13942
14156
|
and will not match new 256-long queries. **Re-embed, or drop `dimensions` to
|
|
13943
14157
|
keep the old lengths.**
|
|
@@ -13955,7 +14169,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
|
|
|
13955
14169
|
1536 for everything, so `text-embedding-3-large` under-reported by half.
|
|
13956
14170
|
- An **unknown model with no `dimensions` is now a construction-time error**
|
|
13957
14171
|
instead of a silent 1536. This is the breaking edge: `openaiEmbedder({
|
|
13958
|
-
|
|
14172
|
+
baseURL, model: 'nomic-embed-text' })` against a gateway, an Ollama server or
|
|
13959
14173
|
an Azure deployment name now throws until you state the length. That
|
|
13960
14174
|
population is exactly the one that was being lied to. One option fixes it:
|
|
13961
14175
|
`{ dimensions: 768 }`.
|
|
@@ -13971,7 +14185,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
|
|
|
13971
14185
|
- **`localEmbedder({ backend })` / `staticEmbedder({ backend })` — pass an
|
|
13972
14186
|
already-imported module, and the on-device embedders work in a browser.** To
|
|
13973
14187
|
keep the heavy peer deps optional, both factories import them through a
|
|
13974
|
-
|
|
14188
|
+
_variable_ specifier — which no bundler can see through. The bare name
|
|
13975
14189
|
survived a production build and reached the browser unresolved:
|
|
13976
14190
|
`TypeError: Failed to resolve module specifier '@huggingface/transformers'`.
|
|
13977
14191
|
The capability was there all along; only the packaging blocked it. Now the
|
|
@@ -14008,7 +14222,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
|
|
|
14008
14222
|
|
|
14009
14223
|
- **Docs-truth check — an ongoing, honest answer to "do the docs describe what
|
|
14010
14224
|
the code actually does?"** `npm run docs:truth` (new CI job `docs-truth`)
|
|
14011
|
-
answers three
|
|
14225
|
+
answers three _separate_ questions for every capability the package exposes,
|
|
14012
14226
|
because their combinations are different bugs: DECLARED (in the published
|
|
14013
14227
|
surface), DOCUMENTED (described in prose on the site), EXERCISED (a real run
|
|
14014
14228
|
produces it). Declared/documented/never-exercised is the shape a dead or
|
|
@@ -14022,7 +14236,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
|
|
|
14022
14236
|
therefore cannot see a single `agentfootprint/<subpath>` symbol. The
|
|
14023
14237
|
surface is reported per subpath, since root-barrel-vs-subpath is itself a
|
|
14024
14238
|
known source of user confusion. Events come from `ALL_EVENT_TYPES`.
|
|
14025
|
-
- The DOCUMENTED column counts
|
|
14239
|
+
- The DOCUMENTED column counts _only_ prose on the 63 hand-written pages
|
|
14026
14240
|
under `docs-next/content/docs`. Both TypeDoc trees
|
|
14027
14241
|
(`docs-next/content/docs/api/`, `docs/api-reference/`) are excluded — they
|
|
14028
14242
|
are generated from source, so every symbol appears in them by construction
|