agentfootprint 9.101.1 → 9.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/dist/core/Agent.js +98 -2
  3. package/dist/core/Agent.js.map +1 -1
  4. package/dist/core/agent/AgentBuilder.js +26 -4
  5. package/dist/core/agent/AgentBuilder.js.map +1 -1
  6. package/dist/core/agent/buildAgentChart.js +11 -0
  7. package/dist/core/agent/buildAgentChart.js.map +1 -1
  8. package/dist/core/agent/buildDynamicAgentChart.js +11 -0
  9. package/dist/core/agent/buildDynamicAgentChart.js.map +1 -1
  10. package/dist/core/agent/findings/ledger.js +34 -10
  11. package/dist/core/agent/findings/ledger.js.map +1 -1
  12. package/dist/core/agent/findings/offer.js +179 -0
  13. package/dist/core/agent/findings/offer.js.map +1 -0
  14. package/dist/core/agent/findings/reserved.js +167 -14
  15. package/dist/core/agent/findings/reserved.js.map +1 -1
  16. package/dist/core/agent/findings/serve.js +72 -49
  17. package/dist/core/agent/findings/serve.js.map +1 -1
  18. package/dist/core/agent/findings/types.js +10 -1
  19. package/dist/core/agent/findings/types.js.map +1 -1
  20. package/dist/core/agent/stages/callLLM.js +1 -1
  21. package/dist/core/agent/stages/callLLM.js.map +1 -1
  22. package/dist/core/agent/stages/route.js +7 -1
  23. package/dist/core/agent/stages/route.js.map +1 -1
  24. package/dist/core/agent/stages/seed.js +16 -2
  25. package/dist/core/agent/stages/seed.js.map +1 -1
  26. package/dist/core/agent/stages/toolCalls.js +16 -10
  27. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  28. package/dist/core/agent/stages/window.js +110 -10
  29. package/dist/core/agent/stages/window.js.map +1 -1
  30. package/dist/core/agent/window/index.js +3 -1
  31. package/dist/core/agent/window/index.js.map +1 -1
  32. package/dist/core/agent/window/ledgerFactPins.js +200 -0
  33. package/dist/core/agent/window/ledgerFactPins.js.map +1 -0
  34. package/dist/core/agent/window/turns.js +45 -11
  35. package/dist/core/agent/window/turns.js.map +1 -1
  36. package/dist/core/slots/buildToolsSlot.js +23 -1
  37. package/dist/core/slots/buildToolsSlot.js.map +1 -1
  38. package/dist/esm/core/Agent.d.ts +14 -2
  39. package/dist/esm/core/Agent.js +98 -2
  40. package/dist/esm/core/Agent.js.map +1 -1
  41. package/dist/esm/core/agent/AgentBuilder.d.ts +13 -3
  42. package/dist/esm/core/agent/AgentBuilder.js +26 -4
  43. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  44. package/dist/esm/core/agent/buildAgentChart.d.ts +14 -0
  45. package/dist/esm/core/agent/buildAgentChart.js +11 -0
  46. package/dist/esm/core/agent/buildAgentChart.js.map +1 -1
  47. package/dist/esm/core/agent/buildDynamicAgentChart.js +11 -0
  48. package/dist/esm/core/agent/buildDynamicAgentChart.js.map +1 -1
  49. package/dist/esm/core/agent/findings/ledger.d.ts +19 -9
  50. package/dist/esm/core/agent/findings/ledger.js +34 -10
  51. package/dist/esm/core/agent/findings/ledger.js.map +1 -1
  52. package/dist/esm/core/agent/findings/offer.d.ts +112 -0
  53. package/dist/esm/core/agent/findings/offer.js +171 -0
  54. package/dist/esm/core/agent/findings/offer.js.map +1 -0
  55. package/dist/esm/core/agent/findings/reserved.d.ts +53 -7
  56. package/dist/esm/core/agent/findings/reserved.js +166 -13
  57. package/dist/esm/core/agent/findings/reserved.js.map +1 -1
  58. package/dist/esm/core/agent/findings/serve.d.ts +39 -11
  59. package/dist/esm/core/agent/findings/serve.js +74 -47
  60. package/dist/esm/core/agent/findings/serve.js.map +1 -1
  61. package/dist/esm/core/agent/findings/types.d.ts +36 -2
  62. package/dist/esm/core/agent/findings/types.js +9 -0
  63. package/dist/esm/core/agent/findings/types.js.map +1 -1
  64. package/dist/esm/core/agent/stages/callLLM.d.ts +9 -1
  65. package/dist/esm/core/agent/stages/callLLM.js +1 -1
  66. package/dist/esm/core/agent/stages/callLLM.js.map +1 -1
  67. package/dist/esm/core/agent/stages/route.js +7 -1
  68. package/dist/esm/core/agent/stages/route.js.map +1 -1
  69. package/dist/esm/core/agent/stages/seed.d.ts +10 -0
  70. package/dist/esm/core/agent/stages/seed.js +16 -2
  71. package/dist/esm/core/agent/stages/seed.js.map +1 -1
  72. package/dist/esm/core/agent/stages/toolCalls.js +16 -10
  73. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  74. package/dist/esm/core/agent/stages/window.d.ts +22 -0
  75. package/dist/esm/core/agent/stages/window.js +111 -11
  76. package/dist/esm/core/agent/stages/window.js.map +1 -1
  77. package/dist/esm/core/agent/types.d.ts +74 -4
  78. package/dist/esm/core/agent/window/index.d.ts +1 -0
  79. package/dist/esm/core/agent/window/index.js +1 -0
  80. package/dist/esm/core/agent/window/index.js.map +1 -1
  81. package/dist/esm/core/agent/window/ledgerFactPins.d.ts +147 -0
  82. package/dist/esm/core/agent/window/ledgerFactPins.js +194 -0
  83. package/dist/esm/core/agent/window/ledgerFactPins.js.map +1 -0
  84. package/dist/esm/core/agent/window/strategy.d.ts +25 -0
  85. package/dist/esm/core/agent/window/turns.d.ts +25 -0
  86. package/dist/esm/core/agent/window/turns.js +45 -11
  87. package/dist/esm/core/agent/window/turns.js.map +1 -1
  88. package/dist/esm/core/agent/window/types.d.ts +64 -0
  89. package/dist/esm/core/slots/buildToolsSlot.d.ts +11 -0
  90. package/dist/esm/core/slots/buildToolsSlot.js +23 -1
  91. package/dist/esm/core/slots/buildToolsSlot.js.map +1 -1
  92. package/dist/esm/events/payloads.d.ts +6 -0
  93. package/dist/esm/index.d.ts +1 -1
  94. package/dist/esm/index.js.map +1 -1
  95. package/dist/esm/lib/time-travel/servedView.js +15 -3
  96. package/dist/esm/lib/time-travel/servedView.js.map +1 -1
  97. package/dist/index.js.map +1 -1
  98. package/dist/lib/time-travel/servedView.js +15 -3
  99. package/dist/lib/time-travel/servedView.js.map +1 -1
  100. package/dist/types/core/Agent.d.ts +14 -2
  101. package/dist/types/core/Agent.d.ts.map +1 -1
  102. package/dist/types/core/agent/AgentBuilder.d.ts +13 -3
  103. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  104. package/dist/types/core/agent/buildAgentChart.d.ts +14 -0
  105. package/dist/types/core/agent/buildAgentChart.d.ts.map +1 -1
  106. package/dist/types/core/agent/buildDynamicAgentChart.d.ts.map +1 -1
  107. package/dist/types/core/agent/findings/ledger.d.ts +19 -9
  108. package/dist/types/core/agent/findings/ledger.d.ts.map +1 -1
  109. package/dist/types/core/agent/findings/offer.d.ts +113 -0
  110. package/dist/types/core/agent/findings/offer.d.ts.map +1 -0
  111. package/dist/types/core/agent/findings/reserved.d.ts +53 -7
  112. package/dist/types/core/agent/findings/reserved.d.ts.map +1 -1
  113. package/dist/types/core/agent/findings/serve.d.ts +39 -11
  114. package/dist/types/core/agent/findings/serve.d.ts.map +1 -1
  115. package/dist/types/core/agent/findings/types.d.ts +36 -2
  116. package/dist/types/core/agent/findings/types.d.ts.map +1 -1
  117. package/dist/types/core/agent/stages/callLLM.d.ts +9 -1
  118. package/dist/types/core/agent/stages/callLLM.d.ts.map +1 -1
  119. package/dist/types/core/agent/stages/route.d.ts.map +1 -1
  120. package/dist/types/core/agent/stages/seed.d.ts +10 -0
  121. package/dist/types/core/agent/stages/seed.d.ts.map +1 -1
  122. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  123. package/dist/types/core/agent/stages/window.d.ts +22 -0
  124. package/dist/types/core/agent/stages/window.d.ts.map +1 -1
  125. package/dist/types/core/agent/types.d.ts +74 -4
  126. package/dist/types/core/agent/types.d.ts.map +1 -1
  127. package/dist/types/core/agent/window/index.d.ts +1 -0
  128. package/dist/types/core/agent/window/index.d.ts.map +1 -1
  129. package/dist/types/core/agent/window/ledgerFactPins.d.ts +148 -0
  130. package/dist/types/core/agent/window/ledgerFactPins.d.ts.map +1 -0
  131. package/dist/types/core/agent/window/strategy.d.ts +25 -0
  132. package/dist/types/core/agent/window/strategy.d.ts.map +1 -1
  133. package/dist/types/core/agent/window/turns.d.ts +25 -0
  134. package/dist/types/core/agent/window/turns.d.ts.map +1 -1
  135. package/dist/types/core/agent/window/types.d.ts +64 -0
  136. package/dist/types/core/agent/window/types.d.ts.map +1 -1
  137. package/dist/types/core/slots/buildToolsSlot.d.ts +11 -0
  138. package/dist/types/core/slots/buildToolsSlot.d.ts.map +1 -1
  139. package/dist/types/events/payloads.d.ts +6 -0
  140. package/dist/types/events/payloads.d.ts.map +1 -1
  141. package/dist/types/index.d.ts +1 -1
  142. package/dist/types/index.d.ts.map +1 -1
  143. package/dist/types/lib/time-travel/servedView.d.ts.map +1 -1
  144. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,247 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [9.103.0] - 2026-09-17
9
+
10
+ ### Added — an answer-turn ask on the served ledger piece, as a dial
11
+
12
+ - `findings({ answerAsk: 'quote-facts' })` appends a second model-facing ask
13
+ to the served ledger piece — `FINDINGS_ANSWER_ASK`, its last section after
14
+ a blank line — telling the model HOW to answer from it: answer from the
15
+ `facts` lines and copy each value as written; an `evidenceRefs` or
16
+ `nextSteps` result is unsettled, say so; a `{"collapsed":true,…}` ticket
17
+ carries no data (judged noise or ruled out, or its fact already listed),
18
+ do not draw on it; an undeclared result is
19
+ served in full and may be used; never invent a value. It says what the
20
+ model may do and promises nothing (judged by `unprovable` at the strictest
21
+ lifetime, registered in the model-facing inventory), and it is a constant,
22
+ so the piece's cache law holds with it. The default is `'none'` and the
23
+ dial is BENCH-GATED like `'ledger-only'`: it ships so
24
+ `bench/findings-shuffle.mjs`'s new fourth condition, `ledger+ask`, can
25
+ score it on a real model on `facts-in-answer` against `ledger-and-facts` —
26
+ the design page's fourth run measured one fact value in twelve restated
27
+ when the piece is served, and asked for this instruction variant next. On
28
+ the mock that row equals `ledger-and-facts` to the digit (the scripted
29
+ model ignores prose; the header says so). The dial rides the
30
+ `findingsServe` thread exactly — `Agent.ts` to seed and to call-llm,
31
+ value-conditionally — and lands on the record as the run constant
32
+ `AgentState.findingsAnswerAsk`, written ONLY under `'quote-facts'`, so an
33
+ armed agent on the default commits the key set it committed in 9.102.0,
34
+ every unarmed agent is byte-identical, and `servedAt` re-appends the ask
35
+ from the record by construction. A bad value is refused at build at either
36
+ door. Pinned: `findings/serve.test.ts` (the ask appended only under the
37
+ dial, exact bytes, absent by default, never alone), `findings-served.test.ts`
38
+ (both chart shapes end the served system text with the ask; without the
39
+ dial byte-identical), `findings-declarations.test.ts` (the option, the run
40
+ constant only when armed and on, the refusal), `receipt-conformance.test.ts`
41
+ (the rebuild byte-equal under the dial); all 18 byte-identity references
42
+ pass untouched.
43
+
44
+ ### Measured — the ask on two hosted models (`bench/findings-shuffle.mjs`, noise 4 at the end of the order, 1,000-token payloads, ten runs per condition)
45
+
46
+ - Claude Sonnet 5: facts in the answer 0.883 with the ledger alone → 0.950 with the ask (1.000 without the ledger); noise cited 0.100 with either → 0.300 without the ledger; drift 0.70 → 0.40.
47
+ - Claude Haiku 4.5: facts 0.983 → 1.000 with the ask; noise cited 0.000 (0.100 without the ledger); drift 0.30 → 0.20.
48
+ - Collapsing facts too (`serve: 'ledger-only'`) was the worst row on both models and stays bench-gated. Full tables: docs/design/2026-09-findings-ledger-real-model.md.
49
+
50
+ ## [9.102.0] - 2026-09-17
51
+
52
+ ### Added — a declared fact stays in the window; noise leaves first
53
+
54
+ - The window reads the findings ledger. On an agent with `.findings()` and a
55
+ window strategy, a turn whose tool result the model declared a `fact` is
56
+ HELD beyond `keepRecentTurns` — newest first, up to `keepLedgerFacts`
57
+ (default 4; `false` or `0` for no hold) — and the refusal is on the record
58
+ by name: `WindowRefusalReason` gains `'ledger-fact'`. It is a bounded hold
59
+ in the refusal engine, the content-aware sibling of the last-tool-result
60
+ pin (`ledgerFactPinsOf` beside `toolResultPinsOf`, admitted by the same
61
+ ceiling-spender in `planRemoval`), so `slidingWindow`, `tokenBudget`,
62
+ `summarizeOldest` and a consumer-written strategy all inherit it through
63
+ `planRemoval` — no new strategy file, none of the three shipped ones changed
64
+ a byte, the contiguous span, the drop ladder and the meter's single-seam
65
+ rebase untouched. 'Noise first' follows with no second mechanism: noise,
66
+ ruled-out, open and undeclared turns are unpinned and leave oldest-first as
67
+ they always did, and a judged noise turn that outlives a fact is already a
68
+ ticket on the wire. Why: the served piece restored a declared fact to the
69
+ answer turn, but under a window the fact's RESULT still left by recency —
70
+ the refusal engine saw a declared fact and a declared noise result as the
71
+ same bytes. By the model's claim only: a turn is the removal unit and its
72
+ standing is its most valuable result's (`fact > open > undeclared >
73
+ ruled-out > noise`); a result the model never named is undeclared and is not
74
+ held; the library reads no result's text to decide otherwise.
75
+ - A fact hold never exists without its ceiling and its stand-down. The
76
+ ceiling is spent newest first and a held turn already inside
77
+ `keepRecentTurns` spends no slot (the free-pin law); the turns it turned away
78
+ are `yielded` on the record; nothing at or before the current request is
79
+ pinnable. The stand-down is the pin's: when the two previous visits removed
80
+ nothing and named only pins, the fact pins release for one visit and the
81
+ record says so. It reads BOTH pin names (`'last-tool-result'` and/or
82
+ `'ledger-fact'`), because a turn held by both pins is reported under the
83
+ recency pin's name — a fact stand-down reading only its own would never see
84
+ that turn blocking, and the two pins would alternate under each other's
85
+ name with the window never shrinking (derived, pinned by test, never
86
+ shipped). The recency pin's own stand-down reads only its own name, so its
87
+ 9.57.0 rule is unchanged.
88
+ - The record says what was held and whose standing left. `WindowRecord`
89
+ gains two optional keys, present only on an armed agent and filed by the
90
+ STAGE so a consumer-written strategy's record carries them too:
91
+ `ledgerFacts` (the `WindowObservations` shape — `pinned`, `yielded`,
92
+ `limit` = `keepLedgerFacts`, `standDown: true` on the visit it released)
93
+ and `droppedStandings` (`{ toolCallId, standing? }` for every tool result
94
+ that left; `standing` absent is undeclared, never a verdict the library
95
+ inferred). `WindowStrategyInput.standingOf?` hands a strategy a turn's
96
+ declared standing, bound by the stage from ONE read of the ledger and
97
+ absent on an unarmed agent — a strategy never reads scope for it; it is for
98
+ ordering or reporting among what the engine left removable, never for
99
+ inferring. What the model is TOLD about a drop is unchanged: the notice
100
+ names tools and counts, never a standing, never the model's own line.
101
+ - `keepLedgerFacts` is live: `.findings({ keepLedgerFacts })` and
102
+ `Agent.create({ keepLedgerFacts })` (the `.findings()` door wins when both
103
+ are given), resolved once at build and validated there — a negative or
104
+ non-integer value is refused, never mid-run. Without `.findings()` the
105
+ option is accepted and does nothing (the keepLastToolResults-without-a-window
106
+ precedent). Exported from the root: `LedgerFactPin` (the candidate a hold is
107
+ built from: `toolCallIds`, `toolName`, `turnIndex`, `messageIndex`, `chars`),
108
+ beside the widened `WindowRefusalReason` and the `WindowRecord` additions.
109
+ - Measured (`npm run bench:findings`, mock provider, 30 tool calls, a planted
110
+ fact every third, a sliding window keeping 6 turns): the results of 2 of 10
111
+ planted facts reached the answer turn verbatim by recency alone, 6 under the
112
+ default ceiling and 8 under a ceiling of 6, while the piece carried 9 of 10
113
+ on every armed row (the last batch is undeclared by the no-outputSchema law)
114
+ and the noise share of tool-result bytes on the wire stayed at the collapsed
115
+ level; every noise result left on every armed row, so the hold changed WHICH
116
+ facts left, not whether noise did. The design page has the print and its
117
+ reading; no real-model number exists yet.
118
+
119
+ ### Unchanged — an agent without `.findings()` plans, records and sends the bytes it did before
120
+
121
+ - The hold, the ledger read, `standingOf`, `ledgerFacts` and `droppedStandings`
122
+ are all gated on the door: an unarmed window stage never reads
123
+ `findingsLedger` (pinned by a getter counting reads), hands its strategy the
124
+ exact input it always did, files the exact record it always did, and sends
125
+ the same request bytes — even when the model emits `_findings` on its own.
126
+ The 16 byte-identity references under `test/core/tools/reference/` pass
127
+ untouched; ONE new reference, `agent-findings-window`, was generated alone,
128
+ with what it holds read back from its bytes on the test file's header.
129
+ - `keepLedgerFacts: false` plans exactly as the unarmed window (the bench
130
+ checks it: same facts verbatim, same tool messages, same receipt count, no
131
+ fact held), so the hold is an addition to the plan, never a rewrite of it.
132
+
133
+ ### Added — the ids the model may name are in the schema; the proposition before the call; a bench that can measure
134
+
135
+ - The offer. On a hosted model the ask "by its tool_result id" produced
136
+ standings named by ORDINAL (`"0"`, `"1"`), recorded honestly as `unknownId`
137
+ and settling nothing (`docs/design/2026-09-findings-ledger-real-model.md`).
138
+ So from the second call on, the reserved `_findings` property on every
139
+ served schema binds the ids the model may name: `previous[].toolCallId`
140
+ carries `enum: <the tool results on the wire with no standing yet, newest
141
+ first, at most 32>` and says "one of the ids listed; a result not listed
142
+ cannot be named here"; a clipped list states the cap. The list is what the
143
+ model can still READ: a result with no standing, a `fact` (stood on in the
144
+ piece, served verbatim under the default mode) and an `open` result (served
145
+ verbatim, carrying what would settle it) stay listed, so a later call can
146
+ REVISE a standing — `open` → `fact` when a call settles it, `fact` →
147
+ `ruled-out` when a conflict resolves; the fold's last-wins law is reachable
148
+ through the enum. A result the model declared `noise` or `ruled-out` leaves
149
+ the list: it is a ticket on the wire under every serve mode and the piece
150
+ carries a count or one line, so there is nothing left to re-judge (a wrong
151
+ `ruled-out` is answered by a new call). The instruction asks the model to
152
+ name a result again only to change its standing. An evicted result is not
153
+ on the list, and the first call — with nothing to name — serves the base
154
+ property by reference, byte-identical to 9.101.0. ONE owner of the two sets
155
+ (`findings/offer.ts`: `offeredResultIds` for the enum, `undeclaredIds` for
156
+ the piece's `undeclared:` line — the honest absence, a subset of the offer),
157
+ computed at the Tools mount, where the served history and the ledger meet
158
+ (the slot is an isolated subflow), under the same arm that decorates; bound
159
+ at the ONE decoration site and committed with the tool list, so
160
+ `servedAt(k).tools.schemas` holds exactly what was offered and
161
+ `receipt.tools.schemaHashes` moves when the offer does. The law holds at
162
+ the schema AND at the row: the offer is what the model may COPY, never what
163
+ the library resolves — an id outside it still files as written, `unknownId:
164
+ true`, never mapped to a position or a tool name — and every id INSIDE it
165
+ resolves, because a standing is identified against the same served history
166
+ the offer was read from (`findings/offer.ts · knownResults`: the served
167
+ `role: 'tool'` messages plus the previous batch), so an id copied from the
168
+ offer files with its tool name whichever batch the result came from (the
169
+ first cut resolved against the last batch only, and an offered older id
170
+ filed as `unknownId` — caught in review, never released). The instruction
171
+ asks the JSON answer, which has no schema to bind, for the id exactly as
172
+ the schema listed it.
173
+ - Named, not measured: from the second call on, an armed agent's tool schemas
174
+ vary per call (the enum), so a `'tools'` cache breakpoint cannot hit on such
175
+ a run and, on a prefix-cached wire, every breakpoint after it misses with
176
+ it — the system piece already moved the block on every declaring call
177
+ (step 3's recorded cost); the offer moves the tools prefix ahead of it. No
178
+ bench in the tree counts cache tokens; the lever not taken (the offer in
179
+ the request-only system piece, below the tools breakpoint — prose, which is
180
+ what failed on the real model) is on the design page.
181
+ - The proposition. `FindingsDeclaration` gains `proposition?: string` (what
182
+ the call tests; the schema recommends it when `basis` is `'exploratory'`)
183
+ and `predicts?: string` (what the result should show if it holds), declared
184
+ on the call before its result exists; both land on the `BasisRow`, each cut
185
+ at 240 chars with the cut stated in the text. `FindingsDeclaredPayload`
186
+ gains `hasProposition?: true` — a flag, never the text. The served piece
187
+ quotes the judged call's own proposition on `open` and `ruled-out` lines
188
+ (`… — tested: <proposition>`), never on a fact line; `predicts` is
189
+ record-only.
190
+ - The shuffle bench can measure (`bench/findings-shuffle.mjs`): `NOISE_AT`
191
+ (end / start / spread), `NOISE_SIZE` (about 250 / 1000 / 4000 tokens of
192
+ padding per noise record), noise values within 5% of a fact and never equal,
193
+ a `standing-accuracy` column (the share of the actor's standings on known
194
+ ids that agree with the planted truth — the harness knows the truth, so no
195
+ judge), and `--matrix` (24 cells for one model per invocation, the cost line
196
+ printed before the first call). Default mode prints the pre-packet numbers
197
+ to the digit and the mock smoke stays green. No hosted run was made in
198
+ this release; the real-model page holds the pre-offer tables and names the
199
+ number that must move.
200
+
201
+ ### Changed — `.findings()` refuses `reactMode: 'classic'`
202
+
203
+ - The offer needs the tools slot recomposed every call, so `.findings()` is
204
+ REFUSED at build under `reactMode: 'classic'` — through both doors
205
+ (`.findings()` and `Agent.create({ findings })`), the `selfExplain` twin:
206
+ classic selects the Tools branch on turn 1 only, so an armed classic agent
207
+ would have served the offer-less base on every call and filed every
208
+ standing as `unknownId`, the number this packet exists to move silently
209
+ stuck at zero. 9.101.x accepted the combination and degraded it silently;
210
+ the message names the fix. `'dynamic'` (the default) and `'dynamic-grouped'`
211
+ are unchanged.
212
+
213
+ ### Fixed — the choice seam's enum fence read the decorated schema
214
+
215
+ - Since 9.101.0, `withoutFindingsArgument` recognised the library's
216
+ decoration by REFERENCE only (`properties._findings ===
217
+ FINDINGS_ARGUMENT_SCHEMA`), and the reference never holds on the live path:
218
+ the served list `callLLM` reads is the committed `dynamicToolSchemas`, a
219
+ `structuredClone` of what the slot planted. So the choice seam's enum fence
220
+ (`declaredEnumValuesOf(withoutFindingsArgument(schema))`, the
221
+ `unsupported-argument` check) judged the model's argument values against a
222
+ schema that still carried `_findings`, and a value equal to one of the
223
+ findings vocabulary words — `direct`, `exploratory`, `low`, `medium`,
224
+ `high`, `fact`, `open`, `noise`, `ruled-out` — was excused as a declared
225
+ enum value the model was entitled to. The decoration is recognised by its
226
+ versioned marker now (the first sentence of the reserved property's
227
+ description), so the frozen base, an offer copy and a committed clone of
228
+ either are all peeled, and an author's own `_findings` is still read as
229
+ written. An armed agent may therefore file an `unsupported-argument`
230
+ finding it previously suppressed. Its unit test had passed because it
231
+ handed the function the live reference; `test/core/agent/findings/
232
+ reserved.test.ts` now pins the clone.
233
+
234
+ ### Unchanged — the unarmed agent, and the two armed references regenerated alone
235
+
236
+ - An agent without `.findings()` maps no new key on the Tools mount, reads
237
+ neither `history` nor `findingsLedger` there, and serves and records the
238
+ bytes it did before: the 16 unarmed byte-identity references pass
239
+ untouched. `agent-findings` and `agent-findings-window` were regenerated
240
+ each alone (the rest copied aside and `cmp`-equal after) and their delta is
241
+ on the test file's header: the enum on every epoch after the first, a
242
+ `dynamicToolSchemas` write on each such epoch where an unchanged list used
243
+ to be an empty commit, the two new properties and the instruction's two new
244
+ lines, and the sizes that follow; regenerated once more, each alone, in the
245
+ second review for the revisable offer (a declared fact stays listed) and
246
+ the instruction's revised line. No message, ticket, ledger row, window
247
+ record or gap moved.
248
+
8
249
  ## [9.101.1] - 2026-09-16
9
250
 
10
251
  ### Fixed — the docs site's export-file ceiling
@@ -142,6 +142,36 @@ const registry_js_1 = require("../thinking/registry.js");
142
142
  // Public types (AgentOptions, AgentInput, AgentOutput) extracted to
143
143
  // ./agent/types.ts and re-exported above (v2.11.1).
144
144
  // AgentState extracted to ./agent/types.ts (v2.11.1).
145
+ /**
146
+ * The ledger-fact hold's default ceiling (9.102.0): how many fact turns the
147
+ * window holds beyond `keepRecentTurns` when `.findings()` and a window
148
+ * strategy are both configured and nobody named a number. Resolved in the
149
+ * constructor, ONCE, and threaded to `buildWindowStage` as a plain number —
150
+ * the stage applies no default of its own (absent = unarmed = no hold), so a
151
+ * reader has one place to look. `window/options.ts ·
152
+ * DEFAULT_KEEP_LAST_TOOL_RESULTS` is the pin's twin of this constant.
153
+ */
154
+ const DEFAULT_KEEP_LEDGER_FACTS = 4;
155
+ /**
156
+ * Validate the `keepLedgerFacts` dial — `window/options.ts ·
157
+ * requireKeepLastToolResults`'s twin: the same rule, the same moment.
158
+ * Refused at construction, never mid-run.
159
+ */
160
+ function requireKeepLedgerFacts(value, label) {
161
+ if (value === false)
162
+ return;
163
+ if (!Number.isInteger(value) || value < 0) {
164
+ throw new Error(`${label}: keepLedgerFacts must be a whole number >= 0, or false, got ` +
165
+ `${String(value)}. It is how many fact turns the window holds beyond ` +
166
+ `keepRecentTurns under .findings(); 0 and false both switch the hold off.`);
167
+ }
168
+ }
169
+ /** `false` → `0`; a number as named; nothing named → the default. */
170
+ function resolveKeepLedgerFacts(named) {
171
+ if (named === false)
172
+ return 0;
173
+ return named ?? DEFAULT_KEEP_LEDGER_FACTS;
174
+ }
145
175
  class Agent extends RunnerBase_js_1.RunnerBase {
146
176
  name;
147
177
  id;
@@ -249,8 +279,13 @@ class Agent extends RunnerBase_js_1.RunnerBase {
249
279
  * conditioned on, so an unarmed agent hands each stage exactly the deps
250
280
  * it always did. `serve` is threaded to seed (the run constant
251
281
  * `findingsServe`) and to call-llm (`findingsServe` in its deps) on an
252
- * armed agent only. `keepLedgerFacts` is inert until standing-aware
253
- * eviction lands. */
282
+ * armed agent only; `answerAsk` (9.103.0) rides the same two threads
283
+ * as `findingsAnswerAsk`, and only when it is `'quote-facts'` — the
284
+ * default `'none'` threads nothing. `keepLedgerFacts` is resolved once, here in the
285
+ * constructor (this door over `AgentOptions.keepLedgerFacts`), into the
286
+ * `keepLedgerFacts` field below and threaded to the window stage on an
287
+ * armed agent with a window, where `stages/window.ts · buildWindowStage`
288
+ * spends it as the `'ledger-fact'` pin ceiling. */
254
289
  findingsOptions;
255
290
  /** The opt-in tool-result ceiling in characters (9.11.0). Absent → results
256
291
  * are never measured. See {@link AgentOptions.maxToolResultChars}. */
@@ -314,6 +349,13 @@ class Agent extends RunnerBase_js_1.RunnerBase {
314
349
  /** The last-tool-result pin (9.57.0) — set only when the operator named a
315
350
  * value other than the default 2. See AgentOptions.keepLastToolResults. */
316
351
  keepLastToolResults;
352
+ /** The ledger-fact hold's ceiling (9.102.0), RESOLVED — set exactly when
353
+ * `.findings()` is on: `findings({ keepLedgerFacts })` over
354
+ * `AgentOptions.keepLedgerFacts`, `false` → `0`, nothing named →
355
+ * `DEFAULT_KEEP_LEDGER_FACTS`. Undefined on every unarmed agent, so the
356
+ * thread into the window stage reads as the decision it is. See
357
+ * AgentOptions.keepLedgerFacts. */
358
+ keepLedgerFacts;
317
359
  /** See AgentOptions.integrityPosture (9.60.0). Default 'observe'. */
318
360
  integrityPosture = 'observe';
319
361
  /**
@@ -686,6 +728,35 @@ class Agent extends RunnerBase_js_1.RunnerBase {
686
728
  (0, options_js_1.requireKeepLastToolResults)(opts.keepLastToolResults, 'Agent');
687
729
  this.keepLastToolResults = opts.keepLastToolResults;
688
730
  }
731
+ // Its content-aware sibling (9.102.0). The top-level door is refused
732
+ // HERE, whether or not `.findings()` is on — a dial that is silently
733
+ // ignored is a configuration mistake, not a runtime condition (the
734
+ // `.findings()` door was refused by the builder). The value is resolved
735
+ // ONCE, under the arm only: the `.findings()` door wins when both are
736
+ // given, `false` is `0`, nothing named is the default. Without
737
+ // `.findings()` there is no ledger to hold facts from, so the option is
738
+ // accepted and does nothing — the keepLastToolResults-without-a-window
739
+ // precedent.
740
+ if (opts.keepLedgerFacts !== undefined)
741
+ requireKeepLedgerFacts(opts.keepLedgerFacts, 'Agent');
742
+ if (this.findingsOptions !== undefined) {
743
+ // The ledger needs the tools slot recomposed every call: from the second
744
+ // call on, the served `_findings` property binds the ids the model may
745
+ // name (`findings/offer.ts · offeredResultIds`, bound at the Tools
746
+ // mount) — and on a hosted model that binding is what makes a standing
747
+ // resolve at all (docs/design/2026-09-findings-ledger-real-model.md).
748
+ // `reactMode: 'classic'` selects the Tools branch on turn 1 only, so an
749
+ // armed classic agent would serve the offer-less base on every call and
750
+ // file every standing as `unknownId`. Refused loud, here at build, the
751
+ // `AgentBuilder.selfExplain` twin — never a silent degrade.
752
+ if (this.reactMode === 'classic') {
753
+ throw new Error("Agent: .findings() requires per-iteration slot recomposition — reactMode 'classic' " +
754
+ 'caches the tools slot on turn 1, so the ids the model may name (bound into every ' +
755
+ 'served tool schema from the second call on) would never reach it and every standing ' +
756
+ "would file as unknownId. Use the default 'dynamic' mode (or 'dynamic-grouped').");
757
+ }
758
+ this.keepLedgerFacts = resolveKeepLedgerFacts(this.findingsOptions.keepLedgerFacts ?? opts.keepLedgerFacts);
759
+ }
689
760
  // Refused at construction, never mid-run — a misspelled posture that was
690
761
  // ignored would leave the liveness theorems switched off in an agent
691
762
  // that believes they are on (the concurrency-mode precedent).
@@ -3139,6 +3210,11 @@ class Agent extends RunnerBase_js_1.RunnerBase {
3139
3210
  ...(this.findingsOptions !== undefined && {
3140
3211
  findings: true,
3141
3212
  findingsServe: this.findingsOptions.serve ?? 'ledger-and-facts',
3213
+ // The answer-turn ask (9.103.0): VALUE-conditional inside the arm —
3214
+ // the default `'none'` threads nothing and writes no key.
3215
+ ...(this.findingsOptions.answerAsk === 'quote-facts' && {
3216
+ findingsAnswerAsk: 'quote-facts',
3217
+ }),
3142
3218
  consumePendingResumeFindingsLedger: () => {
3143
3219
  const l = this.pendingResumeFindingsLedger;
3144
3220
  this.pendingResumeFindingsLedger = undefined;
@@ -3492,6 +3568,11 @@ class Agent extends RunnerBase_js_1.RunnerBase {
3492
3568
  findings: true,
3493
3569
  hasFindingsLedger: true,
3494
3570
  findingsServe: this.findingsOptions.serve ?? 'ledger-and-facts',
3571
+ // The answer-turn ask (9.103.0): the same value seed records, so the
3572
+ // wire and the rebuild append it from one fact; absent on `'none'`.
3573
+ ...(this.findingsOptions.answerAsk === 'quote-facts' && {
3574
+ findingsAnswerAsk: 'quote-facts',
3575
+ }),
3495
3576
  }),
3496
3577
  ...(this.answerValidationConfig !== undefined && { suppressDraftTokens: true }),
3497
3578
  // The receipt's salt (9.88.0) — read per call, like seed's own accessor.
@@ -3588,6 +3669,16 @@ class Agent extends RunnerBase_js_1.RunnerBase {
3588
3669
  ...(this.keepLastToolResults !== undefined && {
3589
3670
  keepLastToolResults: this.keepLastToolResults,
3590
3671
  }),
3672
+ // The findings ledger (9.102.0): the arm and the RESOLVED
3673
+ // ledger-fact ceiling, together or not at all. `keepLedgerFacts`
3674
+ // is set exactly when `.findings()` is on, so an unarmed agent
3675
+ // hands the stage exactly the deps object it always did; an
3676
+ // armed agent with no window strategy threads nothing, because
3677
+ // this whole stage does not exist for it.
3678
+ ...(this.keepLedgerFacts !== undefined && {
3679
+ hasFindingsLedger: true,
3680
+ keepLedgerFacts: this.keepLedgerFacts,
3681
+ }),
3591
3682
  ...(pricingTable !== undefined && { pricingTable }),
3592
3683
  ...(costBudget !== undefined && { costBudget }),
3593
3684
  }),
@@ -3840,6 +3931,11 @@ class Agent extends RunnerBase_js_1.RunnerBase {
3840
3931
  // Escalation (9.19.0): the grouped chart threads `skillEscalated`
3841
3932
  // across the sf-llm-call boundary only when the policy exists.
3842
3933
  ...(this.skillBrains?.escalation !== undefined && { hasEscalation: true }),
3934
+ // The findings ledger (9.102.0): both charts compute the OFFER on the
3935
+ // Tools branch's mount only under the arm — the same value-conditional
3936
+ // grammar as the slot's `findings` and callLLM's `hasFindingsLedger`,
3937
+ // so the three can never disagree about whether the ledger is armed.
3938
+ ...(this.findingsOptions !== undefined && { hasFindingsLedger: true }),
3843
3939
  // `.limitsTravelWithTheAnswer()` (this release) — value-conditional, the
3844
3940
  // `resolvedModel` precedent: absent from the deps object entirely for an
3845
3941
  // agent that did not ask, so both builders mount the final-branch stage