agentfootprint 9.87.1 → 9.89.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 (143) hide show
  1. package/CHANGELOG.md +946 -0
  2. package/dist/core/Agent.js +32 -0
  3. package/dist/core/Agent.js.map +1 -1
  4. package/dist/core/LLMCall.js +7 -10
  5. package/dist/core/LLMCall.js.map +1 -1
  6. package/dist/core/agent/buildAgentMessageApiChart.js +2 -4
  7. package/dist/core/agent/buildAgentMessageApiChart.js.map +1 -1
  8. package/dist/core/agent/buildDynamicAgentChart.js +10 -0
  9. package/dist/core/agent/buildDynamicAgentChart.js.map +1 -1
  10. package/dist/core/agent/buildMessageApiChart.js +2 -4
  11. package/dist/core/agent/buildMessageApiChart.js.map +1 -1
  12. package/dist/core/agent/composeRequest.js +134 -0
  13. package/dist/core/agent/composeRequest.js.map +1 -0
  14. package/dist/core/agent/stages/callLLM.js +62 -25
  15. package/dist/core/agent/stages/callLLM.js.map +1 -1
  16. package/dist/core/agent/stages/seed.js +12 -0
  17. package/dist/core/agent/stages/seed.js.map +1 -1
  18. package/dist/core/agent/stages/toolCalls.js +2 -4
  19. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  20. package/dist/core/flowchartAsTool.js.map +1 -1
  21. package/dist/esm/core/Agent.d.ts +3 -0
  22. package/dist/esm/core/Agent.js +32 -0
  23. package/dist/esm/core/Agent.js.map +1 -1
  24. package/dist/esm/core/LLMCall.js +7 -10
  25. package/dist/esm/core/LLMCall.js.map +1 -1
  26. package/dist/esm/core/agent/buildAgentMessageApiChart.js +2 -4
  27. package/dist/esm/core/agent/buildAgentMessageApiChart.js.map +1 -1
  28. package/dist/esm/core/agent/buildDynamicAgentChart.js +10 -0
  29. package/dist/esm/core/agent/buildDynamicAgentChart.js.map +1 -1
  30. package/dist/esm/core/agent/buildMessageApiChart.js +2 -4
  31. package/dist/esm/core/agent/buildMessageApiChart.js.map +1 -1
  32. package/dist/esm/core/agent/composeRequest.d.ts +121 -0
  33. package/dist/esm/core/agent/composeRequest.js +127 -0
  34. package/dist/esm/core/agent/composeRequest.js.map +1 -0
  35. package/dist/esm/core/agent/stages/callLLM.d.ts +28 -0
  36. package/dist/esm/core/agent/stages/callLLM.js +61 -24
  37. package/dist/esm/core/agent/stages/callLLM.js.map +1 -1
  38. package/dist/esm/core/agent/stages/seed.d.ts +26 -0
  39. package/dist/esm/core/agent/stages/seed.js +12 -0
  40. package/dist/esm/core/agent/stages/seed.js.map +1 -1
  41. package/dist/esm/core/agent/stages/toolCalls.js +2 -4
  42. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  43. package/dist/esm/core/agent/types.d.ts +64 -0
  44. package/dist/esm/core/flowchartAsTool.d.ts +15 -7
  45. package/dist/esm/core/flowchartAsTool.js.map +1 -1
  46. package/dist/esm/index.d.ts +1 -0
  47. package/dist/esm/index.js +9 -0
  48. package/dist/esm/index.js.map +1 -1
  49. package/dist/esm/lib/context-bisect/llmEdgeWeigher.d.ts +8 -0
  50. package/dist/esm/lib/context-bisect/llmEdgeWeigher.js +0 -0
  51. package/dist/esm/lib/context-bisect/llmEdgeWeigher.js.map +1 -1
  52. package/dist/esm/lib/context-bisect/trajectory.js +3 -7
  53. package/dist/esm/lib/context-bisect/trajectory.js.map +1 -1
  54. package/dist/esm/lib/context-ledger/contextLedger.js +5 -12
  55. package/dist/esm/lib/context-ledger/contextLedger.js.map +1 -1
  56. package/dist/esm/lib/time-travel/epochs.d.ts +210 -0
  57. package/dist/esm/lib/time-travel/epochs.js +274 -0
  58. package/dist/esm/lib/time-travel/epochs.js.map +1 -0
  59. package/dist/esm/lib/time-travel/index.d.ts +10 -4
  60. package/dist/esm/lib/time-travel/index.js +10 -4
  61. package/dist/esm/lib/time-travel/index.js.map +1 -1
  62. package/dist/esm/lib/time-travel/keyedFold.d.ts +112 -0
  63. package/dist/esm/lib/time-travel/keyedFold.js +286 -0
  64. package/dist/esm/lib/time-travel/keyedFold.js.map +1 -0
  65. package/dist/esm/lib/time-travel/milestoneStops.d.ts +57 -36
  66. package/dist/esm/lib/time-travel/milestoneStops.js +75 -82
  67. package/dist/esm/lib/time-travel/milestoneStops.js.map +1 -1
  68. package/dist/esm/lib/time-travel/receipt.d.ts +496 -0
  69. package/dist/esm/lib/time-travel/receipt.js +434 -0
  70. package/dist/esm/lib/time-travel/receipt.js.map +1 -0
  71. package/dist/esm/lib/time-travel/servedView.d.ts +525 -0
  72. package/dist/esm/lib/time-travel/servedView.js +820 -0
  73. package/dist/esm/lib/time-travel/servedView.js.map +1 -0
  74. package/dist/esm/lib/time-travel/sha256.d.ts +24 -0
  75. package/dist/esm/lib/time-travel/sha256.js +103 -0
  76. package/dist/esm/lib/time-travel/sha256.js.map +1 -0
  77. package/dist/esm/recorders/observability/BoundaryRecorder.d.ts +21 -0
  78. package/dist/esm/recorders/observability/BoundaryRecorder.js +6 -0
  79. package/dist/esm/recorders/observability/BoundaryRecorder.js.map +1 -1
  80. package/dist/index.js +124 -103
  81. package/dist/index.js.map +1 -1
  82. package/dist/lib/context-bisect/llmEdgeWeigher.js +0 -0
  83. package/dist/lib/context-bisect/llmEdgeWeigher.js.map +1 -1
  84. package/dist/lib/context-bisect/trajectory.js +3 -7
  85. package/dist/lib/context-bisect/trajectory.js.map +1 -1
  86. package/dist/lib/context-ledger/contextLedger.js +5 -12
  87. package/dist/lib/context-ledger/contextLedger.js.map +1 -1
  88. package/dist/lib/time-travel/epochs.js +284 -0
  89. package/dist/lib/time-travel/epochs.js.map +1 -0
  90. package/dist/lib/time-travel/index.js +35 -5
  91. package/dist/lib/time-travel/index.js.map +1 -1
  92. package/dist/lib/time-travel/keyedFold.js +290 -0
  93. package/dist/lib/time-travel/keyedFold.js.map +1 -0
  94. package/dist/lib/time-travel/milestoneStops.js +74 -81
  95. package/dist/lib/time-travel/milestoneStops.js.map +1 -1
  96. package/dist/lib/time-travel/receipt.js +442 -0
  97. package/dist/lib/time-travel/receipt.js.map +1 -0
  98. package/dist/lib/time-travel/servedView.js +826 -0
  99. package/dist/lib/time-travel/servedView.js.map +1 -0
  100. package/dist/lib/time-travel/sha256.js +107 -0
  101. package/dist/lib/time-travel/sha256.js.map +1 -0
  102. package/dist/recorders/observability/BoundaryRecorder.js +6 -0
  103. package/dist/recorders/observability/BoundaryRecorder.js.map +1 -1
  104. package/dist/types/core/Agent.d.ts +3 -0
  105. package/dist/types/core/Agent.d.ts.map +1 -1
  106. package/dist/types/core/LLMCall.d.ts.map +1 -1
  107. package/dist/types/core/agent/buildAgentMessageApiChart.d.ts.map +1 -1
  108. package/dist/types/core/agent/buildDynamicAgentChart.d.ts.map +1 -1
  109. package/dist/types/core/agent/buildMessageApiChart.d.ts.map +1 -1
  110. package/dist/types/core/agent/composeRequest.d.ts +122 -0
  111. package/dist/types/core/agent/composeRequest.d.ts.map +1 -0
  112. package/dist/types/core/agent/stages/callLLM.d.ts +28 -0
  113. package/dist/types/core/agent/stages/callLLM.d.ts.map +1 -1
  114. package/dist/types/core/agent/stages/seed.d.ts +26 -0
  115. package/dist/types/core/agent/stages/seed.d.ts.map +1 -1
  116. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  117. package/dist/types/core/agent/types.d.ts +64 -0
  118. package/dist/types/core/agent/types.d.ts.map +1 -1
  119. package/dist/types/core/flowchartAsTool.d.ts +15 -7
  120. package/dist/types/core/flowchartAsTool.d.ts.map +1 -1
  121. package/dist/types/index.d.ts +1 -0
  122. package/dist/types/index.d.ts.map +1 -1
  123. package/dist/types/lib/context-bisect/llmEdgeWeigher.d.ts +8 -0
  124. package/dist/types/lib/context-bisect/llmEdgeWeigher.d.ts.map +1 -1
  125. package/dist/types/lib/context-bisect/trajectory.d.ts.map +1 -1
  126. package/dist/types/lib/context-ledger/contextLedger.d.ts.map +1 -1
  127. package/dist/types/lib/time-travel/epochs.d.ts +211 -0
  128. package/dist/types/lib/time-travel/epochs.d.ts.map +1 -0
  129. package/dist/types/lib/time-travel/index.d.ts +10 -4
  130. package/dist/types/lib/time-travel/index.d.ts.map +1 -1
  131. package/dist/types/lib/time-travel/keyedFold.d.ts +113 -0
  132. package/dist/types/lib/time-travel/keyedFold.d.ts.map +1 -0
  133. package/dist/types/lib/time-travel/milestoneStops.d.ts +57 -36
  134. package/dist/types/lib/time-travel/milestoneStops.d.ts.map +1 -1
  135. package/dist/types/lib/time-travel/receipt.d.ts +497 -0
  136. package/dist/types/lib/time-travel/receipt.d.ts.map +1 -0
  137. package/dist/types/lib/time-travel/servedView.d.ts +526 -0
  138. package/dist/types/lib/time-travel/servedView.d.ts.map +1 -0
  139. package/dist/types/lib/time-travel/sha256.d.ts +25 -0
  140. package/dist/types/lib/time-travel/sha256.d.ts.map +1 -0
  141. package/dist/types/recorders/observability/BoundaryRecorder.d.ts +21 -0
  142. package/dist/types/recorders/observability/BoundaryRecorder.d.ts.map +1 -1
  143. package/package.json +3 -3
@@ -0,0 +1,826 @@
1
+ "use strict";
2
+ /**
3
+ * servedView — the request an epoch was SERVED, rebuilt from what was
4
+ * committed, plus an honest list of what the log cannot rebuild.
5
+ *
6
+ * Role: Fold. `servedAt` reads a finished run's commit log and returns the
7
+ * model-facing request that epoch's call assembled; `receiptAt` reads
8
+ * the fingerprint that call left behind. Neither executes anything,
9
+ * records anything, or stores anything beside the log.
10
+ * Reads: a recording, through `epochs.ts` (which log, which index) and
11
+ * footprintjs's `commitValueAt`.
12
+ * Emits: N/A.
13
+ *
14
+ * ── THE LAW ────────────────────────────────────────────────────────────────
15
+ *
16
+ * hash(servedAt(k)) === receiptAt(k).hash
17
+ *
18
+ * The receipt is minted at the stop, from the values about to go out. The
19
+ * served view is rebuilt afterwards, from the values that were committed. When
20
+ * the two agree, the record is complete: everything the model read is derivable
21
+ * from the trace. When they disagree, something reached the model that the run
22
+ * never wrote down — and that is a defect in the RECORD, not in the check.
23
+ * `test/lib/time-travel/receipt-conformance.test.ts` is where the law is
24
+ * enforced; five real divergences were found by running it and each one was
25
+ * closed by committing a fact or declaring a gap, never by loosening it.
26
+ *
27
+ * ── WHY THE REBUILD DOES NOT RE-IMPLEMENT THE ASSEMBLY ─────────────────────
28
+ * The joined system string is a local inside the calling stage; it is never
29
+ * committed, only its pieces are. So the rebuild has to apply the identical
30
+ * join to the identical records — which is why the join is one exported
31
+ * function, `composeRequest.ts` · `joinSystemPrompt`, called by the stage on
32
+ * the way out and by this file on the way back. Same for the framework-field
33
+ * strip and the staged-refs nudge. A second implementation of any of them would
34
+ * make this file agree with itself and disagree with the wire.
35
+ *
36
+ * ── WHAT A GAP IS ──────────────────────────────────────────────────────────
37
+ * A `ServedGap` is a fact this view cannot PROVE — because the log cannot
38
+ * rebuild it, because the recording travelled without the base it folds
39
+ * against, or because the fact lives past the boundary a receipt is minted at.
40
+ * It is on the view rather than left implicit because a rebuild that quietly
41
+ * omits a piece looks exactly like a rebuild that proved the piece was absent.
42
+ * Naming the gap is what keeps the two apart.
43
+ *
44
+ * ── A GAP, NOT AN `undefined` ──────────────────────────────────────────────
45
+ * When the rebuild cannot recover the conversation at all — a chart that
46
+ * commits neither `history` nor `messagesInjections`, or a recording whose
47
+ * fold base did not travel — `servedAt` still returns a view, carrying a gap,
48
+ * rather than `undefined`.
49
+ *
50
+ * The alternative was tried and it is worse. Returning `undefined` withholds
51
+ * the system prompt, the tool list and the request-only lines, all of which
52
+ * this recording CAN prove, in order to avoid overstating one field it cannot.
53
+ * A Lens may omit; it may not deny — and dropping four proved facts to hide
54
+ * one hole omits more than the hole does. The gap says exactly which field is
55
+ * unproved, in wording a renderer prints verbatim, and leaves the rest
56
+ * standing.
57
+ *
58
+ * ── A CHART THAT MINTS NO RECEIPT DECLARES, IT DOES NOT MINT ───────────────
59
+ * `buildReceipt` is called from exactly one place: the agent charts' `call-llm`
60
+ * stage (`stages/callLLM.ts`). `LLMCall` and the two message-API charts
61
+ * (`buildMessageApiChart`, `buildAgentMessageApiChart`) run their own `call-llm`
62
+ * stage, so `epochAt` locates their epochs and `servedAt` rebuilds their views
63
+ * — but no receipt is minted there, `ServedView.basis` is dropped, and until
64
+ * 9.88.0 nothing said why. That is the Lens denying: a reader saw a view with
65
+ * no model on it and no sentence explaining the absence.
66
+ *
67
+ * MINTING was the other option and it was refused. The reason that holds is
68
+ * narrow and applies to the two message-API charts alone: they are exported
69
+ * chart BUILDERS run on a consumer's own executor and their deps carry no run
70
+ * id, so their hashes could not be salted. It is NOT true of `LLMCall`, which
71
+ * owns its executor and mints a run id exactly as `Agent` does — a paragraph
72
+ * here said otherwise until 9.88.0, in a sentence a renderer printed verbatim.
73
+ *
74
+ * A SECOND REASON WAS RECORDED AT THE TIME AND DOES NOT HOLD, and it is left
75
+ * here refuted rather than deleted so nobody argues it again:
76
+ * `Receipt.cache.transform` was said to have no honest value on a chart that
77
+ * runs no cache strategy, because `'unchanged'` would claim a strategy
78
+ * returned what it was given when none ran. But `Agent` runs with no cache
79
+ * strategy record exactly that today — `buildReceipt` compares the request it
80
+ * was handed against itself and writes `'unchanged'` — and
81
+ * `receipt-conformance.test.ts` asserts it and calls it correct. So NO
82
+ * recorded reason survives for `LLMCall`: it has the salt and it needs no
83
+ * fourth enum value. It runs its own `call-llm` (`LLMCall.ts` · `callLLM`),
84
+ * that stage does not call `buildReceipt`, and until somebody makes it the
85
+ * absence is DECLARED rather than argued away.
86
+ *
87
+ * So the absence is DECLARED: `no-receipt-on-chart` names the fields only a
88
+ * receipt carries and says what follows for them. The rebuild never needed the
89
+ * receipt to work, and `servedAt`'s own `@param` is true about `LLMCall`
90
+ * because the view it returns declares what it has and what it has not.
91
+ *
92
+ * ── AND THE CAUSE IS A VALUE, NOT A LIST INSIDE A SENTENCE ────────────────
93
+ * That gap's sentence used to end with the causes that produce it. That is
94
+ * PROSE DOING DATA'S JOB: a frozen constant cannot know what happened at the
95
+ * site it is printed beside, so it listed the causes somebody could think of —
96
+ * and a path it had never accounted for, the shape refusal in `readReceipt`
97
+ * below, made the count AND the verdict false with nobody editing the string.
98
+ * The discriminating fact moved to the one place that holds it: `readReceipt`
99
+ * reports whether the receipt key held nothing or held something it refused,
100
+ * and {@link ServedGap.cause} carries that answer out.
101
+ *
102
+ * ── AND THEN THE MECHANISM WENT TOO (9.88.0, sixth round) ─────────────────
103
+ * Five rounds tried to write TRUE mechanism sentences, and the rate of new
104
+ * falsehoods did not fall. The sixth round applied one question to all ten
105
+ * printed sentences — COULD THIS BECOME FALSE WITHOUT ANYONE EDITING IT? —
106
+ * and nine could, two of them being false the day they shipped. Exactly one
107
+ * could not, and it is the one that makes no claim about code at all:
108
+ * `UNGAPPED_FIELDS.gaps`, which says what its field MEANS inside the account.
109
+ * Nothing outside that sentence can falsify it.
110
+ *
111
+ * So the conclusion was not to write mechanism sentences better. It was to
112
+ * STOP WRITING THEM. A printed gap sentence may now say only: which fields it
113
+ * covers; what they mean on this view for the person reading (unproven,
114
+ * possibly short, absent-means-unknown, unchecked against what went out); and
115
+ * what to do differently. It may not name a module, a function, a key, a
116
+ * version, a chart, a strategy, an option, or any mechanism at all. If a
117
+ * sentence needs one of those words to be understood, it is explaining WHY the
118
+ * gap exists, which is not the printed sentence's job.
119
+ *
120
+ * NONE OF IT IS LOST. The mechanism is in the comment above each catalogue
121
+ * entry, where a maintainer reads it and review catches its rot, with the
122
+ * `file · symbol` pointers that are correct there and banned in printed prose.
123
+ * The cause is already data. The docs may explain the mechanism at length,
124
+ * because a doc is versioned with the code and its reader can open the file.
125
+ * The rule is enforced by `test/helpers/gapProseClaims.ts`, which on the
126
+ * printed surface admits no code-shaped token and no mechanism verb — close to
127
+ * a whitelist, and a whitelist has no synonyms.
128
+ *
129
+ * ── AND THE SEVENTH ROUND MEASURED WHAT THE SIXTH HAD CLAIMED ─────────────
130
+ * The sixth round said the reduction ENDED the class: a sentence with no code
131
+ * claim in it cannot go false when the code changes. That was checked, sentence
132
+ * by sentence, by driving a real run for each one — and it is not true. TEN of
133
+ * the eleven reduced sentences still make a claim a code edit falsifies. The
134
+ * one that does not is `UNGAPPED_FIELDS.gaps`, and it survives because it is
135
+ * SELF-REFERENTIAL: it says what its field is inside the account, not anything
136
+ * about the request. That is not a shape the other ten can copy. "May be
137
+ * SHORT", "absent means unknown", "the list is complete and the schemas are one
138
+ * short" are all claims about how the rebuild behaves, and the rebuild is code.
139
+ * The reduction changed the VOCABULARY of the claims, not their CLASS.
140
+ *
141
+ * The same round proved what does close it, by finding a BRAND-NEW false
142
+ * sentence in the round written to end false sentences: `no-run-log` shipped
143
+ * "The fields below could not be fully recovered here", and on the ordinary
144
+ * view that raises it nothing is lost at all. No rule caught that. A run
145
+ * caught it.
146
+ *
147
+ * SO THE POSITION IS: a gap sentence MAY make a code claim, because a sentence
148
+ * that makes none cannot inform. Every claim it makes is ASSERTED against a
149
+ * real view in `test/lib/time-travel/gap-sentences.test.ts`, which drives one
150
+ * run per entry and checks the claim rather than the firing. The prose rule
151
+ * STAYS — it keeps the sentences short and readable and stops the enumerations
152
+ * coming back — but it is not what makes them true. The assertion is, and its
153
+ * blind spot is honest and small: a claim nobody wrote an assertion for.
154
+ */
155
+ Object.defineProperty(exports, "__esModule", { value: true });
156
+ exports.receiptAt = exports.servedViews = exports.servedAt = exports.UNGAPPED_FIELDS = exports.SERVED_GAPS = void 0;
157
+ const composeRequest_js_1 = require("../../core/agent/composeRequest.js");
158
+ const stagedRefs_js_1 = require("../../core/agent/stagedRefs.js");
159
+ const epochs_js_1 = require("./epochs.js");
160
+ const receipt_js_1 = require("./receipt.js");
161
+ /**
162
+ * The gap catalogue. Exported because a reader that renders a served view
163
+ * renders its gaps beside it, and a renderer should print the library's own
164
+ * sentence rather than invent one.
165
+ */
166
+ exports.SERVED_GAPS = Object.freeze({
167
+ 'cache-transform': Object.freeze({
168
+ // MECHANISM (not printed — see the header's rule). A `CacheStrategy`'s
169
+ // `prepareRequest` is handed the whole composed request after assembly and
170
+ // hands one back; `buildReceipt` fingerprints the one it was GIVEN. So the
171
+ // field list is everything a rewrite could have touched, not only the
172
+ // three `cache.*` fields that DESCRIBE the rewrite — listing those alone
173
+ // named the report and excused nothing it reports on.
174
+ //
175
+ // THE DAY-ONE FALSEHOOD THIS ENTRY SHIPPED WITH, killed in 9.88.0's sixth
176
+ // round: the printed sentence said "only its INPUTS are on the record".
177
+ // Three of its own fields are OUTPUTS and are on the record —
178
+ // `cache.transform` (the verdict of comparing given against returned),
179
+ // `cache.transformHash` (the digest of what came back when it differed)
180
+ // and `cache.markersApplied` (the breakpoints the strategy actually
181
+ // applied, as against the candidates in `scope.cacheMarkers`, which are
182
+ // the inputs). A sentence describing the mechanism went false about the
183
+ // mechanism; the sentence below describes only what the fields MEAN.
184
+ //
185
+ // This entry is on EVERY view, so it is the weakest claim on the list: it
186
+ // says the rebuild stops before whatever the request met next, never that
187
+ // the rebuild got that far. Whether it did is what the other gaps on the
188
+ // same view say — see the qualifier on `ServedGap.fields`.
189
+ //
190
+ // AND BEING ON EVERY VIEW IS WHY IT NO LONGER QUOTES `RECEIPT_BOUNDARY`
191
+ // (9.88.0, seventh round). That sentence opens "A receipt describes the
192
+ // request as this library last saw it" — and this entry is printed on views
193
+ // that have no receipt at all. Measured: an `LLMCall` view carries exactly
194
+ // `['no-receipt-on-chart', 'cache-transform']`, so the reader was told what
195
+ // a receipt describes beside a view that has none. The boundary CLAIM is
196
+ // not lost — the first sentence below is that claim in the vocabulary of a
197
+ // view, and it is true whether or not a receipt exists. The quote stays on
198
+ // `provider-defaults`, which fires only where a receipt was read.
199
+ fields: Object.freeze([
200
+ 'cache.transform',
201
+ 'cache.transformHash',
202
+ 'cache.markersApplied',
203
+ 'system.hash',
204
+ 'system.chars',
205
+ 'system.pieces',
206
+ 'messages.count',
207
+ 'messages.entries',
208
+ 'messages.requestOnly',
209
+ 'tools.names',
210
+ 'tools.schemaHashes',
211
+ ]),
212
+ why: 'What reached the provider may differ from the fields below, and nothing on this view ' +
213
+ 'would show it. Where another gap on this view covers one of them, that gap is the ' +
214
+ 'stronger claim.',
215
+ }),
216
+ 'forced-tool-schema': Object.freeze({
217
+ // MECHANISM (not printed). Under a `'tool-forced'` output strategy the
218
+ // synthetic answer tool is added at assembly from a build-time schema that
219
+ // no stage commits, so `tools.schemaHashes` on the receipt carries a row
220
+ // the rebuild has no counterpart for. The tool's NAME is a run constant
221
+ // (`FORCED_OUTPUT_TOOL_KEY`, written by `stages/seed.ts`), which is why
222
+ // `tools.names` and `tools.forced` rebuild and the schema body does not.
223
+ fields: Object.freeze(['tools.schemaHashes']),
224
+ why: 'The schema body behind the forced answer tool is not on this view. Its name is, so the ' +
225
+ 'tool list is complete and the schemas beside it are one short.',
226
+ }),
227
+ 'provider-defaults': Object.freeze({
228
+ // MECHANISM (not printed). `buildReceipt` reads `params` off the PREPARED
229
+ // request — the object `LLMProvider.complete` is handed, after any cache
230
+ // strategy has had it — so it is the one part of a receipt read past the
231
+ // strategy, and `cache-transform` deliberately does not name it. What is
232
+ // still past the record is the vendor: an adapter or SDK can resolve a
233
+ // final value the port never saw. `params` is also VALUE-CONDITIONAL —
234
+ // `buildReceipt` writes no key for a dial nobody set — which is the whole
235
+ // reason the printed sentence has to say what an absent key means.
236
+ //
237
+ // THE ONE ENTRY THAT QUOTES `RECEIPT_BOUNDARY`, and it is the only one that
238
+ // honestly can: it is pushed inside `if (receipt !== undefined)`, so a view
239
+ // carrying it always has a receipt for that sentence to be about.
240
+ // `cache-transform` quoted it too until 9.88.0's seventh round, and
241
+ // `cache-transform` is on every view — including the receipt-less ones,
242
+ // where the reader was told what a receipt describes beside a view that has
243
+ // none. Measured: an `LLMCall` view carries `no-receipt-on-chart` and
244
+ // `cache-transform`, and never this entry.
245
+ fields: Object.freeze(['params']),
246
+ why: 'A dial absent below was not recorded; that is not the same as the model running ' +
247
+ 'without one. ' +
248
+ receipt_js_1.RECEIPT_BOUNDARY,
249
+ }),
250
+ 'no-fold-base': Object.freeze({
251
+ // MECHANISM (not printed). Raised when `location.basis` or
252
+ // `location.runBasis` is `'log-only'` — the recording travelled without the
253
+ // `initialState` the fold folds against (`getSnapshot({ redact: true })`
254
+ // hands back exactly that shape: the log travels, the base does not), so
255
+ // every read can see only what the log itself wrote. A rebuild that
256
+ // recovered one turn of three reports one; a system prompt it could not
257
+ // read at all is 0 characters rather than unknown; and `EpochLocation.epoch`
258
+ // falls back to POSITION when it cannot read the run's committed
259
+ // `iteration`, which is how a view can call the second turn the first while
260
+ // that turn's own receipt still says 2.
261
+ //
262
+ // TWO bases, so every field the rebuild reads through a fold. The epoch's
263
+ // own log rebuilds the system prompt, the conversation, the tool schemas
264
+ // and the wrap-up flag; the RUN's log holds the run constants the forced
265
+ // tool name and the staged-refs nudge are composed from. Under
266
+ // 'dynamic-grouped' those are different logs with different bases, and a
267
+ // gap raised off only the first denied the second.
268
+ //
269
+ // THE RULE FOR THIS LIST is mechanical, so it does not go short again: a
270
+ // field belongs here when the rebuild derives it from `readAtCall` or
271
+ // `readRunConstant` — both fold over values the log itself may never have
272
+ // written, so both read absent when the base did not travel. `readAfterCall`
273
+ // is the THIRD reader and is deliberately NOT on the rule: the only thing
274
+ // read through it is the receipt, which the call's own bundle commits, so it
275
+ // is there whatever the base. Naming it made the rule read wider than the
276
+ // mechanism and put the receipt's own fields inside a gap that cannot touch
277
+ // them. `system.chars` and `messages.count` were missed on the first two
278
+ // passes because they are COUNTS of things the list already named, and a
279
+ // short rebuild makes a count wrong exactly as it makes the thing wrong.
280
+ //
281
+ // `epoch` — THE VIEW'S OWN NUMBER — is here for a fourth reason, in
282
+ // `EpochLocation.epoch`'s own words: a fold that cannot read `iteration`
283
+ // numbers the epoch by its POSITION instead. Measured on a resumed run with
284
+ // its base and its `iteration` writes gone, the rebuild called the second
285
+ // turn epoch 1 while that turn's own receipt still said 2. The RECEIPT's
286
+ // `basis.epoch` is NOT here and used to be: it is minted live and rides in
287
+ // the call's bundle, so no missing base can move it. That inversion — the
288
+ // list naming the number a missing base cannot touch, while the number it
289
+ // fabricates was excused as ungappable — is what 9.88.0's fourth review
290
+ // round found.
291
+ //
292
+ // THE PRINTED CLAUSE STOPPED NAMING THE RECEIPT (seventh round). It read
293
+ // "may differ from the one the receipt for this turn carries", and this
294
+ // gap is raised on views that have no receipt to carry anything — a
295
+ // base-less `LLMCall` recording gets both gaps at once. The fact a reader
296
+ // needs is about the NUMBER, not about the witness: it may be the turn's
297
+ // place in run order rather than the count the run kept. Measured on a
298
+ // resumed run with its base and its `iteration` writes gone, the view calls
299
+ // the second turn the first while that turn's own receipt still says 2.
300
+ fields: Object.freeze([
301
+ 'system.hash',
302
+ 'system.chars',
303
+ 'system.pieces',
304
+ 'messages.count',
305
+ 'messages.entries',
306
+ 'messages.requestOnly',
307
+ 'tools.schemaHashes',
308
+ 'tools.names',
309
+ 'tools.forced',
310
+ 'tools.withheld',
311
+ 'epoch',
312
+ ]),
313
+ why: 'The fields below are unproven and may be SHORT: a count can be lower than what really ' +
314
+ 'went out, and a value that could not be recovered reads as empty rather than as ' +
315
+ "unknown. The turn number below may be this turn's place in run order rather than the " +
316
+ 'number the run itself gave it.',
317
+ }),
318
+ 'no-conversation-on-record': Object.freeze({
319
+ // MECHANISM (not printed). `viewOf` reads the conversation from `history`
320
+ // (the agent charts) or, failing that, from `messagesInjections` (`LLMCall`
321
+ // and the message-API charts, which have no history at all). Neither
322
+ // committed ⇒ `conversation` is `undefined` and `asSent` is `[]`, which is
323
+ // indistinguishable from a call that really sent nothing — hence the gap.
324
+ //
325
+ // `messages.requestOnly` is on the list for a second reason: the
326
+ // staged-refs nudge is recomposed FROM the conversation by `findStagedRefs`,
327
+ // so a rebuild with no conversation finds no refs and reports no nudge,
328
+ // which looks exactly like a call that had none.
329
+ fields: Object.freeze(['messages.count', 'messages.entries', 'messages.requestOnly']),
330
+ why: 'The turns that went out are unknown, not empty: an empty list here is the absence of a ' +
331
+ 'record, never a record of absence. The request-only lines are unproved with them.',
332
+ }),
333
+ 'no-receipt-on-chart': Object.freeze({
334
+ // Exactly the fields ONLY a receipt carries — the ones no rebuild produces,
335
+ // so with no receipt they are absent rather than short. Everything else on
336
+ // the view is folded from committed pieces as usual; what it loses is not
337
+ // the value but the WITNESS, and the sentence below says so, because a
338
+ // field list cannot.
339
+ //
340
+ // MECHANISM (not printed). Raised when `readReceipt` returns no receipt —
341
+ // either nothing was committed under `RECEIPT_KEY` (a recording made before
342
+ // the receipt existed, a run with `recordReceipt: false`, or a chart whose
343
+ // `call-llm` stage mints none: `LLMCall` and the two message-API charts) or
344
+ // something WAS committed there and was refused for carrying no basis.
345
+ // Which of the two is {@link ServedGap.cause}, computed at that read.
346
+ // Everything else on the view is still folded from committed pieces; what
347
+ // it loses is the WITNESS, not the value.
348
+ //
349
+ // THE DAY-ONE FALSEHOOD THIS ENTRY SHIPPED WITH, killed in 9.88.0's sixth
350
+ // round: the printed sentence opened "No receipt was found for this epoch"
351
+ // and closed "absent here means unrecorded". Both are false under
352
+ // `'receipt-shape-rejected'`, where a value WAS found and WAS recorded and
353
+ // the read refused it. The sentence asserted a cause; the cause is a field.
354
+ // The sentence below says only what the fields mean, which is the same
355
+ // under either cause.
356
+ //
357
+ // TWO CHANGES in 9.88.0's fourth review round, both of them the same test:
358
+ // does this gap's MECHANISM cause this field's absence?
359
+ // • `basis.epoch` JOINED the list. It is a receipt-only field like the
360
+ // other three on `basis`, and with no receipt it is gone with them. It
361
+ // used to be named by `no-fold-base` alone, which cannot touch it.
362
+ // • `omittedForAttention` LEFT it. It is absent on EVERY view, receipt or
363
+ // not — no chart IN THIS LIBRARY supplies it — so a gap about the
364
+ // missing receipt was explaining an absence it does not cause, and on a
365
+ // view that HAS a receipt nothing explained it at all. It is in
366
+ // `UNGAPPED_FIELDS` now, with the reason that is actually true of it.
367
+ //
368
+ // AND THE SEVENTH ROUND SCOPED THE PRINTED CLAIM TO THE LEVEL IT HOLDS AT.
369
+ // It said the absence of these fields is "a gap in the record, never a call
370
+ // made without them". True of each field AS A WHOLE — a receipt always
371
+ // carries `params` and always carries a `cache.transform` verdict, so those
372
+ // containers go missing only with the receipt. FALSE one level down, and
373
+ // that is the level a reader reads at: measured on an agent that set no
374
+ // dials, `receipt.params` is `{}` — an absence inside the container that IS
375
+ // "a call made without one", and `provider-defaults` says so on the very
376
+ // views that have a receipt. The sentence below claims nothing about what
377
+ // is inside a field it cannot see.
378
+ fields: Object.freeze([
379
+ 'basis.model',
380
+ 'basis.provider',
381
+ 'basis.runId',
382
+ 'basis.epoch',
383
+ 'params',
384
+ 'cache.transform',
385
+ 'cache.transformHash',
386
+ 'cache.markersApplied',
387
+ ]),
388
+ why: 'Nothing on this view has been checked against what went out. Every field below is ' +
389
+ 'missing as a whole, and an absence among them says nothing about the call — not even ' +
390
+ 'that a dial was left unset.',
391
+ }),
392
+ 'no-run-log': Object.freeze({
393
+ // MECHANISM (not printed). Raised on `!location.hasRunLog` — a SUBTREE
394
+ // handed in on its own, with no run log to read. `readRunConstant` is the
395
+ // only reader that touches that log, and everything it fetches is a
396
+ // build-time fact `stages/seed.ts` wrote once: the forced output tool's
397
+ // NAME (`FORCED_OUTPUT_TOOL_KEY`) and `toolWantsByName`, which
398
+ // `findStagedRefs` composes the nudge from.
399
+ //
400
+ // `tools.schemaHashes` is on the list because the forced tool's NAME is the
401
+ // run constant: losing it also loses the row the receipt hashes under that
402
+ // name. The fix a reader has is to pass the whole recording rather than the
403
+ // subtree, which is what the printed sentence says without naming any of
404
+ // this.
405
+ //
406
+ // THE MEASURED-FALSE SENTENCE, killed in 9.88.0's seventh round. It said
407
+ // "The fields below could not be fully recovered here" — a claim that the
408
+ // recovery DID fail. Driven on the ordinary view that raises this gap (a
409
+ // 'dynamic-grouped' agent with one plain tool, run log emptied), the
410
+ // damaged rebuild is byte-identical to the intact one: same `tools.names`,
411
+ // same `tools.schemas`, same absent `tools.forced`, same empty
412
+ // `messages.requestOnly`. The gap fires and costs NOTHING, and the sentence
413
+ // told the reader otherwise.
414
+ //
415
+ // THE REPAIR IS THE SENTENCE, NOT THE CONDITION, and the reason is that the
416
+ // condition cannot be narrowed by anything this file can read. What the
417
+ // missing log would have held is a forced tool's name and a `wants`
418
+ // declaration; whether the run had either is recorded IN THAT LOG. To fire
419
+ // the gap only where it costs something, the read would have to consult the
420
+ // record whose absence raises it. So the gap stays unconditional — it is
421
+ // the "you cannot tell" declaration, which is what this file is for — and
422
+ // the sentence became the conditional it always was: the fields MAY be
423
+ // short. Both directions are measured beside it in
424
+ // `gap-sentences.test.ts`: emptying the run log on a forced-output run
425
+ // takes the tool list from one name to none, on a staged-refs run it takes
426
+ // away the request-only line, and on the plain run it takes nothing.
427
+ fields: Object.freeze([
428
+ 'tools.names',
429
+ 'tools.forced',
430
+ 'tools.schemaHashes',
431
+ 'messages.requestOnly',
432
+ ]),
433
+ why: 'The fields below may be SHORT: a name can be missing from the tool list, and a line ' +
434
+ 'that went out with the request can be missing too. An absence below is not evidence ' +
435
+ 'that there was nothing there — read the whole recording rather than a piece of it.',
436
+ }),
437
+ });
438
+ /**
439
+ * The fields no gap names, and the reason each one needs none — the OTHER half
440
+ * of the account.
441
+ *
442
+ * WHY IT EXISTS. `SERVED_GAPS` was hand-checked against the two shapes three
443
+ * times in one release and came up short every time, because "is every field
444
+ * named by a gap?" was a question a person answered by reading. It is now a
445
+ * question a walk answers: `test/lib/time-travel/gap-catalogue-walk.test.ts`
446
+ * enumerates every field a real `Receipt` and a real `ServedView` carry and
447
+ * requires each one to be named by a gap OR to be a key here. A field in
448
+ * neither fails, by name.
449
+ *
450
+ * So this is not an exemption list. It is the place a field goes when NO GAP'S
451
+ * MECHANISM EXPLAINS IT, and the value is the reason in one sentence, for the
452
+ * next person who asks why the field has no gap. Adding a key here is as
453
+ * reviewable as adding one to a gap, and that is the point: both are a claim
454
+ * somebody wrote down.
455
+ *
456
+ * TWO reasons qualify, and they are not the same reason:
457
+ *
458
+ * • NO FOLD CAN FAIL TO PRODUCE IT — the field is read straight off the
459
+ * located epoch, never through a fold. `callRuntimeStageId` is this kind.
460
+ * • ITS ABSENCE IS UNIVERSAL AND HAS NOTHING TO DO WITH THIS RECORDING — no
461
+ * chart IN THIS LIBRARY supplies it, on any run, so no gap about a limit of
462
+ * the rebuild describes it. `omittedForAttention` is this kind, and it was
463
+ * inside `no-receipt-on-chart` until 9.88.0's fourth review round, where a
464
+ * gap that fires on some views was carrying an absence that is on all of
465
+ * them. The narrowing to THIS LIBRARY is load-bearing: `buildReceipt` is a
466
+ * pure exported mint, so a consumer that calls it can hand it the fact, and
467
+ * a sentence saying no chart anywhere supplies one would be false the day
468
+ * somebody did.
469
+ *
470
+ * A field a gap DOES name never belongs here, whatever else is also true of it.
471
+ * `epoch` was a key here through three rounds, on the true-but-irrelevant
472
+ * ground that `servedAt(k)` hands `k` back; what a base-less fold changes is
473
+ * what that number MEANS, `servedViews()` returns the fold's number outright,
474
+ * and `no-fold-base` names it now.
475
+ *
476
+ * Paths are spelled as they are on the shape that HAS the field: `ServedView`
477
+ * for a view field, `Receipt` for `omittedForAttention`, which is a receipt
478
+ * field the view has no counterpart for. Gap `fields` are spelled as `Receipt`
479
+ * paths — see {@link ServedGap.fields} for the places the two shapes differ.
480
+ *
481
+ * @example
482
+ * ```ts
483
+ * import { SERVED_GAPS, UNGAPPED_FIELDS } from 'agentfootprint';
484
+ *
485
+ * // Why does nothing explain `callRuntimeStageId`? Because nothing has to.
486
+ * UNGAPPED_FIELDS['callRuntimeStageId'];
487
+ * Object.keys(SERVED_GAPS).length; // 7 gap kinds
488
+ * ```
489
+ */
490
+ exports.UNGAPPED_FIELDS = Object.freeze({
491
+ // MECHANISM (not printed). `Receipt.omittedForAttention` is written from
492
+ // `buildReceipt`'s `omittedForAttention` input, and measured on 9.88.0 no
493
+ // chart in this library ever supplies it: no boundary bubbles
494
+ // `slotCompositions` out of the slot subflow that writes it, so request
495
+ // assembly has nothing to pass. Its absence is therefore universal and has
496
+ // nothing to do with any one recording — which is why it is here and not
497
+ // inside `no-receipt-on-chart`, where it sat until 9.88.0's fourth round.
498
+ // `buildReceipt` is a pure exported mint, so a CONSUMER that calls it can
499
+ // hand it the fact; the narrowing to THIS LIBRARY is what keeps that true.
500
+ omittedForAttention: 'Absent means nobody recorded a drop, never that nothing was dropped. No limit of this ' +
501
+ 'rebuild explains it, so no gap names it.',
502
+ // MECHANISM (not printed). Read straight off `EpochLocation.callRuntimeStageId`,
503
+ // which `epochAt` takes from the call's own commit bundle — never through
504
+ // `keyedFold`, so no missing base and no missing run log can touch it. An
505
+ // epoch with no bundle is not located at all, so there is no view to carry it.
506
+ callRuntimeStageId: 'Never absent here, so there is nothing about it for a gap to excuse.',
507
+ // THE SENTENCE THE RULE IS MODELLED ON. It makes no claim about code at all:
508
+ // it says what the field MEANS inside the account, and nothing outside the
509
+ // sentence can falsify it. Every other entry in this file was reduced to
510
+ // this shape in 9.88.0's sixth round.
511
+ gaps: 'The account itself rather than a fact about the request: a gap naming this list would be ' +
512
+ 'the account excusing its own absence.',
513
+ });
514
+ /** A gap record, assembled from the catalogue — plus, where the site that
515
+ * raises it established one, the cause it read. */
516
+ function gapOf(kind, cause) {
517
+ return { gap: kind, ...exports.SERVED_GAPS[kind], ...(cause !== undefined && { cause }) };
518
+ }
519
+ /**
520
+ * The receipt this epoch's call committed, or WHY there is none. Shared by
521
+ * `receiptAt` and by the rebuild, which reads exactly ONE thing off a receipt —
522
+ * the model/provider/runId basis — and nothing else.
523
+ *
524
+ * THE ONE PLACE THAT KNOWS. This function is where the difference between
525
+ * "nothing was committed" and "something was committed and it is not a
526
+ * receipt" exists at all: everywhere else the two are one `undefined`. It used
527
+ * to be spent here, and the gap's printed sentence tried to make it up
528
+ * afterwards from a list of causes — which is how a shape refusal added in this
529
+ * release became a cause that sentence had already ruled out. So the answer
530
+ * carries the reason out, and {@link ServedGap.cause} is where it lands.
531
+ */
532
+ function readReceipt(location) {
533
+ const value = (0, epochs_js_1.readAfterCall)(location, receipt_js_1.RECEIPT_KEY);
534
+ // Nothing under the key: the run never minted one here. The record does not
535
+ // say which of the ways that happens applied, and neither does this.
536
+ if (value === undefined)
537
+ return { cause: 'no-receipt-committed' };
538
+ // Something IS under the key. A value without a basis is not a receipt —
539
+ // refuse it rather than hand back a half-shape a caller would read
540
+ // `.basis.runId` off — and REPORT the refusal, because a log that holds a
541
+ // non-receipt under the receipt key is damaged, which is a different fact
542
+ // about the recording from a run that never minted.
543
+ const candidate = value;
544
+ if (value === null || typeof value !== 'object' || typeof candidate.basis?.epoch !== 'number') {
545
+ return { cause: 'receipt-shape-rejected' };
546
+ }
547
+ return { receipt: value };
548
+ }
549
+ /** `toolWantsByName` back as the map `findStagedRefs` takes. It is committed
550
+ * as a plain record because a `Map` does not survive the scope's write path
551
+ * (an object write is JSON-round-tripped, and a `Map` round-trips to `{}`). */
552
+ function wantsMapOf(value) {
553
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
554
+ return undefined;
555
+ const map = new Map();
556
+ for (const [name, kinds] of Object.entries(value)) {
557
+ if (Array.isArray(kinds) && kinds.every((k) => typeof k === 'string')) {
558
+ map.set(name, kinds);
559
+ }
560
+ }
561
+ return map.size > 0 ? map : undefined;
562
+ }
563
+ /**
564
+ * The BOUNDARY half of the fold law: a container that leaves this file is this
565
+ * file's own.
566
+ *
567
+ * `keyedFold` deep-freezes its answers, so nothing a consumer does to a view
568
+ * can reach a later epoch. This is the second, cheaper guarantee on top of it:
569
+ * the two arrays a view would otherwise ALIAS straight out of the fold — the
570
+ * conversation and the tool schemas — are copied, so `ServedView` is a value in
571
+ * its own right and not a window onto the fold's cache. Elements are the fold's
572
+ * frozen objects; the container is ours.
573
+ *
574
+ * Every other array on a view is built in `viewOf` from scratch and aliases
575
+ * nothing, so it needs the COPY half of this and not the freeze — but it is
576
+ * frozen anyway, by {@link frozenView}, because the type says `readonly`
577
+ * throughout and a claim that is true of two containers out of six is a claim
578
+ * a reader cannot use.
579
+ */
580
+ function detachedList(items) {
581
+ return Object.freeze([...items]);
582
+ }
583
+ /**
584
+ * A `ServedView` is a VALUE — all of it, not the two containers that happened
585
+ * to need copying.
586
+ *
587
+ * Freezing the whole view costs a handful of `Object.freeze` calls per epoch
588
+ * over containers that are already the right size to hand a person: a few
589
+ * system pieces, at most one request-only line, a tool list, the gaps. It buys
590
+ * one sentence a reader can rely on instead of a rule about which two arrays
591
+ * are safe. `messages.asSent` and `tools.schemas` are already frozen by
592
+ * {@link detachedList} — their ELEMENTS are the fold's own frozen objects and
593
+ * this function does not walk into them, because they are not ours to freeze
594
+ * twice.
595
+ *
596
+ * What this is NOT: protection against a shared reference. Nothing on a view
597
+ * except those two arrays is shared with anything — every other container is
598
+ * built here per call. The freeze makes the type's promise exact; the copy in
599
+ * `detachedList` is what makes the fold safe.
600
+ */
601
+ function frozenView(view) {
602
+ for (const piece of view.system.pieces)
603
+ Object.freeze(piece);
604
+ Object.freeze(view.system.pieces);
605
+ for (const line of view.messages.requestOnly)
606
+ Object.freeze(line);
607
+ Object.freeze(view.messages.requestOnly);
608
+ Object.freeze(view.tools.names);
609
+ for (const gap of view.gaps)
610
+ Object.freeze(gap);
611
+ Object.freeze(view.gaps);
612
+ if (view.basis !== undefined)
613
+ Object.freeze(view.basis);
614
+ Object.freeze(view.system);
615
+ Object.freeze(view.messages);
616
+ Object.freeze(view.tools);
617
+ return Object.freeze(view);
618
+ }
619
+ /** Rebuild one epoch's view from a located epoch. */
620
+ function viewOf(location) {
621
+ // The receipt is read for exactly one thing — WHICH model saw this, through
622
+ // WHICH provider, salted with WHICH run id. Every other field below is
623
+ // rebuilt from committed pieces and never from the record it is checked
624
+ // against; a rebuild that read its own answer sheet would prove nothing.
625
+ const read = readReceipt(location);
626
+ const receipt = read.receipt;
627
+ // ── the system prompt ──────────────────────────────────────────────────
628
+ // The pieces are committed; the joined string never is. Same function the
629
+ // stage used, over the records as the stage read them.
630
+ const injections = ((0, epochs_js_1.readAtCall)(location, 'systemPromptInjections') ?? []);
631
+ const pieces = (0, composeRequest_js_1.contributingPieces)(injections).map((record) => ({
632
+ text: record.rawContent,
633
+ slot: record.slot,
634
+ source: record.source,
635
+ }));
636
+ // ── the conversation ───────────────────────────────────────────────────
637
+ // Read BEFORE the call's own bundle: the call appends the assistant turn to
638
+ // `history`, so the committed value at the call already includes the answer.
639
+ //
640
+ // TWO committed sources, because there are two kinds of chart and they do
641
+ // not agree on where the conversation lives. The agent charts keep it in
642
+ // `history` and treat `messagesInjections` as an observability projection.
643
+ // `LLMCall` and the message-API charts have NO history at all: the messages
644
+ // slot IS the conversation, composed by `messagesFromInjections` — the same
645
+ // function called here. Reading only `history` on those charts found nothing
646
+ // and reported an empty conversation the provider never sent.
647
+ //
648
+ // Neither present ⇒ the turns are UNKNOWN, and the gap below says so rather
649
+ // than letting `[]` pass for a proof.
650
+ const committedHistory = (0, epochs_js_1.readAtCall)(location, 'history');
651
+ const committedInjections = (0, epochs_js_1.readAtCall)(location, 'messagesInjections');
652
+ const conversation = Array.isArray(committedHistory)
653
+ ? committedHistory
654
+ : Array.isArray(committedInjections)
655
+ ? [...(0, composeRequest_js_1.messagesFromInjections)(committedInjections)]
656
+ : undefined;
657
+ const asSent = (0, composeRequest_js_1.stripFrameworkFields)(conversation ?? []);
658
+ // ── the tools ──────────────────────────────────────────────────────────
659
+ // `dynamicToolSchemas` is the PRE-assembly list. Two rules run after it and
660
+ // both are on the record: the wrap-up call withholds every tool, and a
661
+ // 'tool-forced' strategy adds one.
662
+ const withheld = (0, epochs_js_1.readAtCall)(location, 'wrapUpAsked') === true;
663
+ const dynamic = ((0, epochs_js_1.readAtCall)(location, 'dynamicToolSchemas') ?? []);
664
+ const registered = withheld ? [] : dynamic;
665
+ const forcedRaw = (0, epochs_js_1.readRunConstant)(location, receipt_js_1.FORCED_OUTPUT_TOOL_KEY);
666
+ const forced = typeof forcedRaw === 'string' && forcedRaw.length > 0 ? forcedRaw : undefined;
667
+ // ── the request-only lines ─────────────────────────────────────────────
668
+ // The staged-refs nudge is a pure function of committed state: the
669
+ // conversation, the tools really served this call, and the `wants`
670
+ // declarations `seed` put on the record for exactly this reason.
671
+ const requestOnly = [];
672
+ const wants = wantsMapOf((0, epochs_js_1.readRunConstant)(location, 'toolWantsByName'));
673
+ if (wants !== undefined) {
674
+ const match = (0, stagedRefs_js_1.findStagedRefs)(asSent, wants, new Set(registered.map((t) => t.name)));
675
+ if (match !== undefined) {
676
+ requestOnly.push({
677
+ role: 'user',
678
+ text: (0, stagedRefs_js_1.stagedRefsNudgeLine)(match),
679
+ reason: 'staged-refs-nudge',
680
+ });
681
+ }
682
+ }
683
+ // ── what this view cannot prove ────────────────────────────────────────
684
+ // Order is deliberate: the two that make the WHOLE view suspect come first,
685
+ // so a renderer that shows one gap shows the one that matters most.
686
+ const gaps = [];
687
+ // BOTH bases, because there are two folds above and either can be baseless.
688
+ // Under 'dynamic-grouped' the epoch's pieces come from the turn's own inner
689
+ // log (`location.basis`) and every run constant — the forced tool's name, the
690
+ // `wants` the nudge is composed from — comes from the RUN log
691
+ // (`location.runBasis`). Raising the gap off the first alone let a grouped
692
+ // recording that travelled without its RUN base read every constant as absent
693
+ // and declare NOTHING: a Lens denying rather than omitting, which is the law
694
+ // this whole file is built on.
695
+ if (location.basis === 'log-only' || location.runBasis === 'log-only') {
696
+ gaps.push(gapOf('no-fold-base'));
697
+ }
698
+ if (conversation === undefined)
699
+ gaps.push(gapOf('no-conversation-on-record'));
700
+ if (!location.hasRunLog)
701
+ gaps.push(gapOf('no-run-log'));
702
+ // No receipt ⇒ nothing checks the rebuild, and the receipt-only fields are
703
+ // absent. Mutually exclusive with `provider-defaults` below, which is the
704
+ // caveat that applies when a receipt DID record the dials.
705
+ //
706
+ // The CAUSE rides along, from the one read that can tell "nothing was
707
+ // committed" from "something was, and it is not a receipt". The gap's
708
+ // sentence says neither — see {@link ServedGap.cause}.
709
+ if (read.receipt === undefined)
710
+ gaps.push(gapOf('no-receipt-on-chart', read.cause));
711
+ gaps.push(gapOf('cache-transform'));
712
+ if (receipt !== undefined)
713
+ gaps.push(gapOf('provider-defaults'));
714
+ if (forced !== undefined)
715
+ gaps.push(gapOf('forced-tool-schema'));
716
+ return frozenView({
717
+ epoch: location.epoch,
718
+ callRuntimeStageId: location.callRuntimeStageId,
719
+ ...(receipt !== undefined && {
720
+ basis: {
721
+ model: receipt.basis.model,
722
+ provider: receipt.basis.provider,
723
+ runId: receipt.basis.runId,
724
+ },
725
+ }),
726
+ system: { text: (0, composeRequest_js_1.joinSystemPrompt)(injections), pieces },
727
+ messages: { asSent: detachedList(asSent), requestOnly },
728
+ tools: {
729
+ names: [...registered.map((t) => t.name), ...(forced !== undefined ? [forced] : [])],
730
+ schemas: detachedList(registered),
731
+ ...(forced !== undefined && { forced }),
732
+ ...(withheld && { withheld: 'wrap-up' }),
733
+ },
734
+ gaps,
735
+ });
736
+ }
737
+ /**
738
+ * Rebuild what the model was SERVED on epoch `k`, from the run's committed
739
+ * pieces alone.
740
+ *
741
+ * Works on a live snapshot and on a recording read back from JSON, in both
742
+ * chart shapes, on a run whose conversation lives in `messagesInjections`
743
+ * rather than `history` (`LLMCall`, the message-API charts), on a resumed run
744
+ * (every read folds from the checkpoint the run was seeded with), on a
745
+ * redacted run (the pieces are read exactly as the run committed them — a
746
+ * redacted piece rebuilds to its redacted bytes, which is what the record says
747
+ * a reader is allowed to see) and on a recording made before the receipt
748
+ * existed.
749
+ *
750
+ * It returns `undefined` for ONE reason only: the run has no such epoch.
751
+ * Anything it cannot prove about an epoch that DOES exist comes back as a
752
+ * named entry in `gaps`, never as a missing view and never as a confident
753
+ * empty one.
754
+ *
755
+ * @param source a runner (`Agent`, `LLMCall`) or a snapshot. On `LLMCall` and
756
+ * the message-API charts the rebuild is complete but UNCHECKED: those charts
757
+ * mint no receipt, so `basis` is absent and `gaps` carries
758
+ * `no-receipt-on-chart` saying so.
759
+ * @param epoch the iteration number, 1-based — the run's own count.
760
+ *
761
+ * @example
762
+ * ```ts
763
+ * import { receiptAt, receiptHash, servedAt } from 'agentfootprint';
764
+ *
765
+ * const snapshot = agent.getSnapshot()!;
766
+ * const view = servedAt(snapshot, 1)!;
767
+ * const receipt = receiptAt(snapshot, 1)!;
768
+ * receiptHash(receipt.basis.runId, view.system.text) === receipt.system.hash; // true
769
+ * ```
770
+ */
771
+ function servedAt(source, epoch) {
772
+ const location = (0, epochs_js_1.epochAt)(source, epoch);
773
+ return location === undefined ? undefined : viewOf(location);
774
+ }
775
+ exports.servedAt = servedAt;
776
+ /**
777
+ * Every epoch's served view, in run order — `servedAt` for a whole run, with
778
+ * one pass over the recording instead of one per epoch.
779
+ *
780
+ * @example
781
+ * ```ts
782
+ * servedViews(agent.getSnapshot()!).map((v) => v.tools.names.length); // [3, 3, 0]
783
+ * ```
784
+ */
785
+ function servedViews(source) {
786
+ return (0, epochs_js_1.epochLocations)(source).map(viewOf);
787
+ }
788
+ exports.servedViews = servedViews;
789
+ /**
790
+ * The receipt epoch `k`'s call left behind, or `undefined`.
791
+ *
792
+ * `undefined` means one of two things on the record, and the same epoch's
793
+ * `servedAt(...)` view carries which: its `no-receipt-on-chart` gap has a
794
+ * {@link ServedGapCause}. Either nothing was committed under the receipt key —
795
+ * a recording made before the receipt existed, a chart whose LLM stage mints
796
+ * none, a run with `recordReceipt: false`, all of which leave that same record
797
+ * — or something WAS committed there and `readReceipt` refused it, because it
798
+ * is not a non-null object with a number at `basis.epoch`, and handing back a
799
+ * half-object a caller reads `.basis.runId` off is worse than saying no.
800
+ *
801
+ * It also returns `undefined` when the run has no epoch `k` at all, which is
802
+ * the answer `epochAt` gives and is not a fact about receipts; ask
803
+ * `servedAt(source, k)` if you need to tell a missing epoch from a missing
804
+ * receipt, because that one returns `undefined` for the missing epoch only.
805
+ *
806
+ * `servedAt` still rebuilds the view of an epoch that exists in every one of
807
+ * these cases, which is what makes an old recording readable instead of
808
+ * unreadable.
809
+ *
810
+ * The receipt comes back DEEP-FROZEN and is the same object every caller gets
811
+ * for this epoch: it is a value folded out of the log, and a fold's answers are
812
+ * detached (`keyedFold.ts` · `freezeDeep`). Copy it if you need to edit one.
813
+ *
814
+ * @example
815
+ * ```ts
816
+ * import { receiptAt } from 'agentfootprint';
817
+ *
818
+ * receiptAt(agent.getSnapshot()!, 3)?.tools.withheld; // 'wrap-up' on a wrap-up call
819
+ * ```
820
+ */
821
+ function receiptAt(source, epoch) {
822
+ const location = (0, epochs_js_1.epochAt)(source, epoch);
823
+ return location === undefined ? undefined : readReceipt(location).receipt;
824
+ }
825
+ exports.receiptAt = receiptAt;
826
+ //# sourceMappingURL=servedView.js.map