retrace-sdk 0.1.3

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 ADDED
@@ -0,0 +1,94 @@
1
+ # retrace
2
+
3
+ Record, replay, fork & share AI agent executions — TypeScript SDK.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install retrace
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { configure, record, trace, TraceStatus } from "retrace";
15
+
16
+ configure({ apiKey: "rt_live_...", projectId: "your-project-id" });
17
+
18
+ // Wrap a function with trace()
19
+ const myAgent = trace((prompt: string) => {
20
+ // OpenAI calls are auto-captured
21
+ const response = await openai.chat.completions.create({
22
+ model: "gpt-4o",
23
+ messages: [{ role: "user", content: prompt }],
24
+ });
25
+ return response.choices[0].message.content;
26
+ }, { name: "my-agent" });
27
+
28
+ const result = myAgent("What is the meaning of life?");
29
+ ```
30
+
31
+ ## Configuration
32
+
33
+ ```typescript
34
+ import { configure } from "retrace";
35
+
36
+ configure({
37
+ apiKey: "rt_live_...", // or RETRACE_API_KEY env var
38
+ baseUrl: "http://localhost:3001", // or RETRACE_BASE_URL env var
39
+ projectId: "...", // or RETRACE_PROJECT_ID env var
40
+ });
41
+ ```
42
+
43
+ Set `RETRACE_ENABLED=false` to disable recording without changing code.
44
+
45
+ ## OpenAI Auto-Capture
46
+
47
+ ```typescript
48
+ import { record, TraceStatus } from "retrace";
49
+ import OpenAI from "openai";
50
+
51
+ const openai = new OpenAI();
52
+ const recorder = record({ name: "chat-agent" });
53
+ recorder.start();
54
+
55
+ const response = await openai.chat.completions.create({
56
+ model: "gpt-4o",
57
+ messages: [{ role: "user", content: "Hello!" }],
58
+ });
59
+ // LLM call is automatically captured as a span
60
+
61
+ recorder.end(response.choices[0].message.content);
62
+ ```
63
+
64
+ ## Manual Span Creation
65
+
66
+ ```typescript
67
+ import { record, SpanType } from "retrace";
68
+
69
+ const recorder = record({ name: "custom-agent" });
70
+ recorder.start();
71
+
72
+ const span = recorder.startSpan("web-search", SpanType.TOOL_CALL, { query: "latest news" });
73
+ // ... do work ...
74
+ recorder.endSpan(span, { results: ["..."] });
75
+
76
+ recorder.end("Done");
77
+ ```
78
+
79
+ ## Transport Options
80
+
81
+ The SDK uses WebSocket by default for real-time streaming. Falls back to HTTP if WS connection fails.
82
+
83
+ ```typescript
84
+ import { createTransport } from "retrace/transport";
85
+
86
+ // Force HTTP transport
87
+ const transport = createTransport("http");
88
+
89
+ // Force WebSocket
90
+ const transport = createTransport("ws");
91
+
92
+ // Auto-detect (default)
93
+ const transport = createTransport("auto");
94
+ ```
@@ -0,0 +1,9 @@
1
+ export interface Config {
2
+ apiKey: string;
3
+ baseUrl: string;
4
+ wsUrl: string;
5
+ projectId: string | undefined;
6
+ enabled: boolean;
7
+ }
8
+ export declare function configure(opts: Partial<Config>): Config;
9
+ export declare function getConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,21 @@
1
+ const config = {
2
+ apiKey: process.env.RETRACE_API_KEY || "",
3
+ baseUrl: process.env.RETRACE_BASE_URL || "http://localhost:3001",
4
+ wsUrl: "",
5
+ projectId: process.env.RETRACE_PROJECT_ID || undefined,
6
+ enabled: !["false", "0"].includes((process.env.RETRACE_ENABLED || "true").toLowerCase()),
7
+ };
8
+ config.wsUrl = config.baseUrl.replace("https://", "wss://").replace("http://", "ws://");
9
+ export function configure(opts) {
10
+ if (opts.apiKey && !opts.apiKey.startsWith("rt_live_")) {
11
+ console.warn("[retrace] API key does not start with 'rt_live_'. This may be invalid.");
12
+ }
13
+ Object.assign(config, opts);
14
+ if (opts.baseUrl && !opts.wsUrl) {
15
+ config.wsUrl = config.baseUrl.replace("https://", "wss://").replace("http://", "ws://");
16
+ }
17
+ return config;
18
+ }
19
+ export function getConfig() {
20
+ return config;
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { configure, getConfig } from "./config.js";
3
+ describe("SDK config", () => {
4
+ it("has defaults", () => {
5
+ const c = getConfig();
6
+ expect(c.baseUrl).toBe("http://localhost:3001");
7
+ expect(c.enabled).toBe(true);
8
+ });
9
+ it("configures custom values", () => {
10
+ configure({ apiKey: "rt_live_test", baseUrl: "http://custom:3001" });
11
+ const c = getConfig();
12
+ expect(c.apiKey).toBe("rt_live_test");
13
+ expect(c.baseUrl).toBe("http://custom:3001");
14
+ });
15
+ });
@@ -0,0 +1,6 @@
1
+ export { configure, getConfig } from "./config.js";
2
+ export { record, trace, TraceRecorder } from "./recorder.js";
3
+ export { SpanBuilder, TraceBuilder } from "./trace.js";
4
+ export type { SpanData, TraceData } from "./trace.js";
5
+ export { SpanType, TraceStatus } from "./trace.js";
6
+ export { installGeminiInterceptor, uninstallGeminiInterceptor } from "./interceptors/gemini.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { configure, getConfig } from "./config.js";
2
+ export { record, trace, TraceRecorder } from "./recorder.js";
3
+ export { SpanBuilder, TraceBuilder } from "./trace.js";
4
+ export { SpanType, TraceStatus } from "./trace.js";
5
+ export { installGeminiInterceptor, uninstallGeminiInterceptor } from "./interceptors/gemini.js";
@@ -0,0 +1,3 @@
1
+ import { SpanData } from "../trace.js";
2
+ export declare function installGeminiInterceptor(onSpan: (span: SpanData) => void): void;
3
+ export declare function uninstallGeminiInterceptor(): void;
@@ -0,0 +1,82 @@
1
+ import { SpanType } from "../trace.js";
2
+ import { genId, nowIso, truncateJson } from "../utils.js";
3
+ const PRICING = {
4
+ "gemini-3.1-pro-preview": [2.0, 12.0],
5
+ "gemini-2.5-pro": [1.25, 10.0],
6
+ "gemini-2.5-flash": [0.15, 0.60],
7
+ "gemini-2.0-flash": [0.10, 0.40],
8
+ };
9
+ function calcCost(model, inputTokens, outputTokens) {
10
+ const p = PRICING[model] || [0, 0];
11
+ return (inputTokens * p[0] + outputTokens * p[1]) / 1_000_000;
12
+ }
13
+ let originalGenerateContent = null;
14
+ let installed = false;
15
+ let onSpanCallback = null;
16
+ export function installGeminiInterceptor(onSpan) {
17
+ if (installed) {
18
+ onSpanCallback = onSpan;
19
+ return;
20
+ }
21
+ onSpanCallback = onSpan;
22
+ import("@google/genai").then((genaiMod) => {
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ const mod = genaiMod;
25
+ const modelsProto = mod?.Models?.prototype || mod?.default?.Models?.prototype;
26
+ if (!modelsProto?.generateContent)
27
+ return;
28
+ originalGenerateContent = modelsProto.generateContent;
29
+ modelsProto.generateContent = async function (...args) {
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ const opts = args[0] || {};
32
+ const model = opts.model || "unknown";
33
+ const contents = opts.contents;
34
+ const spanId = genId();
35
+ const startedAt = nowIso();
36
+ const startMs = Date.now();
37
+ try {
38
+ const result = await originalGenerateContent.apply(this, args);
39
+ const durationMs = Date.now() - startMs;
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ const res = result;
42
+ const inputTokens = res?.usageMetadata?.promptTokenCount || 0;
43
+ const outputTokens = res?.usageMetadata?.candidatesTokenCount || 0;
44
+ const span = {
45
+ id: spanId, trace_id: "", parent_id: null,
46
+ span_type: SpanType.LLM_CALL, name: "retrace.ai.generate", model,
47
+ input: truncateJson(contents), output: truncateJson(res?.text || ""),
48
+ input_tokens: inputTokens, output_tokens: outputTokens,
49
+ cost: calcCost(model, inputTokens, outputTokens),
50
+ duration_ms: durationMs, started_at: startedAt, ended_at: nowIso(),
51
+ };
52
+ onSpanCallback?.(span);
53
+ return result;
54
+ }
55
+ catch (err) {
56
+ const span = {
57
+ id: spanId, trace_id: "", parent_id: null,
58
+ span_type: SpanType.LLM_CALL, name: "retrace.ai.generate", model,
59
+ input: truncateJson(contents), started_at: startedAt, ended_at: nowIso(),
60
+ duration_ms: Date.now() - startMs,
61
+ error: err instanceof Error ? err.message : String(err),
62
+ };
63
+ onSpanCallback?.(span);
64
+ throw err;
65
+ }
66
+ };
67
+ installed = true;
68
+ }).catch(() => { });
69
+ }
70
+ export function uninstallGeminiInterceptor() {
71
+ if (!installed || !originalGenerateContent)
72
+ return;
73
+ import("@google/genai").then((genaiMod) => {
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ const mod = genaiMod;
76
+ const modelsProto = mod?.Models?.prototype || mod?.default?.Models?.prototype;
77
+ if (modelsProto)
78
+ modelsProto.generateContent = originalGenerateContent;
79
+ }).catch(() => { });
80
+ installed = false;
81
+ onSpanCallback = null;
82
+ }
@@ -0,0 +1 @@
1
+ export { installGeminiInterceptor, uninstallGeminiInterceptor } from "./gemini.js";
@@ -0,0 +1 @@
1
+ export { installGeminiInterceptor, uninstallGeminiInterceptor } from "./gemini.js";
@@ -0,0 +1,22 @@
1
+ import { SpanBuilder, SpanData, SpanType, TraceStatus } from "./trace.js";
2
+ export interface RecordOptions {
3
+ name?: string;
4
+ input?: unknown;
5
+ metadata?: Record<string, unknown>;
6
+ }
7
+ export declare class TraceRecorder {
8
+ private builder;
9
+ private transport;
10
+ private interceptorsInstalled;
11
+ output: unknown;
12
+ constructor(opts?: RecordOptions);
13
+ get traceId(): string;
14
+ start(name?: string, input?: unknown): this;
15
+ end(output?: unknown, status?: TraceStatus): void;
16
+ addSpan(span: SpanData): void;
17
+ startSpan(name: string, spanType?: SpanType, input?: unknown, model?: string, parentId?: string): SpanBuilder;
18
+ endSpan(spanBuilder: SpanBuilder, output?: unknown, error?: string): void;
19
+ private installInterceptors;
20
+ }
21
+ export declare function record(opts?: RecordOptions): TraceRecorder;
22
+ export declare function trace<T>(fn: (...args: unknown[]) => T, opts?: RecordOptions): (...args: unknown[]) => T;
@@ -0,0 +1,123 @@
1
+ import { getConfig } from "./config.js";
2
+ import { SpanBuilder, SpanType, TraceBuilder, TraceStatus } from "./trace.js";
3
+ import { createTransport } from "./transport.js";
4
+ import { installGeminiInterceptor } from "./interceptors/gemini.js";
5
+ export class TraceRecorder {
6
+ builder;
7
+ transport;
8
+ interceptorsInstalled = false;
9
+ output = undefined;
10
+ constructor(opts) {
11
+ this.builder = new TraceBuilder();
12
+ this.transport = createTransport();
13
+ const cfg = getConfig();
14
+ if (cfg.projectId)
15
+ this.builder.setProjectId(cfg.projectId);
16
+ if (opts?.metadata)
17
+ this.builder.setMetadata(opts.metadata);
18
+ if (opts?.name || opts?.input) {
19
+ this.builder.start(opts.name, opts.input);
20
+ }
21
+ }
22
+ get traceId() { return this.builder.id; }
23
+ start(name, input) {
24
+ this.builder.start(name, input);
25
+ this.installInterceptors();
26
+ this.transport.send("trace_started", this.builder.toDict());
27
+ return this;
28
+ }
29
+ end(output, status = TraceStatus.COMPLETED) {
30
+ if (output !== undefined)
31
+ this.output = output;
32
+ const data = this.builder.end(this.output, status);
33
+ this.transport.send("trace_ended", {
34
+ id: data.id,
35
+ ended_at: data.ended_at,
36
+ output: data.output,
37
+ status: data.status,
38
+ total_tokens: data.total_tokens,
39
+ total_cost: data.total_cost,
40
+ });
41
+ this.transport.close();
42
+ }
43
+ addSpan(span) {
44
+ span.trace_id = this.builder.id;
45
+ this.builder.addSpan(span);
46
+ this.transport.send("span_started", span);
47
+ if (span.ended_at) {
48
+ this.transport.send("span_ended", {
49
+ id: span.id,
50
+ ended_at: span.ended_at,
51
+ output: span.output,
52
+ output_tokens: span.output_tokens,
53
+ cost: span.cost,
54
+ error: span.error,
55
+ });
56
+ }
57
+ }
58
+ startSpan(name, spanType = SpanType.LLM_CALL, input, model, parentId) {
59
+ const sb = new SpanBuilder(name, spanType).start();
60
+ sb.setTraceId(this.builder.id);
61
+ if (input !== undefined)
62
+ sb.setInput(input);
63
+ if (model)
64
+ sb.setModel(model);
65
+ if (parentId)
66
+ sb.setParentId(parentId);
67
+ this.transport.send("span_started", sb.toData());
68
+ return sb;
69
+ }
70
+ endSpan(spanBuilder, output, error) {
71
+ const span = spanBuilder.end(output, error);
72
+ span.trace_id = this.builder.id;
73
+ this.builder.addSpan(span);
74
+ this.transport.send("span_ended", {
75
+ id: span.id,
76
+ ended_at: span.ended_at,
77
+ output: span.output,
78
+ output_tokens: span.output_tokens,
79
+ cost: span.cost,
80
+ error: span.error,
81
+ });
82
+ }
83
+ installInterceptors() {
84
+ if (this.interceptorsInstalled)
85
+ return;
86
+ installGeminiInterceptor((span) => this.addSpan(span));
87
+ this.interceptorsInstalled = true;
88
+ }
89
+ }
90
+ export function record(opts) {
91
+ const cfg = getConfig();
92
+ if (!cfg.enabled) {
93
+ // Return a no-op recorder
94
+ return new TraceRecorder(opts);
95
+ }
96
+ return new TraceRecorder(opts);
97
+ }
98
+ export function trace(fn, opts) {
99
+ const cfg = getConfig();
100
+ return (...args) => {
101
+ if (!cfg.enabled)
102
+ return fn(...args);
103
+ const recorder = new TraceRecorder({
104
+ name: opts?.name || fn.name || "anonymous",
105
+ input: opts?.input ?? args,
106
+ metadata: opts?.metadata,
107
+ });
108
+ recorder.start(opts?.name || fn.name || "anonymous", opts?.input ?? args);
109
+ try {
110
+ const result = fn(...args);
111
+ // Handle async functions
112
+ if (result && typeof result.then === "function") {
113
+ return result.then((resolved) => { recorder.end(resolved, TraceStatus.COMPLETED); return resolved; }, (err) => { recorder.end(undefined, TraceStatus.FAILED); throw err; });
114
+ }
115
+ recorder.end(result, TraceStatus.COMPLETED);
116
+ return result;
117
+ }
118
+ catch (err) {
119
+ recorder.end(undefined, TraceStatus.FAILED);
120
+ throw err;
121
+ }
122
+ };
123
+ }
@@ -0,0 +1,74 @@
1
+ export declare enum SpanType {
2
+ LLM_CALL = "llm_call",
3
+ TOOL_CALL = "tool_call",
4
+ TOOL_RESULT = "tool_result",
5
+ REASONING = "reasoning",
6
+ ACTION = "action",
7
+ ERROR = "error",
8
+ FORK_POINT = "fork_point"
9
+ }
10
+ export declare enum TraceStatus {
11
+ RUNNING = "running",
12
+ COMPLETED = "completed",
13
+ FAILED = "failed"
14
+ }
15
+ export interface SpanData {
16
+ id: string;
17
+ trace_id: string;
18
+ parent_id: string | null;
19
+ span_type: string;
20
+ name: string;
21
+ model?: string;
22
+ input?: unknown;
23
+ output?: unknown;
24
+ input_tokens?: number;
25
+ output_tokens?: number;
26
+ cost?: number;
27
+ duration_ms?: number;
28
+ metadata?: Record<string, unknown>;
29
+ started_at: string;
30
+ ended_at?: string;
31
+ error?: string;
32
+ }
33
+ export interface TraceData {
34
+ id: string;
35
+ name?: string;
36
+ input?: unknown;
37
+ output?: unknown;
38
+ status: string;
39
+ total_tokens: number;
40
+ total_cost: number;
41
+ total_duration_ms: number;
42
+ metadata?: Record<string, unknown>;
43
+ started_at: string;
44
+ ended_at?: string;
45
+ spans?: SpanData[];
46
+ project_id?: string;
47
+ }
48
+ export declare class SpanBuilder {
49
+ private data;
50
+ private _startTime?;
51
+ constructor(name: string, spanType: SpanType);
52
+ setModel(model: string): this;
53
+ setInput(input: unknown): this;
54
+ setOutput(output: unknown): this;
55
+ setParentId(id: string): this;
56
+ setTraceId(id: string): this;
57
+ setMetadata(m: Record<string, unknown>): this;
58
+ start(): this;
59
+ end(output?: unknown, error?: string): SpanData;
60
+ get id(): string;
61
+ toData(): SpanData;
62
+ }
63
+ export declare class TraceBuilder {
64
+ private data;
65
+ private _startTime?;
66
+ constructor();
67
+ start(name?: string, input?: unknown): this;
68
+ end(output?: unknown, status?: TraceStatus): TraceData;
69
+ addSpan(span: SpanData): void;
70
+ get id(): string;
71
+ setProjectId(id: string): void;
72
+ setMetadata(m: Record<string, unknown>): void;
73
+ toDict(): TraceData;
74
+ }
package/dist/trace.js ADDED
@@ -0,0 +1,93 @@
1
+ import { genId, nowIso, utcNow } from "./utils.js";
2
+ export var SpanType;
3
+ (function (SpanType) {
4
+ SpanType["LLM_CALL"] = "llm_call";
5
+ SpanType["TOOL_CALL"] = "tool_call";
6
+ SpanType["TOOL_RESULT"] = "tool_result";
7
+ SpanType["REASONING"] = "reasoning";
8
+ SpanType["ACTION"] = "action";
9
+ SpanType["ERROR"] = "error";
10
+ SpanType["FORK_POINT"] = "fork_point";
11
+ })(SpanType || (SpanType = {}));
12
+ export var TraceStatus;
13
+ (function (TraceStatus) {
14
+ TraceStatus["RUNNING"] = "running";
15
+ TraceStatus["COMPLETED"] = "completed";
16
+ TraceStatus["FAILED"] = "failed";
17
+ })(TraceStatus || (TraceStatus = {}));
18
+ export class SpanBuilder {
19
+ data;
20
+ _startTime;
21
+ constructor(name, spanType) {
22
+ this.data = { id: genId(), span_type: spanType, name, trace_id: "", parent_id: null, started_at: nowIso() };
23
+ }
24
+ setModel(model) { this.data.model = model; return this; }
25
+ setInput(input) { this.data.input = input; return this; }
26
+ setOutput(output) { this.data.output = output; return this; }
27
+ setParentId(id) { this.data.parent_id = id; return this; }
28
+ setTraceId(id) { this.data.trace_id = id; return this; }
29
+ setMetadata(m) { this.data.metadata = m; return this; }
30
+ start() {
31
+ this._startTime = utcNow();
32
+ this.data.started_at = this._startTime.toISOString();
33
+ return this;
34
+ }
35
+ end(output, error) {
36
+ const now = utcNow();
37
+ this.data.ended_at = now.toISOString();
38
+ if (output !== undefined)
39
+ this.data.output = output;
40
+ if (error)
41
+ this.data.error = error;
42
+ if (this._startTime) {
43
+ this.data.duration_ms = now.getTime() - this._startTime.getTime();
44
+ }
45
+ return this.data;
46
+ }
47
+ get id() { return this.data.id; }
48
+ toData() { return this.data; }
49
+ }
50
+ export class TraceBuilder {
51
+ data;
52
+ _startTime;
53
+ constructor() {
54
+ this.data = {
55
+ id: genId(),
56
+ status: TraceStatus.RUNNING,
57
+ total_tokens: 0,
58
+ total_cost: 0,
59
+ total_duration_ms: 0,
60
+ started_at: nowIso(),
61
+ spans: [],
62
+ };
63
+ }
64
+ start(name, input) {
65
+ this._startTime = utcNow();
66
+ this.data.started_at = this._startTime.toISOString();
67
+ if (name)
68
+ this.data.name = name;
69
+ if (input !== undefined)
70
+ this.data.input = input;
71
+ return this;
72
+ }
73
+ end(output, status = TraceStatus.COMPLETED) {
74
+ const now = utcNow();
75
+ this.data.ended_at = now.toISOString();
76
+ this.data.status = status;
77
+ if (output !== undefined)
78
+ this.data.output = output;
79
+ if (this._startTime) {
80
+ this.data.total_duration_ms = now.getTime() - this._startTime.getTime();
81
+ }
82
+ return this.data;
83
+ }
84
+ addSpan(span) {
85
+ this.data.spans.push(span);
86
+ this.data.total_tokens += (span.input_tokens || 0) + (span.output_tokens || 0);
87
+ this.data.total_cost += span.cost || 0;
88
+ }
89
+ get id() { return this.data.id; }
90
+ setProjectId(id) { this.data.project_id = id; }
91
+ setMetadata(m) { this.data.metadata = m; }
92
+ toDict() { return this.data; }
93
+ }
@@ -0,0 +1,24 @@
1
+ export interface Transport {
2
+ send(eventType: string, data: Record<string, unknown>): void;
3
+ close(): void;
4
+ }
5
+ export declare class WSTransport implements Transport {
6
+ private ws;
7
+ private connected;
8
+ private backoff;
9
+ private queue;
10
+ connect(): void;
11
+ private reconnect;
12
+ private flushQueue;
13
+ send(eventType: string, data: Record<string, unknown>): void;
14
+ close(): void;
15
+ }
16
+ export declare class HTTPTransport implements Transport {
17
+ private traceData;
18
+ private spans;
19
+ send(eventType: string, data: Record<string, unknown>): void;
20
+ flush(): void;
21
+ private buildSpans;
22
+ close(): void;
23
+ }
24
+ export declare function createTransport(mode?: "ws" | "http" | "auto"): Transport;