@librechat/agents 3.1.67-dev.4 → 3.1.68

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 (162) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +3 -23
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +0 -16
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +0 -91
  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/main.cjs +1 -53
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/messages/format.cjs +12 -74
  12. package/dist/cjs/messages/format.cjs.map +1 -1
  13. package/dist/cjs/run.cjs +0 -111
  14. package/dist/cjs/run.cjs.map +1 -1
  15. package/dist/cjs/summarization/index.cjs +41 -0
  16. package/dist/cjs/summarization/index.cjs.map +1 -1
  17. package/dist/cjs/summarization/node.cjs +121 -63
  18. package/dist/cjs/summarization/node.cjs.map +1 -1
  19. package/dist/cjs/tools/ToolNode.cjs +140 -304
  20. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  21. package/dist/esm/agents/AgentContext.mjs +3 -23
  22. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  23. package/dist/esm/common/enum.mjs +1 -15
  24. package/dist/esm/common/enum.mjs.map +1 -1
  25. package/dist/esm/graphs/Graph.mjs +0 -91
  26. package/dist/esm/graphs/Graph.mjs.map +1 -1
  27. package/dist/esm/graphs/MultiAgentGraph.mjs +36 -0
  28. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  29. package/dist/esm/main.mjs +2 -13
  30. package/dist/esm/main.mjs.map +1 -1
  31. package/dist/esm/messages/format.mjs +4 -66
  32. package/dist/esm/messages/format.mjs.map +1 -1
  33. package/dist/esm/run.mjs +0 -111
  34. package/dist/esm/run.mjs.map +1 -1
  35. package/dist/esm/summarization/index.mjs +41 -1
  36. package/dist/esm/summarization/index.mjs.map +1 -1
  37. package/dist/esm/summarization/node.mjs +121 -63
  38. package/dist/esm/summarization/node.mjs.map +1 -1
  39. package/dist/esm/tools/ToolNode.mjs +142 -306
  40. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  41. package/dist/types/agents/AgentContext.d.ts +0 -6
  42. package/dist/types/common/enum.d.ts +1 -10
  43. package/dist/types/graphs/Graph.d.ts +0 -2
  44. package/dist/types/graphs/MultiAgentGraph.d.ts +12 -0
  45. package/dist/types/index.d.ts +0 -8
  46. package/dist/types/messages/format.d.ts +1 -2
  47. package/dist/types/run.d.ts +0 -1
  48. package/dist/types/summarization/index.d.ts +2 -0
  49. package/dist/types/summarization/node.d.ts +0 -2
  50. package/dist/types/tools/ToolNode.d.ts +2 -24
  51. package/dist/types/types/graph.d.ts +2 -61
  52. package/dist/types/types/index.d.ts +0 -1
  53. package/dist/types/types/run.d.ts +0 -20
  54. package/dist/types/types/tools.d.ts +1 -38
  55. package/package.json +1 -5
  56. package/src/agents/AgentContext.ts +2 -26
  57. package/src/common/enum.ts +0 -15
  58. package/src/graphs/Graph.ts +0 -113
  59. package/src/graphs/MultiAgentGraph.ts +39 -0
  60. package/src/graphs/__tests__/MultiAgentGraph.test.ts +91 -0
  61. package/src/index.ts +0 -10
  62. package/src/messages/format.ts +4 -74
  63. package/src/run.ts +0 -126
  64. package/src/summarization/__tests__/node.test.ts +42 -0
  65. package/src/summarization/__tests__/trigger.test.ts +100 -1
  66. package/src/summarization/index.ts +47 -0
  67. package/src/summarization/node.ts +149 -77
  68. package/src/tools/ToolNode.ts +169 -391
  69. package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
  70. package/src/types/graph.ts +1 -80
  71. package/src/types/index.ts +0 -1
  72. package/src/types/run.ts +0 -20
  73. package/src/types/tools.ts +1 -41
  74. package/dist/cjs/hooks/HookRegistry.cjs +0 -162
  75. package/dist/cjs/hooks/HookRegistry.cjs.map +0 -1
  76. package/dist/cjs/hooks/executeHooks.cjs +0 -276
  77. package/dist/cjs/hooks/executeHooks.cjs.map +0 -1
  78. package/dist/cjs/hooks/matchers.cjs +0 -256
  79. package/dist/cjs/hooks/matchers.cjs.map +0 -1
  80. package/dist/cjs/hooks/types.cjs +0 -27
  81. package/dist/cjs/hooks/types.cjs.map +0 -1
  82. package/dist/cjs/tools/BashExecutor.cjs +0 -175
  83. package/dist/cjs/tools/BashExecutor.cjs.map +0 -1
  84. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +0 -296
  85. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +0 -1
  86. package/dist/cjs/tools/ReadFile.cjs +0 -43
  87. package/dist/cjs/tools/ReadFile.cjs.map +0 -1
  88. package/dist/cjs/tools/SkillTool.cjs +0 -50
  89. package/dist/cjs/tools/SkillTool.cjs.map +0 -1
  90. package/dist/cjs/tools/SubagentTool.cjs +0 -92
  91. package/dist/cjs/tools/SubagentTool.cjs.map +0 -1
  92. package/dist/cjs/tools/skillCatalog.cjs +0 -84
  93. package/dist/cjs/tools/skillCatalog.cjs.map +0 -1
  94. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +0 -511
  95. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +0 -1
  96. package/dist/esm/hooks/HookRegistry.mjs +0 -160
  97. package/dist/esm/hooks/HookRegistry.mjs.map +0 -1
  98. package/dist/esm/hooks/executeHooks.mjs +0 -273
  99. package/dist/esm/hooks/executeHooks.mjs.map +0 -1
  100. package/dist/esm/hooks/matchers.mjs +0 -251
  101. package/dist/esm/hooks/matchers.mjs.map +0 -1
  102. package/dist/esm/hooks/types.mjs +0 -25
  103. package/dist/esm/hooks/types.mjs.map +0 -1
  104. package/dist/esm/tools/BashExecutor.mjs +0 -169
  105. package/dist/esm/tools/BashExecutor.mjs.map +0 -1
  106. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +0 -287
  107. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +0 -1
  108. package/dist/esm/tools/ReadFile.mjs +0 -38
  109. package/dist/esm/tools/ReadFile.mjs.map +0 -1
  110. package/dist/esm/tools/SkillTool.mjs +0 -45
  111. package/dist/esm/tools/SkillTool.mjs.map +0 -1
  112. package/dist/esm/tools/SubagentTool.mjs +0 -85
  113. package/dist/esm/tools/SubagentTool.mjs.map +0 -1
  114. package/dist/esm/tools/skillCatalog.mjs +0 -82
  115. package/dist/esm/tools/skillCatalog.mjs.map +0 -1
  116. package/dist/esm/tools/subagent/SubagentExecutor.mjs +0 -505
  117. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +0 -1
  118. package/dist/types/hooks/HookRegistry.d.ts +0 -56
  119. package/dist/types/hooks/executeHooks.d.ts +0 -79
  120. package/dist/types/hooks/index.d.ts +0 -6
  121. package/dist/types/hooks/matchers.d.ts +0 -95
  122. package/dist/types/hooks/types.d.ts +0 -320
  123. package/dist/types/tools/BashExecutor.d.ts +0 -45
  124. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +0 -72
  125. package/dist/types/tools/ReadFile.d.ts +0 -28
  126. package/dist/types/tools/SkillTool.d.ts +0 -40
  127. package/dist/types/tools/SubagentTool.d.ts +0 -36
  128. package/dist/types/tools/skillCatalog.d.ts +0 -19
  129. package/dist/types/tools/subagent/SubagentExecutor.d.ts +0 -137
  130. package/dist/types/tools/subagent/index.d.ts +0 -2
  131. package/dist/types/types/skill.d.ts +0 -9
  132. package/src/hooks/HookRegistry.ts +0 -208
  133. package/src/hooks/__tests__/HookRegistry.test.ts +0 -190
  134. package/src/hooks/__tests__/compactHooks.test.ts +0 -214
  135. package/src/hooks/__tests__/executeHooks.test.ts +0 -1013
  136. package/src/hooks/__tests__/integration.test.ts +0 -337
  137. package/src/hooks/__tests__/matchers.test.ts +0 -238
  138. package/src/hooks/__tests__/toolHooks.test.ts +0 -669
  139. package/src/hooks/executeHooks.ts +0 -375
  140. package/src/hooks/index.ts +0 -57
  141. package/src/hooks/matchers.ts +0 -280
  142. package/src/hooks/types.ts +0 -404
  143. package/src/messages/formatAgentMessages.skills.test.ts +0 -334
  144. package/src/scripts/multi-agent-subagent.ts +0 -246
  145. package/src/scripts/subagent-event-driven-debug.ts +0 -190
  146. package/src/scripts/subagent-tools-debug.ts +0 -160
  147. package/src/specs/subagent.test.ts +0 -305
  148. package/src/tools/BashExecutor.ts +0 -205
  149. package/src/tools/BashProgrammaticToolCalling.ts +0 -397
  150. package/src/tools/ReadFile.ts +0 -39
  151. package/src/tools/SkillTool.ts +0 -46
  152. package/src/tools/SubagentTool.ts +0 -100
  153. package/src/tools/__tests__/ReadFile.test.ts +0 -44
  154. package/src/tools/__tests__/SkillTool.test.ts +0 -442
  155. package/src/tools/__tests__/SubagentExecutor.test.ts +0 -1148
  156. package/src/tools/__tests__/SubagentTool.test.ts +0 -149
  157. package/src/tools/__tests__/skillCatalog.test.ts +0 -161
  158. package/src/tools/__tests__/subagentHooks.test.ts +0 -215
  159. package/src/tools/skillCatalog.ts +0 -126
  160. package/src/tools/subagent/SubagentExecutor.ts +0 -676
  161. package/src/tools/subagent/index.ts +0 -13
  162. package/src/types/skill.ts +0 -11
@@ -1,669 +0,0 @@
1
- // src/hooks/__tests__/toolHooks.test.ts
2
- import { ToolCall } from '@langchain/core/messages/tool';
3
- import { HumanMessage } from '@langchain/core/messages';
4
- import { HookRegistry } from '../HookRegistry';
5
- import { Run } from '@/run';
6
- import {
7
- GraphEvents,
8
- Providers,
9
- ToolEndHandler,
10
- ModelEndHandler,
11
- } from '@/index';
12
- import type * as t from '@/types';
13
- import type {
14
- HookCallback,
15
- PreToolUseHookOutput,
16
- PostToolUseHookOutput,
17
- PostToolUseFailureHookOutput,
18
- PermissionDeniedHookInput,
19
- PermissionDeniedHookOutput,
20
- PreToolUseHookInput,
21
- PostToolUseHookInput,
22
- PostToolUseFailureHookInput,
23
- } from '../types';
24
-
25
- const llmConfig: t.LLMConfig = {
26
- provider: Providers.OPENAI,
27
- streaming: true,
28
- streamUsage: false,
29
- };
30
-
31
- const callerConfig = {
32
- configurable: { thread_id: 'test-thread' },
33
- streamMode: 'values' as const,
34
- version: 'v2' as const,
35
- };
36
-
37
- const echoToolDef: t.LCTool = {
38
- name: 'echo',
39
- description: 'Echoes input',
40
- parameters: {
41
- type: 'object' as const,
42
- properties: { text: { type: 'string' } },
43
- required: ['text'],
44
- },
45
- };
46
-
47
- let callCounter = 0;
48
-
49
- function makeToolCall(text = 'hello', name = 'echo'): ToolCall {
50
- return {
51
- name,
52
- args: { text },
53
- id: `call_${++callCounter}`,
54
- type: 'tool_call',
55
- };
56
- }
57
-
58
- function createToolExecuteHandler(): t.EventHandler {
59
- return {
60
- handle: async (_event: string, rawData: unknown): Promise<void> => {
61
- const data = rawData as t.ToolExecuteBatchRequest;
62
- const results: t.ToolExecuteResult[] = data.toolCalls.map(
63
- (tc: t.ToolCallRequest) => ({
64
- toolCallId: tc.id,
65
- content: `echo: ${(tc.args as Record<string, string>).text}`,
66
- status: 'success' as const,
67
- })
68
- );
69
- data.resolve(results);
70
- },
71
- };
72
- }
73
-
74
- function createErrorToolExecuteHandler(): t.EventHandler {
75
- return {
76
- handle: async (_event: string, rawData: unknown): Promise<void> => {
77
- const data = rawData as t.ToolExecuteBatchRequest;
78
- const results: t.ToolExecuteResult[] = data.toolCalls.map(
79
- (tc: t.ToolCallRequest) => ({
80
- toolCallId: tc.id,
81
- content: '',
82
- status: 'error' as const,
83
- errorMessage: `tool ${tc.name} failed deliberately`,
84
- })
85
- );
86
- data.resolve(results);
87
- },
88
- };
89
- }
90
-
91
- async function createEventDrivenRun(
92
- hooks: HookRegistry,
93
- toolHandler: t.EventHandler = createToolExecuteHandler(),
94
- runId = 'tool-hook-run'
95
- ): Promise<Run<t.IState>> {
96
- const customHandlers: Record<string, t.EventHandler> = {
97
- [GraphEvents.ON_TOOL_EXECUTE]: toolHandler,
98
- [GraphEvents.TOOL_END]: new ToolEndHandler(),
99
- [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
100
- };
101
-
102
- return Run.create<t.IState>({
103
- runId,
104
- graphConfig: {
105
- type: 'standard',
106
- llmConfig,
107
- toolDefinitions: [echoToolDef],
108
- instructions: 'Use the echo tool when asked.',
109
- },
110
- returnContent: true,
111
- skipCleanup: true,
112
- customHandlers,
113
- hooks,
114
- });
115
- }
116
-
117
- describe('Tool-level hook integration (event-driven mode)', () => {
118
- beforeEach(() => {
119
- callCounter = 0;
120
- });
121
- jest.setTimeout(15000);
122
-
123
- describe('PreToolUse', () => {
124
- it('fires with toolName, toolInput, and toolUseId', async () => {
125
- const registry = new HookRegistry();
126
- let captured: PreToolUseHookInput | undefined;
127
- const hook: HookCallback<'PreToolUse'> = async (
128
- input
129
- ): Promise<PreToolUseHookOutput> => {
130
- captured = input;
131
- return {};
132
- };
133
- registry.register('PreToolUse', { hooks: [hook] });
134
-
135
- const tc = makeToolCall('world');
136
- const run = await createEventDrivenRun(registry);
137
- run.Graph!.overrideTestModel(['calling echo'], 5, [tc]);
138
- await run.processStream(
139
- { messages: [new HumanMessage('echo world')] },
140
- callerConfig
141
- );
142
-
143
- expect(captured).toBeDefined();
144
- expect(captured!.hook_event_name).toBe('PreToolUse');
145
- expect(captured!.toolName).toBe('echo');
146
- expect(captured!.toolInput).toEqual({ text: 'world' });
147
- expect(captured!.toolUseId).toBe(tc.id);
148
- });
149
-
150
- it('deny blocks tool execution and produces error ToolMessage', async () => {
151
- const registry = new HookRegistry();
152
- let toolExecuted = false;
153
- const denyHook: HookCallback<
154
- 'PreToolUse'
155
- > = async (): Promise<PreToolUseHookOutput> => ({
156
- decision: 'deny',
157
- reason: 'not allowed',
158
- });
159
- registry.register('PreToolUse', {
160
- pattern: '^echo$',
161
- hooks: [denyHook],
162
- });
163
-
164
- const spyHandler: t.EventHandler = {
165
- handle: async (_event: string, rawData: unknown): Promise<void> => {
166
- const data = rawData as t.ToolExecuteBatchRequest;
167
- toolExecuted = true;
168
- data.resolve(
169
- data.toolCalls.map((tc: t.ToolCallRequest) => ({
170
- toolCallId: tc.id,
171
- content: 'should not reach',
172
- status: 'success' as const,
173
- }))
174
- );
175
- },
176
- };
177
-
178
- const run = await createEventDrivenRun(registry, spyHandler);
179
- run.Graph!.overrideTestModel(['calling echo'], 5, [makeToolCall()]);
180
- await run.processStream(
181
- { messages: [new HumanMessage('echo hello')] },
182
- callerConfig
183
- );
184
-
185
- expect(toolExecuted).toBe(false);
186
- });
187
-
188
- it('deny dispatches ON_RUN_STEP_COMPLETED for the blocked call', async () => {
189
- const registry = new HookRegistry();
190
- const denyHook: HookCallback<
191
- 'PreToolUse'
192
- > = async (): Promise<PreToolUseHookOutput> => ({
193
- decision: 'deny',
194
- reason: 'not allowed',
195
- });
196
- registry.register('PreToolUse', {
197
- pattern: '^echo$',
198
- hooks: [denyHook],
199
- });
200
-
201
- let stepCompletedData: t.ToolCompleteEvent | undefined;
202
- const stepHandler: t.EventHandler = {
203
- handle: async (_event: string, rawData: unknown): Promise<void> => {
204
- const data = rawData as { result: t.ToolCompleteEvent };
205
- stepCompletedData = data.result;
206
- },
207
- };
208
-
209
- const toolHandler = createToolExecuteHandler();
210
- const customHandlers: Record<string, t.EventHandler> = {
211
- [GraphEvents.ON_TOOL_EXECUTE]: toolHandler,
212
- [GraphEvents.TOOL_END]: new ToolEndHandler(),
213
- [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
214
- [GraphEvents.ON_RUN_STEP_COMPLETED]: stepHandler,
215
- };
216
-
217
- const tc = makeToolCall('hello');
218
- const run = await Run.create<t.IState>({
219
- runId: 'deny-step-run',
220
- graphConfig: {
221
- type: 'standard',
222
- llmConfig,
223
- toolDefinitions: [echoToolDef],
224
- instructions: 'Use the echo tool when asked.',
225
- },
226
- returnContent: true,
227
- skipCleanup: true,
228
- customHandlers,
229
- hooks: registry,
230
- });
231
-
232
- run.Graph!.overrideTestModel(['calling echo'], 5, [tc]);
233
- await run.processStream(
234
- { messages: [new HumanMessage('echo hello')] },
235
- callerConfig
236
- );
237
-
238
- expect(stepCompletedData).toBeDefined();
239
- expect(stepCompletedData!.type).toBe('tool_call');
240
- expect(stepCompletedData!.tool_call.name).toBe('echo');
241
- expect(stepCompletedData!.tool_call.id).toBe(tc.id);
242
- expect(stepCompletedData!.tool_call.output).toContain('Blocked:');
243
- });
244
-
245
- it('ask blocks tool execution in v1 (same as deny)', async () => {
246
- const registry = new HookRegistry();
247
- let toolExecuted = false;
248
- const askHook: HookCallback<
249
- 'PreToolUse'
250
- > = async (): Promise<PreToolUseHookOutput> => ({
251
- decision: 'ask',
252
- reason: 'needs confirmation',
253
- });
254
- registry.register('PreToolUse', { hooks: [askHook] });
255
-
256
- const spyHandler: t.EventHandler = {
257
- handle: async (_event: string, rawData: unknown): Promise<void> => {
258
- const data = rawData as t.ToolExecuteBatchRequest;
259
- toolExecuted = true;
260
- data.resolve(
261
- data.toolCalls.map((tc: t.ToolCallRequest) => ({
262
- toolCallId: tc.id,
263
- content: 'x',
264
- status: 'success' as const,
265
- }))
266
- );
267
- },
268
- };
269
-
270
- const run = await createEventDrivenRun(registry, spyHandler);
271
- run.Graph!.overrideTestModel(['calling echo'], 5, [makeToolCall()]);
272
- await run.processStream(
273
- { messages: [new HumanMessage('echo hello')] },
274
- callerConfig
275
- );
276
-
277
- expect(toolExecuted).toBe(false);
278
- });
279
-
280
- it('updatedInput rewrites tool args before dispatch', async () => {
281
- const registry = new HookRegistry();
282
- let receivedArgs: Record<string, unknown> | undefined;
283
- const rewriteHook: HookCallback<
284
- 'PreToolUse'
285
- > = async (): Promise<PreToolUseHookOutput> => ({
286
- updatedInput: { text: 'sanitized' },
287
- });
288
- registry.register('PreToolUse', { hooks: [rewriteHook] });
289
-
290
- const captureHandler: t.EventHandler = {
291
- handle: async (_event: string, rawData: unknown): Promise<void> => {
292
- const data = rawData as t.ToolExecuteBatchRequest;
293
- receivedArgs = data.toolCalls[0]?.args;
294
- data.resolve(
295
- data.toolCalls.map((tc: t.ToolCallRequest) => ({
296
- toolCallId: tc.id,
297
- content: `echo: ${(tc.args as Record<string, string>).text}`,
298
- status: 'success' as const,
299
- }))
300
- );
301
- },
302
- };
303
-
304
- const run = await createEventDrivenRun(registry, captureHandler);
305
- run.Graph!.overrideTestModel(['calling echo'], 5, [
306
- makeToolCall('dangerous'),
307
- ]);
308
- await run.processStream(
309
- { messages: [new HumanMessage('echo')] },
310
- callerConfig
311
- );
312
-
313
- expect(receivedArgs).toEqual({ text: 'sanitized' });
314
- });
315
-
316
- it('hook errors are non-fatal — tool still executes', async () => {
317
- const registry = new HookRegistry();
318
- let toolExecuted = false;
319
- const throwingHook: HookCallback<
320
- 'PreToolUse'
321
- > = async (): Promise<PreToolUseHookOutput> => {
322
- throw new Error('hook crash');
323
- };
324
- registry.register('PreToolUse', { hooks: [throwingHook] });
325
-
326
- const spyHandler: t.EventHandler = {
327
- handle: async (_event: string, rawData: unknown): Promise<void> => {
328
- const data = rawData as t.ToolExecuteBatchRequest;
329
- toolExecuted = true;
330
- data.resolve(
331
- data.toolCalls.map((tc: t.ToolCallRequest) => ({
332
- toolCallId: tc.id,
333
- content: 'ok',
334
- status: 'success' as const,
335
- }))
336
- );
337
- },
338
- };
339
-
340
- const run = await createEventDrivenRun(registry, spyHandler);
341
- run.Graph!.overrideTestModel(['calling echo'], 5, [makeToolCall()]);
342
- await run.processStream(
343
- { messages: [new HumanMessage('echo')] },
344
- callerConfig
345
- );
346
-
347
- expect(toolExecuted).toBe(true);
348
- });
349
- });
350
-
351
- describe('PermissionDenied', () => {
352
- it('fires after PreToolUse deny with the reason', async () => {
353
- const registry = new HookRegistry();
354
- let pdResolve: () => void;
355
- const pdDone = new Promise<void>((r) => {
356
- pdResolve = r;
357
- });
358
- let captured: PermissionDeniedHookInput | undefined;
359
- const denyHook: HookCallback<
360
- 'PreToolUse'
361
- > = async (): Promise<PreToolUseHookOutput> => ({
362
- decision: 'deny',
363
- reason: 'security policy',
364
- });
365
- const pdHook: HookCallback<'PermissionDenied'> = async (
366
- input
367
- ): Promise<PermissionDeniedHookOutput> => {
368
- captured = input;
369
- pdResolve();
370
- return {};
371
- };
372
- registry.register('PreToolUse', { hooks: [denyHook] });
373
- registry.register('PermissionDenied', { hooks: [pdHook] });
374
-
375
- const run = await createEventDrivenRun(registry);
376
- run.Graph!.overrideTestModel(['calling echo'], 5, [makeToolCall()]);
377
- await run.processStream(
378
- { messages: [new HumanMessage('echo')] },
379
- callerConfig
380
- );
381
-
382
- await pdDone;
383
- expect(captured).toBeDefined();
384
- expect(captured!.reason).toBe('security policy');
385
- expect(captured!.toolName).toBe('echo');
386
- });
387
- });
388
-
389
- describe('PostToolUse', () => {
390
- it('fires after successful tool execution with output', async () => {
391
- const registry = new HookRegistry();
392
- let captured: PostToolUseHookInput | undefined;
393
- const hook: HookCallback<'PostToolUse'> = async (
394
- input
395
- ): Promise<PostToolUseHookOutput> => {
396
- captured = input;
397
- return {};
398
- };
399
- registry.register('PostToolUse', { hooks: [hook] });
400
-
401
- const run = await createEventDrivenRun(registry);
402
- run.Graph!.overrideTestModel(['calling echo'], 5, [makeToolCall('hi')]);
403
- await run.processStream(
404
- { messages: [new HumanMessage('echo hi')] },
405
- callerConfig
406
- );
407
-
408
- expect(captured).toBeDefined();
409
- expect(captured!.hook_event_name).toBe('PostToolUse');
410
- expect(captured!.toolName).toBe('echo');
411
- expect(captured!.toolOutput).toBe('echo: hi');
412
- });
413
-
414
- it('updatedOutput replaces the ToolMessage content', async () => {
415
- const registry = new HookRegistry();
416
- const replaceHook: HookCallback<
417
- 'PostToolUse'
418
- > = async (): Promise<PostToolUseHookOutput> => ({
419
- updatedOutput: 'REDACTED',
420
- });
421
- registry.register('PostToolUse', { hooks: [replaceHook] });
422
-
423
- let resolvedContent: string | undefined;
424
- const captureHandler: t.EventHandler = {
425
- handle: async (_event: string, rawData: unknown): Promise<void> => {
426
- const data = rawData as t.ToolExecuteBatchRequest;
427
- const results = data.toolCalls.map(
428
- (tc: t.ToolCallRequest): t.ToolExecuteResult => ({
429
- toolCallId: tc.id,
430
- content: 'original secret output',
431
- status: 'success' as const,
432
- })
433
- );
434
- data.resolve(results);
435
- },
436
- };
437
-
438
- const run = await createEventDrivenRun(registry, captureHandler);
439
- run.Graph!.overrideTestModel(['calling echo'], 5, [makeToolCall()]);
440
- await run.processStream(
441
- { messages: [new HumanMessage('echo')] },
442
- callerConfig
443
- );
444
-
445
- const messages = run.Graph!.getRunMessages() ?? [];
446
- const toolMsg = messages.find((m) => m.getType() === 'tool');
447
- expect(toolMsg).toBeDefined();
448
- if (toolMsg != null) {
449
- resolvedContent =
450
- typeof toolMsg.content === 'string'
451
- ? toolMsg.content
452
- : JSON.stringify(toolMsg.content);
453
- }
454
-
455
- expect(resolvedContent).toBe('REDACTED');
456
- });
457
- });
458
-
459
- describe('PostToolUseFailure', () => {
460
- it('fires when tool execution returns an error', async () => {
461
- const registry = new HookRegistry();
462
- let captured: PostToolUseFailureHookInput | undefined;
463
- const hook: HookCallback<'PostToolUseFailure'> = async (
464
- input
465
- ): Promise<PostToolUseFailureHookOutput> => {
466
- captured = input;
467
- return {};
468
- };
469
- registry.register('PostToolUseFailure', { hooks: [hook] });
470
-
471
- const run = await createEventDrivenRun(
472
- registry,
473
- createErrorToolExecuteHandler()
474
- );
475
- run.Graph!.overrideTestModel(['calling echo'], 5, [makeToolCall()]);
476
- await run.processStream(
477
- { messages: [new HumanMessage('echo')] },
478
- callerConfig
479
- );
480
-
481
- expect(captured).toBeDefined();
482
- expect(captured!.hook_event_name).toBe('PostToolUseFailure');
483
- expect(captured!.toolName).toBe('echo');
484
- expect(captured!.error).toContain('failed deliberately');
485
- });
486
- });
487
-
488
- describe('multi-call batch', () => {
489
- const mathToolDef: t.LCTool = {
490
- name: 'math',
491
- description: 'Does math',
492
- parameters: {
493
- type: 'object' as const,
494
- properties: { expr: { type: 'string' } },
495
- required: ['expr'],
496
- },
497
- };
498
-
499
- function createMultiToolRun(
500
- hooks: HookRegistry,
501
- runId = 'multi-run'
502
- ): Promise<Run<t.IState>> {
503
- const handler: t.EventHandler = {
504
- handle: async (_event: string, rawData: unknown): Promise<void> => {
505
- const data = rawData as t.ToolExecuteBatchRequest;
506
- data.resolve(
507
- data.toolCalls.map(
508
- (tc: t.ToolCallRequest): t.ToolExecuteResult => ({
509
- toolCallId: tc.id,
510
- content: `${tc.name}: ok`,
511
- status: 'success' as const,
512
- })
513
- )
514
- );
515
- },
516
- };
517
- return Run.create<t.IState>({
518
- runId,
519
- graphConfig: {
520
- type: 'standard',
521
- llmConfig,
522
- toolDefinitions: [echoToolDef, mathToolDef],
523
- instructions: 'Use tools.',
524
- },
525
- returnContent: true,
526
- skipCleanup: true,
527
- customHandlers: {
528
- [GraphEvents.ON_TOOL_EXECUTE]: handler,
529
- [GraphEvents.TOOL_END]: new ToolEndHandler(),
530
- [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
531
- },
532
- hooks,
533
- });
534
- }
535
-
536
- it('partial deny: denied call produces error, approved call executes, order preserved', async () => {
537
- const registry = new HookRegistry();
538
- const denyEcho: HookCallback<'PreToolUse'> = async (
539
- input
540
- ): Promise<PreToolUseHookOutput> =>
541
- input.toolName === 'echo'
542
- ? { decision: 'deny', reason: 'echo blocked' }
543
- : {};
544
- registry.register('PreToolUse', { hooks: [denyEcho] });
545
-
546
- const echoCall = makeToolCall('hi', 'echo');
547
- const mathCall = makeToolCall('1+1', 'math');
548
- const run = await createMultiToolRun(registry);
549
- run.Graph!.overrideTestModel(['calling tools'], 5, [echoCall, mathCall]);
550
- await run.processStream(
551
- { messages: [new HumanMessage('do both')] },
552
- callerConfig
553
- );
554
-
555
- const messages = run.Graph!.getRunMessages() ?? [];
556
- const toolMsgs = messages.filter((m) => m.getType() === 'tool');
557
-
558
- expect(toolMsgs).toHaveLength(2);
559
- const first = toolMsgs[0];
560
- const second = toolMsgs[1];
561
- expect(first.content).toContain('Blocked');
562
- expect(second.content).toContain('math: ok');
563
- });
564
-
565
- it('all denied: no ON_TOOL_EXECUTE dispatch, all error messages', async () => {
566
- const registry = new HookRegistry();
567
- let handlerCalled = false;
568
- const denyAll: HookCallback<
569
- 'PreToolUse'
570
- > = async (): Promise<PreToolUseHookOutput> => ({
571
- decision: 'deny',
572
- reason: 'all blocked',
573
- });
574
- registry.register('PreToolUse', { hooks: [denyAll] });
575
-
576
- const handler: t.EventHandler = {
577
- handle: async (_event: string, rawData: unknown): Promise<void> => {
578
- handlerCalled = true;
579
- const data = rawData as t.ToolExecuteBatchRequest;
580
- data.resolve([]);
581
- },
582
- };
583
-
584
- const run = await Run.create<t.IState>({
585
- runId: 'all-denied-run',
586
- graphConfig: {
587
- type: 'standard',
588
- llmConfig,
589
- toolDefinitions: [echoToolDef, mathToolDef],
590
- instructions: 'Use tools.',
591
- },
592
- returnContent: true,
593
- skipCleanup: true,
594
- customHandlers: {
595
- [GraphEvents.ON_TOOL_EXECUTE]: handler,
596
- [GraphEvents.TOOL_END]: new ToolEndHandler(),
597
- [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
598
- },
599
- hooks: registry,
600
- });
601
- run.Graph!.overrideTestModel(['calling tools'], 5, [
602
- makeToolCall('a', 'echo'),
603
- makeToolCall('b', 'math'),
604
- ]);
605
- await run.processStream(
606
- { messages: [new HumanMessage('do both')] },
607
- callerConfig
608
- );
609
-
610
- expect(handlerCalled).toBe(false);
611
- });
612
- });
613
-
614
- describe('PostToolUse error resilience', () => {
615
- it('PostToolUse hook errors are non-fatal — original output preserved', async () => {
616
- const registry = new HookRegistry();
617
- const throwingHook: HookCallback<
618
- 'PostToolUse'
619
- > = async (): Promise<PostToolUseHookOutput> => {
620
- throw new Error('post hook crash');
621
- };
622
- registry.register('PostToolUse', { hooks: [throwingHook] });
623
-
624
- const run = await createEventDrivenRun(registry);
625
- run.Graph!.overrideTestModel(['calling echo'], 5, [makeToolCall('hi')]);
626
- await run.processStream(
627
- { messages: [new HumanMessage('echo hi')] },
628
- callerConfig
629
- );
630
-
631
- const messages = run.Graph!.getRunMessages() ?? [];
632
- const toolMsg = messages.find((m) => m.getType() === 'tool');
633
- expect(toolMsg).toBeDefined();
634
- const content =
635
- typeof toolMsg!.content === 'string'
636
- ? toolMsg!.content
637
- : JSON.stringify(toolMsg!.content);
638
- expect(content).toContain('echo: hi');
639
- });
640
- });
641
-
642
- describe('no-hooks baseline', () => {
643
- it('event-driven tool execution works identically without hooks', async () => {
644
- const run = await Run.create<t.IState>({
645
- runId: 'no-hooks-tool-run',
646
- graphConfig: {
647
- type: 'standard',
648
- llmConfig,
649
- toolDefinitions: [echoToolDef],
650
- instructions: 'Use echo.',
651
- },
652
- returnContent: true,
653
- skipCleanup: true,
654
- customHandlers: {
655
- [GraphEvents.ON_TOOL_EXECUTE]: createToolExecuteHandler(),
656
- [GraphEvents.TOOL_END]: new ToolEndHandler(),
657
- [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
658
- },
659
- });
660
- run.Graph!.overrideTestModel(['calling echo'], 5, [makeToolCall('test')]);
661
- const result = await run.processStream(
662
- { messages: [new HumanMessage('echo test')] },
663
- callerConfig
664
- );
665
-
666
- expect(result).toBeDefined();
667
- });
668
- });
669
- });