retrace-sdk 0.3.8 → 0.5.0

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/config.d.ts CHANGED
@@ -5,6 +5,8 @@ export interface Config {
5
5
  projectId: string | undefined;
6
6
  enabled: boolean;
7
7
  sampleRate: number;
8
+ /** Optional seed for deterministic sampling. When set, the same trace name always produces the same sample decision. */
9
+ sampleSeed: string | undefined;
8
10
  }
9
11
  export declare function configure(opts: Partial<Config>): Config;
10
12
  export declare function requireApiKey(): string;
package/dist/config.js CHANGED
@@ -5,6 +5,7 @@ const config = {
5
5
  projectId: process.env.RETRACE_PROJECT_ID || undefined,
6
6
  enabled: !["false", "0"].includes((process.env.RETRACE_ENABLED || "true").toLowerCase()),
7
7
  sampleRate: parseFloat(process.env.RETRACE_SAMPLE_RATE || "1"),
8
+ sampleSeed: process.env.RETRACE_SAMPLE_SEED || undefined,
8
9
  };
9
10
  config.wsUrl = config.baseUrl.replace("https://", "wss://").replace("http://", "ws://");
10
11
  export function configure(opts) {
package/dist/index.d.ts CHANGED
@@ -11,3 +11,4 @@ export { registerResumable, handleResume } from "./resume.js";
11
11
  export type { ResumeCommand } from "./resume.js";
12
12
  export { isReplaying, consumeCassetteEntry, handleReplay } from "./replay.js";
13
13
  export type { CassetteEntry, ReplayCommand } from "./replay.js";
14
+ export { setTraceContext, clearTraceContext, getTraceparent, injectTraceparent, parseTraceparent } from "./traceparent.js";
package/dist/index.js CHANGED
@@ -8,3 +8,4 @@ export { installAnthropicInterceptor, uninstallAnthropicInterceptor } from "./in
8
8
  export { RetraceError, RetraceAuthError, RetraceCreditsExhaustedError, RetraceConnectionError, RetraceRateLimitError } from "./errors.js";
9
9
  export { registerResumable, handleResume } from "./resume.js";
10
10
  export { isReplaying, consumeCassetteEntry, handleReplay } from "./replay.js";
11
+ export { setTraceContext, clearTraceContext, getTraceparent, injectTraceparent, parseTraceparent } from "./traceparent.js";
@@ -34,7 +34,8 @@ export function installAnthropicInterceptor(onSpan) {
34
34
  const Anthropic = mod?.Anthropic || mod?.default;
35
35
  if (!Anthropic)
36
36
  return;
37
- const proto = Anthropic.Messages?.prototype || Object.getPrototypeOf(new Anthropic({ apiKey: "dummy" }).messages);
37
+ // Find Messages prototype without instantiating a client
38
+ const proto = Anthropic.Messages?.prototype || Anthropic.prototype?.messages?.constructor?.prototype;
38
39
  if (!proto?.create)
39
40
  return;
40
41
  originalCreate = proto.create;
@@ -1,7 +1,10 @@
1
1
  import { SpanType } from "../trace.js";
2
2
  import { genId, nowIso, truncateJson } from "../utils.js";
3
3
  import { isReplaying, consumeCassetteEntry } from "../replay.js";
4
- const PRICING = {
4
+ import { getConfig } from "../config.js";
5
+ import { RetraceRateLimitError, RetraceAuthError, RetraceConnectionError } from "../errors.js";
6
+ /** Hardcoded fallback pricing ($/1M tokens: [input, output]). Updated periodically. */
7
+ const FALLBACK_PRICING = {
5
8
  "gpt-5.5-pro": [30.0, 180.0],
6
9
  "gpt-5.5": [5.0, 30.0],
7
10
  "gpt-5.4-pro": [15.0, 90.0],
@@ -20,8 +23,35 @@ const PRICING = {
20
23
  "o4-mini": [1.10, 4.40],
21
24
  "o3-mini": [1.10, 4.40],
22
25
  };
26
+ let livePricing = null;
27
+ let pricingFetchedAt = 0;
28
+ const PRICING_TTL_MS = 3600_000; // Refresh every hour
29
+ /** Fetch live pricing from Retrace API. Falls back to hardcoded on failure. */
30
+ async function fetchPricing() {
31
+ if (livePricing && Date.now() - pricingFetchedAt < PRICING_TTL_MS)
32
+ return livePricing;
33
+ try {
34
+ const cfg = getConfig();
35
+ const res = await fetch(`${cfg.baseUrl}/api/v1/pricing/models`, {
36
+ signal: AbortSignal.timeout(3000),
37
+ });
38
+ if (res.ok) {
39
+ const data = await res.json();
40
+ if (data && typeof data === "object") {
41
+ livePricing = data;
42
+ pricingFetchedAt = Date.now();
43
+ return livePricing;
44
+ }
45
+ }
46
+ }
47
+ catch { /* silent — use fallback */ }
48
+ return FALLBACK_PRICING;
49
+ }
50
+ // Kick off initial fetch (non-blocking)
51
+ fetchPricing().catch(() => { });
23
52
  function calcCost(model, inputTokens, outputTokens) {
24
- for (const [key, p] of Object.entries(PRICING)) {
53
+ const pricing = livePricing || FALLBACK_PRICING;
54
+ for (const [key, p] of Object.entries(pricing)) {
25
55
  if (model.includes(key))
26
56
  return (inputTokens * p[0] + outputTokens * p[1]) / 1_000_000;
27
57
  }
@@ -64,9 +94,18 @@ function createPatchedCreate() {
64
94
  const opts = args[0] || {};
65
95
  const model = opts.model || "unknown";
66
96
  const messages = opts.messages || [];
97
+ const isStreaming = !!opts.stream;
67
98
  const spanId = genId();
68
99
  const startedAt = nowIso();
69
100
  const startMs = Date.now();
101
+ // Capture vision (image_url) and structured output (response_format) metadata
102
+ const hasVision = messages.some((m) => Array.isArray(m.content) && m.content.some((p) => p.type === "image_url"));
103
+ const responseFormat = opts.response_format;
104
+ const spanMetadata = {};
105
+ if (hasVision)
106
+ spanMetadata.vision = true;
107
+ if (responseFormat)
108
+ spanMetadata.structured_output = typeof responseFormat === "object" ? responseFormat.type || "json_schema" : responseFormat;
70
109
  // During replay, return mocked response from cassette instead of calling the real API
71
110
  if (isReplaying()) {
72
111
  const entry = consumeCassetteEntry("openai.chat.completions.create", "llm_call");
@@ -92,6 +131,57 @@ function createPatchedCreate() {
92
131
  }
93
132
  try {
94
133
  const result = await originalCreate.apply(this, args);
134
+ // Streaming response: wrap the async iterator to collect chunks
135
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
+ if (isStreaming && result && typeof result[Symbol.asyncIterator] === "function") {
137
+ const chunks = [];
138
+ let inputTokens = 0;
139
+ let outputTokens = 0;
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ const originalIterator = result[Symbol.asyncIterator]();
142
+ const wrappedStream = {
143
+ [Symbol.asyncIterator]() {
144
+ return {
145
+ async next() {
146
+ const { value, done } = await originalIterator.next();
147
+ if (done) {
148
+ // Stream complete — emit span
149
+ const durationMs = Date.now() - startMs;
150
+ const output = chunks.join("");
151
+ const span = {
152
+ id: spanId, trace_id: "", parent_id: null,
153
+ span_type: SpanType.LLM_CALL, name: "openai.chat.completions.create", model,
154
+ input: truncateJson({ messages: messages.slice(0, 10) }),
155
+ output: truncateJson(output),
156
+ input_tokens: inputTokens, output_tokens: outputTokens,
157
+ cost: calcCost(model, inputTokens, outputTokens),
158
+ duration_ms: durationMs, started_at: startedAt, ended_at: nowIso(),
159
+ metadata: { streaming: true },
160
+ };
161
+ onSpanCallback?.(span);
162
+ return { value: undefined, done: true };
163
+ }
164
+ // Collect content delta
165
+ const delta = value?.choices?.[0]?.delta?.content;
166
+ if (delta)
167
+ chunks.push(delta);
168
+ // Collect usage from final chunk
169
+ if (value?.usage) {
170
+ inputTokens = value.usage.prompt_tokens || 0;
171
+ outputTokens = value.usage.completion_tokens || 0;
172
+ }
173
+ return { value, done: false };
174
+ },
175
+ return() { return originalIterator.return?.() ?? Promise.resolve({ value: undefined, done: true }); },
176
+ throw(e) { return originalIterator.throw?.(e) ?? Promise.reject(e); },
177
+ };
178
+ },
179
+ // Preserve tee/controller methods if present
180
+ ...(result.controller ? { controller: result.controller } : {}),
181
+ };
182
+ return wrappedStream;
183
+ }
184
+ // Non-streaming response
95
185
  const durationMs = Date.now() - startMs;
96
186
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
187
  const res = result;
@@ -101,11 +191,12 @@ function createPatchedCreate() {
101
191
  const span = {
102
192
  id: spanId, trace_id: "", parent_id: null,
103
193
  span_type: SpanType.LLM_CALL, name: "openai.chat.completions.create", model,
104
- input: truncateJson({ messages: messages.slice(0, 10) }),
194
+ input: truncateJson({ messages: messages.slice(0, 10), ...(responseFormat ? { response_format: responseFormat } : {}) }),
105
195
  output: truncateJson(output),
106
196
  input_tokens: inputTokens, output_tokens: outputTokens,
107
197
  cost: calcCost(model, inputTokens, outputTokens),
108
198
  duration_ms: durationMs, started_at: startedAt, ended_at: nowIso(),
199
+ ...(Object.keys(spanMetadata).length ? { metadata: spanMetadata } : {}),
109
200
  };
110
201
  onSpanCallback?.(span);
111
202
  return result;
@@ -120,6 +211,16 @@ function createPatchedCreate() {
120
211
  error: err instanceof Error ? err.message : String(err),
121
212
  };
122
213
  onSpanCallback?.(span);
214
+ // Wrap provider errors in typed Retrace exceptions for user-facing clarity
215
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
216
+ const status = err?.status || err?.response?.status;
217
+ if (status === 429)
218
+ throw new RetraceRateLimitError(parseInt(err?.headers?.["retry-after"] || "60", 10));
219
+ if (status === 401 || status === 403)
220
+ throw new RetraceAuthError(`OpenAI auth failed: ${err.message}`);
221
+ if (err?.message?.includes("ECONNREFUSED") || err?.message?.includes("fetch failed")) {
222
+ throw new RetraceConnectionError(`OpenAI connection failed: ${err.message}`);
223
+ }
123
224
  throw err;
124
225
  }
125
226
  };
package/dist/recorder.js CHANGED
@@ -1,14 +1,20 @@
1
1
  import { getConfig, requireApiKey } from "./config.js";
2
2
  import { SpanBuilder, SpanType, TraceBuilder, TraceStatus } from "./trace.js";
3
3
  import { createTransport } from "./transport.js";
4
+ import { shouldSample } from "./utils.js";
4
5
  import { installGeminiInterceptor } from "./interceptors/gemini.js";
5
6
  import { installOpenAIInterceptor } from "./interceptors/openai.js";
6
7
  import { installAnthropicInterceptor } from "./interceptors/anthropic.js";
7
8
  // Shared transport — stays open across multiple traces for resume/replay listening
8
9
  let sharedTransport = null;
9
10
  function getSharedTransport() {
10
- if (!sharedTransport)
11
+ if (!sharedTransport) {
11
12
  sharedTransport = createTransport();
13
+ // Flush pending data before process exits
14
+ if (typeof process !== "undefined") {
15
+ process.on("beforeExit", () => { sharedTransport?.close(); });
16
+ }
17
+ }
12
18
  return sharedTransport;
13
19
  }
14
20
  export class TraceRecorder {
@@ -103,17 +109,17 @@ export class TraceRecorder {
103
109
  }
104
110
  export function record(opts) {
105
111
  const cfg = getConfig();
106
- if (!cfg.enabled || Math.random() > cfg.sampleRate) {
107
- // Return a no-op that silently swallows all method calls
108
- const methods = new Set(["start", "end", "startSpan", "endSpan", "addSpan"]);
109
- const noop = {};
110
- return new Proxy(noop, {
111
- get: (_t, prop) => {
112
- if (typeof prop === "string" && methods.has(prop))
113
- return () => noop;
114
- return undefined;
115
- },
116
- });
112
+ 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
+ };
117
123
  }
118
124
  return new TraceRecorder(opts);
119
125
  }
@@ -126,7 +132,7 @@ export function trace(fn, opts) {
126
132
  });
127
133
  }
128
134
  return (...args) => {
129
- if (!cfg.enabled || Math.random() > cfg.sampleRate)
135
+ if (!cfg.enabled || !shouldSample(cfg.sampleRate, cfg.sampleSeed, opts?.name || fn.name))
130
136
  return fn(...args);
131
137
  const recorder = new TraceRecorder({
132
138
  name: opts?.name || fn.name || "anonymous",
package/dist/replay.d.ts CHANGED
@@ -25,18 +25,21 @@ export interface ReplayCommand {
25
25
  cassette: CassetteEntry[];
26
26
  }
27
27
  /**
28
- * Check if a replay is currently active.
28
+ * Check if a replay is currently active in this async context.
29
29
  */
30
30
  export declare function isReplaying(): boolean;
31
31
  /**
32
32
  * Get the next cassette entry matching a span name and type.
33
33
  * Uses sequential matching with name-based fallback for deterministic replay.
34
+ * Thread-safe: each async context has its own pointer.
34
35
  */
35
36
  export declare function consumeCassetteEntry(name: string, spanType: string): CassetteEntry | null;
36
37
  /**
37
38
  * Handle a replay command from the server.
39
+ * Returns a Promise that resolves when replay completes or rejects on failure.
40
+ * Uses AsyncLocalStorage for per-context cassette isolation.
38
41
  */
39
- export declare function handleReplay(command: ReplayCommand): boolean;
42
+ export declare function handleReplay(command: ReplayCommand): Promise<boolean>;
40
43
  export declare function parseReplayMessage(msg: {
41
44
  type: string;
42
45
  data?: unknown;
package/dist/replay.js CHANGED
@@ -10,50 +10,50 @@
10
10
  * This enables one-click reproduction of any production trace locally.
11
11
  */
12
12
  import { getResumable } from "./resume.js";
13
- // Global cassette state for the current replay session
14
- let activeCassette = null;
15
- let cassettePointer = 0;
13
+ import { AsyncLocalStorage } from "async_hooks";
14
+ const cassetteStorage = new AsyncLocalStorage();
16
15
  /**
17
- * Check if a replay is currently active.
16
+ * Check if a replay is currently active in this async context.
18
17
  */
19
18
  export function isReplaying() {
20
- return activeCassette !== null;
19
+ return cassetteStorage.getStore() !== undefined;
21
20
  }
22
21
  /**
23
22
  * Get the next cassette entry matching a span name and type.
24
23
  * Uses sequential matching with name-based fallback for deterministic replay.
24
+ * Thread-safe: each async context has its own pointer.
25
25
  */
26
26
  export function consumeCassetteEntry(name, spanType) {
27
- if (!activeCassette)
27
+ const ctx = cassetteStorage.getStore();
28
+ if (!ctx)
28
29
  return null;
29
30
  // Primary: sequential pointer (deterministic order)
30
- if (cassettePointer < activeCassette.length) {
31
- const entry = activeCassette[cassettePointer];
31
+ if (ctx.pointer < ctx.cassette.length) {
32
+ const entry = ctx.cassette[ctx.pointer];
32
33
  if (entry.name === name && entry.span_type === spanType) {
33
- cassettePointer++;
34
+ ctx.pointer++;
34
35
  return entry;
35
36
  }
36
37
  }
37
38
  // Fallback: search by name + type from current pointer forward
38
- for (let i = cassettePointer; i < activeCassette.length; i++) {
39
- if (activeCassette[i].name === name && activeCassette[i].span_type === spanType) {
40
- cassettePointer = i + 1;
41
- return activeCassette[i];
39
+ for (let i = ctx.pointer; i < ctx.cassette.length; i++) {
40
+ if (ctx.cassette[i].name === name && ctx.cassette[i].span_type === spanType) {
41
+ ctx.pointer = i + 1;
42
+ return ctx.cassette[i];
42
43
  }
43
44
  }
44
45
  return null;
45
46
  }
46
47
  /**
47
48
  * Handle a replay command from the server.
49
+ * Returns a Promise that resolves when replay completes or rejects on failure.
50
+ * Uses AsyncLocalStorage for per-context cassette isolation.
48
51
  */
49
- export function handleReplay(command) {
52
+ export async function handleReplay(command) {
50
53
  const fn = getResumable(command.traceName);
51
54
  if (!fn)
52
55
  return false;
53
- // Set up cassette
54
- activeCassette = command.cassette;
55
- cassettePointer = 0;
56
- (async () => {
56
+ return cassetteStorage.run({ cassette: command.cassette, pointer: 0 }, async () => {
57
57
  try {
58
58
  const { TraceRecorder } = await import("./recorder.js");
59
59
  const { TraceStatus } = await import("./trace.js");
@@ -72,17 +72,13 @@ export function handleReplay(command) {
72
72
  : Array.isArray(command.input) ? command.input : [command.input];
73
73
  const result = await Promise.resolve(fn(...args));
74
74
  recorder.end(result, TraceStatus.COMPLETED);
75
+ return true;
75
76
  }
76
77
  catch (err) {
77
78
  console.error("[retrace] Deterministic replay failed:", err);
79
+ return false;
78
80
  }
79
- finally {
80
- // Clean up cassette state
81
- activeCassette = null;
82
- cassettePointer = 0;
83
- }
84
- })();
85
- return true;
81
+ });
86
82
  }
87
83
  export function parseReplayMessage(msg) {
88
84
  if (msg.type !== "replay" || !msg.data)
package/dist/resume.js CHANGED
@@ -49,7 +49,7 @@ export function handleResume(command) {
49
49
  catch (err) {
50
50
  console.error("[retrace] Cascade replay failed:", err);
51
51
  }
52
- })();
52
+ })().catch((err) => console.error("[retrace] Replay IIFE unhandled:", err));
53
53
  return true;
54
54
  }
55
55
  export function parseResumeMessage(msg) {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * W3C Trace Context (traceparent) propagation.
3
+ * Format: 00-{trace_id_32hex}-{parent_id_16hex}-{flags_2hex}
4
+ *
5
+ * This enables distributed tracing across service boundaries.
6
+ * When a traced function makes HTTP calls, the traceparent header
7
+ * is injected so downstream services can correlate their spans.
8
+ */
9
+ /** Set the active trace context for outgoing requests. */
10
+ export declare function setTraceContext(traceId: string, spanId: string): void;
11
+ /** Clear the active trace context. */
12
+ export declare function clearTraceContext(): void;
13
+ /** Get the current traceparent header value, or null if no active trace. */
14
+ export declare function getTraceparent(): string | null;
15
+ /**
16
+ * Inject traceparent into a headers object (for fetch/axios/http calls).
17
+ * Returns the headers with traceparent added if a trace is active.
18
+ */
19
+ export declare function injectTraceparent(headers?: Record<string, string>): Record<string, string>;
20
+ /**
21
+ * Parse an incoming traceparent header.
22
+ * Returns { traceId, parentId, sampled } or null if invalid.
23
+ */
24
+ export declare function parseTraceparent(header: string): {
25
+ traceId: string;
26
+ parentId: string;
27
+ sampled: boolean;
28
+ } | null;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * W3C Trace Context (traceparent) propagation.
3
+ * Format: 00-{trace_id_32hex}-{parent_id_16hex}-{flags_2hex}
4
+ *
5
+ * This enables distributed tracing across service boundaries.
6
+ * When a traced function makes HTTP calls, the traceparent header
7
+ * is injected so downstream services can correlate their spans.
8
+ */
9
+ let _currentTraceId = null;
10
+ let _currentSpanId = null;
11
+ /** Set the active trace context for outgoing requests. */
12
+ export function setTraceContext(traceId, spanId) {
13
+ // Convert UUID format to 32-hex (remove dashes)
14
+ _currentTraceId = traceId.replace(/-/g, "");
15
+ // Take first 16 chars of span ID as parent span
16
+ _currentSpanId = spanId.replace(/-/g, "").slice(0, 16);
17
+ }
18
+ /** Clear the active trace context. */
19
+ export function clearTraceContext() {
20
+ _currentTraceId = null;
21
+ _currentSpanId = null;
22
+ }
23
+ /** Get the current traceparent header value, or null if no active trace. */
24
+ export function getTraceparent() {
25
+ if (!_currentTraceId || !_currentSpanId)
26
+ return null;
27
+ // version-trace_id-parent_id-flags (01 = sampled)
28
+ return `00-${_currentTraceId}-${_currentSpanId}-01`;
29
+ }
30
+ /**
31
+ * Inject traceparent into a headers object (for fetch/axios/http calls).
32
+ * Returns the headers with traceparent added if a trace is active.
33
+ */
34
+ export function injectTraceparent(headers = {}) {
35
+ const tp = getTraceparent();
36
+ if (tp) {
37
+ headers["traceparent"] = tp;
38
+ }
39
+ return headers;
40
+ }
41
+ /**
42
+ * Parse an incoming traceparent header.
43
+ * Returns { traceId, parentId, sampled } or null if invalid.
44
+ */
45
+ export function parseTraceparent(header) {
46
+ const parts = header.split("-");
47
+ if (parts.length !== 4 || parts[0] !== "00")
48
+ return null;
49
+ if (parts[1].length !== 32 || parts[2].length !== 16)
50
+ return null;
51
+ return {
52
+ traceId: parts[1],
53
+ parentId: parts[2],
54
+ sampled: parts[3] === "01",
55
+ };
56
+ }
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,16 @@
1
1
  export declare function genId(): string;
2
2
  export declare function nowIso(): string;
3
3
  export declare function utcNow(): Date;
4
+ /**
5
+ * Deterministic sampling decision using FNV-1a hash.
6
+ * When a seed is provided, the same (seed + key) always produces the same decision.
7
+ * Without a seed, falls back to Math.random() for backward compatibility.
8
+ */
9
+ export declare function shouldSample(rate: number, seed?: string, key?: string): boolean;
4
10
  export declare function truncateJson(obj: unknown, maxBytes?: number): unknown;
11
+ /** Configure per-span-type truncation limits. */
12
+ export declare function setTruncationLimits(limits: Record<string, number>): void;
13
+ /** Get the truncation limit for a given span type. */
14
+ export declare function getTruncationLimit(spanType: string): number;
15
+ /** Truncate payload based on span type. */
16
+ export declare function truncateForSpanType(obj: unknown, spanType: string): unknown;
package/dist/utils.js CHANGED
@@ -8,6 +8,27 @@ export function nowIso() {
8
8
  export function utcNow() {
9
9
  return new Date();
10
10
  }
11
+ /**
12
+ * Deterministic sampling decision using FNV-1a hash.
13
+ * When a seed is provided, the same (seed + key) always produces the same decision.
14
+ * Without a seed, falls back to Math.random() for backward compatibility.
15
+ */
16
+ export function shouldSample(rate, seed, key) {
17
+ if (rate >= 1)
18
+ return true;
19
+ if (rate <= 0)
20
+ return false;
21
+ if (!seed)
22
+ return Math.random() < rate;
23
+ // FNV-1a hash of seed+key → deterministic float in [0,1)
24
+ const input = `${seed}:${key || Date.now()}`;
25
+ let hash = 2166136261;
26
+ for (let i = 0; i < input.length; i++) {
27
+ hash ^= input.charCodeAt(i);
28
+ hash = Math.imul(hash, 16777619);
29
+ }
30
+ return ((hash >>> 0) / 4294967296) < rate;
31
+ }
11
32
  export function truncateJson(obj, maxBytes = 10240) {
12
33
  try {
13
34
  const s = JSON.stringify(obj);
@@ -19,3 +40,25 @@ export function truncateJson(obj, maxBytes = 10240) {
19
40
  return String(obj).slice(0, maxBytes);
20
41
  }
21
42
  }
43
+ /** Default per-span-type truncation limits (bytes). */
44
+ const DEFAULT_TRUNCATION_LIMITS = {
45
+ llm_call: 51200, // 50KB — LLM prompts can be large
46
+ tool_call: 10240, // 10KB
47
+ tool_result: 10240, // 10KB
48
+ reasoning: 20480, // 20KB
49
+ action: 5120, // 5KB
50
+ error: 5120, // 5KB
51
+ };
52
+ let customTruncationLimits = {};
53
+ /** Configure per-span-type truncation limits. */
54
+ export function setTruncationLimits(limits) {
55
+ customTruncationLimits = limits;
56
+ }
57
+ /** Get the truncation limit for a given span type. */
58
+ export function getTruncationLimit(spanType) {
59
+ return customTruncationLimits[spanType] || DEFAULT_TRUNCATION_LIMITS[spanType] || 10240;
60
+ }
61
+ /** Truncate payload based on span type. */
62
+ export function truncateForSpanType(obj, spanType) {
63
+ return truncateJson(obj, getTruncationLimit(spanType));
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retrace-sdk",
3
- "version": "0.3.8",
3
+ "version": "0.5.0",
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",