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.
- package/CHANGELOG.md +946 -0
- package/dist/core/Agent.js +32 -0
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/LLMCall.js +7 -10
- package/dist/core/LLMCall.js.map +1 -1
- package/dist/core/agent/buildAgentMessageApiChart.js +2 -4
- package/dist/core/agent/buildAgentMessageApiChart.js.map +1 -1
- package/dist/core/agent/buildDynamicAgentChart.js +10 -0
- package/dist/core/agent/buildDynamicAgentChart.js.map +1 -1
- package/dist/core/agent/buildMessageApiChart.js +2 -4
- package/dist/core/agent/buildMessageApiChart.js.map +1 -1
- package/dist/core/agent/composeRequest.js +134 -0
- package/dist/core/agent/composeRequest.js.map +1 -0
- package/dist/core/agent/stages/callLLM.js +62 -25
- package/dist/core/agent/stages/callLLM.js.map +1 -1
- package/dist/core/agent/stages/seed.js +12 -0
- package/dist/core/agent/stages/seed.js.map +1 -1
- package/dist/core/agent/stages/toolCalls.js +2 -4
- package/dist/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/core/flowchartAsTool.js.map +1 -1
- package/dist/esm/core/Agent.d.ts +3 -0
- package/dist/esm/core/Agent.js +32 -0
- package/dist/esm/core/Agent.js.map +1 -1
- package/dist/esm/core/LLMCall.js +7 -10
- package/dist/esm/core/LLMCall.js.map +1 -1
- package/dist/esm/core/agent/buildAgentMessageApiChart.js +2 -4
- package/dist/esm/core/agent/buildAgentMessageApiChart.js.map +1 -1
- package/dist/esm/core/agent/buildDynamicAgentChart.js +10 -0
- package/dist/esm/core/agent/buildDynamicAgentChart.js.map +1 -1
- package/dist/esm/core/agent/buildMessageApiChart.js +2 -4
- package/dist/esm/core/agent/buildMessageApiChart.js.map +1 -1
- package/dist/esm/core/agent/composeRequest.d.ts +121 -0
- package/dist/esm/core/agent/composeRequest.js +127 -0
- package/dist/esm/core/agent/composeRequest.js.map +1 -0
- package/dist/esm/core/agent/stages/callLLM.d.ts +28 -0
- package/dist/esm/core/agent/stages/callLLM.js +61 -24
- package/dist/esm/core/agent/stages/callLLM.js.map +1 -1
- package/dist/esm/core/agent/stages/seed.d.ts +26 -0
- package/dist/esm/core/agent/stages/seed.js +12 -0
- package/dist/esm/core/agent/stages/seed.js.map +1 -1
- package/dist/esm/core/agent/stages/toolCalls.js +2 -4
- package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/esm/core/agent/types.d.ts +64 -0
- package/dist/esm/core/flowchartAsTool.d.ts +15 -7
- package/dist/esm/core/flowchartAsTool.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/context-bisect/llmEdgeWeigher.d.ts +8 -0
- package/dist/esm/lib/context-bisect/llmEdgeWeigher.js +0 -0
- package/dist/esm/lib/context-bisect/llmEdgeWeigher.js.map +1 -1
- package/dist/esm/lib/context-bisect/trajectory.js +3 -7
- package/dist/esm/lib/context-bisect/trajectory.js.map +1 -1
- package/dist/esm/lib/context-ledger/contextLedger.js +5 -12
- package/dist/esm/lib/context-ledger/contextLedger.js.map +1 -1
- package/dist/esm/lib/time-travel/epochs.d.ts +210 -0
- package/dist/esm/lib/time-travel/epochs.js +274 -0
- package/dist/esm/lib/time-travel/epochs.js.map +1 -0
- package/dist/esm/lib/time-travel/index.d.ts +10 -4
- package/dist/esm/lib/time-travel/index.js +10 -4
- package/dist/esm/lib/time-travel/index.js.map +1 -1
- package/dist/esm/lib/time-travel/keyedFold.d.ts +112 -0
- package/dist/esm/lib/time-travel/keyedFold.js +286 -0
- package/dist/esm/lib/time-travel/keyedFold.js.map +1 -0
- package/dist/esm/lib/time-travel/milestoneStops.d.ts +57 -36
- package/dist/esm/lib/time-travel/milestoneStops.js +75 -82
- package/dist/esm/lib/time-travel/milestoneStops.js.map +1 -1
- package/dist/esm/lib/time-travel/receipt.d.ts +496 -0
- package/dist/esm/lib/time-travel/receipt.js +434 -0
- package/dist/esm/lib/time-travel/receipt.js.map +1 -0
- package/dist/esm/lib/time-travel/servedView.d.ts +525 -0
- package/dist/esm/lib/time-travel/servedView.js +820 -0
- package/dist/esm/lib/time-travel/servedView.js.map +1 -0
- package/dist/esm/lib/time-travel/sha256.d.ts +24 -0
- package/dist/esm/lib/time-travel/sha256.js +103 -0
- package/dist/esm/lib/time-travel/sha256.js.map +1 -0
- package/dist/esm/recorders/observability/BoundaryRecorder.d.ts +21 -0
- package/dist/esm/recorders/observability/BoundaryRecorder.js +6 -0
- package/dist/esm/recorders/observability/BoundaryRecorder.js.map +1 -1
- package/dist/index.js +124 -103
- package/dist/index.js.map +1 -1
- package/dist/lib/context-bisect/llmEdgeWeigher.js +0 -0
- package/dist/lib/context-bisect/llmEdgeWeigher.js.map +1 -1
- package/dist/lib/context-bisect/trajectory.js +3 -7
- package/dist/lib/context-bisect/trajectory.js.map +1 -1
- package/dist/lib/context-ledger/contextLedger.js +5 -12
- package/dist/lib/context-ledger/contextLedger.js.map +1 -1
- package/dist/lib/time-travel/epochs.js +284 -0
- package/dist/lib/time-travel/epochs.js.map +1 -0
- package/dist/lib/time-travel/index.js +35 -5
- package/dist/lib/time-travel/index.js.map +1 -1
- package/dist/lib/time-travel/keyedFold.js +290 -0
- package/dist/lib/time-travel/keyedFold.js.map +1 -0
- package/dist/lib/time-travel/milestoneStops.js +74 -81
- package/dist/lib/time-travel/milestoneStops.js.map +1 -1
- package/dist/lib/time-travel/receipt.js +442 -0
- package/dist/lib/time-travel/receipt.js.map +1 -0
- package/dist/lib/time-travel/servedView.js +826 -0
- package/dist/lib/time-travel/servedView.js.map +1 -0
- package/dist/lib/time-travel/sha256.js +107 -0
- package/dist/lib/time-travel/sha256.js.map +1 -0
- package/dist/recorders/observability/BoundaryRecorder.js +6 -0
- package/dist/recorders/observability/BoundaryRecorder.js.map +1 -1
- package/dist/types/core/Agent.d.ts +3 -0
- package/dist/types/core/Agent.d.ts.map +1 -1
- package/dist/types/core/LLMCall.d.ts.map +1 -1
- package/dist/types/core/agent/buildAgentMessageApiChart.d.ts.map +1 -1
- package/dist/types/core/agent/buildDynamicAgentChart.d.ts.map +1 -1
- package/dist/types/core/agent/buildMessageApiChart.d.ts.map +1 -1
- package/dist/types/core/agent/composeRequest.d.ts +122 -0
- package/dist/types/core/agent/composeRequest.d.ts.map +1 -0
- package/dist/types/core/agent/stages/callLLM.d.ts +28 -0
- package/dist/types/core/agent/stages/callLLM.d.ts.map +1 -1
- package/dist/types/core/agent/stages/seed.d.ts +26 -0
- package/dist/types/core/agent/stages/seed.d.ts.map +1 -1
- package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
- package/dist/types/core/agent/types.d.ts +64 -0
- package/dist/types/core/agent/types.d.ts.map +1 -1
- package/dist/types/core/flowchartAsTool.d.ts +15 -7
- package/dist/types/core/flowchartAsTool.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/context-bisect/llmEdgeWeigher.d.ts +8 -0
- package/dist/types/lib/context-bisect/llmEdgeWeigher.d.ts.map +1 -1
- package/dist/types/lib/context-bisect/trajectory.d.ts.map +1 -1
- package/dist/types/lib/context-ledger/contextLedger.d.ts.map +1 -1
- package/dist/types/lib/time-travel/epochs.d.ts +211 -0
- package/dist/types/lib/time-travel/epochs.d.ts.map +1 -0
- package/dist/types/lib/time-travel/index.d.ts +10 -4
- package/dist/types/lib/time-travel/index.d.ts.map +1 -1
- package/dist/types/lib/time-travel/keyedFold.d.ts +113 -0
- package/dist/types/lib/time-travel/keyedFold.d.ts.map +1 -0
- package/dist/types/lib/time-travel/milestoneStops.d.ts +57 -36
- package/dist/types/lib/time-travel/milestoneStops.d.ts.map +1 -1
- package/dist/types/lib/time-travel/receipt.d.ts +497 -0
- package/dist/types/lib/time-travel/receipt.d.ts.map +1 -0
- package/dist/types/lib/time-travel/servedView.d.ts +526 -0
- package/dist/types/lib/time-travel/servedView.d.ts.map +1 -0
- package/dist/types/lib/time-travel/sha256.d.ts +25 -0
- package/dist/types/lib/time-travel/sha256.d.ts.map +1 -0
- package/dist/types/recorders/observability/BoundaryRecorder.d.ts +21 -0
- package/dist/types/recorders/observability/BoundaryRecorder.d.ts.map +1 -1
- 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
|