@krishnadobhal/rewind-core 0.1.0 → 0.1.1
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/README.md +118 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# @krishnadobhal/rewind-core
|
|
2
|
+
|
|
3
|
+
The question every replay tool has to answer is *"is this call the same call I recorded?"*
|
|
4
|
+
This package is that answer: canonical request hashing, and the schema the rest of
|
|
5
|
+
[Rewind](https://github.com/krishnadobhal/Rewind) is written against.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @krishnadobhal/rewind-core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
You rarely install this directly — it arrives as a dependency of
|
|
12
|
+
[`@krishnadobhal/rewind-sdk-js`](https://www.npmjs.com/package/@krishnadobhal/rewind-sdk-js)
|
|
13
|
+
and [`@krishnadobhal/rewind-server`](https://www.npmjs.com/package/@krishnadobhal/rewind-server).
|
|
14
|
+
Reach for it when you need the hash itself.
|
|
15
|
+
|
|
16
|
+
## The hash
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { reqHash, stateHash, HASH_VERSION } from '@krishnadobhal/rewind-core/hash';
|
|
20
|
+
|
|
21
|
+
reqHash({
|
|
22
|
+
kind: 'model',
|
|
23
|
+
provider: 'anthropic',
|
|
24
|
+
model_id: 'claude-sonnet-5',
|
|
25
|
+
messages: [{ role: 'user', content: 'summarise this' }],
|
|
26
|
+
});
|
|
27
|
+
// 'faba1bab0b866b412ff8992582fc2a4bc0be5864895e9790dc4e4215fdb3301d'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
sha256( HASH_VERSION ‖ 0x00 ‖ jcs( canonical(request) ) )
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`canonical` decides *what counts as the identity of a call*; [RFC 8785
|
|
35
|
+
JCS](https://www.rfc-editor.org/rfc/rfc8785) then serializes it with stable key order,
|
|
36
|
+
number formatting and string escaping. Hand-rolling that last part is where unicode and
|
|
37
|
+
number edge cases bite, so it is not hand-rolled.
|
|
38
|
+
|
|
39
|
+
`reqHash` returns bare hex. `stateHash` runs the same canonicalization over any value and
|
|
40
|
+
returns a `sha256:`-prefixed string — that prefix is the only difference, and mixing the
|
|
41
|
+
two up is the most common mistake when reading these values back.
|
|
42
|
+
|
|
43
|
+
## Getting the boundary right
|
|
44
|
+
|
|
45
|
+
Too strict and every harmless edit looks like a divergence. Too loose and you match a
|
|
46
|
+
stale answer to a different question — worse, because it looks like a result.
|
|
47
|
+
|
|
48
|
+
**In the hash:** provider, model id, normalized messages, system prompt digest, tool
|
|
49
|
+
schemas sorted by name with their full JSON Schema, response format, sampling parameters,
|
|
50
|
+
tool choice. A changed tool parameter changes behaviour, so it changes identity.
|
|
51
|
+
|
|
52
|
+
**Excluded:** request ids, idempotency keys, absolute timestamps, latency, trace headers,
|
|
53
|
+
user agents, retry counters, SDK versions, provider-side message ids, and **every auth
|
|
54
|
+
header and API key**. Excluding secrets is a correctness requirement as much as a security
|
|
55
|
+
one — a rotated key must not invalidate a corpus of recordings.
|
|
56
|
+
|
|
57
|
+
Normalization, in order: Unicode NFC · whitespace collapse (but never re-wrapping, since
|
|
58
|
+
indentation inside fenced code blocks changes model behaviour) · volatile ids scrubbed to
|
|
59
|
+
ordinals so the same conversation shape hashes identically across runs · base64 media
|
|
60
|
+
replaced by the digest of its decoded bytes, so a 4 MB image never enters the hash input ·
|
|
61
|
+
`undefined`/`null`/omitted collapsed to omitted, while empty arrays and strings are kept
|
|
62
|
+
because they are meaningful.
|
|
63
|
+
|
|
64
|
+
## HASH_VERSION is a wire format
|
|
65
|
+
|
|
66
|
+
Currently **3**. Every cassette records the version it was written under, and a reader
|
|
67
|
+
refuses to mix versions rather than silently missing on every step.
|
|
68
|
+
|
|
69
|
+
Any change to canonicalization — however small — needs the version bumped, the goldens
|
|
70
|
+
regenerated, and old recordings re-indexed or quarantined. If the goldens did not move,
|
|
71
|
+
you did not change what you thought you changed.
|
|
72
|
+
|
|
73
|
+
## Schema
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import type { Run, Step, Cassette, StepKind, MatchTier } from '@krishnadobhal/rewind-core/schema';
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Three entities and that is the whole vocabulary. A `Run` has many `Step`s; many steps
|
|
80
|
+
point at the same `Cassette`, which is content-addressed by `req_hash` and immutable — a
|
|
81
|
+
corrected recording is a new cassette, never an overwrite.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
STEP_KINDS // 'model' · 'tool' · 'clock' · 'rng' · 'human' · 'env'
|
|
85
|
+
MATCH_TIERS // 'exact' · 'miss' · 'recorded'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`recorded` is what a live run writes; `exact` and `miss` are what a replay reports. The
|
|
89
|
+
tier is always stored explicitly, never inferred from context.
|
|
90
|
+
|
|
91
|
+
## Request types
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import type { RewindRequest, ModelRequest, ToolRequest, Message } from '@krishnadobhal/rewind-core/request';
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A discriminated union on `kind`. Framework-shaped objects are flattened into these before
|
|
98
|
+
hashing — LangChain `BaseMessage` classes become `{ role, content }`, and a Zod schema
|
|
99
|
+
becomes the JSON Schema `bindTools` produced, with its lazily-filled `_cached` stripped.
|
|
100
|
+
Without that last detail the same schema hashes differently before and after its first
|
|
101
|
+
validation.
|
|
102
|
+
|
|
103
|
+
## Also exported
|
|
104
|
+
|
|
105
|
+
`normalizeText(input)` — the text normalizer, on its own, for testing what a change to it
|
|
106
|
+
would do.
|
|
107
|
+
|
|
108
|
+
`canonical(request)` — the canonical object *before* serialization. Useful when a hash
|
|
109
|
+
mismatch needs explaining: diff two canonical forms and the differing field is the answer.
|
|
110
|
+
|
|
111
|
+
## Requirements
|
|
112
|
+
|
|
113
|
+
Node 22.6+ (24 recommended) and ESM. Types ship with the package. One runtime dependency,
|
|
114
|
+
[`canonicalize`](https://www.npmjs.com/package/canonicalize), for JCS.
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
Apache-2.0
|
package/package.json
CHANGED