@librechat/agents 3.7.9 → 3.7.11

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 (59) hide show
  1. package/dist/cjs/graphs/Graph.cjs +11 -1
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +36 -3
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  5. package/dist/cjs/langfuse.cjs +6 -2
  6. package/dist/cjs/langfuse.cjs.map +1 -1
  7. package/dist/cjs/run.cjs +5 -1
  8. package/dist/cjs/run.cjs.map +1 -1
  9. package/dist/cjs/tools/ToolNode.cjs +12 -2
  10. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  11. package/dist/cjs/tools/search/metrics.cjs +234 -0
  12. package/dist/cjs/tools/search/metrics.cjs.map +1 -0
  13. package/dist/cjs/tools/search/rerankers.cjs +80 -68
  14. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  15. package/dist/cjs/tools/search/search.cjs +95 -45
  16. package/dist/cjs/tools/search/search.cjs.map +1 -1
  17. package/dist/cjs/tools/search/tool.cjs +77 -49
  18. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  19. package/dist/cjs/tools/search/utils.cjs.map +1 -1
  20. package/dist/esm/graphs/Graph.mjs +11 -1
  21. package/dist/esm/graphs/Graph.mjs.map +1 -1
  22. package/dist/esm/graphs/MultiAgentGraph.mjs +36 -3
  23. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  24. package/dist/esm/langfuse.mjs +6 -2
  25. package/dist/esm/langfuse.mjs.map +1 -1
  26. package/dist/esm/run.mjs +5 -1
  27. package/dist/esm/run.mjs.map +1 -1
  28. package/dist/esm/tools/ToolNode.mjs +12 -2
  29. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  30. package/dist/esm/tools/search/metrics.mjs +234 -0
  31. package/dist/esm/tools/search/metrics.mjs.map +1 -0
  32. package/dist/esm/tools/search/rerankers.mjs +80 -68
  33. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  34. package/dist/esm/tools/search/search.mjs +96 -46
  35. package/dist/esm/tools/search/search.mjs.map +1 -1
  36. package/dist/esm/tools/search/tool.mjs +77 -49
  37. package/dist/esm/tools/search/tool.mjs.map +1 -1
  38. package/dist/esm/tools/search/utils.mjs.map +1 -1
  39. package/dist/types/langfuse.d.ts +5 -1
  40. package/dist/types/tools/ToolNode.d.ts +4 -1
  41. package/dist/types/tools/search/metrics.d.ts +15 -0
  42. package/dist/types/tools/search/rerankers.d.ts +30 -5
  43. package/dist/types/tools/search/tool.d.ts +7 -1
  44. package/dist/types/tools/search/types.d.ts +88 -4
  45. package/dist/types/tools/search/utils.d.ts +2 -10
  46. package/dist/types/types/tools.d.ts +5 -0
  47. package/package.json +1 -1
  48. package/src/graphs/Graph.ts +10 -0
  49. package/src/graphs/MultiAgentGraph.ts +71 -2
  50. package/src/langfuse.ts +12 -0
  51. package/src/run.ts +4 -0
  52. package/src/tools/ToolNode.ts +19 -0
  53. package/src/tools/search/metrics.ts +400 -0
  54. package/src/tools/search/rerankers.ts +160 -97
  55. package/src/tools/search/search.ts +139 -56
  56. package/src/tools/search/tool.ts +126 -62
  57. package/src/tools/search/types.ts +104 -4
  58. package/src/tools/search/utils.ts +2 -10
  59. package/src/types/tools.ts +5 -0
@@ -24,7 +24,7 @@ export declare function resolveSearchOutcome(data: t.SearchResultData, query: st
24
24
  * Executes parallel searches and merges the results,
25
25
  * deduplicating top stories by link
26
26
  */
27
- export declare function executeParallelSearches({ searchAPI, query, date, country, safeSearch, images, videos, news, logger, }: {
27
+ export declare function executeParallelSearches({ searchAPI, query, date, country, safeSearch, images, videos, news, logger, provider, metrics, }: {
28
28
  searchAPI: ReturnType<typeof createSearchAPI>;
29
29
  query: string;
30
30
  date?: DATE_RANGE;
@@ -34,5 +34,11 @@ export declare function executeParallelSearches({ searchAPI, query, date, countr
34
34
  videos: boolean;
35
35
  news: boolean;
36
36
  logger: t.Logger;
37
+ /** Labels the provider in the run summary. Optional so the pre-existing
38
+ * call contract still holds for callers outside this package. */
39
+ provider?: string;
40
+ /** Collector owned by the caller. Without one, this call opens and flushes
41
+ * its own, so a direct caller still gets the single summary line. */
42
+ metrics?: t.SearchMetrics;
37
43
  }): Promise<t.SearchResult>;
38
44
  export declare const createSearchTool: (config?: t.SearchToolConfig) => DynamicStructuredTool;
@@ -291,7 +291,9 @@ export interface JinaRerankerResult {
291
291
  }
292
292
  export interface JinaRerankerResponse {
293
293
  model: string;
294
- usage: {
294
+ /** Telemetry only, and absent from some Jina-compatible endpoints — never
295
+ * dereference it on a path that would discard a usable ranking. */
296
+ usage?: {
295
297
  total_tokens: number;
296
298
  };
297
299
  results: JinaRerankerResult[];
@@ -303,12 +305,13 @@ export interface CohereRerankerResult {
303
305
  export interface CohereRerankerResponse {
304
306
  results: CohereRerankerResult[];
305
307
  id: string;
306
- meta: {
307
- api_version: {
308
+ /** Telemetry only; see {@link JinaRerankerResponse.usage}. */
309
+ meta?: {
310
+ api_version?: {
308
311
  version: string;
309
312
  is_experimental: boolean;
310
313
  };
311
- billed_units: {
314
+ billed_units?: {
312
315
  search_units: number;
313
316
  };
314
317
  };
@@ -343,6 +346,81 @@ export interface RagApiRerankResponse {
343
346
  }
344
347
  export type SafeSearchLevel = 0 | 1 | 2;
345
348
  export type Logger = WinstonLogger;
349
+ /** Compact, redacted view of a thrown error, safe to hand to a logger. */
350
+ export interface SafeErrorLog {
351
+ message: string;
352
+ name?: string;
353
+ code?: string;
354
+ status?: number;
355
+ method?: string;
356
+ url?: string;
357
+ responseDataSummary?: string;
358
+ value?: string;
359
+ }
360
+ /** Why a rerank returned the candidates' original order instead of a ranking. */
361
+ export type RerankFallback = 'no_api_key' | 'no_base_url' | 'no_token_supplier' | 'bad_response' | 'invalid_results' | 'chunk_error' | 'placeholder' | 'error';
362
+ /** One provider query. `results` is the row count that query contributed. */
363
+ export interface SearchObservation {
364
+ provider: string;
365
+ type: string;
366
+ results: number;
367
+ durationMs: number;
368
+ error?: string;
369
+ /** The query rejected rather than reporting failure in its response. Those
370
+ * were logged at error level before they were aggregated, so the summary
371
+ * has to carry the distinction to keep that severity. */
372
+ thrown?: boolean;
373
+ }
374
+ /** One scraped link. A failure carries `error`; a success carries the sizes
375
+ * the scrape produced, so the summary can report both without a second pass. */
376
+ export interface ScrapeObservation {
377
+ url: string;
378
+ chars?: number;
379
+ highlights?: number;
380
+ error?: string;
381
+ }
382
+ /** One reranker round trip. A search reranks once per scraped source, so
383
+ * these fold into a single summary rather than logging per source. */
384
+ export interface RerankObservation {
385
+ provider: string;
386
+ chunks: number;
387
+ results: number;
388
+ durationMs: number;
389
+ model?: string;
390
+ /** Provider-reported usage: Jina tokens, Cohere billed search units. */
391
+ units?: number;
392
+ /** Chunks a provider candidate cap dropped before submission. */
393
+ dropped?: number;
394
+ /** Set only when a provider cap reduced the requested result count, so a
395
+ * search returning fewer highlights than configured says why. */
396
+ topK?: number;
397
+ topKLimit?: number;
398
+ reason?: RerankFallback;
399
+ error?: SafeErrorLog;
400
+ }
401
+ /** Per-rerank state threaded from the start of a call to whichever exit it
402
+ * takes, so every path records exactly one observation. */
403
+ export interface RerankRun {
404
+ metrics: SearchMetrics;
405
+ documents: string[];
406
+ topK: number;
407
+ startedAt: number;
408
+ model?: string;
409
+ units?: number;
410
+ dropped?: number;
411
+ topKLimit?: number;
412
+ }
413
+ /**
414
+ * Fold-as-you-go counters for one `web_search` call. Recording is O(1) and
415
+ * allocation-free past a bounded reason map; {@link SearchMetrics.flush}
416
+ * emits at most one line per phase that actually ran.
417
+ */
418
+ export interface SearchMetrics {
419
+ recordSearch(observation: SearchObservation): void;
420
+ recordScrape(observation: ScrapeObservation): void;
421
+ recordRerank(observation: RerankObservation): void;
422
+ flush(): void;
423
+ }
346
424
  export interface SearchToolConfig extends SearchConfig, ProcessSourcesConfig, FirecrawlConfig {
347
425
  tavilyScraperOptions?: TavilyScraperConfig;
348
426
  crwScraperOptions?: CrwScraperConfig;
@@ -572,6 +650,9 @@ export interface FirecrawlScraperConfig extends BaseSearchProviderConfig {
572
650
  onlyMainContent?: boolean;
573
651
  changeTrackingOptions?: object;
574
652
  }
653
+ /** Result kind a parallel sub-search covers; `web` is the untyped main
654
+ * search, which every provider serves without a `type` parameter. */
655
+ export type SubSearchType = 'web' | 'images' | 'videos' | 'news';
575
656
  export type GetSourcesParams = {
576
657
  query: string;
577
658
  date?: DATE_RANGE;
@@ -870,6 +951,9 @@ export type ProcessSourcesFields = {
870
951
  news: boolean;
871
952
  proMode: boolean;
872
953
  onGetHighlights: SearchToolConfig['onGetHighlights'];
954
+ /** Collector owned by the caller; when omitted, one is created and flushed
955
+ * for this call so a direct `processSources` still summarizes itself. */
956
+ metrics?: SearchMetrics;
873
957
  };
874
958
  export interface SearchToolSchema {
875
959
  type: 'object';
@@ -1,14 +1,6 @@
1
+ import type { SafeErrorLog } from './types';
1
2
  import type * as t from './types';
2
- export interface SafeErrorLog {
3
- message: string;
4
- name?: string;
5
- code?: string;
6
- status?: number;
7
- method?: string;
8
- url?: string;
9
- responseDataSummary?: string;
10
- value?: string;
11
- }
3
+ export type { SafeErrorLog } from './types';
12
4
  /**
13
5
  * Creates a default logger that maps to console methods
14
6
  * Uses a singleton pattern to avoid creating multiple instances
@@ -131,6 +131,11 @@ export type ToolNodeOptions = {
131
131
  /** ID of the agent that owns this tool node, surfaced to hooks as `executingAgentId`
132
132
  * so a batch can be attributed to a specific agent even where `agentId` is undefined. */
133
133
  executingAgentId?: string;
134
+ /** Name of the agent that owns this tool node, used for active Langfuse attribution. */
135
+ executingAgentName?: string;
136
+ /** Root graph-agent identity retained alongside the currently executing tool owner. */
137
+ rootAgentId?: string;
138
+ rootAgentName?: string;
134
139
  /** Tool names that must be executed directly (via runTool) even in event-driven mode (e.g., graph-managed handoff tools) */
135
140
  directToolNames?: Set<string>;
136
141
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.7.9",
3
+ "version": "3.7.11",
4
4
  "reova": {
5
5
  "enabled": true,
6
6
  "endpoint": "https://telemetry.reo.dev/data"
@@ -2620,6 +2620,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2620
2620
  // run_id); `executingAgentId` always identifies the owning agent.
2621
2621
  agentId: this.subagentScope ? agentContext?.agentId : undefined,
2622
2622
  executingAgentId: agentContext?.agentId,
2623
+ executingAgentName: agentContext?.name,
2624
+ rootAgentId: this.defaultAgentId,
2625
+ rootAgentName: this.agentContexts.get(this.defaultAgentId)?.name,
2623
2626
  toolCallStepIds: this.toolCallStepIds,
2624
2627
  toolRegistry: agentContext?.toolRegistry,
2625
2628
  getDiscoveredToolNames: (): readonly string[] =>
@@ -2697,6 +2700,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2697
2700
  // hooks can attribute the batch even at the top level.
2698
2701
  agentId: this.subagentScope ? agentContext?.agentId : undefined,
2699
2702
  executingAgentId: agentContext?.agentId,
2703
+ executingAgentName: agentContext?.name,
2704
+ rootAgentId: this.defaultAgentId,
2705
+ rootAgentName: this.agentContexts.get(this.defaultAgentId)?.name,
2700
2706
  toolCallStepIds: this.toolCallStepIds,
2701
2707
  errorHandler: (data, metadata): Promise<boolean> =>
2702
2708
  StandardGraph.handleToolCallErrorStatic(this, data, metadata),
@@ -3815,6 +3821,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3815
3821
  parentMessageId: config.configurable?.requestBody?.parentMessageId,
3816
3822
  agentId,
3817
3823
  agentName: agentContext.name,
3824
+ rootAgentId: this.defaultAgentId,
3825
+ rootAgentName: this.agentContexts.get(this.defaultAgentId)?.name,
3826
+ activeAgentId: agentId,
3827
+ activeAgentName: agentContext.name,
3818
3828
  });
3819
3829
  let langfuseHandler: CallbackEntry | undefined;
3820
3830
  let invokeConfig = {
@@ -101,6 +101,14 @@ function isTransferToolName(name: unknown): boolean {
101
101
  );
102
102
  }
103
103
 
104
+ function graphToolName(tool: unknown): string | undefined {
105
+ if (tool == null || typeof tool !== 'object' || !('name' in tool)) {
106
+ return undefined;
107
+ }
108
+ const { name } = tool;
109
+ return typeof name === 'string' ? name : undefined;
110
+ }
111
+
104
112
  /**
105
113
  * Drop transfer `tool_use` content blocks from an AI message's array content.
106
114
  * Companion to the reception's tool-call filtering: array-content providers
@@ -287,6 +295,21 @@ function withHandoffGroupMetadata(
287
295
  };
288
296
  }
289
297
 
298
+ function withActiveAgentMetadata(
299
+ config: LangGraphRunnableConfig | undefined,
300
+ agentId: string,
301
+ agentName: string | undefined
302
+ ): LangGraphRunnableConfig {
303
+ return {
304
+ ...config,
305
+ metadata: {
306
+ ...config?.metadata,
307
+ activeAgentId: agentId,
308
+ ...(agentName == null ? {} : { activeAgentName: agentName }),
309
+ },
310
+ };
311
+ }
312
+
290
313
  /**
291
314
  * MultiAgentGraph extends StandardGraph to support dynamic multi-agent workflows
292
315
  * with handoffs, fan-in/fan-out, and other composable patterns.
@@ -594,6 +617,24 @@ export class MultiAgentGraph extends StandardGraph {
594
617
  private createHandoffTools(): void {
595
618
  // Group handoff edges by source agent(s)
596
619
  const handoffsByAgent = new Map<string, t.GraphEdge[]>();
620
+ const tokenAccountingRefresh = new Set<string>();
621
+
622
+ /** Transfer tool names are SDK-reserved. Remove externally supplied or
623
+ * stale transfer tools before deriving the source agent's allowed set
624
+ * from the graph edges below. */
625
+ for (const agentContext of this.agentContexts.values()) {
626
+ const originalTools = agentContext.graphTools;
627
+ const retainedTools = agentContext.graphTools?.filter(
628
+ (graphTool) => !isTransferToolName(graphToolName(graphTool))
629
+ );
630
+ if (retainedTools?.length !== originalTools?.length) {
631
+ tokenAccountingRefresh.add(agentContext.agentId);
632
+ }
633
+ agentContext.graphTools =
634
+ retainedTools != null && retainedTools.length > 0
635
+ ? retainedTools
636
+ : undefined;
637
+ }
597
638
 
598
639
  // Only process handoff edges for tool creation
599
640
  for (const edge of this.handoffEdges) {
@@ -643,6 +684,29 @@ export class MultiAgentGraph extends StandardGraph {
643
684
  for (const handoffTool of handoffTools) {
644
685
  agentContext.graphTools.push(handoffTool);
645
686
  }
687
+ if (handoffTools.length > 0) {
688
+ tokenAccountingRefresh.add(agentId);
689
+ }
690
+ }
691
+
692
+ for (const agentId of tokenAccountingRefresh) {
693
+ const agentContext = this.agentContexts.get(agentId);
694
+ if (agentContext?.tokenCounter == null) {
695
+ continue;
696
+ }
697
+ const { tokenCounter, baseIndexTokenCountMap } = agentContext;
698
+ agentContext.tokenCalculationPromise = agentContext
699
+ .calculateInstructionTokens(tokenCounter)
700
+ .then(() => {
701
+ agentContext.updateTokenMapWithInstructions(baseIndexTokenCountMap);
702
+ })
703
+ .catch((err) => {
704
+ // eslint-disable-next-line no-console
705
+ console.error(
706
+ 'Error recalculating instruction tokens after handoff tool updates:',
707
+ err
708
+ );
709
+ });
646
710
  }
647
711
  }
648
712
 
@@ -1239,10 +1303,16 @@ export class MultiAgentGraph extends StandardGraph {
1239
1303
  ): Promise<t.MultiAgentGraphState | Command> => {
1240
1304
  let result: t.MultiAgentGraphState;
1241
1305
  let inputMessages = state.messages;
1242
- const memberConfig =
1306
+ const agentContext = this.agentContexts.get(agentId);
1307
+ const recursionLimitedConfig =
1243
1308
  this.memberRecursionLimit == null
1244
1309
  ? config
1245
1310
  : { ...config, recursionLimit: this.memberRecursionLimit };
1311
+ const memberConfig = withActiveAgentMetadata(
1312
+ recursionLimitedConfig,
1313
+ agentId,
1314
+ agentContext?.name
1315
+ );
1246
1316
 
1247
1317
  /**
1248
1318
  * Check if this agent is receiving a handoff.
@@ -1254,7 +1324,6 @@ export class MultiAgentGraph extends StandardGraph {
1254
1324
  state.messages,
1255
1325
  agentId
1256
1326
  );
1257
- const agentContext = this.agentContexts.get(agentId);
1258
1327
 
1259
1328
  if (
1260
1329
  handoffContext?.sourceAgentName != null &&
package/src/langfuse.ts CHANGED
@@ -783,17 +783,29 @@ export function createLangfuseTraceMetadata({
783
783
  parentMessageId,
784
784
  agentId,
785
785
  agentName,
786
+ rootAgentId,
787
+ rootAgentName,
788
+ activeAgentId,
789
+ activeAgentName,
786
790
  }: {
787
791
  messageId?: unknown;
788
792
  parentMessageId?: unknown;
789
793
  agentId?: unknown;
790
794
  agentName?: unknown;
795
+ rootAgentId?: unknown;
796
+ rootAgentName?: unknown;
797
+ activeAgentId?: unknown;
798
+ activeAgentName?: unknown;
791
799
  }): LangfuseTraceMetadata {
792
800
  return createTraceMetadata({
793
801
  messageId,
794
802
  parentMessageId,
795
803
  agentId,
796
804
  agentName,
805
+ rootAgentId,
806
+ rootAgentName,
807
+ activeAgentId,
808
+ activeAgentName,
797
809
  });
798
810
  }
799
811
 
package/src/run.ts CHANGED
@@ -1311,6 +1311,10 @@ export class Run<_T extends t.BaseGraphState> {
1311
1311
  parentMessageId: config.configurable?.requestBody?.parentMessageId,
1312
1312
  agentId: graph.defaultAgentId,
1313
1313
  agentName: primaryContext?.name,
1314
+ rootAgentId: graph.defaultAgentId,
1315
+ rootAgentName: primaryContext?.name,
1316
+ activeAgentId: graph.defaultAgentId,
1317
+ activeAgentName: primaryContext?.name,
1314
1318
  });
1315
1319
  const traceName = config.runName ?? getLangfuseTraceName(traceMetadata);
1316
1320
  const streamLangfuseConfig = this.getStreamLangfuseConfig(graph);
@@ -738,6 +738,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
738
738
  * even where `agentId` (the subagent-scope marker) is undefined.
739
739
  */
740
740
  private executingAgentId?: string;
741
+ private executingAgentName?: string;
742
+ private rootAgentId?: string;
743
+ private rootAgentName?: string;
741
744
  /** Tool names that bypass event dispatch and execute directly (e.g., graph-managed handoff tools) */
742
745
  private directToolNames?: Set<string>;
743
746
  /**
@@ -823,6 +826,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
823
826
  eagerEventToolSuppressions,
824
827
  agentId,
825
828
  executingAgentId,
829
+ executingAgentName,
830
+ rootAgentId,
831
+ rootAgentName,
826
832
  directToolNames,
827
833
  interruptingToolNames,
828
834
  codeSessionToolNames,
@@ -902,6 +908,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
902
908
  // Default to agentId so callers constructing ToolNode directly (who pass the
903
909
  // existing agentId option) still get attribution without knowing the new option.
904
910
  this.executingAgentId = executingAgentId ?? agentId;
911
+ this.executingAgentName = executingAgentName;
912
+ this.rootAgentId = rootAgentId;
913
+ this.rootAgentName = rootAgentName;
905
914
  this.directToolNames = directToolNames;
906
915
  this.interruptingToolNames =
907
916
  interruptingToolNames != null && interruptingToolNames.size > 0
@@ -963,6 +972,16 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
963
972
  metadata: {
964
973
  ...options?.metadata,
965
974
  agentId: this.executingAgentId,
975
+ activeAgentId: this.executingAgentId,
976
+ ...(this.executingAgentName == null
977
+ ? {}
978
+ : { activeAgentName: this.executingAgentName }),
979
+ ...(this.rootAgentId == null
980
+ ? {}
981
+ : { rootAgentId: this.rootAgentId }),
982
+ ...(this.rootAgentName == null
983
+ ? {}
984
+ : { rootAgentName: this.rootAgentName }),
966
985
  },
967
986
  };
968
987
  return withLangfuseRuntimeScope(