@librechat/agents 3.2.64 → 3.2.66

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 (106) hide show
  1. package/dist/cjs/graphs/Graph.cjs +18 -2
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/instrumentation.cjs +15 -3
  4. package/dist/cjs/instrumentation.cjs.map +1 -1
  5. package/dist/cjs/langfuseToolOutputTracing.cjs +1 -2
  6. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  7. package/dist/cjs/langfuseTraceShaping.cjs +51 -24
  8. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/index.cjs +73 -9
  10. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  11. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  12. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +49 -9
  13. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  14. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +3 -1
  15. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  16. package/dist/cjs/llm/anthropic/utils/stream_events.cjs +337 -0
  17. package/dist/cjs/llm/anthropic/utils/stream_events.cjs.map +1 -0
  18. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +8 -0
  19. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  20. package/dist/cjs/tools/BashExecutor.cjs +9 -8
  21. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  22. package/dist/cjs/tools/CodeExecutor.cjs +9 -7
  23. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  24. package/dist/cjs/tools/search/crw-scraper.cjs +165 -0
  25. package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -0
  26. package/dist/cjs/tools/search/crw-search.cjs +105 -0
  27. package/dist/cjs/tools/search/crw-search.cjs.map +1 -0
  28. package/dist/cjs/tools/search/search.cjs +4 -2
  29. package/dist/cjs/tools/search/search.cjs.map +1 -1
  30. package/dist/cjs/tools/search/tool.cjs +15 -3
  31. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  32. package/dist/esm/graphs/Graph.mjs +18 -2
  33. package/dist/esm/graphs/Graph.mjs.map +1 -1
  34. package/dist/esm/instrumentation.mjs +15 -3
  35. package/dist/esm/instrumentation.mjs.map +1 -1
  36. package/dist/esm/langfuseToolOutputTracing.mjs +1 -2
  37. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  38. package/dist/esm/langfuseTraceShaping.mjs +51 -24
  39. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  40. package/dist/esm/llm/anthropic/index.mjs +73 -9
  41. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  42. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  43. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +49 -9
  44. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  45. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +3 -2
  46. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  47. package/dist/esm/llm/anthropic/utils/stream_events.mjs +337 -0
  48. package/dist/esm/llm/anthropic/utils/stream_events.mjs.map +1 -0
  49. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +8 -0
  50. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  51. package/dist/esm/tools/BashExecutor.mjs +9 -8
  52. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  53. package/dist/esm/tools/CodeExecutor.mjs +9 -7
  54. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  55. package/dist/esm/tools/search/crw-scraper.mjs +163 -0
  56. package/dist/esm/tools/search/crw-scraper.mjs.map +1 -0
  57. package/dist/esm/tools/search/crw-search.mjs +103 -0
  58. package/dist/esm/tools/search/crw-search.mjs.map +1 -0
  59. package/dist/esm/tools/search/search.mjs +4 -2
  60. package/dist/esm/tools/search/search.mjs.map +1 -1
  61. package/dist/esm/tools/search/tool.mjs +15 -3
  62. package/dist/esm/tools/search/tool.mjs.map +1 -1
  63. package/dist/types/graphs/Graph.d.ts +2 -0
  64. package/dist/types/langfuseTraceShaping.d.ts +12 -6
  65. package/dist/types/llm/anthropic/index.d.ts +2 -0
  66. package/dist/types/llm/anthropic/types.d.ts +2 -0
  67. package/dist/types/llm/anthropic/utils/message_outputs.d.ts +1 -2
  68. package/dist/types/llm/anthropic/utils/stream_events.d.ts +25 -0
  69. package/dist/types/tools/BashExecutor.d.ts +7 -6
  70. package/dist/types/tools/CodeExecutor.d.ts +7 -5
  71. package/dist/types/tools/search/crw-scraper.d.ts +41 -0
  72. package/dist/types/tools/search/crw-search.d.ts +4 -0
  73. package/dist/types/tools/search/types.d.ts +88 -3
  74. package/dist/types/types/graph.d.ts +10 -3
  75. package/package.json +1 -1
  76. package/src/graphs/Graph.ts +36 -3
  77. package/src/instrumentation.ts +20 -0
  78. package/src/langfuseToolOutputTracing.ts +2 -4
  79. package/src/langfuseTraceShaping.ts +73 -20
  80. package/src/llm/anthropic/index.ts +134 -10
  81. package/src/llm/anthropic/inherited-content-utils.spec.ts +10 -5
  82. package/src/llm/anthropic/inherited-stream-events.spec.ts +513 -9
  83. package/src/llm/anthropic/llm.spec.ts +100 -16
  84. package/src/llm/anthropic/types.ts +3 -0
  85. package/src/llm/anthropic/utils/cross-provider-server-tools.test.ts +110 -0
  86. package/src/llm/anthropic/utils/message_inputs.ts +75 -3
  87. package/src/llm/anthropic/utils/message_outputs.ts +10 -2
  88. package/src/llm/anthropic/utils/stream_events.ts +471 -0
  89. package/src/llm/bedrock/utils/cross-provider-server-tools.test.ts +122 -0
  90. package/src/llm/bedrock/utils/message_inputs.ts +13 -0
  91. package/src/specs/langfuse-instrumentation.test.ts +64 -0
  92. package/src/specs/langfuse-routing.integration.test.ts +49 -4
  93. package/src/specs/langfuse-tool-output-tracing.test.ts +5 -4
  94. package/src/specs/langfuse-trace-shaping.test.ts +80 -9
  95. package/src/specs/subagent.test.ts +180 -0
  96. package/src/tools/BashExecutor.ts +9 -8
  97. package/src/tools/CodeExecutor.ts +9 -7
  98. package/src/tools/__tests__/BashExecutor.test.ts +16 -5
  99. package/src/tools/__tests__/CodeExecutor.stateful.test.ts +17 -6
  100. package/src/tools/search/crw-scraper.ts +244 -0
  101. package/src/tools/search/crw-search.ts +167 -0
  102. package/src/tools/search/crw.test.ts +836 -0
  103. package/src/tools/search/search.ts +7 -1
  104. package/src/tools/search/tool.ts +23 -3
  105. package/src/tools/search/types.ts +103 -3
  106. package/src/types/graph.ts +10 -3
@@ -2,8 +2,8 @@ import type { RunnableConfig } from '@langchain/core/runnables';
2
2
  import type { Logger as WinstonLogger } from 'winston';
3
3
  import type { BaseReranker } from './rerankers';
4
4
  import { DATE_RANGE } from './schema';
5
- export type SearchProvider = 'serper' | 'searxng' | 'tavily' | 'keenable';
6
- export type ScraperProvider = 'firecrawl' | 'serper' | 'tavily';
5
+ export type SearchProvider = 'serper' | 'searxng' | 'tavily' | 'keenable' | 'crw';
6
+ export type ScraperProvider = 'firecrawl' | 'serper' | 'tavily' | 'crw';
7
7
  export type RerankerType = 'infinity' | 'jina' | 'cohere' | 'none';
8
8
  export interface Highlight {
9
9
  score: number;
@@ -91,6 +91,48 @@ export interface TavilySearchPayload {
91
91
  include_favicon?: boolean;
92
92
  chunks_per_source?: number;
93
93
  }
94
+ export interface CrwSearchOptions {
95
+ /** Max results to request (maps to `limit`; clamped 1..20). */
96
+ maxResults?: number;
97
+ /** Add 'images' to the sources array. */
98
+ includeImages?: boolean;
99
+ timeout?: number;
100
+ }
101
+ export type CrwSearchSource = 'web' | 'images' | 'news';
102
+ export interface CrwSearchPayload {
103
+ query: string;
104
+ limit: number;
105
+ sources?: CrwSearchSource[];
106
+ tbs?: string;
107
+ }
108
+ export interface CrwSearchResult {
109
+ title?: string;
110
+ url?: string;
111
+ description?: string;
112
+ snippet?: string;
113
+ position?: number;
114
+ category?: string;
115
+ }
116
+ export interface CrwImageSearchResult extends CrwSearchResult {
117
+ imageUrl?: string;
118
+ imageFormat?: string;
119
+ }
120
+ export interface CrwNewsSearchResult extends CrwSearchResult {
121
+ publishedDate?: string;
122
+ }
123
+ export interface CrwSearchGroups {
124
+ web?: CrwSearchResult[];
125
+ images?: CrwImageSearchResult[];
126
+ news?: CrwNewsSearchResult[];
127
+ }
128
+ export interface CrwSearchResponse {
129
+ success: boolean;
130
+ data?: (CrwSearchGroups & {
131
+ results?: CrwSearchGroups;
132
+ }) | CrwSearchResult[];
133
+ error?: string;
134
+ error_code?: string;
135
+ }
94
136
  export interface SearchConfig {
95
137
  searchProvider?: SearchProvider;
96
138
  serperApiKey?: string;
@@ -103,6 +145,9 @@ export interface SearchConfig {
103
145
  keenableApiKey?: string;
104
146
  keenableApiUrl?: string;
105
147
  keenableSearchOptions?: KeenableSearchOptions;
148
+ crwApiKey?: string;
149
+ crwApiUrl?: string;
150
+ crwSearchOptions?: CrwSearchOptions;
106
151
  }
107
152
  export interface KeenableSearchOptions {
108
153
  maxResults?: number;
@@ -224,6 +269,7 @@ export type SafeSearchLevel = 0 | 1 | 2;
224
269
  export type Logger = WinstonLogger;
225
270
  export interface SearchToolConfig extends SearchConfig, ProcessSourcesConfig, FirecrawlConfig {
226
271
  tavilyScraperOptions?: TavilyScraperConfig;
272
+ crwScraperOptions?: CrwScraperConfig;
227
273
  /** Max chars of highlight content this tool feeds the MODEL per search (the
228
274
  * dominant, otherwise-unbounded part of the output). Distinct from
229
275
  * `maxContentLength`, which caps scraped/reranked content per source — full
@@ -255,7 +301,7 @@ export type UsedReferences = {
255
301
  originalIndex: number;
256
302
  reference: MediaReference;
257
303
  }[];
258
- export type AnyScraperResponse = FirecrawlScrapeResponse | SerperScrapeResponse | TavilyScrapeResponse;
304
+ export type AnyScraperResponse = FirecrawlScrapeResponse | SerperScrapeResponse | TavilyScrapeResponse | CrwScrapeResponse;
259
305
  /** Base Scraper Interface */
260
306
  export interface BaseScraper {
261
307
  scrapeUrl(url: string, options?: unknown): Promise<[string, AnyScraperResponse]>;
@@ -265,6 +311,24 @@ export interface BaseScraper {
265
311
  }
266
312
  /** Firecrawl */
267
313
  export type FirecrawlScrapeOptions = Omit<FirecrawlScraperConfig, 'apiKey' | 'apiUrl' | 'version' | 'logger'>;
314
+ export interface CrwScraperConfig {
315
+ apiKey?: string;
316
+ apiUrl?: string;
317
+ formats?: string[];
318
+ timeout?: number;
319
+ logger?: Logger;
320
+ onlyMainContent?: boolean;
321
+ includeTags?: string[];
322
+ excludeTags?: string[];
323
+ waitFor?: number;
324
+ headers?: Record<string, string>;
325
+ renderJs?: boolean | null;
326
+ cssSelector?: string;
327
+ xpath?: string;
328
+ proxy?: string;
329
+ stealth?: boolean;
330
+ }
331
+ export type CrwScrapeOptions = Omit<CrwScraperConfig, 'apiKey' | 'apiUrl' | 'logger'>;
268
332
  export type SerperScrapeOptions = Omit<SerperScraperConfig, 'apiKey' | 'apiUrl' | 'logger'>;
269
333
  export type TavilyScrapeOptions = Omit<TavilyScraperConfig, 'apiKey' | 'apiUrl' | 'logger'>;
270
334
  export interface TavilyExtractPayload {
@@ -341,6 +405,27 @@ export interface FirecrawlScrapeResponse {
341
405
  };
342
406
  error?: string;
343
407
  }
408
+ export interface CrwScrapeData {
409
+ markdown?: string;
410
+ html?: string;
411
+ rawHtml?: string;
412
+ plainText?: string;
413
+ screenshot?: string;
414
+ links?: string[];
415
+ metadata?: ScrapeMetadata;
416
+ }
417
+ export interface CrwScrapeResponse {
418
+ success: boolean;
419
+ data?: CrwScrapeData;
420
+ error?: string;
421
+ error_code?: string;
422
+ }
423
+ export interface CrwRawScrapeResponse extends CrwScrapeData {
424
+ success?: boolean;
425
+ data?: CrwScrapeData;
426
+ error?: string;
427
+ error_code?: string;
428
+ }
344
429
  export interface SerperScrapeResponse {
345
430
  success: boolean;
346
431
  data?: {
@@ -406,9 +406,9 @@ export type LangfuseToolOutputTracingConfig = {
406
406
  };
407
407
  export type LangfuseToolNodeTracingConfig = {
408
408
  /**
409
- * Overrides ToolNode callback tracing. ToolNode spans are exported by the
410
- * env-backed Langfuse callback, so this only enables tracing when that
411
- * callback is configured.
409
+ * Opts into the internal ToolNode batch observation. Graph tool-dispatch
410
+ * and individual tool observations are exported without this wrapper, so
411
+ * the default is false to avoid a redundant hierarchy level.
412
412
  */
413
413
  enabled?: boolean;
414
414
  };
@@ -417,6 +417,13 @@ export interface LangfuseConfig {
417
417
  publicKey?: string;
418
418
  secretKey?: string;
419
419
  baseUrl?: string;
420
+ /**
421
+ * Environment identifier attached to exported traces (Langfuse
422
+ * `environment`). When unset, falls back to `LANGFUSE_TRACING_ENVIRONMENT`
423
+ * then `NODE_ENV`, so production traces are not collapsed under the
424
+ * `default` environment.
425
+ */
426
+ environment?: string;
420
427
  metadata?: Record<string, string | number | boolean | null | undefined>;
421
428
  /**
422
429
  * Internal OTLP span attributes to attach to Langfuse observations before
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.2.64",
3
+ "version": "3.2.66",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -96,6 +96,18 @@ const { AGENT, TOOLS, SUMMARIZE } = GraphNodeKeys;
96
96
  /** Minimum relative variance before calibrated toolSchemaTokens overrides current value. */
97
97
  const CALIBRATION_VARIANCE_THRESHOLD = 0.15;
98
98
 
99
+ function createToolHandlerRegistry(
100
+ source: HandlerRegistry | undefined
101
+ ): HandlerRegistry | undefined {
102
+ const toolHandler = source?.getHandler(GraphEvents.ON_TOOL_EXECUTE);
103
+ if (toolHandler == null) {
104
+ return undefined;
105
+ }
106
+ const registry = new HandlerRegistry();
107
+ registry.register(GraphEvents.ON_TOOL_EXECUTE, toolHandler);
108
+ return registry;
109
+ }
110
+
99
111
  /**
100
112
  * Start index of the span post-prune formatters can mutate in place: the
101
113
  * trailing tool batch plus its owning AI message (artifact formatting touches
@@ -576,6 +588,8 @@ export abstract class Graph<
576
588
  /** Set of invoked tool call IDs from non-message run steps completed mid-run, if any */
577
589
  invokedToolIds?: Set<string>;
578
590
  handlerRegistry: HandlerRegistry | undefined;
591
+ /** Host registry retained only for forwarding tools from nested child graphs. */
592
+ protected parentToolHandlerRegistry: HandlerRegistry | undefined;
579
593
  /**
580
594
  * True when event-driven tool execution can be routed through callbacks even
581
595
  * though this graph intentionally does not own the full handler registry.
@@ -661,6 +675,7 @@ export abstract class Graph<
661
675
  this.prelimMessageIdsByStepKey = new Map();
662
676
  this.invokedToolIds = undefined;
663
677
  this.handlerRegistry = undefined;
678
+ this.parentToolHandlerRegistry = undefined;
664
679
  this.hookRegistry = undefined;
665
680
  this.humanInTheLoop = undefined;
666
681
  this.toolOutputReferences = undefined;
@@ -1130,6 +1145,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1130
1145
  /* Misc.*/
1131
1146
 
1132
1147
  getRunMessages(): BaseMessage[] | undefined {
1148
+ if (this.messages == null) {
1149
+ return this.cachedRunMessages;
1150
+ }
1133
1151
  if (this.messages.length === 0 && this.cachedRunMessages != null) {
1134
1152
  return this.cachedRunMessages;
1135
1153
  }
@@ -1137,6 +1155,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1137
1155
  }
1138
1156
 
1139
1157
  getContentParts(): t.MessageContentComplex[] | undefined {
1158
+ // `messages` can be null/undefined on a graph that has been disposed
1159
+ // (clearHeavyState) but is still reachable via a cache (e.g. RedisJobStore's
1160
+ // WeakRef) during a HITL resume/reconnect. Guard instead of dereferencing null.
1161
+ if (this.messages == null) {
1162
+ return undefined;
1163
+ }
1140
1164
  return convertMessagesToContent(this.messages.slice(this.startIndex));
1141
1165
  }
1142
1166
 
@@ -1169,6 +1193,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1169
1193
  * Get all run steps, optionally filtered by agent ID
1170
1194
  */
1171
1195
  getRunSteps(agentId?: string): t.RunStep[] {
1196
+ // `contentData` can be null/undefined on a disposed-but-cached graph during a
1197
+ // HITL resume/reconnect; without this guard `[...this.contentData]` throws
1198
+ // "this.contentData is not iterable".
1199
+ if (this.contentData == null) {
1200
+ return [];
1201
+ }
1172
1202
  if (agentId == null || agentId === '') {
1173
1203
  return [...this.contentData];
1174
1204
  }
@@ -2361,7 +2391,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2361
2391
  );
2362
2392
  if (resolvedConfigs.length > 0) {
2363
2393
  const getParentHandlerRegistry = (): HandlerRegistry | undefined =>
2364
- this.handlerRegistry;
2394
+ this.handlerRegistry ?? this.parentToolHandlerRegistry;
2365
2395
  const executor = new SubagentExecutor({
2366
2396
  configs: new Map(resolvedConfigs.map((c) => [c.type, c])),
2367
2397
  parentSignal: this.signal,
@@ -2378,6 +2408,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2378
2408
  maxDepth: effectiveSubagentDepth,
2379
2409
  createChildGraph: (input): StandardGraph => {
2380
2410
  const childGraph = new StandardGraph(input);
2411
+ const toolHandlerRegistry = createToolHandlerRegistry(
2412
+ getParentHandlerRegistry()
2413
+ );
2381
2414
  childGraph.hookRegistry = this.hookRegistry;
2382
2415
  /**
2383
2416
  * Do not propagate `humanInTheLoop` into the child graph yet:
@@ -2400,9 +2433,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2400
2433
  // have the executable graphTool, the guard correctly applies.
2401
2434
  childGraph.interruptingToolNames = this.interruptingToolNames;
2402
2435
  childGraph.toolExecution = this.toolExecution;
2436
+ childGraph.parentToolHandlerRegistry = toolHandlerRegistry;
2403
2437
  childGraph.eventToolExecutionAvailable =
2404
- this.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) !=
2405
- null;
2438
+ toolHandlerRegistry != null;
2406
2439
  return childGraph;
2407
2440
  },
2408
2441
  });
@@ -75,17 +75,35 @@ export function ensureOpenTelemetryContextManager(): void {
75
75
  }
76
76
  }
77
77
 
78
+ function resolveLangfuseEnvironment(
79
+ langfuse?: t.LangfuseConfig
80
+ ): string | undefined {
81
+ const candidates = [
82
+ langfuse?.environment,
83
+ process.env.LANGFUSE_TRACING_ENVIRONMENT,
84
+ process.env.NODE_ENV,
85
+ ];
86
+ for (const candidate of candidates) {
87
+ if (candidate != null && candidate.trim() !== '') {
88
+ return candidate.trim();
89
+ }
90
+ }
91
+ return undefined;
92
+ }
93
+
78
94
  function getLangfuseSpanProcessorParams(
79
95
  langfuse?: t.LangfuseConfig
80
96
  ): LangfuseSpanProcessorParams | undefined {
81
97
  if (langfuse?.enabled === false) {
82
98
  return undefined;
83
99
  }
100
+ const environment = resolveLangfuseEnvironment(langfuse);
84
101
  if (hasLangfuseConfigCredentials(langfuse)) {
85
102
  return {
86
103
  publicKey: langfuse.publicKey,
87
104
  secretKey: langfuse.secretKey,
88
105
  ...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),
106
+ ...(isPresent(environment) ? { environment } : {}),
89
107
  };
90
108
  }
91
109
  if (hasLangfuseEnvConfig()) {
@@ -97,6 +115,7 @@ function getLangfuseSpanProcessorParams(
97
115
  publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,
98
116
  secretKey: process.env.LANGFUSE_SECRET_KEY as string,
99
117
  ...(isPresent(baseUrl) ? { baseUrl } : {}),
118
+ ...(isPresent(environment) ? { environment } : {}),
100
119
  };
101
120
  }
102
121
  if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {
@@ -104,6 +123,7 @@ function getLangfuseSpanProcessorParams(
104
123
  publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,
105
124
  secretKey: process.env.LANGFUSE_SECRET_KEY as string,
106
125
  baseUrl: langfuse.baseUrl,
126
+ ...(isPresent(environment) ? { environment } : {}),
107
127
  };
108
128
  }
109
129
  return undefined;
@@ -483,10 +483,8 @@ export function shouldTraceToolNodeForLangfuse({
483
483
  }
484
484
 
485
485
  const explicit = langfuse?.toolNodeTracing?.enabled;
486
- if (explicit != null) {
487
- return (
488
- explicit && (hasLangfuseConfigKeys(langfuse) || hasLangfuseEnvKeys())
489
- );
486
+ if (explicit !== true) {
487
+ return false;
490
488
  }
491
489
 
492
490
  return hasLangfuseConfigKeys(langfuse) || hasLangfuseEnvKeys();
@@ -5,8 +5,13 @@ const LANGGRAPH_START_NODE = '__start__';
5
5
  const ANONYMOUS_LAMBDA_NAME = 'RunnableLambda';
6
6
  const LANGGRAPH_AGENT_NODE_PREFIX = 'agent=';
7
7
  const LANGGRAPH_TOOL_NODE_PREFIX = 'tools=';
8
- const TOOL_BATCH_RUN_NAME = 'tool_batch';
9
8
  const AGENT_NODE_SPAN_NAME = 'agent';
9
+ const TOOL_DISPATCH_SPAN_NAME = 'tool-dispatch';
10
+ const GENERATION_SPAN_NAME = 'llm';
11
+ const ROOT_OBSERVATION_TYPE = 'agent';
12
+ const CHAIN_OBSERVATION_TYPE = 'chain';
13
+ const AGENT_TRACE_TAG = 'agent';
14
+ const TITLE_TRACE_TAG = 'title';
10
15
 
11
16
  type MutableSpan = ReadableSpan & {
12
17
  name: string;
@@ -207,6 +212,8 @@ function isRootSpan(span: ReadableSpan): boolean {
207
212
  * LangGraph plumbing observations that add noise without information:
208
213
  * the duplicated `__start__` channel-seed nodes and anonymous
209
214
  * `RunnableLambda` pass-throughs (Langfuse team feedback items 4 & 5).
215
+ * Internal ToolNode batch spans are disabled at their source so traced child
216
+ * tools retain an exported parent. Explicitly traced ToolNodes are preserved.
210
217
  */
211
218
  export function shouldDropLangfuseSpan(spanName: string): boolean {
212
219
  return (
@@ -216,18 +223,26 @@ export function shouldDropLangfuseSpan(spanName: string): boolean {
216
223
 
217
224
  function shapeToolNodeSpan(span: MutableSpan): void {
218
225
  const inputKey = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;
226
+ span.name = TOOL_DISPATCH_SPAN_NAME;
227
+ span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
228
+ CHAIN_OBSERVATION_TYPE;
219
229
  const calls = findPendingToolCalls(
220
230
  parseAttributeValue(span.attributes[inputKey])
221
231
  );
222
232
  if (calls.length === 0) {
223
233
  return;
224
234
  }
225
- span.name = [...new Set(calls.map((call) => call.name))].join(', ');
226
235
  span.attributes[inputKey] = JSON.stringify(
227
236
  calls.map(({ name, args }) => ({ name, args }))
228
237
  );
229
238
  }
230
239
 
240
+ function shapeAgentNodeSpan(span: MutableSpan): void {
241
+ span.name = AGENT_NODE_SPAN_NAME;
242
+ span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
243
+ ROOT_OBSERVATION_TYPE;
244
+ }
245
+
231
246
  function shapeRootSpan(span: MutableSpan): void {
232
247
  const inputKey = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;
233
248
  const outputKey = LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT;
@@ -241,11 +256,47 @@ function shapeRootSpan(span: MutableSpan): void {
241
256
  );
242
257
  if (question != null) {
243
258
  span.attributes[inputKey] = question;
244
- span.attributes[LangfuseOtelSpanAttributes.TRACE_INPUT] = question;
245
259
  }
246
260
  if (answer != null) {
247
261
  span.attributes[outputKey] = answer;
248
- span.attributes[LangfuseOtelSpanAttributes.TRACE_OUTPUT] = answer;
262
+ }
263
+ const traceInput = question ?? span.attributes[inputKey];
264
+ const traceOutput = answer ?? span.attributes[outputKey];
265
+ if (traceInput != null) {
266
+ span.attributes[LangfuseOtelSpanAttributes.TRACE_INPUT] = traceInput;
267
+ }
268
+ if (traceOutput != null) {
269
+ span.attributes[LangfuseOtelSpanAttributes.TRACE_OUTPUT] = traceOutput;
270
+ }
271
+ }
272
+
273
+ function isGenerationSpan(span: MutableSpan): boolean {
274
+ const type = span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE];
275
+ return typeof type === 'string' && type.toLowerCase() === 'generation';
276
+ }
277
+
278
+ function hasTraceTag(span: MutableSpan, expectedTag: string): boolean {
279
+ const tags = parseAttributeValue(
280
+ span.attributes[LangfuseOtelSpanAttributes.TRACE_TAGS]
281
+ );
282
+ return (
283
+ Array.isArray(tags) &&
284
+ tags.some((tag) => typeof tag === 'string' && tag === expectedTag)
285
+ );
286
+ }
287
+
288
+ function shapeRootObservationType(span: MutableSpan): void {
289
+ if (isGenerationSpan(span)) {
290
+ return;
291
+ }
292
+ if (hasTraceTag(span, AGENT_TRACE_TAG)) {
293
+ span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
294
+ ROOT_OBSERVATION_TYPE;
295
+ return;
296
+ }
297
+ if (hasTraceTag(span, TITLE_TRACE_TAG)) {
298
+ span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
299
+ CHAIN_OBSERVATION_TYPE;
249
300
  }
250
301
  }
251
302
 
@@ -254,27 +305,29 @@ function shapeRootSpan(span: MutableSpan): void {
254
305
  * - `agent=<id>` / `tools=<id>` node names carry the ephemeral agent id
255
306
  * (`provider__model`) — strip it so switching models doesn't break
256
307
  * name-based logic (item 1).
257
- * - Tool node spans are renamed to the actual tool name(s) and their
258
- * input scoped to the pending tool-call args instead of the whole
259
- * chat history (items 3 & 4).
260
- * - The root span (and trace) input/output become the user question and
261
- * assistant response so the session view reads as a conversation
262
- * (item 2).
308
+ * - LLM generation spans keep the provider client class name (`ChatOpenAI`,
309
+ * `AzureChatOpenAI`, …); rename them to a provider-agnostic `llm` so the
310
+ * name reflects the operation, not the model (the model stays on the
311
+ * generation's model attribute).
312
+ * - Agent nodes become `agent` observations, while tool-dispatch nodes become
313
+ * stable `chain` observations whose input is scoped to the pending calls.
314
+ * Individual child calls remain `tool` observations (items 3 & 4).
315
+ * - Agent trace roots become `agent` observations and title trace roots become
316
+ * `chain` observations. Root and trace input/output are reduced to the user
317
+ * question and assistant response when chat messages are available (item 2).
263
318
  */
264
319
  export function shapeLangfuseSpan(span: ReadableSpan): void {
265
320
  const mutable = span as MutableSpan;
266
321
  if (mutable.name.startsWith(LANGGRAPH_AGENT_NODE_PREFIX)) {
267
- mutable.name = AGENT_NODE_SPAN_NAME;
268
- return;
269
- }
270
- if (
271
- mutable.name.startsWith(LANGGRAPH_TOOL_NODE_PREFIX) ||
272
- mutable.name === TOOL_BATCH_RUN_NAME
273
- ) {
322
+ shapeAgentNodeSpan(mutable);
323
+ } else if (mutable.name.startsWith(LANGGRAPH_TOOL_NODE_PREFIX)) {
274
324
  shapeToolNodeSpan(mutable);
275
- return;
325
+ } else if (isGenerationSpan(mutable)) {
326
+ mutable.name = GENERATION_SPAN_NAME;
276
327
  }
277
- if (isRootSpan(span)) {
278
- shapeRootSpan(mutable);
328
+ if (!isRootSpan(span)) {
329
+ return;
279
330
  }
331
+ shapeRootObservationType(mutable);
332
+ shapeRootSpan(mutable);
280
333
  }