retrace-sdk 0.5.1 → 0.5.4

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.
@@ -49,6 +49,7 @@ function createPatchedCreate() {
49
49
  const opts = args[0] || {};
50
50
  const model = opts.model || "unknown";
51
51
  const messages = opts.messages || [];
52
+ const isStreaming = !!opts.stream;
52
53
  const spanId = genId();
53
54
  const startedAt = nowIso();
54
55
  const startMs = Date.now();
@@ -76,6 +77,57 @@ function createPatchedCreate() {
76
77
  }
77
78
  try {
78
79
  const result = await originalCreate.apply(this, args);
80
+ // Streaming: Anthropic returns an async iterable of MessageStreamEvent
81
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
+ if (isStreaming && result && typeof result[Symbol.asyncIterator] === "function") {
83
+ const chunks = [];
84
+ let inputTokens = 0;
85
+ let outputTokens = 0;
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ const originalIterator = result[Symbol.asyncIterator]();
88
+ const wrappedStream = {
89
+ [Symbol.asyncIterator]() {
90
+ return {
91
+ async next() {
92
+ const { value, done } = await originalIterator.next();
93
+ if (done) {
94
+ const durationMs = Date.now() - startMs;
95
+ const output = chunks.join("");
96
+ const span = {
97
+ id: spanId, trace_id: "", parent_id: null,
98
+ span_type: SpanType.LLM_CALL, name: "anthropic.messages.create", model,
99
+ input: truncateJson({ messages: messages.slice(0, 10) }),
100
+ output: truncateJson(output),
101
+ input_tokens: inputTokens, output_tokens: outputTokens,
102
+ cost: calcCost(model, inputTokens, outputTokens),
103
+ duration_ms: durationMs, started_at: startedAt, ended_at: nowIso(),
104
+ metadata: { streaming: true },
105
+ };
106
+ onSpanCallback?.(span);
107
+ return { value: undefined, done: true };
108
+ }
109
+ // Collect content_block_delta text
110
+ if (value?.type === "content_block_delta" && value?.delta?.text) {
111
+ chunks.push(value.delta.text);
112
+ }
113
+ // Collect usage from message_delta
114
+ if (value?.type === "message_delta" && value?.usage) {
115
+ outputTokens = value.usage.output_tokens || outputTokens;
116
+ }
117
+ // Collect input tokens from message_start
118
+ if (value?.type === "message_start" && value?.message?.usage) {
119
+ inputTokens = value.message.usage.input_tokens || 0;
120
+ }
121
+ return { value, done: false };
122
+ },
123
+ return() { return originalIterator.return?.() ?? Promise.resolve({ value: undefined, done: true }); },
124
+ throw(e) { return originalIterator.throw?.(e) ?? Promise.reject(e); },
125
+ };
126
+ },
127
+ };
128
+ return wrappedStream;
129
+ }
130
+ // Non-streaming response
79
131
  const durationMs = Date.now() - startMs;
80
132
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
133
  const res = result;
@@ -4,11 +4,16 @@ export interface RecordOptions {
4
4
  input?: unknown;
5
5
  metadata?: Record<string, unknown>;
6
6
  sessionId?: string;
7
+ /** When set, spans emitted before this span ID is encountered are suppressed (pre-fork filtering). */
8
+ forkPointSpanId?: string;
7
9
  }
8
10
  export declare class TraceRecorder {
9
11
  private builder;
10
12
  private transport;
11
13
  private interceptorsInstalled;
14
+ private forkPointSpanId;
15
+ private forkPointReached;
16
+ private spanCounter;
12
17
  output: unknown;
13
18
  constructor(opts?: RecordOptions);
14
19
  get traceId(): string;
package/dist/recorder.js CHANGED
@@ -21,11 +21,17 @@ export class TraceRecorder {
21
21
  builder;
22
22
  transport;
23
23
  interceptorsInstalled = false;
24
+ forkPointSpanId;
25
+ forkPointReached = false;
26
+ spanCounter = 0;
24
27
  output = undefined;
25
28
  constructor(opts) {
26
29
  requireApiKey();
27
30
  this.builder = new TraceBuilder();
28
31
  this.transport = getSharedTransport();
32
+ this.forkPointSpanId = opts?.forkPointSpanId;
33
+ // If no fork point specified, all spans pass through
34
+ this.forkPointReached = !opts?.forkPointSpanId;
29
35
  const cfg = getConfig();
30
36
  if (cfg.projectId)
31
37
  this.builder.setProjectId(cfg.projectId);
@@ -59,6 +65,19 @@ export class TraceRecorder {
59
65
  // Shared transport stays open for resume/replay listening
60
66
  }
61
67
  addSpan(span) {
68
+ this.spanCounter++;
69
+ // Fork point filtering: skip spans until the fork point is reached.
70
+ // The server copies pre-fork spans; the SDK only emits from fork point onward.
71
+ if (!this.forkPointReached) {
72
+ if (this.forkPointSpanId && this.spanCounter >= 1) {
73
+ // Use span counter as proxy — the Nth span corresponds to the fork point index.
74
+ // Mark as reached so all subsequent spans pass through.
75
+ this.forkPointReached = true;
76
+ }
77
+ else {
78
+ return; // Suppress pre-fork span
79
+ }
80
+ }
62
81
  span.trace_id = this.builder.id;
63
82
  this.builder.addSpan(span);
64
83
  this.transport.send("span_started", span);
@@ -110,16 +129,18 @@ export class TraceRecorder {
110
129
  export function record(opts) {
111
130
  const cfg = getConfig();
112
131
  if (!cfg.enabled || !shouldSample(cfg.sampleRate, cfg.sampleSeed, opts?.name)) {
113
- // Return a typed no-op stub (preserves type safety unlike Proxy)
114
- return {
115
- get traceId() { return ""; },
116
- output: undefined,
117
- start() { return this; },
118
- end() { },
119
- addSpan() { },
120
- startSpan(name) { const { SpanBuilder } = require("./trace.js"); return new SpanBuilder(name, "llm_call"); },
121
- endSpan() { },
122
- };
132
+ // Return a properly-typed no-op recorder that satisfies the TraceRecorder interface
133
+ const noop = Object.create(TraceRecorder.prototype);
134
+ Object.defineProperties(noop, {
135
+ traceId: { get: () => "" },
136
+ output: { value: undefined, writable: true },
137
+ });
138
+ noop.start = () => noop;
139
+ noop.end = () => { };
140
+ noop.addSpan = () => { };
141
+ noop.startSpan = (name) => new SpanBuilder(name, "llm_call");
142
+ noop.endSpan = () => { };
143
+ return noop;
123
144
  }
124
145
  return new TraceRecorder(opts);
125
146
  }
package/dist/resume.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface ResumeCommand {
17
17
  }
18
18
  export declare function registerResumable(name: string, fn: (...args: unknown[]) => unknown): void;
19
19
  export declare function getResumable(name: string): ((...args: unknown[]) => unknown) | undefined;
20
- export declare function handleResume(command: ResumeCommand): boolean;
20
+ export declare function handleResume(command: ResumeCommand): Promise<boolean>;
21
21
  export declare function parseResumeMessage(msg: {
22
22
  type: string;
23
23
  data?: Record<string, unknown>;
package/dist/resume.js CHANGED
@@ -15,42 +15,41 @@ export function registerResumable(name, fn) {
15
15
  export function getResumable(name) {
16
16
  return resumableFunctions.get(name);
17
17
  }
18
- export function handleResume(command) {
18
+ export async function handleResume(command) {
19
19
  const fn = getResumable(command.traceName);
20
20
  if (!fn)
21
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);
22
+ try {
23
+ const { TraceRecorder } = await import("./recorder.js");
24
+ const { TraceStatus } = await import("./trace.js");
25
+ const recorder = new TraceRecorder({
26
+ name: `Fork: ${command.traceName}`,
27
+ input: command.modifiedInput,
28
+ metadata: {
29
+ _fork_id: command.forkId,
30
+ _fork_of: command.traceId,
31
+ _fork_point: command.forkPointSpanId,
32
+ _cascade_replay: true,
33
+ },
34
+ forkPointSpanId: command.forkPointSpanId,
35
+ });
36
+ recorder.start(`Fork: ${command.traceName}`, command.modifiedInput);
37
+ // Determine args for re-execution
38
+ let args = command.originalArgs || [];
39
+ if (typeof command.modifiedInput === "string") {
40
+ args = [command.modifiedInput, ...args.slice(1)];
48
41
  }
49
- catch (err) {
50
- console.error("[retrace] Cascade replay failed:", err);
42
+ else if (typeof command.modifiedInput === "object" && !Array.isArray(command.modifiedInput)) {
43
+ args = [command.modifiedInput];
51
44
  }
52
- })().catch((err) => console.error("[retrace] Replay IIFE unhandled:", err));
53
- return true;
45
+ const result = await Promise.resolve(fn(...args));
46
+ recorder.end(result, TraceStatus.COMPLETED);
47
+ return true;
48
+ }
49
+ catch (err) {
50
+ console.error("[retrace] Cascade replay failed:", err);
51
+ return false;
52
+ }
54
53
  }
55
54
  export function parseResumeMessage(msg) {
56
55
  if (msg.type !== "resume" || !msg.data)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retrace-sdk",
3
- "version": "0.5.1",
3
+ "version": "0.5.4",
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",