@librechat/agents 3.1.88 → 3.1.90
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/graphs/Graph.cjs +25 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +14 -7
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +8 -2
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +34 -0
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/main.cjs +9 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/stream.cjs +115 -8
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +10 -9
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +12 -8
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +35 -11
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeSessionFileSummary.cjs +63 -0
- package/dist/cjs/tools/CodeSessionFileSummary.cjs.map +1 -0
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +16 -12
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +32 -12
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +319 -29
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +8 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/events.cjs +3 -1
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +25 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/executeHooks.mjs +14 -7
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +9 -3
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/stream.mjs +115 -8
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +11 -10
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +13 -9
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +29 -12
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeSessionFileSummary.mjs +60 -0
- package/dist/esm/tools/CodeSessionFileSummary.mjs.map +1 -0
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +17 -13
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +32 -12
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +320 -31
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +8 -1
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/events.mjs +3 -1
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +8 -0
- package/dist/types/llm/anthropic/index.d.ts +3 -1
- package/dist/types/llm/anthropic/utils/message_inputs.d.ts +4 -0
- package/dist/types/tools/BashExecutor.d.ts +3 -3
- package/dist/types/tools/CodeExecutor.d.ts +10 -3
- package/dist/types/tools/CodeSessionFileSummary.d.ts +3 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -4
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +8 -5
- package/dist/types/types/tools.d.ts +11 -3
- package/dist/types/utils/events.d.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +1073 -221
- package/src/graphs/Graph.ts +27 -5
- package/src/hooks/__tests__/executeHooks.test.ts +38 -0
- package/src/hooks/executeHooks.ts +27 -7
- package/src/llm/anthropic/index.ts +27 -3
- package/src/llm/anthropic/llm.spec.ts +60 -1
- package/src/llm/anthropic/utils/message_inputs.ts +46 -0
- package/src/specs/subagent.test.ts +87 -1
- package/src/stream.ts +163 -12
- package/src/tools/BashExecutor.ts +21 -10
- package/src/tools/BashProgrammaticToolCalling.ts +21 -9
- package/src/tools/CodeExecutor.ts +55 -12
- package/src/tools/CodeSessionFileSummary.ts +80 -0
- package/src/tools/ProgrammaticToolCalling.ts +25 -12
- package/src/tools/ToolNode.ts +142 -116
- package/src/tools/__tests__/BashExecutor.test.ts +9 -0
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +43 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +100 -16
- package/src/tools/__tests__/SubagentExecutor.test.ts +540 -6
- package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +278 -14
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +52 -0
- package/src/tools/__tests__/subagentHooks.test.ts +237 -0
- package/src/tools/subagent/SubagentExecutor.ts +514 -36
- package/src/types/tools.ts +11 -3
- package/src/utils/events.ts +4 -2
|
@@ -117,6 +117,276 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
117
117
|
expect(result.messages[0].content).toBe('eager result');
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
it('uses a prestarted event result when final args are canonically equivalent', async () => {
|
|
121
|
+
const { toolExecuteCalls } = installToolExecuteResponder('redispatched');
|
|
122
|
+
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
123
|
+
const request: t.ToolCallRequest = {
|
|
124
|
+
id: 'call_weather',
|
|
125
|
+
name: 'weather',
|
|
126
|
+
args: { city: 'NYC', units: 'metric' },
|
|
127
|
+
stepId: 'step_weather',
|
|
128
|
+
turn: 0,
|
|
129
|
+
};
|
|
130
|
+
eagerExecutions.set('call_weather', {
|
|
131
|
+
toolCallId: 'call_weather',
|
|
132
|
+
toolName: 'weather',
|
|
133
|
+
args: { city: 'NYC', units: 'metric' },
|
|
134
|
+
request,
|
|
135
|
+
promise: Promise.resolve({
|
|
136
|
+
results: [
|
|
137
|
+
{
|
|
138
|
+
toolCallId: 'call_weather',
|
|
139
|
+
status: 'success',
|
|
140
|
+
content: 'eager result',
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const toolNode = new ToolNode({
|
|
147
|
+
tools: [createDummyTool('weather')],
|
|
148
|
+
eventDrivenMode: true,
|
|
149
|
+
eagerEventToolExecution: { enabled: true },
|
|
150
|
+
eagerEventToolExecutions: eagerExecutions,
|
|
151
|
+
toolCallStepIds: new Map([['call_weather', 'step_weather']]),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = (await toolNode.invoke({
|
|
155
|
+
messages: [
|
|
156
|
+
createAIMessage('call_weather', 'weather', {
|
|
157
|
+
units: 'metric',
|
|
158
|
+
city: 'NYC',
|
|
159
|
+
}),
|
|
160
|
+
],
|
|
161
|
+
})) as { messages: ToolMessage[] };
|
|
162
|
+
|
|
163
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
164
|
+
expect(eagerExecutions.has('call_weather')).toBe(false);
|
|
165
|
+
expect(result.messages).toHaveLength(1);
|
|
166
|
+
expect(result.messages[0].content).toBe('eager result');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('uses a prestarted event result when only final planning turn differs', async () => {
|
|
170
|
+
const { toolExecuteCalls } = installToolExecuteResponder('redispatched');
|
|
171
|
+
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
172
|
+
const eagerUsageCount = new Map<string, number>([['weather', 1]]);
|
|
173
|
+
const request: t.ToolCallRequest = {
|
|
174
|
+
id: 'call_weather',
|
|
175
|
+
name: 'weather',
|
|
176
|
+
args: { city: 'NYC' },
|
|
177
|
+
stepId: 'step_weather',
|
|
178
|
+
turn: 7,
|
|
179
|
+
};
|
|
180
|
+
eagerExecutions.set('call_weather', {
|
|
181
|
+
toolCallId: 'call_weather',
|
|
182
|
+
toolName: 'weather',
|
|
183
|
+
args: { city: 'NYC' },
|
|
184
|
+
request,
|
|
185
|
+
promise: Promise.resolve({
|
|
186
|
+
results: [
|
|
187
|
+
{
|
|
188
|
+
toolCallId: 'call_weather',
|
|
189
|
+
status: 'success',
|
|
190
|
+
content: 'eager result',
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const toolNode = new ToolNode({
|
|
197
|
+
tools: [createDummyTool('weather')],
|
|
198
|
+
eventDrivenMode: true,
|
|
199
|
+
eagerEventToolExecution: { enabled: true },
|
|
200
|
+
eagerEventToolExecutions: eagerExecutions,
|
|
201
|
+
eagerEventToolUsageCount: eagerUsageCount,
|
|
202
|
+
toolCallStepIds: new Map([['call_weather', 'step_weather']]),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const result = (await toolNode.invoke({
|
|
206
|
+
messages: [createAIMessage('call_weather', 'weather', { city: 'NYC' })],
|
|
207
|
+
})) as { messages: ToolMessage[] };
|
|
208
|
+
|
|
209
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
210
|
+
expect(eagerExecutions.has('call_weather')).toBe(false);
|
|
211
|
+
expect(eagerUsageCount.get('weather')).toBe(1);
|
|
212
|
+
expect(result.messages).toHaveLength(1);
|
|
213
|
+
expect(result.messages[0].status).toBe('success');
|
|
214
|
+
expect(result.messages[0].content).toBe('eager result');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('redispatches completion when a prestarted event result used a stale turn', async () => {
|
|
218
|
+
const stepCompletions: Array<{
|
|
219
|
+
result?: { index?: number; tool_call?: { output?: string } };
|
|
220
|
+
}> = [];
|
|
221
|
+
jest
|
|
222
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
223
|
+
.mockImplementation(async (event, data): Promise<boolean | void> => {
|
|
224
|
+
if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
|
|
225
|
+
stepCompletions.push(data as (typeof stepCompletions)[number]);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
if (event === GraphEvents.ON_TOOL_EXECUTE) {
|
|
229
|
+
throw new Error('tool should not redispatch');
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
234
|
+
const eagerUsageCount = new Map<string, number>([['weather', 1]]);
|
|
235
|
+
const request: t.ToolCallRequest = {
|
|
236
|
+
id: 'call_weather',
|
|
237
|
+
name: 'weather',
|
|
238
|
+
args: { city: 'NYC' },
|
|
239
|
+
stepId: 'step_weather',
|
|
240
|
+
turn: 7,
|
|
241
|
+
};
|
|
242
|
+
eagerExecutions.set('call_weather', {
|
|
243
|
+
toolCallId: 'call_weather',
|
|
244
|
+
toolName: 'weather',
|
|
245
|
+
args: { city: 'NYC' },
|
|
246
|
+
request,
|
|
247
|
+
completionDispatched: true,
|
|
248
|
+
promise: Promise.resolve({
|
|
249
|
+
results: [
|
|
250
|
+
{
|
|
251
|
+
toolCallId: 'call_weather',
|
|
252
|
+
status: 'success',
|
|
253
|
+
content: 'eager result',
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
}),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const toolNode = new ToolNode({
|
|
260
|
+
tools: [createDummyTool('weather')],
|
|
261
|
+
eventDrivenMode: true,
|
|
262
|
+
eagerEventToolExecution: { enabled: true },
|
|
263
|
+
eagerEventToolExecutions: eagerExecutions,
|
|
264
|
+
eagerEventToolUsageCount: eagerUsageCount,
|
|
265
|
+
toolCallStepIds: new Map([['call_weather', 'step_weather']]),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const result = (await toolNode.invoke({
|
|
269
|
+
messages: [createAIMessage('call_weather', 'weather', { city: 'NYC' })],
|
|
270
|
+
})) as { messages: ToolMessage[] };
|
|
271
|
+
|
|
272
|
+
expect(stepCompletions).toHaveLength(1);
|
|
273
|
+
expect(stepCompletions[0].result?.index).toBe(0);
|
|
274
|
+
expect(stepCompletions[0].result?.tool_call?.output).toBe('eager result');
|
|
275
|
+
expect(result.messages).toHaveLength(1);
|
|
276
|
+
expect(result.messages[0].content).toBe('eager result');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('uses a matching prestarted event result when output references are enabled', async () => {
|
|
280
|
+
const { toolExecuteCalls } = installToolExecuteResponder('redispatched');
|
|
281
|
+
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
282
|
+
const eagerUsageCount = new Map<string, number>([['weather', 1]]);
|
|
283
|
+
const request: t.ToolCallRequest = {
|
|
284
|
+
id: 'call_weather',
|
|
285
|
+
name: 'weather',
|
|
286
|
+
args: { city: 'NYC' },
|
|
287
|
+
stepId: 'step_weather',
|
|
288
|
+
turn: 0,
|
|
289
|
+
};
|
|
290
|
+
eagerExecutions.set('call_weather', {
|
|
291
|
+
toolCallId: 'call_weather',
|
|
292
|
+
toolName: 'weather',
|
|
293
|
+
args: { city: 'NYC' },
|
|
294
|
+
request,
|
|
295
|
+
promise: Promise.resolve({
|
|
296
|
+
results: [
|
|
297
|
+
{
|
|
298
|
+
toolCallId: 'call_weather',
|
|
299
|
+
status: 'success',
|
|
300
|
+
content: 'eager result',
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
}),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const toolNode = new ToolNode({
|
|
307
|
+
tools: [createDummyTool('weather')],
|
|
308
|
+
eventDrivenMode: true,
|
|
309
|
+
eagerEventToolExecution: { enabled: true },
|
|
310
|
+
eagerEventToolExecutions: eagerExecutions,
|
|
311
|
+
eagerEventToolUsageCount: eagerUsageCount,
|
|
312
|
+
toolCallStepIds: new Map([['call_weather', 'step_weather']]),
|
|
313
|
+
toolOutputReferences: { enabled: true },
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const result = (await toolNode.invoke(
|
|
317
|
+
{
|
|
318
|
+
messages: [createAIMessage('call_weather', 'weather', { city: 'NYC' })],
|
|
319
|
+
},
|
|
320
|
+
{ configurable: { run_id: 'run_1' } }
|
|
321
|
+
)) as { messages: ToolMessage[] };
|
|
322
|
+
|
|
323
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
324
|
+
expect(eagerExecutions.has('call_weather')).toBe(false);
|
|
325
|
+
expect(result.messages).toHaveLength(1);
|
|
326
|
+
expect(result.messages[0].content).toBe('eager result');
|
|
327
|
+
expect(result.messages[0].additional_kwargs._refKey).toBe('tool0turn0');
|
|
328
|
+
expect(result.messages[0].additional_kwargs._refScope).toBe('run_1');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('does not redispatch completion when eager path already emitted it', async () => {
|
|
332
|
+
const completedEvents: Array<{ result: t.ToolEndEvent }> = [];
|
|
333
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
334
|
+
jest
|
|
335
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
336
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
337
|
+
if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
|
|
338
|
+
completedEvents.push(data as { result: t.ToolEndEvent });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
toolExecuteCalls.push(data as t.ToolExecuteBatchRequest);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
348
|
+
const request: t.ToolCallRequest = {
|
|
349
|
+
id: 'call_weather',
|
|
350
|
+
name: 'weather',
|
|
351
|
+
args: { city: 'NYC' },
|
|
352
|
+
stepId: 'step_weather',
|
|
353
|
+
turn: 0,
|
|
354
|
+
};
|
|
355
|
+
eagerExecutions.set('call_weather', {
|
|
356
|
+
toolCallId: 'call_weather',
|
|
357
|
+
toolName: 'weather',
|
|
358
|
+
args: { city: 'NYC' },
|
|
359
|
+
request,
|
|
360
|
+
completionDispatched: true,
|
|
361
|
+
promise: Promise.resolve({
|
|
362
|
+
results: [
|
|
363
|
+
{
|
|
364
|
+
toolCallId: 'call_weather',
|
|
365
|
+
status: 'success',
|
|
366
|
+
content: 'eager result',
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
}),
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const toolNode = new ToolNode({
|
|
373
|
+
tools: [createDummyTool('weather')],
|
|
374
|
+
eventDrivenMode: true,
|
|
375
|
+
eagerEventToolExecution: { enabled: true },
|
|
376
|
+
eagerEventToolExecutions: eagerExecutions,
|
|
377
|
+
eagerEventToolUsageCount: new Map([['weather', 1]]),
|
|
378
|
+
toolCallStepIds: new Map([['call_weather', 'step_weather']]),
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const result = (await toolNode.invoke({
|
|
382
|
+
messages: [createAIMessage('call_weather', 'weather', { city: 'NYC' })],
|
|
383
|
+
})) as { messages: ToolMessage[] };
|
|
384
|
+
|
|
385
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
386
|
+
expect(completedEvents).toHaveLength(0);
|
|
387
|
+
expect(result.messages[0].content).toBe('eager result');
|
|
388
|
+
});
|
|
389
|
+
|
|
120
390
|
it('fails closed without redispatching when final args differ from the prestarted call', async () => {
|
|
121
391
|
const { toolExecuteCalls } = installToolExecuteResponder('fresh result');
|
|
122
392
|
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
@@ -196,7 +466,7 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
196
466
|
const { toolExecuteCalls } = installToolExecuteResponder('normal result');
|
|
197
467
|
const eagerExecutions = new Map<string, t.EagerEventToolExecution>();
|
|
198
468
|
const eagerUsageCount = new Map<string, number>([['weather', 1]]);
|
|
199
|
-
const
|
|
469
|
+
const prestartedRequest: t.ToolCallRequest = {
|
|
200
470
|
id: 'call_weather_2',
|
|
201
471
|
name: 'weather',
|
|
202
472
|
args: { city: 'Boston' },
|
|
@@ -207,13 +477,13 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
207
477
|
toolCallId: 'call_weather_2',
|
|
208
478
|
toolName: 'weather',
|
|
209
479
|
args: { city: 'Boston' },
|
|
210
|
-
request:
|
|
480
|
+
request: prestartedRequest,
|
|
211
481
|
promise: Promise.resolve({
|
|
212
482
|
results: [
|
|
213
483
|
{
|
|
214
484
|
toolCallId: 'call_weather_2',
|
|
215
485
|
status: 'success',
|
|
216
|
-
content: '
|
|
486
|
+
content: 'prestarted eager result',
|
|
217
487
|
},
|
|
218
488
|
],
|
|
219
489
|
}),
|
|
@@ -263,8 +533,8 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
263
533
|
'call_weather_2',
|
|
264
534
|
]);
|
|
265
535
|
expect(result.messages[0].status).toBe('success');
|
|
266
|
-
expect(result.messages[1].status).toBe('
|
|
267
|
-
expect(result.messages[1].content).
|
|
536
|
+
expect(result.messages[1].status).toBe('success');
|
|
537
|
+
expect(result.messages[1].content).toBe('prestarted eager result');
|
|
268
538
|
});
|
|
269
539
|
|
|
270
540
|
it('returns a per-call error for malformed event tool args without aborting the batch', async () => {
|
|
@@ -314,9 +584,7 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
314
584
|
'call_weather_good',
|
|
315
585
|
]);
|
|
316
586
|
expect(result.messages[0].status).toBe('error');
|
|
317
|
-
expect(result.messages[0].content).toContain(
|
|
318
|
-
'Invalid tool call arguments'
|
|
319
|
-
);
|
|
587
|
+
expect(result.messages[0].content).toContain('Invalid tool call arguments');
|
|
320
588
|
expect(result.messages[1].status).toBe('success');
|
|
321
589
|
expect(result.messages[1].content).toBe('normal result');
|
|
322
590
|
});
|
|
@@ -328,9 +596,7 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
328
596
|
const hookRegistry = new HookRegistry();
|
|
329
597
|
hookRegistry.register('PreToolUse', {
|
|
330
598
|
hooks: [
|
|
331
|
-
async (
|
|
332
|
-
input: PreToolUseHookInput
|
|
333
|
-
): Promise<PreToolUseHookOutput> => {
|
|
599
|
+
async (input: PreToolUseHookInput): Promise<PreToolUseHookOutput> => {
|
|
334
600
|
preToolTurns.push(input.turn);
|
|
335
601
|
return { decision: 'allow' };
|
|
336
602
|
},
|
|
@@ -351,9 +617,7 @@ describe('ToolNode eager event tool execution', () => {
|
|
|
351
617
|
});
|
|
352
618
|
|
|
353
619
|
await toolNode.invoke({
|
|
354
|
-
messages: [
|
|
355
|
-
createAIMessage('call_weather_1', 'weather', { city: 'NYC' }),
|
|
356
|
-
],
|
|
620
|
+
messages: [createAIMessage('call_weather_1', 'weather', { city: 'NYC' })],
|
|
357
621
|
});
|
|
358
622
|
|
|
359
623
|
eagerUsageCount.clear();
|
|
@@ -204,6 +204,58 @@ describe('ToolNode tool output references', () => {
|
|
|
204
204
|
expect(capturedArgs).toEqual(['first', 'echo raw-payload']);
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
+
it('keeps generated-file summaries out of registered outputs', async () => {
|
|
208
|
+
const rawOutput = [
|
|
209
|
+
'stdout:',
|
|
210
|
+
'{"ok":true}',
|
|
211
|
+
'',
|
|
212
|
+
'Generated files:',
|
|
213
|
+
'Session files: 1 persisted file(s) are available in /mnt/data, including 0 image(s). Use known /mnt/data paths directly in later code-tool calls. The app displays files/images automatically; do not invent download links or wrap generated images in Markdown.',
|
|
214
|
+
].join('\n');
|
|
215
|
+
const cleanOutput = 'stdout:\n{"ok":true}';
|
|
216
|
+
const capturedArgs: string[] = [];
|
|
217
|
+
const filesTool = tool(
|
|
218
|
+
async () =>
|
|
219
|
+
new ToolMessage({
|
|
220
|
+
status: 'success',
|
|
221
|
+
content: rawOutput,
|
|
222
|
+
name: 'files',
|
|
223
|
+
tool_call_id: 'c1',
|
|
224
|
+
}),
|
|
225
|
+
{
|
|
226
|
+
name: 'files',
|
|
227
|
+
description: 'returns generated-file summary output',
|
|
228
|
+
schema: z.object({ command: z.string() }),
|
|
229
|
+
}
|
|
230
|
+
) as unknown as StructuredToolInterface;
|
|
231
|
+
const echo = createEchoTool({
|
|
232
|
+
capturedArgs,
|
|
233
|
+
outputs: ['resolved'],
|
|
234
|
+
});
|
|
235
|
+
const node = new ToolNode({
|
|
236
|
+
tools: [filesTool, echo],
|
|
237
|
+
toolOutputReferences: { enabled: true },
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const [msg] = await invokeBatch(node, [
|
|
241
|
+
{ id: 'c1', name: 'files', command: 'first' },
|
|
242
|
+
]);
|
|
243
|
+
await invokeBatch(node, [
|
|
244
|
+
{
|
|
245
|
+
id: 'c2',
|
|
246
|
+
name: 'echo',
|
|
247
|
+
command: 'echo {{tool0turn0}}',
|
|
248
|
+
},
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
expect(msg.content).toBe(rawOutput);
|
|
252
|
+
expect(getRefKey(msg)).toBe('tool0turn0');
|
|
253
|
+
expect(
|
|
254
|
+
node._unsafeGetToolOutputRegistry()!.get('test-run', 'tool0turn0')
|
|
255
|
+
).toBe(cleanOutput);
|
|
256
|
+
expect(capturedArgs).toEqual([`echo ${cleanOutput}`]);
|
|
257
|
+
});
|
|
258
|
+
|
|
207
259
|
it('increments the turn counter per ToolNode batch', async () => {
|
|
208
260
|
const capturedArgs: string[] = [];
|
|
209
261
|
const t1 = createEchoTool({
|
|
@@ -4,6 +4,9 @@ import type { ToolCall } from '@langchain/core/messages/tool';
|
|
|
4
4
|
import type * as t from '@/types';
|
|
5
5
|
import type {
|
|
6
6
|
HookCallback,
|
|
7
|
+
PermissionDeniedHookOutput,
|
|
8
|
+
PostToolUseHookOutput,
|
|
9
|
+
PreToolUseHookOutput,
|
|
7
10
|
SubagentStartHookInput,
|
|
8
11
|
SubagentStartHookOutput,
|
|
9
12
|
SubagentStopHookInput,
|
|
@@ -11,6 +14,7 @@ import type {
|
|
|
11
14
|
} from '@/hooks/types';
|
|
12
15
|
import { HookRegistry } from '@/hooks/HookRegistry';
|
|
13
16
|
import { Run } from '@/run';
|
|
17
|
+
import { FakeChatModel } from '@/llm/fake';
|
|
14
18
|
import {
|
|
15
19
|
Constants,
|
|
16
20
|
GraphEvents,
|
|
@@ -22,6 +26,18 @@ import * as providers from '@/llm/providers';
|
|
|
22
26
|
|
|
23
27
|
const CHILD_RESPONSE = 'Hook test child response.';
|
|
24
28
|
|
|
29
|
+
const calculatorDef: t.LCTool = {
|
|
30
|
+
name: 'calculator',
|
|
31
|
+
description: 'Evaluate a math expression.',
|
|
32
|
+
parameters: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
expression: { type: 'string' },
|
|
36
|
+
},
|
|
37
|
+
required: ['expression'],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
25
41
|
const callerConfig = {
|
|
26
42
|
configurable: { thread_id: 'hook-test-thread' },
|
|
27
43
|
streamMode: 'values' as const,
|
|
@@ -66,6 +82,40 @@ function createParentAgent(): t.AgentInputs {
|
|
|
66
82
|
};
|
|
67
83
|
}
|
|
68
84
|
|
|
85
|
+
function createParentAgentWithChildTool(): t.AgentInputs {
|
|
86
|
+
return {
|
|
87
|
+
agentId: 'hook-parent',
|
|
88
|
+
provider: Providers.OPENAI,
|
|
89
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
90
|
+
instructions: 'Delegate research tasks to subagents.',
|
|
91
|
+
maxContextTokens: 8000,
|
|
92
|
+
subagentConfigs: [
|
|
93
|
+
{
|
|
94
|
+
type: 'researcher',
|
|
95
|
+
name: 'Researcher',
|
|
96
|
+
description: 'Researches topics',
|
|
97
|
+
agentInputs: {
|
|
98
|
+
agentId: 'researcher-child',
|
|
99
|
+
provider: Providers.OPENAI,
|
|
100
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
101
|
+
instructions: 'Use calculator for arithmetic, then answer concisely.',
|
|
102
|
+
maxContextTokens: 8000,
|
|
103
|
+
toolDefinitions: [calculatorDef],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function createCalculatorToolCall(): ToolCall {
|
|
111
|
+
return {
|
|
112
|
+
name: 'calculator',
|
|
113
|
+
args: { expression: '21 * 2' },
|
|
114
|
+
id: 'call_child_calculator',
|
|
115
|
+
type: 'tool_call',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
69
119
|
async function createSubagentRun(
|
|
70
120
|
hooks: HookRegistry,
|
|
71
121
|
runId = `subagent-hook-${Date.now()}`
|
|
@@ -212,4 +262,191 @@ describe('Subagent hook integration (end-to-end via Run)', () => {
|
|
|
212
262
|
'Blocked: policy violation'
|
|
213
263
|
);
|
|
214
264
|
});
|
|
265
|
+
|
|
266
|
+
it('PreToolUse and PostToolUse fire for event-driven tools inside subagents', async () => {
|
|
267
|
+
getChatModelClassSpy.mockImplementation(((provider: Providers) => {
|
|
268
|
+
if (provider === Providers.OPENAI) {
|
|
269
|
+
return class extends FakeChatModel {
|
|
270
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
271
|
+
constructor(_options: any) {
|
|
272
|
+
super({
|
|
273
|
+
responses: ['Using calculator.', CHILD_RESPONSE],
|
|
274
|
+
sleep: 1,
|
|
275
|
+
toolCalls: [createCalculatorToolCall()],
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
bindTools(
|
|
279
|
+
tools: unknown
|
|
280
|
+
): ReturnType<FakeChatModel['withConfig']> {
|
|
281
|
+
const config = {
|
|
282
|
+
tools,
|
|
283
|
+
} as Parameters<FakeChatModel['withConfig']>[0];
|
|
284
|
+
return this.withConfig(config);
|
|
285
|
+
}
|
|
286
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
287
|
+
} as any;
|
|
288
|
+
}
|
|
289
|
+
return originalGetChatModelClass(provider);
|
|
290
|
+
}) as typeof providers.getChatModelClass);
|
|
291
|
+
|
|
292
|
+
const registry = new HookRegistry();
|
|
293
|
+
const preToolEvents: string[] = [];
|
|
294
|
+
const postToolEvents: string[] = [];
|
|
295
|
+
|
|
296
|
+
const preHook: HookCallback<'PreToolUse'> = async (
|
|
297
|
+
input
|
|
298
|
+
): Promise<PreToolUseHookOutput> => {
|
|
299
|
+
preToolEvents.push(`${input.agentId ?? '-'}:${input.toolName}`);
|
|
300
|
+
return { decision: 'allow' };
|
|
301
|
+
};
|
|
302
|
+
registry.register('PreToolUse', { hooks: [preHook] });
|
|
303
|
+
|
|
304
|
+
const postHook: HookCallback<'PostToolUse'> = async (
|
|
305
|
+
input
|
|
306
|
+
): Promise<PostToolUseHookOutput> => {
|
|
307
|
+
postToolEvents.push(`${input.agentId ?? '-'}:${input.toolName}`);
|
|
308
|
+
return {};
|
|
309
|
+
};
|
|
310
|
+
registry.register('PostToolUse', { hooks: [postHook] });
|
|
311
|
+
|
|
312
|
+
const customHandlers: Record<string, t.EventHandler> = {
|
|
313
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
|
314
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
|
|
315
|
+
[GraphEvents.ON_TOOL_EXECUTE]: {
|
|
316
|
+
handle: (_event, rawData): void => {
|
|
317
|
+
const request = rawData as t.ToolExecuteBatchRequest;
|
|
318
|
+
const results: t.ToolExecuteResult[] = request.toolCalls.map(
|
|
319
|
+
(call) => ({
|
|
320
|
+
toolCallId: call.id,
|
|
321
|
+
status: 'success',
|
|
322
|
+
content: '42',
|
|
323
|
+
})
|
|
324
|
+
);
|
|
325
|
+
request.resolve(results);
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const run = await Run.create<t.IState>({
|
|
331
|
+
runId: `subagent-tool-hook-${Date.now()}`,
|
|
332
|
+
graphConfig: {
|
|
333
|
+
type: 'standard',
|
|
334
|
+
agents: [createParentAgentWithChildTool()],
|
|
335
|
+
},
|
|
336
|
+
returnContent: true,
|
|
337
|
+
skipCleanup: true,
|
|
338
|
+
customHandlers,
|
|
339
|
+
hooks: registry,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const tc = makeSubagentToolCall();
|
|
343
|
+
run.Graph!.overrideTestModel(['Delegating...', 'Final answer.'], 5, [tc]);
|
|
344
|
+
|
|
345
|
+
await run.processStream(
|
|
346
|
+
{ messages: [new HumanMessage('calculate something')] },
|
|
347
|
+
callerConfig
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
expect(preToolEvents).toContain('-:subagent');
|
|
351
|
+
expect(preToolEvents).toContain('researcher-child:calculator');
|
|
352
|
+
expect(postToolEvents).toContain('-:subagent');
|
|
353
|
+
expect(postToolEvents).toContain('researcher-child:calculator');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('child subagent tool ask hooks fail closed instead of starting unsupported nested HITL', async () => {
|
|
357
|
+
getChatModelClassSpy.mockImplementation(((provider: Providers) => {
|
|
358
|
+
if (provider === Providers.OPENAI) {
|
|
359
|
+
return class extends FakeChatModel {
|
|
360
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
361
|
+
constructor(_options: any) {
|
|
362
|
+
super({
|
|
363
|
+
responses: ['Using calculator.', CHILD_RESPONSE],
|
|
364
|
+
sleep: 1,
|
|
365
|
+
toolCalls: [createCalculatorToolCall()],
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
bindTools(
|
|
369
|
+
tools: unknown
|
|
370
|
+
): ReturnType<FakeChatModel['withConfig']> {
|
|
371
|
+
const config = {
|
|
372
|
+
tools,
|
|
373
|
+
} as Parameters<FakeChatModel['withConfig']>[0];
|
|
374
|
+
return this.withConfig(config);
|
|
375
|
+
}
|
|
376
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
377
|
+
} as any;
|
|
378
|
+
}
|
|
379
|
+
return originalGetChatModelClass(provider);
|
|
380
|
+
}) as typeof providers.getChatModelClass);
|
|
381
|
+
|
|
382
|
+
const registry = new HookRegistry();
|
|
383
|
+
const deniedTools: string[] = [];
|
|
384
|
+
const executedTools: string[] = [];
|
|
385
|
+
|
|
386
|
+
const preHook: HookCallback<'PreToolUse'> = async (
|
|
387
|
+
input
|
|
388
|
+
): Promise<PreToolUseHookOutput> => {
|
|
389
|
+
if (input.toolName === 'calculator') {
|
|
390
|
+
return { decision: 'ask', reason: 'review calculator' };
|
|
391
|
+
}
|
|
392
|
+
return { decision: 'allow' };
|
|
393
|
+
};
|
|
394
|
+
registry.register('PreToolUse', { hooks: [preHook] });
|
|
395
|
+
|
|
396
|
+
const deniedHook: HookCallback<'PermissionDenied'> = async (
|
|
397
|
+
input
|
|
398
|
+
): Promise<PermissionDeniedHookOutput> => {
|
|
399
|
+
deniedTools.push(
|
|
400
|
+
`${input.agentId ?? '-'}:${input.toolName}:${input.reason}`
|
|
401
|
+
);
|
|
402
|
+
return {};
|
|
403
|
+
};
|
|
404
|
+
registry.register('PermissionDenied', { hooks: [deniedHook] });
|
|
405
|
+
|
|
406
|
+
const customHandlers: Record<string, t.EventHandler> = {
|
|
407
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
|
408
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
|
|
409
|
+
[GraphEvents.ON_TOOL_EXECUTE]: {
|
|
410
|
+
handle: (_event, rawData): void => {
|
|
411
|
+
const request = rawData as t.ToolExecuteBatchRequest;
|
|
412
|
+
executedTools.push(...request.toolCalls.map((call) => call.name));
|
|
413
|
+
const results: t.ToolExecuteResult[] = request.toolCalls.map(
|
|
414
|
+
(call) => ({
|
|
415
|
+
toolCallId: call.id,
|
|
416
|
+
status: 'success',
|
|
417
|
+
content: '42',
|
|
418
|
+
})
|
|
419
|
+
);
|
|
420
|
+
request.resolve(results);
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const run = await Run.create<t.IState>({
|
|
426
|
+
runId: `subagent-tool-ask-${Date.now()}`,
|
|
427
|
+
graphConfig: {
|
|
428
|
+
type: 'standard',
|
|
429
|
+
agents: [createParentAgentWithChildTool()],
|
|
430
|
+
},
|
|
431
|
+
returnContent: true,
|
|
432
|
+
skipCleanup: true,
|
|
433
|
+
customHandlers,
|
|
434
|
+
hooks: registry,
|
|
435
|
+
humanInTheLoop: { enabled: true },
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const tc = makeSubagentToolCall();
|
|
439
|
+
run.Graph!.overrideTestModel(['Delegating...', 'Final answer.'], 5, [tc]);
|
|
440
|
+
|
|
441
|
+
await run.processStream(
|
|
442
|
+
{ messages: [new HumanMessage('calculate something')] },
|
|
443
|
+
callerConfig
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
expect(run.getInterrupt()).toBeUndefined();
|
|
447
|
+
expect(deniedTools).toContain(
|
|
448
|
+
'researcher-child:calculator:review calculator'
|
|
449
|
+
);
|
|
450
|
+
expect(executedTools).not.toContain('calculator');
|
|
451
|
+
});
|
|
215
452
|
});
|