@librechat/agents 3.3.13 → 3.4.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 (192) hide show
  1. package/dist/cjs/graphs/Graph.cjs +121 -27
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/hooks/HookRegistry.cjs +83 -0
  4. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  5. package/dist/cjs/hooks/executeHooks.cjs +60 -14
  6. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  7. package/dist/cjs/hooks/index.cjs.map +1 -1
  8. package/dist/cjs/hooks/types.cjs +2 -0
  9. package/dist/cjs/hooks/types.cjs.map +1 -1
  10. package/dist/cjs/llm/anthropic/index.cjs +35 -206
  11. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  12. package/dist/cjs/llm/bedrock/index.cjs +121 -241
  13. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  14. package/dist/cjs/llm/google/index.cjs +19 -12
  15. package/dist/cjs/llm/google/index.cjs.map +1 -1
  16. package/dist/cjs/llm/init.cjs +3 -3
  17. package/dist/cjs/llm/invoke.cjs +2 -2
  18. package/dist/cjs/llm/mistral/index.cjs +26 -0
  19. package/dist/cjs/llm/mistral/index.cjs.map +1 -0
  20. package/dist/cjs/llm/openai/index.cjs +82 -80
  21. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  22. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  23. package/dist/cjs/llm/providers.cjs +19 -19
  24. package/dist/cjs/llm/providers.cjs.map +1 -1
  25. package/dist/cjs/llm/stream/chunkAdapters.cjs +198 -0
  26. package/dist/cjs/llm/stream/chunkAdapters.cjs.map +1 -0
  27. package/dist/cjs/llm/stream/smoother.cjs +369 -0
  28. package/dist/cjs/llm/stream/smoother.cjs.map +1 -0
  29. package/dist/cjs/llm/vertexai/index.cjs +13 -1
  30. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  31. package/dist/cjs/main.cjs +25 -16
  32. package/dist/cjs/messages/format.cjs +40 -23
  33. package/dist/cjs/messages/format.cjs.map +1 -1
  34. package/dist/cjs/run.cjs +82 -15
  35. package/dist/cjs/run.cjs.map +1 -1
  36. package/dist/cjs/session/AgentSession.cjs +37 -2
  37. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  38. package/dist/cjs/stream.cjs +1 -1
  39. package/dist/cjs/summarization/node.cjs +6 -3
  40. package/dist/cjs/summarization/node.cjs.map +1 -1
  41. package/dist/cjs/tools/BashExecutor.cjs +1 -1
  42. package/dist/cjs/tools/CodeExecutor.cjs +1 -1
  43. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
  44. package/dist/cjs/tools/ToolNode.cjs +316 -94
  45. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  46. package/dist/cjs/tools/ToolSearch.cjs +1 -1
  47. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +664 -80
  48. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  49. package/dist/cjs/tools/subagent/SubagentReplay.cjs +175 -0
  50. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
  51. package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
  52. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  53. package/dist/cjs/utils/index.cjs +2 -2
  54. package/dist/cjs/utils/tokens.cjs +76 -29
  55. package/dist/cjs/utils/tokens.cjs.map +1 -1
  56. package/dist/esm/graphs/Graph.mjs +121 -27
  57. package/dist/esm/graphs/Graph.mjs.map +1 -1
  58. package/dist/esm/hooks/HookRegistry.mjs +83 -0
  59. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  60. package/dist/esm/hooks/executeHooks.mjs +60 -14
  61. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  62. package/dist/esm/hooks/index.mjs.map +1 -1
  63. package/dist/esm/hooks/types.mjs +2 -1
  64. package/dist/esm/hooks/types.mjs.map +1 -1
  65. package/dist/esm/llm/anthropic/index.mjs +34 -205
  66. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  67. package/dist/esm/llm/bedrock/index.mjs +120 -240
  68. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  69. package/dist/esm/llm/google/index.mjs +19 -12
  70. package/dist/esm/llm/google/index.mjs.map +1 -1
  71. package/dist/esm/llm/init.mjs +1 -1
  72. package/dist/esm/llm/invoke.mjs +2 -2
  73. package/dist/esm/llm/mistral/index.mjs +26 -0
  74. package/dist/esm/llm/mistral/index.mjs.map +1 -0
  75. package/dist/esm/llm/openai/index.mjs +82 -80
  76. package/dist/esm/llm/openai/index.mjs.map +1 -1
  77. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  78. package/dist/esm/llm/providers.mjs +3 -3
  79. package/dist/esm/llm/providers.mjs.map +1 -1
  80. package/dist/esm/llm/stream/chunkAdapters.mjs +195 -0
  81. package/dist/esm/llm/stream/chunkAdapters.mjs.map +1 -0
  82. package/dist/esm/llm/stream/smoother.mjs +365 -0
  83. package/dist/esm/llm/stream/smoother.mjs.map +1 -0
  84. package/dist/esm/llm/vertexai/index.mjs +13 -1
  85. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  86. package/dist/esm/main.mjs +13 -11
  87. package/dist/esm/messages/format.mjs +40 -23
  88. package/dist/esm/messages/format.mjs.map +1 -1
  89. package/dist/esm/run.mjs +83 -16
  90. package/dist/esm/run.mjs.map +1 -1
  91. package/dist/esm/session/AgentSession.mjs +37 -2
  92. package/dist/esm/session/AgentSession.mjs.map +1 -1
  93. package/dist/esm/stream.mjs +1 -1
  94. package/dist/esm/summarization/node.mjs +6 -3
  95. package/dist/esm/summarization/node.mjs.map +1 -1
  96. package/dist/esm/tools/BashExecutor.mjs +1 -1
  97. package/dist/esm/tools/CodeExecutor.mjs +1 -1
  98. package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
  99. package/dist/esm/tools/ToolNode.mjs +317 -95
  100. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  101. package/dist/esm/tools/ToolSearch.mjs +1 -1
  102. package/dist/esm/tools/subagent/SubagentExecutor.mjs +665 -81
  103. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  104. package/dist/esm/tools/subagent/SubagentReplay.mjs +168 -0
  105. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
  106. package/dist/esm/tools/toolOutputReferences.mjs +20 -0
  107. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  108. package/dist/esm/utils/index.mjs +2 -2
  109. package/dist/esm/utils/tokens.mjs +76 -30
  110. package/dist/esm/utils/tokens.mjs.map +1 -1
  111. package/dist/types/graphs/Graph.d.ts +19 -1
  112. package/dist/types/hooks/HookRegistry.d.ts +10 -1
  113. package/dist/types/hooks/executeHooks.d.ts +5 -1
  114. package/dist/types/hooks/index.d.ts +2 -2
  115. package/dist/types/hooks/types.d.ts +12 -0
  116. package/dist/types/index.d.ts +3 -0
  117. package/dist/types/llm/google/index.d.ts +2 -0
  118. package/dist/types/llm/mistral/index.d.ts +11 -0
  119. package/dist/types/llm/openai/index.d.ts +20 -4
  120. package/dist/types/llm/openrouter/index.d.ts +4 -1
  121. package/dist/types/llm/stream/chunkAdapters.d.ts +48 -0
  122. package/dist/types/llm/stream/smoother.d.ts +95 -0
  123. package/dist/types/llm/vertexai/index.d.ts +2 -0
  124. package/dist/types/run.d.ts +4 -1
  125. package/dist/types/session/AgentSession.d.ts +1 -0
  126. package/dist/types/tools/ToolNode.d.ts +23 -15
  127. package/dist/types/tools/subagent/SubagentExecutor.d.ts +49 -7
  128. package/dist/types/tools/subagent/SubagentReplay.d.ts +81 -0
  129. package/dist/types/tools/toolOutputReferences.d.ts +12 -0
  130. package/dist/types/types/graph.d.ts +5 -5
  131. package/dist/types/types/hitl.d.ts +15 -0
  132. package/dist/types/types/llm.d.ts +21 -14
  133. package/dist/types/utils/tokens.d.ts +10 -0
  134. package/package.json +6 -2
  135. package/src/graphs/Graph.ts +257 -52
  136. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
  137. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
  138. package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
  139. package/src/hooks/HookRegistry.ts +151 -1
  140. package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
  141. package/src/hooks/__tests__/executeHooks.test.ts +160 -0
  142. package/src/hooks/executeHooks.ts +133 -15
  143. package/src/hooks/index.ts +3 -1
  144. package/src/hooks/types.ts +16 -0
  145. package/src/index.ts +8 -0
  146. package/src/llm/anthropic/index.ts +85 -354
  147. package/src/llm/bedrock/index.ts +174 -390
  148. package/src/llm/bedrock/llm.spec.ts +2 -0
  149. package/src/llm/bedrock/streamSealDispatch.test.ts +76 -0
  150. package/src/llm/custom-chat-models.smoke.test.ts +16 -1
  151. package/src/llm/google/index.ts +17 -18
  152. package/src/llm/google/streamSmoothing.test.ts +121 -0
  153. package/src/llm/mistral/index.ts +33 -0
  154. package/src/llm/mistral/streamSmoothing.test.ts +97 -0
  155. package/src/llm/openai/deepseek.test.ts +56 -0
  156. package/src/llm/openai/index.ts +119 -126
  157. package/src/llm/openrouter/index.ts +4 -1
  158. package/src/llm/providers.ts +3 -3
  159. package/src/llm/stream/chunkAdapters.test.ts +202 -0
  160. package/src/llm/stream/chunkAdapters.ts +317 -0
  161. package/src/llm/stream/reassembly.test.ts +241 -0
  162. package/src/llm/stream/smoother.bench.test.ts +155 -0
  163. package/src/llm/stream/smoother.test.ts +519 -0
  164. package/src/llm/stream/smoother.ts +574 -0
  165. package/src/llm/vertexai/index.ts +17 -1
  166. package/src/llm/vertexai/streamSmoothing.test.ts +109 -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 +164 -18
  172. package/src/session/AgentSession.ts +52 -2
  173. package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
  174. package/src/specs/subagent.test.ts +44 -0
  175. package/src/specs/tokens.test.ts +159 -14
  176. package/src/summarization/__tests__/node.test.ts +32 -14
  177. package/src/summarization/node.ts +15 -11
  178. package/src/tools/ToolNode.ts +631 -157
  179. package/src/tools/__tests__/SubagentExecutor.test.ts +590 -7
  180. package/src/tools/__tests__/SubagentReplay.test.ts +300 -0
  181. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
  182. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +486 -2
  183. package/src/tools/__tests__/hitl.test.ts +234 -5
  184. package/src/tools/__tests__/subagentHooks.test.ts +1040 -62
  185. package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
  186. package/src/tools/subagent/SubagentExecutor.ts +1438 -136
  187. package/src/tools/subagent/SubagentReplay.ts +575 -0
  188. package/src/tools/toolOutputReferences.ts +40 -1
  189. package/src/types/graph.ts +5 -5
  190. package/src/types/hitl.ts +16 -0
  191. package/src/types/llm.ts +53 -36
  192. package/src/utils/tokens.ts +115 -30
@@ -32,9 +32,9 @@ import type {
32
32
  UserPromptSubmitHookOutput,
33
33
  } from '@/hooks';
34
34
  import type * as t from '@/types';
35
- import { Providers as providers, GraphEvents } from '@/common';
36
- import * as events from '@/utils/events';
35
+ import { Constants, Providers as providers, GraphEvents } from '@/common';
37
36
  import { HookRegistry, createToolPolicyHook } from '@/hooks';
37
+ import * as events from '@/utils/events';
38
38
  import { askUserQuestion } from '@/hitl';
39
39
  import { ToolNode } from '../ToolNode';
40
40
 
@@ -1015,13 +1015,13 @@ describe('Run integration — HITL fallback checkpointer + resume', () => {
1015
1015
  },
1016
1016
  ],
1017
1017
  // Caller compileOptions without a checkpointer: HITL adds a MemorySaver
1018
- // fallback to the compiled graph, but the constructor restores this raw
1019
- // metadata (no checkpointer) onto Graph.compileOptions.
1018
+ // fallback while preserving the caller's remaining compile options.
1020
1019
  compileOptions: { interruptBefore: [] },
1021
1020
  },
1022
1021
  humanInTheLoop: { enabled: true },
1023
1022
  });
1024
- expect(run.Graph?.compileOptions?.checkpointer).toBeUndefined();
1023
+ expect(run.Graph?.compileOptions?.checkpointer).toBeInstanceOf(MemorySaver);
1024
+ expect(run.Graph?.compileOptions?.interruptBefore).toEqual([]);
1025
1025
 
1026
1026
  const graph = new StateGraph(MessagesAnnotation)
1027
1027
  .addNode('noop', (): MessagesUpdate => ({ messages: [] }))
@@ -1090,6 +1090,99 @@ describe('Run integration — HITL fallback checkpointer + resume', () => {
1090
1090
  expect(cmd2.goto).toEqual([]);
1091
1091
  });
1092
1092
 
1093
+ it('Run.resume restores a persisted interrupt before scoping a rebuilt resume', async () => {
1094
+ const { Run } = await import('@/run');
1095
+ const { Providers } = await import('@/common');
1096
+
1097
+ const registry = new HookRegistry();
1098
+ const persistedMatcher = {
1099
+ hooks: [async (): Promise<PreToolUseHookOutput> => ({ decision: 'ask' })],
1100
+ };
1101
+ registry.registerSession(
1102
+ 'persisted-hook-session',
1103
+ 'PreToolUse',
1104
+ persistedMatcher
1105
+ );
1106
+
1107
+ const run = await Run.create<t.IState>({
1108
+ runId: 'hitl-rebuilt-scope',
1109
+ graphConfig: {
1110
+ type: 'standard',
1111
+ agents: [
1112
+ {
1113
+ agentId: 'a',
1114
+ provider: Providers.OPENAI,
1115
+ clientOptions: {
1116
+ modelName: 'gpt-4o-mini',
1117
+ apiKey: 'test-key',
1118
+ },
1119
+ instructions: 'noop',
1120
+ maxContextTokens: 8000,
1121
+ },
1122
+ ],
1123
+ },
1124
+ hooks: registry,
1125
+ humanInTheLoop: { enabled: true },
1126
+ });
1127
+ const persistedState = {
1128
+ config: {
1129
+ configurable: {
1130
+ thread_id: 'durable-thread',
1131
+ checkpoint_id: 'interrupted-checkpoint',
1132
+ checkpoint_ns: '',
1133
+ },
1134
+ },
1135
+ tasks: [
1136
+ {
1137
+ interrupts: [
1138
+ {
1139
+ id: 'persisted-interrupt',
1140
+ value: {
1141
+ type: 'tool_approval',
1142
+ hook_session_id: 'persisted-hook-session',
1143
+ },
1144
+ },
1145
+ ],
1146
+ },
1147
+ ],
1148
+ };
1149
+ const getState = jest.fn(async (_config: RunnableConfig) => persistedState);
1150
+ run.graphRunnable = { getState } as unknown as t.CompiledStateWorkflow;
1151
+ const processSpy = jest
1152
+ .spyOn(run, 'processStream')
1153
+ .mockResolvedValue(undefined);
1154
+ const callerConfig = {
1155
+ version: 'v2' as const,
1156
+ configurable: { thread_id: 'durable-thread' },
1157
+ };
1158
+ const decision = [{ type: 'approve' as const }];
1159
+
1160
+ await run.resume(decision, callerConfig);
1161
+
1162
+ expect(getState).toHaveBeenCalledWith(callerConfig);
1163
+ const command = processSpy.mock.calls[0]?.[0] as Command;
1164
+ expect(command.resume).toEqual({ 'persisted-interrupt': decision });
1165
+ expect(processSpy.mock.calls[0]?.[1].configurable).toMatchObject({
1166
+ thread_id: 'durable-thread',
1167
+ checkpoint_id: 'interrupted-checkpoint',
1168
+ checkpoint_ns: '',
1169
+ });
1170
+ expect(run.getInterrupt()).toMatchObject({
1171
+ interruptId: 'persisted-interrupt',
1172
+ threadId: 'durable-thread',
1173
+ payload: {
1174
+ type: 'tool_approval',
1175
+ hook_session_id: 'persisted-hook-session',
1176
+ },
1177
+ });
1178
+ expect(
1179
+ registry.getMatchers('PreToolUse', 'persisted-hook-session')
1180
+ ).toEqual([persistedMatcher]);
1181
+ expect(registry.getMatchers('PreToolUse', run.id)).toEqual([
1182
+ persistedMatcher,
1183
+ ]);
1184
+ });
1185
+
1093
1186
  it('re-exports langgraph HITL primitives from the SDK barrel for host use', async () => {
1094
1187
  const indexExports = await import('@/index');
1095
1188
  expect(indexExports.MemorySaver).toBe(MemorySaver);
@@ -2194,6 +2287,81 @@ describe('Codex review fixes', () => {
2194
2287
  jest.restoreAllMocks();
2195
2288
  });
2196
2289
 
2290
+ it('reruns live hooks while replaying a consumed event-tool approval', async () => {
2291
+ let dispatchCalls = 0;
2292
+ jest
2293
+ .spyOn(events, 'safeDispatchCustomEvent')
2294
+ .mockImplementation(async (event, data) => {
2295
+ if (event !== 'on_tool_execute') {
2296
+ return;
2297
+ }
2298
+ dispatchCalls += 1;
2299
+ const request = data as {
2300
+ resolve: (results: t.ToolExecuteResult[]) => void;
2301
+ };
2302
+ request.resolve([
2303
+ { toolCallId: 'call_event', content: 'ran', status: 'success' },
2304
+ ]);
2305
+ });
2306
+
2307
+ const registry = new HookRegistry();
2308
+ let onceCalls = 0;
2309
+ let liveCalls = 0;
2310
+ registry.register('PreToolUse', {
2311
+ once: true,
2312
+ pattern: '^echo$',
2313
+ hooks: [
2314
+ async (): Promise<PreToolUseHookOutput> => {
2315
+ onceCalls += 1;
2316
+ return { decision: 'ask', reason: 'review once' };
2317
+ },
2318
+ ],
2319
+ });
2320
+ registry.register('PreToolUse', {
2321
+ pattern: '^echo$',
2322
+ hooks: [
2323
+ async (): Promise<PreToolUseHookOutput> => {
2324
+ liveCalls += 1;
2325
+ return liveCalls === 1
2326
+ ? { decision: 'allow' }
2327
+ : { decision: 'deny', reason: 'policy changed before resume' };
2328
+ },
2329
+ ],
2330
+ });
2331
+ const node = new ToolNode({
2332
+ tools: [createSchemaStub('echo')],
2333
+ eventDrivenMode: true,
2334
+ agentId: 'agent-event-replay',
2335
+ toolCallStepIds: new Map([['call_event', 'step_event']]),
2336
+ hookRegistry: registry,
2337
+ humanInTheLoop: { enabled: true },
2338
+ });
2339
+ const graph = buildHITLGraph(node, [
2340
+ { id: 'call_event', name: 'echo', args: { command: 'run' } },
2341
+ ]);
2342
+ const config = {
2343
+ configurable: { thread_id: 'thread-event-hook-replay' },
2344
+ };
2345
+
2346
+ const interrupted = await graph.invoke({ messages: [] }, config);
2347
+ expect(isInterrupted(interrupted)).toBe(true);
2348
+ const resumed = await graph.invoke(
2349
+ new Command({ resume: [{ type: 'approve' }] }),
2350
+ config
2351
+ );
2352
+
2353
+ expect(onceCalls).toBe(1);
2354
+ expect(liveCalls).toBe(2);
2355
+ expect(dispatchCalls).toBe(0);
2356
+ const messages = (resumed as { messages: ToolMessage[] }).messages;
2357
+ const result = messages.find(
2358
+ (message) =>
2359
+ message instanceof ToolMessage && message.tool_call_id === 'call_event'
2360
+ );
2361
+ expect(result?.status).toBe('error');
2362
+ expect(String(result?.content)).toContain('policy changed before resume');
2363
+ });
2364
+
2197
2365
  it('preserves session-scoped hooks across HITL interrupt so the policy still fires on resume', async () => {
2198
2366
  let dispatchCalls = 0;
2199
2367
  jest
@@ -2295,6 +2463,10 @@ describe('Codex review fixes', () => {
2295
2463
  * MUST still be present — the regression was that finally cleared
2296
2464
  * it, leaving the resume to bypass the policy entirely. */
2297
2465
  expect(run.getInterrupt()).toBeDefined();
2466
+ expect(run.getInterrupt()?.payload).toMatchObject({
2467
+ type: 'tool_approval',
2468
+ hook_session_id: runId,
2469
+ });
2298
2470
  expect(preCallCount).toBe(1);
2299
2471
  expect(registry.hasHookFor('PreToolUse', runId)).toBe(true);
2300
2472
  expect(dispatchCalls).toBe(0);
@@ -2310,6 +2482,63 @@ describe('Codex review fixes', () => {
2310
2482
  expect(registry.hasHookFor('PreToolUse', runId)).toBe(false);
2311
2483
  });
2312
2484
 
2485
+ it('persists the parent hook session when a direct subagent approval interrupts', async () => {
2486
+ const runId = 'parent-subagent-policy';
2487
+ const executeSubagent = jest.fn(async () => 'child result');
2488
+ const subagentTool = tool(executeSubagent, {
2489
+ name: Constants.SUBAGENT,
2490
+ description: 'execute a child agent',
2491
+ schema: z.object({
2492
+ description: z.string(),
2493
+ subagent_type: z.string(),
2494
+ }),
2495
+ }) as unknown as StructuredToolInterface;
2496
+ const registry = new HookRegistry();
2497
+ registry.registerSession(runId, 'PreToolUse', {
2498
+ hooks: [
2499
+ async (): Promise<PreToolUseHookOutput> => ({
2500
+ decision: 'ask',
2501
+ reason: 'approve child delegation',
2502
+ }),
2503
+ ],
2504
+ });
2505
+ const node = new ToolNode({
2506
+ tools: [subagentTool],
2507
+ hookRegistry: registry,
2508
+ directToolNames: new Set([Constants.SUBAGENT]),
2509
+ humanInTheLoop: { enabled: true },
2510
+ });
2511
+ const graph = buildHITLGraph(node, [
2512
+ {
2513
+ id: 'call_subagent',
2514
+ name: Constants.SUBAGENT,
2515
+ args: { description: 'inspect logs', subagent_type: 'researcher' },
2516
+ },
2517
+ ]);
2518
+
2519
+ const interrupted = await graph.invoke(
2520
+ { messages: [] },
2521
+ {
2522
+ configurable: {
2523
+ thread_id: 'parent-subagent-thread',
2524
+ run_id: runId,
2525
+ },
2526
+ }
2527
+ );
2528
+
2529
+ if (!isInterrupted<t.ToolApprovalInterruptPayload>(interrupted)) {
2530
+ throw new Error('expected a subagent approval interrupt');
2531
+ }
2532
+ expect(interrupted.__interrupt__[0].value).toMatchObject({
2533
+ type: 'tool_approval',
2534
+ hook_session_id: runId,
2535
+ action_requests: [
2536
+ { tool_call_id: 'call_subagent', name: Constants.SUBAGENT },
2537
+ ],
2538
+ });
2539
+ expect(executeSubagent).not.toHaveBeenCalled();
2540
+ });
2541
+
2313
2542
  it('denied tool in a deny+ask batch dispatches ON_RUN_STEP_COMPLETED exactly once across interrupt + resume', async () => {
2314
2543
  const stepCompletedDispatches: string[] = [];
2315
2544
  /** Spy on the underlying custom event dispatcher to capture every