@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
@@ -17,6 +17,7 @@ import {
17
17
  import type { BaseMessage, AIMessageChunk } from '@langchain/core/messages';
18
18
  import type { LangGraphRunnableConfig } from '@langchain/langgraph';
19
19
  import type { ToolRuntime } from '@langchain/core/tools';
20
+ import type { GraphFactoryDependencies } from '@/graphs/graphFactory';
20
21
  import type * as t from '@/types';
21
22
  import { serializeToolContentBounded } from '@/utils/toolContent';
22
23
  import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
@@ -124,6 +125,30 @@ function isValidHandoffGroupId(value: unknown): value is number {
124
125
  return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
125
126
  }
126
127
 
128
+ function getLastNewAiMessage(
129
+ messages: BaseMessage[],
130
+ previousMessages: BaseMessage[]
131
+ ): BaseMessage | undefined {
132
+ const previousMessageObjects = new Set(previousMessages);
133
+ const previousMessageIds = new Set<string>();
134
+ for (const message of previousMessages) {
135
+ if (message.id != null) {
136
+ previousMessageIds.add(message.id);
137
+ }
138
+ }
139
+ for (let index = messages.length - 1; index >= 0; index--) {
140
+ const message = messages[index];
141
+ if (
142
+ message.getType() === 'ai' &&
143
+ !previousMessageObjects.has(message) &&
144
+ (message.id == null || !previousMessageIds.has(message.id))
145
+ ) {
146
+ return message;
147
+ }
148
+ }
149
+ return undefined;
150
+ }
151
+
127
152
  function withHandoffGroupMetadata(
128
153
  config: LangGraphRunnableConfig | undefined,
129
154
  groupId: number | undefined
@@ -156,6 +181,9 @@ export class MultiAgentGraph extends StandardGraph {
156
181
  private startingNodes: Set<string> = new Set();
157
182
  private directEdges: t.GraphEdge[] = [];
158
183
  private handoffEdges: t.GraphEdge[] = [];
184
+ private handoffSourceIds = new Set<string>();
185
+ private readonly resultAgentId?: string;
186
+ private readonly memberRecursionLimit?: number;
159
187
  private handoffPromptLabels: Map<string, Set<string>> = new Map();
160
188
  /**
161
189
  * Map of agentId to parallel group info.
@@ -169,11 +197,34 @@ export class MultiAgentGraph extends StandardGraph {
169
197
  */
170
198
  private agentParallelGroups: Map<string, number> = new Map();
171
199
 
172
- constructor(input: t.MultiAgentGraphInput) {
173
- super(input);
200
+ constructor(
201
+ input: t.MultiAgentGraphInput,
202
+ dependencies?: GraphFactoryDependencies
203
+ ) {
204
+ super(input, dependencies);
174
205
  this.edges = input.edges;
206
+ this.resultAgentId = input.resultAgentId;
207
+ this.memberRecursionLimit = input.memberRecursionLimit;
208
+ if (
209
+ this.memberRecursionLimit != null &&
210
+ (!Number.isSafeInteger(this.memberRecursionLimit) ||
211
+ this.memberRecursionLimit <= 0)
212
+ ) {
213
+ throw new Error(
214
+ 'MultiAgentGraph: memberRecursionLimit must be a positive safe integer.'
215
+ );
216
+ }
217
+ if (
218
+ this.resultAgentId != null &&
219
+ !this.agentContexts.has(this.resultAgentId)
220
+ ) {
221
+ throw new Error(
222
+ `MultiAgentGraph: resultAgentId "${this.resultAgentId}" is not present in agents.`
223
+ );
224
+ }
175
225
  this.validateEdgeAgents();
176
226
  this.categorizeEdges();
227
+ this.validateCommandRoutedDirectEdges();
177
228
  this.analyzeGraph();
178
229
  this.createHandoffTools();
179
230
  }
@@ -221,26 +272,61 @@ export class MultiAgentGraph extends StandardGraph {
221
272
  */
222
273
  private categorizeEdges(): void {
223
274
  for (const edge of this.edges) {
224
- // Default behavior: edges with conditions or explicit 'handoff' type are handoff edges
225
- // Edges with explicit 'direct' type or multi-destination without conditions are direct edges
226
- if (edge.edgeType === 'direct') {
275
+ const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
276
+ const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
277
+ const isDefaultDirect =
278
+ edge.edgeType == null &&
279
+ edge.condition == null &&
280
+ sources.length === 1 &&
281
+ destinations.length > 1;
282
+ if (edge.edgeType === 'direct' || isDefaultDirect) {
227
283
  this.directEdges.push(edge);
228
- } else if (edge.edgeType === 'handoff' || edge.condition != null) {
229
- this.handoffEdges.push(edge);
230
- } else {
231
- // Default: single-to-single edges are handoff, single-to-multiple are direct
232
- const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
233
- const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
284
+ continue;
285
+ }
286
+ this.handoffEdges.push(edge);
287
+ for (const source of sources) {
288
+ this.handoffSourceIds.add(source);
289
+ }
290
+ }
291
+ }
234
292
 
235
- if (sources.length === 1 && destinations.length > 1) {
236
- // Fan-out pattern defaults to direct
237
- this.directEdges.push(edge);
238
- } else {
239
- // Everything else defaults to handoff
240
- this.handoffEdges.push(edge);
241
- }
293
+ /** Static waiting/prompt edges cannot also be driven by Command routing. */
294
+ private validateCommandRoutedDirectEdges(): void {
295
+ const destinationGroups = new Map<
296
+ string,
297
+ { hasPrompt: boolean; commandSource?: string }
298
+ >();
299
+ for (const edge of this.directEdges) {
300
+ const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
301
+ const commandSource = sources.find((source) =>
302
+ this.handoffSourceIds.has(source)
303
+ );
304
+ if (commandSource != null && sources.length > 1) {
305
+ throw new Error(
306
+ 'MultiAgentGraph: grouped direct edge cannot include command-routed ' +
307
+ `source "${commandSource}". Split handoff routing from all-of fan-in.`
308
+ );
309
+ }
310
+ const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
311
+ const hasPrompt = edge.prompt != null && edge.prompt !== '';
312
+ for (const destination of destinations) {
313
+ const group = destinationGroups.get(destination) ?? {
314
+ hasPrompt: false,
315
+ };
316
+ group.hasPrompt ||= hasPrompt;
317
+ group.commandSource ??= commandSource;
318
+ destinationGroups.set(destination, group);
242
319
  }
243
320
  }
321
+ for (const { hasPrompt, commandSource } of destinationGroups.values()) {
322
+ if (!hasPrompt || commandSource == null) {
323
+ continue;
324
+ }
325
+ throw new Error(
326
+ 'MultiAgentGraph: prompted direct edge cannot include command-routed ' +
327
+ `source "${commandSource}". Move the prompt into the routed node.`
328
+ );
329
+ }
244
330
  }
245
331
 
246
332
  /**
@@ -582,7 +668,7 @@ export class MultiAgentGraph extends StandardGraph {
582
668
  * 3. Include all messages before the AIMessage plus the filtered pair
583
669
  */
584
670
  const messages = state.messages;
585
- let filteredMessages = messages;
671
+ let filteredMessages: BaseMessage[];
586
672
  let aiMessageIndex = -1;
587
673
 
588
674
  /** Find the AIMessage containing this tool call */
@@ -946,9 +1032,22 @@ export class MultiAgentGraph extends StandardGraph {
946
1032
  reducer: (a, b) => b,
947
1033
  default: () => [],
948
1034
  }),
1035
+ subagentResult: Annotation<t.SubagentGraphResult | undefined>({
1036
+ reducer: (_current, update) => update,
1037
+ default: () => undefined,
1038
+ }),
949
1039
  });
950
1040
 
951
1041
  const builder = new StateGraph(StateAnnotation);
1042
+ const addDirectEdge = (sources: string[], destination: string): void => {
1043
+ if (sources.length === 0) {
1044
+ return;
1045
+ }
1046
+ const source = sources.length === 1 ? sources[0] : sources;
1047
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1048
+ /** @ts-ignore */
1049
+ builder.addEdge(source, destination);
1050
+ };
952
1051
 
953
1052
  // Add all agents as complete subgraphs
954
1053
  for (const [agentId] of this.agentContexts) {
@@ -997,6 +1096,11 @@ export class MultiAgentGraph extends StandardGraph {
997
1096
  config?: LangGraphRunnableConfig
998
1097
  ): Promise<t.MultiAgentGraphState | Command> => {
999
1098
  let result: t.MultiAgentGraphState;
1099
+ let inputMessages = state.messages;
1100
+ const memberConfig =
1101
+ this.memberRecursionLimit == null
1102
+ ? config
1103
+ : { ...config, recursionLimit: this.memberRecursionLimit };
1000
1104
 
1001
1105
  /**
1002
1106
  * Check if this agent is receiving a handoff.
@@ -1092,9 +1196,10 @@ export class MultiAgentGraph extends StandardGraph {
1092
1196
  ...state,
1093
1197
  messages: messagesForAgent,
1094
1198
  };
1199
+ inputMessages = messagesForAgent;
1095
1200
  result = await agentSubgraph.invoke(
1096
1201
  transformedState,
1097
- withHandoffGroupMetadata(config, parallelGroupId)
1202
+ withHandoffGroupMetadata(memberConfig, parallelGroupId)
1098
1203
  );
1099
1204
  result = {
1100
1205
  ...result,
@@ -1140,14 +1245,25 @@ export class MultiAgentGraph extends StandardGraph {
1140
1245
  ...state,
1141
1246
  messages: state.agentMessages,
1142
1247
  };
1143
- result = await agentSubgraph.invoke(transformedState, config);
1248
+ inputMessages = state.agentMessages;
1249
+ result = await agentSubgraph.invoke(transformedState, memberConfig);
1144
1250
  result = {
1145
1251
  ...result,
1146
1252
  /** Clear agentMessages for next agent */
1147
1253
  agentMessages: [],
1148
1254
  };
1149
1255
  } else {
1150
- result = await agentSubgraph.invoke(state, config);
1256
+ result = await agentSubgraph.invoke(state, memberConfig);
1257
+ }
1258
+
1259
+ if (this.resultAgentId === agentId) {
1260
+ result = {
1261
+ ...result,
1262
+ subagentResult: {
1263
+ agentId,
1264
+ message: getLastNewAiMessage(result.messages, inputMessages),
1265
+ },
1266
+ };
1151
1267
  }
1152
1268
 
1153
1269
  /** If agent has both handoff and direct edges, use Command for exclusive routing */
@@ -1296,11 +1412,7 @@ export class MultiAgentGraph extends StandardGraph {
1296
1412
  /** Add edges from all sources to the wrapper, then wrapper to destination */
1297
1413
  for (const edge of edges) {
1298
1414
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
1299
- for (const source of sources) {
1300
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1301
- /** @ts-ignore */
1302
- builder.addEdge(source, wrapperNodeId);
1303
- }
1415
+ addDirectEdge(sources, wrapperNodeId);
1304
1416
  }
1305
1417
 
1306
1418
  /** Single edge from wrapper to destination */
@@ -1311,26 +1423,16 @@ export class MultiAgentGraph extends StandardGraph {
1311
1423
  /** No prompt instructions, add direct edges (skip if source uses Command routing) */
1312
1424
  for (const edge of edges) {
1313
1425
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
1314
- for (const source of sources) {
1315
- /** Check if this source node has both handoff and direct edges */
1316
- const sourceHandoffEdges = this.handoffEdges.filter((e) => {
1317
- const eSources = Array.isArray(e.from) ? e.from : [e.from];
1318
- return eSources.includes(source);
1319
- });
1320
- const sourceDirectEdges = this.directEdges.filter((e) => {
1321
- const eSources = Array.isArray(e.from) ? e.from : [e.from];
1322
- return eSources.includes(source);
1323
- });
1324
-
1325
- /** Skip adding edge if source uses Command routing (has both types) */
1326
- if (sourceHandoffEdges.length > 0 && sourceDirectEdges.length > 0) {
1327
- continue;
1328
- }
1329
-
1330
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1331
- /** @ts-ignore */
1332
- builder.addEdge(source, destination);
1333
- }
1426
+ const staticSources = sources.filter(
1427
+ (source) =>
1428
+ !this.handoffEdges.some((handoffEdge) => {
1429
+ const handoffSources = Array.isArray(handoffEdge.from)
1430
+ ? handoffEdge.from
1431
+ : [handoffEdge.from];
1432
+ return handoffSources.includes(source);
1433
+ })
1434
+ );
1435
+ addDirectEdge(staticSources, destination);
1334
1436
  }
1335
1437
  }
1336
1438
  }
@@ -1,9 +1,9 @@
1
- import { describe, it, expect } from '@jest/globals';
1
+ import { describe, it, expect, jest } from '@jest/globals';
2
2
  import type { RunnableConfig } from '@langchain/core/runnables';
3
3
  import type * as t from '@/types';
4
4
  import { StreamLimitExceededError } from '@/llm/streamLimits';
5
- import { Providers } from '@/common';
6
5
  import { StandardGraph } from '../Graph';
6
+ import { Providers } from '@/common';
7
7
 
8
8
  const makeAgent = (agentId: string): t.AgentInputs => ({
9
9
  agentId,
@@ -25,7 +25,51 @@ const makeTrip = (): StreamLimitExceededError =>
25
25
  toolName: 'db_query',
26
26
  });
27
27
 
28
+ type CompiledRegistrationInternals = {
29
+ _compiledToolNodes: Set<{ clearDirectPathTurns(): void }>;
30
+ _subagentExecutors: Set<{
31
+ clearHeavyState(): void;
32
+ resetCheckpointThreadIds(): void;
33
+ }>;
34
+ };
35
+
28
36
  describe('run breaker lifecycle', () => {
37
+ it('retains compiled cleanup registrations across graph reuse', () => {
38
+ const graph = makeGraph();
39
+ const internals = graph as unknown as CompiledRegistrationInternals;
40
+ const clearDirectPathTurns = jest.fn();
41
+ const clearSubagentState = jest.fn();
42
+ const resetCheckpointThreadIds = jest.fn();
43
+ internals._compiledToolNodes.add({ clearDirectPathTurns });
44
+ internals._subagentExecutors.add({
45
+ clearHeavyState: clearSubagentState,
46
+ resetCheckpointThreadIds,
47
+ });
48
+
49
+ graph.clearHeavyState();
50
+ graph.clearHeavyState();
51
+
52
+ expect(clearDirectPathTurns).toHaveBeenCalledTimes(2);
53
+ expect(clearSubagentState).toHaveBeenCalledTimes(2);
54
+ expect(internals._compiledToolNodes.size).toBe(1);
55
+ expect(internals._subagentExecutors.size).toBe(1);
56
+ });
57
+
58
+ it('resets subagent checkpoint ownership at each fresh run start', () => {
59
+ const graph = makeGraph();
60
+ const internals = graph as unknown as CompiledRegistrationInternals;
61
+ const resetCheckpointThreadIds = jest.fn();
62
+ internals._subagentExecutors.add({
63
+ clearHeavyState: jest.fn(),
64
+ resetCheckpointThreadIds,
65
+ });
66
+
67
+ graph.resetValues();
68
+ graph.resetValues();
69
+
70
+ expect(resetCheckpointThreadIds).toHaveBeenCalledTimes(2);
71
+ });
72
+
29
73
  it('replaces the breaker at every run start, even when un-aborted', () => {
30
74
  const graph = makeGraph();
31
75
  const previous = graph.breakerAbort;
@@ -115,9 +159,7 @@ describe('run breaker lifecycle', () => {
115
159
  model: model as unknown as t.ChatModel,
116
160
  messages: [new HumanMessage('hi')],
117
161
  provider: Providers.OPENAI,
118
- context: graph as Parameters<
119
- typeof attemptInvoke
120
- >[0]['context'],
162
+ context: graph as Parameters<typeof attemptInvoke>[0]['context'],
121
163
  onChunk: () => {
122
164
  /* producer loop enforcement is under test */
123
165
  },
@@ -225,10 +267,7 @@ describe('run breaker lifecycle', () => {
225
267
  /** With a live breaker the entry guard must not fire; the node then
226
268
  * fails later, on the missing config, proving it got past the guard. */
227
269
  await expect(
228
- node(
229
- { messages: [] } as unknown as t.AgentSubgraphState,
230
- undefined
231
- )
270
+ node({ messages: [] } as unknown as t.AgentSubgraphState, undefined)
232
271
  ).rejects.toThrow('No config provided');
233
272
  });
234
273
  });
@@ -1338,7 +1338,7 @@ describe('context overflow recovery', () => {
1338
1338
  if (agentContext == null) {
1339
1339
  throw new Error('Expected default agent context');
1340
1340
  }
1341
- expect(run.Graph.compileOptions?.checkpointer).toBeUndefined();
1341
+ expect(run.Graph.compileOptions?.checkpointer).toBeInstanceOf(MemorySaver);
1342
1342
  expect(run.Graph.hasCompiledCheckpointer).toBe(true);
1343
1343
  run.Graph.resetValues(undefined, 'checkpoint-scope');
1344
1344
  agentContext.preserveOriginalToolContent(new Map([[2, 'full output']]));
@@ -0,0 +1,80 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import type { SubagentToolNodeResumeState } from '@/tools/subagent/SubagentReplay';
3
+ import { StandardGraph } from '../Graph';
4
+ import { Providers } from '@/common';
5
+
6
+ type ResumeToolNode = {
7
+ createSubagentResumeState(): SubagentToolNodeResumeState;
8
+ restoreSubagentResumeState(state: SubagentToolNodeResumeState): void;
9
+ };
10
+
11
+ const makeGraph = (runId: string): StandardGraph =>
12
+ new StandardGraph({
13
+ runId,
14
+ agents: [
15
+ {
16
+ agentId: 'child-agent',
17
+ provider: Providers.OPENAI,
18
+ instructions: 'Test child state restoration.',
19
+ },
20
+ ],
21
+ });
22
+
23
+ const initializeToolNode = (graph: StandardGraph): ResumeToolNode => {
24
+ const agentContext = graph.agentContexts.get('child-agent');
25
+ return graph.initializeTools({
26
+ currentTools: [],
27
+ agentContext,
28
+ }) as unknown as ResumeToolNode;
29
+ };
30
+
31
+ describe('subagent graph resume state', () => {
32
+ it('restores tool identity, turns, references, and sessions into a branch', () => {
33
+ const source = makeGraph('source');
34
+ source.toolOutputReferences = { enabled: true };
35
+ const sourceNode = initializeToolNode(source);
36
+ source.toolCallStepIds.set('call_tool', 'step_tool');
37
+ source.sessions.set('execute_code', {
38
+ session_id: 'sandbox-session',
39
+ lastUpdated: 42,
40
+ });
41
+ source
42
+ .getOrCreateToolOutputRegistry()
43
+ ?.set('source-scope', 'tool0turn0', 'raw output');
44
+ source.getEagerEventToolUsageCount('child-agent').set('calculator', 3);
45
+ source.eagerEventToolSuppressions.add('unstable_search');
46
+ const sourceNodeState = sourceNode.createSubagentResumeState();
47
+ sourceNode.restoreSubagentResumeState({
48
+ ...sourceNodeState,
49
+ toolUsageCounts: [{ toolName: 'calculator', count: 3 }],
50
+ directPathTurns: [{ toolCallId: 'call_tool', turn: 2 }],
51
+ });
52
+
53
+ const target = makeGraph('target');
54
+ target.toolOutputReferences = { enabled: true };
55
+ const targetNode = initializeToolNode(target);
56
+ target.restoreSubagentResumeState(
57
+ source.createSubagentResumeState('source-scope'),
58
+ 'target-scope'
59
+ );
60
+
61
+ expect(target.toolCallStepIds.get('call_tool')).toBe('step_tool');
62
+ expect(target.sessions.get('execute_code')).toMatchObject({
63
+ session_id: 'sandbox-session',
64
+ lastUpdated: 42,
65
+ });
66
+ expect(
67
+ target.getOrCreateToolOutputRegistry()?.get('target-scope', 'tool0turn0')
68
+ ).toBe('raw output');
69
+ expect(targetNode.createSubagentResumeState()).toMatchObject({
70
+ toolUsageCounts: [{ toolName: 'calculator', count: 3 }],
71
+ directPathTurns: [{ toolCallId: 'call_tool', turn: 2 }],
72
+ });
73
+ expect(
74
+ target.getEagerEventToolUsageCount('child-agent').get('calculator')
75
+ ).toBe(3);
76
+ expect(target.eagerEventToolSuppressions).toEqual(
77
+ new Set(['unstable_search'])
78
+ );
79
+ });
80
+ });
@@ -88,4 +88,90 @@ describe('MultiAgentGraph.validateEdgeAgents', () => {
88
88
  };
89
89
  expect(() => new MultiAgentGraph(input)).not.toThrow();
90
90
  });
91
+
92
+ it('rejects grouped fan-in containing a command-routed source', () => {
93
+ const input: t.MultiAgentGraphInput = {
94
+ runId: 'hybrid-fan-in',
95
+ agents: [
96
+ makeAgent('router'),
97
+ makeAgent('worker'),
98
+ makeAgent('result'),
99
+ makeAgent('alternate'),
100
+ ],
101
+ edges: [
102
+ {
103
+ from: ['router', 'worker'],
104
+ to: 'result',
105
+ edgeType: 'direct',
106
+ },
107
+ {
108
+ from: 'router',
109
+ to: 'alternate',
110
+ edgeType: 'handoff',
111
+ },
112
+ ],
113
+ };
114
+
115
+ expect(() => new MultiAgentGraph(input)).toThrow(
116
+ /grouped direct edge.*command-routed source.*router/i
117
+ );
118
+ });
119
+
120
+ it('rejects a prompted direct edge from a command-routed source', () => {
121
+ const input: t.MultiAgentGraphInput = {
122
+ runId: 'hybrid-prompt',
123
+ agents: [
124
+ makeAgent('router'),
125
+ makeAgent('result'),
126
+ makeAgent('alternate'),
127
+ ],
128
+ edges: [
129
+ {
130
+ from: 'router',
131
+ to: 'result',
132
+ edgeType: 'direct',
133
+ prompt: 'Continue through the direct path.',
134
+ },
135
+ {
136
+ from: 'router',
137
+ to: 'alternate',
138
+ edgeType: 'handoff',
139
+ },
140
+ ],
141
+ };
142
+
143
+ expect(() => new MultiAgentGraph(input)).toThrow(
144
+ /prompted direct edge.*command-routed source.*router/i
145
+ );
146
+ });
147
+
148
+ it('rejects a command-routed source sharing a prompted destination group', () => {
149
+ const input: t.MultiAgentGraphInput = {
150
+ runId: 'hybrid-prompt-group',
151
+ agents: [
152
+ makeAgent('router'),
153
+ makeAgent('worker'),
154
+ makeAgent('result'),
155
+ makeAgent('alternate'),
156
+ ],
157
+ edges: [
158
+ { from: 'router', to: 'result', edgeType: 'direct' },
159
+ {
160
+ from: 'worker',
161
+ to: 'result',
162
+ edgeType: 'direct',
163
+ prompt: 'Combine the inputs.',
164
+ },
165
+ {
166
+ from: 'router',
167
+ to: 'alternate',
168
+ edgeType: 'handoff',
169
+ },
170
+ ],
171
+ };
172
+
173
+ expect(() => new MultiAgentGraph(input)).toThrow(
174
+ /prompted direct edge.*command-routed source.*router/i
175
+ );
176
+ });
91
177
  });