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,434 @@
1
+ /**
2
+ * receipt — what the model was actually handed, recorded at the stop where it
3
+ * was handed it.
4
+ *
5
+ * Role: Fold. This module owns the receipt's SHAPE and the one function that
6
+ * mints one (`buildReceipt`); the agent's request assembly calls it and
7
+ * commits the result under the scope key `receipt`, so the record lands
8
+ * in the call-llm bundle that already exists. Nothing here executes,
9
+ * reads scope, or emits.
10
+ * Reads: its arguments.
11
+ * Emits: N/A.
12
+ *
13
+ * ── THE FIRST LAW: NO AUTHORITY OMISSIONS ──────────────────────────────────
14
+ * A receipt never names — and never counts — what a caller's ROLE was not
15
+ * allowed to see. Committed state is readable by the trace toolpack's
16
+ * debugging tools, so a receipt carrying `hiddenSkillIds`, or even "3 skills
17
+ * withheld", would turn a permission decision into a leak path: the thing the
18
+ * check exists to hide, restated one layer down where nobody is checking. A
19
+ * reader that needs those ids reads them from the fold (`hiddenSkillIds` is
20
+ * committed state, and reading it is governed where it is read), not from
21
+ * here.
22
+ *
23
+ * ATTENTION omissions are a different fact and MAY ride the receipt:
24
+ * `omittedForAttention` says a slot's budget dropped content. Nobody was
25
+ * refused anything — the request simply did not fit — and a reader chasing "why
26
+ * did it not know that?" needs to see it. Even there the summaries are hashed,
27
+ * never quoted, for the same reason everything else on a receipt is.
28
+ *
29
+ * Measured on 9.88.0, NO chart shape supplies it: a slot writes its drops to
30
+ * `slotCompositions` inside its own subflow and no boundary bubbles that record
31
+ * out, so the agent's request assembly has nothing to pass. The field is on the
32
+ * shape because `buildReceipt` is a pure exported mint a caller can hand the
33
+ * fact to, and because the law it obeys is worth stating once rather than the
34
+ * day the record starts crossing. What is NOT done is go looking for it: a
35
+ * tracked read of an always-absent key is a read edge the trace then has to
36
+ * explain.
37
+ *
38
+ * ── THE SECOND LAW: HASHES AND REFERENCES, NEVER BYTES ─────────────────────
39
+ * A receipt is a fingerprint, not a copy. It records that a piece of a given
40
+ * shape and size was in a given position, and nothing about what it said. The
41
+ * bytes are already governed — `recordSystemPrompt` is opt-in for exactly this
42
+ * reason, redaction patterns scrub the committed mirror, and a window strategy
43
+ * decides what survives. A receipt that carried content would quietly reopen
44
+ * all three.
45
+ *
46
+ * ── THE THIRD LAW: HASHES ARE SALTED WITH THE RUN ID ───────────────────────
47
+ * `hash = sha256(runId + '\u001f' + content)`, first 16 hex characters. The
48
+ * salt is what stops a short piece from being fingerprinted ACROSS runs: an
49
+ * unsalted hash of a one-line system prompt, a tool schema, or a two-word user
50
+ * turn is a dictionary lookup away from being read back, and the receipt is
51
+ * committed state that travels in recordings. Salted per run, the same
52
+ * sentence in two runs has two hashes, so the fingerprint answers "is this the
53
+ * same as THAT piece of THIS run?" — which is the only question the
54
+ * conformance law asks — and answers nothing else.
55
+ *
56
+ * Hashes are NOT redacted, and the salt is what makes shipping THEM safe — not
57
+ * an assumption that something scrubbed them. It says nothing about the rest of
58
+ * the recording, and this law must not be read as if it did.
59
+ *
60
+ * WHAT A RECORDING ACTUALLY CONTAINS. An agent run is not redacted:
61
+ * `Agent.create(...)` has no redaction door, so a secret in a system prompt is
62
+ * in the commit log and in `servedAt(k).system.text`, verbatim. Redaction in
63
+ * this library is EXECUTOR-level — `flowchartAsTool({ redact })` /
64
+ * `runbookAsTool({ redact })` set a policy on an inner run, and footprintjs
65
+ * scrubs at COMMIT time, so a redacted key never enters that inner commit log
66
+ * (the same snapshot's live `sharedState` is a different view and is not
67
+ * scrubbed). A snapshot taken with `redact: true` also omits `initialState`,
68
+ * so folding it reports `basis: 'log-only'` and `servedView.ts` raises
69
+ * `no-fold-base` rather than rebuilding a short view in silence. Treat a
70
+ * recording as the plaintext it is; the salt protects the fingerprints, and
71
+ * only the fingerprints.
72
+ *
73
+ * ── THE FOURTH LAW: THE RECEIPT IS MINTED AT THE PROVIDER PORT ─────────────
74
+ * Everything on a receipt describes the request AS HANDED TO
75
+ * `LLMProvider.complete` — the port, not the wire. Anything downstream of that
76
+ * boundary is outside what this record can witness: a decorated provider, a
77
+ * vendor adapter's own serializer, a consumer's hand-written `complete`, or the
78
+ * vendor's server-side defaults. `RECEIPT_BOUNDARY` states it in one sentence
79
+ * for a renderer to print, and `servedView.ts` · `SERVED_GAPS` names the two
80
+ * places a reader is most likely to mistake it for wire-level proof.
81
+ */
82
+ import { sha256Hex } from './sha256.js';
83
+ /**
84
+ * The separator between two fields inside one digest input — ASCII UNIT
85
+ * SEPARATOR, which no prompt, tool name or JSON payload can contain (`JSON.
86
+ * stringify` escapes it). Written as an escape rather than a literal control
87
+ * character so a reader can see it and a formatter cannot eat it.
88
+ *
89
+ * Without it a digest is a bare concatenation, and a bare concatenation is
90
+ * ambiguous: `role + content` cannot tell `'user' + 'x'` from `'use' + 'rx'`.
91
+ */
92
+ const SEP = '\u001F';
93
+ /** The separator between two tool calls inside one message's digest. */
94
+ const CALL_SEP = '\u001C';
95
+ /** The separator between the fields of ONE tool call. */
96
+ const CALL_FIELD_SEP = '\u001D';
97
+ /** The separator between the top-level parts of a message's digest. */
98
+ const FIELD_SEP = '\u001E';
99
+ /**
100
+ * What a digest input records in place of a value JSON cannot express — a
101
+ * cycle, a `BigInt`, anything `stableJson` refuses. It is a MARK, not an empty
102
+ * string: two unserializable values are recorded as unserializable rather than
103
+ * as identical, which is what stops the cache comparison claiming "the strategy
104
+ * changed nothing" about two requests it could not read.
105
+ */
106
+ export const UNSERIALIZABLE = '\u001F<unserializable>';
107
+ /**
108
+ * The boundary every receipt field is true at, in one sentence — exported so a
109
+ * renderer prints the library's own wording instead of inferring a stronger
110
+ * claim from a `null`.
111
+ *
112
+ * WHY IT IS A CONSTANT AND NOT A COMMENT. A reader who sees
113
+ * `cache.transform: 'unchanged'` will conclude "this request was not rewritten"
114
+ * unless something on the screen says otherwise, and the person who reads the
115
+ * screen is rarely the person who read the source. Print it beside the record.
116
+ *
117
+ * ── THE MECHANISM, WHICH THE PRINTED SENTENCE NO LONGER NAMES (9.88.0) ─────
118
+ * `buildReceipt` is called from `stages/callLLM.ts` with the request about to
119
+ * be passed to `LLMProvider.complete` — the PORT, this library's last sight of
120
+ * it. Three things sit downstream of that call and none of them is on the
121
+ * record: a provider decorated by the consumer (`complete()` wrapping
122
+ * `complete()`), a vendor adapter's own serializer, and the vendor's
123
+ * server-side defaults. `test/lib/time-travel/receipt-conformance.test.ts`
124
+ * reproduces the first of those — a decorator that appends a system suffix and
125
+ * a ghost tool after the mint, with `cache.transform` still reading
126
+ * `'unchanged'`, correctly.
127
+ *
128
+ * The sentence below used to say all of that, and it was PRINTED beside a
129
+ * trace by renderers that append it to a gap. A printed sentence names no
130
+ * module, function or call: it says which fields, what they mean here, and
131
+ * what to do — the rule in `test/helpers/gapProseClaims.ts`. So the mechanism
132
+ * lives in this comment, where a maintainer reads it and review catches its
133
+ * rot, and the constant says only the thing a reader must not get wrong.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * import { RECEIPT_BOUNDARY, receiptAt } from 'agentfootprint';
138
+ *
139
+ * const receipt = receiptAt(agent.getSnapshot()!, 1)!;
140
+ * if (receipt.cache.transform === 'unchanged') {
141
+ * console.log(`the cache strategy changed nothing. ${RECEIPT_BOUNDARY}`);
142
+ * }
143
+ * ```
144
+ */
145
+ export const RECEIPT_BOUNDARY = 'A receipt describes the request as this library last saw it. Whatever ' +
146
+ 'handled it after that could have changed it, and nothing on the receipt ' +
147
+ 'would show that.';
148
+ /** The scope key the receipt is committed under. */
149
+ export const RECEIPT_KEY = 'receipt';
150
+ /** The scope key holding the name of the tool a `'tool-forced'` output
151
+ * strategy put on the wire. `seed` commits it; `servedView.ts` · `servedAt`
152
+ * reads it — so a rebuild can NAME the forced tool without reading the
153
+ * receipt it is being checked against. */
154
+ export const FORCED_OUTPUT_TOOL_KEY = 'forcedOutputToolName';
155
+ /** How many hex characters of the digest a receipt keeps. 16 — 64 bits, which
156
+ * is far past collision range for the few dozen pieces of one run, and short
157
+ * enough to read in a terminal. */
158
+ export const RECEIPT_HASH_CHARS = 16;
159
+ /**
160
+ * The receipt's hash: run-salted SHA-256, first {@link RECEIPT_HASH_CHARS} hex
161
+ * characters. Exported because it is half of the conformance law — a reader
162
+ * that rebuilds a served view proves the rebuild by hashing it the same way.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * import { receiptAt, receiptHash, servedAt } from 'agentfootprint';
167
+ *
168
+ * const view = servedAt(agent.getSnapshot()!, 1)!;
169
+ * const receipt = receiptAt(agent.getSnapshot()!, 1)!;
170
+ * receiptHash(receipt.basis.runId, view.system.text) === receipt.system.hash; // true
171
+ * ```
172
+ */
173
+ export function receiptHash(runId, content) {
174
+ return sha256Hex(`${runId}${SEP}${content}`).slice(0, RECEIPT_HASH_CHARS);
175
+ }
176
+ /**
177
+ * JSON with object keys in sorted order, at every depth — so two structurally
178
+ * equal values hash the same however they were built. Property order survives
179
+ * `structuredClone`, but it does NOT survive every path a value takes through
180
+ * a delta-encoded commit log and back, and a fingerprint that depends on
181
+ * insertion order is a fingerprint that reports a change nobody made.
182
+ *
183
+ * Returns `undefined` — never `''` — for anything JSON cannot express (a
184
+ * cycle, a `BigInt`, a value whose `toJSON` throws). It does not throw,
185
+ * because a receipt is bookkeeping and must never be able to stop a run; and
186
+ * it does not collapse, because `''` made every unreadable value equal to
187
+ * every other one. Two requests that both failed to serialize compared EQUAL,
188
+ * and the receipt wrote `transformHash: null` — "the cache strategy changed
189
+ * nothing" — about a pair it had not read. Callers substitute
190
+ * {@link UNSERIALIZABLE} where a digest needs bytes, and branch where a
191
+ * comparison needs truth.
192
+ */
193
+ export function stableJson(value) {
194
+ const canonical = (input) => {
195
+ if (Array.isArray(input))
196
+ return input.map(canonical);
197
+ if (input !== null && typeof input === 'object') {
198
+ const source = input;
199
+ const out = {};
200
+ for (const key of Object.keys(source).sort()) {
201
+ const next = canonical(source[key]);
202
+ if (next === undefined)
203
+ continue;
204
+ out[key] = next;
205
+ }
206
+ return out;
207
+ }
208
+ return input;
209
+ };
210
+ try {
211
+ return JSON.stringify(canonical(value)) ?? undefined;
212
+ }
213
+ catch {
214
+ return undefined;
215
+ }
216
+ }
217
+ /** `stableJson`, with the unreadable marked rather than dropped — the form a
218
+ * digest input takes. */
219
+ function digestJson(value) {
220
+ return stableJson(value) ?? UNSERIALIZABLE;
221
+ }
222
+ /**
223
+ * The bytes a tool schema's hash covers: the whole `LLMToolSchema` — `name`,
224
+ * `description`, `inputSchema` — as sorted-key JSON, so two schemas that are
225
+ * structurally the same hash the same however they were built, and with a
226
+ * schema JSON cannot express (a cycle, a `BigInt`) marked {@link UNSERIALIZABLE}
227
+ * rather than dropped or thrown on.
228
+ *
229
+ * WHAT TO PASS (9.89.0). The receipt hashes each tool AS HANDED TO THE PORT:
230
+ * `buildReceipt` is given the request's tool list, and that list is the
231
+ * `dynamicToolSchemas` the run committed, plus a forced answer tool when an
232
+ * output strategy adds one. `servedAt(k).tools.schemas[i]` is that committed
233
+ * list read back — the same shape, the same bytes — so a consumer holding a
234
+ * served view already holds the object this function takes. It does not take
235
+ * the served view, a `Tool`, or a `defineTool` definition: those carry an
236
+ * `execute`, `wants` and other fields the model never saw.
237
+ *
238
+ * WHY IT IS EXPORTED. A reader that rebuilds a served view proves the rebuild
239
+ * by hashing it the way the receipt did. `receiptHash` and `messageDigestInput`
240
+ * were exported in 9.88.0 for the system text, the pieces and the messages,
241
+ * and a consumer could verify all of them — and NOT the tools, because the
242
+ * serializer behind `schemaHashes` was internal. Its only options were to copy
243
+ * `stableJson` (a second owner of the rule, which drifts the day the digest
244
+ * gains a field, as the message digest did in 9.88.0) or to leave the schema
245
+ * rows unverified. This is the third digest half of the law, beside its two
246
+ * siblings, and the ONLY spelling of the schema rule: `buildReceipt` calls it
247
+ * too. `stableJson` stays off the root barrel for the same reason — a consumer
248
+ * composing `hash(stableJson(tool))` would be writing the rule a second time.
249
+ *
250
+ * @example verify every schema row of a receipt from outside
251
+ * ```ts
252
+ * import { receiptAt, receiptHash, servedAt, toolDigestInput } from 'agentfootprint';
253
+ *
254
+ * const snapshot = agent.getSnapshot()!;
255
+ * const view = servedAt(snapshot, 1)!;
256
+ * const receipt = receiptAt(snapshot, 1)!;
257
+ * for (const tool of view.tools.schemas) {
258
+ * receiptHash(receipt.basis.runId, toolDigestInput(tool)) ===
259
+ * receipt.tools.schemaHashes[tool.name]; // true, for every tool of every epoch
260
+ * }
261
+ * // A forced answer tool is in `schemaHashes` and NOT in `schemas` — its body
262
+ * // is a declared gap (`forced-tool-schema`), so there is no row to check.
263
+ * ```
264
+ */
265
+ export function toolDigestInput(tool) {
266
+ return digestJson(tool);
267
+ }
268
+ /**
269
+ * The bytes a message's hash covers, each field behind a separator: its role,
270
+ * its text, its `toolCallId`, its `toolName`, the calls it asked for (id, name,
271
+ * arguments and `providerMeta`) and its `thinkingBlocks`.
272
+ *
273
+ * WHY `toolCallId` (9.88.0). It is the JOIN KEY — `tool_use_id` on Anthropic's
274
+ * wire, `tool_call_id` on OpenAI's — that pairs a tool result to the call that
275
+ * asked for it. Two parallel calls whose results happen to have identical text
276
+ * hashed the SAME without it, so a mis-pairing (the answer to `c1` filed under
277
+ * `c2`) was invisible to the law: exactly the defect a receipt exists to catch.
278
+ *
279
+ * WHY `toolName` TOO (9.88.0). On two shipped providers the id is not the join
280
+ * key at all. `adapters/llm/GeminiProvider.ts` · `toGeminiContents` pairs a
281
+ * `functionResponse` to its call BY NAME and says so in its own comment; the
282
+ * id is optional there and dropped when it is not a real one.
283
+ * `adapters/llm/OllamaProvider.ts` · `toOllamaMessages` puts `tool_name` on
284
+ * the wire and only falls back to the id. So on those providers a receipt that
285
+ * covered `toolCallId` alone still could not see the very mis-pairing the
286
+ * field was added for: two `role:'tool'` messages with identical text and
287
+ * SWAPPED names fingerprinted identically. It rides as its own field behind
288
+ * the separator, beside the id, so a name moving between two results changes
289
+ * both hashes and neither can absorb the other's bytes.
290
+ *
291
+ * WHY `thinkingBlocks` and `providerMeta` (9.88.0). `adapters/types.ts` is
292
+ * explicit that a signed thinking block MUST be echoed byte-exact or the API
293
+ * rejects the turn, and `providerMeta` round-trips vendor state (Gemini's
294
+ * `thoughtSignature`, without which the turn after a tool call is refused).
295
+ * Both are things the model receives; a fingerprint that omitted them said two
296
+ * requests were the same when one of them would be rejected. They enter as a
297
+ * `stableJson` FINGERPRINT, so nothing quotable is added — a signature is an
298
+ * opaque token, not content.
299
+ *
300
+ * Still deliberately NOT covered: `injectedBy` (stripped before the request
301
+ * exists) and `ephemeral` (a persistence flag, invisible to the model). Two
302
+ * messages that hash the same are the same thing said to the model.
303
+ */
304
+ export function messageDigestInput(message) {
305
+ const calls = (message.toolCalls ?? []).map((call) => [
306
+ call.id,
307
+ call.name,
308
+ digestJson(call.args),
309
+ call.providerMeta === undefined ? '' : digestJson(call.providerMeta),
310
+ ].join(CALL_FIELD_SEP));
311
+ return [
312
+ message.role,
313
+ message.content,
314
+ message.toolCallId ?? '',
315
+ message.toolName ?? '',
316
+ calls.join(CALL_SEP),
317
+ message.thinkingBlocks === undefined || message.thinkingBlocks.length === 0
318
+ ? ''
319
+ : digestJson(message.thinkingBlocks),
320
+ ].join(FIELD_SEP);
321
+ }
322
+ /**
323
+ * The sampling knobs off the request the port was handed.
324
+ *
325
+ * Duck-typed and value-conditional: an agent that set no dial records no key,
326
+ * so `params` is `{}` rather than a row of `undefined`s claiming defaults the
327
+ * library never sent.
328
+ */
329
+ function paramsOf(request) {
330
+ if (request === null || typeof request !== 'object')
331
+ return {};
332
+ const req = request;
333
+ const stop = Array.isArray(req.stop) ? req.stop.filter((s) => typeof s === 'string') : undefined;
334
+ return {
335
+ ...(typeof req.temperature === 'number' && { temperature: req.temperature }),
336
+ ...(typeof req.maxTokens === 'number' && { maxTokens: req.maxTokens }),
337
+ ...(typeof req.thinking?.budget === 'number' && { thinkingBudget: req.thinking.budget }),
338
+ ...(stop !== undefined && stop.length > 0 && { stop }),
339
+ ...(typeof req.toolChoice?.type === 'string' && {
340
+ toolChoice: {
341
+ type: req.toolChoice.type,
342
+ ...(typeof req.toolChoice.name === 'string' && { name: req.toolChoice.name }),
343
+ },
344
+ }),
345
+ };
346
+ }
347
+ /**
348
+ * Mint the receipt for one composed request.
349
+ *
350
+ * Pure and total: every branch here is arithmetic over what it was handed, so
351
+ * a receipt cannot fail a call. The one judgement it makes is the cache
352
+ * comparison, and it makes it in three values rather than two: `'unchanged'`
353
+ * exactly when the strategy handed back a request that serializes identically
354
+ * to the one it was given, `'rewritten'` when it did not, and `'unknown'` when
355
+ * either request could not be serialized at all — because "I could not read
356
+ * them" is not the same claim as "they were the same".
357
+ *
358
+ * Every value it produces is true AT THE PROVIDER PORT and nowhere past it —
359
+ * see {@link RECEIPT_BOUNDARY}.
360
+ */
361
+ export function buildReceipt(input) {
362
+ const hash = (content) => receiptHash(input.runId, content);
363
+ const schemaHashes = {};
364
+ for (const tool of input.tools)
365
+ schemaHashes[tool.name] = hash(toolDigestInput(tool));
366
+ // The cache verdict, three-valued. A pair the receipt could not read is
367
+ // 'unknown' — never 'unchanged', which is what `''` used to make it.
368
+ const base = stableJson(input.baseRequest);
369
+ const prepared = stableJson(input.preparedRequest);
370
+ const comparable = base !== undefined && prepared !== undefined;
371
+ const transform = !comparable
372
+ ? 'unknown'
373
+ : base === prepared
374
+ ? 'unchanged'
375
+ : 'rewritten';
376
+ return {
377
+ system: {
378
+ hash: hash(input.systemText),
379
+ chars: input.systemText.length,
380
+ pieces: input.systemPieces.map((piece) => ({
381
+ hash: hash(piece.text),
382
+ slot: piece.slot,
383
+ source: piece.source,
384
+ })),
385
+ },
386
+ messages: {
387
+ count: input.messages.length + input.requestOnly.length,
388
+ entries: input.messages.map((message) => ({
389
+ role: message.role,
390
+ hash: hash(messageDigestInput(message)),
391
+ ...(message.toolCallId !== undefined && { key: message.toolCallId }),
392
+ })),
393
+ requestOnly: input.requestOnly.map((line) => ({
394
+ role: line.message.role,
395
+ hash: hash(messageDigestInput(line.message)),
396
+ reason: line.reason,
397
+ })),
398
+ },
399
+ tools: {
400
+ names: input.tools.map((tool) => tool.name),
401
+ schemaHashes,
402
+ forced: input.forced,
403
+ withheld: input.withheld,
404
+ },
405
+ cache: {
406
+ transform,
407
+ transformHash: transform === 'rewritten' ? hash(prepared) : null,
408
+ markersApplied: (input.markersApplied ?? []).map((marker) => ({
409
+ field: marker.field,
410
+ boundaryIndex: marker.boundaryIndex,
411
+ ttl: marker.ttl,
412
+ })),
413
+ },
414
+ // The PREPARED request, not the base one: the port is handed what the cache
415
+ // strategy returned, and a strategy that rewrote maxTokens must not leave
416
+ // five receipt fields describing a request nobody sent. See
417
+ // {@link RECEIPT_BOUNDARY} — this is the boundary those fields are true at.
418
+ params: paramsOf(input.preparedRequest),
419
+ ...(input.omittedForAttention !== undefined &&
420
+ input.omittedForAttention.count > 0 && {
421
+ omittedForAttention: {
422
+ count: input.omittedForAttention.count,
423
+ hashes: input.omittedForAttention.summaries.map((summary) => hash(summary)),
424
+ },
425
+ }),
426
+ basis: {
427
+ epoch: input.epoch,
428
+ runId: input.runId,
429
+ model: input.model,
430
+ provider: input.provider,
431
+ },
432
+ };
433
+ }
434
+ //# sourceMappingURL=receipt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipt.js","sourceRoot":"","sources":["../../../../src/lib/time-travel/receipt.ts"],"names":[],"mappings":"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"}