@librechat/agents 3.1.67 → 3.1.68-dev.1

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 (185) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +23 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +16 -1
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +91 -0
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +36 -0
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/hooks/HookRegistry.cjs +162 -0
  10. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
  11. package/dist/cjs/hooks/executeHooks.cjs +276 -0
  12. package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
  13. package/dist/cjs/hooks/matchers.cjs +256 -0
  14. package/dist/cjs/hooks/matchers.cjs.map +1 -0
  15. package/dist/cjs/hooks/types.cjs +27 -0
  16. package/dist/cjs/hooks/types.cjs.map +1 -0
  17. package/dist/cjs/main.cjs +54 -0
  18. package/dist/cjs/main.cjs.map +1 -1
  19. package/dist/cjs/messages/format.cjs +74 -12
  20. package/dist/cjs/messages/format.cjs.map +1 -1
  21. package/dist/cjs/run.cjs +111 -0
  22. package/dist/cjs/run.cjs.map +1 -1
  23. package/dist/cjs/summarization/index.cjs +41 -0
  24. package/dist/cjs/summarization/index.cjs.map +1 -1
  25. package/dist/cjs/summarization/node.cjs +165 -19
  26. package/dist/cjs/summarization/node.cjs.map +1 -1
  27. package/dist/cjs/tools/BashExecutor.cjs +165 -0
  28. package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
  29. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +287 -0
  30. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
  31. package/dist/cjs/tools/CodeExecutor.cjs +0 -9
  32. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  33. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +7 -23
  34. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  35. package/dist/cjs/tools/ReadFile.cjs +43 -0
  36. package/dist/cjs/tools/ReadFile.cjs.map +1 -0
  37. package/dist/cjs/tools/SkillTool.cjs +50 -0
  38. package/dist/cjs/tools/SkillTool.cjs.map +1 -0
  39. package/dist/cjs/tools/SubagentTool.cjs +92 -0
  40. package/dist/cjs/tools/SubagentTool.cjs.map +1 -0
  41. package/dist/cjs/tools/ToolNode.cjs +304 -140
  42. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  43. package/dist/cjs/tools/ToolSearch.cjs +2 -13
  44. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  45. package/dist/cjs/tools/skillCatalog.cjs +84 -0
  46. package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
  47. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +511 -0
  48. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -0
  49. package/dist/esm/agents/AgentContext.mjs +23 -3
  50. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  51. package/dist/esm/common/enum.mjs +15 -2
  52. package/dist/esm/common/enum.mjs.map +1 -1
  53. package/dist/esm/graphs/Graph.mjs +91 -0
  54. package/dist/esm/graphs/Graph.mjs.map +1 -1
  55. package/dist/esm/graphs/MultiAgentGraph.mjs +36 -0
  56. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  57. package/dist/esm/hooks/HookRegistry.mjs +160 -0
  58. package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
  59. package/dist/esm/hooks/executeHooks.mjs +273 -0
  60. package/dist/esm/hooks/executeHooks.mjs.map +1 -0
  61. package/dist/esm/hooks/matchers.mjs +251 -0
  62. package/dist/esm/hooks/matchers.mjs.map +1 -0
  63. package/dist/esm/hooks/types.mjs +25 -0
  64. package/dist/esm/hooks/types.mjs.map +1 -0
  65. package/dist/esm/main.mjs +13 -2
  66. package/dist/esm/main.mjs.map +1 -1
  67. package/dist/esm/messages/format.mjs +66 -4
  68. package/dist/esm/messages/format.mjs.map +1 -1
  69. package/dist/esm/run.mjs +111 -0
  70. package/dist/esm/run.mjs.map +1 -1
  71. package/dist/esm/summarization/index.mjs +41 -1
  72. package/dist/esm/summarization/index.mjs.map +1 -1
  73. package/dist/esm/summarization/node.mjs +165 -19
  74. package/dist/esm/summarization/node.mjs.map +1 -1
  75. package/dist/esm/tools/BashExecutor.mjs +159 -0
  76. package/dist/esm/tools/BashExecutor.mjs.map +1 -0
  77. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +278 -0
  78. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
  79. package/dist/esm/tools/CodeExecutor.mjs +0 -9
  80. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  81. package/dist/esm/tools/ProgrammaticToolCalling.mjs +8 -24
  82. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  83. package/dist/esm/tools/ReadFile.mjs +38 -0
  84. package/dist/esm/tools/ReadFile.mjs.map +1 -0
  85. package/dist/esm/tools/SkillTool.mjs +45 -0
  86. package/dist/esm/tools/SkillTool.mjs.map +1 -0
  87. package/dist/esm/tools/SubagentTool.mjs +85 -0
  88. package/dist/esm/tools/SubagentTool.mjs.map +1 -0
  89. package/dist/esm/tools/ToolNode.mjs +306 -142
  90. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  91. package/dist/esm/tools/ToolSearch.mjs +3 -14
  92. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  93. package/dist/esm/tools/skillCatalog.mjs +82 -0
  94. package/dist/esm/tools/skillCatalog.mjs.map +1 -0
  95. package/dist/esm/tools/subagent/SubagentExecutor.mjs +505 -0
  96. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -0
  97. package/dist/types/agents/AgentContext.d.ts +6 -0
  98. package/dist/types/common/enum.d.ts +10 -2
  99. package/dist/types/graphs/Graph.d.ts +2 -0
  100. package/dist/types/graphs/MultiAgentGraph.d.ts +12 -0
  101. package/dist/types/hooks/HookRegistry.d.ts +56 -0
  102. package/dist/types/hooks/executeHooks.d.ts +79 -0
  103. package/dist/types/hooks/index.d.ts +6 -0
  104. package/dist/types/hooks/matchers.d.ts +95 -0
  105. package/dist/types/hooks/types.d.ts +320 -0
  106. package/dist/types/index.d.ts +8 -0
  107. package/dist/types/messages/format.d.ts +2 -1
  108. package/dist/types/run.d.ts +1 -0
  109. package/dist/types/summarization/index.d.ts +2 -0
  110. package/dist/types/summarization/node.d.ts +2 -0
  111. package/dist/types/tools/BashExecutor.d.ts +45 -0
  112. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
  113. package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -9
  114. package/dist/types/tools/ReadFile.d.ts +28 -0
  115. package/dist/types/tools/SkillTool.d.ts +40 -0
  116. package/dist/types/tools/SubagentTool.d.ts +36 -0
  117. package/dist/types/tools/ToolNode.d.ts +24 -2
  118. package/dist/types/tools/ToolSearch.d.ts +2 -2
  119. package/dist/types/tools/skillCatalog.d.ts +19 -0
  120. package/dist/types/tools/subagent/SubagentExecutor.d.ts +137 -0
  121. package/dist/types/tools/subagent/index.d.ts +2 -0
  122. package/dist/types/types/graph.d.ts +61 -2
  123. package/dist/types/types/index.d.ts +1 -0
  124. package/dist/types/types/run.d.ts +20 -0
  125. package/dist/types/types/skill.d.ts +9 -0
  126. package/dist/types/types/tools.d.ts +38 -10
  127. package/package.json +5 -1
  128. package/src/agents/AgentContext.ts +26 -2
  129. package/src/common/enum.ts +15 -1
  130. package/src/graphs/Graph.ts +113 -0
  131. package/src/graphs/MultiAgentGraph.ts +39 -0
  132. package/src/graphs/__tests__/MultiAgentGraph.test.ts +91 -0
  133. package/src/hooks/HookRegistry.ts +208 -0
  134. package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
  135. package/src/hooks/__tests__/compactHooks.test.ts +214 -0
  136. package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
  137. package/src/hooks/__tests__/integration.test.ts +337 -0
  138. package/src/hooks/__tests__/matchers.test.ts +238 -0
  139. package/src/hooks/__tests__/toolHooks.test.ts +669 -0
  140. package/src/hooks/executeHooks.ts +375 -0
  141. package/src/hooks/index.ts +57 -0
  142. package/src/hooks/matchers.ts +280 -0
  143. package/src/hooks/types.ts +404 -0
  144. package/src/index.ts +10 -0
  145. package/src/messages/format.ts +74 -4
  146. package/src/messages/formatAgentMessages.skills.test.ts +334 -0
  147. package/src/run.ts +126 -0
  148. package/src/scripts/multi-agent-subagent.ts +246 -0
  149. package/src/scripts/programmatic_exec.ts +1 -10
  150. package/src/scripts/subagent-event-driven-debug.ts +190 -0
  151. package/src/scripts/subagent-tools-debug.ts +160 -0
  152. package/src/scripts/test_code_api.ts +0 -7
  153. package/src/scripts/tool_search.ts +1 -10
  154. package/src/specs/subagent.test.ts +305 -0
  155. package/src/summarization/__tests__/node.test.ts +42 -0
  156. package/src/summarization/__tests__/trigger.test.ts +100 -1
  157. package/src/summarization/index.ts +47 -0
  158. package/src/summarization/node.ts +202 -24
  159. package/src/tools/BashExecutor.ts +193 -0
  160. package/src/tools/BashProgrammaticToolCalling.ts +381 -0
  161. package/src/tools/CodeExecutor.ts +0 -11
  162. package/src/tools/ProgrammaticToolCalling.ts +4 -29
  163. package/src/tools/ReadFile.ts +39 -0
  164. package/src/tools/SkillTool.ts +46 -0
  165. package/src/tools/SubagentTool.ts +100 -0
  166. package/src/tools/ToolNode.ts +391 -169
  167. package/src/tools/ToolSearch.ts +3 -19
  168. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +7 -8
  169. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +0 -1
  170. package/src/tools/__tests__/ReadFile.test.ts +44 -0
  171. package/src/tools/__tests__/SkillTool.test.ts +442 -0
  172. package/src/tools/__tests__/SubagentExecutor.test.ts +1148 -0
  173. package/src/tools/__tests__/SubagentTool.test.ts +149 -0
  174. package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
  175. package/src/tools/__tests__/ToolSearch.integration.test.ts +7 -8
  176. package/src/tools/__tests__/skillCatalog.test.ts +161 -0
  177. package/src/tools/__tests__/subagentHooks.test.ts +215 -0
  178. package/src/tools/skillCatalog.ts +126 -0
  179. package/src/tools/subagent/SubagentExecutor.ts +676 -0
  180. package/src/tools/subagent/index.ts +13 -0
  181. package/src/types/graph.ts +80 -1
  182. package/src/types/index.ts +1 -0
  183. package/src/types/run.ts +20 -0
  184. package/src/types/skill.ts +11 -0
  185. package/src/types/tools.ts +41 -10
@@ -0,0 +1,676 @@
1
+ import { nanoid } from 'nanoid';
2
+ import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
3
+ import { HumanMessage } from '@langchain/core/messages';
4
+ import type { BaseMessage } from '@langchain/core/messages';
5
+ import type { Callbacks } from '@langchain/core/callbacks/manager';
6
+ import type {
7
+ AgentInputs,
8
+ StandardGraphInput,
9
+ ResolvedSubagentConfig,
10
+ SubagentConfig,
11
+ SubagentUpdateEvent,
12
+ SubagentUpdatePhase,
13
+ ToolExecuteBatchRequest,
14
+ TokenCounter,
15
+ } from '@/types';
16
+ import type { AggregatedHookResult, HookRegistry } from '@/hooks';
17
+ import type { AgentContext } from '@/agents/AgentContext';
18
+ import type { StandardGraph } from '@/graphs/Graph';
19
+ import { GraphEvents, Callback } from '@/common';
20
+ import type { HandlerRegistry } from '@/events';
21
+ import { executeHooks } from '@/hooks';
22
+
23
+ const DEFAULT_MAX_TURNS = 25;
24
+ const RECURSION_MULTIPLIER = 3;
25
+ const ERROR_MESSAGE_MAX_CHARS = 200;
26
+
27
+ const HOOK_FALLBACK: AggregatedHookResult = Object.freeze({
28
+ additionalContexts: [] as string[],
29
+ errors: [] as string[],
30
+ });
31
+
32
+ export type SubagentExecuteParams = {
33
+ description: string;
34
+ subagentType: string;
35
+ threadId?: string;
36
+ /**
37
+ * Parent-side `tool_call_id` of the `subagent` tool invocation that
38
+ * triggered this execution. Surfaced on {@link SubagentUpdateEvent} so
39
+ * hosts can correlate child updates back to the originating tool call
40
+ * without relying on event ordering heuristics.
41
+ */
42
+ parentToolCallId?: string;
43
+ };
44
+
45
+ export type SubagentExecuteResult = {
46
+ content: string;
47
+ messages: BaseMessage[];
48
+ };
49
+
50
+ /**
51
+ * Factory that constructs a child graph for subagent execution. Injected
52
+ * rather than imported so that `SubagentExecutor` does not have a runtime
53
+ * dependency on `StandardGraph` — this avoids a circular dependency between
54
+ * `src/graphs/Graph.ts` and `src/tools/subagent/` that would otherwise break
55
+ * Rollup's chunking under `preserveModules`.
56
+ */
57
+ export type ChildGraphFactory = (input: StandardGraphInput) => StandardGraph;
58
+
59
+ export type SubagentExecutorOptions = {
60
+ configs: Map<string, ResolvedSubagentConfig>;
61
+ parentSignal?: AbortSignal;
62
+ hookRegistry?: HookRegistry;
63
+ parentRunId: string;
64
+ parentAgentId?: string;
65
+ tokenCounter?: TokenCounter;
66
+ /** Remaining nesting budget. 0 or negative blocks execution. */
67
+ maxDepth?: number;
68
+ /**
69
+ * Factory for constructing the isolated child graph. Callers pass
70
+ * `(input) => new StandardGraph(input)` — injected to break a circular
71
+ * module dependency.
72
+ */
73
+ createChildGraph: ChildGraphFactory;
74
+ /**
75
+ * Parent's event handler registry. When provided, child-graph events are
76
+ * forwarded through this registry so hosts can:
77
+ * (a) execute event-driven tools (`ON_TOOL_EXECUTE` routed to parent's handler),
78
+ * (b) surface child activity to a UI via wrapped {@link GraphEvents.ON_SUBAGENT_UPDATE}.
79
+ * When omitted, the child runs fully isolated (legacy behavior).
80
+ *
81
+ * Can be a direct `HandlerRegistry` or a zero-arg getter — use the getter
82
+ * form when the registry is assigned to the graph AFTER the executor is
83
+ * constructed (the current `Run.create` flow sets `handlerRegistry`
84
+ * post-`createWorkflow`, so `createAgentNode` must capture lazily).
85
+ */
86
+ parentHandlerRegistry?: HandlerRegistry | (() => HandlerRegistry | undefined);
87
+ };
88
+
89
+ export class SubagentExecutor {
90
+ private readonly configs: Map<string, ResolvedSubagentConfig>;
91
+ private readonly parentSignal?: AbortSignal;
92
+ private readonly hookRegistry?: HookRegistry;
93
+ private readonly parentRunId: string;
94
+ private readonly parentAgentId?: string;
95
+ private readonly tokenCounter?: TokenCounter;
96
+ private readonly maxDepth: number;
97
+ private readonly createChildGraph: ChildGraphFactory;
98
+ private readonly resolveParentHandlerRegistry?: () =>
99
+ | HandlerRegistry
100
+ | undefined;
101
+
102
+ constructor(options: SubagentExecutorOptions) {
103
+ this.configs = options.configs;
104
+ this.parentSignal = options.parentSignal;
105
+ this.hookRegistry = options.hookRegistry;
106
+ this.parentRunId = options.parentRunId;
107
+ this.parentAgentId = options.parentAgentId;
108
+ this.tokenCounter = options.tokenCounter;
109
+ this.maxDepth = options.maxDepth ?? 1;
110
+ this.createChildGraph = options.createChildGraph;
111
+ const rawRegistry = options.parentHandlerRegistry;
112
+ if (typeof rawRegistry === 'function') {
113
+ this.resolveParentHandlerRegistry = rawRegistry;
114
+ } else if (rawRegistry != null) {
115
+ this.resolveParentHandlerRegistry = (): HandlerRegistry => rawRegistry;
116
+ }
117
+ }
118
+
119
+ /** Snapshot of the parent's registry at the moment a subagent is dispatched. */
120
+ private getParentHandlerRegistry(): HandlerRegistry | undefined {
121
+ return this.resolveParentHandlerRegistry?.();
122
+ }
123
+
124
+ async execute(params: SubagentExecuteParams): Promise<SubagentExecuteResult> {
125
+ const { description, subagentType, threadId, parentToolCallId } = params;
126
+ const config = this.configs.get(subagentType);
127
+
128
+ if (!config) {
129
+ const available = [...this.configs.keys()].join(', ');
130
+ return {
131
+ content: `Error: Unknown subagent type "${subagentType}". Available types: ${available}`,
132
+ messages: [],
133
+ };
134
+ }
135
+
136
+ if (this.maxDepth <= 0) {
137
+ return {
138
+ content: 'Error: Maximum subagent nesting depth exceeded.',
139
+ messages: [],
140
+ };
141
+ }
142
+
143
+ const childAgentId =
144
+ config.agentInputs.agentId ||
145
+ `${this.parentAgentId ?? 'agent'}_sub_${nanoid(8)}`;
146
+
147
+ if (
148
+ this.hookRegistry?.hasHookFor('SubagentStart', this.parentRunId) === true
149
+ ) {
150
+ const hookResult = await executeHooks({
151
+ registry: this.hookRegistry,
152
+ input: {
153
+ hook_event_name: 'SubagentStart',
154
+ runId: this.parentRunId,
155
+ threadId,
156
+ parentAgentId: this.parentAgentId,
157
+ agentId: childAgentId,
158
+ agentType: subagentType,
159
+ inputs: [new HumanMessage(description)],
160
+ },
161
+ sessionId: this.parentRunId,
162
+ matchQuery: subagentType,
163
+ }).catch((): AggregatedHookResult => HOOK_FALLBACK);
164
+
165
+ /**
166
+ * `ask` is treated identically to `deny` in the subagent context:
167
+ * subagents are non-interactive, so there is no prompt path for `ask`.
168
+ * Both decisions block execution and return a "Blocked" tool result.
169
+ */
170
+ if (hookResult.decision === 'deny' || hookResult.decision === 'ask') {
171
+ return {
172
+ content: `Blocked: ${hookResult.reason ?? 'Blocked by hook'}`,
173
+ messages: [],
174
+ };
175
+ }
176
+ }
177
+
178
+ const parentRegistry = this.getParentHandlerRegistry();
179
+ const forwardingEnabled = parentRegistry != null;
180
+ /**
181
+ * Keep `toolDefinitions` only when the host has actually wired an
182
+ * `ON_TOOL_EXECUTE` handler. `Run` always constructs a `HandlerRegistry`,
183
+ * so treating any registry as "forwarding enabled" would leak
184
+ * `toolDefinitions` into children whose hosts cannot execute them — the
185
+ * child's `ToolNode` batch promise would hang forever with no handler to
186
+ * resolve/reject. Gating on the tool-execute handler preserves the
187
+ * recoverable "no tools" path for registry-but-no-handler configs.
188
+ */
189
+ const hasToolExecuteHandler =
190
+ parentRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) != null;
191
+ const childInputs = buildChildInputs(
192
+ config,
193
+ childAgentId,
194
+ this.maxDepth,
195
+ /* keepToolDefinitions */ hasToolExecuteHandler
196
+ );
197
+ const childRunId = `${this.parentRunId}_sub_${nanoid(8)}`;
198
+ const maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
199
+
200
+ const childGraph = this.createChildGraph({
201
+ runId: childRunId,
202
+ signal: this.parentSignal,
203
+ agents: [childInputs],
204
+ tokenCounter: this.tokenCounter,
205
+ });
206
+
207
+ const forwarder = forwardingEnabled
208
+ ? this.createForwarderCallback({
209
+ parentRegistry: parentRegistry!,
210
+ subagentType,
211
+ subagentAgentId: childAgentId,
212
+ childRunId,
213
+ parentToolCallId,
214
+ })
215
+ : undefined;
216
+
217
+ if (forwarder) {
218
+ await this.emitSubagentUpdate(parentRegistry!, {
219
+ childRunId,
220
+ subagentType,
221
+ subagentAgentId: childAgentId,
222
+ parentToolCallId,
223
+ phase: 'start',
224
+ label: `Subagent "${subagentType}" started`,
225
+ });
226
+ }
227
+
228
+ let result: { messages: BaseMessage[] };
229
+ try {
230
+ const workflow = childGraph.createWorkflow();
231
+ /**
232
+ * When `parentHandlerRegistry` is provided (forwarding mode), attach a
233
+ * lightweight callback that intercepts the child's `on_custom_event`
234
+ * dispatches and routes them to the parent's registry — either as
235
+ * operational events (ON_TOOL_EXECUTE) or wrapped ON_SUBAGENT_UPDATE
236
+ * envelopes. Native LangChain streaming events (on_chat_model_stream,
237
+ * etc.) still do NOT propagate to the parent's outer streamEvents
238
+ * iterator — the `callbacks` array REPLACES the inherited chain, so
239
+ * parent handlers won't receive child stream chunks and raise "No
240
+ * agent context found" lookups on the parent's agentContexts map.
241
+ *
242
+ * When no registry is provided (legacy isolation), `callbacks: []`
243
+ * fully detaches the child.
244
+ *
245
+ * `runName` gives the child a distinct LangSmith trace root (avoids
246
+ * nested trace pollution).
247
+ */
248
+ const callbacks: Callbacks = forwarder ? [forwarder] : [];
249
+ result = await workflow.invoke(
250
+ { messages: [new HumanMessage(description)] },
251
+ {
252
+ recursionLimit: maxTurns * RECURSION_MULTIPLIER,
253
+ signal: this.parentSignal,
254
+ callbacks,
255
+ runName: `subagent:${subagentType}`,
256
+ configurable: {
257
+ thread_id: childRunId,
258
+ },
259
+ }
260
+ );
261
+ } catch (error) {
262
+ const errorMessage = truncateErrorMessage(error);
263
+ if (forwarder) {
264
+ await this.emitSubagentUpdate(parentRegistry!, {
265
+ childRunId,
266
+ subagentType,
267
+ subagentAgentId: childAgentId,
268
+ parentToolCallId,
269
+ phase: 'error',
270
+ label: `Subagent "${subagentType}" errored: ${errorMessage}`,
271
+ data: { message: errorMessage },
272
+ });
273
+ }
274
+ childGraph.clearHeavyState();
275
+ return {
276
+ content: `Subagent error: ${errorMessage}`,
277
+ messages: [],
278
+ };
279
+ }
280
+
281
+ const filteredContent = filterSubagentResult(result.messages);
282
+
283
+ if (
284
+ this.hookRegistry?.hasHookFor('SubagentStop', this.parentRunId) === true
285
+ ) {
286
+ /**
287
+ * Awaited (not fire-and-forget) for deterministic test synchronization
288
+ * and consistency with PostCompact. The parent is already waiting on the
289
+ * tool result, so the small extra latency is acceptable. Errors are
290
+ * swallowed — SubagentStop is observational.
291
+ */
292
+ await executeHooks({
293
+ registry: this.hookRegistry,
294
+ input: {
295
+ hook_event_name: 'SubagentStop',
296
+ runId: this.parentRunId,
297
+ threadId,
298
+ agentId: childAgentId,
299
+ agentType: subagentType,
300
+ messages: result.messages,
301
+ },
302
+ sessionId: this.parentRunId,
303
+ matchQuery: subagentType,
304
+ }).catch(() => {
305
+ /* SubagentStop is observational — swallow errors */
306
+ });
307
+ }
308
+
309
+ if (forwarder) {
310
+ await this.emitSubagentUpdate(parentRegistry!, {
311
+ childRunId,
312
+ subagentType,
313
+ subagentAgentId: childAgentId,
314
+ parentToolCallId,
315
+ phase: 'stop',
316
+ label: `Subagent "${subagentType}" finished`,
317
+ });
318
+ }
319
+
320
+ childGraph.clearHeavyState();
321
+
322
+ return { content: filteredContent, messages: result.messages };
323
+ }
324
+
325
+ /**
326
+ * Emits a single {@link GraphEvents.ON_SUBAGENT_UPDATE} envelope through the
327
+ * parent's handler registry. Silent no-op when no parent registry is set.
328
+ * Errors are swallowed — update events are observational.
329
+ */
330
+ private async emitSubagentUpdate(
331
+ parentRegistry: HandlerRegistry,
332
+ args: {
333
+ childRunId: string;
334
+ subagentType: string;
335
+ subagentAgentId: string;
336
+ parentToolCallId?: string;
337
+ phase: SubagentUpdatePhase;
338
+ data?: unknown;
339
+ label?: string;
340
+ }
341
+ ): Promise<void> {
342
+ const handler = parentRegistry.getHandler(GraphEvents.ON_SUBAGENT_UPDATE);
343
+ if (!handler) {
344
+ return;
345
+ }
346
+ const event: SubagentUpdateEvent = {
347
+ runId: this.parentRunId,
348
+ subagentRunId: args.childRunId,
349
+ subagentType: args.subagentType,
350
+ subagentAgentId: args.subagentAgentId,
351
+ parentAgentId: this.parentAgentId,
352
+ parentToolCallId: args.parentToolCallId,
353
+ phase: args.phase,
354
+ data: args.data,
355
+ label: args.label,
356
+ timestamp: new Date().toISOString(),
357
+ };
358
+ try {
359
+ await handler.handle(GraphEvents.ON_SUBAGENT_UPDATE, event);
360
+ } catch {
361
+ /* observational — swallow */
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Builds a BaseCallbackHandler that intercepts the child graph's custom
367
+ * events. Routing rules:
368
+ * - `ON_TOOL_EXECUTE` → forwarded as-is to the parent's ON_TOOL_EXECUTE
369
+ * handler (so event-driven tools work identically for child and parent).
370
+ * - `ON_RUN_STEP` / `ON_RUN_STEP_DELTA` / `ON_RUN_STEP_COMPLETED` /
371
+ * `ON_MESSAGE_DELTA` / `ON_REASONING_DELTA` → wrapped in a
372
+ * {@link GraphEvents.ON_SUBAGENT_UPDATE} envelope with a human-readable
373
+ * label, delivered to the parent's subagent-update handler.
374
+ * - Everything else → ignored (keeps parent's UI scoped to the events it
375
+ * cares about; host apps can extend by registering more phases).
376
+ */
377
+ private createForwarderCallback(args: {
378
+ parentRegistry: HandlerRegistry;
379
+ subagentType: string;
380
+ subagentAgentId: string;
381
+ childRunId: string;
382
+ parentToolCallId?: string;
383
+ }): BaseCallbackHandler {
384
+ const {
385
+ parentRegistry,
386
+ subagentType,
387
+ subagentAgentId,
388
+ childRunId,
389
+ parentToolCallId,
390
+ } = args;
391
+ const parentRunId = this.parentRunId;
392
+ const parentAgentId = this.parentAgentId;
393
+
394
+ const wrap = async (
395
+ eventName: string,
396
+ phase: SubagentUpdatePhase,
397
+ data: unknown
398
+ ): Promise<void> => {
399
+ const handler = parentRegistry.getHandler(GraphEvents.ON_SUBAGENT_UPDATE);
400
+ if (!handler) {
401
+ return;
402
+ }
403
+ const event: SubagentUpdateEvent = {
404
+ runId: parentRunId,
405
+ subagentRunId: childRunId,
406
+ subagentType,
407
+ subagentAgentId,
408
+ parentAgentId,
409
+ parentToolCallId,
410
+ phase,
411
+ data,
412
+ label: summarizeEvent(eventName, data),
413
+ timestamp: new Date().toISOString(),
414
+ };
415
+ try {
416
+ await handler.handle(GraphEvents.ON_SUBAGENT_UPDATE, event);
417
+ } catch {
418
+ /* observational — swallow */
419
+ }
420
+ };
421
+
422
+ const handler = BaseCallbackHandler.fromMethods({
423
+ [Callback.CUSTOM_EVENT]: async (
424
+ eventName: string,
425
+ data: unknown
426
+ ): Promise<void> => {
427
+ if (eventName === GraphEvents.ON_TOOL_EXECUTE) {
428
+ const toolHandler = parentRegistry.getHandler(
429
+ GraphEvents.ON_TOOL_EXECUTE
430
+ );
431
+ if (toolHandler) {
432
+ await toolHandler.handle(
433
+ GraphEvents.ON_TOOL_EXECUTE,
434
+ data as ToolExecuteBatchRequest
435
+ );
436
+ }
437
+ /**
438
+ * We also surface a short notice in the subagent-update stream so
439
+ * the UI can show "calling <tool>" for each tool the child spawns.
440
+ */
441
+ await wrap(eventName, 'run_step', data);
442
+ return;
443
+ }
444
+
445
+ if (eventName === GraphEvents.ON_RUN_STEP) {
446
+ await wrap(eventName, 'run_step', data);
447
+ return;
448
+ }
449
+ if (eventName === GraphEvents.ON_RUN_STEP_DELTA) {
450
+ await wrap(eventName, 'run_step_delta', data);
451
+ return;
452
+ }
453
+ if (eventName === GraphEvents.ON_RUN_STEP_COMPLETED) {
454
+ await wrap(eventName, 'run_step_completed', data);
455
+ return;
456
+ }
457
+ if (eventName === GraphEvents.ON_MESSAGE_DELTA) {
458
+ await wrap(eventName, 'message_delta', data);
459
+ return;
460
+ }
461
+ if (eventName === GraphEvents.ON_REASONING_DELTA) {
462
+ await wrap(eventName, 'reasoning_delta', data);
463
+ return;
464
+ }
465
+ },
466
+ });
467
+ /**
468
+ * `awaitHandlers = true` is required so the child's `ToolNode` actually
469
+ * blocks on the parent's `ON_TOOL_EXECUTE` handler until it resolves
470
+ * the batch request. The same flag applies to observational events
471
+ * (message_delta, run_step, …), which means a slow
472
+ * `ON_SUBAGENT_UPDATE` handler on the host serializes the child
473
+ * stream. If host-side latency becomes a concern, a future
474
+ * refinement could split operational and observational events into
475
+ * separate callback handlers with distinct await semantics.
476
+ */
477
+ handler.awaitHandlers = true;
478
+ return handler;
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Produces a short single-line label for an arbitrary forwarded child event.
484
+ * Used to populate {@link SubagentUpdateEvent.label} so the host UI can show
485
+ * a compact status ticker without parsing the raw payload.
486
+ */
487
+ export function summarizeEvent(eventName: string, data: unknown): string {
488
+ if (eventName === GraphEvents.ON_TOOL_EXECUTE) {
489
+ const req = data as { toolCalls?: Array<{ name?: string }> };
490
+ const names = (req.toolCalls ?? [])
491
+ .map((c) => c.name)
492
+ .filter((n): n is string => typeof n === 'string');
493
+ return names.length > 0 ? `Calling ${names.join(', ')}` : 'Calling tool';
494
+ }
495
+ if (eventName === GraphEvents.ON_RUN_STEP) {
496
+ const step = data as {
497
+ type?: string;
498
+ stepDetails?: { type?: string; tool_calls?: Array<{ name?: string }> };
499
+ };
500
+ const detailType = step.stepDetails?.type ?? step.type ?? 'step';
501
+ if (detailType === 'tool_calls') {
502
+ const names = (step.stepDetails?.tool_calls ?? [])
503
+ .map((c) => c.name)
504
+ .filter((n): n is string => typeof n === 'string');
505
+ return names.length > 0
506
+ ? `Using tool: ${names.join(', ')}`
507
+ : 'Planning tool call';
508
+ }
509
+ if (detailType === 'message_creation') {
510
+ return 'Thinking…';
511
+ }
512
+ return `Step: ${detailType}`;
513
+ }
514
+ if (eventName === GraphEvents.ON_RUN_STEP_COMPLETED) {
515
+ const step = data as {
516
+ result?: {
517
+ type?: string;
518
+ tool_call?: { name?: string; output?: string };
519
+ };
520
+ };
521
+ const tool = step.result?.tool_call;
522
+ if (tool?.name != null && tool.name !== '') {
523
+ return `Tool ${tool.name} complete`;
524
+ }
525
+ return 'Step complete';
526
+ }
527
+ if (eventName === GraphEvents.ON_MESSAGE_DELTA) {
528
+ return 'Streaming…';
529
+ }
530
+ return eventName;
531
+ }
532
+
533
+ /**
534
+ * Walk messages from last to first, returning the text content of the most
535
+ * recent AIMessage that has any. Non-text blocks (tool_use, thinking,
536
+ * redacted_thinking, tool_result) are stripped. If the last AIMessage is
537
+ * pure tool_use (e.g. the subagent hit `maxTurns` mid-tool-call), the walk
538
+ * continues to earlier AIMessages so partial progress is salvaged — this
539
+ * matches Claude Code's behavior in `agentToolUtils.finalizeAgentTool`.
540
+ * Returns "Task completed" only when no AIMessage in the history contains
541
+ * any text.
542
+ */
543
+ export function filterSubagentResult(messages: BaseMessage[]): string {
544
+ for (let i = messages.length - 1; i >= 0; i--) {
545
+ if (messages[i]._getType() !== 'ai') {
546
+ continue;
547
+ }
548
+
549
+ const content = messages[i].content;
550
+
551
+ if (typeof content === 'string') {
552
+ if (content) return content;
553
+ continue;
554
+ }
555
+
556
+ if (!Array.isArray(content)) {
557
+ continue;
558
+ }
559
+
560
+ const textParts: string[] = [];
561
+ for (const block of content) {
562
+ if (typeof block === 'string') {
563
+ textParts.push(block);
564
+ } else if ('type' in block && block.type === 'text' && 'text' in block) {
565
+ textParts.push(block.text as string);
566
+ }
567
+ }
568
+
569
+ if (textParts.length > 0) {
570
+ return textParts.join('\n');
571
+ }
572
+ }
573
+
574
+ return 'Task completed';
575
+ }
576
+
577
+ /**
578
+ * Resolve self-spawn configs by filling in agentInputs from the parent context.
579
+ * Returns configs with agentInputs guaranteed present. Throws on duplicate
580
+ * `type` values to prevent silent config shadowing.
581
+ */
582
+ export function resolveSubagentConfigs(
583
+ configs: SubagentConfig[],
584
+ parentContext: AgentContext
585
+ ): ResolvedSubagentConfig[] {
586
+ const resolved = configs
587
+ .map((config) => {
588
+ if (config.agentInputs != null) {
589
+ return config as ResolvedSubagentConfig;
590
+ }
591
+ if (config.self !== true || parentContext._sourceInputs == null) {
592
+ return null;
593
+ }
594
+ return {
595
+ ...config,
596
+ agentInputs: { ...parentContext._sourceInputs },
597
+ } as ResolvedSubagentConfig;
598
+ })
599
+ .filter((c): c is ResolvedSubagentConfig => c != null);
600
+
601
+ const seenTypes = new Set<string>();
602
+ for (const config of resolved) {
603
+ if (seenTypes.has(config.type)) {
604
+ throw new Error(
605
+ `Duplicate subagent type "${config.type}". Each SubagentConfig must have a unique "type" field.`
606
+ );
607
+ }
608
+ seenTypes.add(config.type);
609
+ }
610
+
611
+ return resolved;
612
+ }
613
+
614
+ /**
615
+ * Build child AgentInputs from a resolved config, stripping nesting and
616
+ * (optionally) event-driven fields. When `allowNested: true`, the child's
617
+ * `maxSubagentDepth` is decremented so that depth is consumed as the call
618
+ * chain deepens across graph boundaries — the parent's executor-level check
619
+ * alone cannot see into the child graph's separate executor.
620
+ *
621
+ * When `keepToolDefinitions` is `true`, the child retains the parent's
622
+ * `toolDefinitions` so event-driven tools remain usable. This is only safe
623
+ * when the caller has wired a forwarder for `ON_TOOL_EXECUTE` to a
624
+ * registered handler — otherwise the child will hang on tool dispatch.
625
+ *
626
+ * @remarks Advanced utility: exported primarily for testing and by
627
+ * {@link SubagentExecutor}. Host applications configuring subagents should
628
+ * not need to call this directly — it is invoked internally when a subagent
629
+ * tool is dispatched. The depth-countdown contract (parent's `maxDepth` in,
630
+ * child's decremented `maxSubagentDepth` on the returned inputs) is the
631
+ * mechanism that bounds nesting across graph boundaries; callers must
632
+ * respect it.
633
+ */
634
+ export function buildChildInputs(
635
+ config: ResolvedSubagentConfig,
636
+ childAgentId: string,
637
+ parentMaxDepth: number,
638
+ keepToolDefinitions: boolean = false
639
+ ): AgentInputs {
640
+ const { agentInputs } = config;
641
+ const childInputs: AgentInputs = {
642
+ ...agentInputs,
643
+ agentId: childAgentId,
644
+ toolDefinitions: keepToolDefinitions
645
+ ? agentInputs.toolDefinitions
646
+ : undefined,
647
+ /**
648
+ * Subagents run in an isolated context by contract. Parent-run-scoped
649
+ * fields that would otherwise survive the shallow-spread clone — the
650
+ * cross-run conversation summary and the prior-turn tool-discovery
651
+ * set — are cleared here so the child starts fresh. Host applications
652
+ * that want a subagent to see parent context must thread it in
653
+ * explicitly (e.g. via the `description` argument to the subagent
654
+ * tool), not via inherited state.
655
+ */
656
+ initialSummary: undefined,
657
+ discoveredTools: undefined,
658
+ };
659
+
660
+ if (config.allowNested === true) {
661
+ childInputs.maxSubagentDepth = Math.max(0, parentMaxDepth - 1);
662
+ } else {
663
+ childInputs.subagentConfigs = undefined;
664
+ childInputs.maxSubagentDepth = undefined;
665
+ }
666
+
667
+ return childInputs;
668
+ }
669
+
670
+ function truncateErrorMessage(error: unknown): string {
671
+ const message = error instanceof Error ? error.message : String(error);
672
+ if (message.length <= ERROR_MESSAGE_MAX_CHARS) {
673
+ return message;
674
+ }
675
+ return `${message.slice(0, ERROR_MESSAGE_MAX_CHARS)}...`;
676
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ SubagentExecutor,
3
+ filterSubagentResult,
4
+ resolveSubagentConfigs,
5
+ buildChildInputs,
6
+ summarizeEvent,
7
+ } from './SubagentExecutor';
8
+ export type {
9
+ SubagentExecuteParams,
10
+ SubagentExecuteResult,
11
+ SubagentExecutorOptions,
12
+ ChildGraphFactory,
13
+ } from './SubagentExecutor';