@krishnadobhal/rewind-sdk-js 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 +182 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# @krishnadobhal/rewind-sdk-js
|
|
2
|
+
|
|
3
|
+
Record every boundary a LangGraph agent crosses — each model call, each tool call, the
|
|
4
|
+
exact request and the exact response — so you can read back what actually happened
|
|
5
|
+
instead of guessing from logs.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @krishnadobhal/rewind-sdk-js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
An agent that goes wrong leaves you a transcript and a bill. What you need is the
|
|
14
|
+
*request* — the fully-resolved prompt after every template, tool schema and message
|
|
15
|
+
rewrite — and there is no log line that contains it. Rewind sits at the boundary, hashes
|
|
16
|
+
the request canonically, and stores both halves of the exchange.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { withRewind } from '@krishnadobhal/rewind-sdk-js/middleware';
|
|
22
|
+
|
|
23
|
+
const rw = await withRewind({ model, tools });
|
|
24
|
+
|
|
25
|
+
// Hand the instrumented pair to your graph. Nothing else changes.
|
|
26
|
+
const graph = buildGraph({ model: rw.model, tools: rw.tools });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then switch it on from the environment:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
REWIND_ENABLED=1 node agent.js
|
|
33
|
+
cat .rewind/runs/*/steps.jsonl | jq '{seq,node,kind,req_hash}'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**With no variable set, `withRewind` hands back your original model and tools**, unwrapped.
|
|
37
|
+
An uninstrumented run costs nothing and takes no branches, so this is safe to leave in
|
|
38
|
+
production code.
|
|
39
|
+
|
|
40
|
+
## The environment is the interface
|
|
41
|
+
|
|
42
|
+
There is no CLI. Configuration is the variables the SDK reads inside your process, because
|
|
43
|
+
redaction has to happen *before* anything leaves it — a wrapper watching from outside only
|
|
44
|
+
sees bytes on a socket, by which point the PII is already gone.
|
|
45
|
+
|
|
46
|
+
| | |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `REWIND_ENABLED=1` | record this run |
|
|
49
|
+
| `REWIND_DIR` | where recordings go (default `.rewind`) |
|
|
50
|
+
| `REWIND_SERVER` | send writes to an ingest server instead of a directory |
|
|
51
|
+
| `REWIND_TOKEN` | bearer token for that server |
|
|
52
|
+
| `REWIND_CONFIG` | path to a `rewind.config.ts` |
|
|
53
|
+
| `REWIND_ENV_FILE` | a `.env` to load, or `0` to skip it |
|
|
54
|
+
|
|
55
|
+
A `.env` in the working directory is loaded automatically, *before* `REWIND_ENABLED` is
|
|
56
|
+
checked — so a value in the file can switch recording on without touching your shell.
|
|
57
|
+
|
|
58
|
+
## What gets recorded
|
|
59
|
+
|
|
60
|
+
Every crossing becomes a `Step` plus a content-addressed `Cassette`:
|
|
61
|
+
|
|
62
|
+
```jsonc
|
|
63
|
+
{
|
|
64
|
+
"seq": 0,
|
|
65
|
+
"node": "plan", // the LangGraph node it happened in
|
|
66
|
+
"kind": "model", // model · tool · clock · rng · human · env
|
|
67
|
+
"req_hash": "9c28dd0e…", // sha256 over the canonical request
|
|
68
|
+
"match_tier": "recorded",
|
|
69
|
+
"latency_ms": 420,
|
|
70
|
+
"tokens": 900,
|
|
71
|
+
"cost_usd": 0.004
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The hash is what makes a call *identifiable*. Two runs that asked the same thing share one
|
|
76
|
+
cassette, so 500 runs behind the same system prompt store it once. Canonicalization is
|
|
77
|
+
[RFC 8785 JCS](https://www.rfc-editor.org/rfc/rfc8785) plus normalization that scrubs
|
|
78
|
+
volatile ids to ordinals — which is why two recordings of the same question hash
|
|
79
|
+
identically even though LangChain stamps a fresh UUID on every message.
|
|
80
|
+
|
|
81
|
+
## Redaction happens at write time
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// rewind.config.ts
|
|
85
|
+
import { defineConfig } from '@krishnadobhal/rewind-sdk-js/config';
|
|
86
|
+
|
|
87
|
+
export default defineConfig({
|
|
88
|
+
dir: '.rewind',
|
|
89
|
+
redact: {
|
|
90
|
+
preset: 'default', // emails, phones, cards, keys, JWTs
|
|
91
|
+
fields: ['ssn', 'dateOfBirth'], // by key name, at any depth
|
|
92
|
+
custom: [/ACC-\d{8}/g],
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A cassette is written already redacted. There is no unredacted copy on disk to leak,
|
|
98
|
+
because the plaintext never reaches the store — only the hash is computed over it, so
|
|
99
|
+
recordings stay matchable without holding the sensitive value.
|
|
100
|
+
|
|
101
|
+
The redactor is also callable directly:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { redact } from '@krishnadobhal/rewind-sdk-js/redact';
|
|
105
|
+
|
|
106
|
+
const { value, map } = redact({ to: 'a@b.com', note: 'call 555-123-4567' }, { preset: 'default' });
|
|
107
|
+
|
|
108
|
+
// value { to: '[redacted:email:0]', note: 'call [redacted:phone:0]' }
|
|
109
|
+
// map { '[redacted:email:0]': 'email', '[redacted:phone:0]': 'phone' }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Recording somewhere other than a disk
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
REWIND_ENABLED=1 REWIND_SERVER=http://localhost:4000 node agent.js
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The SDK swaps its directory store for a batching HTTP client and your graph is none the
|
|
119
|
+
wiser. `Recorder.record()` runs inside your nodes, where it may neither block nor be
|
|
120
|
+
awaited, so writes go through a bounded queue with a background flush:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { bufferedStore } from '@krishnadobhal/rewind-sdk-js/emitter';
|
|
124
|
+
import { httpStore } from '@krishnadobhal/rewind-sdk-js/http';
|
|
125
|
+
|
|
126
|
+
const store = bufferedStore({ target: httpStore({ url, token }), maxQueue: 1000 });
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
See [`@krishnadobhal/rewind-server`](https://www.npmjs.com/package/@krishnadobhal/rewind-server)
|
|
130
|
+
for the other end, and [`@krishnadobhal/rewind-ui`](https://www.npmjs.com/package/@krishnadobhal/rewind-ui)
|
|
131
|
+
for the viewer.
|
|
132
|
+
|
|
133
|
+
## It fails open, on purpose
|
|
134
|
+
|
|
135
|
+
A full disk, an unreachable server, a serialization bug — each is counted and logged, not
|
|
136
|
+
thrown. Your agent keeps running and the run is marked `partial`. Observability must never
|
|
137
|
+
take down the thing it observes, and a recorder that can crash your agent is worse than no
|
|
138
|
+
recorder.
|
|
139
|
+
|
|
140
|
+
A `partial` run is honestly labelled rather than quietly trusted: it dropped events, so
|
|
141
|
+
nothing may treat it as a complete account of what happened.
|
|
142
|
+
|
|
143
|
+
## Reading recordings back
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { readTrace, listRuns, readCassette } from '@krishnadobhal/rewind-sdk-js/store';
|
|
147
|
+
|
|
148
|
+
for (const id of listRuns('.rewind')) {
|
|
149
|
+
const { run, steps } = readTrace('.rewind', id)!;
|
|
150
|
+
console.log(id, run.status, steps.length);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Exports
|
|
155
|
+
|
|
156
|
+
| | |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `/middleware` | `withRewind` — the one function most code needs |
|
|
159
|
+
| `/recorder` | `Recorder` for driving it yourself |
|
|
160
|
+
| `/redact` | `redact()` and its config types |
|
|
161
|
+
| `/store` | `fileStore`, `readTrace`, `listRuns`, `readCassette` |
|
|
162
|
+
| `/emitter` | `bufferedStore` — the bounded queue |
|
|
163
|
+
| `/http` | `httpStore` — the ingest client |
|
|
164
|
+
| `/config` | `defineConfig`, `DEFAULT_DIR` |
|
|
165
|
+
| `/env` | the variable names, `loadEnvFile`, `recorderFromEnv` |
|
|
166
|
+
|
|
167
|
+
## Requirements
|
|
168
|
+
|
|
169
|
+
Node 22.6+ (24 recommended) and ESM. TypeScript types ship with the package.
|
|
170
|
+
|
|
171
|
+
## Scope
|
|
172
|
+
|
|
173
|
+
Recording is what this does. **Replay — re-running a recorded run against its cassettes
|
|
174
|
+
with the network blocked — is not in this release**; it is parked on the `replay-engine`
|
|
175
|
+
branch pending a way to replay a run that lives on a server rather than on local disk.
|
|
176
|
+
|
|
177
|
+
No clock or RNG shims yet either, so an agent that reads the wall clock or a random source
|
|
178
|
+
has a boundary Rewind does not see.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
Apache-2.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krishnadobhal/rewind-sdk-js",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Record every boundary an LLM agent crosses: withRewind, the recorder, the redactor, and the stores.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Krishna Dobhal",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@krishnadobhal/rewind-core": "0.1.
|
|
41
|
+
"@krishnadobhal/rewind-core": "0.1.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^24.13.3"
|