@librechat/agents 3.4.0 → 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 (204) hide show
  1. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  2. package/dist/cjs/graphs/Graph.cjs +165 -57
  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/HookRegistry.cjs +83 -0
  12. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  13. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
  14. package/dist/cjs/hooks/executeHooks.cjs +60 -14
  15. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  16. package/dist/cjs/hooks/index.cjs.map +1 -1
  17. package/dist/cjs/hooks/types.cjs +2 -0
  18. package/dist/cjs/hooks/types.cjs.map +1 -1
  19. package/dist/cjs/llm/init.cjs +3 -3
  20. package/dist/cjs/llm/invoke.cjs +2 -2
  21. package/dist/cjs/main.cjs +34 -24
  22. package/dist/cjs/messages/format.cjs +40 -23
  23. package/dist/cjs/messages/format.cjs.map +1 -1
  24. package/dist/cjs/run.cjs +130 -53
  25. package/dist/cjs/run.cjs.map +1 -1
  26. package/dist/cjs/session/AgentSession.cjs +37 -2
  27. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  28. package/dist/cjs/session/JsonlSessionStore.cjs +2 -2
  29. package/dist/cjs/stream.cjs +1 -1
  30. package/dist/cjs/summarization/node.cjs +6 -3
  31. package/dist/cjs/summarization/node.cjs.map +1 -1
  32. package/dist/cjs/tools/BashExecutor.cjs +1 -1
  33. package/dist/cjs/tools/CodeExecutor.cjs +1 -1
  34. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
  35. package/dist/cjs/tools/SubagentTool.cjs +4 -4
  36. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  37. package/dist/cjs/tools/ToolNode.cjs +321 -95
  38. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolSearch.cjs +1 -1
  40. package/dist/cjs/tools/eagerEventExecution.cjs +1 -0
  41. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +3 -3
  42. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +2 -2
  43. package/dist/cjs/tools/search/rerankers.cjs +154 -1
  44. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  45. package/dist/cjs/tools/search/tool.cjs +4 -1
  46. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  47. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +601 -0
  48. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -0
  49. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1114 -233
  50. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  51. package/dist/cjs/tools/subagent/SubagentReplay.cjs +177 -0
  52. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
  53. package/dist/cjs/tools/subagent/childGraphConfig.cjs +278 -0
  54. package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -0
  55. package/dist/cjs/tools/subagent/index.cjs +1 -0
  56. package/dist/cjs/tools/subagent/runtimeLimits.cjs +5 -0
  57. package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -0
  58. package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
  59. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  60. package/dist/cjs/utils/index.cjs +2 -2
  61. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  62. package/dist/esm/graphs/Graph.mjs +165 -57
  63. package/dist/esm/graphs/Graph.mjs.map +1 -1
  64. package/dist/esm/graphs/MultiAgentGraph.mjs +81 -33
  65. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  66. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs +14 -0
  67. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -0
  68. package/dist/esm/graphs/createGraph.mjs +15 -0
  69. package/dist/esm/graphs/createGraph.mjs.map +1 -0
  70. package/dist/esm/graphs/index.mjs +1 -0
  71. package/dist/esm/hooks/HookRegistry.mjs +83 -0
  72. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  73. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
  74. package/dist/esm/hooks/executeHooks.mjs +60 -14
  75. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  76. package/dist/esm/hooks/index.mjs.map +1 -1
  77. package/dist/esm/hooks/types.mjs +2 -1
  78. package/dist/esm/hooks/types.mjs.map +1 -1
  79. package/dist/esm/llm/init.mjs +1 -1
  80. package/dist/esm/llm/invoke.mjs +2 -2
  81. package/dist/esm/main.mjs +21 -19
  82. package/dist/esm/messages/format.mjs +40 -23
  83. package/dist/esm/messages/format.mjs.map +1 -1
  84. package/dist/esm/run.mjs +131 -54
  85. package/dist/esm/run.mjs.map +1 -1
  86. package/dist/esm/session/AgentSession.mjs +37 -2
  87. package/dist/esm/session/AgentSession.mjs.map +1 -1
  88. package/dist/esm/session/JsonlSessionStore.mjs +2 -2
  89. package/dist/esm/stream.mjs +1 -1
  90. package/dist/esm/summarization/node.mjs +6 -3
  91. package/dist/esm/summarization/node.mjs.map +1 -1
  92. package/dist/esm/tools/BashExecutor.mjs +1 -1
  93. package/dist/esm/tools/CodeExecutor.mjs +1 -1
  94. package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
  95. package/dist/esm/tools/SubagentTool.mjs +4 -4
  96. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  97. package/dist/esm/tools/ToolNode.mjs +322 -96
  98. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  99. package/dist/esm/tools/ToolSearch.mjs +1 -1
  100. package/dist/esm/tools/eagerEventExecution.mjs +1 -1
  101. package/dist/esm/tools/local/LocalExecutionEngine.mjs +3 -3
  102. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +2 -2
  103. package/dist/esm/tools/search/rerankers.mjs +154 -1
  104. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  105. package/dist/esm/tools/search/tool.mjs +4 -1
  106. package/dist/esm/tools/search/tool.mjs.map +1 -1
  107. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +597 -0
  108. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -0
  109. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1114 -233
  110. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  111. package/dist/esm/tools/subagent/SubagentReplay.mjs +170 -0
  112. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
  113. package/dist/esm/tools/subagent/childGraphConfig.mjs +272 -0
  114. package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -0
  115. package/dist/esm/tools/subagent/index.mjs +1 -0
  116. package/dist/esm/tools/subagent/runtimeLimits.mjs +5 -0
  117. package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -0
  118. package/dist/esm/tools/toolOutputReferences.mjs +20 -0
  119. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  120. package/dist/esm/utils/index.mjs +2 -2
  121. package/dist/types/agents/AgentContext.d.ts +1 -1
  122. package/dist/types/graphs/Graph.d.ts +22 -2
  123. package/dist/types/graphs/MultiAgentGraph.d.ts +7 -1
  124. package/dist/types/graphs/applyGraphRuntimeConfig.d.ts +3 -0
  125. package/dist/types/graphs/createGraph.d.ts +13 -0
  126. package/dist/types/graphs/graphFactory.d.ts +13 -0
  127. package/dist/types/graphs/index.d.ts +2 -0
  128. package/dist/types/hooks/HookRegistry.d.ts +10 -1
  129. package/dist/types/hooks/executeHooks.d.ts +5 -1
  130. package/dist/types/hooks/index.d.ts +2 -2
  131. package/dist/types/hooks/types.d.ts +12 -0
  132. package/dist/types/run.d.ts +6 -3
  133. package/dist/types/session/AgentSession.d.ts +1 -0
  134. package/dist/types/tools/SubagentTool.d.ts +1 -1
  135. package/dist/types/tools/ToolNode.d.ts +23 -15
  136. package/dist/types/tools/search/rerankers.d.ts +28 -0
  137. package/dist/types/tools/search/types.d.ts +38 -1
  138. package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +171 -0
  139. package/dist/types/tools/subagent/SubagentExecutor.d.ts +70 -36
  140. package/dist/types/tools/subagent/SubagentReplay.d.ts +85 -0
  141. package/dist/types/tools/subagent/childGraphConfig.d.ts +37 -0
  142. package/dist/types/tools/subagent/index.d.ts +1 -1
  143. package/dist/types/tools/subagent/runtimeLimits.d.ts +4 -0
  144. package/dist/types/tools/toolOutputReferences.d.ts +12 -0
  145. package/dist/types/types/graph.d.ts +189 -24
  146. package/dist/types/types/hitl.d.ts +15 -0
  147. package/package.json +8 -2
  148. package/src/agents/AgentContext.ts +1 -1
  149. package/src/graphs/Graph.ts +349 -104
  150. package/src/graphs/MultiAgentGraph.ts +149 -47
  151. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
  152. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
  153. package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
  154. package/src/graphs/__tests__/MultiAgentGraph.test.ts +86 -0
  155. package/src/graphs/__tests__/composition.smoke.test.ts +152 -0
  156. package/src/graphs/__tests__/createGraph.test.ts +206 -0
  157. package/src/graphs/applyGraphRuntimeConfig.ts +25 -0
  158. package/src/graphs/createGraph.ts +32 -0
  159. package/src/graphs/graphFactory.ts +12 -0
  160. package/src/graphs/index.ts +2 -0
  161. package/src/hooks/HookRegistry.ts +151 -1
  162. package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
  163. package/src/hooks/__tests__/executeHooks.test.ts +160 -0
  164. package/src/hooks/executeHooks.ts +133 -15
  165. package/src/hooks/index.ts +3 -1
  166. package/src/hooks/types.ts +16 -0
  167. package/src/messages/format.ts +54 -26
  168. package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
  169. package/src/messages/formatAgentMessages.steer.test.ts +16 -11
  170. package/src/messages/formatAgentMessages.test.ts +7 -2
  171. package/src/run.ts +214 -56
  172. package/src/session/AgentSession.ts +52 -2
  173. package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
  174. package/src/specs/graph-subagent.live.test.ts +242 -0
  175. package/src/specs/graph-subagent.test.ts +322 -0
  176. package/src/specs/langfuse-routing.integration.test.ts +120 -1
  177. package/src/specs/subagent.test.ts +130 -4
  178. package/src/summarization/__tests__/node.test.ts +32 -14
  179. package/src/summarization/node.ts +15 -11
  180. package/src/tools/SubagentTool.ts +4 -4
  181. package/src/tools/ToolNode.ts +643 -157
  182. package/src/tools/__tests__/SubagentExecutor.test.ts +740 -15
  183. package/src/tools/__tests__/SubagentReplay.test.ts +329 -0
  184. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
  185. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +512 -2
  186. package/src/tools/__tests__/hitl.test.ts +234 -5
  187. package/src/tools/__tests__/subagentHooks.test.ts +1126 -61
  188. package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
  189. package/src/tools/search/rag-api-reranker.test.ts +891 -0
  190. package/src/tools/search/rerankers.ts +260 -0
  191. package/src/tools/search/tool.ts +6 -0
  192. package/src/tools/search/types.ts +45 -1
  193. package/src/tools/subagent/SubagentExecutionRegistry.ts +1159 -0
  194. package/src/tools/subagent/SubagentExecutor.ts +2232 -312
  195. package/src/tools/subagent/SubagentReplay.ts +584 -0
  196. package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +637 -0
  197. package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +3054 -0
  198. package/src/tools/subagent/__tests__/childGraphConfig.test.ts +521 -0
  199. package/src/tools/subagent/childGraphConfig.ts +694 -0
  200. package/src/tools/subagent/index.ts +6 -0
  201. package/src/tools/subagent/runtimeLimits.ts +7 -0
  202. package/src/tools/toolOutputReferences.ts +40 -1
  203. package/src/types/graph.ts +222 -24
  204. package/src/types/hitl.ts +16 -0
@@ -0,0 +1,242 @@
1
+ import { config as dotenvConfig } from 'dotenv';
2
+
3
+ dotenvConfig(
4
+ process.env.LIVE_ENV_PATH == null
5
+ ? undefined
6
+ : { path: process.env.LIVE_ENV_PATH }
7
+ );
8
+ dotenvConfig();
9
+
10
+ import { describe, expect, it, jest } from '@jest/globals';
11
+ import type { RunnableConfig } from '@langchain/core/runnables';
12
+ import type * as t from '@/types';
13
+ import { Constants, GraphEvents, Providers } from '@/common';
14
+ import { StandardGraph } from '@/graphs/Graph';
15
+ import { Run } from '@/run';
16
+
17
+ jest.setTimeout(180_000);
18
+
19
+ const liveEnabled = process.env.RUN_GRAPH_SUBAGENT_LIVE_TESTS === '1';
20
+ const requestedProvider = process.env.GRAPH_SUBAGENT_LIVE_PROVIDER;
21
+ const describeIfLive = liveEnabled ? describe : describe.skip;
22
+
23
+ type LiveProviderCase = {
24
+ label: string;
25
+ provider: Providers;
26
+ model: string;
27
+ apiKey?: string;
28
+ };
29
+
30
+ const LIVE_PROVIDER_CASES: readonly LiveProviderCase[] = [
31
+ {
32
+ label: 'Anthropic Haiku',
33
+ provider: Providers.ANTHROPIC,
34
+ model:
35
+ process.env.ANTHROPIC_GRAPH_SUBAGENT_LIVE_MODEL ?? 'claude-haiku-4-5',
36
+ apiKey: process.env.ANTHROPIC_API_KEY,
37
+ },
38
+ {
39
+ label: 'OpenAI GPT-4.1 mini',
40
+ provider: Providers.OPENAI,
41
+ model: process.env.OPENAI_GRAPH_SUBAGENT_LIVE_MODEL ?? 'gpt-4.1-mini',
42
+ apiKey: process.env.OPENAI_API_KEY,
43
+ },
44
+ ];
45
+
46
+ const explicitlyRequestedCase =
47
+ requestedProvider == null || requestedProvider === ''
48
+ ? undefined
49
+ : LIVE_PROVIDER_CASES.find(
50
+ ({ provider }) =>
51
+ provider.toLowerCase() === requestedProvider.toLowerCase()
52
+ );
53
+
54
+ if (
55
+ liveEnabled &&
56
+ requestedProvider != null &&
57
+ requestedProvider !== '' &&
58
+ explicitlyRequestedCase == null
59
+ ) {
60
+ throw new Error(
61
+ `Unknown GRAPH_SUBAGENT_LIVE_PROVIDER "${requestedProvider}".`
62
+ );
63
+ }
64
+ if (
65
+ liveEnabled &&
66
+ explicitlyRequestedCase != null &&
67
+ (explicitlyRequestedCase.apiKey == null ||
68
+ explicitlyRequestedCase.apiKey === '')
69
+ ) {
70
+ throw new Error(
71
+ `Missing credentials for GRAPH_SUBAGENT_LIVE_PROVIDER "${requestedProvider}".`
72
+ );
73
+ }
74
+ if (
75
+ liveEnabled &&
76
+ explicitlyRequestedCase == null &&
77
+ !LIVE_PROVIDER_CASES.some(({ apiKey }) => apiKey != null && apiKey !== '')
78
+ ) {
79
+ throw new Error(
80
+ 'No graph-subagent live-provider credentials are configured.'
81
+ );
82
+ }
83
+
84
+ function createLiveAgent(
85
+ testCase: LiveProviderCase,
86
+ agentId: string,
87
+ marker: string
88
+ ): t.AgentInputs {
89
+ return {
90
+ agentId,
91
+ provider: testCase.provider,
92
+ clientOptions: {
93
+ modelName: testCase.model,
94
+ apiKey: testCase.apiKey,
95
+ temperature: 0,
96
+ maxTokens: 64,
97
+ streaming: true,
98
+ streamUsage: true,
99
+ },
100
+ instructions: `You are the ${agentId} validation stage. When asked to proceed, reply with exactly ${marker} and no other text.`,
101
+ maxContextTokens: 8_000,
102
+ };
103
+ }
104
+
105
+ function getGraphSubagentTool(run: Run<t.IState>): t.GenericTool {
106
+ const tools = (run.Graph as StandardGraph).agentContexts.get('parent')
107
+ ?.graphTools as t.GenericTool[] | undefined;
108
+ const subagentTool = tools?.find(
109
+ (tool) => 'name' in tool && tool.name === Constants.SUBAGENT
110
+ );
111
+ if (subagentTool == null) {
112
+ throw new Error('Expected graph subagent tool');
113
+ }
114
+ return subagentTool;
115
+ }
116
+
117
+ describeIfLive('Graph subagent live providers', () => {
118
+ for (const testCase of LIVE_PROVIDER_CASES) {
119
+ const providerSelected =
120
+ requestedProvider == null ||
121
+ requestedProvider === '' ||
122
+ requestedProvider.toLowerCase() === testCase.provider.toLowerCase();
123
+ const itIfAvailable =
124
+ providerSelected && testCase.apiKey != null && testCase.apiKey !== ''
125
+ ? it
126
+ : it.skip;
127
+
128
+ itIfAvailable(
129
+ `${testCase.label} runs a bounded team and returns only its result member`,
130
+ async () => {
131
+ const usageEvents: t.SubagentUsageEvent[] = [];
132
+ const updateEvents: t.SubagentUpdateEvent[] = [];
133
+ const entry = createLiveAgent(testCase, 'entry', 'ENTRY_MARKER');
134
+ const worker = createLiveAgent(testCase, 'worker', 'WORKER_MARKER');
135
+ const resultAgent = createLiveAgent(testCase, 'result', 'FINAL_MARKER');
136
+ const parent: t.AgentInputs = {
137
+ ...createLiveAgent(testCase, 'parent', 'PARENT_MARKER'),
138
+ maxSubagentDepth: 1,
139
+ subagentConfigs: [
140
+ {
141
+ kind: 'graph',
142
+ type: 'live-team',
143
+ name: 'Live Team',
144
+ description: 'Runs a three-member live-provider chain.',
145
+ maxTurns: 2,
146
+ agents: [entry, worker, resultAgent],
147
+ edges: [
148
+ {
149
+ from: 'entry',
150
+ to: 'worker',
151
+ edgeType: 'direct',
152
+ prompt: 'Proceed with the worker validation stage.',
153
+ },
154
+ {
155
+ from: 'worker',
156
+ to: 'result',
157
+ edgeType: 'direct',
158
+ prompt: 'Proceed with the result validation stage.',
159
+ },
160
+ ],
161
+ entryAgentId: 'entry',
162
+ resultAgentId: 'result',
163
+ },
164
+ ],
165
+ };
166
+ const rootRunId = `graph-subagent-live-${testCase.provider}-${Date.now()}`;
167
+ const run = await Run.create<t.IState>({
168
+ runId: rootRunId,
169
+ graphConfig: { type: 'standard', agents: [parent] },
170
+ returnContent: true,
171
+ skipCleanup: true,
172
+ subagentUsageSink: (event) => {
173
+ usageEvents.push(event);
174
+ },
175
+ customHandlers: {
176
+ [GraphEvents.ON_SUBAGENT_UPDATE]: {
177
+ handle: (_event, data): void => {
178
+ updateEvents.push(data as t.SubagentUpdateEvent);
179
+ },
180
+ },
181
+ },
182
+ });
183
+ const invokeConfig: RunnableConfig = {
184
+ configurable: {
185
+ thread_id: `graph-subagent-live-${testCase.provider}`,
186
+ },
187
+ };
188
+
189
+ const output = await getGraphSubagentTool(run).invoke(
190
+ {
191
+ description: 'Begin the entry validation stage.',
192
+ subagent_type: 'live-team',
193
+ },
194
+ invokeConfig
195
+ );
196
+ const outputText = String(output);
197
+
198
+ expect(outputText).toContain('FINAL_MARKER');
199
+ expect(outputText).not.toContain('ENTRY_MARKER');
200
+ expect(outputText).not.toContain('WORKER_MARKER');
201
+ expect(usageEvents.map((event) => event.memberAgentId).sort()).toEqual([
202
+ 'entry',
203
+ 'result',
204
+ 'worker',
205
+ ]);
206
+ for (const event of usageEvents) {
207
+ expect(event.runId).toBe(rootRunId);
208
+ expect(event.parentRunId).toBe(rootRunId);
209
+ expect(event.depth).toBe(1);
210
+ expect(event.ancestry?.map((entry) => entry.subagentType)).toEqual([
211
+ 'live-team',
212
+ ]);
213
+ expect(event.subagentKind).toBe('graph');
214
+ expect(event.provider).toBe(testCase.provider);
215
+ expect(event.model).toBeTruthy();
216
+ expect(event.usage.total_tokens).toBeGreaterThan(0);
217
+ }
218
+ expect(
219
+ updateEvents.filter((event) => event.phase === 'start')
220
+ ).toHaveLength(1);
221
+ expect(
222
+ updateEvents.filter((event) => event.phase === 'stop')
223
+ ).toHaveLength(1);
224
+ for (const event of updateEvents) {
225
+ expect(event.runId).toBe(rootRunId);
226
+ expect(event.parentRunId).toBe(rootRunId);
227
+ expect(event.depth).toBe(1);
228
+ expect(event.ancestry?.map((entry) => entry.subagentType)).toEqual([
229
+ 'live-team',
230
+ ]);
231
+ }
232
+ expect(
233
+ new Set(
234
+ updateEvents
235
+ .map((event) => event.memberAgentId)
236
+ .filter((agentId): agentId is string => agentId != null)
237
+ )
238
+ ).toEqual(new Set(['entry', 'worker', 'result']));
239
+ }
240
+ );
241
+ }
242
+ });
@@ -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'),