@librechat/agents 3.1.66-dev.0 → 3.1.67
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.
- package/dist/cjs/agents/AgentContext.cjs +24 -15
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +0 -13
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +0 -3
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +0 -40
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +12 -74
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +0 -111
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +140 -304
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +24 -15
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +1 -12
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +0 -3
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -10
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +4 -66
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +0 -111
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +142 -306
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +6 -0
- package/dist/types/common/enum.d.ts +1 -7
- package/dist/types/graphs/Graph.d.ts +0 -2
- package/dist/types/index.d.ts +0 -6
- package/dist/types/messages/format.d.ts +1 -2
- package/dist/types/run.d.ts +0 -1
- package/dist/types/tools/ToolNode.d.ts +2 -24
- package/dist/types/types/index.d.ts +0 -1
- package/dist/types/types/llm.d.ts +14 -2
- package/dist/types/types/run.d.ts +0 -20
- package/dist/types/types/tools.d.ts +1 -38
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +28 -15
- package/src/agents/__tests__/AgentContext.test.ts +110 -0
- package/src/common/enum.ts +0 -12
- package/src/graphs/Graph.ts +0 -4
- package/src/index.ts +0 -8
- package/src/messages/format.ts +4 -74
- package/src/run.ts +0 -126
- package/src/tools/ToolNode.ts +169 -391
- package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
- package/src/types/index.ts +0 -1
- package/src/types/llm.ts +16 -2
- package/src/types/run.ts +0 -20
- package/src/types/tools.ts +1 -41
- package/dist/cjs/hooks/HookRegistry.cjs +0 -162
- package/dist/cjs/hooks/HookRegistry.cjs.map +0 -1
- package/dist/cjs/hooks/executeHooks.cjs +0 -276
- package/dist/cjs/hooks/executeHooks.cjs.map +0 -1
- package/dist/cjs/hooks/matchers.cjs +0 -256
- package/dist/cjs/hooks/matchers.cjs.map +0 -1
- package/dist/cjs/hooks/types.cjs +0 -27
- package/dist/cjs/hooks/types.cjs.map +0 -1
- package/dist/cjs/tools/BashExecutor.cjs +0 -175
- package/dist/cjs/tools/BashExecutor.cjs.map +0 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +0 -296
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +0 -1
- package/dist/cjs/tools/ReadFile.cjs +0 -43
- package/dist/cjs/tools/ReadFile.cjs.map +0 -1
- package/dist/cjs/tools/SkillTool.cjs +0 -50
- package/dist/cjs/tools/SkillTool.cjs.map +0 -1
- package/dist/cjs/tools/skillCatalog.cjs +0 -84
- package/dist/cjs/tools/skillCatalog.cjs.map +0 -1
- package/dist/esm/hooks/HookRegistry.mjs +0 -160
- package/dist/esm/hooks/HookRegistry.mjs.map +0 -1
- package/dist/esm/hooks/executeHooks.mjs +0 -273
- package/dist/esm/hooks/executeHooks.mjs.map +0 -1
- package/dist/esm/hooks/matchers.mjs +0 -251
- package/dist/esm/hooks/matchers.mjs.map +0 -1
- package/dist/esm/hooks/types.mjs +0 -25
- package/dist/esm/hooks/types.mjs.map +0 -1
- package/dist/esm/tools/BashExecutor.mjs +0 -169
- package/dist/esm/tools/BashExecutor.mjs.map +0 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +0 -287
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +0 -1
- package/dist/esm/tools/ReadFile.mjs +0 -38
- package/dist/esm/tools/ReadFile.mjs.map +0 -1
- package/dist/esm/tools/SkillTool.mjs +0 -45
- package/dist/esm/tools/SkillTool.mjs.map +0 -1
- package/dist/esm/tools/skillCatalog.mjs +0 -82
- package/dist/esm/tools/skillCatalog.mjs.map +0 -1
- package/dist/types/hooks/HookRegistry.d.ts +0 -56
- package/dist/types/hooks/executeHooks.d.ts +0 -79
- package/dist/types/hooks/index.d.ts +0 -6
- package/dist/types/hooks/matchers.d.ts +0 -95
- package/dist/types/hooks/types.d.ts +0 -309
- package/dist/types/tools/BashExecutor.d.ts +0 -45
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +0 -72
- package/dist/types/tools/ReadFile.d.ts +0 -28
- package/dist/types/tools/SkillTool.d.ts +0 -40
- package/dist/types/tools/skillCatalog.d.ts +0 -19
- package/dist/types/types/skill.d.ts +0 -9
- package/src/hooks/HookRegistry.ts +0 -208
- package/src/hooks/__tests__/HookRegistry.test.ts +0 -190
- package/src/hooks/__tests__/executeHooks.test.ts +0 -1013
- package/src/hooks/__tests__/integration.test.ts +0 -337
- package/src/hooks/__tests__/matchers.test.ts +0 -238
- package/src/hooks/__tests__/toolHooks.test.ts +0 -669
- package/src/hooks/executeHooks.ts +0 -375
- package/src/hooks/index.ts +0 -55
- package/src/hooks/matchers.ts +0 -280
- package/src/hooks/types.ts +0 -388
- package/src/messages/formatAgentMessages.skills.test.ts +0 -334
- package/src/tools/BashExecutor.ts +0 -205
- package/src/tools/BashProgrammaticToolCalling.ts +0 -397
- package/src/tools/ReadFile.ts +0 -39
- package/src/tools/SkillTool.ts +0 -46
- package/src/tools/__tests__/ReadFile.test.ts +0 -44
- package/src/tools/__tests__/SkillTool.test.ts +0 -442
- package/src/tools/__tests__/skillCatalog.test.ts +0 -161
- package/src/tools/skillCatalog.ts +0 -126
- 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
|
-
});
|