@librechat/agents 3.4.1 → 3.4.2

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 (128) hide show
  1. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  2. package/dist/cjs/graphs/Graph.cjs +57 -33
  3. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs +81 -33
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  6. package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs +14 -0
  7. package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs.map +1 -0
  8. package/dist/cjs/graphs/createGraph.cjs +15 -0
  9. package/dist/cjs/graphs/createGraph.cjs.map +1 -0
  10. package/dist/cjs/graphs/index.cjs +1 -0
  11. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
  12. package/dist/cjs/main.cjs +20 -11
  13. package/dist/cjs/run.cjs +48 -38
  14. package/dist/cjs/run.cjs.map +1 -1
  15. package/dist/cjs/session/JsonlSessionStore.cjs +2 -2
  16. package/dist/cjs/tools/SubagentTool.cjs +4 -4
  17. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  18. package/dist/cjs/tools/ToolNode.cjs +13 -9
  19. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  20. package/dist/cjs/tools/eagerEventExecution.cjs +1 -0
  21. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +3 -3
  22. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +2 -2
  23. package/dist/cjs/tools/search/rerankers.cjs +154 -1
  24. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  25. package/dist/cjs/tools/search/tool.cjs +4 -1
  26. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  27. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +601 -0
  28. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -0
  29. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +672 -343
  30. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  31. package/dist/cjs/tools/subagent/SubagentReplay.cjs +3 -1
  32. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  33. package/dist/cjs/tools/subagent/childGraphConfig.cjs +278 -0
  34. package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -0
  35. package/dist/cjs/tools/subagent/index.cjs +1 -0
  36. package/dist/cjs/tools/subagent/runtimeLimits.cjs +5 -0
  37. package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -0
  38. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  39. package/dist/esm/graphs/Graph.mjs +57 -33
  40. package/dist/esm/graphs/Graph.mjs.map +1 -1
  41. package/dist/esm/graphs/MultiAgentGraph.mjs +81 -33
  42. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  43. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs +14 -0
  44. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -0
  45. package/dist/esm/graphs/createGraph.mjs +15 -0
  46. package/dist/esm/graphs/createGraph.mjs.map +1 -0
  47. package/dist/esm/graphs/index.mjs +1 -0
  48. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
  49. package/dist/esm/main.mjs +12 -10
  50. package/dist/esm/run.mjs +48 -38
  51. package/dist/esm/run.mjs.map +1 -1
  52. package/dist/esm/session/JsonlSessionStore.mjs +2 -2
  53. package/dist/esm/tools/SubagentTool.mjs +4 -4
  54. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  55. package/dist/esm/tools/ToolNode.mjs +13 -9
  56. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  57. package/dist/esm/tools/eagerEventExecution.mjs +1 -1
  58. package/dist/esm/tools/local/LocalExecutionEngine.mjs +3 -3
  59. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +2 -2
  60. package/dist/esm/tools/search/rerankers.mjs +154 -1
  61. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  62. package/dist/esm/tools/search/tool.mjs +4 -1
  63. package/dist/esm/tools/search/tool.mjs.map +1 -1
  64. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +597 -0
  65. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -0
  66. package/dist/esm/tools/subagent/SubagentExecutor.mjs +672 -343
  67. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  68. package/dist/esm/tools/subagent/SubagentReplay.mjs +3 -1
  69. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  70. package/dist/esm/tools/subagent/childGraphConfig.mjs +272 -0
  71. package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -0
  72. package/dist/esm/tools/subagent/index.mjs +1 -0
  73. package/dist/esm/tools/subagent/runtimeLimits.mjs +5 -0
  74. package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -0
  75. package/dist/types/agents/AgentContext.d.ts +1 -1
  76. package/dist/types/graphs/Graph.d.ts +7 -2
  77. package/dist/types/graphs/MultiAgentGraph.d.ts +7 -1
  78. package/dist/types/graphs/applyGraphRuntimeConfig.d.ts +3 -0
  79. package/dist/types/graphs/createGraph.d.ts +13 -0
  80. package/dist/types/graphs/graphFactory.d.ts +13 -0
  81. package/dist/types/graphs/index.d.ts +2 -0
  82. package/dist/types/run.d.ts +2 -2
  83. package/dist/types/tools/SubagentTool.d.ts +1 -1
  84. package/dist/types/tools/search/rerankers.d.ts +28 -0
  85. package/dist/types/tools/search/types.d.ts +38 -1
  86. package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +171 -0
  87. package/dist/types/tools/subagent/SubagentExecutor.d.ts +32 -37
  88. package/dist/types/tools/subagent/SubagentReplay.d.ts +5 -1
  89. package/dist/types/tools/subagent/childGraphConfig.d.ts +37 -0
  90. package/dist/types/tools/subagent/index.d.ts +1 -1
  91. package/dist/types/tools/subagent/runtimeLimits.d.ts +4 -0
  92. package/dist/types/types/graph.d.ts +184 -19
  93. package/package.json +3 -1
  94. package/src/agents/AgentContext.ts +1 -1
  95. package/src/graphs/Graph.ts +111 -60
  96. package/src/graphs/MultiAgentGraph.ts +149 -47
  97. package/src/graphs/__tests__/MultiAgentGraph.test.ts +86 -0
  98. package/src/graphs/__tests__/composition.smoke.test.ts +152 -0
  99. package/src/graphs/__tests__/createGraph.test.ts +206 -0
  100. package/src/graphs/applyGraphRuntimeConfig.ts +25 -0
  101. package/src/graphs/createGraph.ts +32 -0
  102. package/src/graphs/graphFactory.ts +12 -0
  103. package/src/graphs/index.ts +2 -0
  104. package/src/run.ts +50 -38
  105. package/src/specs/graph-subagent.live.test.ts +242 -0
  106. package/src/specs/graph-subagent.test.ts +322 -0
  107. package/src/specs/langfuse-routing.integration.test.ts +120 -1
  108. package/src/specs/subagent.test.ts +130 -4
  109. package/src/tools/SubagentTool.ts +4 -4
  110. package/src/tools/ToolNode.ts +22 -10
  111. package/src/tools/__tests__/SubagentExecutor.test.ts +198 -9
  112. package/src/tools/__tests__/SubagentReplay.test.ts +29 -0
  113. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +28 -2
  114. package/src/tools/__tests__/subagentHooks.test.ts +90 -3
  115. package/src/tools/search/rag-api-reranker.test.ts +891 -0
  116. package/src/tools/search/rerankers.ts +260 -0
  117. package/src/tools/search/tool.ts +6 -0
  118. package/src/tools/search/types.ts +45 -1
  119. package/src/tools/subagent/SubagentExecutionRegistry.ts +1159 -0
  120. package/src/tools/subagent/SubagentExecutor.ts +1245 -563
  121. package/src/tools/subagent/SubagentReplay.ts +10 -1
  122. package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +637 -0
  123. package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +3054 -0
  124. package/src/tools/subagent/__tests__/childGraphConfig.test.ts +521 -0
  125. package/src/tools/subagent/childGraphConfig.ts +694 -0
  126. package/src/tools/subagent/index.ts +6 -0
  127. package/src/tools/subagent/runtimeLimits.ts +7 -0
  128. package/src/types/graph.ts +217 -19
@@ -0,0 +1,322 @@
1
+ import { z } from 'zod';
2
+ import { tool } from '@langchain/core/tools';
3
+ import { AIMessageChunk } from '@langchain/core/messages';
4
+ import { ChatGenerationChunk } from '@langchain/core/outputs';
5
+ import { FakeListChatModel } from '@langchain/core/utils/testing';
6
+ import type { RunnableConfig } from '@langchain/core/runnables';
7
+ import type { GraphSubagentConfig } from '@/types';
8
+ import type * as t from '@/types';
9
+ import { createFakeStreamingLLM } from '@/llm/fake';
10
+ import { Constants, Providers } from '@/common';
11
+ import { StandardGraph } from '@/graphs/Graph';
12
+ import { Run } from '@/run';
13
+
14
+ const invokeConfig: RunnableConfig = {
15
+ configurable: { thread_id: 'graph-subagent-test' },
16
+ };
17
+
18
+ const usage = {
19
+ input_tokens: 5,
20
+ output_tokens: 3,
21
+ total_tokens: 8,
22
+ };
23
+
24
+ class UsageFakeChatModel extends FakeListChatModel {
25
+ constructor() {
26
+ super({ responses: ['member response'] });
27
+ }
28
+
29
+ override async *_streamResponseChunks(
30
+ ...args: Parameters<FakeListChatModel['_streamResponseChunks']>
31
+ ): ReturnType<FakeListChatModel['_streamResponseChunks']> {
32
+ yield* super._streamResponseChunks(...args);
33
+ yield new ChatGenerationChunk({
34
+ text: '',
35
+ message: new AIMessageChunk({
36
+ content: '',
37
+ usage_metadata: { ...usage },
38
+ }),
39
+ });
40
+ }
41
+ }
42
+
43
+ class LoopingFakeChatModel extends FakeListChatModel {
44
+ private invocationCount = 0;
45
+
46
+ constructor() {
47
+ super({ responses: ['unused'] });
48
+ }
49
+
50
+ override async *_streamResponseChunks(
51
+ ..._args: Parameters<FakeListChatModel['_streamResponseChunks']>
52
+ ): ReturnType<FakeListChatModel['_streamResponseChunks']> {
53
+ this.invocationCount++;
54
+ if (this.invocationCount <= 2) {
55
+ yield new ChatGenerationChunk({
56
+ text: '',
57
+ message: new AIMessageChunk({
58
+ content: '',
59
+ tool_call_chunks: [
60
+ {
61
+ name: 'loop_tool',
62
+ args: '{}',
63
+ id: `loop-${this.invocationCount}`,
64
+ index: 0,
65
+ type: 'tool_call_chunk',
66
+ },
67
+ ],
68
+ }),
69
+ });
70
+ return;
71
+ }
72
+ yield new ChatGenerationChunk({
73
+ text: 'escaped member turn limit',
74
+ message: new AIMessageChunk('escaped member turn limit'),
75
+ });
76
+ }
77
+ }
78
+
79
+ const makeAgent = (agentId: string): t.AgentInputs => ({
80
+ agentId,
81
+ provider: Providers.OPENAI,
82
+ clientOptions: { modelName: `${agentId}-model`, apiKey: 'test-key' },
83
+ instructions: `You are ${agentId}.`,
84
+ });
85
+
86
+ const makeGraphConfig = (): GraphSubagentConfig => ({
87
+ kind: 'graph',
88
+ type: 'research-team',
89
+ name: 'Research Team',
90
+ description: 'Coordinates parallel research and synthesis.',
91
+ entryAgentId: 'coordinator',
92
+ resultAgentId: 'synthesizer',
93
+ agents: [
94
+ makeAgent('coordinator'),
95
+ makeAgent('left'),
96
+ makeAgent('right'),
97
+ makeAgent('synthesizer'),
98
+ ],
99
+ edges: [
100
+ {
101
+ from: 'coordinator',
102
+ to: ['left', 'right'],
103
+ edgeType: 'direct',
104
+ },
105
+ {
106
+ from: ['left', 'right'],
107
+ to: 'synthesizer',
108
+ edgeType: 'direct',
109
+ },
110
+ ],
111
+ });
112
+
113
+ const createRun = async (
114
+ graphConfig: GraphSubagentConfig,
115
+ overrides: Partial<t.RunConfig> = {}
116
+ ): Promise<Run<t.IState>> =>
117
+ Run.create<t.IState>({
118
+ runId: `graph-subagent-${Date.now()}-${Math.random()}`,
119
+ graphConfig: {
120
+ type: 'standard',
121
+ agents: [
122
+ {
123
+ ...makeAgent('parent'),
124
+ maxSubagentDepth: 2,
125
+ subagentConfigs: [graphConfig],
126
+ },
127
+ ],
128
+ },
129
+ returnContent: true,
130
+ skipCleanup: true,
131
+ ...overrides,
132
+ });
133
+
134
+ const getGraphSubagentTool = (run: Run<t.IState>): t.GenericTool => {
135
+ const tools = (run.Graph as StandardGraph).agentContexts.get('parent')
136
+ ?.graphTools as t.GenericTool[] | undefined;
137
+ const tool = tools?.find(
138
+ (candidate) => 'name' in candidate && candidate.name === Constants.SUBAGENT
139
+ );
140
+ if (tool == null) {
141
+ throw new Error('Expected graph subagent tool');
142
+ }
143
+ return tool;
144
+ };
145
+
146
+ describe('Graph subagent integration', () => {
147
+ it('runs a convergent direct DAG and returns only the designated result', async () => {
148
+ const graphConfig = { ...makeGraphConfig(), maxTurns: 1 };
149
+ const run = await createRun(graphConfig);
150
+ (run.Graph as StandardGraph).setSubagentModelOverride(
151
+ createFakeStreamingLLM({
152
+ responses: [
153
+ 'coordinator plan',
154
+ 'left research',
155
+ 'right research',
156
+ 'synthesized answer',
157
+ ],
158
+ })
159
+ );
160
+
161
+ const result = await getGraphSubagentTool(run).invoke(
162
+ {
163
+ description: 'Research and synthesize the question.',
164
+ subagent_type: 'research-team',
165
+ },
166
+ invokeConfig
167
+ );
168
+
169
+ expect(result).toBe('synthesized answer');
170
+ });
171
+
172
+ it('does not fall back to worker text when the result member is textless', async () => {
173
+ const graphConfig: GraphSubagentConfig = {
174
+ ...makeGraphConfig(),
175
+ agents: [makeAgent('worker'), makeAgent('result')],
176
+ edges: [{ from: 'worker', to: 'result', edgeType: 'direct' }],
177
+ entryAgentId: 'worker',
178
+ resultAgentId: 'result',
179
+ };
180
+ const run = await createRun(graphConfig);
181
+ (run.Graph as StandardGraph).setSubagentModelOverride(
182
+ createFakeStreamingLLM({ responses: ['worker text', ''] })
183
+ );
184
+
185
+ const result = await getGraphSubagentTool(run).invoke(
186
+ {
187
+ description: 'Complete the two-step task.',
188
+ subagent_type: 'research-team',
189
+ },
190
+ invokeConfig
191
+ );
192
+
193
+ expect(result).toBe('Task completed');
194
+ });
195
+
196
+ it('keeps each member turn budget independent from the outer graph budget', async () => {
197
+ const loopTool = tool(async (): Promise<string> => 'continue', {
198
+ name: 'loop_tool',
199
+ description: 'Continue the member loop.',
200
+ schema: z.object({}),
201
+ });
202
+ const graphConfig: GraphSubagentConfig = {
203
+ ...makeGraphConfig(),
204
+ maxTurns: 1,
205
+ agents: [
206
+ { ...makeAgent('entry'), graphTools: [loopTool] },
207
+ makeAgent('result'),
208
+ ],
209
+ edges: [{ from: 'entry', to: 'result', edgeType: 'direct' }],
210
+ entryAgentId: 'entry',
211
+ resultAgentId: 'result',
212
+ };
213
+ const run = await createRun(graphConfig);
214
+ (run.Graph as StandardGraph).setSubagentModelOverride(
215
+ new LoopingFakeChatModel()
216
+ );
217
+
218
+ const output = await getGraphSubagentTool(run).invoke(
219
+ {
220
+ description: 'Try to exceed the member turn budget.',
221
+ subagent_type: 'research-team',
222
+ },
223
+ invokeConfig
224
+ );
225
+
226
+ expect(output).toMatch(/Subagent error: Recursion limit of 3 reached/);
227
+ });
228
+
229
+ it('fails closed when human-in-the-loop is enabled', async () => {
230
+ const run = await createRun(makeGraphConfig(), {
231
+ humanInTheLoop: { enabled: true },
232
+ });
233
+
234
+ const result = await getGraphSubagentTool(run).invoke(
235
+ {
236
+ description: 'Do not start this graph.',
237
+ subagent_type: 'research-team',
238
+ },
239
+ invokeConfig
240
+ );
241
+
242
+ expect(result).toBe(
243
+ 'Error: Human-in-the-loop execution is not yet supported for graph subagents.'
244
+ );
245
+ });
246
+
247
+ it('attributes usage to each graph member', async () => {
248
+ const graphConfig: GraphSubagentConfig = {
249
+ ...makeGraphConfig(),
250
+ agents: [makeAgent('entry'), makeAgent('worker'), makeAgent('result')],
251
+ edges: [
252
+ { from: 'entry', to: 'worker', edgeType: 'direct' },
253
+ { from: 'worker', to: 'result', edgeType: 'direct' },
254
+ ],
255
+ entryAgentId: 'entry',
256
+ resultAgentId: 'result',
257
+ };
258
+ const usageEvents: t.SubagentUsageEvent[] = [];
259
+ const run = await createRun(graphConfig, {
260
+ subagentUsageSink: (event) => {
261
+ usageEvents.push(event);
262
+ },
263
+ });
264
+ (run.Graph as StandardGraph).setSubagentModelOverride(
265
+ new UsageFakeChatModel()
266
+ );
267
+
268
+ await getGraphSubagentTool(run).invoke(
269
+ {
270
+ description: 'Complete the measured graph.',
271
+ subagent_type: 'research-team',
272
+ },
273
+ invokeConfig
274
+ );
275
+
276
+ expect(usageEvents).toHaveLength(3);
277
+ expect(
278
+ usageEvents.map((event) => ({
279
+ memberAgentId: event.memberAgentId,
280
+ model: event.model,
281
+ provider: event.provider,
282
+ subagentKind: event.subagentKind,
283
+ }))
284
+ ).toEqual([
285
+ {
286
+ memberAgentId: 'entry',
287
+ model: 'entry-model',
288
+ provider: Providers.OPENAI,
289
+ subagentKind: 'graph',
290
+ },
291
+ {
292
+ memberAgentId: 'worker',
293
+ model: 'worker-model',
294
+ provider: Providers.OPENAI,
295
+ subagentKind: 'graph',
296
+ },
297
+ {
298
+ memberAgentId: 'result',
299
+ model: 'result-model',
300
+ provider: Providers.OPENAI,
301
+ subagentKind: 'graph',
302
+ },
303
+ ]);
304
+ expect(
305
+ new Set(usageEvents.map((event) => event.subagentAgentId)).size
306
+ ).toBe(1);
307
+ const rootRunId = (run.Graph as StandardGraph).runId;
308
+ for (const event of usageEvents) {
309
+ expect(event).toMatchObject({
310
+ runId: rootRunId,
311
+ parentRunId: rootRunId,
312
+ depth: 1,
313
+ });
314
+ expect(event.ancestry).toHaveLength(1);
315
+ expect(event.ancestry?.[0]).toMatchObject({
316
+ subagentType: 'research-team',
317
+ subagentKind: 'graph',
318
+ parentRunId: rootRunId,
319
+ });
320
+ }
321
+ });
322
+ });
@@ -319,6 +319,50 @@ function createAgent(tenantId: string): t.AgentInputs {
319
319
  };
320
320
  }
321
321
 
322
+ function createGraphSubagentParent(tenantId: string): t.AgentInputs {
323
+ const makeMember = (agentId: string): t.AgentInputs => ({
324
+ agentId,
325
+ name: `${agentId} ${tenantId}`,
326
+ provider: Providers.OPENAI,
327
+ clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
328
+ instructions: `Complete the ${agentId} stage.`,
329
+ maxContextTokens: 8000,
330
+ });
331
+ return {
332
+ ...makeMember('parent'),
333
+ maxSubagentDepth: 1,
334
+ subagentConfigs: [
335
+ {
336
+ kind: 'graph',
337
+ type: 'research-team',
338
+ name: 'Research Team',
339
+ description: 'Runs a bounded research chain.',
340
+ agents: [
341
+ makeMember('entry'),
342
+ makeMember('worker'),
343
+ makeMember('result'),
344
+ ],
345
+ edges: [
346
+ {
347
+ from: 'entry',
348
+ to: 'worker',
349
+ edgeType: 'direct',
350
+ prompt: 'Proceed to worker.',
351
+ },
352
+ {
353
+ from: 'worker',
354
+ to: 'result',
355
+ edgeType: 'direct',
356
+ prompt: 'Proceed to result.',
357
+ },
358
+ ],
359
+ entryAgentId: 'entry',
360
+ resultAgentId: 'result',
361
+ },
362
+ ],
363
+ };
364
+ }
365
+
322
366
  async function runTenantFlow(tenantId: string): Promise<void> {
323
367
  const runId = `routing-${tenantId}`;
324
368
  const run = await Run.create<t.IState>({
@@ -382,6 +426,49 @@ async function runTenantFlow(tenantId: string): Promise<void> {
382
426
  });
383
427
  }
384
428
 
429
+ async function runGraphSubagentTenantFlow(tenantId: string): Promise<void> {
430
+ const runId = `routing-graph-${tenantId}`;
431
+ const run = await Run.create<t.IState>({
432
+ runId,
433
+ graphConfig: {
434
+ type: 'standard',
435
+ agents: [createGraphSubagentParent(tenantId)],
436
+ },
437
+ langfuse: tenantLangfuse(tenantId),
438
+ returnContent: true,
439
+ skipCleanup: true,
440
+ });
441
+ run.Graph?.overrideTestModel(
442
+ [
443
+ `Delegating graph work for ${tenantId}.`,
444
+ `Graph work complete for ${tenantId}.`,
445
+ ],
446
+ 1,
447
+ [
448
+ {
449
+ id: `call_graph_subagent_${tenantId}`,
450
+ name: Constants.SUBAGENT,
451
+ args: {
452
+ description: `Run the research chain for ${tenantId}.`,
453
+ subagent_type: 'research-team',
454
+ },
455
+ type: 'tool_call',
456
+ },
457
+ ]
458
+ );
459
+
460
+ await run.processStream(
461
+ { messages: [new HumanMessage(`Run graph work for ${tenantId}`)] },
462
+ {
463
+ ...callerConfig,
464
+ configurable: {
465
+ thread_id: `graph-thread-${tenantId}`,
466
+ user_id: `graph-user-${tenantId}`,
467
+ },
468
+ }
469
+ );
470
+ }
471
+
385
472
  const compactingTokenCounter: t.TokenCounter = (message) => {
386
473
  if (message._getType() === 'system') {
387
474
  return 1;
@@ -454,7 +541,7 @@ describe('Langfuse per-run routing integration', () => {
454
541
  .spyOn(providers, 'getChatModelClass')
455
542
  .mockImplementation(((provider: Providers) => {
456
543
  if (provider === Providers.OPENAI) {
457
- return class extends FakeListChatModel {
544
+ return class RoutingProviderFakeChatModel extends FakeListChatModel {
458
545
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
459
546
  constructor(_options: any) {
460
547
  super({ responses: ['provider response'] });
@@ -531,6 +618,38 @@ describe('Langfuse per-run routing integration', () => {
531
618
  }
532
619
  });
533
620
 
621
+ it('keeps graph-subagent member spans in the owning tenant trace', async () => {
622
+ await Promise.all([
623
+ runGraphSubagentTenantFlow('graph-tenant-a'),
624
+ runGraphSubagentTenantFlow('graph-tenant-b'),
625
+ ]);
626
+
627
+ for (const tenantId of ['graph-tenant-a', 'graph-tenant-b']) {
628
+ const otherTenantId =
629
+ tenantId === 'graph-tenant-a' ? 'graph-tenant-b' : 'graph-tenant-a';
630
+ const starts = startsForTenant(tenantId);
631
+ const traceId = traceIdFromSeed(`routing-graph-${tenantId}`);
632
+
633
+ expectTenantCredentials(starts, tenantId);
634
+ expectOnlyTraceIds(starts, [traceId]);
635
+ expectNamedSpansUseTraceId({
636
+ starts,
637
+ traceId,
638
+ names: [
639
+ `LibreChat Agent: parent ${tenantId}`,
640
+ 'FakeChatModel',
641
+ 'subagent',
642
+ ],
643
+ });
644
+ expect(
645
+ starts.filter(
646
+ (record) => record.name === 'RoutingProviderFakeChatModel'
647
+ )
648
+ ).toHaveLength(3);
649
+ expectNoCrossTenantTrace({ tenantId, otherTenantId, traceId });
650
+ }
651
+ });
652
+
534
653
  it('routes parallel summarization spans to each run config', async () => {
535
654
  await Promise.all([
536
655
  runTenantSummarizationFlow('tenant-a'),
@@ -5,8 +5,8 @@ import {
5
5
  AIMessageChunk,
6
6
  HumanMessage,
7
7
  } from '@langchain/core/messages';
8
+ import type { UsageMetadata, BaseMessage } from '@langchain/core/messages';
8
9
  import type { RunnableConfig } from '@langchain/core/runnables';
9
- import type { UsageMetadata } from '@langchain/core/messages';
10
10
  import type { ToolCall } from '@langchain/core/messages/tool';
11
11
  import type * as t from '@/types';
12
12
  import {
@@ -371,6 +371,8 @@ describe('Subagent Integration', () => {
371
371
  }
372
372
  );
373
373
  const parentUpdateHandler = jest.fn();
374
+ const rootModelEndHandler = jest.fn();
375
+ const nestedUsageEvents: t.SubagentUsageEvent[] = [];
374
376
  let specialistToolDefinitions: t.LCTool[] | undefined;
375
377
  let forwardedToolResults: t.ToolExecuteResult[] | undefined;
376
378
 
@@ -409,7 +411,12 @@ describe('Subagent Integration', () => {
409
411
  const invokeOptions = options as
410
412
  | { callbacks?: unknown[] }
411
413
  | undefined;
412
- const forwarder = (invokeOptions?.callbacks ?? [])[0] as
414
+ const callbacks = invokeOptions?.callbacks ?? [];
415
+ const forwarder = callbacks.find(
416
+ (callback) =>
417
+ typeof (callback as { handleCustomEvent?: unknown })
418
+ .handleCustomEvent === 'function'
419
+ ) as
413
420
  | {
414
421
  handleCustomEvent?: (
415
422
  eventName: string,
@@ -418,6 +425,27 @@ describe('Subagent Integration', () => {
418
425
  ) => Promise<void> | void;
419
426
  }
420
427
  | undefined;
428
+ const usageHandler = callbacks.find(
429
+ (callback) =>
430
+ typeof (callback as { handleLLMEnd?: unknown })
431
+ .handleLLMEnd === 'function'
432
+ ) as
433
+ | {
434
+ handleChatModelStart?: (
435
+ llm: unknown,
436
+ messages: BaseMessage[][],
437
+ runId: string,
438
+ parentRunId?: string,
439
+ extraParams?: unknown,
440
+ tags?: string[],
441
+ metadata?: Record<string, unknown>
442
+ ) => Promise<void> | void;
443
+ handleLLMEnd?: (
444
+ output: unknown,
445
+ runId: string
446
+ ) => Promise<void> | void;
447
+ }
448
+ | undefined;
421
449
  if (forwarder?.handleCustomEvent != null) {
422
450
  forwardedToolResults = await new Promise<t.ToolExecuteResult[]>(
423
451
  (resolve, reject) => {
@@ -442,6 +470,38 @@ describe('Subagent Integration', () => {
442
470
  'specialist-run'
443
471
  );
444
472
  }
473
+ await usageHandler?.handleChatModelStart?.(
474
+ {},
475
+ [[]],
476
+ 'specialist-model-call',
477
+ undefined,
478
+ undefined,
479
+ undefined,
480
+ {
481
+ agentId: 'specialist',
482
+ ls_model_name: 'gpt-4o-mini',
483
+ }
484
+ );
485
+ await usageHandler?.handleLLMEnd?.(
486
+ {
487
+ generations: [
488
+ [
489
+ {
490
+ text: 'specialist done',
491
+ message: new AIMessage({
492
+ content: 'specialist done',
493
+ usage_metadata: {
494
+ input_tokens: 5,
495
+ output_tokens: 3,
496
+ total_tokens: 8,
497
+ },
498
+ }),
499
+ },
500
+ ],
501
+ ],
502
+ },
503
+ 'specialist-model-call'
504
+ );
445
505
  return { messages: [new AIMessage('specialist done')] };
446
506
  }),
447
507
  } as unknown as ReturnType<StandardGraph['createWorkflow']>;
@@ -492,17 +552,22 @@ describe('Subagent Integration', () => {
492
552
  };
493
553
 
494
554
  try {
555
+ const rootRunId = `nested-event-tools-${Date.now()}`;
495
556
  const run = await Run.create<t.IState>({
496
- runId: `nested-event-tools-${Date.now()}`,
557
+ runId: rootRunId,
497
558
  graphConfig: { type: 'standard', agents: [rootAgent] },
498
559
  customHandlers: {
499
560
  [GraphEvents.ON_TOOL_EXECUTE]: { handle: parentToolHandler },
561
+ [GraphEvents.CHAT_MODEL_END]: { handle: rootModelEndHandler },
500
562
  [GraphEvents.ON_SUBAGENT_UPDATE]: {
501
563
  handle: parentUpdateHandler,
502
564
  },
503
565
  },
504
566
  returnContent: true,
505
567
  skipCleanup: true,
568
+ subagentUsageSink: (event) => {
569
+ nestedUsageEvents.push(event);
570
+ },
506
571
  });
507
572
  const rootContext = (run.Graph as StandardGraph).agentContexts.get(
508
573
  'root'
@@ -530,7 +595,68 @@ describe('Subagent Integration', () => {
530
595
  ([, data]) => (data as t.SubagentUpdateEvent).subagentType
531
596
  );
532
597
  expect(forwardedSubagentTypes).toContain('router');
533
- expect(forwardedSubagentTypes).not.toContain('specialist');
598
+ expect(forwardedSubagentTypes).toContain('specialist');
599
+ const specialistStepUpdates = parentUpdateHandler.mock.calls.filter(
600
+ ([, data]) => {
601
+ const event = data as t.SubagentUpdateEvent;
602
+ return (
603
+ event.subagentType === 'specialist' &&
604
+ (event.data as { id?: string } | undefined)?.id ===
605
+ 'specialist-step'
606
+ );
607
+ }
608
+ );
609
+ expect(specialistStepUpdates).toHaveLength(1);
610
+ const routerEvent = parentUpdateHandler.mock.calls
611
+ .map(([, data]) => data as t.SubagentUpdateEvent)
612
+ .find((event) => event.subagentType === 'router');
613
+ const specialistEvent =
614
+ specialistStepUpdates[0][1] as t.SubagentUpdateEvent;
615
+ expect(routerEvent).toMatchObject({
616
+ runId: rootRunId,
617
+ parentRunId: rootRunId,
618
+ depth: 1,
619
+ });
620
+ expect(routerEvent?.ancestry?.map((entry) => entry.subagentType)).toEqual(
621
+ ['router']
622
+ );
623
+ expect(specialistEvent).toMatchObject({
624
+ runId: rootRunId,
625
+ parentRunId: routerEvent?.subagentRunId,
626
+ depth: 2,
627
+ });
628
+ expect(
629
+ specialistEvent.ancestry?.map((entry) => ({
630
+ type: entry.subagentType,
631
+ kind: entry.subagentKind,
632
+ }))
633
+ ).toEqual([
634
+ { type: 'router', kind: 'agent' },
635
+ { type: 'specialist', kind: 'agent' },
636
+ ]);
637
+ expect(nestedUsageEvents).toHaveLength(1);
638
+ expect(rootModelEndHandler).not.toHaveBeenCalled();
639
+ expect(nestedUsageEvents[0]).toMatchObject({
640
+ runId: rootRunId,
641
+ parentRunId: routerEvent?.subagentRunId,
642
+ subagentType: 'specialist',
643
+ subagentAgentId: 'specialist',
644
+ depth: 2,
645
+ usage: {
646
+ input_tokens: 5,
647
+ output_tokens: 3,
648
+ total_tokens: 8,
649
+ },
650
+ });
651
+ expect(
652
+ nestedUsageEvents[0].ancestry?.map((entry) => ({
653
+ type: entry.subagentType,
654
+ kind: entry.subagentKind,
655
+ }))
656
+ ).toEqual([
657
+ { type: 'router', kind: 'agent' },
658
+ { type: 'specialist', kind: 'agent' },
659
+ ]);
534
660
  } finally {
535
661
  createWorkflowSpy.mockRestore();
536
662
  }
@@ -5,7 +5,7 @@ import { Constants } from '@/common';
5
5
 
6
6
  export const SubagentToolName = Constants.SUBAGENT;
7
7
 
8
- export const SubagentToolDescription = `Delegate a task to a specialized subagent that runs in an isolated context window. The subagent executes independently and returns only its final text result — all intermediate tool calls, reasoning, and context stay isolated.
8
+ export const SubagentToolDescription = `Delegate a task to a specialized subagent or bounded agent team that runs in an isolated context window. The delegated execution returns only its designated final text result — all intermediate tool calls, reasoning, and context stay isolated.
9
9
 
10
10
  WHEN TO USE:
11
11
  - The task is self-contained and can be described in a single prompt.
@@ -13,9 +13,9 @@ WHEN TO USE:
13
13
  - A specialized subagent is available for the task domain.
14
14
 
15
15
  WHAT HAPPENS:
16
- - A fresh agent is created with the task description as its only input.
17
- - The subagent runs to completion using its own tools and context.
18
- - Only the final text response is returned to you.
16
+ - A fresh agent or configured agent graph is created with the task description as its only input.
17
+ - The delegated agent or team runs to completion using isolated tools and context.
18
+ - Only the single agent's final response or the graph's designated result-agent response is returned to you.
19
19
 
20
20
  CONSTRAINTS:
21
21
  - subagent_type must match one of the available types listed below.