retrace-sdk 0.5.2 → 0.6.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/README.md CHANGED
@@ -111,6 +111,12 @@ configure({ apiKey: "rt_live_...", sampleRate: 0.1 }); // Record 10% of traces
111
111
 
112
112
  - **Fixed** — OpenAI interceptor no longer creates dummy client instance to find prototype
113
113
 
114
+ ### 0.6.0
115
+
116
+ - **Token ID capture** — Stores output token IDs + logprobs from OpenAI responses (enables speculative decoding during replay)
117
+ - **SpanData extended** — New `token_ids` and `logprobs` fields on SpanData interface
118
+ - **Shared schema** — SpanInputSchema updated with `token_ids` and `logprobs` optional arrays
119
+
114
120
  ### 0.2.1
115
121
 
116
122
  - **Offline buffer** — stores up to 1000 messages when WebSocket disconnects, flushes on reconnect
@@ -188,6 +188,12 @@ function createPatchedCreate() {
188
188
  const inputTokens = res?.usage?.prompt_tokens || 0;
189
189
  const outputTokens = res?.usage?.completion_tokens || 0;
190
190
  const output = res?.choices?.[0]?.message?.content || "";
191
+ // Extract token IDs and logprobs if available (requires logprobs: true in request)
192
+ const choiceLogprobs = res?.choices?.[0]?.logprobs?.content;
193
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
194
+ const tokenIds = choiceLogprobs?.map((t) => t.token_id ?? t.top_logprobs?.[0]?.token_id).filter(Boolean);
195
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
196
+ const logprobValues = choiceLogprobs?.map((t) => t.logprob).filter((v) => v !== undefined);
191
197
  const span = {
192
198
  id: spanId, trace_id: "", parent_id: null,
193
199
  span_type: SpanType.LLM_CALL, name: "openai.chat.completions.create", model,
@@ -196,6 +202,8 @@ function createPatchedCreate() {
196
202
  input_tokens: inputTokens, output_tokens: outputTokens,
197
203
  cost: calcCost(model, inputTokens, outputTokens),
198
204
  duration_ms: durationMs, started_at: startedAt, ended_at: nowIso(),
205
+ ...(tokenIds?.length ? { token_ids: tokenIds } : {}),
206
+ ...(logprobValues?.length ? { logprobs: logprobValues } : {}),
199
207
  ...(Object.keys(spanMetadata).length ? { metadata: spanMetadata } : {}),
200
208
  };
201
209
  onSpanCallback?.(span);
package/dist/recorder.js CHANGED
@@ -129,16 +129,18 @@ export class TraceRecorder {
129
129
  export function record(opts) {
130
130
  const cfg = getConfig();
131
131
  if (!cfg.enabled || !shouldSample(cfg.sampleRate, cfg.sampleSeed, opts?.name)) {
132
- // Return a typed no-op stub (preserves type safety unlike Proxy)
133
- return {
134
- get traceId() { return ""; },
135
- output: undefined,
136
- start() { return this; },
137
- end() { },
138
- addSpan() { },
139
- startSpan(name) { const { SpanBuilder } = require("./trace.js"); return new SpanBuilder(name, "llm_call"); },
140
- endSpan() { },
141
- };
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;
142
144
  }
143
145
  return new TraceRecorder(opts);
144
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,46 +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
- // Signal the recorder to skip spans until the fork point is reached.
37
- // Pre-fork spans are copied server-side; the SDK only needs to emit
38
- // spans from the fork point onward.
39
- forkPointSpanId: command.forkPointSpanId,
40
- });
41
- recorder.start(`Fork: ${command.traceName}`, command.modifiedInput);
42
- // Determine args for re-execution
43
- let args = command.originalArgs || [];
44
- if (typeof command.modifiedInput === "string") {
45
- args = [command.modifiedInput, ...args.slice(1)];
46
- }
47
- else if (typeof command.modifiedInput === "object" && !Array.isArray(command.modifiedInput)) {
48
- args = [command.modifiedInput];
49
- }
50
- const result = await Promise.resolve(fn(...args));
51
- 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)];
52
41
  }
53
- catch (err) {
54
- console.error("[retrace] Cascade replay failed:", err);
42
+ else if (typeof command.modifiedInput === "object" && !Array.isArray(command.modifiedInput)) {
43
+ args = [command.modifiedInput];
55
44
  }
56
- })().catch((err) => console.error("[retrace] Replay IIFE unhandled:", err));
57
- 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
+ }
58
53
  }
59
54
  export function parseResumeMessage(msg) {
60
55
  if (msg.type !== "resume" || !msg.data)
package/dist/trace.d.ts CHANGED
@@ -30,6 +30,8 @@ export interface SpanData {
30
30
  started_at: string;
31
31
  ended_at?: string;
32
32
  error?: string;
33
+ token_ids?: number[];
34
+ logprobs?: number[];
33
35
  }
34
36
  export interface TraceData {
35
37
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retrace-sdk",
3
- "version": "0.5.2",
3
+ "version": "0.6.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",