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 +46 -2
- package/dist/config.js +1 -1
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +16 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/recorder.d.ts +3 -1
- package/dist/recorder.js +6 -0
- package/dist/resume.d.ts +24 -0
- package/dist/resume.js +66 -0
- package/dist/transport.d.ts +1 -0
- package/dist/transport.js +31 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,9 +67,53 @@ recorder.endSpan(span, { results: ["..."] });
|
|
|
67
67
|
recorder.end("Done");
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
##
|
|
70
|
+
## Resumable Execution (Cascade Replay)
|
|
71
71
|
|
|
72
|
-
|
|
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 || "
|
|
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()),
|
package/dist/errors.d.ts
ADDED
|
@@ -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";
|
package/dist/recorder.d.ts
CHANGED
|
@@ -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
|
|
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);
|
package/dist/resume.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/transport.d.ts
CHANGED
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
}
|