retrace-sdk 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,9 +67,53 @@ recorder.endSpan(span, { results: ["..."] });
67
67
  recorder.end("Done");
68
68
  ```
69
69
 
70
- ## API Key
70
+ ## Resumable Execution (Cascade Replay)
71
71
 
72
- A Retrace API key (`rt_live_...`) is required. Get yours free at [retrace.yashbogam.me/settings](https://retrace.yashbogam.me/settings).
72
+ Mark a function as resumable to enable full cascade replay from the dashboard:
73
+
74
+ ```typescript
75
+ import { configure, trace } from "retrace-sdk";
76
+
77
+ configure({ apiKey: "rt_live_..." });
78
+
79
+ const myAgent = trace(async (prompt: string) => {
80
+ const plan = await planner(prompt);
81
+ const result = await executor(plan);
82
+ return summarize(result);
83
+ }, { name: "my-agent", resumable: true });
84
+ ```
85
+
86
+ When you fork at any span in the dashboard, the SDK re-executes the entire function with modified input — not just one LLM call.
87
+
88
+ ## Error Handling
89
+
90
+ ```typescript
91
+ import { RetraceError, RetraceAuthError, RetraceCreditsExhaustedError, RetraceRateLimitError } from "retrace-sdk";
92
+ ```
93
+
94
+ Typed errors for auth failures, credit exhaustion, and rate limiting.
95
+
96
+ ## Sampling
97
+
98
+ ```typescript
99
+ configure({ apiKey: "rt_live_...", sampleRate: 0.1 }); // Record 10% of traces
100
+ ```
101
+
102
+ ## Changelog
103
+
104
+ ### 0.2.1
105
+
106
+ - **Offline buffer** — stores up to 1000 messages when WebSocket disconnects, flushes on reconnect
107
+ - **HTTP retry** — 3 attempts with exponential backoff on fallback transport
108
+ - **Cascade replay** — `resumable: true` option registers function for SDK-level re-execution
109
+ - **Resume listener** — handles server 'resume' commands for fork replay
110
+
111
+ ### 0.2.0
112
+
113
+ - Typed errors (RetraceAuthError, RetraceCreditsExhaustedError, RetraceRateLimitError)
114
+ - Trace sampling via `sampleRate` config
115
+ - Auto-instrumentation for OpenAI, Anthropic, Gemini
116
+ - WebSocket + HTTP fallback transport
73
117
 
74
118
  ## Links
75
119
 
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const config = {
2
2
  apiKey: process.env.RETRACE_API_KEY || "",
3
- baseUrl: process.env.RETRACE_BASE_URL || "http://localhost:3001",
3
+ baseUrl: process.env.RETRACE_BASE_URL || "https://api-retrace.yashbogam.me",
4
4
  wsUrl: "",
5
5
  projectId: process.env.RETRACE_PROJECT_ID || undefined,
6
6
  enabled: !["false", "0"].includes((process.env.RETRACE_ENABLED || "true").toLowerCase()),
@@ -0,0 +1,16 @@
1
+ export declare class RetraceError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class RetraceAuthError extends RetraceError {
5
+ constructor(message?: string);
6
+ }
7
+ export declare class RetraceCreditsExhaustedError extends RetraceError {
8
+ constructor(message?: string);
9
+ }
10
+ export declare class RetraceConnectionError extends RetraceError {
11
+ constructor(message?: string);
12
+ }
13
+ export declare class RetraceRateLimitError extends RetraceError {
14
+ retryAfter: number;
15
+ constructor(retryAfter: number);
16
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,16 @@
1
+ export class RetraceError extends Error {
2
+ constructor(message) { super(message); this.name = "RetraceError"; }
3
+ }
4
+ export class RetraceAuthError extends RetraceError {
5
+ constructor(message = "Invalid or missing API key") { super(message); this.name = "RetraceAuthError"; }
6
+ }
7
+ export class RetraceCreditsExhaustedError extends RetraceError {
8
+ constructor(message = "Monthly trace limit reached. Upgrade at retrace.yashbogam.me/pricing") { super(message); this.name = "RetraceCreditsExhaustedError"; }
9
+ }
10
+ export class RetraceConnectionError extends RetraceError {
11
+ constructor(message = "Failed to connect to Retrace API") { super(message); this.name = "RetraceConnectionError"; }
12
+ }
13
+ export class RetraceRateLimitError extends RetraceError {
14
+ retryAfter;
15
+ constructor(retryAfter) { super(`Rate limited. Retry after ${retryAfter}s`); this.name = "RetraceRateLimitError"; this.retryAfter = retryAfter; }
16
+ }
package/dist/index.d.ts CHANGED
@@ -6,3 +6,6 @@ export { SpanType, TraceStatus } from "./trace.js";
6
6
  export { installGeminiInterceptor, uninstallGeminiInterceptor } from "./interceptors/gemini.js";
7
7
  export { installOpenAIInterceptor, uninstallOpenAIInterceptor } from "./interceptors/openai.js";
8
8
  export { installAnthropicInterceptor, uninstallAnthropicInterceptor } from "./interceptors/anthropic.js";
9
+ export { RetraceError, RetraceAuthError, RetraceCreditsExhaustedError, RetraceConnectionError, RetraceRateLimitError } from "./errors.js";
10
+ export { registerResumable, handleResume } from "./resume.js";
11
+ export type { ResumeCommand } from "./resume.js";
package/dist/index.js CHANGED
@@ -5,3 +5,5 @@ export { SpanType, TraceStatus } from "./trace.js";
5
5
  export { installGeminiInterceptor, uninstallGeminiInterceptor } from "./interceptors/gemini.js";
6
6
  export { installOpenAIInterceptor, uninstallOpenAIInterceptor } from "./interceptors/openai.js";
7
7
  export { installAnthropicInterceptor, uninstallAnthropicInterceptor } from "./interceptors/anthropic.js";
8
+ export { RetraceError, RetraceAuthError, RetraceCreditsExhaustedError, RetraceConnectionError, RetraceRateLimitError } from "./errors.js";
9
+ export { registerResumable, handleResume } from "./resume.js";
@@ -19,4 +19,6 @@ export declare class TraceRecorder {
19
19
  private installInterceptors;
20
20
  }
21
21
  export declare function record(opts?: RecordOptions): TraceRecorder;
22
- export declare function trace<T>(fn: (...args: unknown[]) => T, opts?: RecordOptions): (...args: unknown[]) => T;
22
+ export declare function trace<T>(fn: (...args: unknown[]) => T, opts?: RecordOptions & {
23
+ resumable?: boolean;
24
+ }): (...args: unknown[]) => T;
package/dist/recorder.js CHANGED
@@ -103,6 +103,12 @@ export function record(opts) {
103
103
  }
104
104
  export function trace(fn, opts) {
105
105
  const cfg = getConfig();
106
+ // Register for cascade replay if resumable
107
+ if (opts?.resumable) {
108
+ import("./resume.js").then(({ registerResumable }) => {
109
+ registerResumable(opts?.name || fn.name || "anonymous", fn);
110
+ });
111
+ }
106
112
  return (...args) => {
107
113
  if (!cfg.enabled || Math.random() > cfg.sampleRate)
108
114
  return fn(...args);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Full cascade replay for Retrace TypeScript SDK.
3
+ *
4
+ * When trace({ resumable: true }) is used, the SDK:
5
+ * 1. Stores the function reference
6
+ * 2. Listens for 'resume' commands on WebSocket
7
+ * 3. Re-executes the function with modified input
8
+ * 4. Streams new spans back
9
+ */
10
+ export interface ResumeCommand {
11
+ forkId: string;
12
+ traceId: string;
13
+ traceName: string;
14
+ forkPointSpanId: string;
15
+ modifiedInput: unknown;
16
+ originalArgs?: unknown[];
17
+ }
18
+ export declare function registerResumable(name: string, fn: (...args: unknown[]) => unknown): void;
19
+ export declare function getResumable(name: string): ((...args: unknown[]) => unknown) | undefined;
20
+ export declare function handleResume(command: ResumeCommand): boolean;
21
+ export declare function parseResumeMessage(msg: {
22
+ type: string;
23
+ data?: Record<string, unknown>;
24
+ }): ResumeCommand | null;
package/dist/resume.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Full cascade replay for Retrace TypeScript SDK.
3
+ *
4
+ * When trace({ resumable: true }) is used, the SDK:
5
+ * 1. Stores the function reference
6
+ * 2. Listens for 'resume' commands on WebSocket
7
+ * 3. Re-executes the function with modified input
8
+ * 4. Streams new spans back
9
+ */
10
+ // Registry of resumable functions
11
+ const resumableFunctions = new Map();
12
+ export function registerResumable(name, fn) {
13
+ resumableFunctions.set(name, fn);
14
+ }
15
+ export function getResumable(name) {
16
+ return resumableFunctions.get(name);
17
+ }
18
+ export function handleResume(command) {
19
+ const fn = getResumable(command.traceName);
20
+ if (!fn)
21
+ return false;
22
+ // Re-execute async in background
23
+ (async () => {
24
+ try {
25
+ const { TraceRecorder } = await import("./recorder.js");
26
+ const { TraceStatus } = await import("./trace.js");
27
+ const recorder = new TraceRecorder({
28
+ name: `Fork: ${command.traceName}`,
29
+ input: command.modifiedInput,
30
+ metadata: {
31
+ _fork_id: command.forkId,
32
+ _fork_of: command.traceId,
33
+ _fork_point: command.forkPointSpanId,
34
+ _cascade_replay: true,
35
+ },
36
+ });
37
+ recorder.start(`Fork: ${command.traceName}`, command.modifiedInput);
38
+ // Determine args for re-execution
39
+ let args = command.originalArgs || [];
40
+ if (typeof command.modifiedInput === "string") {
41
+ args = [command.modifiedInput, ...args.slice(1)];
42
+ }
43
+ else if (typeof command.modifiedInput === "object" && !Array.isArray(command.modifiedInput)) {
44
+ args = [command.modifiedInput];
45
+ }
46
+ const result = await Promise.resolve(fn(...args));
47
+ recorder.end(result, TraceStatus.COMPLETED);
48
+ }
49
+ catch (err) {
50
+ console.error("[retrace] Cascade replay failed:", err);
51
+ }
52
+ })();
53
+ return true;
54
+ }
55
+ export function parseResumeMessage(msg) {
56
+ if (msg.type !== "resume" || !msg.data)
57
+ return null;
58
+ return {
59
+ forkId: msg.data.forkId,
60
+ traceId: msg.data.traceId,
61
+ traceName: msg.data.traceName,
62
+ forkPointSpanId: msg.data.forkPointSpanId,
63
+ modifiedInput: msg.data.modifiedInput,
64
+ originalArgs: msg.data.originalArgs,
65
+ };
66
+ }
@@ -8,6 +8,7 @@ export declare class WSTransport implements Transport {
8
8
  private closed;
9
9
  private backoff;
10
10
  private queue;
11
+ onError?: (type: string, message: string) => void;
11
12
  get isConnected(): boolean;
12
13
  connect(): void;
13
14
  private reconnect;
package/dist/transport.js CHANGED
@@ -6,6 +6,7 @@ export class WSTransport {
6
6
  closed = false;
7
7
  backoff = 1000;
8
8
  queue = [];
9
+ onError;
9
10
  get isConnected() { return this.connected; }
10
11
  connect() {
11
12
  if (this.closed)
@@ -26,6 +27,22 @@ export class WSTransport {
26
27
  else if (msg.type === "ping") {
27
28
  this.ws?.send(JSON.stringify({ type: "pong" }));
28
29
  }
30
+ else if (msg.type === "error") {
31
+ const err = msg.error;
32
+ if (err?.includes("limit reached"))
33
+ this.onError?.("credits_exhausted", err);
34
+ else if (err?.includes("Rate limit"))
35
+ this.onError?.("rate_limited", err);
36
+ else
37
+ this.onError?.("error", err);
38
+ }
39
+ else if (msg.type === "resume") {
40
+ import("./resume.js").then(({ parseResumeMessage, handleResume }) => {
41
+ const cmd = parseResumeMessage(msg);
42
+ if (cmd)
43
+ handleResume(cmd);
44
+ });
45
+ }
29
46
  });
30
47
  this.ws.on("close", () => {
31
48
  this.connected = false;
@@ -54,7 +71,9 @@ export class WSTransport {
54
71
  this.ws.send(msg);
55
72
  }
56
73
  else {
57
- this.queue.push(msg);
74
+ // Cap offline buffer at 1000 messages to prevent memory leak
75
+ if (this.queue.length < 1000)
76
+ this.queue.push(msg);
58
77
  if (!this.ws && !this.closed)
59
78
  this.connect();
60
79
  }
@@ -90,11 +109,17 @@ export class HTTPTransport {
90
109
  const cfg = getConfig();
91
110
  const url = `${cfg.baseUrl}/api/v1/traces`;
92
111
  const body = { ...this.traceData, spans: this.buildSpans() };
93
- fetch(url, {
94
- method: "POST",
95
- headers: { "x-retrace-key": cfg.apiKey, "Content-Type": "application/json" },
96
- body: JSON.stringify(body),
97
- }).catch(() => { });
112
+ const payload = JSON.stringify(body);
113
+ // Retry up to 3 times with exponential backoff
114
+ const attempt = (n, delay) => {
115
+ fetch(url, {
116
+ method: "POST",
117
+ headers: { "x-retrace-key": cfg.apiKey, "Content-Type": "application/json" },
118
+ body: payload,
119
+ }).catch(() => { if (n < 3)
120
+ setTimeout(() => attempt(n + 1, delay * 2), delay); });
121
+ };
122
+ attempt(1, 1000);
98
123
  this.traceData = null;
99
124
  this.spans = [];
100
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retrace-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "The execution replay engine for AI agents. Record, replay, fork, and share agent executions.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",