@traceai/codex-sdk 0.1.0 → 0.1.1

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.
Files changed (55) hide show
  1. package/README.md +14 -6
  2. package/dist/esm/codexWrapper.d.ts +5 -0
  3. package/dist/esm/codexWrapper.d.ts.map +1 -0
  4. package/dist/esm/codexWrapper.js +63 -0
  5. package/dist/esm/codexWrapper.js.map +1 -0
  6. package/dist/esm/instrumentation.d.ts +9 -5
  7. package/dist/esm/instrumentation.d.ts.map +1 -1
  8. package/dist/esm/instrumentation.js +40 -31
  9. package/dist/esm/instrumentation.js.map +1 -1
  10. package/dist/esm/spans.js +1 -1
  11. package/dist/esm/spans.js.map +1 -1
  12. package/dist/esm/threadWrapper.d.ts.map +1 -1
  13. package/dist/esm/threadWrapper.js +38 -16
  14. package/dist/esm/threadWrapper.js.map +1 -1
  15. package/dist/esm/version.d.ts +1 -1
  16. package/dist/esm/version.js +1 -1
  17. package/dist/esm/version.js.map +1 -1
  18. package/dist/esnext/codexWrapper.d.ts +5 -0
  19. package/dist/esnext/codexWrapper.d.ts.map +1 -0
  20. package/dist/esnext/codexWrapper.js +65 -0
  21. package/dist/esnext/codexWrapper.js.map +1 -0
  22. package/dist/esnext/instrumentation.d.ts +9 -5
  23. package/dist/esnext/instrumentation.d.ts.map +1 -1
  24. package/dist/esnext/instrumentation.js +40 -31
  25. package/dist/esnext/instrumentation.js.map +1 -1
  26. package/dist/esnext/spans.js +1 -1
  27. package/dist/esnext/spans.js.map +1 -1
  28. package/dist/esnext/threadWrapper.d.ts.map +1 -1
  29. package/dist/esnext/threadWrapper.js +39 -16
  30. package/dist/esnext/threadWrapper.js.map +1 -1
  31. package/dist/esnext/version.d.ts +1 -1
  32. package/dist/esnext/version.js +1 -1
  33. package/dist/esnext/version.js.map +1 -1
  34. package/dist/src/codexWrapper.d.ts +5 -0
  35. package/dist/src/codexWrapper.d.ts.map +1 -0
  36. package/dist/src/codexWrapper.js +68 -0
  37. package/dist/src/codexWrapper.js.map +1 -0
  38. package/dist/src/instrumentation.d.ts +9 -5
  39. package/dist/src/instrumentation.d.ts.map +1 -1
  40. package/dist/src/instrumentation.js +40 -31
  41. package/dist/src/instrumentation.js.map +1 -1
  42. package/dist/src/spans.js +1 -1
  43. package/dist/src/spans.js.map +1 -1
  44. package/dist/src/threadWrapper.d.ts.map +1 -1
  45. package/dist/src/threadWrapper.js +39 -16
  46. package/dist/src/threadWrapper.js.map +1 -1
  47. package/dist/src/version.d.ts +1 -1
  48. package/dist/src/version.js +1 -1
  49. package/dist/src/version.js.map +1 -1
  50. package/package.json +6 -5
  51. package/src/codexWrapper.ts +71 -0
  52. package/src/instrumentation.ts +39 -27
  53. package/src/spans.ts +1 -1
  54. package/src/threadWrapper.ts +38 -17
  55. package/src/version.ts +1 -1
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { TracerProvider, diag, trace } from "@opentelemetry/api";
7
7
  import { FITracer, TraceConfigOptions } from "@traceai/fi-core";
8
+ import { patchCodex } from "./codexWrapper";
8
9
  import { ContentPolicy } from "./spans";
9
10
  import { TurnTracingContext, patchThread } from "./threadWrapper";
10
11
  import { CodexModuleLike, ThreadLike } from "./types";
@@ -69,9 +70,20 @@ export function resolveContentPolicy(traceConfig?: TraceConfigOptions): ContentP
69
70
  export class CodexSDKInstrumentation {
70
71
  private tracerProvider?: TracerProvider;
71
72
  private readonly traceConfig?: TraceConfigOptions;
72
- /** Undo functions for the class patch and wrapCodex() factories (never one per thread). */
73
+ /** Undo functions for class patches. */
73
74
  private readonly undo: Array<() => void> = [];
74
- /** False after unpatch(): instance patches stay in place but pass events through untraced. */
75
+ /** Client undo functions hold only WeakRefs; collected clients release their bookkeeping. */
76
+ private readonly clientUndo = new Set<() => void>();
77
+ private readonly clientFinalizer = new FinalizationRegistry<() => void>((undo) => {
78
+ // Deactivate the owner too: saved factories must remain inert after collection.
79
+ undo();
80
+ this.clientUndo.delete(undo);
81
+ });
82
+ /** Thread undo functions hold only WeakRefs; collected threads release their bookkeeping. */
83
+ private readonly threadUndo = new Set<() => void>();
84
+ private readonly threadFinalizer = new FinalizationRegistry<() => void>((undo) => {
85
+ this.threadUndo.delete(undo);
86
+ });
75
87
  private enabled = true;
76
88
 
77
89
  constructor(config: CodexSDKInstrumentationConfig = {}) {
@@ -106,13 +118,16 @@ export class CodexSDKInstrumentation {
106
118
  }
107
119
 
108
120
  /**
109
- * Patch one Thread instance. Returns the same instance. No reference to the
110
- * thread is kept: after `unpatch()` the patched method passes events through
111
- * untraced instead of being removed.
121
+ * Patch one Thread instance. Returns the same instance. Only a weak reference
122
+ * is kept, so `unpatch()` can restore live threads without retaining dropped ones.
112
123
  */
113
124
  wrapThread<T extends object>(thread: T): T {
114
125
  this.enabled = true;
115
- patchThread(thread as ThreadLike, () => this.tracingContext());
126
+ const undo = patchThread(thread as ThreadLike, () => this.tracingContext());
127
+ if (undo) {
128
+ this.threadUndo.add(undo);
129
+ this.threadFinalizer.register(thread, undo, undo);
130
+ }
116
131
  return thread;
117
132
  }
118
133
 
@@ -120,27 +135,10 @@ export class CodexSDKInstrumentation {
120
135
  wrapCodex<T extends object>(codex: T): T {
121
136
  if (!codex || typeof codex !== "object") return codex;
122
137
  this.enabled = true;
123
- const target = codex as Record<string, unknown>;
124
- // Held weakly: a wrapped client the app drops can be collected before unpatch().
125
- const ref = new WeakRef(target);
126
- for (const method of ["startThread", "resumeThread"] as const) {
127
- const original = target[method] as ((...args: unknown[]) => object) | undefined;
128
- if (typeof original !== "function") continue;
129
- const hadOwn = Object.prototype.hasOwnProperty.call(codex, method);
130
- const self = this;
131
- Object.defineProperty(codex, method, {
132
- value: function tracedThreadFactory(this: unknown, ...args: unknown[]) {
133
- return self.wrapThread(original.apply(this, args));
134
- },
135
- writable: true,
136
- configurable: true,
137
- });
138
- this.undo.push(() => {
139
- const live = ref.deref();
140
- if (!live) return;
141
- if (hadOwn) Object.defineProperty(live, method, { value: original, writable: true, configurable: true });
142
- else delete live[method];
143
- });
138
+ const undo = patchCodex(codex as Record<string | symbol, unknown>, (thread) => this.wrapThread(thread));
139
+ if (undo) {
140
+ this.clientUndo.add(undo);
141
+ this.clientFinalizer.register(codex, undo, undo);
144
142
  }
145
143
  return codex;
146
144
  }
@@ -148,6 +146,20 @@ export class CodexSDKInstrumentation {
148
146
  /** Remove every patch this instrumentation applied. */
149
147
  unpatch(): void {
150
148
  this.enabled = false;
149
+ for (const undo of this.threadUndo) {
150
+ this.threadFinalizer.unregister(undo);
151
+ undo();
152
+ }
153
+ this.threadUndo.clear();
154
+ for (const undo of this.clientUndo) {
155
+ this.clientFinalizer.unregister(undo);
156
+ try {
157
+ undo();
158
+ } catch (error) {
159
+ diag.debug(`${INSTRUMENTATION_NAME}: unpatch failed: ${error}`);
160
+ }
161
+ }
162
+ this.clientUndo.clear();
151
163
  while (this.undo.length) {
152
164
  const fn = this.undo.pop();
153
165
  try {
package/src/spans.ts CHANGED
@@ -515,7 +515,7 @@ export class TurnTracer {
515
515
  if (turnUsage) {
516
516
  if (this.threadId) usageBaselines.set(this.threadId, cumulative);
517
517
  } else {
518
- // The total went down (a reset, or an SDK that reports per-turn usage): use it as
518
+ // The total went down (a reset within the supported cumulative-usage SDKs): use it as
519
519
  // reported, and keep the larger baseline so a later turn is not counted twice.
520
520
  turnUsage = cumulative;
521
521
  attributes[C.USAGE_NON_MONOTONIC] = true;
@@ -16,6 +16,7 @@ import { ContentPolicy, TurnTracer, clockMs } from "./spans";
16
16
  import { InputLike, ThreadEventLike, ThreadLike, TurnOptionsLike } from "./types";
17
17
 
18
18
  const PATCHED = Symbol.for("@traceai/codex-sdk/patched");
19
+ const PATCH_RECORD = Symbol.for("@traceai/codex-sdk/patch-record");
19
20
  const LOG = "@traceai/codex-sdk";
20
21
 
21
22
  export interface TurnTracingContext {
@@ -26,16 +27,37 @@ export interface TurnTracingContext {
26
27
  type GetContext = () => TurnTracingContext | undefined;
27
28
 
28
29
  interface PatchRecord {
29
- target: ThreadLike;
30
30
  method: "runStreamedInternal" | "runStreamed";
31
- original: unknown;
32
- hadOwn: boolean;
31
+ originalDescriptor: PropertyDescriptor | undefined;
32
+ wrapper: unknown;
33
+ owner: symbol;
33
34
  }
34
35
 
35
36
  export function isPatched(target: unknown): boolean {
36
37
  return !!target && (target as Record<symbol, unknown>)[PATCHED] === true;
37
38
  }
38
39
 
40
+ /** The undo closure retains neither the target nor its possibly bound original method. */
41
+ function weakUndo(ref: WeakRef<ThreadLike>, owner: symbol): () => void {
42
+ return () => {
43
+ const target = ref.deref();
44
+ if (!target) return;
45
+ const record = Object.getOwnPropertyDescriptor(target, PATCH_RECORD)?.value as PatchRecord | undefined;
46
+ if (record?.owner !== owner) return;
47
+ try {
48
+ // Do not clobber a method that the application replaced after our patch.
49
+ if (Object.getOwnPropertyDescriptor(target, record.method)?.value === record.wrapper) {
50
+ if (record.originalDescriptor) Object.defineProperty(target, record.method, record.originalDescriptor);
51
+ else delete (target as Record<string, unknown>)[record.method];
52
+ }
53
+ delete (target as Record<symbol, unknown>)[PATCHED];
54
+ delete (target as Record<symbol, unknown>)[PATCH_RECORD];
55
+ } catch (error) {
56
+ diag.debug(`${LOG}: unpatch failed: ${error}`);
57
+ }
58
+ };
59
+ }
60
+
39
61
  /**
40
62
  * Patch a Thread prototype or a single Thread instance. Returns an undo
41
63
  * function, or `undefined` when the target was already patched or has no
@@ -44,10 +66,11 @@ export function isPatched(target: unknown): boolean {
44
66
  export function patchThread(target: ThreadLike, getContext: GetContext): (() => void) | undefined {
45
67
  if (!target || typeof target !== "object" || isPatched(target)) return undefined;
46
68
 
47
- let record: PatchRecord | undefined;
69
+ const owner = Symbol();
70
+ let record: PatchRecord;
48
71
  if (typeof target.runStreamedInternal === "function") {
49
72
  const original = target.runStreamedInternal;
50
- record = { target, method: "runStreamedInternal", original, hadOwn: Object.prototype.hasOwnProperty.call(target, "runStreamedInternal") };
73
+ const originalDescriptor = Object.getOwnPropertyDescriptor(target, "runStreamedInternal");
51
74
  const traced = function tracedRunStreamedInternal(this: ThreadLike, input: InputLike, turnOptions?: TurnOptionsLike) {
52
75
  const startTimeMs = clockMs();
53
76
  const parentContext = otelContext.active();
@@ -55,10 +78,11 @@ export function patchThread(target: ThreadLike, getContext: GetContext): (() =>
55
78
  const generator = original.apply(this, arguments as unknown as [InputLike, TurnOptionsLike?]);
56
79
  return traceEvents(generator, this, input, turnOptions, parentContext, startTimeMs, getContext);
57
80
  };
81
+ record = { method: "runStreamedInternal", originalDescriptor, wrapper: traced, owner };
58
82
  defineMethod(target, "runStreamedInternal", traced);
59
83
  } else if (typeof target.runStreamed === "function") {
60
84
  const original = target.runStreamed;
61
- record = { target, method: "runStreamed", original, hadOwn: Object.prototype.hasOwnProperty.call(target, "runStreamed") };
85
+ const originalDescriptor = Object.getOwnPropertyDescriptor(target, "runStreamed");
62
86
  diag.warn(`${LOG}: Thread.runStreamedInternal not found; tracing runStreamed() only, run() may be untraced`);
63
87
  const traced = async function tracedRunStreamed(this: ThreadLike, input: InputLike, turnOptions?: TurnOptionsLike) {
64
88
  const startTimeMs = clockMs();
@@ -71,22 +95,19 @@ export function patchThread(target: ThreadLike, getContext: GetContext): (() =>
71
95
  events: traceEvents(result.events, this, input, turnOptions, parentContext, startTimeMs, getContext),
72
96
  };
73
97
  };
98
+ record = { method: "runStreamed", originalDescriptor, wrapper: traced, owner };
74
99
  defineMethod(target, "runStreamed", traced);
75
100
  } else {
76
101
  return undefined;
77
102
  }
78
103
 
79
- Object.defineProperty(target, PATCHED, { value: true, configurable: true });
80
- const done = record;
81
- return () => {
82
- try {
83
- if (done.hadOwn) defineMethod(done.target, done.method, done.original);
84
- else delete (done.target as Record<string, unknown>)[done.method];
85
- delete (done.target as Record<symbol, unknown>)[PATCHED];
86
- } catch (error) {
87
- diag.debug(`${LOG}: unpatch failed: ${error}`);
88
- }
89
- };
104
+ // Keep the public marker boolean so older installed copies also recognize an
105
+ // active patch. Store ownership separately, only on the weakly tracked target.
106
+ Object.defineProperties(target, {
107
+ [PATCHED]: { value: true, configurable: true },
108
+ [PATCH_RECORD]: { value: record, configurable: true },
109
+ });
110
+ return weakUndo(new WeakRef(target), owner);
90
111
  }
91
112
 
92
113
  function defineMethod(target: object, name: string, value: unknown): void {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.1.0";
1
+ export const VERSION = "0.1.1"; // x-release-please-version