@reactive-agents/replay 0.11.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.
@@ -0,0 +1,139 @@
1
+ import { Trace, TraceEvent } from '@reactive-agents/trace';
2
+ import { Layer } from 'effect';
3
+ import { ToolService } from '@reactive-agents/tools';
4
+
5
+ interface RecordedToolResult {
6
+ readonly toolName: string;
7
+ readonly argsHash: string;
8
+ readonly args: unknown;
9
+ readonly result: unknown;
10
+ readonly ok: boolean;
11
+ readonly error?: string;
12
+ readonly durationMs: number;
13
+ readonly iter: number;
14
+ readonly seq: number;
15
+ readonly truncated?: boolean;
16
+ }
17
+ interface RecordedRun {
18
+ readonly runId: string;
19
+ readonly task: string;
20
+ readonly model: string;
21
+ readonly provider: string;
22
+ readonly config: Readonly<Record<string, unknown>>;
23
+ readonly trace: Trace;
24
+ readonly toolTable: ReadonlyMap<string, readonly RecordedToolResult[]>;
25
+ }
26
+ interface ReplayOverrides {
27
+ readonly systemPrompt?: string;
28
+ readonly model?: string;
29
+ readonly temperature?: number;
30
+ /** strict (default): error on unrecorded tool call. lenient: return no-recording marker. */
31
+ readonly onMissingToolResult?: "strict" | "lenient";
32
+ }
33
+ type BuilderFn = (() => Promise<unknown>) | ((ctx: BuildContext) => Promise<unknown>);
34
+ interface BuildContext {
35
+ readonly overrides: ReplayOverrides;
36
+ readonly recordedRun: RecordedRun;
37
+ }
38
+ interface TraceSnapshot {
39
+ readonly runId: string;
40
+ readonly task: string;
41
+ readonly model: string;
42
+ readonly iterations: number;
43
+ readonly toolCalls: readonly {
44
+ readonly toolName: string;
45
+ readonly argsHash: string;
46
+ readonly ok: boolean;
47
+ }[];
48
+ readonly output: string | undefined;
49
+ readonly totalTokens: number;
50
+ readonly totalCostUsd: number;
51
+ readonly durationMs: number;
52
+ }
53
+ type ToolSeqEdit = {
54
+ readonly kind: "added";
55
+ readonly toolName: string;
56
+ readonly argsHash: string;
57
+ readonly atIndex: number;
58
+ } | {
59
+ readonly kind: "removed";
60
+ readonly toolName: string;
61
+ readonly argsHash: string;
62
+ readonly atIndex: number;
63
+ } | {
64
+ readonly kind: "reordered";
65
+ readonly toolName: string;
66
+ readonly argsHash: string;
67
+ readonly from: number;
68
+ readonly to: number;
69
+ };
70
+ interface ReplayDiff {
71
+ readonly identical: boolean;
72
+ readonly iterationsDelta: number;
73
+ readonly toolSequenceDiff: readonly ToolSeqEdit[];
74
+ readonly outputDiff: {
75
+ readonly original: string | undefined;
76
+ readonly replay: string | undefined;
77
+ readonly equal: boolean;
78
+ };
79
+ readonly tokensDelta: number;
80
+ readonly costDelta: number;
81
+ readonly durationDeltaMs: number;
82
+ }
83
+ interface ReplayResult {
84
+ readonly original: TraceSnapshot;
85
+ readonly replay: TraceSnapshot;
86
+ readonly diff: ReplayDiff;
87
+ }
88
+
89
+ declare function loadRecordedRun(idOrPath: string): Promise<RecordedRun>;
90
+
91
+ declare function computeArgsHash(args: unknown): string;
92
+ declare function buildToolTable(events: readonly TraceEvent[]): Map<string, RecordedToolResult[]>;
93
+
94
+ type ReplayHit = {
95
+ readonly hit: true;
96
+ readonly result: unknown;
97
+ readonly ok: boolean;
98
+ readonly error?: string;
99
+ readonly truncated?: boolean;
100
+ } | {
101
+ readonly hit: false;
102
+ };
103
+ interface ReplayResultProvider {
104
+ readonly next: (toolName: string, args: unknown) => ReplayHit;
105
+ }
106
+ declare function makeReplayController(table: ReadonlyMap<string, readonly RecordedToolResult[]>): ReplayResultProvider;
107
+
108
+ declare function makeReplayToolLayer(provider: ReplayResultProvider, mode?: "strict" | "lenient"): Layer.Layer<ToolService, never, never>;
109
+
110
+ declare function diffTraces(a: TraceSnapshot, b: TraceSnapshot): ReplayDiff;
111
+
112
+ declare function snapshotFromRecordedRun(run: RecordedRun): TraceSnapshot;
113
+ interface AgentRunOutcome {
114
+ readonly output?: string;
115
+ readonly totalTokens?: number;
116
+ readonly totalCostUsd?: number;
117
+ readonly durationMs?: number;
118
+ readonly toolCalls?: readonly {
119
+ readonly toolName: string;
120
+ readonly argsHash: string;
121
+ readonly ok: boolean;
122
+ }[];
123
+ readonly iterations?: number;
124
+ }
125
+ declare function snapshotFromAgentResult(result: AgentRunOutcome, recordedRun: RecordedRun): TraceSnapshot;
126
+
127
+ /**
128
+ * Re-run a recorded agent run with optional prompt/model overrides.
129
+ *
130
+ * The builder function is responsible for constructing an agent that uses the
131
+ * replay tool layer ({@link makeReplayToolLayer}) so tool results are dispensed
132
+ * from the recording rather than executed live. Builder must return an object
133
+ * exposing `run(task: string): Promise<AgentRunOutcome>` and an optional
134
+ * `dispose()`. The orchestrator invokes `run(recordedRun.task)`, captures the
135
+ * outcome, and produces a structural diff against the original.
136
+ */
137
+ declare function replay(recordedRun: RecordedRun, builderFn: BuilderFn, overrides?: ReplayOverrides): Promise<ReplayResult>;
138
+
139
+ export { type AgentRunOutcome, type BuildContext, type BuilderFn, type RecordedRun, type RecordedToolResult, type ReplayDiff, type ReplayHit, type ReplayOverrides, type ReplayResult, type ReplayResultProvider, type ToolSeqEdit, type TraceSnapshot, buildToolTable, computeArgsHash, diffTraces, loadRecordedRun, makeReplayController, makeReplayToolLayer, replay, snapshotFromAgentResult, snapshotFromRecordedRun };
package/dist/index.js ADDED
@@ -0,0 +1,262 @@
1
+ // src/load.ts
2
+ import { existsSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join, isAbsolute } from "path";
5
+ import { loadTrace } from "@reactive-agents/trace";
6
+
7
+ // src/tool-table.ts
8
+ import { createHash } from "crypto";
9
+ function stableStringify(v) {
10
+ if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "null";
11
+ if (Array.isArray(v)) return "[" + v.map(stableStringify).join(",") + "]";
12
+ const obj = v;
13
+ const keys = Object.keys(obj).sort();
14
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
15
+ }
16
+ function computeArgsHash(args) {
17
+ return createHash("sha256").update(stableStringify(args)).digest("hex").slice(0, 16);
18
+ }
19
+ function buildToolTable(events) {
20
+ const table = /* @__PURE__ */ new Map();
21
+ for (const ev of events) {
22
+ if (ev.kind !== "tool-call-end") continue;
23
+ const e = ev;
24
+ const argsHash = computeArgsHash(e.args);
25
+ const key = `${e.toolName}::${argsHash}`;
26
+ const entry = {
27
+ toolName: e.toolName,
28
+ argsHash,
29
+ args: e.args,
30
+ result: e.result,
31
+ ok: e.ok ?? true,
32
+ error: e.error,
33
+ durationMs: e.durationMs ?? 0,
34
+ iter: e.iter,
35
+ seq: e.seq,
36
+ truncated: e.resultTruncated
37
+ };
38
+ const list = table.get(key) ?? [];
39
+ list.push(entry);
40
+ table.set(key, list);
41
+ }
42
+ return table;
43
+ }
44
+
45
+ // src/load.ts
46
+ var SEARCH_DIRS = [
47
+ join(homedir(), ".reactive-agents", "traces"),
48
+ join(process.cwd(), ".reactive-agents", "traces")
49
+ ];
50
+ async function loadRecordedRun(idOrPath) {
51
+ const path = resolvePath(idOrPath);
52
+ const trace = await loadTrace(path);
53
+ const runStarted = trace.events.find(
54
+ (e) => e.kind === "run-started"
55
+ );
56
+ if (!runStarted) {
57
+ throw new Error(`replay: no run-started event in ${path}`);
58
+ }
59
+ const toolTable = buildToolTable(trace.events);
60
+ return {
61
+ runId: trace.runId,
62
+ task: runStarted.task,
63
+ model: runStarted.model,
64
+ provider: runStarted.provider,
65
+ config: runStarted.config,
66
+ trace,
67
+ toolTable
68
+ };
69
+ }
70
+ function resolvePath(idOrPath) {
71
+ if (isAbsolute(idOrPath) && existsSync(idOrPath)) return idOrPath;
72
+ if (existsSync(idOrPath)) return idOrPath;
73
+ for (const dir of SEARCH_DIRS) {
74
+ const candidate = join(dir, `${idOrPath}.jsonl`);
75
+ if (existsSync(candidate)) return candidate;
76
+ }
77
+ throw new Error(`replay: cannot resolve ${idOrPath}; searched ${SEARCH_DIRS.join(", ")}`);
78
+ }
79
+
80
+ // src/replay-controller.ts
81
+ function makeReplayController(table) {
82
+ const cursors = /* @__PURE__ */ new Map();
83
+ return {
84
+ next(toolName, args) {
85
+ const key = `${toolName}::${computeArgsHash(args)}`;
86
+ const list = table.get(key);
87
+ if (!list) return { hit: false };
88
+ const idx = cursors.get(key) ?? 0;
89
+ if (idx >= list.length) return { hit: false };
90
+ cursors.set(key, idx + 1);
91
+ const rec = list[idx];
92
+ return {
93
+ hit: true,
94
+ result: rec.result,
95
+ ok: rec.ok,
96
+ error: rec.error,
97
+ truncated: rec.truncated
98
+ };
99
+ }
100
+ };
101
+ }
102
+
103
+ // src/replay-tool-layer.ts
104
+ import { Effect, Layer } from "effect";
105
+ import { ToolService } from "@reactive-agents/tools";
106
+ var die = (m) => Effect.die(new Error(`replay: ToolService.${m} not supported during replay`));
107
+ function makeReplayToolLayer(provider, mode = "strict") {
108
+ return Layer.succeed(
109
+ ToolService,
110
+ ToolService.of({
111
+ execute: ((input) => Effect.gen(function* () {
112
+ const hit = provider.next(input.toolName, input.arguments);
113
+ const started = Date.now();
114
+ if (!hit.hit) {
115
+ if (mode === "strict") {
116
+ return yield* Effect.die(
117
+ new Error(
118
+ `replay: unrecorded tool call ${input.toolName} (strict mode); switch to onMissingToolResult:"lenient" or extend the recording`
119
+ )
120
+ );
121
+ }
122
+ return {
123
+ toolName: input.toolName,
124
+ success: false,
125
+ error: "replay: no recording for this call (lenient mode)",
126
+ executionTimeMs: Date.now() - started
127
+ };
128
+ }
129
+ if (!hit.ok) {
130
+ return {
131
+ toolName: input.toolName,
132
+ success: false,
133
+ error: hit.error ?? "replay: recorded error",
134
+ executionTimeMs: Date.now() - started
135
+ };
136
+ }
137
+ if (hit.truncated && mode === "strict") {
138
+ return yield* Effect.die(
139
+ new Error(
140
+ `replay: recorded tool result for ${input.toolName} was truncated; live re-execution may diverge. Switch to lenient mode to proceed.`
141
+ )
142
+ );
143
+ }
144
+ return {
145
+ toolName: input.toolName,
146
+ success: true,
147
+ result: hit.result,
148
+ executionTimeMs: Date.now() - started
149
+ };
150
+ })),
151
+ register: (() => die("register")),
152
+ unregisterTool: (() => Effect.succeed(void 0)),
153
+ connectMCPServer: (() => die("connectMCPServer")),
154
+ disconnectMCPServer: (() => die("disconnectMCPServer")),
155
+ listTools: (() => Effect.succeed([])),
156
+ getTool: (() => die("getTool")),
157
+ toFunctionCallingFormat: (() => Effect.succeed([])),
158
+ listMCPServers: (() => Effect.succeed([]))
159
+ })
160
+ );
161
+ }
162
+
163
+ // src/diff.ts
164
+ function diffTraces(a, b) {
165
+ const outputEqual = a.output === b.output;
166
+ const toolSequenceDiff = diffToolSequence(a.toolCalls, b.toolCalls);
167
+ const identical = outputEqual && a.iterations === b.iterations && toolSequenceDiff.length === 0 && a.totalTokens === b.totalTokens;
168
+ return {
169
+ identical,
170
+ iterationsDelta: b.iterations - a.iterations,
171
+ toolSequenceDiff,
172
+ outputDiff: { original: a.output, replay: b.output, equal: outputEqual },
173
+ tokensDelta: b.totalTokens - a.totalTokens,
174
+ costDelta: b.totalCostUsd - a.totalCostUsd,
175
+ durationDeltaMs: b.durationMs - a.durationMs
176
+ };
177
+ }
178
+ function diffToolSequence(a, b) {
179
+ const edits = [];
180
+ const len = Math.max(a.length, b.length);
181
+ for (let i = 0; i < len; i++) {
182
+ const x = a[i];
183
+ const y = b[i];
184
+ if (x && !y) {
185
+ edits.push({ kind: "removed", toolName: x.toolName, argsHash: x.argsHash, atIndex: i });
186
+ } else if (!x && y) {
187
+ edits.push({ kind: "added", toolName: y.toolName, argsHash: y.argsHash, atIndex: i });
188
+ } else if (x && y && (x.toolName !== y.toolName || x.argsHash !== y.argsHash)) {
189
+ edits.push({ kind: "removed", toolName: x.toolName, argsHash: x.argsHash, atIndex: i });
190
+ edits.push({ kind: "added", toolName: y.toolName, argsHash: y.argsHash, atIndex: i });
191
+ }
192
+ }
193
+ return edits;
194
+ }
195
+
196
+ // src/snapshot.ts
197
+ import { traceStats } from "@reactive-agents/trace";
198
+ function snapshotFromRecordedRun(run) {
199
+ const stats = traceStats(run.trace);
200
+ const completed = run.trace.events.find(
201
+ (e) => e.kind === "run-completed"
202
+ );
203
+ const toolCalls = [];
204
+ for (const [, list] of run.toolTable) {
205
+ for (const r of list) {
206
+ toolCalls.push({ toolName: r.toolName, argsHash: r.argsHash, ok: r.ok });
207
+ }
208
+ }
209
+ return {
210
+ runId: run.runId,
211
+ task: run.task,
212
+ model: run.model,
213
+ iterations: stats.iterations,
214
+ toolCalls,
215
+ output: completed?.output,
216
+ totalTokens: stats.totalTokens,
217
+ totalCostUsd: completed?.totalCostUsd ?? 0,
218
+ durationMs: stats.durationMs
219
+ };
220
+ }
221
+ function snapshotFromAgentResult(result, recordedRun) {
222
+ return {
223
+ runId: `${recordedRun.runId}-replay`,
224
+ task: recordedRun.task,
225
+ model: recordedRun.model,
226
+ iterations: result.iterations ?? 0,
227
+ toolCalls: result.toolCalls ?? [],
228
+ output: result.output,
229
+ totalTokens: result.totalTokens ?? 0,
230
+ totalCostUsd: result.totalCostUsd ?? 0,
231
+ durationMs: result.durationMs ?? 0
232
+ };
233
+ }
234
+
235
+ // src/replay.ts
236
+ async function replay(recordedRun, builderFn, overrides = {}) {
237
+ const ctx = { overrides, recordedRun };
238
+ const agent = await (builderFn.length > 0 ? builderFn(ctx) : builderFn());
239
+ try {
240
+ const result = await agent.run(recordedRun.task);
241
+ const original = snapshotFromRecordedRun(recordedRun);
242
+ const replaySnapshot = snapshotFromAgentResult(result, recordedRun);
243
+ const diff = diffTraces(original, replaySnapshot);
244
+ return { original, replay: replaySnapshot, diff };
245
+ } finally {
246
+ if (typeof agent.dispose === "function") {
247
+ await agent.dispose();
248
+ }
249
+ }
250
+ }
251
+ export {
252
+ buildToolTable,
253
+ computeArgsHash,
254
+ diffTraces,
255
+ loadRecordedRun,
256
+ makeReplayController,
257
+ makeReplayToolLayer,
258
+ replay,
259
+ snapshotFromAgentResult,
260
+ snapshotFromRecordedRun
261
+ };
262
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/load.ts","../src/tool-table.ts","../src/replay-controller.ts","../src/replay-tool-layer.ts","../src/diff.ts","../src/snapshot.ts","../src/replay.ts"],"sourcesContent":["import { existsSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join, isAbsolute } from \"node:path\"\nimport { loadTrace } from \"@reactive-agents/trace\"\nimport type { TraceEvent } from \"@reactive-agents/trace\"\nimport { buildToolTable } from \"./tool-table.js\"\nimport type { RecordedRun } from \"./types.js\"\n\nconst SEARCH_DIRS = [\n join(homedir(), \".reactive-agents\", \"traces\"),\n join(process.cwd(), \".reactive-agents\", \"traces\"),\n]\n\ninterface RunStartedShape {\n readonly kind: \"run-started\"\n readonly task: string\n readonly model: string\n readonly provider: string\n readonly config: Record<string, unknown>\n}\n\nexport async function loadRecordedRun(idOrPath: string): Promise<RecordedRun> {\n const path = resolvePath(idOrPath)\n const trace = await loadTrace(path)\n const runStarted = trace.events.find(\n (e): e is TraceEvent & RunStartedShape => e.kind === \"run-started\",\n )\n if (!runStarted) {\n throw new Error(`replay: no run-started event in ${path}`)\n }\n const toolTable = buildToolTable(trace.events)\n return {\n runId: trace.runId,\n task: runStarted.task,\n model: runStarted.model,\n provider: runStarted.provider,\n config: runStarted.config,\n trace,\n toolTable,\n }\n}\n\nfunction resolvePath(idOrPath: string): string {\n if (isAbsolute(idOrPath) && existsSync(idOrPath)) return idOrPath\n if (existsSync(idOrPath)) return idOrPath\n for (const dir of SEARCH_DIRS) {\n const candidate = join(dir, `${idOrPath}.jsonl`)\n if (existsSync(candidate)) return candidate\n }\n throw new Error(`replay: cannot resolve ${idOrPath}; searched ${SEARCH_DIRS.join(\", \")}`)\n}\n","import { createHash } from \"node:crypto\"\nimport type { TraceEvent } from \"@reactive-agents/trace\"\nimport type { RecordedToolResult } from \"./types.js\"\n\nfunction stableStringify(v: unknown): string {\n if (v === null || typeof v !== \"object\") return JSON.stringify(v) ?? \"null\"\n if (Array.isArray(v)) return \"[\" + v.map(stableStringify).join(\",\") + \"]\"\n const obj = v as Record<string, unknown>\n const keys = Object.keys(obj).sort()\n return \"{\" + keys.map((k) => JSON.stringify(k) + \":\" + stableStringify(obj[k])).join(\",\") + \"}\"\n}\n\nexport function computeArgsHash(args: unknown): string {\n return createHash(\"sha256\").update(stableStringify(args)).digest(\"hex\").slice(0, 16)\n}\n\ninterface ToolCallEndShape {\n readonly kind: \"tool-call-end\"\n readonly toolName: string\n readonly args?: unknown\n readonly result?: unknown\n readonly resultTruncated?: boolean\n readonly ok?: boolean\n readonly error?: string\n readonly durationMs?: number\n readonly iter: number\n readonly seq: number\n}\n\nexport function buildToolTable(events: readonly TraceEvent[]): Map<string, RecordedToolResult[]> {\n const table = new Map<string, RecordedToolResult[]>()\n for (const ev of events) {\n if (ev.kind !== \"tool-call-end\") continue\n const e = ev as unknown as ToolCallEndShape\n const argsHash = computeArgsHash(e.args)\n const key = `${e.toolName}::${argsHash}`\n const entry: RecordedToolResult = {\n toolName: e.toolName,\n argsHash,\n args: e.args,\n result: e.result,\n ok: e.ok ?? true,\n error: e.error,\n durationMs: e.durationMs ?? 0,\n iter: e.iter,\n seq: e.seq,\n truncated: e.resultTruncated,\n }\n const list = table.get(key) ?? []\n list.push(entry)\n table.set(key, list)\n }\n return table\n}\n","import { computeArgsHash } from \"./tool-table.js\"\nimport type { RecordedToolResult } from \"./types.js\"\n\nexport type ReplayHit =\n | { readonly hit: true; readonly result: unknown; readonly ok: boolean; readonly error?: string; readonly truncated?: boolean }\n | { readonly hit: false }\n\nexport interface ReplayResultProvider {\n readonly next: (toolName: string, args: unknown) => ReplayHit\n}\n\nexport function makeReplayController(\n table: ReadonlyMap<string, readonly RecordedToolResult[]>,\n): ReplayResultProvider {\n const cursors = new Map<string, number>()\n return {\n next(toolName, args) {\n const key = `${toolName}::${computeArgsHash(args)}`\n const list = table.get(key)\n if (!list) return { hit: false }\n const idx = cursors.get(key) ?? 0\n if (idx >= list.length) return { hit: false }\n cursors.set(key, idx + 1)\n const rec = list[idx]\n return {\n hit: true,\n result: rec.result,\n ok: rec.ok,\n error: rec.error,\n truncated: rec.truncated,\n }\n },\n }\n}\n","import { Effect, Layer } from \"effect\"\nimport { ToolService } from \"@reactive-agents/tools\"\nimport type { ReplayResultProvider } from \"./replay-controller.js\"\n\nconst die = (m: string): Effect.Effect<never, never, never> =>\n Effect.die(new Error(`replay: ToolService.${m} not supported during replay`))\n\nexport function makeReplayToolLayer(\n provider: ReplayResultProvider,\n mode: \"strict\" | \"lenient\" = \"strict\",\n): Layer.Layer<ToolService, never, never> {\n return Layer.succeed(\n ToolService,\n ToolService.of({\n execute: ((input: { toolName: string; arguments?: unknown }) =>\n Effect.gen(function* () {\n const hit = provider.next(input.toolName, input.arguments)\n const started = Date.now()\n if (!hit.hit) {\n if (mode === \"strict\") {\n return yield* Effect.die(\n new Error(\n `replay: unrecorded tool call ${input.toolName} (strict mode); switch to onMissingToolResult:\"lenient\" or extend the recording`,\n ),\n )\n }\n return {\n toolName: input.toolName,\n success: false,\n error: \"replay: no recording for this call (lenient mode)\",\n executionTimeMs: Date.now() - started,\n }\n }\n if (!hit.ok) {\n return {\n toolName: input.toolName,\n success: false,\n error: hit.error ?? \"replay: recorded error\",\n executionTimeMs: Date.now() - started,\n }\n }\n if (hit.truncated && mode === \"strict\") {\n return yield* Effect.die(\n new Error(\n `replay: recorded tool result for ${input.toolName} was truncated; live re-execution may diverge. Switch to lenient mode to proceed.`,\n ),\n )\n }\n return {\n toolName: input.toolName,\n success: true,\n result: hit.result,\n executionTimeMs: Date.now() - started,\n }\n })) as never,\n register: (() => die(\"register\")) as never,\n unregisterTool: (() => Effect.succeed(undefined)) as never,\n connectMCPServer: (() => die(\"connectMCPServer\")) as never,\n disconnectMCPServer: (() => die(\"disconnectMCPServer\")) as never,\n listTools: (() => Effect.succeed([] as never)) as never,\n getTool: (() => die(\"getTool\")) as never,\n toFunctionCallingFormat: (() => Effect.succeed([] as never)) as never,\n listMCPServers: (() => Effect.succeed([] as never)) as never,\n }),\n )\n}\n","import type { ReplayDiff, TraceSnapshot, ToolSeqEdit } from \"./types.js\"\n\nexport function diffTraces(a: TraceSnapshot, b: TraceSnapshot): ReplayDiff {\n const outputEqual = a.output === b.output\n const toolSequenceDiff = diffToolSequence(a.toolCalls, b.toolCalls)\n const identical =\n outputEqual &&\n a.iterations === b.iterations &&\n toolSequenceDiff.length === 0 &&\n a.totalTokens === b.totalTokens\n return {\n identical,\n iterationsDelta: b.iterations - a.iterations,\n toolSequenceDiff,\n outputDiff: { original: a.output, replay: b.output, equal: outputEqual },\n tokensDelta: b.totalTokens - a.totalTokens,\n costDelta: b.totalCostUsd - a.totalCostUsd,\n durationDeltaMs: b.durationMs - a.durationMs,\n }\n}\n\nfunction diffToolSequence(\n a: readonly { readonly toolName: string; readonly argsHash: string }[],\n b: readonly { readonly toolName: string; readonly argsHash: string }[],\n): ToolSeqEdit[] {\n const edits: ToolSeqEdit[] = []\n const len = Math.max(a.length, b.length)\n for (let i = 0; i < len; i++) {\n const x = a[i]\n const y = b[i]\n if (x && !y) {\n edits.push({ kind: \"removed\", toolName: x.toolName, argsHash: x.argsHash, atIndex: i })\n } else if (!x && y) {\n edits.push({ kind: \"added\", toolName: y.toolName, argsHash: y.argsHash, atIndex: i })\n } else if (x && y && (x.toolName !== y.toolName || x.argsHash !== y.argsHash)) {\n edits.push({ kind: \"removed\", toolName: x.toolName, argsHash: x.argsHash, atIndex: i })\n edits.push({ kind: \"added\", toolName: y.toolName, argsHash: y.argsHash, atIndex: i })\n }\n }\n return edits\n}\n","import { traceStats } from \"@reactive-agents/trace\"\nimport type { RecordedRun, TraceSnapshot } from \"./types.js\"\n\ninterface RunCompletedShape {\n readonly kind: \"run-completed\"\n readonly output?: string\n readonly totalCostUsd?: number\n}\n\nexport function snapshotFromRecordedRun(run: RecordedRun): TraceSnapshot {\n const stats = traceStats(run.trace)\n const completed = run.trace.events.find(\n (e): e is typeof e & RunCompletedShape => e.kind === \"run-completed\",\n )\n const toolCalls: { toolName: string; argsHash: string; ok: boolean }[] = []\n for (const [, list] of run.toolTable) {\n for (const r of list) {\n toolCalls.push({ toolName: r.toolName, argsHash: r.argsHash, ok: r.ok })\n }\n }\n return {\n runId: run.runId,\n task: run.task,\n model: run.model,\n iterations: stats.iterations,\n toolCalls,\n output: completed?.output,\n totalTokens: stats.totalTokens,\n totalCostUsd: completed?.totalCostUsd ?? 0,\n durationMs: stats.durationMs,\n }\n}\n\nexport interface AgentRunOutcome {\n readonly output?: string\n readonly totalTokens?: number\n readonly totalCostUsd?: number\n readonly durationMs?: number\n readonly toolCalls?: readonly { readonly toolName: string; readonly argsHash: string; readonly ok: boolean }[]\n readonly iterations?: number\n}\n\nexport function snapshotFromAgentResult(\n result: AgentRunOutcome,\n recordedRun: RecordedRun,\n): TraceSnapshot {\n return {\n runId: `${recordedRun.runId}-replay`,\n task: recordedRun.task,\n model: recordedRun.model,\n iterations: result.iterations ?? 0,\n toolCalls: result.toolCalls ?? [],\n output: result.output,\n totalTokens: result.totalTokens ?? 0,\n totalCostUsd: result.totalCostUsd ?? 0,\n durationMs: result.durationMs ?? 0,\n }\n}\n","import { diffTraces } from \"./diff.js\"\nimport { snapshotFromAgentResult, snapshotFromRecordedRun, type AgentRunOutcome } from \"./snapshot.js\"\nimport type { BuildContext, BuilderFn, RecordedRun, ReplayOverrides, ReplayResult } from \"./types.js\"\n\n/**\n * Re-run a recorded agent run with optional prompt/model overrides.\n *\n * The builder function is responsible for constructing an agent that uses the\n * replay tool layer ({@link makeReplayToolLayer}) so tool results are dispensed\n * from the recording rather than executed live. Builder must return an object\n * exposing `run(task: string): Promise<AgentRunOutcome>` and an optional\n * `dispose()`. The orchestrator invokes `run(recordedRun.task)`, captures the\n * outcome, and produces a structural diff against the original.\n */\nexport async function replay(\n recordedRun: RecordedRun,\n builderFn: BuilderFn,\n overrides: ReplayOverrides = {},\n): Promise<ReplayResult> {\n const ctx: BuildContext = { overrides, recordedRun }\n const agent = (await (builderFn.length > 0\n ? (builderFn as (c: BuildContext) => Promise<unknown>)(ctx)\n : (builderFn as () => Promise<unknown>)())) as {\n run: (task: string) => Promise<AgentRunOutcome>\n dispose?: () => Promise<void>\n }\n try {\n const result = await agent.run(recordedRun.task)\n const original = snapshotFromRecordedRun(recordedRun)\n const replaySnapshot = snapshotFromAgentResult(result, recordedRun)\n const diff = diffTraces(original, replaySnapshot)\n return { original, replay: replaySnapshot, diff }\n } finally {\n if (typeof agent.dispose === \"function\") {\n await agent.dispose()\n }\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@reactive-agents/replay",
3
+ "version": "0.11.0",
4
+ "description": "Deterministic re-run of recorded reactive-agent traces with prompt/model overrides",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsup --config ../../tsup.config.base.ts",
8
+ "typecheck": "tsc --noEmit",
9
+ "test": "bun test --reporter=dots",
10
+ "test:watch": "bun test --watch"
11
+ },
12
+ "dependencies": {
13
+ "@reactive-agents/core": "0.11.0",
14
+ "@reactive-agents/trace": "0.11.0",
15
+ "@reactive-agents/tools": "0.11.0",
16
+ "effect": "^3.10.0"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^6.0.3",
20
+ "bun-types": "latest"
21
+ },
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/tylerjrbuell/reactive-agents-ts.git",
26
+ "directory": "packages/replay"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "exports": {
37
+ ".": {
38
+ "bun": "./dist/index.js",
39
+ "import": "./dist/index.js",
40
+ "types": "./dist/index.d.ts"
41
+ }
42
+ },
43
+ "homepage": "https://docs.reactiveagents.dev/",
44
+ "bugs": {
45
+ "url": "https://github.com/tylerjrbuell/reactive-agents-ts/issues"
46
+ }
47
+ }