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/dist/vitest.cjs
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var path = require('path');
|
|
4
|
+
var crypto = require('crypto');
|
|
5
|
+
var promises = require('fs/promises');
|
|
6
|
+
|
|
7
|
+
// src/adapters/testing.ts
|
|
8
|
+
|
|
9
|
+
// src/core/canonical.ts
|
|
10
|
+
var ISO_TIMESTAMP = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})\b/giu;
|
|
11
|
+
var UNIX_TIMESTAMP = /\b1[5-9]\d{8}(?:\d{3})?\b/gu;
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function asJsonValue(value) {
|
|
16
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
17
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
|
|
18
|
+
if (Array.isArray(value)) return value.map(asJsonValue);
|
|
19
|
+
if (isRecord(value)) {
|
|
20
|
+
const result = {};
|
|
21
|
+
for (const [key, item] of Object.entries(value)) {
|
|
22
|
+
if (item !== void 0) result[key] = asJsonValue(item);
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
return String(value);
|
|
27
|
+
}
|
|
28
|
+
function stableStringify(value) {
|
|
29
|
+
return JSON.stringify(sortValue(asJsonValue(value)));
|
|
30
|
+
}
|
|
31
|
+
function sortValue(value) {
|
|
32
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
33
|
+
if (isRecord(value)) {
|
|
34
|
+
const result = {};
|
|
35
|
+
for (const key of Object.keys(value).sort()) {
|
|
36
|
+
const item = value[key];
|
|
37
|
+
if (item !== void 0) result[key] = sortValue(item);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function normalizeSemanticText(text) {
|
|
44
|
+
return text.normalize("NFKC").replace(ISO_TIMESTAMP, "<timestamp>").replace(UNIX_TIMESTAMP, "<timestamp>").replace(/\s+/gu, " ").trim();
|
|
45
|
+
}
|
|
46
|
+
function contentToText(content) {
|
|
47
|
+
return content.map((part) => {
|
|
48
|
+
if (part.type === "text") return part.text;
|
|
49
|
+
if (part.type === "json") return stableStringify(part.value);
|
|
50
|
+
return `[image:${part.source}]`;
|
|
51
|
+
}).join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/core/diff.ts
|
|
55
|
+
function structuredDiff(expected, actual) {
|
|
56
|
+
const entries = [];
|
|
57
|
+
walk(expected, actual, "$", entries);
|
|
58
|
+
return entries;
|
|
59
|
+
}
|
|
60
|
+
function walk(expected, actual, path, entries) {
|
|
61
|
+
if (Object.is(expected, actual)) return;
|
|
62
|
+
if (expected === void 0) {
|
|
63
|
+
entries.push({ path, kind: "added", actual: toDiffValue(actual) });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (actual === void 0) {
|
|
67
|
+
entries.push({ path, kind: "removed", expected: toDiffValue(expected) });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(expected) && Array.isArray(actual)) {
|
|
71
|
+
const length = Math.max(expected.length, actual.length);
|
|
72
|
+
for (let index = 0; index < length; index += 1) {
|
|
73
|
+
walk(expected[index], actual[index], `${path}[${index}]`, entries);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (isPlainObject(expected) && isPlainObject(actual)) {
|
|
78
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(expected), ...Object.keys(actual)]);
|
|
79
|
+
for (const key of [...keys].sort()) walk(expected[key], actual[key], `${path}.${key}`, entries);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const kind = typeof expected === typeof actual ? "changed" : "type-changed";
|
|
83
|
+
entries.push({ path, kind, expected: toDiffValue(expected), actual: toDiffValue(actual) });
|
|
84
|
+
}
|
|
85
|
+
function isPlainObject(value) {
|
|
86
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
87
|
+
}
|
|
88
|
+
function toDiffValue(value) {
|
|
89
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
90
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
|
|
91
|
+
if (Array.isArray(value)) return value.map(toDiffValue);
|
|
92
|
+
if (isPlainObject(value)) {
|
|
93
|
+
const result = {};
|
|
94
|
+
for (const [key, item] of Object.entries(value)) {
|
|
95
|
+
if (item !== void 0) result[key] = toDiffValue(item);
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
return String(value);
|
|
100
|
+
}
|
|
101
|
+
var CassetteDivergenceError = class extends Error {
|
|
102
|
+
report;
|
|
103
|
+
constructor(report) {
|
|
104
|
+
super(formatDivergence(report));
|
|
105
|
+
this.name = "CassetteDivergenceError";
|
|
106
|
+
this.report = report;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
function formatDivergence(report) {
|
|
110
|
+
const lines = [
|
|
111
|
+
`Agent cassette diverged at turn ${report.turn + 1} in "${report.cassette}".`,
|
|
112
|
+
`Expected fingerprint: ${report.expectedFingerprint}`,
|
|
113
|
+
`Actual fingerprint: ${report.actualFingerprint}`,
|
|
114
|
+
"",
|
|
115
|
+
"Structured request diff:"
|
|
116
|
+
];
|
|
117
|
+
if (report.diff.length === 0) {
|
|
118
|
+
lines.push(" Requests differ under the configured fingerprint, but their stored structures are equal.");
|
|
119
|
+
} else {
|
|
120
|
+
for (const item of report.diff.slice(0, 30)) {
|
|
121
|
+
const expected = item.expected === void 0 ? "<missing>" : stableStringify(item.expected);
|
|
122
|
+
const actual = item.actual === void 0 ? "<missing>" : stableStringify(item.actual);
|
|
123
|
+
lines.push(` ${item.path} (${item.kind})`, ` expected: ${expected}`, ` actual: ${actual}`);
|
|
124
|
+
}
|
|
125
|
+
if (report.diff.length > 30) lines.push(` \u2026and ${report.diff.length - 30} more differences.`);
|
|
126
|
+
}
|
|
127
|
+
lines.push("", "Fix the changed request, re-record this cassette intentionally, or provide a custom fingerprint when the change is non-behavioral.");
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/core/events.ts
|
|
132
|
+
function eventsForTurn(request, response) {
|
|
133
|
+
const events = [{ type: "messages.sent", messages: request.messages }];
|
|
134
|
+
for (const message of request.messages) {
|
|
135
|
+
if (message.role === "tool") events.push({ type: "tool.result.returned", message });
|
|
136
|
+
}
|
|
137
|
+
for (const call of response.toolCalls) events.push({ type: "tool.call.proposed", call });
|
|
138
|
+
if (response.content.length > 0) events.push({ type: "output.final", content: response.content });
|
|
139
|
+
return events;
|
|
140
|
+
}
|
|
141
|
+
var defaultFingerprint = (request) => {
|
|
142
|
+
const finalUser = [...request.messages].reverse().find((message) => message.role === "user");
|
|
143
|
+
const shape = request.messages.map((message) => ({
|
|
144
|
+
role: message.role,
|
|
145
|
+
content: message.content.map((part) => part.type),
|
|
146
|
+
...message.name === void 0 ? {} : { name: message.name },
|
|
147
|
+
...message.toolCallId === void 0 ? {} : { toolCallId: message.toolCallId }
|
|
148
|
+
}));
|
|
149
|
+
const tools = request.tools.map((tool) => ({
|
|
150
|
+
name: tool.name,
|
|
151
|
+
...tool.description === void 0 ? {} : { description: normalizeSemanticText(tool.description) },
|
|
152
|
+
inputSchema: tool.inputSchema
|
|
153
|
+
}));
|
|
154
|
+
const semanticFinalUserTurn = finalUser ? normalizeSemanticText(contentToText(finalUser.content)) : "";
|
|
155
|
+
const decisionHistory = request.messages.flatMap((message, index) => {
|
|
156
|
+
if (message.role === "system" || message === finalUser) return [];
|
|
157
|
+
return [{
|
|
158
|
+
index,
|
|
159
|
+
role: message.role,
|
|
160
|
+
content: normalizeSemanticText(contentToText(message.content)),
|
|
161
|
+
toolCalls: message.toolCalls ?? [],
|
|
162
|
+
...message.name === void 0 ? {} : { name: message.name },
|
|
163
|
+
...message.toolCallId === void 0 ? {} : { toolCallId: message.toolCallId }
|
|
164
|
+
}];
|
|
165
|
+
});
|
|
166
|
+
const canonical = stableStringify({ tools, shape, decisionHistory, semanticFinalUserTurn });
|
|
167
|
+
return `sha256:${crypto.createHash("sha256").update(canonical).digest("hex")}`;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// src/core/redaction.ts
|
|
171
|
+
var SECRET_KEY = /(?:api[-_]?key|authorization|password|passwd|secret|access[-_]?token|refresh[-_]?token|cookie)/iu;
|
|
172
|
+
var BEARER_TOKEN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/giu;
|
|
173
|
+
var EMAIL = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/giu;
|
|
174
|
+
var PHONE_CANDIDATE = /(?<!\d)(?:\+?\d[\d ().-]{7,}\d)(?!\d)/gu;
|
|
175
|
+
var defaultRedactionRules = [
|
|
176
|
+
{
|
|
177
|
+
name: "secret-key",
|
|
178
|
+
match: ({ key }) => key !== void 0 && SECRET_KEY.test(key)
|
|
179
|
+
}
|
|
180
|
+
];
|
|
181
|
+
function redact(value, options = {}) {
|
|
182
|
+
const rules = [
|
|
183
|
+
...options.includeDefaults === false ? [] : defaultRedactionRules,
|
|
184
|
+
...options.rules ?? []
|
|
185
|
+
];
|
|
186
|
+
return visit(value, "$", void 0, rules, options.includeDefaults !== false);
|
|
187
|
+
}
|
|
188
|
+
function visit(value, path, key, rules, redactDefaultStrings) {
|
|
189
|
+
const json = asJsonValue(value);
|
|
190
|
+
const context = key === void 0 ? { path, value: json } : { path, key, value: json };
|
|
191
|
+
const rule = rules.find((candidate) => {
|
|
192
|
+
try {
|
|
193
|
+
return candidate.match(context);
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
if (rule) return rule.replacement ?? "[REDACTED]";
|
|
199
|
+
if (typeof value === "string" && redactDefaultStrings) {
|
|
200
|
+
return value.replace(BEARER_TOKEN, "Bearer [REDACTED]").replace(EMAIL, "[REDACTED_EMAIL]").replace(PHONE_CANDIDATE, (candidate) => countDigits(candidate) >= 10 ? "[REDACTED_PHONE]" : candidate);
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(value)) {
|
|
203
|
+
return value.map((item, index) => visit(item, `${path}[${index}]`, void 0, rules, redactDefaultStrings));
|
|
204
|
+
}
|
|
205
|
+
if (isRecord(value)) {
|
|
206
|
+
const result = {};
|
|
207
|
+
for (const [childKey, item] of Object.entries(value)) {
|
|
208
|
+
result[childKey] = visit(item, `${path}.${childKey}`, childKey, rules, redactDefaultStrings);
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
function countDigits(value) {
|
|
215
|
+
return value.replace(/\D/gu, "").length;
|
|
216
|
+
}
|
|
217
|
+
var FileCassetteStore = class {
|
|
218
|
+
directory;
|
|
219
|
+
constructor(directory = ".agentcassette/cassettes") {
|
|
220
|
+
this.directory = path.resolve(directory);
|
|
221
|
+
}
|
|
222
|
+
async exists(name) {
|
|
223
|
+
try {
|
|
224
|
+
await promises.stat(this.pathFor(name));
|
|
225
|
+
return true;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (hasCode(error, "ENOENT")) return false;
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async read(name) {
|
|
232
|
+
const source = await promises.readFile(this.pathFor(name), "utf8");
|
|
233
|
+
return parseCassette(source, name);
|
|
234
|
+
}
|
|
235
|
+
async write(cassette) {
|
|
236
|
+
const destination = this.pathFor(cassette.name);
|
|
237
|
+
await promises.mkdir(path.dirname(destination), { recursive: true });
|
|
238
|
+
const temporary = `${destination}.${process.pid}.${Date.now()}.tmp`;
|
|
239
|
+
await promises.writeFile(temporary, `${JSON.stringify(cassette, null, 2)}
|
|
240
|
+
`, { encoding: "utf8", mode: 384 });
|
|
241
|
+
await promises.rename(temporary, destination);
|
|
242
|
+
}
|
|
243
|
+
async list() {
|
|
244
|
+
try {
|
|
245
|
+
const entries = await promises.readdir(this.directory, { withFileTypes: true });
|
|
246
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -5)).filter(isValidCassetteName).sort();
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (hasCode(error, "ENOENT")) return [];
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async remove(name) {
|
|
253
|
+
await promises.rm(this.pathFor(name), { force: true });
|
|
254
|
+
}
|
|
255
|
+
pathFor(name) {
|
|
256
|
+
if (!isValidCassetteName(name)) {
|
|
257
|
+
throw new Error("Cassette names must start with a lowercase letter or digit and contain only lowercase letters, digits, dots, underscores, or hyphens.");
|
|
258
|
+
}
|
|
259
|
+
return path.join(this.directory, `${name}.json`);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
function parseCassette(source, expectedName) {
|
|
263
|
+
const value = JSON.parse(source);
|
|
264
|
+
if (!isCassette(value)) throw new Error(`Invalid agentcassette file${expectedName ? `: ${expectedName}` : ""}.`);
|
|
265
|
+
if (expectedName !== void 0 && value.name !== expectedName) {
|
|
266
|
+
throw new Error(`Cassette identity mismatch: expected "${expectedName}", file contains "${value.name}".`);
|
|
267
|
+
}
|
|
268
|
+
return value;
|
|
269
|
+
}
|
|
270
|
+
function isValidCassetteName(name) {
|
|
271
|
+
return /^[a-z0-9][a-z0-9._-]*$/u.test(name);
|
|
272
|
+
}
|
|
273
|
+
function isCassette(value) {
|
|
274
|
+
if (!isObject(value)) return false;
|
|
275
|
+
return value.schemaVersion === 1 && typeof value.name === "string" && isValidCassetteName(value.name) && typeof value.createdAt === "string" && typeof value.updatedAt === "string" && (value.metadata === void 0 || isMetadata(value.metadata)) && Array.isArray(value.turns) && value.turns.every((turn, index) => isTurn(turn, index));
|
|
276
|
+
}
|
|
277
|
+
function isTurn(value, index) {
|
|
278
|
+
return isObject(value) && value.index === index && typeof value.fingerprint === "string" && isRequest(value.request) && Array.isArray(value.events) && value.events.every(isEvent) && isResponse(value.response);
|
|
279
|
+
}
|
|
280
|
+
function isMetadata(value) {
|
|
281
|
+
return isObject(value) && (value.provider === void 0 || typeof value.provider === "string") && (value.testFile === void 0 || typeof value.testFile === "string") && (value.tags === void 0 || Array.isArray(value.tags) && value.tags.every((tag) => typeof tag === "string")) && (value.fingerprint === void 0 || value.fingerprint === "default" || value.fingerprint === "custom");
|
|
282
|
+
}
|
|
283
|
+
function isEvent(value) {
|
|
284
|
+
if (!isObject(value)) return false;
|
|
285
|
+
if (value.type === "messages.sent") return Array.isArray(value.messages) && value.messages.every(isMessage);
|
|
286
|
+
if (value.type === "tool.result.returned") return isMessage(value.message);
|
|
287
|
+
if (value.type === "tool.call.proposed") return isToolCall(value.call);
|
|
288
|
+
return value.type === "output.final" && Array.isArray(value.content) && value.content.every(isContentPart);
|
|
289
|
+
}
|
|
290
|
+
function isRequest(value) {
|
|
291
|
+
return isObject(value) && Array.isArray(value.messages) && value.messages.every(isMessage) && Array.isArray(value.tools) && value.tools.every((tool) => isObject(tool) && typeof tool.name === "string" && isJsonValue(tool.inputSchema)) && (value.model === void 0 || typeof value.model === "string");
|
|
292
|
+
}
|
|
293
|
+
function isResponse(value) {
|
|
294
|
+
return isObject(value) && Array.isArray(value.content) && value.content.every(isContentPart) && Array.isArray(value.toolCalls) && value.toolCalls.every(isToolCall) && (value.sequence === void 0 || Array.isArray(value.sequence) && value.sequence.every(isResponseItem)) && (value.id === void 0 || typeof value.id === "string") && (value.model === void 0 || typeof value.model === "string") && (value.finishReason === void 0 || typeof value.finishReason === "string") && (value.usage === void 0 || isObject(value.usage) && typeof value.usage.inputTokens === "number" && typeof value.usage.outputTokens === "number" && typeof value.usage.totalTokens === "number" && (value.usage.costUsd === void 0 || typeof value.usage.costUsd === "number"));
|
|
295
|
+
}
|
|
296
|
+
function isMessage(value) {
|
|
297
|
+
return isObject(value) && (value.role === "system" || value.role === "user" || value.role === "assistant" || value.role === "tool") && Array.isArray(value.content) && value.content.every(isContentPart) && (value.name === void 0 || typeof value.name === "string") && (value.toolCallId === void 0 || typeof value.toolCallId === "string") && (value.toolCalls === void 0 || Array.isArray(value.toolCalls) && value.toolCalls.every(isToolCall));
|
|
298
|
+
}
|
|
299
|
+
function isContentPart(value) {
|
|
300
|
+
if (!isObject(value)) return false;
|
|
301
|
+
if (value.type === "text") return typeof value.text === "string";
|
|
302
|
+
if (value.type === "image") return typeof value.source === "string";
|
|
303
|
+
return value.type === "json" && isJsonValue(value.value);
|
|
304
|
+
}
|
|
305
|
+
function isResponseItem(value) {
|
|
306
|
+
return isObject(value) && (value.type === "content" && isContentPart(value.part) || value.type === "toolCall" && isToolCall(value.call));
|
|
307
|
+
}
|
|
308
|
+
function isToolCall(value) {
|
|
309
|
+
return isObject(value) && typeof value.name === "string" && isJsonValue(value.arguments) && (value.id === void 0 || typeof value.id === "string");
|
|
310
|
+
}
|
|
311
|
+
function isJsonValue(value) {
|
|
312
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
313
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
314
|
+
if (Array.isArray(value)) return value.every(isJsonValue);
|
|
315
|
+
return isObject(value) && Object.values(value).every(isJsonValue);
|
|
316
|
+
}
|
|
317
|
+
function isObject(value) {
|
|
318
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
319
|
+
}
|
|
320
|
+
function hasCode(error, code) {
|
|
321
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/core/client.ts
|
|
325
|
+
async function createCassetteClient(options) {
|
|
326
|
+
validateOptions(options);
|
|
327
|
+
const store = options.store ?? new FileCassetteStore();
|
|
328
|
+
const requestedMode = options.mode ?? "auto";
|
|
329
|
+
const mode = requestedMode === "auto" ? await store.exists(options.name) ? "replay" : "record" : requestedMode;
|
|
330
|
+
const fingerprint = options.fingerprint ?? defaultFingerprint;
|
|
331
|
+
const replayTurns = options.replayTurns ?? Number.POSITIVE_INFINITY;
|
|
332
|
+
let cassette = mode === "replay" ? await store.read(options.name) : newCassette(options.name, options.adapter.provider, options.metadata, options.fingerprint === void 0);
|
|
333
|
+
let position = 0;
|
|
334
|
+
let queue = Promise.resolve();
|
|
335
|
+
const invoke = async (input) => {
|
|
336
|
+
const normalizedRequest = redacted(options.adapter.normalizeRequest(input), options.redaction);
|
|
337
|
+
const actualFingerprint = fingerprint(normalizedRequest);
|
|
338
|
+
const shouldReplay = mode === "replay" && position < replayTurns;
|
|
339
|
+
if (shouldReplay) {
|
|
340
|
+
const expected = cassette.turns[position];
|
|
341
|
+
if (!expected) {
|
|
342
|
+
throw new CassetteDivergenceError({
|
|
343
|
+
cassette: cassette.name,
|
|
344
|
+
turn: position,
|
|
345
|
+
expectedFingerprint: "<end-of-cassette>",
|
|
346
|
+
actualFingerprint,
|
|
347
|
+
expected: emptyRequest(),
|
|
348
|
+
actual: normalizedRequest,
|
|
349
|
+
diff: structuredDiff(emptyRequest(), normalizedRequest)
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (expected.fingerprint !== actualFingerprint) {
|
|
353
|
+
throw new CassetteDivergenceError({
|
|
354
|
+
cassette: cassette.name,
|
|
355
|
+
turn: position,
|
|
356
|
+
expectedFingerprint: expected.fingerprint,
|
|
357
|
+
actualFingerprint,
|
|
358
|
+
expected: expected.request,
|
|
359
|
+
actual: normalizedRequest,
|
|
360
|
+
diff: structuredDiff(expected.request, normalizedRequest)
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
position += 1;
|
|
364
|
+
if (options.onReplay) {
|
|
365
|
+
await options.onReplay({ cassette: cassette.name, turn: expected.index, response: structuredClone(expected.response) });
|
|
366
|
+
}
|
|
367
|
+
return options.adapter.replayResponse(structuredClone(expected.response));
|
|
368
|
+
}
|
|
369
|
+
const client = options.client;
|
|
370
|
+
if (!client) {
|
|
371
|
+
const reason = mode === "record" ? "record mode" : `partial replay after turn ${replayTurns}`;
|
|
372
|
+
throw new Error(`A real client boundary is required for ${reason}.`);
|
|
373
|
+
}
|
|
374
|
+
const liveResponse = await client(input);
|
|
375
|
+
const normalizedResponse = withCalculatedCost(
|
|
376
|
+
redacted(options.adapter.normalizeResponse(liveResponse), options.redaction),
|
|
377
|
+
normalizedRequest,
|
|
378
|
+
options.calculateCost
|
|
379
|
+
);
|
|
380
|
+
if (mode === "record") {
|
|
381
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
382
|
+
cassette.turns.push({
|
|
383
|
+
index: position,
|
|
384
|
+
fingerprint: actualFingerprint,
|
|
385
|
+
request: normalizedRequest,
|
|
386
|
+
events: eventsForTurn(normalizedRequest, normalizedResponse),
|
|
387
|
+
response: normalizedResponse
|
|
388
|
+
});
|
|
389
|
+
cassette.updatedAt = now;
|
|
390
|
+
await store.write(cassette);
|
|
391
|
+
}
|
|
392
|
+
position += 1;
|
|
393
|
+
return liveResponse;
|
|
394
|
+
};
|
|
395
|
+
const wrapped = ((input) => {
|
|
396
|
+
const result = queue.then(() => invoke(input));
|
|
397
|
+
queue = result.then(() => void 0, () => void 0);
|
|
398
|
+
return result;
|
|
399
|
+
});
|
|
400
|
+
Object.defineProperties(wrapped, {
|
|
401
|
+
cassetteName: { value: options.name, enumerable: true },
|
|
402
|
+
mode: { value: mode, enumerable: true },
|
|
403
|
+
turn: { get: () => position, enumerable: true }
|
|
404
|
+
});
|
|
405
|
+
return wrapped;
|
|
406
|
+
}
|
|
407
|
+
function validateOptions(options) {
|
|
408
|
+
if (!options.name.trim()) throw new Error("Cassette name cannot be empty.");
|
|
409
|
+
if (options.replayTurns !== void 0 && (!Number.isInteger(options.replayTurns) || options.replayTurns < 0)) {
|
|
410
|
+
throw new Error("replayTurns must be a non-negative integer.");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function redacted(value, options) {
|
|
414
|
+
return redact(value, options ?? {});
|
|
415
|
+
}
|
|
416
|
+
function withCalculatedCost(response, request, calculator) {
|
|
417
|
+
if (!calculator) return response;
|
|
418
|
+
const cost = calculator({ request, response });
|
|
419
|
+
if (cost === void 0) return response;
|
|
420
|
+
if (!Number.isFinite(cost) || cost < 0) throw new Error("calculateCost must return a non-negative finite number or undefined.");
|
|
421
|
+
const usage = response.usage ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
422
|
+
return { ...response, usage: { ...usage, costUsd: cost } };
|
|
423
|
+
}
|
|
424
|
+
function newCassette(name, provider, metadata, usesDefaultFingerprint) {
|
|
425
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
426
|
+
const mergedMetadata = {
|
|
427
|
+
...metadata,
|
|
428
|
+
provider,
|
|
429
|
+
fingerprint: usesDefaultFingerprint ? "default" : "custom"
|
|
430
|
+
};
|
|
431
|
+
return {
|
|
432
|
+
schemaVersion: 1,
|
|
433
|
+
name,
|
|
434
|
+
createdAt: now,
|
|
435
|
+
updatedAt: now,
|
|
436
|
+
turns: [],
|
|
437
|
+
metadata: mergedMetadata
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function emptyRequest() {
|
|
441
|
+
return { messages: [], tools: [] };
|
|
442
|
+
}
|
|
443
|
+
var defaultUsageFile = ".agentcassette/usage.ndjson";
|
|
444
|
+
async function appendUsageEvent(event, file = defaultUsageFile) {
|
|
445
|
+
await promises.mkdir(path.dirname(file), { recursive: true });
|
|
446
|
+
await promises.appendFile(file, `${JSON.stringify(event)}
|
|
447
|
+
`, { encoding: "utf8", mode: 384 });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/adapters/testing.ts
|
|
451
|
+
async function withTestCassette(runner, options) {
|
|
452
|
+
const directory = options.directory ?? process.env.AGENTCASSETTE_DIR ?? ".agentcassette/cassettes";
|
|
453
|
+
const usageFile = options.usageFile ?? process.env.AGENTCASSETTE_USAGE_FILE ?? path.join(path.dirname(directory), "usage.ndjson");
|
|
454
|
+
const testFile = options.testFile;
|
|
455
|
+
const mode = environmentMode() ?? options.mode ?? "auto";
|
|
456
|
+
const metadata = {
|
|
457
|
+
...options.metadata,
|
|
458
|
+
...testFile === void 0 ? {} : { testFile }
|
|
459
|
+
};
|
|
460
|
+
const userOnReplay = options.onReplay;
|
|
461
|
+
const wrapped = await createCassetteClient({
|
|
462
|
+
...options,
|
|
463
|
+
mode,
|
|
464
|
+
metadata,
|
|
465
|
+
store: options.store ?? new FileCassetteStore(directory),
|
|
466
|
+
onReplay: async (context) => {
|
|
467
|
+
await appendUsageEvent({
|
|
468
|
+
type: "replay",
|
|
469
|
+
cassette: context.cassette,
|
|
470
|
+
turn: context.turn,
|
|
471
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
472
|
+
...context.response.usage?.costUsd === void 0 ? {} : { costSavedUsd: context.response.usage.costUsd }
|
|
473
|
+
}, usageFile);
|
|
474
|
+
await userOnReplay?.(context);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
await appendUsageEvent({
|
|
478
|
+
type: "use",
|
|
479
|
+
cassette: options.name,
|
|
480
|
+
runner,
|
|
481
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
482
|
+
...testFile === void 0 ? {} : { testFile }
|
|
483
|
+
}, usageFile);
|
|
484
|
+
return wrapped;
|
|
485
|
+
}
|
|
486
|
+
function environmentMode() {
|
|
487
|
+
const value = process.env.AGENTCASSETTE_MODE;
|
|
488
|
+
if (value === void 0 || value === "") return void 0;
|
|
489
|
+
if (value === "record" || value === "replay" || value === "auto") return value;
|
|
490
|
+
throw new Error(`Invalid AGENTCASSETTE_MODE "${value}". Expected record, replay, or auto.`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/adapters/vitest.ts
|
|
494
|
+
function withVitestCassette(options) {
|
|
495
|
+
return withTestCassette("vitest", options);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
exports.withVitestCassette = withVitestCassette;
|
|
499
|
+
//# sourceMappingURL=vitest.cjs.map
|
|
500
|
+
//# sourceMappingURL=vitest.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/canonical.ts","../src/core/diff.ts","../src/core/events.ts","../src/core/fingerprint.ts","../src/core/redaction.ts","../src/core/storage.ts","../src/core/client.ts","../src/adapters/usage.ts","../src/adapters/testing.ts","../src/adapters/vitest.ts"],"names":["createHash","resolve","stat","readFile","mkdir","dirname","writeFile","rename","readdir","rm","join","appendFile"],"mappings":"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","file":"vitest.cjs","sourcesContent":["import type { ContentPart, JsonValue, MessageRole, SemanticMessage } from \"./types.js\";\n\nconst ISO_TIMESTAMP = /\\b\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,9})?)?(?:Z|[+-]\\d{2}:?\\d{2})\\b/giu;\nconst UNIX_TIMESTAMP = /\\b1[5-9]\\d{8}(?:\\d{3})?\\b/gu;\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function asJsonValue(value: unknown): JsonValue {\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") return value;\n if (typeof value === \"number\") return Number.isFinite(value) ? value : String(value);\n if (Array.isArray(value)) return value.map(asJsonValue);\n if (isRecord(value)) {\n const result: { [key: string]: JsonValue } = {};\n for (const [key, item] of Object.entries(value)) {\n if (item !== undefined) result[key] = asJsonValue(item);\n }\n return result;\n }\n return String(value);\n}\n\nexport function stableStringify(value: unknown): string {\n return JSON.stringify(sortValue(asJsonValue(value)));\n}\n\nfunction sortValue(value: JsonValue): JsonValue {\n if (Array.isArray(value)) return value.map(sortValue);\n if (isRecord(value)) {\n const result: { [key: string]: JsonValue } = {};\n for (const key of Object.keys(value).sort()) {\n const item = value[key];\n if (item !== undefined) result[key] = sortValue(item as JsonValue);\n }\n return result;\n }\n return value;\n}\n\nexport function normalizeSemanticText(text: string): string {\n return text\n .normalize(\"NFKC\")\n .replace(ISO_TIMESTAMP, \"<timestamp>\")\n .replace(UNIX_TIMESTAMP, \"<timestamp>\")\n .replace(/\\s+/gu, \" \")\n .trim();\n}\n\nexport function textPart(text: string): ContentPart {\n return { type: \"text\", text };\n}\n\nexport function normalizeRole(value: unknown): MessageRole {\n if (value === \"system\" || value === \"assistant\" || value === \"tool\") return value;\n if (value === \"model\") return \"assistant\";\n if (value === \"function\") return \"tool\";\n return \"user\";\n}\n\nexport function contentToText(content: ContentPart[]): string {\n return content\n .map((part) => {\n if (part.type === \"text\") return part.text;\n if (part.type === \"json\") return stableStringify(part.value);\n return `[image:${part.source}]`;\n })\n .join(\"\\n\");\n}\n\nexport function unknownToContent(value: unknown): ContentPart[] {\n if (typeof value === \"string\") return [textPart(value)];\n if (value === null || value === undefined) return [];\n if (Array.isArray(value)) {\n return value.flatMap((part) => {\n if (typeof part === \"string\") return [textPart(part)];\n if (!isRecord(part)) return [{ type: \"json\", value: asJsonValue(part) } satisfies ContentPart];\n if (typeof part.text === \"string\") return [textPart(part.text)];\n if (part.type === \"text\" && typeof part.text === \"string\") return [textPart(part.text)];\n if (part.type === \"image_url\" && isRecord(part.image_url) && typeof part.image_url.url === \"string\") {\n return [{ type: \"image\", source: part.image_url.url } satisfies ContentPart];\n }\n return [{ type: \"json\", value: asJsonValue(part) } satisfies ContentPart];\n });\n }\n return [{ type: \"json\", value: asJsonValue(value) }];\n}\n\nexport function messageFromUnknown(value: unknown): SemanticMessage {\n if (!isRecord(value)) return { role: \"user\", content: unknownToContent(value) };\n const message: SemanticMessage = {\n role: normalizeRole(value.role),\n content: unknownToContent(value.content),\n };\n if (typeof value.name === \"string\") message.name = value.name;\n if (typeof value.tool_call_id === \"string\") message.toolCallId = value.tool_call_id;\n if (typeof value.toolCallId === \"string\") message.toolCallId = value.toolCallId;\n return message;\n}\n","import { stableStringify } from \"./canonical.js\";\nimport type { JsonValue, SemanticRequest } from \"./types.js\";\n\nexport type DiffKind = \"added\" | \"removed\" | \"changed\" | \"type-changed\";\n\nexport interface DiffEntry {\n path: string;\n kind: DiffKind;\n expected?: JsonValue;\n actual?: JsonValue;\n}\n\nexport function structuredDiff(expected: unknown, actual: unknown): DiffEntry[] {\n const entries: DiffEntry[] = [];\n walk(expected, actual, \"$\", entries);\n return entries;\n}\n\nfunction walk(expected: unknown, actual: unknown, path: string, entries: DiffEntry[]): void {\n if (Object.is(expected, actual)) return;\n if (expected === undefined) {\n entries.push({ path, kind: \"added\", actual: toDiffValue(actual) });\n return;\n }\n if (actual === undefined) {\n entries.push({ path, kind: \"removed\", expected: toDiffValue(expected) });\n return;\n }\n if (Array.isArray(expected) && Array.isArray(actual)) {\n const length = Math.max(expected.length, actual.length);\n for (let index = 0; index < length; index += 1) {\n walk(expected[index], actual[index], `${path}[${index}]`, entries);\n }\n return;\n }\n if (isPlainObject(expected) && isPlainObject(actual)) {\n const keys = new Set([...Object.keys(expected), ...Object.keys(actual)]);\n for (const key of [...keys].sort()) walk(expected[key], actual[key], `${path}.${key}`, entries);\n return;\n }\n const kind: DiffKind = typeof expected === typeof actual ? \"changed\" : \"type-changed\";\n entries.push({ path, kind, expected: toDiffValue(expected), actual: toDiffValue(actual) });\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction toDiffValue(value: unknown): JsonValue {\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") return value;\n if (typeof value === \"number\") return Number.isFinite(value) ? value : String(value);\n if (Array.isArray(value)) return value.map(toDiffValue);\n if (isPlainObject(value)) {\n const result: { [key: string]: JsonValue } = {};\n for (const [key, item] of Object.entries(value)) {\n if (item !== undefined) result[key] = toDiffValue(item);\n }\n return result;\n }\n return String(value);\n}\n\nexport interface DivergenceReport {\n cassette: string;\n turn: number;\n expectedFingerprint: string;\n actualFingerprint: string;\n expected: SemanticRequest;\n actual: SemanticRequest;\n diff: DiffEntry[];\n}\n\nexport class CassetteDivergenceError extends Error {\n readonly report: DivergenceReport;\n\n constructor(report: DivergenceReport) {\n super(formatDivergence(report));\n this.name = \"CassetteDivergenceError\";\n this.report = report;\n }\n}\n\nfunction formatDivergence(report: DivergenceReport): string {\n const lines = [\n `Agent cassette diverged at turn ${report.turn + 1} in \"${report.cassette}\".`,\n `Expected fingerprint: ${report.expectedFingerprint}`,\n `Actual fingerprint: ${report.actualFingerprint}`,\n \"\",\n \"Structured request diff:\",\n ];\n if (report.diff.length === 0) {\n lines.push(\" Requests differ under the configured fingerprint, but their stored structures are equal.\");\n } else {\n for (const item of report.diff.slice(0, 30)) {\n const expected = item.expected === undefined ? \"<missing>\" : stableStringify(item.expected);\n const actual = item.actual === undefined ? \"<missing>\" : stableStringify(item.actual);\n lines.push(` ${item.path} (${item.kind})`, ` expected: ${expected}`, ` actual: ${actual}`);\n }\n if (report.diff.length > 30) lines.push(` …and ${report.diff.length - 30} more differences.`);\n }\n lines.push(\"\", \"Fix the changed request, re-record this cassette intentionally, or provide a custom fingerprint when the change is non-behavioral.\");\n return lines.join(\"\\n\");\n}\n","import type { SemanticEvent, SemanticRequest, SemanticResponse } from \"./types.js\";\n\nexport function eventsForTurn(request: SemanticRequest, response: SemanticResponse): SemanticEvent[] {\n const events: SemanticEvent[] = [{ type: \"messages.sent\", messages: request.messages }];\n for (const message of request.messages) {\n if (message.role === \"tool\") events.push({ type: \"tool.result.returned\", message });\n }\n for (const call of response.toolCalls) events.push({ type: \"tool.call.proposed\", call });\n if (response.content.length > 0) events.push({ type: \"output.final\", content: response.content });\n return events;\n}\n","import { createHash } from \"node:crypto\";\nimport { contentToText, normalizeSemanticText, stableStringify } from \"./canonical.js\";\nimport type { FingerprintFunction, SemanticRequest } from \"./types.js\";\n\nexport const defaultFingerprint: FingerprintFunction = (request) => {\n const finalUser = [...request.messages].reverse().find((message) => message.role === \"user\");\n const shape = request.messages.map((message) => ({\n role: message.role,\n content: message.content.map((part) => part.type),\n ...(message.name === undefined ? {} : { name: message.name }),\n ...(message.toolCallId === undefined ? {} : { toolCallId: message.toolCallId }),\n }));\n const tools = request.tools.map((tool) => ({\n name: tool.name,\n ...(tool.description === undefined ? {} : { description: normalizeSemanticText(tool.description) }),\n inputSchema: tool.inputSchema,\n }));\n const semanticFinalUserTurn = finalUser\n ? normalizeSemanticText(contentToText(finalUser.content))\n : \"\";\n const decisionHistory = request.messages.flatMap((message, index) => {\n if (message.role === \"system\" || message === finalUser) return [];\n return [{\n index,\n role: message.role,\n content: normalizeSemanticText(contentToText(message.content)),\n toolCalls: message.toolCalls ?? [],\n ...(message.name === undefined ? {} : { name: message.name }),\n ...(message.toolCallId === undefined ? {} : { toolCallId: message.toolCallId }),\n }];\n });\n const canonical = stableStringify({ tools, shape, decisionHistory, semanticFinalUserTurn });\n return `sha256:${createHash(\"sha256\").update(canonical).digest(\"hex\")}`;\n};\n\nexport function explainDefaultFingerprint(request: SemanticRequest): {\n conversationRoles: string[];\n finalUserTurn: string;\n toolNames: string[];\n} {\n const finalUser = [...request.messages].reverse().find((message) => message.role === \"user\");\n return {\n conversationRoles: request.messages.map((message) => message.role),\n finalUserTurn: finalUser ? normalizeSemanticText(contentToText(finalUser.content)) : \"\",\n toolNames: request.tools.map((tool) => tool.name),\n };\n}\n","import { asJsonValue, isRecord } from \"./canonical.js\";\nimport type { JsonValue } from \"./types.js\";\n\nexport interface RedactionContext {\n path: string;\n key?: string;\n value: JsonValue;\n}\n\nexport interface RedactionRule {\n name: string;\n match(context: RedactionContext): boolean;\n replacement?: JsonValue;\n}\n\nexport interface RedactionOptions {\n rules?: RedactionRule[];\n includeDefaults?: boolean;\n}\n\nconst SECRET_KEY = /(?:api[-_]?key|authorization|password|passwd|secret|access[-_]?token|refresh[-_]?token|cookie)/iu;\nconst BEARER_TOKEN = /\\bBearer\\s+[A-Za-z0-9._~+/=-]+/giu;\nconst EMAIL = /\\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}\\b/giu;\nconst PHONE_CANDIDATE = /(?<!\\d)(?:\\+?\\d[\\d ().-]{7,}\\d)(?!\\d)/gu;\n\nexport const defaultRedactionRules: RedactionRule[] = [\n {\n name: \"secret-key\",\n match: ({ key }) => key !== undefined && SECRET_KEY.test(key),\n },\n];\n\nexport function redact<T>(value: T, options: RedactionOptions = {}): T {\n const rules = [\n ...(options.includeDefaults === false ? [] : defaultRedactionRules),\n ...(options.rules ?? []),\n ];\n return visit(value, \"$\", undefined, rules, options.includeDefaults !== false) as T;\n}\n\nfunction visit(\n value: unknown,\n path: string,\n key: string | undefined,\n rules: RedactionRule[],\n redactDefaultStrings: boolean,\n): unknown {\n const json = asJsonValue(value);\n const context: RedactionContext = key === undefined ? { path, value: json } : { path, key, value: json };\n const rule = rules.find((candidate) => {\n try {\n return candidate.match(context);\n } catch {\n return false;\n }\n });\n if (rule) return rule.replacement ?? \"[REDACTED]\";\n\n if (typeof value === \"string\" && redactDefaultStrings) {\n return value\n .replace(BEARER_TOKEN, \"Bearer [REDACTED]\")\n .replace(EMAIL, \"[REDACTED_EMAIL]\")\n .replace(PHONE_CANDIDATE, (candidate) => countDigits(candidate) >= 10 ? \"[REDACTED_PHONE]\" : candidate);\n }\n if (Array.isArray(value)) {\n return value.map((item, index) => visit(item, `${path}[${index}]`, undefined, rules, redactDefaultStrings));\n }\n if (isRecord(value)) {\n const result: Record<string, unknown> = {};\n for (const [childKey, item] of Object.entries(value)) {\n result[childKey] = visit(item, `${path}.${childKey}`, childKey, rules, redactDefaultStrings);\n }\n return result;\n }\n return value;\n}\n\nfunction countDigits(value: string): number {\n return value.replace(/\\D/gu, \"\").length;\n}\n","import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport type { Cassette, ContentPart, JsonValue, SemanticMessage, SemanticRequest, SemanticResponse, ToolCall } from \"./types.js\";\n\nexport interface CassetteStore {\n exists(name: string): Promise<boolean>;\n read(name: string): Promise<Cassette>;\n write(cassette: Cassette): Promise<void>;\n list(): Promise<string[]>;\n remove(name: string): Promise<void>;\n}\n\nexport class FileCassetteStore implements CassetteStore {\n readonly directory: string;\n\n constructor(directory = \".agentcassette/cassettes\") {\n this.directory = resolve(directory);\n }\n\n async exists(name: string): Promise<boolean> {\n try {\n await stat(this.pathFor(name));\n return true;\n } catch (error) {\n if (hasCode(error, \"ENOENT\")) return false;\n throw error;\n }\n }\n\n async read(name: string): Promise<Cassette> {\n const source = await readFile(this.pathFor(name), \"utf8\");\n return parseCassette(source, name);\n }\n\n async write(cassette: Cassette): Promise<void> {\n const destination = this.pathFor(cassette.name);\n await mkdir(dirname(destination), { recursive: true });\n const temporary = `${destination}.${process.pid}.${Date.now()}.tmp`;\n await writeFile(temporary, `${JSON.stringify(cassette, null, 2)}\\n`, { encoding: \"utf8\", mode: 0o600 });\n await rename(temporary, destination);\n }\n\n async list(): Promise<string[]> {\n try {\n const entries = await readdir(this.directory, { withFileTypes: true });\n return entries\n .filter((entry) => entry.isFile() && entry.name.endsWith(\".json\"))\n .map((entry) => entry.name.slice(0, -5))\n .filter(isValidCassetteName)\n .sort();\n } catch (error) {\n if (hasCode(error, \"ENOENT\")) return [];\n throw error;\n }\n }\n\n async remove(name: string): Promise<void> {\n await rm(this.pathFor(name), { force: true });\n }\n\n pathFor(name: string): string {\n if (!isValidCassetteName(name)) {\n throw new Error(\"Cassette names must start with a lowercase letter or digit and contain only lowercase letters, digits, dots, underscores, or hyphens.\");\n }\n return join(this.directory, `${name}.json`);\n }\n}\n\nexport class MemoryCassetteStore implements CassetteStore {\n readonly cassettes = new Map<string, Cassette>();\n\n async exists(name: string): Promise<boolean> {\n return this.cassettes.has(name);\n }\n\n async read(name: string): Promise<Cassette> {\n const cassette = this.cassettes.get(name);\n if (!cassette) throw new Error(`Cassette \"${name}\" does not exist.`);\n return structuredClone(cassette);\n }\n\n async write(cassette: Cassette): Promise<void> {\n this.cassettes.set(cassette.name, structuredClone(cassette));\n }\n\n async list(): Promise<string[]> {\n return [...this.cassettes.keys()].sort();\n }\n\n async remove(name: string): Promise<void> {\n this.cassettes.delete(name);\n }\n}\n\nexport function parseCassette(source: string, expectedName?: string): Cassette {\n const value: unknown = JSON.parse(source);\n if (!isCassette(value)) throw new Error(`Invalid agentcassette file${expectedName ? `: ${expectedName}` : \"\"}.`);\n if (expectedName !== undefined && value.name !== expectedName) {\n throw new Error(`Cassette identity mismatch: expected \"${expectedName}\", file contains \"${value.name}\".`);\n }\n return value;\n}\n\nfunction isValidCassetteName(name: string): boolean {\n return /^[a-z0-9][a-z0-9._-]*$/u.test(name);\n}\n\nfunction isCassette(value: unknown): value is Cassette {\n if (!isObject(value)) return false;\n return value.schemaVersion === 1\n && typeof value.name === \"string\"\n && isValidCassetteName(value.name)\n && typeof value.createdAt === \"string\"\n && typeof value.updatedAt === \"string\"\n && (value.metadata === undefined || isMetadata(value.metadata))\n && Array.isArray(value.turns)\n && value.turns.every((turn, index) => isTurn(turn, index));\n}\n\nfunction isTurn(value: unknown, index: number): boolean {\n return isObject(value)\n && value.index === index\n && typeof value.fingerprint === \"string\"\n && isRequest(value.request)\n && Array.isArray(value.events)\n && value.events.every(isEvent)\n && isResponse(value.response);\n}\n\nfunction isMetadata(value: unknown): boolean {\n return isObject(value)\n && (value.provider === undefined || typeof value.provider === \"string\")\n && (value.testFile === undefined || typeof value.testFile === \"string\")\n && (value.tags === undefined || (Array.isArray(value.tags) && value.tags.every((tag) => typeof tag === \"string\")))\n && (value.fingerprint === undefined || value.fingerprint === \"default\" || value.fingerprint === \"custom\");\n}\n\nfunction isEvent(value: unknown): boolean {\n if (!isObject(value)) return false;\n if (value.type === \"messages.sent\") return Array.isArray(value.messages) && value.messages.every(isMessage);\n if (value.type === \"tool.result.returned\") return isMessage(value.message);\n if (value.type === \"tool.call.proposed\") return isToolCall(value.call);\n return value.type === \"output.final\" && Array.isArray(value.content) && value.content.every(isContentPart);\n}\n\nfunction isRequest(value: unknown): value is SemanticRequest {\n return isObject(value)\n && Array.isArray(value.messages)\n && value.messages.every(isMessage)\n && Array.isArray(value.tools)\n && value.tools.every((tool) => isObject(tool) && typeof tool.name === \"string\" && isJsonValue(tool.inputSchema))\n && (value.model === undefined || typeof value.model === \"string\");\n}\n\nfunction isResponse(value: unknown): value is SemanticResponse {\n return isObject(value)\n && Array.isArray(value.content)\n && value.content.every(isContentPart)\n && Array.isArray(value.toolCalls)\n && value.toolCalls.every(isToolCall)\n && (value.sequence === undefined || (Array.isArray(value.sequence) && value.sequence.every(isResponseItem)))\n && (value.id === undefined || typeof value.id === \"string\")\n && (value.model === undefined || typeof value.model === \"string\")\n && (value.finishReason === undefined || typeof value.finishReason === \"string\")\n && (value.usage === undefined || (isObject(value.usage)\n && typeof value.usage.inputTokens === \"number\"\n && typeof value.usage.outputTokens === \"number\"\n && typeof value.usage.totalTokens === \"number\"\n && (value.usage.costUsd === undefined || typeof value.usage.costUsd === \"number\")));\n}\n\nfunction isMessage(value: unknown): value is SemanticMessage {\n return isObject(value)\n && (value.role === \"system\" || value.role === \"user\" || value.role === \"assistant\" || value.role === \"tool\")\n && Array.isArray(value.content)\n && value.content.every(isContentPart)\n && (value.name === undefined || typeof value.name === \"string\")\n && (value.toolCallId === undefined || typeof value.toolCallId === \"string\")\n && (value.toolCalls === undefined || (Array.isArray(value.toolCalls) && value.toolCalls.every(isToolCall)));\n}\n\nfunction isContentPart(value: unknown): value is ContentPart {\n if (!isObject(value)) return false;\n if (value.type === \"text\") return typeof value.text === \"string\";\n if (value.type === \"image\") return typeof value.source === \"string\";\n return value.type === \"json\" && isJsonValue(value.value);\n}\n\nfunction isResponseItem(value: unknown): boolean {\n return isObject(value)\n && ((value.type === \"content\" && isContentPart(value.part))\n || (value.type === \"toolCall\" && isToolCall(value.call)));\n}\n\nfunction isToolCall(value: unknown): value is ToolCall {\n return isObject(value)\n && typeof value.name === \"string\"\n && isJsonValue(value.arguments)\n && (value.id === undefined || typeof value.id === \"string\");\n}\n\nfunction isJsonValue(value: unknown): value is JsonValue {\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") return true;\n if (typeof value === \"number\") return Number.isFinite(value);\n if (Array.isArray(value)) return value.every(isJsonValue);\n return isObject(value) && Object.values(value).every(isJsonValue);\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasCode(error: unknown, code: string): boolean {\n return typeof error === \"object\" && error !== null && \"code\" in error && (error as { code?: unknown }).code === code;\n}\n","import { CassetteDivergenceError, structuredDiff } from \"./diff.js\";\nimport { eventsForTurn } from \"./events.js\";\nimport { defaultFingerprint } from \"./fingerprint.js\";\nimport { redact, type RedactionOptions } from \"./redaction.js\";\nimport { FileCassetteStore, type CassetteStore } from \"./storage.js\";\nimport type {\n Cassette,\n CassetteMetadata,\n CassetteMode,\n ClientBoundary,\n CostCalculator,\n FingerprintFunction,\n ProviderAdapter,\n SemanticRequest,\n SemanticResponse,\n} from \"./types.js\";\n\nexport interface ReplayContext {\n cassette: string;\n turn: number;\n response: SemanticResponse;\n}\n\nexport interface CassetteClientOptions<Request, Response> {\n name: string;\n adapter: ProviderAdapter<Request, Response>;\n client?: ClientBoundary<Request, Response>;\n mode?: CassetteMode;\n store?: CassetteStore;\n fingerprint?: FingerprintFunction;\n redaction?: RedactionOptions;\n replayTurns?: number;\n metadata?: CassetteMetadata;\n calculateCost?: CostCalculator;\n onReplay?: (context: ReplayContext) => void | Promise<void>;\n}\n\nexport interface CassetteClient<Request, Response> {\n (request: Request): Promise<Response>;\n readonly cassetteName: string;\n readonly mode: \"record\" | \"replay\";\n readonly turn: number;\n}\n\nexport async function createCassetteClient<Request, Response>(\n options: CassetteClientOptions<Request, Response>,\n): Promise<CassetteClient<Request, Response>> {\n validateOptions(options);\n const store = options.store ?? new FileCassetteStore();\n const requestedMode = options.mode ?? \"auto\";\n const mode = requestedMode === \"auto\"\n ? (await store.exists(options.name) ? \"replay\" : \"record\")\n : requestedMode;\n const fingerprint = options.fingerprint ?? defaultFingerprint;\n const replayTurns = options.replayTurns ?? Number.POSITIVE_INFINITY;\n let cassette = mode === \"replay\"\n ? await store.read(options.name)\n : newCassette(options.name, options.adapter.provider, options.metadata, options.fingerprint === undefined);\n let position = 0;\n let queue: Promise<void> = Promise.resolve();\n\n const invoke = async (input: Request): Promise<Response> => {\n const normalizedRequest = redacted(options.adapter.normalizeRequest(input), options.redaction);\n const actualFingerprint = fingerprint(normalizedRequest);\n const shouldReplay = mode === \"replay\" && position < replayTurns;\n\n if (shouldReplay) {\n const expected = cassette.turns[position];\n if (!expected) {\n throw new CassetteDivergenceError({\n cassette: cassette.name,\n turn: position,\n expectedFingerprint: \"<end-of-cassette>\",\n actualFingerprint,\n expected: emptyRequest(),\n actual: normalizedRequest,\n diff: structuredDiff(emptyRequest(), normalizedRequest),\n });\n }\n if (expected.fingerprint !== actualFingerprint) {\n throw new CassetteDivergenceError({\n cassette: cassette.name,\n turn: position,\n expectedFingerprint: expected.fingerprint,\n actualFingerprint,\n expected: expected.request,\n actual: normalizedRequest,\n diff: structuredDiff(expected.request, normalizedRequest),\n });\n }\n position += 1;\n if (options.onReplay) {\n await options.onReplay({ cassette: cassette.name, turn: expected.index, response: structuredClone(expected.response) });\n }\n return options.adapter.replayResponse(structuredClone(expected.response));\n }\n\n const client = options.client;\n if (!client) {\n const reason = mode === \"record\" ? \"record mode\" : `partial replay after turn ${replayTurns}`;\n throw new Error(`A real client boundary is required for ${reason}.`);\n }\n const liveResponse = await client(input);\n const normalizedResponse = withCalculatedCost(\n redacted(options.adapter.normalizeResponse(liveResponse), options.redaction),\n normalizedRequest,\n options.calculateCost,\n );\n\n if (mode === \"record\") {\n const now = new Date().toISOString();\n cassette.turns.push({\n index: position,\n fingerprint: actualFingerprint,\n request: normalizedRequest,\n events: eventsForTurn(normalizedRequest, normalizedResponse),\n response: normalizedResponse,\n });\n cassette.updatedAt = now;\n await store.write(cassette);\n }\n position += 1;\n return liveResponse;\n };\n\n const wrapped = ((input: Request): Promise<Response> => {\n const result = queue.then(() => invoke(input));\n queue = result.then(() => undefined, () => undefined);\n return result;\n }) as CassetteClient<Request, Response>;\n\n Object.defineProperties(wrapped, {\n cassetteName: { value: options.name, enumerable: true },\n mode: { value: mode, enumerable: true },\n turn: { get: () => position, enumerable: true },\n });\n return wrapped;\n}\n\nfunction validateOptions<Request, Response>(options: CassetteClientOptions<Request, Response>): void {\n if (!options.name.trim()) throw new Error(\"Cassette name cannot be empty.\");\n if (options.replayTurns !== undefined && (!Number.isInteger(options.replayTurns) || options.replayTurns < 0)) {\n throw new Error(\"replayTurns must be a non-negative integer.\");\n }\n}\n\nfunction redacted<T>(value: T, options: RedactionOptions | undefined): T {\n return redact(value, options ?? {});\n}\n\nfunction withCalculatedCost(\n response: SemanticResponse,\n request: SemanticRequest,\n calculator: CostCalculator | undefined,\n): SemanticResponse {\n if (!calculator) return response;\n const cost = calculator({ request, response });\n if (cost === undefined) return response;\n if (!Number.isFinite(cost) || cost < 0) throw new Error(\"calculateCost must return a non-negative finite number or undefined.\");\n const usage = response.usage ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 };\n return { ...response, usage: { ...usage, costUsd: cost } };\n}\n\nfunction newCassette(\n name: string,\n provider: string,\n metadata: CassetteMetadata | undefined,\n usesDefaultFingerprint: boolean,\n): Cassette {\n const now = new Date().toISOString();\n const mergedMetadata: CassetteMetadata = {\n ...metadata,\n provider,\n fingerprint: usesDefaultFingerprint ? \"default\" : \"custom\",\n };\n return {\n schemaVersion: 1,\n name,\n createdAt: now,\n updatedAt: now,\n turns: [],\n metadata: mergedMetadata,\n };\n}\n\nfunction emptyRequest(): SemanticRequest {\n return { messages: [], tools: [] };\n}\n","import { appendFile, mkdir } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\nexport type UsageEvent =\n | { type: \"use\"; cassette: string; runner: \"vitest\" | \"jest\"; timestamp: string; testFile?: string }\n | { type: \"replay\"; cassette: string; turn: number; timestamp: string; costSavedUsd?: number };\n\nexport const defaultUsageFile = \".agentcassette/usage.ndjson\";\n\nexport async function appendUsageEvent(event: UsageEvent, file = defaultUsageFile): Promise<void> {\n await mkdir(dirname(file), { recursive: true });\n await appendFile(file, `${JSON.stringify(event)}\\n`, { encoding: \"utf8\", mode: 0o600 });\n}\n","import { dirname, join } from \"node:path\";\nimport { createCassetteClient, type CassetteClient, type CassetteClientOptions } from \"../core/client.js\";\nimport { FileCassetteStore } from \"../core/storage.js\";\nimport type { CassetteMode } from \"../core/types.js\";\nimport { appendUsageEvent } from \"./usage.js\";\n\nexport type TestCassetteOptions<Request, Response> = CassetteClientOptions<Request, Response> & {\n directory?: string;\n usageFile?: string;\n testFile?: string;\n};\n\nexport async function withTestCassette<Request, Response>(\n runner: \"vitest\" | \"jest\",\n options: TestCassetteOptions<Request, Response>,\n): Promise<CassetteClient<Request, Response>> {\n const directory = options.directory ?? process.env.AGENTCASSETTE_DIR ?? \".agentcassette/cassettes\";\n const usageFile = options.usageFile\n ?? process.env.AGENTCASSETTE_USAGE_FILE\n ?? join(dirname(directory), \"usage.ndjson\");\n const testFile = options.testFile;\n const mode = environmentMode() ?? options.mode ?? \"auto\";\n const metadata = {\n ...options.metadata,\n ...(testFile === undefined ? {} : { testFile }),\n };\n const userOnReplay = options.onReplay;\n const wrapped = await createCassetteClient({\n ...options,\n mode,\n metadata,\n store: options.store ?? new FileCassetteStore(directory),\n onReplay: async (context) => {\n await appendUsageEvent({\n type: \"replay\",\n cassette: context.cassette,\n turn: context.turn,\n timestamp: new Date().toISOString(),\n ...(context.response.usage?.costUsd === undefined ? {} : { costSavedUsd: context.response.usage.costUsd }),\n }, usageFile);\n await userOnReplay?.(context);\n },\n });\n await appendUsageEvent({\n type: \"use\",\n cassette: options.name,\n runner,\n timestamp: new Date().toISOString(),\n ...(testFile === undefined ? {} : { testFile }),\n }, usageFile);\n return wrapped;\n}\n\nfunction environmentMode(): CassetteMode | undefined {\n const value = process.env.AGENTCASSETTE_MODE;\n if (value === undefined || value === \"\") return undefined;\n if (value === \"record\" || value === \"replay\" || value === \"auto\") return value;\n throw new Error(`Invalid AGENTCASSETTE_MODE \"${value}\". Expected record, replay, or auto.`);\n}\n","export type { CassetteClient, ReplayContext } from \"../core/client.js\";\nexport type { TestCassetteOptions } from \"./testing.js\";\n\nimport type { CassetteClient } from \"../core/client.js\";\nimport { withTestCassette, type TestCassetteOptions } from \"./testing.js\";\n\nexport function withVitestCassette<Request, Response>(\n options: TestCassetteOptions<Request, Response>,\n): Promise<CassetteClient<Request, Response>> {\n return withTestCassette(\"vitest\", options);\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { C as CassetteClient } from './client-CgkW5WWa.cjs';
|
|
2
|
+
export { R as ReplayContext } from './client-CgkW5WWa.cjs';
|
|
3
|
+
import { T as TestCassetteOptions } from './testing-S6vBH_XM.cjs';
|
|
4
|
+
|
|
5
|
+
declare function withVitestCassette<Request, Response>(options: TestCassetteOptions<Request, Response>): Promise<CassetteClient<Request, Response>>;
|
|
6
|
+
|
|
7
|
+
export { CassetteClient, TestCassetteOptions, withVitestCassette };
|
package/dist/vitest.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { C as CassetteClient } from './client-CgkW5WWa.js';
|
|
2
|
+
export { R as ReplayContext } from './client-CgkW5WWa.js';
|
|
3
|
+
import { T as TestCassetteOptions } from './testing-Bj7F-tqU.js';
|
|
4
|
+
|
|
5
|
+
declare function withVitestCassette<Request, Response>(options: TestCassetteOptions<Request, Response>): Promise<CassetteClient<Request, Response>>;
|
|
6
|
+
|
|
7
|
+
export { CassetteClient, TestCassetteOptions, withVitestCassette };
|