@introspection-sdk/introspection-node 0.4.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.
Files changed (89) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-check-types.log +5 -0
  3. package/.turbo/turbo-lint.log +5 -0
  4. package/dist/ai-sdk-integration.d.ts +123 -0
  5. package/dist/ai-sdk-integration.d.ts.map +1 -0
  6. package/dist/ai-sdk-integration.js +475 -0
  7. package/dist/anthropic.d.ts +54 -0
  8. package/dist/anthropic.d.ts.map +1 -0
  9. package/dist/anthropic.js +375 -0
  10. package/dist/claude-hooks.d.ts +328 -0
  11. package/dist/claude-hooks.d.ts.map +1 -0
  12. package/dist/claude-hooks.js +571 -0
  13. package/dist/claude-wrapper.d.ts +93 -0
  14. package/dist/claude-wrapper.d.ts.map +1 -0
  15. package/dist/claude-wrapper.js +154 -0
  16. package/dist/client.d.ts +157 -0
  17. package/dist/client.d.ts.map +1 -0
  18. package/dist/client.js +394 -0
  19. package/dist/converters/ai-sdk.d.ts +65 -0
  20. package/dist/converters/ai-sdk.d.ts.map +1 -0
  21. package/dist/converters/ai-sdk.js +238 -0
  22. package/dist/converters/claude.d.ts +146 -0
  23. package/dist/converters/claude.d.ts.map +1 -0
  24. package/dist/converters/claude.js +202 -0
  25. package/dist/converters/index.d.ts +9 -0
  26. package/dist/converters/index.d.ts.map +1 -0
  27. package/dist/converters/index.js +8 -0
  28. package/dist/converters/openai.d.ts +51 -0
  29. package/dist/converters/openai.d.ts.map +1 -0
  30. package/dist/converters/openai.js +268 -0
  31. package/dist/converters/openclaw.d.ts +16 -0
  32. package/dist/converters/openclaw.d.ts.map +1 -0
  33. package/dist/converters/openclaw.js +49 -0
  34. package/dist/converters/openinference.d.ts +107 -0
  35. package/dist/converters/openinference.d.ts.map +1 -0
  36. package/dist/converters/openinference.js +552 -0
  37. package/dist/converters/vercel.d.ts +21 -0
  38. package/dist/converters/vercel.d.ts.map +1 -0
  39. package/dist/converters/vercel.js +208 -0
  40. package/dist/index.d.ts +38 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +39 -0
  43. package/dist/langchain-handler.d.ts +101 -0
  44. package/dist/langchain-handler.d.ts.map +1 -0
  45. package/dist/langchain-handler.js +489 -0
  46. package/dist/mastra-exporter.d.ts +85 -0
  47. package/dist/mastra-exporter.d.ts.map +1 -0
  48. package/dist/mastra-exporter.js +471 -0
  49. package/dist/span-processor.d.ts +62 -0
  50. package/dist/span-processor.d.ts.map +1 -0
  51. package/dist/span-processor.js +310 -0
  52. package/dist/tracing-processor.d.ts +136 -0
  53. package/dist/tracing-processor.d.ts.map +1 -0
  54. package/dist/tracing-processor.js +368 -0
  55. package/dist/types/genai.d.ts +165 -0
  56. package/dist/types/genai.d.ts.map +1 -0
  57. package/dist/types/genai.js +82 -0
  58. package/dist/types.d.ts +7 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +1 -0
  61. package/dist/utils.d.ts +19 -0
  62. package/dist/utils.d.ts.map +1 -0
  63. package/dist/utils.js +57 -0
  64. package/dist/version.d.ts +3 -0
  65. package/dist/version.d.ts.map +1 -0
  66. package/dist/version.js +2 -0
  67. package/eslint.config.mjs +4 -0
  68. package/package.json +78 -0
  69. package/src/ai-sdk-integration.ts +689 -0
  70. package/src/anthropic.ts +465 -0
  71. package/src/claude-hooks.ts +1010 -0
  72. package/src/claude-wrapper.ts +240 -0
  73. package/src/client.ts +513 -0
  74. package/src/converters/ai-sdk.ts +298 -0
  75. package/src/converters/claude.ts +334 -0
  76. package/src/converters/index.ts +44 -0
  77. package/src/converters/openai.ts +302 -0
  78. package/src/converters/openinference.ts +673 -0
  79. package/src/converters/vercel.ts +264 -0
  80. package/src/index.ts +121 -0
  81. package/src/langchain-handler.ts +722 -0
  82. package/src/mastra-exporter.ts +650 -0
  83. package/src/span-processor.ts +418 -0
  84. package/src/tracing-processor.ts +548 -0
  85. package/src/types/genai.ts +264 -0
  86. package/src/types.ts +13 -0
  87. package/src/utils.ts +69 -0
  88. package/src/version.ts +2 -0
  89. package/tsconfig.json +8 -0
@@ -0,0 +1,722 @@
1
+ /**
2
+ * LangChain/LangGraph Callback Handler for Introspection.
3
+ *
4
+ * Hooks into LangChain's native callback system to capture LLM, tool, and
5
+ * chain interactions as gen_ai.* OTel spans for the Introspection backend.
6
+ *
7
+ * Similar to IntrospectionTracingProcessor for OpenAI Agents SDK and
8
+ * IntrospectionClaudeHooks for Claude Agent SDK.
9
+ *
10
+ * **Important:** This module requires `@langchain/core` at runtime.
11
+ * Import via the subpath: `@introspection-sdk/introspection-node/langchain`
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { IntrospectionCallbackHandler } from "@introspection-sdk/introspection-node/langchain";
16
+ * import { ChatOpenAI } from "@langchain/openai";
17
+ *
18
+ * const handler = new IntrospectionCallbackHandler({
19
+ * serviceName: "my-langchain-app",
20
+ * });
21
+ *
22
+ * const model = new ChatOpenAI({ modelName: "gpt-4o" });
23
+ * const response = await model.invoke("Hello!", { callbacks: [handler] });
24
+ *
25
+ * await handler.shutdown();
26
+ * ```
27
+ */
28
+
29
+ import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
30
+ import type { Serialized } from "@langchain/core/load/serializable";
31
+ import type { LLMResult } from "@langchain/core/outputs";
32
+ import type { BaseMessage } from "@langchain/core/messages";
33
+ import type { ChainValues } from "@langchain/core/utils/types";
34
+
35
+ import {
36
+ context as otelContext,
37
+ trace as otelTrace,
38
+ type Span as OtelSpan,
39
+ } from "@opentelemetry/api";
40
+ import {
41
+ BasicTracerProvider,
42
+ BatchSpanProcessor,
43
+ SimpleSpanProcessor,
44
+ type SpanExporter,
45
+ type IdGenerator,
46
+ } from "@opentelemetry/sdk-trace-base";
47
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
48
+ import { resourceFromAttributes } from "@opentelemetry/resources";
49
+ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
50
+ import { randomUUID } from "crypto";
51
+
52
+ import { logger } from "./utils.js";
53
+ import { VERSION } from "./version.js";
54
+ import type {
55
+ InputMessage,
56
+ OutputMessage,
57
+ MessagePart,
58
+ } from "./types/genai.js";
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Configuration
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /** Advanced options for testing and customization. */
65
+ export interface LangChainHandlerAdvancedOptions {
66
+ /** Custom span exporter for testing. */
67
+ spanExporter?: SpanExporter;
68
+ /** Custom ID generator for testing. */
69
+ idGenerator?: IdGenerator;
70
+ /** Use SimpleSpanProcessor instead of BatchSpanProcessor. */
71
+ useSimpleSpanProcessor?: boolean;
72
+ }
73
+
74
+ /** Configuration for {@link IntrospectionCallbackHandler}. */
75
+ export interface IntrospectionCallbackHandlerOptions {
76
+ /** Authentication token (env: INTROSPECTION_TOKEN). */
77
+ token?: string;
78
+ /** Base URL for the API (env: INTROSPECTION_BASE_URL). */
79
+ baseUrl?: string;
80
+ /** Service name for telemetry (env: INTROSPECTION_SERVICE_NAME). */
81
+ serviceName?: string;
82
+ /** Additional headers to include in requests. */
83
+ additionalHeaders?: Record<string, string>;
84
+ /** Advanced options for testing. */
85
+ advanced?: LangChainHandlerAdvancedOptions;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Handler
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /**
93
+ * LangChain callback handler that captures LLM, tool, and chain events
94
+ * as gen_ai.* OTel spans and exports them to the Introspection backend.
95
+ *
96
+ * Pass as a callback to any LangChain invoke call, or set globally via
97
+ * `setGlobalHandler()`.
98
+ */
99
+ export class IntrospectionCallbackHandler extends BaseCallbackHandler {
100
+ name = "IntrospectionCallbackHandler";
101
+
102
+ private _tracerProvider: BasicTracerProvider;
103
+ private _tracer: ReturnType<BasicTracerProvider["getTracer"]>;
104
+ private _spans: Map<string, OtelSpan> = new Map();
105
+ private _rootSpan: OtelSpan | null = null;
106
+ private _conversationId: string;
107
+ // Track span names and parents so children can resolve gen_ai.agent.name
108
+ private _spanNames: Map<string, string> = new Map();
109
+ private _spanParents: Map<string, string> = new Map();
110
+ // LangChain wrapper names to skip when resolving agent names
111
+ private static _wrapperNames = new Set([
112
+ "RunnableSequence",
113
+ "RunnableParallel",
114
+ "RunnableMap",
115
+ "RunnableLambda",
116
+ "RunnableRetry",
117
+ "_ConfigurableModel",
118
+ "ChatOpenAI",
119
+ "ChatAnthropic",
120
+ "ChatGoogleGenerativeAI",
121
+ "ChatGroq",
122
+ ]);
123
+ // Store LLM input messages per runId for use in handleLLMEnd
124
+ private _llmInputs: Map<string, InputMessage[]> = new Map();
125
+
126
+ constructor(options: IntrospectionCallbackHandlerOptions = {}) {
127
+ super();
128
+
129
+ const advanced = options.advanced;
130
+ const serviceName =
131
+ options.serviceName || process.env.INTROSPECTION_SERVICE_NAME;
132
+ const resource = serviceName
133
+ ? resourceFromAttributes({ [ATTR_SERVICE_NAME]: serviceName })
134
+ : undefined;
135
+
136
+ if (!advanced?.spanExporter) {
137
+ const token = options.token || process.env.INTROSPECTION_TOKEN;
138
+ if (!token) {
139
+ throw new Error("INTROSPECTION_TOKEN is required");
140
+ }
141
+
142
+ const baseUrl =
143
+ options.baseUrl ||
144
+ process.env.INTROSPECTION_BASE_URL ||
145
+ "https://otel.introspection.dev";
146
+
147
+ const endpoint = baseUrl.endsWith("/v1/traces")
148
+ ? baseUrl
149
+ : `${baseUrl.replace(/\/$/, "")}/v1/traces`;
150
+
151
+ const headers: Record<string, string> = {
152
+ "User-Agent": `introspection-sdk/${VERSION}`,
153
+ Authorization: `Bearer ${token}`,
154
+ ...options.additionalHeaders,
155
+ };
156
+
157
+ logger.info(
158
+ `IntrospectionCallbackHandler initialized: endpoint=${endpoint}`,
159
+ );
160
+
161
+ const spanExporter = new OTLPTraceExporter({ url: endpoint, headers });
162
+
163
+ const effectiveBatchSize =
164
+ token.startsWith("intro_dev") || token.startsWith("intro_staging")
165
+ ? 1
166
+ : undefined;
167
+ const useSimple =
168
+ advanced?.useSimpleSpanProcessor || effectiveBatchSize === 1;
169
+
170
+ const spanProcessor = useSimple
171
+ ? new SimpleSpanProcessor(spanExporter)
172
+ : new BatchSpanProcessor(spanExporter, {
173
+ scheduledDelayMillis: 1000,
174
+ ...(effectiveBatchSize
175
+ ? { maxExportBatchSize: effectiveBatchSize }
176
+ : {}),
177
+ });
178
+
179
+ this._tracerProvider = new BasicTracerProvider({
180
+ resource,
181
+ idGenerator: advanced?.idGenerator,
182
+ spanProcessors: [spanProcessor],
183
+ });
184
+ } else {
185
+ logger.info(
186
+ "IntrospectionCallbackHandler initialized in test mode with custom exporter",
187
+ );
188
+
189
+ const spanProcessor = advanced.useSimpleSpanProcessor
190
+ ? new SimpleSpanProcessor(advanced.spanExporter)
191
+ : new BatchSpanProcessor(advanced.spanExporter, {
192
+ scheduledDelayMillis: 100,
193
+ });
194
+
195
+ this._tracerProvider = new BasicTracerProvider({
196
+ resource,
197
+ idGenerator: advanced.idGenerator,
198
+ spanProcessors: [spanProcessor],
199
+ });
200
+ }
201
+
202
+ this._tracer = this._tracerProvider.getTracer("langchain", VERSION);
203
+ this._conversationId = `intro_conv_${randomUUID().replace(/-/g, "")}`;
204
+ }
205
+
206
+ /** Get or create a root span so all callbacks share the same traceId. */
207
+ private _ensureRoot(): OtelSpan {
208
+ if (!this._rootSpan) {
209
+ this._rootSpan = this._tracer.startSpan("langchain-run");
210
+ this._rootSpan.setAttribute(
211
+ "gen_ai.conversation.id",
212
+ this._conversationId,
213
+ );
214
+ }
215
+ return this._rootSpan;
216
+ }
217
+
218
+ /** Create a child span under the root (or under a parent if provided).
219
+ * Sets gen_ai.agent.name to the parent span's name for hierarchy. */
220
+ private _createChildSpan(
221
+ name: string,
222
+ runId: string,
223
+ parentRunId?: string,
224
+ ): OtelSpan {
225
+ const parent =
226
+ (parentRunId ? this._spans.get(parentRunId) : undefined) ||
227
+ this._ensureRoot();
228
+ const ctx = otelTrace.setSpan(otelContext.active(), parent);
229
+ const span = this._tracer.startSpan(name, {}, ctx);
230
+ this._spanNames.set(runId, name);
231
+ if (parentRunId) {
232
+ this._spanParents.set(runId, parentRunId);
233
+ }
234
+
235
+ // Walk up parents to find first non-wrapper name for gen_ai.agent.name
236
+ const agentName = this._findAgentName(parentRunId);
237
+ if (agentName) {
238
+ span.setAttribute("gen_ai.agent.name", agentName);
239
+ }
240
+
241
+ return span;
242
+ }
243
+
244
+ // -------------------------------------------------------------------------
245
+ // Chat model callbacks (preferred over LLM callbacks for chat models)
246
+ // -------------------------------------------------------------------------
247
+
248
+ handleChatModelStart(
249
+ llm: Serialized,
250
+ messages: BaseMessage[][],
251
+ runId: string,
252
+ parentRunId?: string,
253
+ extraParams?: Record<string, unknown>,
254
+ _tags?: string[],
255
+ metadata?: Record<string, unknown>,
256
+ runName?: string,
257
+ ): void {
258
+ const modelName = this._extractModelName(llm, extraParams);
259
+ const spanName = modelName ? `chat ${modelName}` : runName || "chat";
260
+
261
+ const otelSpan = this._createChildSpan(spanName, runId, parentRunId);
262
+ this._spans.set(runId, otelSpan);
263
+
264
+ const conversationId = this._getConversationId(metadata);
265
+
266
+ otelSpan.setAttribute("gen_ai.operation.name", "chat");
267
+ otelSpan.setAttribute("gen_ai.conversation.id", conversationId);
268
+ otelSpan.setAttribute("openinference.span.kind", "LLM");
269
+
270
+ if (modelName) {
271
+ otelSpan.setAttribute("gen_ai.request.model", modelName);
272
+ }
273
+
274
+ const flatMessages = messages[0] || [];
275
+ const { inputMessages, systemInstructions } =
276
+ this._convertMessages(flatMessages);
277
+
278
+ if (inputMessages.length > 0) {
279
+ otelSpan.setAttribute(
280
+ "gen_ai.input.messages",
281
+ JSON.stringify(inputMessages),
282
+ );
283
+ this._llmInputs.set(runId, inputMessages);
284
+ }
285
+
286
+ if (systemInstructions.length > 0) {
287
+ otelSpan.setAttribute(
288
+ "gen_ai.system_instructions",
289
+ JSON.stringify(
290
+ systemInstructions.map((s) => ({ type: "text", content: s })),
291
+ ),
292
+ );
293
+ }
294
+
295
+ const provider = this._extractProvider(llm);
296
+ if (provider) {
297
+ otelSpan.setAttribute("gen_ai.system", provider);
298
+ }
299
+
300
+ const invocationParams = extraParams?.["invocation_params"] as
301
+ | Record<string, unknown>
302
+ | undefined;
303
+
304
+ const tools = invocationParams?.["tools"] as
305
+ | Array<Record<string, unknown>>
306
+ | undefined;
307
+ if (tools && tools.length > 0) {
308
+ otelSpan.setAttribute(
309
+ "gen_ai.tool.definitions",
310
+ JSON.stringify(
311
+ tools.map((t) => {
312
+ const fn = (t.function || t) as Record<string, unknown>;
313
+ return {
314
+ type: (t.type as string) || "function",
315
+ name: (fn.name || "") as string,
316
+ description: (fn.description || "") as string,
317
+ parameters: fn.parameters,
318
+ };
319
+ }),
320
+ ),
321
+ );
322
+ }
323
+
324
+ if (invocationParams?.["temperature"] != null) {
325
+ otelSpan.setAttribute(
326
+ "gen_ai.request.temperature",
327
+ invocationParams["temperature"] as number,
328
+ );
329
+ }
330
+ }
331
+
332
+ handleLLMStart(
333
+ llm: Serialized,
334
+ prompts: string[],
335
+ runId: string,
336
+ parentRunId?: string,
337
+ extraParams?: Record<string, unknown>,
338
+ _tags?: string[],
339
+ metadata?: Record<string, unknown>,
340
+ runName?: string,
341
+ ): void {
342
+ const modelName = this._extractModelName(llm, extraParams);
343
+ const spanName = modelName ? `chat ${modelName}` : runName || "llm";
344
+
345
+ const otelSpan = this._createChildSpan(spanName, runId, parentRunId);
346
+ this._spans.set(runId, otelSpan);
347
+
348
+ const conversationId = this._getConversationId(metadata);
349
+
350
+ otelSpan.setAttribute("gen_ai.operation.name", "chat");
351
+ otelSpan.setAttribute("gen_ai.conversation.id", conversationId);
352
+ otelSpan.setAttribute("openinference.span.kind", "LLM");
353
+
354
+ if (modelName) {
355
+ otelSpan.setAttribute("gen_ai.request.model", modelName);
356
+ }
357
+
358
+ if (prompts.length > 0) {
359
+ const inputMessages: InputMessage[] = prompts.map((p) => ({
360
+ role: "user" as const,
361
+ parts: [{ type: "text" as const, content: p }],
362
+ }));
363
+ otelSpan.setAttribute(
364
+ "gen_ai.input.messages",
365
+ JSON.stringify(inputMessages),
366
+ );
367
+ this._llmInputs.set(runId, inputMessages);
368
+ }
369
+ }
370
+
371
+ handleLLMEnd(output: LLMResult, runId: string): void {
372
+ const otelSpan = this._spans.get(runId);
373
+ if (!otelSpan) return;
374
+
375
+ this._spans.delete(runId);
376
+ this._spanNames.delete(runId);
377
+ this._llmInputs.delete(runId);
378
+
379
+ const generations = output.generations?.[0];
380
+ if (generations && generations.length > 0) {
381
+ const parts: MessagePart[] = [];
382
+ for (const gen of generations) {
383
+ if (gen.text) {
384
+ parts.push({ type: "text", content: gen.text });
385
+ }
386
+ const msg = (gen as unknown as Record<string, unknown>).message as
387
+ | Record<string, unknown>
388
+ | undefined;
389
+ const kwargs = (msg?.["kwargs"] || msg) as
390
+ | Record<string, unknown>
391
+ | undefined;
392
+ const additionalKwargs = kwargs?.["additional_kwargs"] as
393
+ | Record<string, unknown>
394
+ | undefined;
395
+ const toolCalls = (additionalKwargs?.["tool_calls"] ||
396
+ kwargs?.["tool_calls"]) as Array<Record<string, unknown>> | undefined;
397
+ if (toolCalls) {
398
+ for (const tc of toolCalls) {
399
+ const fn = (tc.function || tc) as Record<string, unknown>;
400
+ parts.push({
401
+ type: "tool_call",
402
+ name: (fn.name || tc.name || "") as string,
403
+ id: (tc.id || "") as string,
404
+ arguments: (fn.arguments || tc.args || "") as string,
405
+ });
406
+ }
407
+ }
408
+ }
409
+
410
+ if (parts.length > 0) {
411
+ const outputMessages: OutputMessage[] = [{ role: "assistant", parts }];
412
+ otelSpan.setAttribute(
413
+ "gen_ai.output.messages",
414
+ JSON.stringify(outputMessages),
415
+ );
416
+ }
417
+ }
418
+
419
+ const usage = output.llmOutput as Record<string, unknown> | undefined;
420
+ const tokenUsage = (usage?.["tokenUsage"] ||
421
+ usage?.["token_usage"] ||
422
+ usage?.["usage"]) as Record<string, unknown> | undefined;
423
+ if (tokenUsage) {
424
+ const inputTokens =
425
+ tokenUsage["promptTokens"] ||
426
+ tokenUsage["prompt_tokens"] ||
427
+ tokenUsage["input_tokens"];
428
+ const outputTokens =
429
+ tokenUsage["completionTokens"] ||
430
+ tokenUsage["completion_tokens"] ||
431
+ tokenUsage["output_tokens"];
432
+ if (typeof inputTokens === "number") {
433
+ otelSpan.setAttribute("gen_ai.usage.input_tokens", inputTokens);
434
+ }
435
+ if (typeof outputTokens === "number") {
436
+ otelSpan.setAttribute("gen_ai.usage.output_tokens", outputTokens);
437
+ }
438
+ }
439
+
440
+ const model = usage?.["model"] || usage?.["model_name"];
441
+ if (typeof model === "string") {
442
+ otelSpan.setAttribute("gen_ai.response.model", model);
443
+ }
444
+
445
+ // gen_ai.response.id — required by the server for conversation tracking.
446
+ const responseId = usage?.["id"] || usage?.["system_fingerprint"];
447
+ otelSpan.setAttribute(
448
+ "gen_ai.response.id",
449
+ typeof responseId === "string" ? responseId : `langchain-${runId}`,
450
+ );
451
+
452
+ otelSpan.end();
453
+ }
454
+
455
+ handleLLMError(err: Error, runId: string): void {
456
+ const otelSpan = this._spans.get(runId);
457
+ if (!otelSpan) return;
458
+ this._spans.delete(runId);
459
+ this._spanNames.delete(runId);
460
+ this._llmInputs.delete(runId);
461
+ otelSpan.setAttribute("error", true);
462
+ otelSpan.setAttribute("error.message", err.message);
463
+ otelSpan.end();
464
+ }
465
+
466
+ // -------------------------------------------------------------------------
467
+ // Chain callbacks
468
+ // -------------------------------------------------------------------------
469
+
470
+ handleChainStart(
471
+ chain: Serialized,
472
+ _inputs: ChainValues,
473
+ runId: string,
474
+ _runType?: string,
475
+ _tags?: string[],
476
+ metadata?: Record<string, unknown>,
477
+ runName?: string,
478
+ parentRunId?: string,
479
+ ): void {
480
+ const name =
481
+ runName || chain?.name || chain?.id?.[chain.id.length - 1] || "chain";
482
+
483
+ const otelSpan = this._createChildSpan(name, runId, parentRunId);
484
+ this._spans.set(runId, otelSpan);
485
+
486
+ const conversationId = this._getConversationId(metadata);
487
+ otelSpan.setAttribute("gen_ai.conversation.id", conversationId);
488
+ }
489
+
490
+ handleChainEnd(_outputs: ChainValues, runId: string): void {
491
+ const otelSpan = this._spans.get(runId);
492
+ if (!otelSpan) return;
493
+ this._spans.delete(runId);
494
+ otelSpan.end();
495
+ }
496
+
497
+ handleChainError(err: Error, runId: string): void {
498
+ const otelSpan = this._spans.get(runId);
499
+ if (!otelSpan) return;
500
+ this._spans.delete(runId);
501
+ otelSpan.setAttribute("error", true);
502
+ otelSpan.setAttribute("error.message", err.message);
503
+ otelSpan.end();
504
+ }
505
+
506
+ // -------------------------------------------------------------------------
507
+ // Tool callbacks
508
+ // -------------------------------------------------------------------------
509
+
510
+ handleToolStart(
511
+ tool: Serialized,
512
+ input: string,
513
+ runId: string,
514
+ parentRunId?: string,
515
+ _tags?: string[],
516
+ _metadata?: Record<string, unknown>,
517
+ runName?: string,
518
+ ): void {
519
+ const toolName =
520
+ runName || tool?.name || tool?.id?.[tool.id.length - 1] || "tool";
521
+
522
+ const otelSpan = this._createChildSpan(toolName, runId, parentRunId);
523
+ this._spans.set(runId, otelSpan);
524
+
525
+ otelSpan.setAttribute("gen_ai.tool.name", toolName);
526
+ otelSpan.setAttribute("openinference.span.kind", "TOOL");
527
+ otelSpan.setAttribute("gen_ai.tool.input", input);
528
+ otelSpan.setAttribute("gen_ai.conversation.id", this._conversationId);
529
+ }
530
+
531
+ handleToolEnd(output: unknown, runId: string): void {
532
+ const otelSpan = this._spans.get(runId);
533
+ if (!otelSpan) return;
534
+ this._spans.delete(runId);
535
+
536
+ if (output != null) {
537
+ otelSpan.setAttribute(
538
+ "gen_ai.tool.output",
539
+ typeof output === "string" ? output : JSON.stringify(output),
540
+ );
541
+ }
542
+
543
+ otelSpan.end();
544
+ }
545
+
546
+ handleToolError(err: Error, runId: string): void {
547
+ const otelSpan = this._spans.get(runId);
548
+ if (!otelSpan) return;
549
+ this._spans.delete(runId);
550
+ otelSpan.setAttribute("error", true);
551
+ otelSpan.setAttribute("error.message", err.message);
552
+ otelSpan.end();
553
+ }
554
+
555
+ // -------------------------------------------------------------------------
556
+ // Lifecycle
557
+ // -------------------------------------------------------------------------
558
+
559
+ async shutdown(): Promise<void> {
560
+ for (const span of this._spans.values()) span.end();
561
+ this._spans.clear();
562
+ if (this._rootSpan) {
563
+ this._rootSpan.end();
564
+ this._rootSpan = null;
565
+ }
566
+ await this._tracerProvider.shutdown();
567
+ }
568
+
569
+ async forceFlush(): Promise<void> {
570
+ await this._tracerProvider.forceFlush();
571
+ }
572
+
573
+ // -------------------------------------------------------------------------
574
+ // Helpers
575
+ // -------------------------------------------------------------------------
576
+
577
+ private _extractModelName(
578
+ llm: Serialized,
579
+ extraParams?: Record<string, unknown>,
580
+ ): string | undefined {
581
+ const invocationParams = extraParams?.["invocation_params"] as
582
+ | Record<string, unknown>
583
+ | undefined;
584
+ if (invocationParams) {
585
+ const model =
586
+ invocationParams["model"] ||
587
+ invocationParams["model_name"] ||
588
+ invocationParams["modelName"];
589
+ if (typeof model === "string") return model;
590
+ }
591
+
592
+ const kwargs = (llm as unknown as Record<string, unknown>)?.kwargs as
593
+ | Record<string, unknown>
594
+ | undefined;
595
+ if (kwargs) {
596
+ const model =
597
+ kwargs["model"] || kwargs["model_name"] || kwargs["modelName"];
598
+ if (typeof model === "string") return model;
599
+ }
600
+
601
+ return undefined;
602
+ }
603
+
604
+ private _extractProvider(llm: Serialized): string | undefined {
605
+ const id = llm?.id;
606
+ if (Array.isArray(id) && id.length > 0) {
607
+ return id[id.length - 1] as string;
608
+ }
609
+ return undefined;
610
+ }
611
+
612
+ /** Walk up the span tree to find the first non-wrapper span name. */
613
+ private _findAgentName(runId?: string): string | undefined {
614
+ let current = runId;
615
+ for (let i = 0; i < 20 && current; i++) {
616
+ const name = this._spanNames.get(current);
617
+ if (name && !IntrospectionCallbackHandler._wrapperNames.has(name)) {
618
+ return name;
619
+ }
620
+ current = this._spanParents.get(current);
621
+ }
622
+ return undefined;
623
+ }
624
+
625
+ private _getConversationId(metadata?: Record<string, unknown>): string {
626
+ const metaConvId = metadata?.["gen_ai.conversation.id"] as
627
+ | string
628
+ | undefined;
629
+ return metaConvId || this._conversationId;
630
+ }
631
+
632
+ private _convertMessages(messages: BaseMessage[]): {
633
+ inputMessages: InputMessage[];
634
+ systemInstructions: string[];
635
+ } {
636
+ const inputMessages: InputMessage[] = [];
637
+ const systemInstructions: string[] = [];
638
+
639
+ for (const msg of messages) {
640
+ const msgType =
641
+ (msg as unknown as { _getType?: () => string })._getType?.() ||
642
+ (msg as unknown as Record<string, unknown>).type ||
643
+ "unknown";
644
+ const role = this._mapRole(msgType as string);
645
+ const content = this._extractContent(msg.content);
646
+
647
+ if (role === "system") {
648
+ systemInstructions.push(
649
+ typeof msg.content === "string"
650
+ ? msg.content
651
+ : JSON.stringify(msg.content),
652
+ );
653
+ continue;
654
+ }
655
+
656
+ const parts: MessagePart[] = [];
657
+
658
+ if (role === "tool") {
659
+ const toolCallId =
660
+ ((msg as unknown as Record<string, unknown>)
661
+ .tool_call_id as string) || "";
662
+ parts.push({
663
+ type: "tool_call_response",
664
+ id: toolCallId,
665
+ response: content,
666
+ });
667
+ } else if (content) {
668
+ parts.push({ type: "text", content });
669
+ }
670
+
671
+ const toolCalls = (msg as unknown as Record<string, unknown>)
672
+ .tool_calls as Array<Record<string, unknown>> | undefined;
673
+ if (toolCalls) {
674
+ for (const tc of toolCalls) {
675
+ parts.push({
676
+ type: "tool_call",
677
+ name: (tc.name || "") as string,
678
+ id: (tc.id || "") as string,
679
+ arguments:
680
+ typeof tc.args === "string" ? tc.args : JSON.stringify(tc.args),
681
+ });
682
+ }
683
+ }
684
+
685
+ if (parts.length > 0) {
686
+ inputMessages.push({ role, parts });
687
+ }
688
+ }
689
+
690
+ return { inputMessages, systemInstructions };
691
+ }
692
+
693
+ private _mapRole(msgType: string): "user" | "assistant" | "system" | "tool" {
694
+ switch (msgType) {
695
+ case "human":
696
+ return "user";
697
+ case "ai":
698
+ return "assistant";
699
+ case "system":
700
+ return "system";
701
+ case "tool":
702
+ return "tool";
703
+ default:
704
+ return "user";
705
+ }
706
+ }
707
+
708
+ private _extractContent(content: unknown): string {
709
+ if (typeof content === "string") return content;
710
+ if (Array.isArray(content)) {
711
+ return content
712
+ .map((part: unknown) => {
713
+ const p = part as Record<string, unknown>;
714
+ if (p.type === "text") return (p.text || "") as string;
715
+ return "";
716
+ })
717
+ .filter(Boolean)
718
+ .join("");
719
+ }
720
+ return String(content || "");
721
+ }
722
+ }