agentcassette 0.1.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/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/cli.cjs +437 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +435 -0
- package/dist/cli.js.map +1 -0
- package/dist/client-CgkW5WWa.d.cts +179 -0
- package/dist/client-CgkW5WWa.d.ts +179 -0
- package/dist/index.cjs +903 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +881 -0
- package/dist/index.js.map +1 -0
- package/dist/jest.cjs +500 -0
- package/dist/jest.cjs.map +1 -0
- package/dist/jest.d.cts +7 -0
- package/dist/jest.d.ts +7 -0
- package/dist/jest.js +498 -0
- package/dist/jest.js.map +1 -0
- package/dist/testing-Bj7F-tqU.d.ts +9 -0
- package/dist/testing-S6vBH_XM.d.cts +9 -0
- package/dist/vitest.cjs +500 -0
- package/dist/vitest.cjs.map +1 -0
- package/dist/vitest.d.cts +7 -0
- package/dist/vitest.d.ts +7 -0
- package/dist/vitest.js +498 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Om Thakur
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# agentcassette
|
|
2
|
+
|
|
3
|
+
[](https://github.com/theomthakur/agentcassette/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/agentcassette)
|
|
5
|
+
[](package.json)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
**Record an LLM agent run once. Replay it in CI with zero API calls and zero non-determinism.**
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install --save-dev agentcassette
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Recording happens at the **semantic layer**, not the HTTP layer: messages, tool calls with their
|
|
15
|
+
arguments, tool results and final output, normalized across OpenAI, Anthropic, Google and
|
|
16
|
+
OpenAI-compatible shapes. A cassette therefore survives an SDK upgrade or a provider swap, which
|
|
17
|
+
is exactly where HTTP-level recorders like Polly and nock break.
|
|
18
|
+
|
|
19
|
+
Zero runtime dependencies.
|
|
20
|
+
|
|
21
|
+
## When a replay diverges, it tells you why
|
|
22
|
+
|
|
23
|
+
Matching uses a configurable request fingerprint rather than byte equality, so prompts can change
|
|
24
|
+
without invalidating every cassette. When a request genuinely no longer matches, you get this
|
|
25
|
+
instead of a bare failure:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Agent cassette diverged at turn 1 in "trip-planner".
|
|
29
|
+
Expected fingerprint: sha256:4a462fea...
|
|
30
|
+
Actual fingerprint: sha256:6e2ebeae...
|
|
31
|
+
|
|
32
|
+
Structured request diff:
|
|
33
|
+
$.messages[0].content[0].text (changed)
|
|
34
|
+
expected: "capital of France?"
|
|
35
|
+
actual: "capital of Germany?"
|
|
36
|
+
|
|
37
|
+
Fix the changed request, re-record this cassette intentionally, or provide a custom
|
|
38
|
+
fingerprint when the change is non-behavioral.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
You fix it from the message without opening the cassette.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
Deterministic, semantic record and replay for LLM agent tests. Run agent tests in CI with **zero API calls** and no provider-shaped fixture code.
|
|
46
|
+
|
|
47
|
+
## Before and after
|
|
48
|
+
|
|
49
|
+
Before, every test calls the provider and can fail because the model changed:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const result = await openai.chat.completions.create(request);
|
|
53
|
+
expect(result.choices[0].message.content).toContain("Paris");
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
After, wrap that same client boundary once. The first run records; every later run replays:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { openAI } from "agentcassette";
|
|
60
|
+
import { withVitestCassette } from "agentcassette/vitest";
|
|
61
|
+
|
|
62
|
+
type Request = Parameters<typeof openai.chat.completions.create>[0];
|
|
63
|
+
type Response = Awaited<ReturnType<typeof openai.chat.completions.create>>;
|
|
64
|
+
|
|
65
|
+
const chat = await withVitestCassette<Request, Response>({
|
|
66
|
+
name: "trip-planner",
|
|
67
|
+
client: (request) => openai.chat.completions.create(request),
|
|
68
|
+
adapter: openAI<Request, Response>(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const result = await chat(request);
|
|
72
|
+
expect(result.choices[0].message.content).toContain("Paris");
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
No mode switch is needed. `auto` (the default) replays when `.agentcassette/cassettes/trip-planner.json` exists and records when it does not. Cassette names must be lowercase and may contain digits, dots, underscores, and hyphens; unsafe or ambiguous filesystem names are rejected.
|
|
76
|
+
|
|
77
|
+
## Why semantic recording
|
|
78
|
+
|
|
79
|
+
HTTP cassettes capture URLs, headers, streaming frames, and SDK serialization details. They break when a provider or SDK changes even if the agent makes the same decisions. agentcassette records the provider-independent behavior instead:
|
|
80
|
+
|
|
81
|
+
1. messages sent;
|
|
82
|
+
2. tool calls proposed, including normalized arguments;
|
|
83
|
+
3. tool results returned;
|
|
84
|
+
4. final output and token/cost usage.
|
|
85
|
+
|
|
86
|
+
Built-in adapters normalize OpenAI, OpenAI-compatible APIs, Anthropic, and Google into the same format. Cassettes are formatted JSON intended to be read and reviewed in pull requests.
|
|
87
|
+
|
|
88
|
+
## Install
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
npm install --save-dev agentcassette
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Node.js 18.18 or newer is required. The package has zero runtime dependencies and ships ESM and CommonJS builds.
|
|
95
|
+
|
|
96
|
+
## Modes
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const chat = await createCassetteClient({
|
|
100
|
+
name: "trip-planner",
|
|
101
|
+
mode: "record", // "record" | "replay" | "auto"
|
|
102
|
+
client: callProvider,
|
|
103
|
+
adapter: openAI(),
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- `record` calls the real boundary and writes each completed turn atomically.
|
|
108
|
+
- `replay` serves normalized responses from the cassette. The real boundary is never called for replayed turns and may be omitted.
|
|
109
|
+
- `auto` replays an existing cassette or records a new one.
|
|
110
|
+
|
|
111
|
+
Calls through one wrapper are serialized so concurrent application code cannot make cassette order nondeterministic.
|
|
112
|
+
|
|
113
|
+
## Matching and divergence
|
|
114
|
+
|
|
115
|
+
The default request fingerprint includes:
|
|
116
|
+
|
|
117
|
+
- tool names and normalized input schemas;
|
|
118
|
+
- conversation role/content-part structure;
|
|
119
|
+
- normalized prior user turns, assistant decisions, and tool results;
|
|
120
|
+
- normalized semantic text of the final user turn.
|
|
121
|
+
|
|
122
|
+
It ignores model names, system-prompt text, whitespace changes, and timestamps. Replace it when your agent has different behavioral boundaries:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const chat = await createCassetteClient({
|
|
126
|
+
name: "router",
|
|
127
|
+
mode: "replay",
|
|
128
|
+
adapter: anthropic(),
|
|
129
|
+
fingerprint: (request) => `route:${request.tools.map((tool) => tool.name).join(",")}`,
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A mismatch throws `CassetteDivergenceError`. Its message includes the one-based divergent turn, expected and actual fingerprints, path-level additions/removals/changes, expected and actual values, and remediation. The same structured data is available on `error.report`.
|
|
134
|
+
|
|
135
|
+
## Partial replay
|
|
136
|
+
|
|
137
|
+
Replay a known-good prefix, then debug the live tail:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const chat = await createCassetteClient({
|
|
141
|
+
name: "long-agent-run",
|
|
142
|
+
mode: "replay",
|
|
143
|
+
replayTurns: 12,
|
|
144
|
+
client: callProvider,
|
|
145
|
+
adapter: anthropic(),
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Turns 1–12 make zero network calls. Turn 13 onward uses `client`; tail requests are intentionally not matched against or written to the cassette.
|
|
150
|
+
|
|
151
|
+
## Redaction
|
|
152
|
+
|
|
153
|
+
Common secret keys, bearer tokens, email addresses, and phone numbers are redacted before writing. Add domain-specific rules:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const chat = await createCassetteClient({
|
|
157
|
+
name: "support-agent",
|
|
158
|
+
mode: "record",
|
|
159
|
+
client: callProvider,
|
|
160
|
+
adapter: google(),
|
|
161
|
+
redaction: {
|
|
162
|
+
rules: [{
|
|
163
|
+
name: "customer-id",
|
|
164
|
+
match: ({ key }) => key === "customerId",
|
|
165
|
+
replacement: "[CUSTOMER]",
|
|
166
|
+
}],
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Redaction happens before fingerprinting, so replay and `verify` remain consistent without retaining the original sensitive value. Review custom rules carefully: a cassette is still source-controlled test data.
|
|
172
|
+
|
|
173
|
+
## Provider adapters
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { openAI, openAICompatible, anthropic, google, semantic } from "agentcassette";
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Each adapter accepts request/response generics and synthesizes a plain provider-shaped data object during replay, including normalized IDs, model, content, tool calls, finish reason, and usage when recorded. SDK class methods and non-semantic transport metadata are intentionally not reproduced; use a custom `ProviderAdapter<Request, Response>` when application code depends on them. `semantic()` is useful for a custom boundary that already uses `SemanticRequest` and `SemanticResponse`.
|
|
180
|
+
|
|
181
|
+
## Vitest and Jest
|
|
182
|
+
|
|
183
|
+
Both adapters use the same one-wrapper-call API and do not import their test runner at runtime:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { withVitestCassette } from "agentcassette/vitest";
|
|
187
|
+
// import { withJestCassette } from "agentcassette/jest";
|
|
188
|
+
|
|
189
|
+
const agent = await withVitestCassette({
|
|
190
|
+
name: "tool-agent",
|
|
191
|
+
client: runAgentTurn,
|
|
192
|
+
adapter: semantic(),
|
|
193
|
+
testFile: import.meta.filename,
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Set `AGENTCASSETTE_MODE=record|replay|auto` to override mode for a whole test run. `AGENTCASSETTE_DIR` changes the cassette directory.
|
|
198
|
+
|
|
199
|
+
## CLI
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
npx agentcassette list
|
|
203
|
+
npx agentcassette stats
|
|
204
|
+
npx agentcassette verify -- npm test
|
|
205
|
+
npx agentcassette prune # dry run
|
|
206
|
+
npx agentcassette prune --yes --name old-agent-cassette # delete one reviewed candidate
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`verify` validates complete cassette structure, indexes, and fingerprints and checks source references or observed test-runner usage. With a command after `--`, it runs that command with replay forced, so divergence or missing cassettes fail CI. `prune` uses source references and locally tracked runtime usage only to propose candidates; deletion requires `--yes` plus an explicit `--name` for every reviewed cassette, so heuristics never authorize deletion. `stats` reports recorded tokens/cost and savings from replays performed through the Vitest/Jest adapters.
|
|
210
|
+
|
|
211
|
+
Providers generally report tokens but not dollar cost. Store cost with `calculateCost`:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
calculateCost: ({ response }) => {
|
|
215
|
+
const usage = response.usage;
|
|
216
|
+
return usage ? usage.inputTokens * 0.000001 + usage.outputTokens * 0.000002 : undefined;
|
|
217
|
+
},
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Use prices appropriate to your provider and model; agentcassette deliberately does not ship a changing pricing table.
|
|
221
|
+
|
|
222
|
+
## Cassette format
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"schemaVersion": 1,
|
|
227
|
+
"name": "trip-planner",
|
|
228
|
+
"turns": [{
|
|
229
|
+
"index": 0,
|
|
230
|
+
"fingerprint": "sha256:…",
|
|
231
|
+
"request": { "messages": [], "tools": [] },
|
|
232
|
+
"events": [
|
|
233
|
+
{ "type": "messages.sent", "messages": [] },
|
|
234
|
+
{ "type": "output.final", "content": [{ "type": "text", "text": "…" }] }
|
|
235
|
+
],
|
|
236
|
+
"response": { "content": [], "toolCalls": [] }
|
|
237
|
+
}]
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The complete request and response are retained in normalized form so divergence reports are self-contained. No raw HTTP payload, authorization header, provider URL, or SDK object is recorded.
|
|
242
|
+
|
|
243
|
+
## Scope
|
|
244
|
+
|
|
245
|
+
agentcassette records and replays deterministic agent behavior. It does not evaluate, score, grade, trace, host, or visualize agent runs.
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT
|