retrace-sdk 0.3.1 → 0.3.2
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/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/replay.d.ts +43 -0
- package/dist/replay.js +97 -0
- package/dist/transport.js +7 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -9,3 +9,5 @@ export { installAnthropicInterceptor, uninstallAnthropicInterceptor } from "./in
|
|
|
9
9
|
export { RetraceError, RetraceAuthError, RetraceCreditsExhaustedError, RetraceConnectionError, RetraceRateLimitError } from "./errors.js";
|
|
10
10
|
export { registerResumable, handleResume } from "./resume.js";
|
|
11
11
|
export type { ResumeCommand } from "./resume.js";
|
|
12
|
+
export { isReplaying, consumeCassetteEntry, handleReplay } from "./replay.js";
|
|
13
|
+
export type { CassetteEntry, ReplayCommand } from "./replay.js";
|
package/dist/index.js
CHANGED
|
@@ -7,3 +7,4 @@ export { installOpenAIInterceptor, uninstallOpenAIInterceptor } from "./intercep
|
|
|
7
7
|
export { installAnthropicInterceptor, uninstallAnthropicInterceptor } from "./interceptors/anthropic.js";
|
|
8
8
|
export { RetraceError, RetraceAuthError, RetraceCreditsExhaustedError, RetraceConnectionError, RetraceRateLimitError } from "./errors.js";
|
|
9
9
|
export { registerResumable, handleResume } from "./resume.js";
|
|
10
|
+
export { isReplaying, consumeCassetteEntry, handleReplay } from "./replay.js";
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Replay with Cassettes for Retrace TypeScript SDK.
|
|
3
|
+
*
|
|
4
|
+
* When the server sends a "replay" command, the SDK:
|
|
5
|
+
* 1. Loads the cassette (recorded span inputs/outputs)
|
|
6
|
+
* 2. Sets up a global cassette store
|
|
7
|
+
* 3. Re-executes the trace function
|
|
8
|
+
* 4. Tool calls are intercepted and return recorded outputs from the cassette
|
|
9
|
+
*
|
|
10
|
+
* This enables one-click reproduction of any production trace locally.
|
|
11
|
+
*/
|
|
12
|
+
export interface CassetteEntry {
|
|
13
|
+
index: number;
|
|
14
|
+
span_type: string;
|
|
15
|
+
name: string;
|
|
16
|
+
model: string | null;
|
|
17
|
+
input: unknown;
|
|
18
|
+
output: unknown;
|
|
19
|
+
error: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface ReplayCommand {
|
|
22
|
+
traceId: string;
|
|
23
|
+
traceName: string;
|
|
24
|
+
input: unknown;
|
|
25
|
+
cassette: CassetteEntry[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if a replay is currently active.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isReplaying(): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Get the next cassette entry matching a span name and type.
|
|
33
|
+
* Uses sequential matching with name-based fallback for deterministic replay.
|
|
34
|
+
*/
|
|
35
|
+
export declare function consumeCassetteEntry(name: string, spanType: string): CassetteEntry | null;
|
|
36
|
+
/**
|
|
37
|
+
* Handle a replay command from the server.
|
|
38
|
+
*/
|
|
39
|
+
export declare function handleReplay(command: ReplayCommand): boolean;
|
|
40
|
+
export declare function parseReplayMessage(msg: {
|
|
41
|
+
type: string;
|
|
42
|
+
data?: unknown;
|
|
43
|
+
}): ReplayCommand | null;
|
package/dist/replay.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Replay with Cassettes for Retrace TypeScript SDK.
|
|
3
|
+
*
|
|
4
|
+
* When the server sends a "replay" command, the SDK:
|
|
5
|
+
* 1. Loads the cassette (recorded span inputs/outputs)
|
|
6
|
+
* 2. Sets up a global cassette store
|
|
7
|
+
* 3. Re-executes the trace function
|
|
8
|
+
* 4. Tool calls are intercepted and return recorded outputs from the cassette
|
|
9
|
+
*
|
|
10
|
+
* This enables one-click reproduction of any production trace locally.
|
|
11
|
+
*/
|
|
12
|
+
import { getResumable } from "./resume.js";
|
|
13
|
+
// Global cassette state for the current replay session
|
|
14
|
+
let activeCassette = null;
|
|
15
|
+
let cassettePointer = 0;
|
|
16
|
+
/**
|
|
17
|
+
* Check if a replay is currently active.
|
|
18
|
+
*/
|
|
19
|
+
export function isReplaying() {
|
|
20
|
+
return activeCassette !== null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get the next cassette entry matching a span name and type.
|
|
24
|
+
* Uses sequential matching with name-based fallback for deterministic replay.
|
|
25
|
+
*/
|
|
26
|
+
export function consumeCassetteEntry(name, spanType) {
|
|
27
|
+
if (!activeCassette)
|
|
28
|
+
return null;
|
|
29
|
+
// Primary: sequential pointer (deterministic order)
|
|
30
|
+
if (cassettePointer < activeCassette.length) {
|
|
31
|
+
const entry = activeCassette[cassettePointer];
|
|
32
|
+
if (entry.name === name && entry.span_type === spanType) {
|
|
33
|
+
cassettePointer++;
|
|
34
|
+
return entry;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Fallback: search by name + type from current pointer forward
|
|
38
|
+
for (let i = cassettePointer; i < activeCassette.length; i++) {
|
|
39
|
+
if (activeCassette[i].name === name && activeCassette[i].span_type === spanType) {
|
|
40
|
+
cassettePointer = i + 1;
|
|
41
|
+
return activeCassette[i];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Handle a replay command from the server.
|
|
48
|
+
*/
|
|
49
|
+
export function handleReplay(command) {
|
|
50
|
+
const fn = getResumable(command.traceName);
|
|
51
|
+
if (!fn)
|
|
52
|
+
return false;
|
|
53
|
+
// Set up cassette
|
|
54
|
+
activeCassette = command.cassette;
|
|
55
|
+
cassettePointer = 0;
|
|
56
|
+
(async () => {
|
|
57
|
+
try {
|
|
58
|
+
const { TraceRecorder } = await import("./recorder.js");
|
|
59
|
+
const { TraceStatus } = await import("./trace.js");
|
|
60
|
+
const recorder = new TraceRecorder({
|
|
61
|
+
name: `Replay: ${command.traceName}`,
|
|
62
|
+
input: command.input,
|
|
63
|
+
metadata: {
|
|
64
|
+
_replay_of: command.traceId,
|
|
65
|
+
_deterministic_replay: true,
|
|
66
|
+
_cassette_size: command.cassette.length,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
recorder.start(`Replay: ${command.traceName}`, command.input);
|
|
70
|
+
const args = typeof command.input === "string"
|
|
71
|
+
? [command.input]
|
|
72
|
+
: Array.isArray(command.input) ? command.input : [command.input];
|
|
73
|
+
const result = await Promise.resolve(fn(...args));
|
|
74
|
+
recorder.end(result, TraceStatus.COMPLETED);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error("[retrace] Deterministic replay failed:", err);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
// Clean up cassette state
|
|
81
|
+
activeCassette = null;
|
|
82
|
+
cassettePointer = 0;
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
export function parseReplayMessage(msg) {
|
|
88
|
+
if (msg.type !== "replay" || !msg.data)
|
|
89
|
+
return null;
|
|
90
|
+
const data = msg.data;
|
|
91
|
+
return {
|
|
92
|
+
traceId: data.traceId,
|
|
93
|
+
traceName: data.traceName,
|
|
94
|
+
input: data.input,
|
|
95
|
+
cassette: data.cassette,
|
|
96
|
+
};
|
|
97
|
+
}
|
package/dist/transport.js
CHANGED
|
@@ -43,6 +43,13 @@ export class WSTransport {
|
|
|
43
43
|
handleResume(cmd);
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
+
else if (msg.type === "replay") {
|
|
47
|
+
import("./replay.js").then(({ parseReplayMessage, handleReplay }) => {
|
|
48
|
+
const cmd = parseReplayMessage(msg);
|
|
49
|
+
if (cmd)
|
|
50
|
+
handleReplay(cmd);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
46
53
|
else if (msg.type === "halt") {
|
|
47
54
|
const reason = msg.data?.reason || "Guardrail triggered";
|
|
48
55
|
this.onError?.("halt", reason);
|