@cloudbase/agent-observability 0.0.16

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.
@@ -0,0 +1,896 @@
1
+ /**
2
+ * LangChain Callback Handler for AG-Kit Observability
3
+ *
4
+ * Converts LangChain callback events into AG-Kit observations with OpenInference semantics.
5
+ */
6
+
7
+ import type { AgentAction, AgentFinish } from "@langchain/core/agents";
8
+ import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
9
+ import type { Document } from "@langchain/core/documents";
10
+ import type { Serialized } from "@langchain/core/load/serializable";
11
+ import {
12
+ AIMessage,
13
+ AIMessageChunk,
14
+ BaseMessage,
15
+ type UsageMetadata,
16
+ type BaseMessageFields,
17
+ type MessageContent,
18
+ } from "@langchain/core/messages";
19
+ import type { Generation, LLMResult } from "@langchain/core/outputs";
20
+ import type { ChainValues } from "@langchain/core/utils/types";
21
+
22
+ import {
23
+ startObservation,
24
+ type ObservationLLM,
25
+ type ObservationSpan,
26
+ type ObservationTool,
27
+ type Observation,
28
+ type ObservationAttributes,
29
+ } from "../index.js";
30
+ import type { SpanContext } from "@opentelemetry/api";
31
+ import { type Logger, noopLogger } from "@cloudbase/agent-shared";
32
+
33
+ /**
34
+ * Constructor parameters for CallbackHandler.
35
+ *
36
+ * @public
37
+ */
38
+ type ConstructorParams = {
39
+ userId?: string;
40
+ sessionId?: string;
41
+ tags?: string[];
42
+ version?: string;
43
+ traceMetadata?: Record<string, unknown>;
44
+ adapterName?: string; // e.g., "LangGraph" or "LangChain"
45
+ /** Logger for debug output. Defaults to noopLogger (silent). */
46
+ logger?: Logger;
47
+ };
48
+
49
+ /**
50
+ * Message format for LLM input/output.
51
+ *
52
+ * @public
53
+ */
54
+ export type LlmMessage = {
55
+ role: string;
56
+ content: BaseMessageFields["content"];
57
+ additional_kwargs?: BaseMessageFields["additional_kwargs"];
58
+ };
59
+
60
+ /**
61
+ * Anonymous message format (without role).
62
+ *
63
+ * @public
64
+ */
65
+ export type AnonymousLlmMessage = {
66
+ content: BaseMessageFields["content"];
67
+ additional_kwargs?: BaseMessageFields["additional_kwargs"];
68
+ };
69
+
70
+ /**
71
+ * Prompt information for linking to generations.
72
+ *
73
+ * @public
74
+ */
75
+ type PromptInfo = {
76
+ name: string;
77
+ version: number;
78
+ isFallback: boolean;
79
+ };
80
+
81
+ /**
82
+ * LangChain Callback Handler for AG-Kit Observability.
83
+ *
84
+ * This handler intercepts LangChain callbacks and converts them into
85
+ * AG-Kit observations following OpenInference semantic conventions.
86
+ *
87
+ * @public
88
+ */
89
+ export class CallbackHandler extends BaseCallbackHandler {
90
+ name = "ObservabilityCallbackHandler";
91
+
92
+ private userId?: string;
93
+ private version?: string;
94
+ private sessionId?: string;
95
+ private tags: string[];
96
+ private traceMetadata?: Record<string, unknown>;
97
+
98
+ private completionStartTimes: Record<string, Date> = {};
99
+ private promptToParentRunMap;
100
+ private runMap: Map<string, Observation> = new Map();
101
+
102
+ public last_trace_id: string | null = null;
103
+
104
+ // External parent context from AG-UI.Server span
105
+ private externalParentSpanContext?: SpanContext;
106
+
107
+ // Adapter name for ROOT span prefix
108
+ private adapterName?: string;
109
+
110
+ // Logger for debug output (defaults to noopLogger for silent operation)
111
+ private logger: Logger;
112
+
113
+ constructor(params?: ConstructorParams) {
114
+ super();
115
+
116
+ this.sessionId = params?.sessionId;
117
+ this.userId = params?.userId;
118
+ this.tags = params?.tags ?? [];
119
+ this.traceMetadata = params?.traceMetadata;
120
+ this.version = params?.version;
121
+ this.adapterName = params?.adapterName;
122
+ this.logger = params?.logger ?? noopLogger;
123
+
124
+ this.promptToParentRunMap = new Map<string, PromptInfo>();
125
+ }
126
+
127
+ /**
128
+ * Set external parent SpanContext from AG-UI.Server span.
129
+ * This allows the CallbackHandler to link LangChain/LangGraph spans
130
+ * to the server-level span, creating a unified trace hierarchy.
131
+ *
132
+ * @param spanContext - SpanContext from the AG-UI.Server span
133
+ * @public
134
+ */
135
+ setExternalParentContext(spanContext: SpanContext): void {
136
+ this.externalParentSpanContext = spanContext;
137
+ }
138
+
139
+ async handleLLMNewToken(
140
+ token: string,
141
+ _idx: any,
142
+ runId: string,
143
+ _parentRunId?: string,
144
+ _tags?: string[],
145
+ _fields?: any
146
+ ): Promise<void> {
147
+ if (runId && !(runId in this.completionStartTimes)) {
148
+ this.logger.debug?.(`LLM first streaming token: ${runId}`);
149
+ this.completionStartTimes[runId] = new Date();
150
+ }
151
+ }
152
+
153
+ async handleChainStart(
154
+ chain: Serialized,
155
+ inputs: ChainValues,
156
+ runId: string,
157
+ parentRunId?: string | undefined,
158
+ tags?: string[] | undefined,
159
+ metadata?: Record<string, unknown> | undefined,
160
+ runType?: string,
161
+ name?: string
162
+ ): Promise<void> {
163
+ try {
164
+ this.logger.debug?.(`Chain start with Id: ${runId}`);
165
+
166
+ const runName = name ?? chain.id.at(-1)?.toString() ?? "Langchain Run";
167
+
168
+ this.registerPromptInfo(parentRunId, metadata);
169
+
170
+ let finalInput: string | ChainValues = inputs;
171
+ if (
172
+ typeof inputs === "object" &&
173
+ "input" in inputs &&
174
+ Array.isArray(inputs["input"]) &&
175
+ inputs["input"].every((m: unknown) => m instanceof BaseMessage)
176
+ ) {
177
+ finalInput = inputs["input"].map((m: BaseMessage) =>
178
+ this.extractChatMessageContent(m)
179
+ );
180
+ } else if (
181
+ typeof inputs === "object" &&
182
+ "messages" in inputs &&
183
+ Array.isArray(inputs["messages"]) &&
184
+ inputs["messages"].every((m: unknown) => m instanceof BaseMessage)
185
+ ) {
186
+ finalInput = inputs["messages"].map((m: BaseMessage) =>
187
+ this.extractChatMessageContent(m)
188
+ );
189
+ } else if (
190
+ typeof inputs === "object" &&
191
+ "content" in inputs &&
192
+ typeof inputs["content"] === "string"
193
+ ) {
194
+ finalInput = inputs["content"];
195
+ }
196
+
197
+ const observation = this.startAndRegisterObservation({
198
+ runName,
199
+ parentRunId,
200
+ runId,
201
+ tags,
202
+ metadata,
203
+ attributes: {
204
+ input: finalInput,
205
+ },
206
+ asType: "span",
207
+ });
208
+
209
+ const traceTags = [...new Set([...(tags ?? []), ...this.tags])];
210
+
211
+ if (!parentRunId) {
212
+ observation.updateTrace({
213
+ tags: traceTags,
214
+ userId:
215
+ metadata &&
216
+ "userId" in metadata &&
217
+ typeof metadata["userId"] === "string"
218
+ ? metadata["userId"]
219
+ : this.userId,
220
+ sessionId:
221
+ metadata &&
222
+ "sessionId" in metadata &&
223
+ typeof metadata["sessionId"] === "string"
224
+ ? metadata["sessionId"]
225
+ : this.sessionId,
226
+ metadata: this.traceMetadata,
227
+ version: this.version,
228
+ });
229
+ }
230
+ } catch (e) {
231
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
232
+ }
233
+ }
234
+
235
+ async handleAgentAction(
236
+ action: AgentAction,
237
+ runId: string,
238
+ parentRunId?: string
239
+ ): Promise<void> {
240
+ try {
241
+ this.logger.debug?.(`Agent action ${action.tool} with ID: ${runId}`);
242
+ this.startAndRegisterObservation({
243
+ runId,
244
+ parentRunId,
245
+ runName: action.tool,
246
+ attributes: {
247
+ input: action,
248
+ },
249
+ asType: "tool",
250
+ });
251
+ } catch (e) {
252
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
253
+ }
254
+ }
255
+
256
+ async handleAgentEnd?(
257
+ action: AgentFinish,
258
+ runId: string,
259
+ _parentRunId?: string
260
+ ): Promise<void> {
261
+ try {
262
+ this.logger.debug?.(`Agent finish with ID: ${runId}`);
263
+ this.handleObservationEnd({
264
+ runId,
265
+ attributes: { output: action },
266
+ });
267
+ } catch (e) {
268
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
269
+ }
270
+ }
271
+
272
+ async handleChainError(
273
+ err: any,
274
+ runId: string,
275
+ _parentRunId?: string | undefined
276
+ ): Promise<void> {
277
+ try {
278
+ this.logger.debug?.(`Chain error: ${err} with ID: ${runId}`);
279
+ this.handleObservationEnd({
280
+ runId,
281
+ attributes: {
282
+ level: "ERROR",
283
+ statusMessage: err.toString(),
284
+ },
285
+ });
286
+ } catch (e) {
287
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
288
+ }
289
+ }
290
+
291
+ async handleGenerationStart(
292
+ llm: Serialized,
293
+ messages: (LlmMessage | MessageContent | AnonymousLlmMessage)[],
294
+ runId: string,
295
+ parentRunId?: string | undefined,
296
+ extraParams?: Record<string, unknown> | undefined,
297
+ tags?: string[] | undefined,
298
+ metadata?: Record<string, unknown> | undefined,
299
+ name?: string
300
+ ): Promise<void> {
301
+ this.logger.debug?.(
302
+ `Generation start with ID: ${runId} and parentRunId ${parentRunId}`
303
+ );
304
+
305
+ const runName = name ?? llm.id.at(-1)?.toString() ?? "Langchain Generation";
306
+
307
+ const modelParameters: Record<string, any> = {};
308
+ const invocationParams = extraParams?.["invocation_params"];
309
+
310
+ for (const [key, value] of Object.entries({
311
+ temperature: (invocationParams as any)?.temperature,
312
+ max_tokens: (invocationParams as any)?.max_tokens,
313
+ top_p: (invocationParams as any)?.top_p,
314
+ frequency_penalty: (invocationParams as any)?.frequency_penalty,
315
+ presence_penalty: (invocationParams as any)?.presence_penalty,
316
+ request_timeout: (invocationParams as any)?.request_timeout,
317
+ })) {
318
+ if (value !== undefined && value !== null) {
319
+ modelParameters[key] = value;
320
+ }
321
+ }
322
+
323
+ interface InvocationParams {
324
+ _type?: string;
325
+ model?: string;
326
+ model_name?: string;
327
+ repo_id?: string;
328
+ }
329
+
330
+ let extractedModelName: string | undefined;
331
+ if (extraParams) {
332
+ const invocationParamsModelName = (
333
+ extraParams.invocation_params as InvocationParams
334
+ ).model;
335
+ const metadataModelName =
336
+ metadata && "ls_model_name" in metadata
337
+ ? (metadata["ls_model_name"] as string)
338
+ : undefined;
339
+
340
+ extractedModelName = invocationParamsModelName ?? metadataModelName;
341
+ }
342
+
343
+ const registeredPrompt = this.promptToParentRunMap.get(
344
+ parentRunId ?? "root"
345
+ );
346
+ if (registeredPrompt && parentRunId) {
347
+ this.deregisterPromptInfo(parentRunId);
348
+ }
349
+
350
+ this.startAndRegisterObservation({
351
+ runId,
352
+ parentRunId,
353
+ metadata,
354
+ tags,
355
+ runName,
356
+ attributes: {
357
+ input: messages,
358
+ model: extractedModelName,
359
+ modelParameters: modelParameters,
360
+ },
361
+ asType: "llm",
362
+ });
363
+ }
364
+
365
+ async handleChatModelStart(
366
+ llm: Serialized,
367
+ messages: BaseMessage[][],
368
+ runId: string,
369
+ parentRunId?: string | undefined,
370
+ extraParams?: Record<string, unknown> | undefined,
371
+ tags?: string[] | undefined,
372
+ metadata?: Record<string, unknown> | undefined,
373
+ name?: string
374
+ ): Promise<void> {
375
+ try {
376
+ this.logger.debug?.(`Chat model start with ID: ${runId}`);
377
+
378
+ const prompts = messages.flatMap((message) =>
379
+ message.map((m) => this.extractChatMessageContent(m))
380
+ );
381
+
382
+ this.handleGenerationStart(
383
+ llm,
384
+ prompts,
385
+ runId,
386
+ parentRunId,
387
+ extraParams,
388
+ tags,
389
+ metadata,
390
+ name
391
+ );
392
+ } catch (e) {
393
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
394
+ }
395
+ }
396
+
397
+ async handleChainEnd(
398
+ outputs: ChainValues,
399
+ runId: string,
400
+ _parentRunId?: string | undefined
401
+ ): Promise<void> {
402
+ try {
403
+ this.logger.debug?.(`Chain end with ID: ${runId}`);
404
+
405
+ let finalOutput: ChainValues | string = outputs;
406
+ if (
407
+ typeof outputs === "object" &&
408
+ "output" in outputs &&
409
+ typeof outputs["output"] === "string"
410
+ ) {
411
+ finalOutput = outputs["output"];
412
+ } else if (
413
+ typeof outputs === "object" &&
414
+ "messages" in outputs &&
415
+ Array.isArray(outputs["messages"]) &&
416
+ outputs["messages"].every((m: unknown) => m instanceof BaseMessage)
417
+ ) {
418
+ finalOutput = {
419
+ messages: outputs.messages.map((message: BaseMessage) =>
420
+ this.extractChatMessageContent(message)
421
+ ),
422
+ };
423
+ }
424
+
425
+ this.handleObservationEnd({
426
+ runId,
427
+ attributes: {
428
+ output: finalOutput,
429
+ },
430
+ });
431
+ this.deregisterPromptInfo(runId);
432
+ } catch (e) {
433
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
434
+ }
435
+ }
436
+
437
+ async handleLLMStart(
438
+ llm: Serialized,
439
+ prompts: string[],
440
+ runId: string,
441
+ parentRunId?: string | undefined,
442
+ extraParams?: Record<string, unknown> | undefined,
443
+ tags?: string[] | undefined,
444
+ metadata?: Record<string, unknown> | undefined,
445
+ name?: string
446
+ ): Promise<void> {
447
+ try {
448
+ this.logger.debug?.(`LLM start with ID: ${runId}`);
449
+ this.handleGenerationStart(
450
+ llm,
451
+ prompts,
452
+ runId,
453
+ parentRunId,
454
+ extraParams,
455
+ tags,
456
+ metadata,
457
+ name
458
+ );
459
+ } catch (e) {
460
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
461
+ }
462
+ }
463
+
464
+ async handleToolStart(
465
+ tool: Serialized,
466
+ input: string,
467
+ runId: string,
468
+ parentRunId?: string | undefined,
469
+ tags?: string[] | undefined,
470
+ metadata?: Record<string, unknown> | undefined,
471
+ name?: string
472
+ ): Promise<void> {
473
+ try {
474
+ this.logger.debug?.(`Tool start with ID: ${runId}`);
475
+ this.startAndRegisterObservation({
476
+ runId,
477
+ parentRunId,
478
+ runName: name ?? tool.id.at(-1)?.toString() ?? "Tool execution",
479
+ attributes: {
480
+ input,
481
+ },
482
+ metadata,
483
+ tags,
484
+ asType: "tool",
485
+ });
486
+ } catch (e) {
487
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
488
+ }
489
+ }
490
+
491
+ async handleRetrieverStart(
492
+ retriever: Serialized,
493
+ query: string,
494
+ runId: string,
495
+ parentRunId?: string | undefined,
496
+ tags?: string[] | undefined,
497
+ metadata?: Record<string, unknown> | undefined,
498
+ name?: string
499
+ ): Promise<void> {
500
+ try {
501
+ this.logger.debug?.(`Retriever start with ID: ${runId}`);
502
+ this.startAndRegisterObservation({
503
+ runId,
504
+ parentRunId,
505
+ runName: name ?? retriever.id.at(-1)?.toString() ?? "Retriever",
506
+ attributes: {
507
+ input: query,
508
+ },
509
+ tags,
510
+ metadata,
511
+ asType: "span",
512
+ });
513
+ } catch (e) {
514
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
515
+ }
516
+ }
517
+
518
+ async handleRetrieverEnd(
519
+ documents: Document<Record<string, any>>[],
520
+ runId: string,
521
+ _parentRunId?: string | undefined
522
+ ): Promise<void> {
523
+ try {
524
+ this.logger.debug?.(`Retriever end with ID: ${runId}`);
525
+ this.handleObservationEnd({
526
+ runId,
527
+ attributes: {
528
+ output: documents,
529
+ },
530
+ });
531
+ } catch (e) {
532
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
533
+ }
534
+ }
535
+
536
+ async handleRetrieverError(
537
+ err: any,
538
+ runId: string,
539
+ _parentRunId?: string | undefined
540
+ ): Promise<void> {
541
+ try {
542
+ this.logger.debug?.(`Retriever error: ${err} with ID: ${runId}`);
543
+ this.handleObservationEnd({
544
+ runId,
545
+ attributes: {
546
+ level: "ERROR",
547
+ statusMessage: err.toString(),
548
+ },
549
+ });
550
+ } catch (e) {
551
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
552
+ }
553
+ }
554
+
555
+ async handleToolEnd(
556
+ output: string,
557
+ runId: string,
558
+ _parentRunId?: string | undefined
559
+ ): Promise<void> {
560
+ try {
561
+ this.logger.debug?.(`Tool end with ID: ${runId}`);
562
+ this.handleObservationEnd({
563
+ runId,
564
+ attributes: { output },
565
+ });
566
+ } catch (e) {
567
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
568
+ }
569
+ }
570
+
571
+ async handleToolError(
572
+ err: any,
573
+ runId: string,
574
+ _parentRunId?: string | undefined
575
+ ): Promise<void> {
576
+ try {
577
+ this.logger.debug?.(`Tool error ${err} with ID: ${runId}`);
578
+ this.handleObservationEnd({
579
+ runId,
580
+ attributes: {
581
+ level: "ERROR",
582
+ statusMessage: err.toString(),
583
+ },
584
+ });
585
+ } catch (e) {
586
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
587
+ }
588
+ }
589
+
590
+ async handleLLMEnd(
591
+ output: LLMResult,
592
+ runId: string,
593
+ _parentRunId?: string | undefined
594
+ ): Promise<void> {
595
+ try {
596
+ this.logger.debug?.(`LLM end with ID: ${runId}`);
597
+
598
+ const lastResponse =
599
+ output.generations[output.generations.length - 1][
600
+ output.generations[output.generations.length - 1].length - 1
601
+ ];
602
+ const llmUsage =
603
+ this.extractUsageMetadata(lastResponse) ??
604
+ output.llmOutput?.["tokenUsage"];
605
+ const modelName = this.extractModelNameFromMetadata(lastResponse);
606
+
607
+ const usageDetails: Record<string, any> = {
608
+ input:
609
+ llmUsage?.input_tokens ??
610
+ ("promptTokens" in llmUsage ? llmUsage?.promptTokens : undefined),
611
+ output:
612
+ llmUsage?.output_tokens ??
613
+ ("completionTokens" in llmUsage
614
+ ? llmUsage?.completionTokens
615
+ : undefined),
616
+ total:
617
+ llmUsage?.total_tokens ??
618
+ ("totalTokens" in llmUsage ? llmUsage?.totalTokens : undefined),
619
+ };
620
+
621
+ if (llmUsage && "input_token_details" in llmUsage) {
622
+ for (const [key, val] of Object.entries(
623
+ llmUsage["input_token_details"] ?? {}
624
+ )) {
625
+ usageDetails[`input_${key}`] = val;
626
+ if ("input" in usageDetails && typeof val === "number") {
627
+ usageDetails["input"] = Math.max(0, usageDetails["input"] - val);
628
+ }
629
+ }
630
+ }
631
+
632
+ if (llmUsage && "output_token_details" in llmUsage) {
633
+ for (const [key, val] of Object.entries(
634
+ llmUsage["output_token_details"] ?? {}
635
+ )) {
636
+ usageDetails[`output_${key}`] = val;
637
+ if ("output" in usageDetails && typeof val === "number") {
638
+ usageDetails["output"] = Math.max(0, usageDetails["output"] - val);
639
+ }
640
+ }
641
+ }
642
+
643
+ const extractedOutput =
644
+ "message" in lastResponse
645
+ ? this.extractChatMessageContent(
646
+ lastResponse["message"] as BaseMessage
647
+ )
648
+ : lastResponse.text;
649
+
650
+ this.handleObservationEnd({
651
+ runId,
652
+ attributes: {
653
+ model: modelName,
654
+ output: extractedOutput,
655
+ completionStartTime:
656
+ runId in this.completionStartTimes
657
+ ? this.completionStartTimes[runId]
658
+ : undefined,
659
+ usageDetails: usageDetails,
660
+ },
661
+ });
662
+
663
+ if (runId in this.completionStartTimes) {
664
+ delete this.completionStartTimes[runId];
665
+ }
666
+ } catch (e) {
667
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
668
+ }
669
+ }
670
+
671
+ async handleLLMError(
672
+ err: any,
673
+ runId: string,
674
+ _parentRunId?: string | undefined
675
+ ): Promise<void> {
676
+ try {
677
+ this.logger.debug?.(`LLM error ${err} with ID: ${runId}`);
678
+ this.handleObservationEnd({
679
+ runId,
680
+ attributes: {
681
+ level: "ERROR",
682
+ statusMessage: err.toString(),
683
+ },
684
+ });
685
+ } catch (e) {
686
+ this.logger.debug?.(e instanceof Error ? e.message : String(e));
687
+ }
688
+ }
689
+
690
+ private registerPromptInfo(
691
+ parentRunId?: string,
692
+ metadata?: Record<string, unknown>
693
+ ): void {
694
+ if (metadata && "promptInfo" in metadata && parentRunId) {
695
+ this.promptToParentRunMap.set(
696
+ parentRunId,
697
+ metadata.promptInfo as PromptInfo
698
+ );
699
+ }
700
+ }
701
+
702
+ private deregisterPromptInfo(runId: string): void {
703
+ this.promptToParentRunMap.delete(runId);
704
+ }
705
+
706
+ private startAndRegisterObservation(params: {
707
+ runName: string;
708
+ runId: string;
709
+ parentRunId?: string;
710
+ attributes: Record<string, unknown>;
711
+ metadata?: Record<string, unknown>;
712
+ tags?: string[];
713
+ asType?: "span" | "llm" | "tool";
714
+ }): Observation {
715
+ const { runName, runId, parentRunId, attributes, metadata, tags, asType } =
716
+ params;
717
+
718
+ // Determine parent context:
719
+ // 1. If parentRunId exists, use the parent span from runMap (internal LangChain/LangGraph hierarchy)
720
+ // 2. If no parentRunId (ROOT span) but externalParentSpanContext exists, use it (link to AG-UI.Server)
721
+ // 3. Otherwise, create a new root span
722
+ let parentSpanContext: SpanContext | undefined;
723
+
724
+ if (parentRunId) {
725
+ // Internal parent from LangChain/LangGraph
726
+ parentSpanContext = this.runMap.get(parentRunId)?.otelSpan.spanContext();
727
+ } else if (this.externalParentSpanContext) {
728
+ // External parent from AG-UI.Server
729
+ parentSpanContext = this.externalParentSpanContext;
730
+ }
731
+
732
+ // Add adapter name prefix to ROOT span
733
+ let finalRunName = runName;
734
+ if (!parentRunId && this.adapterName) {
735
+ // ROOT span: add Adapter.LangGraph or Adapter.LangChain prefix
736
+ finalRunName = `Adapter.${this.adapterName}`;
737
+ }
738
+
739
+ const observation = startObservation(
740
+ finalRunName,
741
+ {
742
+ version: this.version,
743
+ metadata: this.joinTagsAndMetaData(tags, metadata),
744
+ ...attributes,
745
+ },
746
+ {
747
+ asType: asType ?? "span",
748
+ parentSpanContext,
749
+ }
750
+ );
751
+ this.runMap.set(runId, observation);
752
+
753
+ return observation;
754
+ }
755
+
756
+ private handleObservationEnd(params: {
757
+ runId: string;
758
+ attributes?: Record<string, unknown>;
759
+ }) {
760
+ const { runId, attributes = {} } = params;
761
+
762
+ const observation = this.runMap.get(runId);
763
+ if (!observation) {
764
+ this.logger.warn?.("Observation not found in runMap. Skipping operation.");
765
+ return;
766
+ }
767
+
768
+ // Type-safe update: cast to ObservationAttributes which is the union of all observation attribute types
769
+ observation.update(attributes as ObservationAttributes).end();
770
+
771
+ this.last_trace_id = observation.traceId;
772
+ this.runMap.delete(runId);
773
+ }
774
+
775
+ private joinTagsAndMetaData(
776
+ tags?: string[] | undefined,
777
+ metadata1?: Record<string, unknown> | undefined,
778
+ metadata2?: Record<string, unknown> | undefined
779
+ ): Record<string, unknown> | undefined {
780
+ const finalDict: Record<string, unknown> = {};
781
+ if (tags && tags.length > 0) {
782
+ finalDict.tags = tags;
783
+ }
784
+ if (metadata1) {
785
+ Object.assign(finalDict, metadata1);
786
+ }
787
+ if (metadata2) {
788
+ Object.assign(finalDict, metadata2);
789
+ }
790
+ return this.stripObservabilityKeysFromMetadata(finalDict);
791
+ }
792
+
793
+ private stripObservabilityKeysFromMetadata(
794
+ metadata?: Record<string, unknown>
795
+ ): Record<string, unknown> | undefined {
796
+ if (!metadata) {
797
+ return;
798
+ }
799
+
800
+ const reservedKeys = ["promptInfo", "userId", "sessionId"];
801
+
802
+ return Object.fromEntries(
803
+ Object.entries(metadata).filter(([key, _]) => !reservedKeys.includes(key))
804
+ );
805
+ }
806
+
807
+ private extractUsageMetadata(
808
+ generation: Generation
809
+ ): UsageMetadata | undefined {
810
+ try {
811
+ const usageMetadata =
812
+ "message" in generation &&
813
+ (AIMessage.isInstance(generation["message"]) ||
814
+ AIMessageChunk.isInstance(generation["message"]))
815
+ ? generation["message"].usage_metadata
816
+ : undefined;
817
+ return usageMetadata;
818
+ } catch (err) {
819
+ this.logger.debug?.(`Error extracting usage metadata: ${err}`);
820
+ return;
821
+ }
822
+ }
823
+
824
+ private extractModelNameFromMetadata(generation: any): string | undefined {
825
+ try {
826
+ return "message" in generation &&
827
+ (AIMessage.isInstance(generation["message"]) ||
828
+ AIMessageChunk.isInstance(generation["message"]))
829
+ ? generation["message"].response_metadata.model_name
830
+ : undefined;
831
+ } catch {}
832
+ }
833
+
834
+ private extractChatMessageContent(
835
+ message: BaseMessage
836
+ ): LlmMessage | AnonymousLlmMessage | MessageContent {
837
+ let response = undefined;
838
+
839
+ if (message.getType() === "human") {
840
+ response = { content: message.content, role: "user" };
841
+ } else if (message.getType() === "generic") {
842
+ response = {
843
+ content: message.content,
844
+ role: "human",
845
+ };
846
+ } else if (message.getType() === "ai") {
847
+ response = { content: message.content, role: "assistant" };
848
+
849
+ if (
850
+ "tool_calls" in message &&
851
+ Array.isArray(message.tool_calls) &&
852
+ (message.tool_calls?.length ?? 0) > 0
853
+ ) {
854
+ (response as any)["tool_calls"] = message["tool_calls"];
855
+ }
856
+ if (
857
+ "additional_kwargs" in message &&
858
+ "tool_calls" in message["additional_kwargs"]
859
+ ) {
860
+ (response as any)["tool_calls"] =
861
+ message["additional_kwargs"]["tool_calls"];
862
+ }
863
+ } else if (message.getType() === "system") {
864
+ response = { content: message.content, role: "system" };
865
+ } else if (message.getType() === "function") {
866
+ response = {
867
+ content: message.content,
868
+ additional_kwargs: message.additional_kwargs,
869
+ role: message.name,
870
+ };
871
+ } else if (message.getType() === "tool") {
872
+ response = {
873
+ content: message.content,
874
+ additional_kwargs: message.additional_kwargs,
875
+ role: message.name,
876
+ };
877
+ } else if (!message.name) {
878
+ response = { content: message.content };
879
+ } else {
880
+ response = {
881
+ role: message.name,
882
+ content: message.content,
883
+ };
884
+ }
885
+
886
+ if (
887
+ (message.additional_kwargs.function_call ||
888
+ message.additional_kwargs.tool_calls) &&
889
+ (response as any)["tool_calls"] === undefined
890
+ ) {
891
+ return { ...response, additional_kwargs: message.additional_kwargs };
892
+ }
893
+
894
+ return response;
895
+ }
896
+ }