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.
- package/dist/interceptors/anthropic.js +52 -0
- package/dist/recorder.d.ts +5 -0
- package/dist/recorder.js +31 -10
- package/dist/resume.d.ts +1 -1
- package/dist/resume.js +30 -31
- package/package.json +1 -1
|
@@ -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;
|
package/dist/recorder.d.ts
CHANGED
|
@@ -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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
50
|
-
|
|
42
|
+
else if (typeof command.modifiedInput === "object" && !Array.isArray(command.modifiedInput)) {
|
|
43
|
+
args = [command.modifiedInput];
|
|
51
44
|
}
|
|
52
|
-
|
|
53
|
-
|
|
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)
|