@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,521 @@
1
+ import type {
2
+ AgentInputs,
3
+ GraphEdge,
4
+ GraphSubagentConfig,
5
+ SubagentConfig,
6
+ SubagentConfigEntry,
7
+ } from '@/types';
8
+ import {
9
+ buildGraphChildInputs,
10
+ normalizeSubagentConfigEntries,
11
+ normalizeSubagentConfigs,
12
+ resolveSubagentConfigEntries,
13
+ resolveSubagentConfigs,
14
+ validateGraphSubagentConfig,
15
+ } from '@/tools/subagent/childGraphConfig';
16
+ import { AgentContext } from '@/agents/AgentContext';
17
+ import { Providers } from '@/common';
18
+
19
+ const makeAgent = (agentId: string): AgentInputs => ({
20
+ agentId,
21
+ provider: Providers.OPENAI,
22
+ instructions: `You are ${agentId}.`,
23
+ });
24
+
25
+ const makeGraphConfig = (): GraphSubagentConfig => ({
26
+ kind: 'graph',
27
+ type: 'research-team',
28
+ name: 'Research Team',
29
+ description: 'Coordinates parallel research and synthesis.',
30
+ entryAgentId: 'coordinator',
31
+ resultAgentId: 'synthesizer',
32
+ agents: [
33
+ makeAgent('coordinator'),
34
+ makeAgent('left'),
35
+ makeAgent('right'),
36
+ makeAgent('synthesizer'),
37
+ ],
38
+ edges: [
39
+ {
40
+ from: 'coordinator',
41
+ to: ['left', 'right'],
42
+ edgeType: 'direct',
43
+ },
44
+ {
45
+ from: ['left', 'right'],
46
+ to: 'synthesizer',
47
+ edgeType: 'direct',
48
+ },
49
+ ],
50
+ });
51
+
52
+ describe('graph subagent config', () => {
53
+ it('accepts a single-entry, single-result convergent direct DAG', () => {
54
+ const config = makeGraphConfig();
55
+ const parentContext = AgentContext.fromConfig(makeAgent('parent'));
56
+
57
+ expect(() => validateGraphSubagentConfig(config)).not.toThrow();
58
+ expect(resolveSubagentConfigEntries([config], parentContext)).toEqual([
59
+ config,
60
+ ]);
61
+ });
62
+
63
+ it('accepts prompts in a chain and on a converged result transition', () => {
64
+ const chain = makeGraphConfig();
65
+ chain.agents.splice(2, 1);
66
+ chain.edges = [
67
+ {
68
+ from: 'coordinator',
69
+ to: 'left',
70
+ edgeType: 'direct',
71
+ prompt: 'Continue the chain.',
72
+ },
73
+ {
74
+ from: 'left',
75
+ to: 'synthesizer',
76
+ edgeType: 'direct',
77
+ prompt: 'Produce the result.',
78
+ excludeResults: true,
79
+ },
80
+ ];
81
+ const converged = makeGraphConfig();
82
+ converged.edges[1].prompt = 'Synthesize the branch results.';
83
+
84
+ expect(() => validateGraphSubagentConfig(chain)).not.toThrow();
85
+ expect(() => validateGraphSubagentConfig(converged)).not.toThrow();
86
+ });
87
+
88
+ it('preserves legacy configs with host-defined metadata fields', () => {
89
+ interface HostSubagentConfig extends SubagentConfig {
90
+ kind: 'host-defined';
91
+ agents: string[];
92
+ }
93
+ const config: HostSubagentConfig = {
94
+ kind: 'host-defined',
95
+ type: 'legacy-host',
96
+ name: 'Legacy Host',
97
+ description: 'Uses metadata owned by the host.',
98
+ agents: ['host-metadata'],
99
+ agentInputs: makeAgent('legacy-child'),
100
+ };
101
+ const parentContext = AgentContext.fromConfig(makeAgent('parent'));
102
+
103
+ expect(resolveSubagentConfigs([config], parentContext)[0].agentInputs).toBe(
104
+ config.agentInputs
105
+ );
106
+ });
107
+
108
+ it('rejects graph topology fields on the explicit agent variant', () => {
109
+ const config = {
110
+ kind: 'agent',
111
+ type: 'invalid-agent',
112
+ name: 'Invalid Agent',
113
+ description: 'Mixes variants.',
114
+ agentInputs: makeAgent('child'),
115
+ agents: [makeAgent('graph-member')],
116
+ } as SubagentConfigEntry;
117
+ const parentContext = AgentContext.fromConfig(makeAgent('parent'));
118
+
119
+ expect(() => resolveSubagentConfigEntries([config], parentContext)).toThrow(
120
+ /cannot define graph topology fields/
121
+ );
122
+ });
123
+
124
+ it.each([
125
+ [
126
+ 'implicit edge type',
127
+ (config: GraphSubagentConfig) => {
128
+ (config.edges[0] as GraphEdge).edgeType = undefined;
129
+ },
130
+ /edgeType to "direct"/,
131
+ ],
132
+ [
133
+ 'handoff edge',
134
+ (config: GraphSubagentConfig) => {
135
+ (config.edges[0] as GraphEdge).edgeType = 'handoff';
136
+ },
137
+ /edgeType to "direct"/,
138
+ ],
139
+ [
140
+ 'conditional edge',
141
+ (config: GraphSubagentConfig) => {
142
+ (config.edges[0] as GraphEdge).condition = () => true;
143
+ },
144
+ /cannot define a condition/,
145
+ ],
146
+ [
147
+ 'custom prompt key',
148
+ (config: GraphSubagentConfig) => {
149
+ (config.edges[0] as GraphEdge).promptKey = 'custom';
150
+ },
151
+ /cannot define promptKey/,
152
+ ],
153
+ [
154
+ 'invalid prompt',
155
+ (config: GraphSubagentConfig) => {
156
+ Reflect.set(config.edges[0], 'prompt', 123);
157
+ },
158
+ /prompt must be a string or function/,
159
+ ],
160
+ [
161
+ 'invalid excludeResults',
162
+ (config: GraphSubagentConfig) => {
163
+ Reflect.set(config.edges[0], 'excludeResults', 'yes');
164
+ },
165
+ /excludeResults must be a boolean/,
166
+ ],
167
+ [
168
+ 'excludeResults without a prompt',
169
+ (config: GraphSubagentConfig) => {
170
+ config.edges[0].excludeResults = true;
171
+ },
172
+ /cannot define excludeResults without a prompt/,
173
+ ],
174
+ [
175
+ 'a prompted fan-out edge',
176
+ (config: GraphSubagentConfig) => {
177
+ config.edges[0].prompt = 'Branch prompt';
178
+ },
179
+ /prompted edge.*exactly one destination/,
180
+ ],
181
+ [
182
+ 'a prompted branch from a fan-out source',
183
+ (config: GraphSubagentConfig) => {
184
+ config.edges.splice(
185
+ 0,
186
+ 1,
187
+ {
188
+ from: 'coordinator',
189
+ to: 'left',
190
+ edgeType: 'direct',
191
+ prompt: 'Left branch prompt',
192
+ },
193
+ {
194
+ from: 'coordinator',
195
+ to: 'right',
196
+ edgeType: 'direct',
197
+ }
198
+ );
199
+ },
200
+ /prompted edge.*must target resultAgentId "synthesizer"/,
201
+ ],
202
+ [
203
+ 'parallel intermediate prompts after a fan-out',
204
+ (config: GraphSubagentConfig) => {
205
+ config.agents.splice(
206
+ 3,
207
+ 0,
208
+ makeAgent('left-result'),
209
+ makeAgent('right-result')
210
+ );
211
+ config.edges.splice(
212
+ 1,
213
+ 1,
214
+ {
215
+ from: 'left',
216
+ to: 'left-result',
217
+ edgeType: 'direct',
218
+ prompt: 'Left result prompt',
219
+ },
220
+ {
221
+ from: 'right',
222
+ to: 'right-result',
223
+ edgeType: 'direct',
224
+ prompt: 'Right result prompt',
225
+ },
226
+ {
227
+ from: ['left-result', 'right-result'],
228
+ to: 'synthesizer',
229
+ edgeType: 'direct',
230
+ }
231
+ );
232
+ },
233
+ /prompted edge.*must target resultAgentId "synthesizer"/,
234
+ ],
235
+ [
236
+ 'unknown endpoint',
237
+ (config: GraphSubagentConfig) => {
238
+ config.edges[0].to = ['left', 'missing'];
239
+ },
240
+ /unknown agent "missing"/,
241
+ ],
242
+ [
243
+ 'duplicate member ID',
244
+ (config: GraphSubagentConfig) => {
245
+ config.agents.push(makeAgent('left'));
246
+ },
247
+ /duplicate agent ID "left"/,
248
+ ],
249
+ [
250
+ 'nested execution',
251
+ (config: GraphSubagentConfig) => {
252
+ (config as { allowNested?: boolean }).allowNested = true;
253
+ },
254
+ /cannot enable allowNested/,
255
+ ],
256
+ [
257
+ 'more than 32 members',
258
+ (config: GraphSubagentConfig) => {
259
+ config.agents = Array.from({ length: 33 }, (_, index) =>
260
+ makeAgent(`member-${index}`)
261
+ );
262
+ },
263
+ /cannot exceed 32 agents/,
264
+ ],
265
+ [
266
+ 'ambiguous fan-in',
267
+ (config: GraphSubagentConfig) => {
268
+ config.edges[1] = {
269
+ from: 'left',
270
+ to: 'synthesizer',
271
+ edgeType: 'direct',
272
+ };
273
+ config.edges.push({
274
+ from: 'right',
275
+ to: 'synthesizer',
276
+ edgeType: 'direct',
277
+ });
278
+ },
279
+ /must use one edge with an array-valued from field/,
280
+ ],
281
+ [
282
+ 'second root',
283
+ (config: GraphSubagentConfig) => {
284
+ config.edges[0].to = 'left';
285
+ config.edges[1].from = 'left';
286
+ },
287
+ /only root/,
288
+ ],
289
+ [
290
+ 'second sink',
291
+ (config: GraphSubagentConfig) => {
292
+ config.edges[1].from = 'left';
293
+ },
294
+ /only sink/,
295
+ ],
296
+ ])('rejects %s', (_name, mutate, expected) => {
297
+ const config = makeGraphConfig();
298
+ mutate(config);
299
+ expect(() => validateGraphSubagentConfig(config)).toThrow(expected);
300
+ });
301
+
302
+ it.each([
303
+ '__start__',
304
+ '__end__',
305
+ 'messages',
306
+ 'agentMessages',
307
+ 'subagentResult',
308
+ 'member|branch',
309
+ 'member:branch',
310
+ ])('rejects graph-runtime member ID %s', (agentId) => {
311
+ const config = makeGraphConfig();
312
+ config.agents[0].agentId = agentId;
313
+
314
+ expect(() => validateGraphSubagentConfig(config)).toThrow(
315
+ /is reserved by the graph runtime/
316
+ );
317
+ });
318
+
319
+ it.each([
320
+ [null, /must be an object/],
321
+ [{ ...makeGraphConfig(), agents: undefined }, /agents must be an array/],
322
+ [{ ...makeGraphConfig(), edges: [null] }, /edges must contain objects/],
323
+ [{ ...makeGraphConfig(), maxTurns: 0 }, /positive safe integer/],
324
+ [
325
+ { ...makeGraphConfig(), maxTurns: Number.MAX_SAFE_INTEGER },
326
+ /exceeds the safe graph recursion budget/,
327
+ ],
328
+ [
329
+ { ...makeGraphConfig(), agentInputs: makeAgent('legacy') },
330
+ /cannot define agentInputs or self/,
331
+ ],
332
+ ])('rejects malformed runtime graph config %#', (config, expected) => {
333
+ expect(() => validateGraphSubagentConfig(config)).toThrow(expected);
334
+ });
335
+
336
+ it('rejects cycles even when entry and result remain unique', () => {
337
+ const config: GraphSubagentConfig = {
338
+ ...makeGraphConfig(),
339
+ agents: [
340
+ makeAgent('entry'),
341
+ makeAgent('a'),
342
+ makeAgent('b'),
343
+ makeAgent('result'),
344
+ ],
345
+ entryAgentId: 'entry',
346
+ resultAgentId: 'result',
347
+ edges: [
348
+ { from: ['entry', 'b'], to: 'a', edgeType: 'direct' },
349
+ { from: 'a', to: 'b', edgeType: 'direct' },
350
+ { from: 'b', to: 'result', edgeType: 'direct' },
351
+ ],
352
+ };
353
+
354
+ expect(() => validateGraphSubagentConfig(config)).toThrow(/acyclic/);
355
+ });
356
+
357
+ it('prepares every member once without mutating host inputs', () => {
358
+ const graphTool = { name: 'direct_tool' } as NonNullable<
359
+ AgentInputs['graphTools']
360
+ >[number];
361
+ const nestedConfig = {
362
+ type: 'nested',
363
+ name: 'Nested',
364
+ description: 'Nested worker',
365
+ agentInputs: makeAgent('nested'),
366
+ };
367
+ const sourceAgent: AgentInputs = {
368
+ ...makeAgent('coordinator'),
369
+ toolDefinitions: [{ name: 'event_tool' }],
370
+ graphTools: [graphTool],
371
+ initialSummary: { text: 'old summary', tokenCount: 2 },
372
+ discoveredTools: ['event_tool'],
373
+ subagentConfigs: [nestedConfig],
374
+ maxSubagentDepth: 9,
375
+ };
376
+ const config: GraphSubagentConfig = {
377
+ ...makeGraphConfig(),
378
+ agents: [sourceAgent],
379
+ edges: [],
380
+ entryAgentId: 'coordinator',
381
+ resultAgentId: 'coordinator',
382
+ };
383
+
384
+ const prepared = buildGraphChildInputs(config, 3, true);
385
+
386
+ expect(prepared).toHaveLength(1);
387
+ expect(prepared[0]).toMatchObject({
388
+ agentId: 'coordinator',
389
+ toolDefinitions: [{ name: 'event_tool' }],
390
+ graphTools: [graphTool],
391
+ });
392
+ expect(prepared[0].subagentConfigs).toBeUndefined();
393
+ expect(prepared[0].maxSubagentDepth).toBeUndefined();
394
+ expect(prepared[0].initialSummary).toBeUndefined();
395
+ expect(prepared[0].discoveredTools).toBeUndefined();
396
+ expect(sourceAgent.initialSummary).toEqual({
397
+ text: 'old summary',
398
+ tokenCount: 2,
399
+ });
400
+ expect(sourceAgent.maxSubagentDepth).toBe(9);
401
+ });
402
+ });
403
+
404
+ describe('lazy and graph subagent normalization', () => {
405
+ const makeParentContext = (): AgentContext =>
406
+ AgentContext.fromConfig(makeAgent('parent'));
407
+
408
+ it('retains a versioned lazy descriptor without invoking its resolver', () => {
409
+ const resolveAgentInputs = jest.fn(async () => makeAgent('lazy-child'));
410
+ const lazyConfig: SubagentConfig = {
411
+ type: 'lazy-worker',
412
+ name: 'Lazy Worker',
413
+ description: 'Loads only after selection.',
414
+ configId: 'lazy-worker@v1',
415
+ resolveAgentInputs,
416
+ };
417
+
418
+ expect(normalizeSubagentConfigs([lazyConfig], makeParentContext())).toEqual(
419
+ [lazyConfig]
420
+ );
421
+ expect(resolveAgentInputs).not.toHaveBeenCalled();
422
+ });
423
+
424
+ it.each(['', ' '])(
425
+ 'rejects lazy descriptors with empty configId %#',
426
+ (configId) => {
427
+ const lazyConfig: SubagentConfig = {
428
+ type: 'unversioned',
429
+ name: 'Unversioned Worker',
430
+ description: 'Has no durable configuration identity.',
431
+ configId,
432
+ resolveAgentInputs: async () => makeAgent('lazy-child'),
433
+ };
434
+
435
+ expect(() =>
436
+ normalizeSubagentConfigs([lazyConfig], makeParentContext())
437
+ ).toThrow('requires a non-empty "configId"');
438
+ }
439
+ );
440
+
441
+ it('normalizes mixed graph and lazy entries without resolving the lazy entry', () => {
442
+ const graphConfig = makeGraphConfig();
443
+ const resolveAgentInputs = jest.fn(async () => makeAgent('lazy-child'));
444
+ const lazyConfig: SubagentConfig = {
445
+ type: 'lazy-worker',
446
+ name: 'Lazy Worker',
447
+ description: 'Loads only after selection.',
448
+ configId: 'lazy-worker@v1',
449
+ resolveAgentInputs,
450
+ };
451
+
452
+ expect(
453
+ normalizeSubagentConfigEntries(
454
+ [graphConfig, lazyConfig],
455
+ makeParentContext()
456
+ )
457
+ ).toEqual([graphConfig, lazyConfig]);
458
+ expect(resolveAgentInputs).not.toHaveBeenCalled();
459
+ });
460
+
461
+ it('rejects duplicate types across graph and lazy variants', () => {
462
+ const graphConfig = makeGraphConfig();
463
+ const lazyConfig: SubagentConfig = {
464
+ type: graphConfig.type,
465
+ name: 'Conflicting Worker',
466
+ description: 'Conflicts with a graph descriptor.',
467
+ configId: 'conflicting-worker@v1',
468
+ resolveAgentInputs: async () => makeAgent('lazy-child'),
469
+ };
470
+
471
+ expect(() =>
472
+ normalizeSubagentConfigEntries(
473
+ [graphConfig, lazyConfig],
474
+ makeParentContext()
475
+ )
476
+ ).toThrow(`Duplicate subagent type "${graphConfig.type}"`);
477
+ });
478
+
479
+ it.each([
480
+ ['configId', 'graph@v1'],
481
+ ['resolveAgentInputs', async () => makeAgent('unexpected')],
482
+ ] as const)('rejects %s on graph descriptors', (field, value) => {
483
+ const graphConfig = makeGraphConfig();
484
+ Reflect.set(graphConfig, field, value);
485
+
486
+ expect(() =>
487
+ normalizeSubagentConfigEntries([graphConfig], makeParentContext())
488
+ ).toThrow(/lazy fields configId\/resolveAgentInputs/);
489
+ });
490
+
491
+ it('rejects unresolved self configs that also declare a resolver', () => {
492
+ const invalidConfig: SubagentConfig = {
493
+ type: 'self-or-lazy',
494
+ name: 'Ambiguous Worker',
495
+ description: 'Cannot choose between self and lazy resolution.',
496
+ self: true,
497
+ configId: 'self-or-lazy@v1',
498
+ resolveAgentInputs: async () => makeAgent('unexpected'),
499
+ };
500
+
501
+ expect(() =>
502
+ normalizeSubagentConfigs([invalidConfig], makeParentContext())
503
+ ).toThrow(/cannot combine self with resolveAgentInputs/);
504
+ });
505
+
506
+ it('rejects self configs with both eager inputs and a resolver', () => {
507
+ const invalidConfig: SubagentConfig = {
508
+ type: 'eager-self-or-lazy',
509
+ name: 'Ambiguous Eager Worker',
510
+ description: 'Cannot combine self shaping with lazy resolution.',
511
+ self: true,
512
+ agentInputs: makeAgent('eager-child'),
513
+ configId: 'eager-self-or-lazy@v1',
514
+ resolveAgentInputs: async () => makeAgent('unexpected'),
515
+ };
516
+
517
+ expect(() =>
518
+ normalizeSubagentConfigs([invalidConfig], makeParentContext())
519
+ ).toThrow(/cannot combine self with resolveAgentInputs/);
520
+ });
521
+ });