@introspection-sdk/introspection-node 0.4.2 → 0.4.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.
Files changed (219) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/client.d.ts +42 -132
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +67 -366
  5. package/dist/client.js.map +1 -0
  6. package/dist/converters/claude.d.ts +4 -0
  7. package/dist/converters/claude.d.ts.map +1 -1
  8. package/dist/converters/claude.js +7 -0
  9. package/dist/converters/claude.js.map +1 -0
  10. package/dist/converters/gemini.d.ts +90 -0
  11. package/dist/converters/gemini.d.ts.map +1 -0
  12. package/dist/converters/gemini.js +171 -0
  13. package/dist/converters/gemini.js.map +1 -0
  14. package/dist/converters/index.d.ts +1 -1
  15. package/dist/converters/index.d.ts.map +1 -1
  16. package/dist/converters/index.js +2 -1
  17. package/dist/converters/index.js.map +1 -0
  18. package/dist/converters/openai.js +1 -0
  19. package/dist/converters/openai.js.map +1 -0
  20. package/dist/converters/openinference.js +1 -0
  21. package/dist/converters/openinference.js.map +1 -0
  22. package/dist/converters/vercel.d.ts.map +1 -1
  23. package/dist/converters/vercel.js +31 -3
  24. package/dist/converters/vercel.js.map +1 -0
  25. package/dist/http.d.ts +36 -0
  26. package/dist/http.d.ts.map +1 -0
  27. package/dist/http.js +145 -0
  28. package/dist/http.js.map +1 -0
  29. package/dist/index.d.ts +24 -27
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +22 -32
  32. package/dist/index.js.map +1 -0
  33. package/dist/{anthropic.d.ts → otel/anthropic.d.ts} +23 -13
  34. package/dist/otel/anthropic.d.ts.map +1 -0
  35. package/dist/{anthropic.js → otel/anthropic.js} +132 -40
  36. package/dist/otel/anthropic.js.map +1 -0
  37. package/dist/{claude-hooks.d.ts → otel/claude-hooks.d.ts} +19 -1
  38. package/dist/otel/claude-hooks.d.ts.map +1 -0
  39. package/dist/{claude-hooks.js → otel/claude-hooks.js} +94 -20
  40. package/dist/otel/claude-hooks.js.map +1 -0
  41. package/dist/otel/claude-wrapper.d.ts.map +1 -0
  42. package/dist/{claude-wrapper.js → otel/claude-wrapper.js} +2 -1
  43. package/dist/otel/claude-wrapper.js.map +1 -0
  44. package/dist/otel/gemini.d.ts +56 -0
  45. package/dist/otel/gemini.d.ts.map +1 -0
  46. package/dist/otel/gemini.js +348 -0
  47. package/dist/otel/gemini.js.map +1 -0
  48. package/dist/otel/index.d.ts +32 -0
  49. package/dist/otel/index.d.ts.map +1 -0
  50. package/dist/otel/index.js +41 -0
  51. package/dist/otel/index.js.map +1 -0
  52. package/dist/otel/init.d.ts +126 -0
  53. package/dist/otel/init.d.ts.map +1 -0
  54. package/dist/otel/init.js +259 -0
  55. package/dist/otel/init.js.map +1 -0
  56. package/dist/otel/integrations/anthropic.d.ts +10 -0
  57. package/dist/otel/integrations/anthropic.d.ts.map +1 -0
  58. package/dist/otel/integrations/anthropic.js +22 -0
  59. package/dist/otel/integrations/anthropic.js.map +1 -0
  60. package/dist/otel/integrations/base.d.ts +83 -0
  61. package/dist/otel/integrations/base.d.ts.map +1 -0
  62. package/dist/otel/integrations/base.js +23 -0
  63. package/dist/otel/integrations/base.js.map +1 -0
  64. package/dist/otel/integrations/claude-agent.d.ts +13 -0
  65. package/dist/otel/integrations/claude-agent.d.ts.map +1 -0
  66. package/dist/otel/integrations/claude-agent.js +27 -0
  67. package/dist/otel/integrations/claude-agent.js.map +1 -0
  68. package/dist/otel/integrations/gemini.d.ts +11 -0
  69. package/dist/otel/integrations/gemini.d.ts.map +1 -0
  70. package/dist/otel/integrations/gemini.js +21 -0
  71. package/dist/otel/integrations/gemini.js.map +1 -0
  72. package/dist/otel/integrations/index.d.ts +39 -0
  73. package/dist/otel/integrations/index.d.ts.map +1 -0
  74. package/dist/otel/integrations/index.js +131 -0
  75. package/dist/otel/integrations/index.js.map +1 -0
  76. package/dist/otel/integrations/langchain.d.ts +16 -0
  77. package/dist/otel/integrations/langchain.d.ts.map +1 -0
  78. package/dist/otel/integrations/langchain.js +41 -0
  79. package/dist/otel/integrations/langchain.js.map +1 -0
  80. package/dist/otel/integrations/mastra.d.ts +14 -0
  81. package/dist/otel/integrations/mastra.d.ts.map +1 -0
  82. package/dist/otel/integrations/mastra.js +27 -0
  83. package/dist/otel/integrations/mastra.js.map +1 -0
  84. package/dist/otel/integrations/openai-agents.d.ts +11 -0
  85. package/dist/otel/integrations/openai-agents.d.ts.map +1 -0
  86. package/dist/otel/integrations/openai-agents.js +25 -0
  87. package/dist/otel/integrations/openai-agents.js.map +1 -0
  88. package/dist/otel/integrations/pi.d.ts +12 -0
  89. package/dist/otel/integrations/pi.d.ts.map +1 -0
  90. package/dist/otel/integrations/pi.js +20 -0
  91. package/dist/otel/integrations/pi.js.map +1 -0
  92. package/dist/otel/integrations/vercel.d.ts +15 -0
  93. package/dist/otel/integrations/vercel.d.ts.map +1 -0
  94. package/dist/otel/integrations/vercel.js +20 -0
  95. package/dist/otel/integrations/vercel.js.map +1 -0
  96. package/dist/{langchain-handler.d.ts → otel/langchain-handler.d.ts} +10 -1
  97. package/dist/otel/langchain-handler.d.ts.map +1 -0
  98. package/dist/{langchain-handler.js → otel/langchain-handler.js} +43 -6
  99. package/dist/otel/langchain-handler.js.map +1 -0
  100. package/dist/otel/logs.d.ts +82 -0
  101. package/dist/otel/logs.d.ts.map +1 -0
  102. package/dist/otel/logs.js +271 -0
  103. package/dist/otel/logs.js.map +1 -0
  104. package/dist/{mastra-exporter.d.ts → otel/mastra-exporter.d.ts} +3 -1
  105. package/dist/otel/mastra-exporter.d.ts.map +1 -0
  106. package/dist/{mastra-exporter.js → otel/mastra-exporter.js} +71 -17
  107. package/dist/otel/mastra-exporter.js.map +1 -0
  108. package/dist/otel/pi.d.ts +59 -0
  109. package/dist/otel/pi.d.ts.map +1 -0
  110. package/dist/otel/pi.js +67 -0
  111. package/dist/otel/pi.js.map +1 -0
  112. package/dist/otel/register.d.ts +8 -0
  113. package/dist/otel/register.d.ts.map +1 -0
  114. package/dist/otel/register.js +48 -0
  115. package/dist/otel/register.js.map +1 -0
  116. package/dist/otel/setup.d.ts +52 -0
  117. package/dist/otel/setup.d.ts.map +1 -0
  118. package/dist/otel/setup.js +146 -0
  119. package/dist/otel/setup.js.map +1 -0
  120. package/dist/{span-processor.d.ts → otel/span-processor.d.ts} +1 -1
  121. package/dist/otel/span-processor.d.ts.map +1 -0
  122. package/dist/{span-processor.js → otel/span-processor.js} +11 -5
  123. package/dist/otel/span-processor.js.map +1 -0
  124. package/dist/{tracing-processor.d.ts → otel/tracing-processor.d.ts} +4 -1
  125. package/dist/otel/tracing-processor.d.ts.map +1 -0
  126. package/dist/{tracing-processor.js → otel/tracing-processor.js} +64 -14
  127. package/dist/otel/tracing-processor.js.map +1 -0
  128. package/dist/resources/experiments.d.ts +31 -0
  129. package/dist/resources/experiments.d.ts.map +1 -0
  130. package/dist/resources/experiments.js +133 -0
  131. package/dist/resources/experiments.js.map +1 -0
  132. package/dist/resources/recipes.d.ts +22 -0
  133. package/dist/resources/recipes.d.ts.map +1 -0
  134. package/dist/resources/recipes.js +61 -0
  135. package/dist/resources/recipes.js.map +1 -0
  136. package/dist/resources/runtimes.d.ts +76 -0
  137. package/dist/resources/runtimes.d.ts.map +1 -0
  138. package/dist/resources/runtimes.js +179 -0
  139. package/dist/resources/runtimes.js.map +1 -0
  140. package/dist/runner-resources/files.d.ts +35 -0
  141. package/dist/runner-resources/files.d.ts.map +1 -0
  142. package/dist/runner-resources/files.js +131 -0
  143. package/dist/runner-resources/files.js.map +1 -0
  144. package/dist/runner-resources/tasks.d.ts +39 -0
  145. package/dist/runner-resources/tasks.d.ts.map +1 -0
  146. package/dist/runner-resources/tasks.js +129 -0
  147. package/dist/runner-resources/tasks.js.map +1 -0
  148. package/dist/runner.d.ts +80 -0
  149. package/dist/runner.d.ts.map +1 -0
  150. package/dist/runner.js +159 -0
  151. package/dist/runner.js.map +1 -0
  152. package/dist/streaming.d.ts +8 -0
  153. package/dist/streaming.d.ts.map +1 -0
  154. package/dist/streaming.js +67 -0
  155. package/dist/streaming.js.map +1 -0
  156. package/dist/types.js +1 -0
  157. package/dist/types.js.map +1 -0
  158. package/dist/utils.d.ts +10 -0
  159. package/dist/utils.d.ts.map +1 -1
  160. package/dist/utils.js +13 -4
  161. package/dist/utils.js.map +1 -0
  162. package/dist/version.js +1 -0
  163. package/dist/version.js.map +1 -0
  164. package/package.json +91 -22
  165. package/src/client.ts +76 -473
  166. package/src/converters/claude.ts +10 -0
  167. package/src/converters/gemini.ts +243 -0
  168. package/src/converters/index.ts +13 -7
  169. package/src/converters/vercel.ts +41 -3
  170. package/src/http.ts +163 -0
  171. package/src/index.ts +110 -75
  172. package/src/{anthropic.ts → otel/anthropic.ts} +206 -54
  173. package/src/{claude-hooks.ts → otel/claude-hooks.ts} +119 -20
  174. package/src/{claude-wrapper.ts → otel/claude-wrapper.ts} +1 -1
  175. package/src/otel/gemini.ts +491 -0
  176. package/src/otel/index.ts +121 -0
  177. package/src/otel/init.ts +411 -0
  178. package/src/otel/integrations/anthropic.ts +26 -0
  179. package/src/otel/integrations/base.ts +95 -0
  180. package/src/otel/integrations/claude-agent.ts +32 -0
  181. package/src/otel/integrations/gemini.ts +25 -0
  182. package/src/otel/integrations/index.ts +151 -0
  183. package/src/otel/integrations/langchain.ts +45 -0
  184. package/src/otel/integrations/mastra.ts +30 -0
  185. package/src/otel/integrations/openai-agents.ts +31 -0
  186. package/src/otel/integrations/pi.ts +24 -0
  187. package/src/otel/integrations/vercel.ts +24 -0
  188. package/src/{langchain-handler.ts → otel/langchain-handler.ts} +53 -6
  189. package/src/otel/logs.ts +394 -0
  190. package/src/{mastra-exporter.ts → otel/mastra-exporter.ts} +83 -22
  191. package/src/otel/pi.ts +88 -0
  192. package/src/otel/register.ts +50 -0
  193. package/src/otel/setup.ts +215 -0
  194. package/src/{span-processor.ts → otel/span-processor.ts} +11 -6
  195. package/src/{tracing-processor.ts → otel/tracing-processor.ts} +94 -19
  196. package/src/resources/experiments.ts +170 -0
  197. package/src/resources/recipes.ts +73 -0
  198. package/src/resources/runtimes.ts +231 -0
  199. package/src/runner-resources/files.ts +163 -0
  200. package/src/runner-resources/tasks.ts +156 -0
  201. package/src/runner.ts +194 -0
  202. package/src/streaming.ts +57 -0
  203. package/src/utils.ts +12 -5
  204. package/dist/ai-sdk-integration.d.ts +0 -123
  205. package/dist/ai-sdk-integration.d.ts.map +0 -1
  206. package/dist/ai-sdk-integration.js +0 -475
  207. package/dist/anthropic.d.ts.map +0 -1
  208. package/dist/claude-hooks.d.ts.map +0 -1
  209. package/dist/claude-wrapper.d.ts.map +0 -1
  210. package/dist/converters/ai-sdk.d.ts +0 -65
  211. package/dist/converters/ai-sdk.d.ts.map +0 -1
  212. package/dist/converters/ai-sdk.js +0 -261
  213. package/dist/langchain-handler.d.ts.map +0 -1
  214. package/dist/mastra-exporter.d.ts.map +0 -1
  215. package/dist/span-processor.d.ts.map +0 -1
  216. package/dist/tracing-processor.d.ts.map +0 -1
  217. package/src/ai-sdk-integration.ts +0 -691
  218. package/src/converters/ai-sdk.ts +0 -323
  219. /package/dist/{claude-wrapper.d.ts → otel/claude-wrapper.d.ts} +0 -0
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Zero-code preload entry point for Introspection tracing.
3
+ *
4
+ * Usage (ESM):
5
+ *
6
+ * ```bash
7
+ * node --import @introspection-sdk/introspection-node/otel/register app.js
8
+ * ```
9
+ *
10
+ * Runs `introspection.init()` from the environment **before** the app's module
11
+ * graph executes, so no `init()` call is needed in application source. Because
12
+ * the Anthropic / Gemini one-liner works by prototype-patching the SDK classes
13
+ * (see `AnthropicInstrumentor.instrumentClass`), every client constructed after
14
+ * this preload is traced with zero code changes.
15
+ *
16
+ * Configuration is read from the usual environment variables
17
+ * (`INTROSPECTION_TOKEN`, `INTROSPECTION_SERVICE_NAME`,
18
+ * `INTROSPECTION_BASE_OTEL_URL`). If no token is configured the preload logs a
19
+ * warning and does nothing, so it never crashes the host application.
20
+ *
21
+ * This is the counterpart to the private SDK's `register.ts`, adapted to the
22
+ * `src/otel` surface: it drives the existing `init()` rather than registering an
23
+ * import-in-the-middle loader hook (the prototype-patch path does not need one).
24
+ */
25
+ import { logger } from "../utils.js";
26
+ import { init } from "./init.js";
27
+
28
+ /**
29
+ * Run `init()` if a token is configured. Exported (and awaited at module load)
30
+ * so tests can invoke it deterministically; the module-level `await` below
31
+ * guarantees the preload finishes — including framework auto-discovery and the
32
+ * prototype patch — before Node evaluates the application entry point.
33
+ */
34
+ export async function registerFromEnv(): Promise<void> {
35
+ if (!process.env.INTROSPECTION_TOKEN) {
36
+ logger.warn(
37
+ "[introspection] register: INTROSPECTION_TOKEN not set; tracing not initialised.",
38
+ );
39
+ return;
40
+ }
41
+ try {
42
+ await init();
43
+ logger.debug("[introspection] register: tracing initialised from preload.");
44
+ } catch (err) {
45
+ // Never let a preload failure take down the host application.
46
+ logger.warn(`[introspection] register: init failed: ${String(err)}`);
47
+ }
48
+ }
49
+
50
+ await registerFromEnv();
@@ -0,0 +1,215 @@
1
+ /**
2
+ * One-call tracing bootstrap for Introspection.
3
+ *
4
+ * Wires up the three things every host process needs for baggage-based
5
+ * agent / conversation propagation to work:
6
+ *
7
+ * 1. `AsyncLocalStorageContextManager` — without this, `context.with()` is a
8
+ * no-op (the default `NOOP_CONTEXT_MANAGER` silently drops the context),
9
+ * which makes `IntrospectionClient.withAgent()`, `.withConversation()`,
10
+ * etc. dead code.
11
+ * 2. `W3CBaggagePropagator` — so baggage is carried across HTTP boundaries
12
+ * via the `baggage` header (alongside W3C trace-context).
13
+ * 3. A registered `NodeTracerProvider` with an `IntrospectionSpanProcessor`
14
+ * on it — so spans are exported to Introspection and baggage values
15
+ * (conversation / agent IDs) are merged into span attributes at
16
+ * `onEnd()` time.
17
+ *
18
+ * Call this once at process start (before any instrumentors run). It is
19
+ * idempotent across re-invocations — the cached provider is returned and
20
+ * the global registrations are not touched a second time.
21
+ *
22
+ * **Loud-by-default registration.** OTel's `setGlobalContextManager` /
23
+ * `setGlobalPropagator` silently refuse to replace an existing
24
+ * registration. If anything (another lib, a stale call, a test fixture)
25
+ * has already installed a context manager that does NOT propagate W3C
26
+ * baggage, our `withAgent` / `withConversation` calls become silent
27
+ * no-ops and users get spans with no identity attached — exactly the
28
+ * footgun this SDK exists to prevent.
29
+ *
30
+ * `setupTracing` defends against that:
31
+ * - If no manager / propagator is registered: install ours.
32
+ * - If one is registered: WARN (default) or THROW (strict mode) so the
33
+ * host author sees it, rather than discovering missing baggage in
34
+ * production telemetry weeks later.
35
+ *
36
+ * Pass `{ onConflict: "throw" }` (or set `INTROSPECTION_STRICT_TRACING=1`
37
+ * in the env) for the strict variant. Pass `{ onConflict: "replace" }`
38
+ * to force replacement when you know the prior registration is wrong
39
+ * (e.g. test setup, or migrating from another tracer).
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import { setupTracing, IntrospectionClient } from "@introspection-sdk/introspection-node";
44
+ *
45
+ * setupTracing();
46
+ *
47
+ * const introspect = new IntrospectionClient();
48
+ *
49
+ * await introspect.withAgent("researcher", "researcher-1", () =>
50
+ * introspect.withConversation(conversationId, undefined, () =>
51
+ * client.messages.create({ ... }),
52
+ * ),
53
+ * );
54
+ * ```
55
+ */
56
+ import { context, propagation } from "@opentelemetry/api";
57
+ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
58
+ import {
59
+ CompositePropagator,
60
+ W3CBaggagePropagator,
61
+ W3CTraceContextPropagator,
62
+ } from "@opentelemetry/core";
63
+ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
64
+ import type { SpanProcessor } from "@opentelemetry/sdk-trace-base";
65
+
66
+ import { logger } from "../utils.js";
67
+ import {
68
+ IntrospectionSpanProcessor,
69
+ type IntrospectionSpanProcessorOptions,
70
+ } from "./span-processor.js";
71
+
72
+ let registeredProvider: NodeTracerProvider | undefined;
73
+
74
+ export type ConflictBehavior = "warn" | "throw" | "replace";
75
+
76
+ export interface SetupTracingOptions extends IntrospectionSpanProcessorOptions {
77
+ /**
78
+ * What to do if a context manager or propagator is already registered.
79
+ *
80
+ * - `"warn"` (default) — log a warning and continue with the existing
81
+ * registration. Spans still flow but baggage may not propagate as
82
+ * expected. Set `INTROSPECTION_STRICT_TRACING=1` to upgrade to throw.
83
+ * - `"throw"` — throw an error. Use in environments where a silent
84
+ * baggage drop is unacceptable (production telemetry pipelines).
85
+ * - `"replace"` — force-register over the existing manager / propagator.
86
+ * Useful in tests or when migrating from another tracer; can break
87
+ * other libraries that relied on the previous registration.
88
+ */
89
+ onConflict?: ConflictBehavior;
90
+
91
+ /**
92
+ * Extra span processors attached to the same `NodeTracerProvider`, so a
93
+ * single set of spans fans out to other backends (Langfuse, Arize, …).
94
+ * This is the low-level dual-export hook:
95
+ *
96
+ * ```ts
97
+ * setupTracing({
98
+ * serviceName: "my-app",
99
+ * additionalSpanProcessors: [new BatchSpanProcessor(langfuseExporter)],
100
+ * });
101
+ * ```
102
+ *
103
+ * Note: the {@link IntrospectionSpanProcessor} forwards its *own* converted
104
+ * copy of each span to Introspection — it does not mutate the shared span —
105
+ * so these processors receive the raw span and run independently of it.
106
+ * Order is therefore irrelevant. `introspection.init({ spanProcessors })`
107
+ * is the equivalent for the one-liner surface.
108
+ */
109
+ additionalSpanProcessors?: SpanProcessor[];
110
+ }
111
+
112
+ function defaultConflictBehavior(): ConflictBehavior {
113
+ return process.env.INTROSPECTION_STRICT_TRACING ? "throw" : "warn";
114
+ }
115
+
116
+ function registerContextManager(behavior: ConflictBehavior): void {
117
+ const ok = context.setGlobalContextManager(
118
+ new AsyncLocalStorageContextManager().enable(),
119
+ );
120
+ if (ok) return;
121
+ // OTel silently refuses replacement — we have to disable+retry to force.
122
+ const msg =
123
+ "setupTracing: another OTel context manager was already registered. " +
124
+ "withAgent / withConversation baggage scopes will not propagate unless " +
125
+ "that manager is an AsyncLocalStorageContextManager honoring W3C baggage.";
126
+ if (behavior === "throw") {
127
+ throw new Error(`[introspection] ${msg}`);
128
+ }
129
+ if (behavior === "replace") {
130
+ context.disable();
131
+ if (
132
+ !context.setGlobalContextManager(
133
+ new AsyncLocalStorageContextManager().enable(),
134
+ )
135
+ ) {
136
+ throw new Error(
137
+ "[introspection] setupTracing: failed to replace context manager " +
138
+ "even after disable(); the OTel global registry is in a bad state.",
139
+ );
140
+ }
141
+ return;
142
+ }
143
+ logger.warn(msg);
144
+ }
145
+
146
+ function registerPropagator(behavior: ConflictBehavior): void {
147
+ const propagator = new CompositePropagator({
148
+ propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()],
149
+ });
150
+ const ok = propagation.setGlobalPropagator(propagator);
151
+ if (ok) return;
152
+ const msg =
153
+ "setupTracing: another OTel propagator was already registered. " +
154
+ "Cross-process baggage propagation depends on the existing propagator " +
155
+ "carrying the W3C `baggage` header.";
156
+ if (behavior === "throw") {
157
+ throw new Error(`[introspection] ${msg}`);
158
+ }
159
+ if (behavior === "replace") {
160
+ propagation.disable();
161
+ if (!propagation.setGlobalPropagator(propagator)) {
162
+ throw new Error(
163
+ "[introspection] setupTracing: failed to replace propagator even " +
164
+ "after disable(); the OTel global registry is in a bad state.",
165
+ );
166
+ }
167
+ return;
168
+ }
169
+ logger.warn(msg);
170
+ }
171
+
172
+ /**
173
+ * Install the OTel context manager + W3C (trace-context + baggage) propagator
174
+ * that Introspection's baggage scopes depend on.
175
+ *
176
+ * Shared by {@link setupTracing} and `introspection.init()`. `behavior`
177
+ * controls what happens when a manager / propagator is already registered
178
+ * (see {@link ConflictBehavior}).
179
+ */
180
+ export function registerOTelGlobals(behavior?: ConflictBehavior): void {
181
+ const resolved = behavior ?? defaultConflictBehavior();
182
+ registerContextManager(resolved);
183
+ registerPropagator(resolved);
184
+ }
185
+
186
+ /**
187
+ * Initialise Introspection tracing for a Node.js process.
188
+ */
189
+ export function setupTracing(
190
+ options?: SetupTracingOptions,
191
+ ): NodeTracerProvider {
192
+ if (registeredProvider) {
193
+ return registeredProvider;
194
+ }
195
+
196
+ const behavior: ConflictBehavior =
197
+ options?.onConflict ?? defaultConflictBehavior();
198
+
199
+ registerOTelGlobals(behavior);
200
+
201
+ const provider = new NodeTracerProvider({
202
+ // Order is irrelevant: IntrospectionSpanProcessor exports its own converted
203
+ // copy and does not mutate the shared span, so co-located dual-export
204
+ // processors receive the raw span regardless of position. Listed first here
205
+ // (and in init()) purely for a consistent, readable ordering story.
206
+ spanProcessors: [
207
+ new IntrospectionSpanProcessor(options),
208
+ ...(options?.additionalSpanProcessors ?? []),
209
+ ],
210
+ });
211
+ provider.register();
212
+
213
+ registeredProvider = provider;
214
+ return provider;
215
+ }
@@ -16,14 +16,14 @@ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
16
16
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
17
17
  import { ExportResult, ExportResultCode } from "@opentelemetry/core";
18
18
  import { randomUUID } from "crypto";
19
- import type { AdvancedOptions } from "./types.js";
20
- import { logger, withOtlpHttpsProxy } from "./utils.js";
19
+ import type { AdvancedOptions } from "../types.js";
20
+ import { logger, withOtlpHttpsProxy } from "../utils.js";
21
21
  import {
22
22
  isOpenInferenceSpan,
23
23
  replaceOpenInferenceWithGenAI,
24
24
  isVercelAISpan,
25
25
  convertVercelAIToGenAI,
26
- } from "./converters/index.js";
26
+ } from "../converters/index.js";
27
27
 
28
28
  /**
29
29
  * Extended span type that includes both v1 and v2 instrumentation properties.
@@ -157,7 +157,7 @@ export class IntrospectionSpanProcessor implements SpanProcessor {
157
157
  const advanced = options.advanced || {};
158
158
  const baseUrl =
159
159
  advanced.baseUrl ||
160
- process.env.INTROSPECTION_BASE_URL ||
160
+ process.env.INTROSPECTION_BASE_OTEL_URL ||
161
161
  "https://otel.introspection.dev";
162
162
 
163
163
  const headers: Record<string, string> = {
@@ -381,11 +381,16 @@ export class IntrospectionSpanProcessor implements SpanProcessor {
381
381
  attrs["gen_ai.operation.name"] = "chat";
382
382
  }
383
383
 
384
- // Agent name from baggage
384
+ // Agent name / id from baggage (baggage wins so per-call agent identity
385
+ // overrides any default stamped at instrumentor-construction time).
385
386
  const baggageAgentName = baggage?.getEntry("gen_ai.agent.name")?.value;
386
- if (baggageAgentName && !attrs["gen_ai.agent.name"]) {
387
+ if (baggageAgentName) {
387
388
  attrs["gen_ai.agent.name"] = baggageAgentName;
388
389
  }
390
+ const baggageAgentId = baggage?.getEntry("gen_ai.agent.id")?.value;
391
+ if (baggageAgentId) {
392
+ attrs["gen_ai.agent.id"] = baggageAgentId;
393
+ }
389
394
 
390
395
  // Override resource with service name if provided
391
396
  let resource: Resource | undefined;
@@ -27,13 +27,13 @@ import {
27
27
  convertResponsesOutputsToSemconv,
28
28
  convertResponsesToolsToSemconv,
29
29
  convertResponsesInstructionsToSemconv,
30
- } from "./converters/openai.js";
30
+ } from "../converters/openai.js";
31
31
  import type {
32
32
  Response as OpenAIResponse,
33
33
  ResponseInputItem,
34
- } from "./converters/openai.js";
35
- import { logger, withOtlpHttpsProxy } from "./utils.js";
36
- import { VERSION } from "./version.js";
34
+ } from "../converters/openai.js";
35
+ import { logger, withOtlpHttpsProxy } from "../utils.js";
36
+ import { VERSION } from "../version.js";
37
37
 
38
38
  // Type imports from @openai/agents
39
39
  import type {
@@ -79,7 +79,7 @@ export interface TracingProcessorAdvancedOptions {
79
79
  export interface IntrospectionTracingProcessorOptions {
80
80
  /** Authentication token (env: INTROSPECTION_TOKEN) */
81
81
  token?: string;
82
- /** Base URL for the API (env: INTROSPECTION_BASE_URL, default: "https://otel.introspection.dev") */
82
+ /** Base URL for the API (env: INTROSPECTION_BASE_OTEL_URL, default: "https://otel.introspection.dev") */
83
83
  baseUrl?: string;
84
84
  /** Service name for telemetry (env: INTROSPECTION_SERVICE_NAME) */
85
85
  serviceName?: string;
@@ -131,7 +131,7 @@ export class IntrospectionTracingProcessor implements TracingProcessor {
131
131
 
132
132
  const baseUrl =
133
133
  options.baseUrl ||
134
- process.env.INTROSPECTION_BASE_URL ||
134
+ process.env.INTROSPECTION_BASE_OTEL_URL ||
135
135
  "https://otel.introspection.dev";
136
136
 
137
137
  const headers: Record<string, string> = {
@@ -220,7 +220,10 @@ export class IntrospectionTracingProcessor implements TracingProcessor {
220
220
  async onTraceStart(trace: Trace): Promise<void> {
221
221
  logger.debug(`onTraceStart called: ${trace?.name} (${trace?.traceId})`);
222
222
  if (trace) {
223
- const conversationId = `intro_conv_${randomUUID().replace(/-/g, "")}`;
223
+ // Use groupId as conversationId when provided so callers can link
224
+ // multiple traces (e.g. multi-turn orchestrator runs) into one conversation.
225
+ const conversationId =
226
+ trace.groupId ?? `intro_conv_${randomUUID().replace(/-/g, "")}`;
224
227
  this._conversationIds.set(trace.traceId, conversationId);
225
228
  const otelSpan = this._tracer.startSpan(trace.name);
226
229
  this._spans.set(trace.traceId, otelSpan);
@@ -286,14 +289,10 @@ export class IntrospectionTracingProcessor implements TracingProcessor {
286
289
  * @param span - The OpenAI Agents SDK span.
287
290
  */
288
291
  async onSpanEnd(span: Span<SpanData>): Promise<void> {
289
- logger.debug(
290
- `onSpanEnd called: ${span?.spanId} (type: ${(span?.spanData as SpanData)?.type})`,
291
- );
292
292
  if (!span) return;
293
293
 
294
294
  const otelSpan = this._spans.get(span.spanId);
295
295
  if (!otelSpan) {
296
- logger.debug(`No OTel span found for spanId: ${span.spanId}`);
297
296
  return;
298
297
  }
299
298
 
@@ -448,6 +447,14 @@ export class IntrospectionTracingProcessor implements TracingProcessor {
448
447
  usage.output_tokens,
449
448
  );
450
449
  }
450
+ // OpenAI returns cached token count in input_tokens_details.cached_tokens
451
+ const cachedTokens = usage.input_tokens_details?.cached_tokens;
452
+ if (cachedTokens) {
453
+ otelSpan.setAttribute(
454
+ "gen_ai.usage.cache_read.input_tokens",
455
+ cachedTokens,
456
+ );
457
+ }
451
458
  }
452
459
 
453
460
  // Model info
@@ -474,6 +481,9 @@ export class IntrospectionTracingProcessor implements TracingProcessor {
474
481
 
475
482
  /**
476
483
  * Extract attributes from generation spans.
484
+ * Handles both structured GenerationUsageData (spanData.usage) and raw
485
+ * Chat Completions API responses stored in spanData.output[0] by
486
+ * OpenAIChatCompletionsModel.
477
487
  */
478
488
  private _processGenerationSpan(
479
489
  otelSpan: OtelSpan,
@@ -487,15 +497,78 @@ export class IntrospectionTracingProcessor implements TracingProcessor {
487
497
  otelSpan.setAttribute("gen_ai.request.model", spanData.model);
488
498
  }
489
499
 
490
- if (spanData.usage) {
491
- const usage = spanData.usage as Record<string, number>;
492
- if (usage.input_tokens) {
493
- otelSpan.setAttribute("gen_ai.usage.input_tokens", usage.input_tokens);
500
+ // GenerationUsageData uses snake_case: input_tokens / output_tokens.
501
+ if (spanData.usage?.input_tokens) {
502
+ otelSpan.setAttribute(
503
+ "gen_ai.usage.input_tokens",
504
+ spanData.usage.input_tokens,
505
+ );
506
+ }
507
+ if (spanData.usage?.output_tokens) {
508
+ otelSpan.setAttribute(
509
+ "gen_ai.usage.output_tokens",
510
+ spanData.usage.output_tokens,
511
+ );
512
+ }
513
+
514
+ // OpenAIChatCompletionsModel stores the raw ChatCompletion response object
515
+ // in spanData.output[0] (it never sets spanData.usage). Extract usage,
516
+ // response ID, response model, and the actual assistant message from it.
517
+ if (!spanData.usage) {
518
+ const rawResponse = spanData.output?.[0] as
519
+ | Record<string, unknown>
520
+ | undefined;
521
+
522
+ // Response ID — required by the backend processor to register this span
523
+ // as a conversation item (build_request returns None without it).
524
+ const responseId = rawResponse?.id;
525
+ if (typeof responseId === "string" && responseId) {
526
+ otelSpan.setAttribute("gen_ai.response.id", responseId);
494
527
  }
495
- if (usage.output_tokens) {
528
+
529
+ // Resolved model name (e.g. "gpt-4o-mini-2024-07-18" vs "gpt-4o-mini").
530
+ const responseModel = rawResponse?.model;
531
+ if (typeof responseModel === "string" && responseModel) {
532
+ otelSpan.setAttribute("gen_ai.response.model", responseModel);
533
+ }
534
+
535
+ const rawUsage = rawResponse?.usage as
536
+ | Record<string, unknown>
537
+ | undefined;
538
+ if (rawUsage) {
539
+ const promptTokens = rawUsage.prompt_tokens;
540
+ const completionTokens = rawUsage.completion_tokens;
541
+ if (typeof promptTokens === "number") {
542
+ otelSpan.setAttribute("gen_ai.usage.input_tokens", promptTokens);
543
+ }
544
+ if (typeof completionTokens === "number") {
545
+ otelSpan.setAttribute("gen_ai.usage.output_tokens", completionTokens);
546
+ }
547
+ const promptDetails = rawUsage.prompt_tokens_details as
548
+ | Record<string, unknown>
549
+ | undefined;
550
+ const cachedTokens = promptDetails?.cached_tokens;
551
+ if (typeof cachedTokens === "number" && cachedTokens > 0) {
552
+ otelSpan.setAttribute(
553
+ "gen_ai.usage.cache_read.input_tokens",
554
+ cachedTokens,
555
+ );
556
+ }
557
+ }
558
+
559
+ // Extract the actual assistant message from choices[0].message so that
560
+ // gen_ai.output.messages is a proper [{role, content}] array rather than
561
+ // the full ChatCompletion envelope (which the backend cannot parse).
562
+ const choices = rawResponse?.choices as
563
+ | Array<Record<string, unknown>>
564
+ | undefined;
565
+ const assistantMessage = choices?.[0]?.message as
566
+ | Record<string, unknown>
567
+ | undefined;
568
+ if (assistantMessage) {
496
569
  otelSpan.setAttribute(
497
- "gen_ai.usage.output_tokens",
498
- usage.output_tokens,
570
+ "gen_ai.output.messages",
571
+ JSON.stringify([assistantMessage]),
499
572
  );
500
573
  }
501
574
  }
@@ -507,7 +580,9 @@ export class IntrospectionTracingProcessor implements TracingProcessor {
507
580
  );
508
581
  }
509
582
 
510
- if (spanData.output) {
583
+ // For non-Chat-Completions generation spans that do have usage set,
584
+ // still export output messages in the standard format if present.
585
+ if (spanData.usage && spanData.output) {
511
586
  otelSpan.setAttribute(
512
587
  "gen_ai.output.messages",
513
588
  JSON.stringify(spanData.output),
@@ -0,0 +1,170 @@
1
+ import type {
2
+ Experiment,
3
+ ExperimentEndParams,
4
+ ExperimentCreate,
5
+ ExperimentListParams,
6
+ ExperimentUpdate,
7
+ Paginated,
8
+ RunRequest,
9
+ RunnerSpec,
10
+ Uuid,
11
+ } from "@introspection-sdk/types";
12
+ import type { HttpClient } from "../http.js";
13
+ import type { IntrospectionClient } from "../client.js";
14
+ import { Runner, type RunnerSource } from "../runner.js";
15
+
16
+ interface ExperimentRunRequestBody {
17
+ identity?: {
18
+ user_id?: string;
19
+ anonymous_id?: string;
20
+ conversation_id?: string;
21
+ };
22
+ ttl_seconds?: number;
23
+ }
24
+
25
+ function toRunBody(opts?: RunRequest): ExperimentRunRequestBody | undefined {
26
+ if (!opts) return undefined;
27
+ const out: ExperimentRunRequestBody = {};
28
+ if (opts.identity) out.identity = opts.identity;
29
+ if (opts.ttl_seconds !== undefined) out.ttl_seconds = opts.ttl_seconds;
30
+ return out;
31
+ }
32
+
33
+ export class ExperimentsApi {
34
+ constructor(
35
+ private readonly http: HttpClient,
36
+ private readonly client: IntrospectionClient,
37
+ ) {}
38
+
39
+ list(params: ExperimentListParams): Promise<Paginated<Experiment>> {
40
+ return this.http.request<Paginated<Experiment>>({
41
+ method: "GET",
42
+ path: "/v1/experiments",
43
+ query: params as unknown as Record<string, unknown>,
44
+ });
45
+ }
46
+
47
+ async *listAll(params: ExperimentListParams): AsyncIterable<Experiment> {
48
+ let next: string | undefined = params.next;
49
+ do {
50
+ const page = await this.list({ ...params, next });
51
+ for (const r of page.records) yield r;
52
+ next = page.next ?? undefined;
53
+ } while (next);
54
+ }
55
+
56
+ get(id: Uuid): Promise<Experiment> {
57
+ return this.http.request<Experiment>({
58
+ method: "GET",
59
+ path: `/v1/experiments/${encodeURIComponent(id)}`,
60
+ });
61
+ }
62
+
63
+ create(input: ExperimentCreate): Promise<Experiment> {
64
+ return this.http.request<Experiment>({
65
+ method: "POST",
66
+ path: "/v1/experiments",
67
+ body: input,
68
+ });
69
+ }
70
+
71
+ update(id: Uuid, input: ExperimentUpdate): Promise<Experiment> {
72
+ return this.http.request<Experiment>({
73
+ method: "PATCH",
74
+ path: `/v1/experiments/${encodeURIComponent(id)}`,
75
+ body: input,
76
+ });
77
+ }
78
+
79
+ delete(id: Uuid): Promise<void> {
80
+ return this.http.request<void>({
81
+ method: "DELETE",
82
+ path: `/v1/experiments/${encodeURIComponent(id)}`,
83
+ expect: "empty",
84
+ });
85
+ }
86
+
87
+ startById(id: Uuid): Promise<Experiment> {
88
+ return this.http.request<Experiment>({
89
+ method: "POST",
90
+ path: `/v1/experiments/${encodeURIComponent(id)}/start`,
91
+ body: {},
92
+ });
93
+ }
94
+
95
+ endById(id: Uuid, params?: ExperimentEndParams): Promise<Experiment> {
96
+ return this.http.request<Experiment>({
97
+ method: "POST",
98
+ path: `/v1/experiments/${encodeURIComponent(id)}/end`,
99
+ body: params ?? {},
100
+ });
101
+ }
102
+
103
+ cancelById(id: Uuid): Promise<Experiment> {
104
+ return this.http.request<Experiment>({
105
+ method: "POST",
106
+ path: `/v1/experiments/${encodeURIComponent(id)}/cancel`,
107
+ body: {},
108
+ });
109
+ }
110
+
111
+ async runById(id: Uuid, opts?: RunRequest): Promise<Runner> {
112
+ const spec = await this.http.request<RunnerSpec>({
113
+ method: "POST",
114
+ path: `/v1/experiments/${encodeURIComponent(id)}/run`,
115
+ body: toRunBody(opts) ?? {},
116
+ });
117
+ const source: RunnerSource = {
118
+ kind: "experiment",
119
+ id,
120
+ options: opts,
121
+ };
122
+ return new Runner(this.client, source, spec);
123
+ }
124
+ }
125
+
126
+ export class ExperimentHandle {
127
+ constructor(
128
+ private readonly api: ExperimentsApi,
129
+ private readonly id: Uuid,
130
+ ) {}
131
+
132
+ run(opts?: RunRequest): Promise<Runner> {
133
+ return this.api.runById(this.id, opts);
134
+ }
135
+
136
+ start(): Promise<Experiment> {
137
+ return this.api.startById(this.id);
138
+ }
139
+
140
+ end(opts?: ExperimentEndParams): Promise<Experiment> {
141
+ return this.api.endById(this.id, opts);
142
+ }
143
+
144
+ cancel(): Promise<Experiment> {
145
+ return this.api.cancelById(this.id);
146
+ }
147
+ }
148
+
149
+ export type ExperimentHandleFactory = (id: Uuid) => ExperimentHandle;
150
+
151
+ export function attachExperiments(
152
+ client: IntrospectionClient,
153
+ http: HttpClient,
154
+ ): ExperimentsApi & ExperimentHandleFactory {
155
+ const api = new ExperimentsApi(http, client);
156
+ const factory: ExperimentHandleFactory = (id: Uuid) =>
157
+ new ExperimentHandle(api, id);
158
+ const hybrid = factory as ExperimentsApi & ExperimentHandleFactory;
159
+ hybrid.list = api.list.bind(api);
160
+ hybrid.listAll = api.listAll.bind(api);
161
+ hybrid.get = api.get.bind(api);
162
+ hybrid.create = api.create.bind(api);
163
+ hybrid.update = api.update.bind(api);
164
+ hybrid.delete = api.delete.bind(api);
165
+ hybrid.startById = api.startById.bind(api);
166
+ hybrid.endById = api.endById.bind(api);
167
+ hybrid.cancelById = api.cancelById.bind(api);
168
+ hybrid.runById = api.runById.bind(api);
169
+ return hybrid;
170
+ }