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,496 @@
|
|
|
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 type { LLMMessage, LLMToolSchema } from '../../adapters/types.js';
|
|
83
|
+
import type { ContextRole, ContextSlot, ContextSource } from '../../events/types.js';
|
|
84
|
+
/**
|
|
85
|
+
* What a digest input records in place of a value JSON cannot express — a
|
|
86
|
+
* cycle, a `BigInt`, anything `stableJson` refuses. It is a MARK, not an empty
|
|
87
|
+
* string: two unserializable values are recorded as unserializable rather than
|
|
88
|
+
* as identical, which is what stops the cache comparison claiming "the strategy
|
|
89
|
+
* changed nothing" about two requests it could not read.
|
|
90
|
+
*/
|
|
91
|
+
export declare const UNSERIALIZABLE = "\u001F<unserializable>";
|
|
92
|
+
/**
|
|
93
|
+
* The boundary every receipt field is true at, in one sentence — exported so a
|
|
94
|
+
* renderer prints the library's own wording instead of inferring a stronger
|
|
95
|
+
* claim from a `null`.
|
|
96
|
+
*
|
|
97
|
+
* WHY IT IS A CONSTANT AND NOT A COMMENT. A reader who sees
|
|
98
|
+
* `cache.transform: 'unchanged'` will conclude "this request was not rewritten"
|
|
99
|
+
* unless something on the screen says otherwise, and the person who reads the
|
|
100
|
+
* screen is rarely the person who read the source. Print it beside the record.
|
|
101
|
+
*
|
|
102
|
+
* ── THE MECHANISM, WHICH THE PRINTED SENTENCE NO LONGER NAMES (9.88.0) ─────
|
|
103
|
+
* `buildReceipt` is called from `stages/callLLM.ts` with the request about to
|
|
104
|
+
* be passed to `LLMProvider.complete` — the PORT, this library's last sight of
|
|
105
|
+
* it. Three things sit downstream of that call and none of them is on the
|
|
106
|
+
* record: a provider decorated by the consumer (`complete()` wrapping
|
|
107
|
+
* `complete()`), a vendor adapter's own serializer, and the vendor's
|
|
108
|
+
* server-side defaults. `test/lib/time-travel/receipt-conformance.test.ts`
|
|
109
|
+
* reproduces the first of those — a decorator that appends a system suffix and
|
|
110
|
+
* a ghost tool after the mint, with `cache.transform` still reading
|
|
111
|
+
* `'unchanged'`, correctly.
|
|
112
|
+
*
|
|
113
|
+
* The sentence below used to say all of that, and it was PRINTED beside a
|
|
114
|
+
* trace by renderers that append it to a gap. A printed sentence names no
|
|
115
|
+
* module, function or call: it says which fields, what they mean here, and
|
|
116
|
+
* what to do — the rule in `test/helpers/gapProseClaims.ts`. So the mechanism
|
|
117
|
+
* lives in this comment, where a maintainer reads it and review catches its
|
|
118
|
+
* rot, and the constant says only the thing a reader must not get wrong.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* import { RECEIPT_BOUNDARY, receiptAt } from 'agentfootprint';
|
|
123
|
+
*
|
|
124
|
+
* const receipt = receiptAt(agent.getSnapshot()!, 1)!;
|
|
125
|
+
* if (receipt.cache.transform === 'unchanged') {
|
|
126
|
+
* console.log(`the cache strategy changed nothing. ${RECEIPT_BOUNDARY}`);
|
|
127
|
+
* }
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export declare const RECEIPT_BOUNDARY: string;
|
|
131
|
+
/** The scope key the receipt is committed under. */
|
|
132
|
+
export declare const RECEIPT_KEY = "receipt";
|
|
133
|
+
/** The scope key holding the name of the tool a `'tool-forced'` output
|
|
134
|
+
* strategy put on the wire. `seed` commits it; `servedView.ts` · `servedAt`
|
|
135
|
+
* reads it — so a rebuild can NAME the forced tool without reading the
|
|
136
|
+
* receipt it is being checked against. */
|
|
137
|
+
export declare const FORCED_OUTPUT_TOOL_KEY = "forcedOutputToolName";
|
|
138
|
+
/** How many hex characters of the digest a receipt keeps. 16 — 64 bits, which
|
|
139
|
+
* is far past collision range for the few dozen pieces of one run, and short
|
|
140
|
+
* enough to read in a terminal. */
|
|
141
|
+
export declare const RECEIPT_HASH_CHARS = 16;
|
|
142
|
+
/** One piece of the composed system string. */
|
|
143
|
+
export interface ReceiptPiece {
|
|
144
|
+
readonly hash: string;
|
|
145
|
+
readonly slot: ContextSlot;
|
|
146
|
+
readonly source: ContextSource;
|
|
147
|
+
}
|
|
148
|
+
/** One message as it went out. */
|
|
149
|
+
export interface ReceiptMessage {
|
|
150
|
+
readonly role: ContextRole;
|
|
151
|
+
readonly hash: string;
|
|
152
|
+
/** The message's own join key when it has one — a tool result's
|
|
153
|
+
* `toolCallId`. Absent for every other message. */
|
|
154
|
+
readonly key?: string;
|
|
155
|
+
}
|
|
156
|
+
/** A line that existed on the request only and was never written to history. */
|
|
157
|
+
export interface ReceiptRequestOnlyMessage {
|
|
158
|
+
readonly role: ContextRole;
|
|
159
|
+
readonly hash: string;
|
|
160
|
+
/** Which library mechanism composed it. `'staged-refs-nudge'` today. */
|
|
161
|
+
readonly reason: string;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* One `cache_control` breakpoint the cache strategy actually APPLIED — three
|
|
165
|
+
* scalars, no bytes.
|
|
166
|
+
*
|
|
167
|
+
* `cache.transformHash` collapses a whole rewrite into one bit ("something
|
|
168
|
+
* changed"), and a digest over the whole prepared request is not comparable
|
|
169
|
+
* across epochs. The question that decides an Anthropic bill is narrower and
|
|
170
|
+
* concrete: *did the breakpoints move between call 3 and call 4?* Two receipts'
|
|
171
|
+
* `markersApplied` answer it by inspection.
|
|
172
|
+
*
|
|
173
|
+
* APPLIED, not offered: `scope.cacheMarkers` holds the CANDIDATES the strategy
|
|
174
|
+
* was given, and a strategy clamps them to what the provider allows. The
|
|
175
|
+
* candidates are on the record; which survived is not, which is why they ride
|
|
176
|
+
* here.
|
|
177
|
+
*
|
|
178
|
+
* @example did the breakpoints move between two turns?
|
|
179
|
+
* ```ts
|
|
180
|
+
* import { receiptAt } from 'agentfootprint';
|
|
181
|
+
*
|
|
182
|
+
* const at = (k: number) =>
|
|
183
|
+
* JSON.stringify(receiptAt(agent.getSnapshot()!, k)?.cache.markersApplied);
|
|
184
|
+
* at(3) === at(4); // false ⇒ the cached prefix moved, and the bill with it
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export interface ReceiptCacheMarker {
|
|
188
|
+
readonly field: 'system' | 'tools' | 'messages';
|
|
189
|
+
readonly boundaryIndex: number;
|
|
190
|
+
readonly ttl: 'short' | 'long';
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The sampling knobs the call went out with — scalars and short strings, no
|
|
194
|
+
* bytes, no privacy change.
|
|
195
|
+
*
|
|
196
|
+
* They decide the answer as surely as the prompt does: the same context at
|
|
197
|
+
* `temperature: 0` and at `1.2` is a different call, and "why did this turn
|
|
198
|
+
* ramble?" is unanswerable from a record that kept the prompt and dropped the
|
|
199
|
+
* dial.
|
|
200
|
+
*
|
|
201
|
+
* READ OFF THE PREPARED REQUEST — the object the port is handed, after the
|
|
202
|
+
* cache strategy has had it. This is the one part of a receipt that describes
|
|
203
|
+
* the post-strategy request rather than the pre-strategy one, and deliberately
|
|
204
|
+
* so: a strategy holds the whole composed request and can move a dial, and
|
|
205
|
+
* `params` claims in this very docstring to be what the PORT got.
|
|
206
|
+
* `servedView.ts` · `SERVED_GAPS` says the same from the other side —
|
|
207
|
+
* `cache-transform` names every field the log rebuilds pre-strategy, and
|
|
208
|
+
* `params` is not among them.
|
|
209
|
+
*
|
|
210
|
+
* PORT VALUES ONLY — see {@link RECEIPT_BOUNDARY}. An unset `maxTokens` may
|
|
211
|
+
* still become the model's own maximum inside a vendor SDK, and the receipt
|
|
212
|
+
* records that the library sent nothing, never what the vendor decided.
|
|
213
|
+
*
|
|
214
|
+
* @example compare the dial two turns went out on
|
|
215
|
+
* ```ts
|
|
216
|
+
* import { receiptAt } from 'agentfootprint';
|
|
217
|
+
*
|
|
218
|
+
* const snapshot = agent.getSnapshot()!;
|
|
219
|
+
* receiptAt(snapshot, 1)?.params.temperature; // 0.25
|
|
220
|
+
* receiptAt(snapshot, 2)?.params.temperature; // 0.25 — the dial did not move
|
|
221
|
+
* receiptAt(snapshot, 1)?.params.stop; // undefined: none was sent
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
export interface ReceiptParams {
|
|
225
|
+
readonly temperature?: number;
|
|
226
|
+
readonly maxTokens?: number;
|
|
227
|
+
/** `LLMRequest.thinking.budget` — the reasoning-token ceiling asked for. */
|
|
228
|
+
readonly thinkingBudget?: number;
|
|
229
|
+
readonly stop?: readonly string[];
|
|
230
|
+
/** The forced tool choice, as the port carried it. */
|
|
231
|
+
readonly toolChoice?: {
|
|
232
|
+
readonly type: string;
|
|
233
|
+
readonly name?: string;
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/** What a slot's budget dropped before the request was composed. */
|
|
237
|
+
export interface ReceiptAttentionOmission {
|
|
238
|
+
readonly count: number;
|
|
239
|
+
/** One hash per dropped summary — see the first law for why not the text. */
|
|
240
|
+
readonly hashes: readonly string[];
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* THE RECEIPT. One per composed request, committed at the call-llm stop.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```ts
|
|
247
|
+
* import { receiptAt } from 'agentfootprint';
|
|
248
|
+
*
|
|
249
|
+
* // Epochs are the run's OWN iteration numbers and start at 1.
|
|
250
|
+
* const receipt = receiptAt(agent.getSnapshot()!, 1);
|
|
251
|
+
* receipt?.tools.withheld; // 'wrap-up' on the out-of-budget call
|
|
252
|
+
* receipt?.messages.count; // how many turns went out
|
|
253
|
+
* receipt?.params.temperature; // the dial this turn went out on
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
export interface Receipt {
|
|
257
|
+
readonly system: {
|
|
258
|
+
readonly hash: string;
|
|
259
|
+
readonly chars: number;
|
|
260
|
+
readonly pieces: readonly ReceiptPiece[];
|
|
261
|
+
};
|
|
262
|
+
readonly messages: {
|
|
263
|
+
readonly count: number;
|
|
264
|
+
readonly entries: readonly ReceiptMessage[];
|
|
265
|
+
readonly requestOnly: readonly ReceiptRequestOnlyMessage[];
|
|
266
|
+
};
|
|
267
|
+
readonly tools: {
|
|
268
|
+
readonly names: readonly string[];
|
|
269
|
+
readonly schemaHashes: Readonly<Record<string, string>>;
|
|
270
|
+
/** The tool the model was forced to answer through, or `null`. */
|
|
271
|
+
readonly forced: string | null;
|
|
272
|
+
/** Why the tool list is empty when it would not otherwise be. */
|
|
273
|
+
readonly withheld: 'wrap-up' | null;
|
|
274
|
+
};
|
|
275
|
+
readonly cache: {
|
|
276
|
+
/**
|
|
277
|
+
* What the comparison between the request handed TO the cache strategy and
|
|
278
|
+
* the one it handed back could establish. BRANCH ON THIS, never on
|
|
279
|
+
* `transformHash === null`:
|
|
280
|
+
*
|
|
281
|
+
* - `'unchanged'` — the two serialize identically.
|
|
282
|
+
* - `'rewritten'` — they do not, and `transformHash` fingerprints the
|
|
283
|
+
* result.
|
|
284
|
+
* - `'unknown'` — one of them could not be serialized at all, so the
|
|
285
|
+
* receipt refuses to claim either.
|
|
286
|
+
*
|
|
287
|
+
* It scopes to the CACHE STRATEGY and to nothing else — see
|
|
288
|
+
* {@link RECEIPT_BOUNDARY}.
|
|
289
|
+
*/
|
|
290
|
+
readonly transform: 'unchanged' | 'rewritten' | 'unknown';
|
|
291
|
+
/** Hash of the request the cache strategy handed back, when it differed
|
|
292
|
+
* from the one it was given; `null` otherwise. */
|
|
293
|
+
readonly transformHash: string | null;
|
|
294
|
+
/** The breakpoints the strategy actually applied, in the order it applied
|
|
295
|
+
* them — see {@link ReceiptCacheMarker}. Empty when it applied none. */
|
|
296
|
+
readonly markersApplied: readonly ReceiptCacheMarker[];
|
|
297
|
+
};
|
|
298
|
+
/** The sampling knobs the call went out with — see {@link ReceiptParams}. */
|
|
299
|
+
readonly params: ReceiptParams;
|
|
300
|
+
/**
|
|
301
|
+
* Absent when no slot reported a drop — which, measured on 9.88.0, is EVERY
|
|
302
|
+
* run in both chart shapes: no boundary bubbles `slotCompositions` out of the
|
|
303
|
+
* slot subflow that writes it, so request assembly has nothing to pass. It is
|
|
304
|
+
* a key of `servedView.ts` · `UNGAPPED_FIELDS` for that reason: its absence
|
|
305
|
+
* is universal and says nothing about any particular recording. Absent means
|
|
306
|
+
* nobody recorded a drop, never that nothing was dropped.
|
|
307
|
+
*/
|
|
308
|
+
readonly omittedForAttention?: ReceiptAttentionOmission;
|
|
309
|
+
readonly basis: {
|
|
310
|
+
readonly epoch: number;
|
|
311
|
+
readonly runId: string;
|
|
312
|
+
readonly model: string;
|
|
313
|
+
readonly provider: string;
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* The receipt's hash: run-salted SHA-256, first {@link RECEIPT_HASH_CHARS} hex
|
|
318
|
+
* characters. Exported because it is half of the conformance law — a reader
|
|
319
|
+
* that rebuilds a served view proves the rebuild by hashing it the same way.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```ts
|
|
323
|
+
* import { receiptAt, receiptHash, servedAt } from 'agentfootprint';
|
|
324
|
+
*
|
|
325
|
+
* const view = servedAt(agent.getSnapshot()!, 1)!;
|
|
326
|
+
* const receipt = receiptAt(agent.getSnapshot()!, 1)!;
|
|
327
|
+
* receiptHash(receipt.basis.runId, view.system.text) === receipt.system.hash; // true
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
export declare function receiptHash(runId: string, content: string): string;
|
|
331
|
+
/**
|
|
332
|
+
* JSON with object keys in sorted order, at every depth — so two structurally
|
|
333
|
+
* equal values hash the same however they were built. Property order survives
|
|
334
|
+
* `structuredClone`, but it does NOT survive every path a value takes through
|
|
335
|
+
* a delta-encoded commit log and back, and a fingerprint that depends on
|
|
336
|
+
* insertion order is a fingerprint that reports a change nobody made.
|
|
337
|
+
*
|
|
338
|
+
* Returns `undefined` — never `''` — for anything JSON cannot express (a
|
|
339
|
+
* cycle, a `BigInt`, a value whose `toJSON` throws). It does not throw,
|
|
340
|
+
* because a receipt is bookkeeping and must never be able to stop a run; and
|
|
341
|
+
* it does not collapse, because `''` made every unreadable value equal to
|
|
342
|
+
* every other one. Two requests that both failed to serialize compared EQUAL,
|
|
343
|
+
* and the receipt wrote `transformHash: null` — "the cache strategy changed
|
|
344
|
+
* nothing" — about a pair it had not read. Callers substitute
|
|
345
|
+
* {@link UNSERIALIZABLE} where a digest needs bytes, and branch where a
|
|
346
|
+
* comparison needs truth.
|
|
347
|
+
*/
|
|
348
|
+
export declare function stableJson(value: unknown): string | undefined;
|
|
349
|
+
/**
|
|
350
|
+
* The bytes a tool schema's hash covers: the whole `LLMToolSchema` — `name`,
|
|
351
|
+
* `description`, `inputSchema` — as sorted-key JSON, so two schemas that are
|
|
352
|
+
* structurally the same hash the same however they were built, and with a
|
|
353
|
+
* schema JSON cannot express (a cycle, a `BigInt`) marked {@link UNSERIALIZABLE}
|
|
354
|
+
* rather than dropped or thrown on.
|
|
355
|
+
*
|
|
356
|
+
* WHAT TO PASS (9.89.0). The receipt hashes each tool AS HANDED TO THE PORT:
|
|
357
|
+
* `buildReceipt` is given the request's tool list, and that list is the
|
|
358
|
+
* `dynamicToolSchemas` the run committed, plus a forced answer tool when an
|
|
359
|
+
* output strategy adds one. `servedAt(k).tools.schemas[i]` is that committed
|
|
360
|
+
* list read back — the same shape, the same bytes — so a consumer holding a
|
|
361
|
+
* served view already holds the object this function takes. It does not take
|
|
362
|
+
* the served view, a `Tool`, or a `defineTool` definition: those carry an
|
|
363
|
+
* `execute`, `wants` and other fields the model never saw.
|
|
364
|
+
*
|
|
365
|
+
* WHY IT IS EXPORTED. A reader that rebuilds a served view proves the rebuild
|
|
366
|
+
* by hashing it the way the receipt did. `receiptHash` and `messageDigestInput`
|
|
367
|
+
* were exported in 9.88.0 for the system text, the pieces and the messages,
|
|
368
|
+
* and a consumer could verify all of them — and NOT the tools, because the
|
|
369
|
+
* serializer behind `schemaHashes` was internal. Its only options were to copy
|
|
370
|
+
* `stableJson` (a second owner of the rule, which drifts the day the digest
|
|
371
|
+
* gains a field, as the message digest did in 9.88.0) or to leave the schema
|
|
372
|
+
* rows unverified. This is the third digest half of the law, beside its two
|
|
373
|
+
* siblings, and the ONLY spelling of the schema rule: `buildReceipt` calls it
|
|
374
|
+
* too. `stableJson` stays off the root barrel for the same reason — a consumer
|
|
375
|
+
* composing `hash(stableJson(tool))` would be writing the rule a second time.
|
|
376
|
+
*
|
|
377
|
+
* @example verify every schema row of a receipt from outside
|
|
378
|
+
* ```ts
|
|
379
|
+
* import { receiptAt, receiptHash, servedAt, toolDigestInput } from 'agentfootprint';
|
|
380
|
+
*
|
|
381
|
+
* const snapshot = agent.getSnapshot()!;
|
|
382
|
+
* const view = servedAt(snapshot, 1)!;
|
|
383
|
+
* const receipt = receiptAt(snapshot, 1)!;
|
|
384
|
+
* for (const tool of view.tools.schemas) {
|
|
385
|
+
* receiptHash(receipt.basis.runId, toolDigestInput(tool)) ===
|
|
386
|
+
* receipt.tools.schemaHashes[tool.name]; // true, for every tool of every epoch
|
|
387
|
+
* }
|
|
388
|
+
* // A forced answer tool is in `schemaHashes` and NOT in `schemas` — its body
|
|
389
|
+
* // is a declared gap (`forced-tool-schema`), so there is no row to check.
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
export declare function toolDigestInput(tool: LLMToolSchema): string;
|
|
393
|
+
/**
|
|
394
|
+
* The bytes a message's hash covers, each field behind a separator: its role,
|
|
395
|
+
* its text, its `toolCallId`, its `toolName`, the calls it asked for (id, name,
|
|
396
|
+
* arguments and `providerMeta`) and its `thinkingBlocks`.
|
|
397
|
+
*
|
|
398
|
+
* WHY `toolCallId` (9.88.0). It is the JOIN KEY — `tool_use_id` on Anthropic's
|
|
399
|
+
* wire, `tool_call_id` on OpenAI's — that pairs a tool result to the call that
|
|
400
|
+
* asked for it. Two parallel calls whose results happen to have identical text
|
|
401
|
+
* hashed the SAME without it, so a mis-pairing (the answer to `c1` filed under
|
|
402
|
+
* `c2`) was invisible to the law: exactly the defect a receipt exists to catch.
|
|
403
|
+
*
|
|
404
|
+
* WHY `toolName` TOO (9.88.0). On two shipped providers the id is not the join
|
|
405
|
+
* key at all. `adapters/llm/GeminiProvider.ts` · `toGeminiContents` pairs a
|
|
406
|
+
* `functionResponse` to its call BY NAME and says so in its own comment; the
|
|
407
|
+
* id is optional there and dropped when it is not a real one.
|
|
408
|
+
* `adapters/llm/OllamaProvider.ts` · `toOllamaMessages` puts `tool_name` on
|
|
409
|
+
* the wire and only falls back to the id. So on those providers a receipt that
|
|
410
|
+
* covered `toolCallId` alone still could not see the very mis-pairing the
|
|
411
|
+
* field was added for: two `role:'tool'` messages with identical text and
|
|
412
|
+
* SWAPPED names fingerprinted identically. It rides as its own field behind
|
|
413
|
+
* the separator, beside the id, so a name moving between two results changes
|
|
414
|
+
* both hashes and neither can absorb the other's bytes.
|
|
415
|
+
*
|
|
416
|
+
* WHY `thinkingBlocks` and `providerMeta` (9.88.0). `adapters/types.ts` is
|
|
417
|
+
* explicit that a signed thinking block MUST be echoed byte-exact or the API
|
|
418
|
+
* rejects the turn, and `providerMeta` round-trips vendor state (Gemini's
|
|
419
|
+
* `thoughtSignature`, without which the turn after a tool call is refused).
|
|
420
|
+
* Both are things the model receives; a fingerprint that omitted them said two
|
|
421
|
+
* requests were the same when one of them would be rejected. They enter as a
|
|
422
|
+
* `stableJson` FINGERPRINT, so nothing quotable is added — a signature is an
|
|
423
|
+
* opaque token, not content.
|
|
424
|
+
*
|
|
425
|
+
* Still deliberately NOT covered: `injectedBy` (stripped before the request
|
|
426
|
+
* exists) and `ephemeral` (a persistence flag, invisible to the model). Two
|
|
427
|
+
* messages that hash the same are the same thing said to the model.
|
|
428
|
+
*/
|
|
429
|
+
export declare function messageDigestInput(message: LLMMessage): string;
|
|
430
|
+
/** What `buildReceipt` needs from the stage that composed the request. */
|
|
431
|
+
export interface BuildReceiptInput {
|
|
432
|
+
readonly runId: string;
|
|
433
|
+
readonly epoch: number;
|
|
434
|
+
readonly model: string;
|
|
435
|
+
readonly provider: string;
|
|
436
|
+
/** The joined system string, exactly as sent. */
|
|
437
|
+
readonly systemText: string;
|
|
438
|
+
/** The pieces it was joined from, in order. */
|
|
439
|
+
readonly systemPieces: readonly {
|
|
440
|
+
readonly text: string;
|
|
441
|
+
readonly slot: ContextSlot;
|
|
442
|
+
readonly source: ContextSource;
|
|
443
|
+
}[];
|
|
444
|
+
/** The messages that came from history, post-strip, in wire order. */
|
|
445
|
+
readonly messages: readonly LLMMessage[];
|
|
446
|
+
/** Lines appended to this request only, each with the mechanism that
|
|
447
|
+
* composed it. */
|
|
448
|
+
readonly requestOnly: readonly {
|
|
449
|
+
readonly message: LLMMessage;
|
|
450
|
+
readonly reason: string;
|
|
451
|
+
}[];
|
|
452
|
+
/** The tool list as sent — including a forced output tool, if there is one. */
|
|
453
|
+
readonly tools: readonly LLMToolSchema[];
|
|
454
|
+
readonly forced: string | null;
|
|
455
|
+
readonly withheld: 'wrap-up' | null;
|
|
456
|
+
/** The request handed TO the cache strategy — one half of the cache
|
|
457
|
+
* comparison, and nothing else is read off it. */
|
|
458
|
+
readonly baseRequest: unknown;
|
|
459
|
+
/**
|
|
460
|
+
* The request it handed back: the object the provider port is given.
|
|
461
|
+
*
|
|
462
|
+
* The other half of the cache comparison, AND where {@link ReceiptParams} is
|
|
463
|
+
* read from. A strategy is handed the whole composed request, so it can move
|
|
464
|
+
* a dial as easily as a marker; reading the knobs off `baseRequest` recorded
|
|
465
|
+
* what assembly PROPOSED and called it what the port was handed, which is
|
|
466
|
+
* the one thing `RECEIPT_BOUNDARY` promises a receipt never does.
|
|
467
|
+
*/
|
|
468
|
+
readonly preparedRequest: unknown;
|
|
469
|
+
/** The breakpoints the strategy reported applying (`prepareRequest`'s
|
|
470
|
+
* `markersApplied`). Absent ⇒ recorded as none. */
|
|
471
|
+
readonly markersApplied?: readonly {
|
|
472
|
+
readonly field: 'system' | 'tools' | 'messages';
|
|
473
|
+
readonly boundaryIndex: number;
|
|
474
|
+
readonly ttl: 'short' | 'long';
|
|
475
|
+
}[];
|
|
476
|
+
/** What a slot's budget dropped, when a slot reported any. */
|
|
477
|
+
readonly omittedForAttention?: {
|
|
478
|
+
readonly count: number;
|
|
479
|
+
readonly summaries: readonly string[];
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Mint the receipt for one composed request.
|
|
484
|
+
*
|
|
485
|
+
* Pure and total: every branch here is arithmetic over what it was handed, so
|
|
486
|
+
* a receipt cannot fail a call. The one judgement it makes is the cache
|
|
487
|
+
* comparison, and it makes it in three values rather than two: `'unchanged'`
|
|
488
|
+
* exactly when the strategy handed back a request that serializes identically
|
|
489
|
+
* to the one it was given, `'rewritten'` when it did not, and `'unknown'` when
|
|
490
|
+
* either request could not be serialized at all — because "I could not read
|
|
491
|
+
* them" is not the same claim as "they were the same".
|
|
492
|
+
*
|
|
493
|
+
* Every value it produces is true AT THE PROVIDER PORT and nowhere past it —
|
|
494
|
+
* see {@link RECEIPT_BOUNDARY}.
|
|
495
|
+
*/
|
|
496
|
+
export declare function buildReceipt(input: BuildReceiptInput): Receipt;
|