@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
@@ -1,3 +1,4 @@
1
+ import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
1
2
  import { HumanMessage, getBufferString } from '@langchain/core/messages';
2
3
  import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
3
4
  import type { ChatGenerationChunk } from '@langchain/core/outputs';
@@ -65,6 +66,40 @@ class CapturingChatModel extends FakeChatModel {
65
66
  }
66
67
  }
67
68
 
69
+ class GatedMessageCountChatModel extends FakeChatModel {
70
+ private responseIndex = 0;
71
+
72
+ constructor(
73
+ private readonly gatedAiMessageCount: number,
74
+ private readonly onGatedStart: () => void,
75
+ private readonly releaseGate: Promise<void>
76
+ ) {
77
+ super({ responses: ['unused'] });
78
+ }
79
+
80
+ override async *_streamResponseChunks(
81
+ messages: BaseMessage[],
82
+ _options: this['ParsedCallOptions'],
83
+ runManager?: CallbackManagerForLLMRun
84
+ ): AsyncGenerator<ChatGenerationChunk> {
85
+ const aiMessageCount = messages.filter(
86
+ (message) => message.getType() === 'ai'
87
+ ).length;
88
+ if (aiMessageCount === this.gatedAiMessageCount) {
89
+ this.onGatedStart();
90
+ await this.releaseGate;
91
+ }
92
+ const output = `response-${this.responseIndex++}`;
93
+ yield this._createResponseChunk(output);
94
+ void runManager?.handleLLMNewToken(output);
95
+ }
96
+ }
97
+
98
+ type AgentInvocation = {
99
+ agentId: string;
100
+ messages: BaseMessage[];
101
+ };
102
+
68
103
  const expectCompiledWorkflow = (
69
104
  workflow: t.CompiledWorkflow | t.CompiledMultiAgentWorkflow
70
105
  ): void => {
@@ -342,6 +377,123 @@ describe('LangGraph composition smoke tests', () => {
342
377
  expect(graph.getParallelGroupId('final')).toBeUndefined();
343
378
  });
344
379
 
380
+ it.each([
381
+ ['without a prompt wrapper', undefined],
382
+ ['with a prompt wrapper', 'Summarize these results:\n{results}'],
383
+ ])('waits for every explicit fan-in source %s', async (_label, prompt) => {
384
+ const invocations: AgentInvocation[] = [];
385
+ let releaseLeft2!: () => void;
386
+ let markLeft2Started!: () => void;
387
+ const left2Release = new Promise<void>((resolve) => {
388
+ releaseLeft2 = resolve;
389
+ });
390
+ const left2Started = new Promise<void>((resolve) => {
391
+ markLeft2Started = resolve;
392
+ });
393
+ const invocationHandler = BaseCallbackHandler.fromMethods({
394
+ handleChatModelStart: (
395
+ _llm,
396
+ messages,
397
+ _runId,
398
+ _parentRunId,
399
+ _extraParams,
400
+ _tags,
401
+ metadata
402
+ ) => {
403
+ const agentId = metadata?.agentId;
404
+ if (typeof agentId === 'string') {
405
+ invocations.push({ agentId, messages: [...messages[0]] });
406
+ }
407
+ },
408
+ });
409
+ const graph = new MultiAgentGraph({
410
+ runId: 'fan-in-waiting-edge-smoke',
411
+ agents: [
412
+ makeAgent('root'),
413
+ makeAgent('left'),
414
+ makeAgent('left2'),
415
+ makeAgent('right'),
416
+ makeAgent('final'),
417
+ ],
418
+ edges: [
419
+ { from: 'root', to: ['left', 'right'], edgeType: 'direct' },
420
+ { from: 'left', to: 'left2', edgeType: 'direct' },
421
+ {
422
+ from: ['left2', 'right'],
423
+ to: 'final',
424
+ edgeType: 'direct',
425
+ prompt,
426
+ },
427
+ ],
428
+ });
429
+ graph.overrideModel = new GatedMessageCountChatModel(
430
+ 3,
431
+ markLeft2Started,
432
+ left2Release
433
+ );
434
+
435
+ const invocation = graph.createWorkflow().invoke(
436
+ { messages: [new HumanMessage('start')] },
437
+ {
438
+ ...makeConfig('fan-in-waiting-edge-smoke'),
439
+ callbacks: [invocationHandler],
440
+ }
441
+ );
442
+
443
+ let gateTimeout: ReturnType<typeof setTimeout> | undefined;
444
+ try {
445
+ await Promise.race([
446
+ left2Started,
447
+ new Promise<void>((_resolve, reject) => {
448
+ gateTimeout = setTimeout(
449
+ () =>
450
+ reject(
451
+ new Error(
452
+ `Timed out waiting for the gated branch; started: ${invocations
453
+ .map(({ agentId }) => agentId)
454
+ .join(', ')}`
455
+ )
456
+ ),
457
+ 5_000
458
+ );
459
+ }),
460
+ ]);
461
+ } finally {
462
+ clearTimeout(gateTimeout);
463
+ }
464
+ await new Promise<void>((resolve) => setImmediate(resolve));
465
+ expect(
466
+ invocations.filter(({ agentId }) => agentId === 'final')
467
+ ).toHaveLength(0);
468
+ releaseLeft2();
469
+ await invocation;
470
+
471
+ const invokedAgentIds = invocations.map(({ agentId }) => agentId);
472
+ expect(
473
+ invokedAgentIds.filter((agentId) => agentId === 'final')
474
+ ).toHaveLength(1);
475
+ expect(invokedAgentIds.indexOf('final')).toBeGreaterThan(
476
+ invokedAgentIds.indexOf('left2')
477
+ );
478
+ expect(invokedAgentIds.indexOf('final')).toBeGreaterThan(
479
+ invokedAgentIds.indexOf('right')
480
+ );
481
+ const finalInvocation = invocations.find(
482
+ ({ agentId }) => agentId === 'final'
483
+ );
484
+ if (prompt == null) {
485
+ expect(
486
+ finalInvocation?.messages.filter(
487
+ (message) => message.getType() === 'ai'
488
+ )
489
+ ).toHaveLength(4);
490
+ } else {
491
+ expect(getBufferString(finalInvocation?.messages ?? [])).toContain(
492
+ 'response-3'
493
+ );
494
+ }
495
+ });
496
+
345
497
  it('compiles mixed handoff and direct routing from the same agent', () => {
346
498
  const graph = new MultiAgentGraph({
347
499
  runId: 'mixed-routing-smoke',
@@ -0,0 +1,206 @@
1
+ import type { RunnableConfig } from '@langchain/core/runnables';
2
+ import type { ToolCall } from '@langchain/core/messages/tool';
3
+ import type { GraphFactory, GraphFactoryRequest } from '@/graphs/graphFactory';
4
+ import type * as t from '@/types';
5
+ import { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
6
+ import { createFakeStreamingLLM } from '@/llm/fake';
7
+ import { createGraph } from '@/graphs/createGraph';
8
+ import { Constants, Providers } from '@/common';
9
+ import { StandardGraph } from '@/graphs/Graph';
10
+
11
+ const invokeConfig: RunnableConfig = {
12
+ configurable: { thread_id: 'graph-factory-test' },
13
+ };
14
+
15
+ const makeAgent = (agentId: string): t.AgentInputs => ({
16
+ agentId,
17
+ provider: Providers.OPENAI,
18
+ instructions: `You are ${agentId}.`,
19
+ });
20
+
21
+ const getSubagentTool = (
22
+ graph: StandardGraph,
23
+ agentId: string
24
+ ): t.GenericTool => {
25
+ graph.createWorkflow();
26
+ const tools = graph.agentContexts.get(agentId)?.graphTools as
27
+ | t.GenericTool[]
28
+ | undefined;
29
+ const tool = tools?.find(
30
+ (candidate) => 'name' in candidate && candidate.name === Constants.SUBAGENT
31
+ );
32
+ if (tool == null) {
33
+ throw new Error(`Expected subagent tool for ${agentId}`);
34
+ }
35
+ return tool;
36
+ };
37
+
38
+ describe('graph factory', () => {
39
+ it('constructs standard and multi-agent graph adapters', () => {
40
+ const standard = createGraph({
41
+ kind: 'standard',
42
+ input: { runId: 'standard-factory', agents: [makeAgent('standard')] },
43
+ });
44
+ const multiAgent = createGraph({
45
+ kind: 'multi-agent',
46
+ input: {
47
+ runId: 'multi-agent-factory',
48
+ agents: [makeAgent('multi')],
49
+ edges: [],
50
+ },
51
+ });
52
+
53
+ expect(standard).toBeInstanceOf(StandardGraph);
54
+ expect(standard).not.toBeInstanceOf(MultiAgentGraph);
55
+ expect(multiAgent).toBeInstanceOf(MultiAgentGraph);
56
+ });
57
+
58
+ it('accepts a union-typed graph factory request', () => {
59
+ const construct = (request: GraphFactoryRequest): StandardGraph =>
60
+ createGraph(request);
61
+ const graph = construct({
62
+ kind: 'standard',
63
+ input: { runId: 'union-factory', agents: [makeAgent('union')] },
64
+ });
65
+
66
+ expect(graph).toBeInstanceOf(StandardGraph);
67
+ });
68
+
69
+ it('rejects invalid per-member recursion limits', () => {
70
+ expect(() =>
71
+ createGraph({
72
+ kind: 'multi-agent',
73
+ input: {
74
+ runId: 'invalid-member-limit',
75
+ agents: [makeAgent('member')],
76
+ edges: [],
77
+ memberRecursionLimit: 0,
78
+ },
79
+ })
80
+ ).toThrow(/memberRecursionLimit must be a positive safe integer/);
81
+ });
82
+
83
+ it('keeps direct StandardGraph construction compatible with agent children', async () => {
84
+ const parent = {
85
+ ...makeAgent('parent'),
86
+ subagentConfigs: [
87
+ {
88
+ type: 'worker',
89
+ name: 'Worker',
90
+ description: 'Completes one isolated task.',
91
+ agentInputs: makeAgent('worker'),
92
+ },
93
+ ],
94
+ } satisfies t.AgentInputs;
95
+ const graph = new StandardGraph({
96
+ runId: 'direct-standard-factory',
97
+ agents: [parent],
98
+ });
99
+ graph.setSubagentModelOverride(
100
+ createFakeStreamingLLM({ responses: ['worker complete'] })
101
+ );
102
+
103
+ const result = await getSubagentTool(graph, 'parent').invoke(
104
+ { description: 'Complete the task.', subagent_type: 'worker' },
105
+ invokeConfig
106
+ );
107
+
108
+ expect(result).toBe('worker complete');
109
+ });
110
+
111
+ it('fails early when direct construction cannot instantiate a graph child', () => {
112
+ const parent = {
113
+ ...makeAgent('parent'),
114
+ subagentConfigs: [
115
+ {
116
+ kind: 'graph' as const,
117
+ type: 'team',
118
+ name: 'Team',
119
+ description: 'Runs one member.',
120
+ agents: [makeAgent('member')],
121
+ edges: [],
122
+ entryAgentId: 'member',
123
+ resultAgentId: 'member',
124
+ },
125
+ ],
126
+ } satisfies t.AgentInputs;
127
+ const graph = new StandardGraph({
128
+ runId: 'direct-graph-child-factory',
129
+ agents: [parent],
130
+ });
131
+
132
+ expect(() => graph.createWorkflow()).toThrow(
133
+ /constructing the parent with createGraph\(\)/
134
+ );
135
+ });
136
+
137
+ it('propagates an injected factory through child and grandchild graphs', async () => {
138
+ const requests: GraphFactoryRequest[] = [];
139
+ const graphFactory: GraphFactory = (request) => {
140
+ requests.push(request);
141
+ if (request.kind === 'multi-agent') {
142
+ return new MultiAgentGraph(request.input, { graphFactory });
143
+ }
144
+ return new StandardGraph(request.input, { graphFactory });
145
+ };
146
+ const nestedToolCall: ToolCall = {
147
+ id: 'call_nested_specialist',
148
+ name: Constants.SUBAGENT,
149
+ args: {
150
+ description: 'Complete the specialist step.',
151
+ subagent_type: 'specialist',
152
+ },
153
+ type: 'tool_call',
154
+ };
155
+ const router = {
156
+ ...makeAgent('router'),
157
+ subagentConfigs: [
158
+ {
159
+ type: 'specialist',
160
+ name: 'Specialist',
161
+ description: 'Completes the nested specialist step.',
162
+ agentInputs: makeAgent('specialist'),
163
+ },
164
+ ],
165
+ } satisfies t.AgentInputs;
166
+ const root = {
167
+ ...makeAgent('root'),
168
+ maxSubagentDepth: 2,
169
+ subagentConfigs: [
170
+ {
171
+ type: 'router',
172
+ name: 'Router',
173
+ description: 'Delegates one nested specialist step.',
174
+ allowNested: true,
175
+ agentInputs: router,
176
+ },
177
+ ],
178
+ } satisfies t.AgentInputs;
179
+ const graph = new StandardGraph(
180
+ { runId: 'recursive-graph-factory', agents: [root] },
181
+ { graphFactory }
182
+ );
183
+ graph.setSubagentModelOverride(
184
+ createFakeStreamingLLM({
185
+ responses: ['delegating', 'specialist complete', 'router complete'],
186
+ toolCalls: [nestedToolCall],
187
+ })
188
+ );
189
+
190
+ const result = await getSubagentTool(graph, 'root').invoke(
191
+ { description: 'Route the task.', subagent_type: 'router' },
192
+ invokeConfig
193
+ );
194
+
195
+ expect(result).toBe('router complete');
196
+ expect(
197
+ requests.map(({ kind, input }) => ({
198
+ kind,
199
+ agentIds: input.agents.map(({ agentId }) => agentId),
200
+ }))
201
+ ).toEqual([
202
+ { kind: 'standard', agentIds: ['router'] },
203
+ { kind: 'standard', agentIds: ['specialist'] },
204
+ ]);
205
+ });
206
+ });
@@ -0,0 +1,25 @@
1
+ import type { StandardGraph } from './Graph';
2
+
3
+ export type GraphRuntimeConfig = Pick<
4
+ StandardGraph,
5
+ | 'hookRegistry'
6
+ | 'humanInTheLoop'
7
+ | 'toolOutputReferences'
8
+ | 'eagerEventToolExecution'
9
+ | 'codeSessionToolNames'
10
+ | 'interruptingToolNames'
11
+ | 'toolExecution'
12
+ >;
13
+
14
+ export function applyGraphRuntimeConfig(
15
+ graph: StandardGraph,
16
+ config: GraphRuntimeConfig
17
+ ): void {
18
+ graph.hookRegistry = config.hookRegistry;
19
+ graph.humanInTheLoop = config.humanInTheLoop;
20
+ graph.toolOutputReferences = config.toolOutputReferences;
21
+ graph.eagerEventToolExecution = config.eagerEventToolExecution;
22
+ graph.codeSessionToolNames = config.codeSessionToolNames;
23
+ graph.interruptingToolNames = config.interruptingToolNames;
24
+ graph.toolExecution = config.toolExecution;
25
+ }
@@ -0,0 +1,32 @@
1
+ import type {
2
+ GraphFactory,
3
+ GraphFactoryDependencies,
4
+ GraphFactoryRequest,
5
+ } from '@/graphs/graphFactory';
6
+ import type { MultiAgentGraphInput, StandardGraphInput } from '@/types';
7
+ import { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
8
+ import { StandardGraph } from '@/graphs/Graph';
9
+
10
+ const createGraphByKind: GraphFactory = (request) => {
11
+ if (request.kind === 'multi-agent') {
12
+ return new MultiAgentGraph(request.input, dependencies);
13
+ }
14
+ return new StandardGraph(request.input, dependencies);
15
+ };
16
+
17
+ const dependencies: GraphFactoryDependencies = {
18
+ graphFactory: createGraphByKind,
19
+ };
20
+
21
+ export function createGraph(request: {
22
+ kind: 'standard';
23
+ input: StandardGraphInput;
24
+ }): StandardGraph;
25
+ export function createGraph(request: {
26
+ kind: 'multi-agent';
27
+ input: MultiAgentGraphInput;
28
+ }): MultiAgentGraph;
29
+ export function createGraph(request: GraphFactoryRequest): StandardGraph;
30
+ export function createGraph(request: GraphFactoryRequest): StandardGraph {
31
+ return createGraphByKind(request);
32
+ }
@@ -0,0 +1,12 @@
1
+ import type { StandardGraph } from './Graph';
2
+ import type * as t from '@/types';
3
+
4
+ export type GraphFactoryRequest =
5
+ | { kind: 'standard'; input: t.StandardGraphInput }
6
+ | { kind: 'multi-agent'; input: t.MultiAgentGraphInput };
7
+
8
+ export type GraphFactory = (request: GraphFactoryRequest) => StandardGraph;
9
+
10
+ export type GraphFactoryDependencies = {
11
+ graphFactory: GraphFactory;
12
+ };
@@ -1,2 +1,4 @@
1
1
  export * from './Graph';
2
2
  export * from './MultiAgentGraph';
3
+ export * from './createGraph';
4
+ export type * from './graphFactory';
@@ -1,5 +1,31 @@
1
1
  // src/hooks/HookRegistry.ts
2
- import type { HookEvent, HookMatcher } from './types';
2
+ import type {
3
+ HookEvent,
4
+ HookMatcher,
5
+ ToolApprovalReplayKey,
6
+ ToolApprovalReplaySnapshot,
7
+ AggregatedHookResult,
8
+ } from './types';
9
+
10
+ function serializeApprovalKey(key: ToolApprovalReplayKey): string {
11
+ return JSON.stringify([key.executionScope, key.agentId, key.toolUseId]);
12
+ }
13
+
14
+ function deserializeApprovalKey(value: string): ToolApprovalReplayKey | null {
15
+ const parsed: unknown = JSON.parse(value);
16
+ if (
17
+ !Array.isArray(parsed) ||
18
+ parsed.length !== 3 ||
19
+ parsed.some((part) => typeof part !== 'string')
20
+ ) {
21
+ return null;
22
+ }
23
+ return {
24
+ executionScope: parsed[0],
25
+ agentId: parsed[1],
26
+ toolUseId: parsed[2],
27
+ };
28
+ }
3
29
 
4
30
  /**
5
31
  * Internal matcher storage type.
@@ -71,6 +97,11 @@ export class HookRegistry {
71
97
  * O(1) insertion in hot paths, no spread-on-write.
72
98
  */
73
99
  private readonly haltSignals: Map<string, HookHaltSignal> = new Map();
100
+ /** One-shot hook contributions retained until approval is consumed. */
101
+ private readonly pendingToolApprovals = new Map<
102
+ string,
103
+ Map<string, AggregatedHookResult>
104
+ >();
74
105
 
75
106
  /**
76
107
  * Register a matcher for the lifetime of this registry (= one Run).
@@ -160,6 +191,125 @@ export class HookRegistry {
160
191
  */
161
192
  clearSession(sessionId: string): void {
162
193
  this.sessions.delete(sessionId);
194
+ this.pendingToolApprovals.delete(sessionId);
195
+ }
196
+
197
+ /** Copies session-scoped policy into a rebuilt or branched Run. */
198
+ copySession(sourceSessionId: string, targetSessionId: string): void {
199
+ if (sourceSessionId === targetSessionId) {
200
+ return;
201
+ }
202
+ const source = this.sessions.get(sourceSessionId);
203
+ if (source != null) {
204
+ const target = this.ensureSessionBucket(targetSessionId);
205
+ for (const event of Object.keys(source) as HookEvent[]) {
206
+ const targetList = ensureList(target, event);
207
+ for (const matcher of readList(source, event)) {
208
+ if (!targetList.includes(matcher)) {
209
+ targetList.push(matcher);
210
+ }
211
+ }
212
+ }
213
+ }
214
+ const pending = this.pendingToolApprovals.get(sourceSessionId);
215
+ if (pending == null) {
216
+ return;
217
+ }
218
+ let targetPending = this.pendingToolApprovals.get(targetSessionId);
219
+ if (targetPending == null) {
220
+ targetPending = new Map();
221
+ this.pendingToolApprovals.set(targetSessionId, targetPending);
222
+ }
223
+ for (const [toolUseId, result] of pending) {
224
+ if (!targetPending.has(toolUseId)) {
225
+ targetPending.set(toolUseId, result);
226
+ }
227
+ }
228
+ }
229
+
230
+ getPendingToolApproval(
231
+ sessionId: string,
232
+ key: ToolApprovalReplayKey
233
+ ): AggregatedHookResult | undefined {
234
+ return this.pendingToolApprovals
235
+ .get(sessionId)
236
+ ?.get(serializeApprovalKey(key));
237
+ }
238
+
239
+ setPendingToolApproval(
240
+ sessionId: string,
241
+ key: ToolApprovalReplayKey,
242
+ result: AggregatedHookResult
243
+ ): void {
244
+ let pending = this.pendingToolApprovals.get(sessionId);
245
+ if (pending == null) {
246
+ pending = new Map();
247
+ this.pendingToolApprovals.set(sessionId, pending);
248
+ }
249
+ pending.set(serializeApprovalKey(key), result);
250
+ }
251
+
252
+ clearPendingToolApproval(
253
+ sessionId: string,
254
+ key: ToolApprovalReplayKey
255
+ ): void {
256
+ const pending = this.pendingToolApprovals.get(sessionId);
257
+ if (pending == null) {
258
+ return;
259
+ }
260
+ pending.delete(serializeApprovalKey(key));
261
+ if (pending.size === 0) {
262
+ this.pendingToolApprovals.delete(sessionId);
263
+ }
264
+ }
265
+
266
+ snapshotPendingToolApprovals(
267
+ sessionId: string,
268
+ executionScope: string
269
+ ): ToolApprovalReplaySnapshot[] {
270
+ const pending = this.pendingToolApprovals.get(sessionId);
271
+ if (pending == null) {
272
+ return [];
273
+ }
274
+ const snapshots: ToolApprovalReplaySnapshot[] = [];
275
+ for (const [serializedKey, result] of pending) {
276
+ const key = deserializeApprovalKey(serializedKey);
277
+ if (key == null || key.executionScope !== executionScope) {
278
+ continue;
279
+ }
280
+ snapshots.push({ key, result });
281
+ }
282
+ return snapshots;
283
+ }
284
+
285
+ restorePendingToolApprovals(
286
+ sessionId: string,
287
+ targetExecutionScope: string,
288
+ snapshots: ReadonlyArray<ToolApprovalReplaySnapshot>
289
+ ): void {
290
+ const restored = new Map<string, AggregatedHookResult>();
291
+ for (const [serializedKey, result] of this.pendingToolApprovals.get(
292
+ sessionId
293
+ ) ?? []) {
294
+ const key = deserializeApprovalKey(serializedKey);
295
+ if (key?.executionScope !== targetExecutionScope) {
296
+ restored.set(serializedKey, result);
297
+ }
298
+ }
299
+ for (const snapshot of snapshots) {
300
+ restored.set(
301
+ serializeApprovalKey({
302
+ ...snapshot.key,
303
+ executionScope: targetExecutionScope,
304
+ }),
305
+ snapshot.result
306
+ );
307
+ }
308
+ if (restored.size === 0) {
309
+ this.pendingToolApprovals.delete(sessionId);
310
+ return;
311
+ }
312
+ this.pendingToolApprovals.set(sessionId, restored);
163
313
  }
164
314
 
165
315
  /**