retrace-sdk 0.11.3 → 0.11.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 +12 -9
- package/dist/interceptors/openai.js +13 -9
- package/dist/recorder.d.ts +4 -0
- package/dist/recorder.js +11 -7
- package/dist/resume.d.ts +3 -0
- package/dist/resume.js +2 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +10 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SpanType } from "../trace.js";
|
|
2
|
-
import { genId, nowIso, truncateJson } from "../utils.js";
|
|
2
|
+
import { genId, nowIso, truncateJson, wasTruncated } from "../utils.js";
|
|
3
3
|
import { isReplaying, consumeCassetteEntry } from "../replay.js";
|
|
4
4
|
import { emitAnthropicToolCalls, emitAnthropicToolResults, parseToolArgs, resetToolResultDedup, extractToolSchemas, extractSamplingParams } from "./tool-spans.js";
|
|
5
5
|
import { dispatchRegisterOpenSpan, dispatchUnregisterOpenSpan } from "./_dispatch.js";
|
|
@@ -23,15 +23,18 @@ function calcCost(model, inputTokens, outputTokens) {
|
|
|
23
23
|
}
|
|
24
24
|
let originalCreate = null;
|
|
25
25
|
let installed = false;
|
|
26
|
+
// Set SYNCHRONOUSLY before the async import() so a second concurrent install can't double-wrap the
|
|
27
|
+
// prototype. (`installed` is set inside the .then() and is therefore too late to guard the race.)
|
|
28
|
+
let installStarted = false;
|
|
26
29
|
let onSpanCallback = null;
|
|
27
30
|
export function installAnthropicInterceptor(onSpan) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
resetToolResultDedup();
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
31
|
+
// Always refresh the active callback; the prototype PATCH must happen at most once (a synchronous
|
|
32
|
+
// guard so two concurrent installs can't both patch and double-wrap create() → doubled spans).
|
|
33
33
|
onSpanCallback = onSpan;
|
|
34
34
|
resetToolResultDedup();
|
|
35
|
+
if (installStarted)
|
|
36
|
+
return;
|
|
37
|
+
installStarted = true;
|
|
35
38
|
import("@anthropic-ai/sdk").then((anthropicMod) => {
|
|
36
39
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
40
|
const mod = anthropicMod;
|
|
@@ -45,7 +48,7 @@ export function installAnthropicInterceptor(onSpan) {
|
|
|
45
48
|
originalCreate = proto.create;
|
|
46
49
|
proto.create = createPatchedCreate();
|
|
47
50
|
installed = true;
|
|
48
|
-
}).catch(() => { });
|
|
51
|
+
}).catch(() => { installStarted = false; });
|
|
49
52
|
}
|
|
50
53
|
function createPatchedCreate() {
|
|
51
54
|
return async function (...args) {
|
|
@@ -112,7 +115,7 @@ function createPatchedCreate() {
|
|
|
112
115
|
input_tokens: inputTokens, output_tokens: outputTokens,
|
|
113
116
|
cost: calcCost(model, inputTokens, outputTokens),
|
|
114
117
|
duration_ms: durationMs, started_at: startedAt, ended_at: nowIso(),
|
|
115
|
-
metadata: { streaming: true, ...(reason === "partial" ? { partial: true } : {}), ...spanMeta },
|
|
118
|
+
metadata: { streaming: true, ...(reason === "partial" ? { partial: true } : {}), ...(wasTruncated(output) ? { truncated: true } : {}), ...spanMeta },
|
|
116
119
|
};
|
|
117
120
|
onSpanCallback?.(span);
|
|
118
121
|
if (onSpanCallback && reason === "complete") {
|
|
@@ -176,7 +179,7 @@ function createPatchedCreate() {
|
|
|
176
179
|
input_tokens: inputTokens, output_tokens: outputTokens,
|
|
177
180
|
cost: calcCost(model, inputTokens, outputTokens),
|
|
178
181
|
duration_ms: durationMs, started_at: startedAt, ended_at: nowIso(),
|
|
179
|
-
...(Object.keys(spanMeta).length ? { metadata: spanMeta } : {}),
|
|
182
|
+
...(Object.keys(spanMeta).length || wasTruncated(output) ? { metadata: { ...spanMeta, ...(wasTruncated(output) ? { truncated: true } : {}) } } : {}),
|
|
180
183
|
};
|
|
181
184
|
onSpanCallback?.(span);
|
|
182
185
|
// Auto-capture tool usage (tool_use blocks in response, tool_result blocks in input).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SpanType } from "../trace.js";
|
|
2
|
-
import { genId, nowIso, truncateJson } from "../utils.js";
|
|
2
|
+
import { genId, nowIso, truncateJson, wasTruncated } from "../utils.js";
|
|
3
3
|
import { isReplaying, consumeCassetteEntry } from "../replay.js";
|
|
4
4
|
import { getConfig } from "../config.js";
|
|
5
5
|
import { RetraceRateLimitError, RetraceAuthError, RetraceConnectionError } from "../errors.js";
|
|
@@ -61,15 +61,19 @@ function calcCost(model, inputTokens, outputTokens) {
|
|
|
61
61
|
}
|
|
62
62
|
let originalCreate = null;
|
|
63
63
|
let installed = false;
|
|
64
|
+
// Set SYNCHRONOUSLY before the async import() so a second concurrent install can't double-wrap the
|
|
65
|
+
// prototype. (`installed` is set inside the .then() and is therefore too late to guard the race.)
|
|
66
|
+
let installStarted = false;
|
|
64
67
|
let onSpanCallback = null;
|
|
65
68
|
export function installOpenAIInterceptor(onSpan) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
69
|
+
// Always refresh the active callback; the prototype PATCH must happen at most once. The guard is
|
|
70
|
+
// a synchronous flag set before import() so two concurrent installs (e.g. two recorders starting
|
|
71
|
+
// before "openai" resolves) can't both patch and double-wrap create() → doubled spans/billing.
|
|
71
72
|
onSpanCallback = onSpan;
|
|
72
73
|
resetToolResultDedup();
|
|
74
|
+
if (installStarted)
|
|
75
|
+
return;
|
|
76
|
+
installStarted = true;
|
|
73
77
|
import("openai").then((openaiMod) => {
|
|
74
78
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
79
|
const mod = openaiMod;
|
|
@@ -90,7 +94,7 @@ export function installOpenAIInterceptor(onSpan) {
|
|
|
90
94
|
originalCreate = proto.create;
|
|
91
95
|
proto.create = createPatchedCreate();
|
|
92
96
|
installed = true;
|
|
93
|
-
}).catch(() => { });
|
|
97
|
+
}).catch(() => { installStarted = false; });
|
|
94
98
|
}
|
|
95
99
|
function createPatchedCreate() {
|
|
96
100
|
return async function (...args) {
|
|
@@ -172,7 +176,7 @@ function createPatchedCreate() {
|
|
|
172
176
|
input_tokens: inputTokens, output_tokens: outputTokens,
|
|
173
177
|
cost: calcCost(model, inputTokens, outputTokens),
|
|
174
178
|
duration_ms: durationMs, started_at: startedAt, ended_at: nowIso(),
|
|
175
|
-
metadata: { streaming: true, ...(reason === "partial" ? { partial: true } : {}), ...(toolSchemas ? { tool_schemas: toolSchemas } : {}), ...(sampling ? { sampling } : {}) },
|
|
179
|
+
metadata: { streaming: true, ...(reason === "partial" ? { partial: true } : {}), ...(wasTruncated(output) ? { truncated: true } : {}), ...(toolSchemas ? { tool_schemas: toolSchemas } : {}), ...(sampling ? { sampling } : {}) },
|
|
176
180
|
};
|
|
177
181
|
onSpanCallback?.(span);
|
|
178
182
|
if (onSpanCallback && reason === "complete") {
|
|
@@ -250,7 +254,7 @@ function createPatchedCreate() {
|
|
|
250
254
|
duration_ms: durationMs, started_at: startedAt, ended_at: nowIso(),
|
|
251
255
|
...(tokenIds?.length ? { token_ids: tokenIds } : {}),
|
|
252
256
|
...(logprobValues?.length ? { logprobs: logprobValues } : {}),
|
|
253
|
-
...(Object.keys(spanMetadata).length ? { metadata: spanMetadata } : {}),
|
|
257
|
+
...(Object.keys(spanMetadata).length || wasTruncated(output) ? { metadata: { ...spanMetadata, ...(wasTruncated(output) ? { truncated: true } : {}) } } : {}),
|
|
254
258
|
};
|
|
255
259
|
onSpanCallback?.(span);
|
|
256
260
|
// Auto-capture tool usage: tool_result spans from the fed-back tool messages (deduped),
|
package/dist/recorder.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export interface RecordOptions {
|
|
|
10
10
|
sessionId?: string;
|
|
11
11
|
/** When set, spans emitted before this span ID is encountered are suppressed (pre-fork filtering). */
|
|
12
12
|
forkPointSpanId?: string;
|
|
13
|
+
/** 0-based ordinal of the fork-point span among the original ordered spans. Suppression is
|
|
14
|
+
* positional: spans with counter <= index are suppressed, emission starts at index+1. */
|
|
15
|
+
forkPointIndex?: number;
|
|
13
16
|
}
|
|
14
17
|
export declare class TraceRecorder {
|
|
15
18
|
private builder;
|
|
@@ -19,6 +22,7 @@ export declare class TraceRecorder {
|
|
|
19
22
|
private prevFallback;
|
|
20
23
|
private prevFallbackSink;
|
|
21
24
|
private forkPointSpanId;
|
|
25
|
+
private forkPointIndex;
|
|
22
26
|
private forkPointReached;
|
|
23
27
|
private spanCounter;
|
|
24
28
|
output: unknown;
|
package/dist/recorder.js
CHANGED
|
@@ -55,6 +55,7 @@ export class TraceRecorder {
|
|
|
55
55
|
prevFallback = null;
|
|
56
56
|
prevFallbackSink = null;
|
|
57
57
|
forkPointSpanId;
|
|
58
|
+
forkPointIndex;
|
|
58
59
|
forkPointReached = false;
|
|
59
60
|
spanCounter = 0;
|
|
60
61
|
output = undefined;
|
|
@@ -63,8 +64,10 @@ export class TraceRecorder {
|
|
|
63
64
|
this.builder = new TraceBuilder();
|
|
64
65
|
this.transport = getSharedTransport();
|
|
65
66
|
this.forkPointSpanId = opts?.forkPointSpanId;
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
this.forkPointIndex = opts?.forkPointIndex;
|
|
68
|
+
// Suppress pre-fork spans only when BOTH a fork point and its positional index are known;
|
|
69
|
+
// otherwise (normal recording, or a fork command without an index) emit everything.
|
|
70
|
+
this.forkPointReached = !opts?.forkPointSpanId || opts?.forkPointIndex === undefined;
|
|
68
71
|
const cfg = getConfig();
|
|
69
72
|
if (cfg.projectId)
|
|
70
73
|
this.builder.setProjectId(cfg.projectId);
|
|
@@ -160,12 +163,13 @@ export class TraceRecorder {
|
|
|
160
163
|
}
|
|
161
164
|
addSpan(span) {
|
|
162
165
|
this.spanCounter++;
|
|
163
|
-
// Fork
|
|
164
|
-
//
|
|
166
|
+
// Fork-point filtering: during cascade replay suppress the pre-fork spans (the server already
|
|
167
|
+
// has them / they replay from the cassette) and emit only from the fork point onward. The fork
|
|
168
|
+
// point is the (forkPointIndex)-th span (0-based), i.e. the (index+1)-th counted here, so
|
|
169
|
+
// suppress while spanCounter <= index and emit once spanCounter > index. (Previously this
|
|
170
|
+
// compared spanCounter >= 1, which is always true after the increment ⇒ zero suppression.)
|
|
165
171
|
if (!this.forkPointReached) {
|
|
166
|
-
if (this.
|
|
167
|
-
// Use span counter as proxy — the Nth span corresponds to the fork point index.
|
|
168
|
-
// Mark as reached so all subsequent spans pass through.
|
|
172
|
+
if (this.forkPointIndex !== undefined && this.spanCounter > this.forkPointIndex) {
|
|
169
173
|
this.forkPointReached = true;
|
|
170
174
|
}
|
|
171
175
|
else {
|
package/dist/resume.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export interface ResumeCommand {
|
|
|
12
12
|
traceId: string;
|
|
13
13
|
traceName: string;
|
|
14
14
|
forkPointSpanId: string;
|
|
15
|
+
/** 0-based ordinal of the fork-point span among the original ordered spans. Pre-fork spans
|
|
16
|
+
* (counter <= index) are suppressed on re-exec; the server already has them. */
|
|
17
|
+
forkPointIndex?: number;
|
|
15
18
|
modifiedInput: unknown;
|
|
16
19
|
originalArgs?: unknown[];
|
|
17
20
|
}
|
package/dist/resume.js
CHANGED
|
@@ -32,6 +32,7 @@ export async function handleResume(command) {
|
|
|
32
32
|
_cascade_replay: true,
|
|
33
33
|
},
|
|
34
34
|
forkPointSpanId: command.forkPointSpanId,
|
|
35
|
+
forkPointIndex: command.forkPointIndex,
|
|
35
36
|
});
|
|
36
37
|
recorder.start(`Fork: ${command.traceName}`, command.modifiedInput);
|
|
37
38
|
// Determine args for re-execution
|
|
@@ -59,6 +60,7 @@ export function parseResumeMessage(msg) {
|
|
|
59
60
|
traceId: msg.data.traceId,
|
|
60
61
|
traceName: msg.data.traceName,
|
|
61
62
|
forkPointSpanId: msg.data.forkPointSpanId,
|
|
63
|
+
forkPointIndex: msg.data.forkPointIndex,
|
|
62
64
|
modifiedInput: msg.data.modifiedInput,
|
|
63
65
|
originalArgs: msg.data.originalArgs,
|
|
64
66
|
};
|
package/dist/utils.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export declare function utcNow(): Date;
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function shouldSample(rate: number, seed?: string, key?: string): boolean;
|
|
10
10
|
export declare function truncateJson(obj: unknown, maxBytes?: number): unknown;
|
|
11
|
+
/** True if truncateJson(obj, maxBytes) would drop bytes. Used to flag a span's output as truncated
|
|
12
|
+
* so the server refuses to byte-replay it (the replayed value would differ from the original). */
|
|
13
|
+
export declare function wasTruncated(obj: unknown, maxBytes?: number): boolean;
|
|
11
14
|
/** Configure per-span-type truncation limits. */
|
|
12
15
|
export declare function setTruncationLimits(limits: Record<string, number>): void;
|
|
13
16
|
/** Get the truncation limit for a given span type. */
|
package/dist/utils.js
CHANGED
|
@@ -40,6 +40,16 @@ export function truncateJson(obj, maxBytes = 10240) {
|
|
|
40
40
|
return String(obj).slice(0, maxBytes);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
/** True if truncateJson(obj, maxBytes) would drop bytes. Used to flag a span's output as truncated
|
|
44
|
+
* so the server refuses to byte-replay it (the replayed value would differ from the original). */
|
|
45
|
+
export function wasTruncated(obj, maxBytes = 10240) {
|
|
46
|
+
try {
|
|
47
|
+
return Buffer.byteLength(JSON.stringify(obj)) > maxBytes;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return String(obj).length > maxBytes;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
43
53
|
/** Default per-span-type truncation limits (bytes). */
|
|
44
54
|
const DEFAULT_TRUNCATION_LIMITS = {
|
|
45
55
|
llm_call: 51200, // 50KB — LLM prompts can be large
|