@librechat/agents 3.1.66 → 3.1.67-dev.4
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 +23 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +16 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +91 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +162 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
- package/dist/cjs/hooks/executeHooks.cjs +276 -0
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
- package/dist/cjs/hooks/matchers.cjs +256 -0
- package/dist/cjs/hooks/matchers.cjs.map +1 -0
- package/dist/cjs/hooks/types.cjs +27 -0
- package/dist/cjs/hooks/types.cjs.map +1 -0
- package/dist/cjs/main.cjs +53 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +74 -12
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +111 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +44 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +175 -0
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +296 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/ReadFile.cjs +43 -0
- package/dist/cjs/tools/ReadFile.cjs.map +1 -0
- package/dist/cjs/tools/SkillTool.cjs +50 -0
- package/dist/cjs/tools/SkillTool.cjs.map +1 -0
- package/dist/cjs/tools/SubagentTool.cjs +92 -0
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -0
- package/dist/cjs/tools/ToolNode.cjs +304 -140
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/skillCatalog.cjs +84 -0
- package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +511 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +23 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +15 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +91 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +160 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
- package/dist/esm/hooks/executeHooks.mjs +273 -0
- package/dist/esm/hooks/executeHooks.mjs.map +1 -0
- package/dist/esm/hooks/matchers.mjs +251 -0
- package/dist/esm/hooks/matchers.mjs.map +1 -0
- package/dist/esm/hooks/types.mjs +25 -0
- package/dist/esm/hooks/types.mjs.map +1 -0
- package/dist/esm/main.mjs +12 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +66 -4
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +111 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +44 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +169 -0
- package/dist/esm/tools/BashExecutor.mjs.map +1 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +287 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/ReadFile.mjs +38 -0
- package/dist/esm/tools/ReadFile.mjs.map +1 -0
- package/dist/esm/tools/SkillTool.mjs +45 -0
- package/dist/esm/tools/SkillTool.mjs.map +1 -0
- package/dist/esm/tools/SubagentTool.mjs +85 -0
- package/dist/esm/tools/SubagentTool.mjs.map +1 -0
- package/dist/esm/tools/ToolNode.mjs +306 -142
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/skillCatalog.mjs +82 -0
- package/dist/esm/tools/skillCatalog.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +505 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +6 -0
- package/dist/types/common/enum.d.ts +10 -1
- package/dist/types/graphs/Graph.d.ts +2 -0
- package/dist/types/hooks/HookRegistry.d.ts +56 -0
- package/dist/types/hooks/executeHooks.d.ts +79 -0
- package/dist/types/hooks/index.d.ts +6 -0
- package/dist/types/hooks/matchers.d.ts +95 -0
- package/dist/types/hooks/types.d.ts +320 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/messages/format.d.ts +2 -1
- package/dist/types/run.d.ts +1 -0
- package/dist/types/summarization/node.d.ts +2 -0
- package/dist/types/tools/BashExecutor.d.ts +45 -0
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
- package/dist/types/tools/ReadFile.d.ts +28 -0
- package/dist/types/tools/SkillTool.d.ts +40 -0
- package/dist/types/tools/SubagentTool.d.ts +36 -0
- package/dist/types/tools/ToolNode.d.ts +24 -2
- package/dist/types/tools/skillCatalog.d.ts +19 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +137 -0
- package/dist/types/tools/subagent/index.d.ts +2 -0
- package/dist/types/types/graph.d.ts +61 -2
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/llm.d.ts +14 -2
- package/dist/types/types/run.d.ts +20 -0
- package/dist/types/types/skill.d.ts +9 -0
- package/dist/types/types/tools.d.ts +38 -1
- package/package.json +5 -1
- package/src/agents/AgentContext.ts +26 -2
- package/src/common/enum.ts +15 -0
- package/src/graphs/Graph.ts +113 -0
- package/src/hooks/HookRegistry.ts +208 -0
- package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
- package/src/hooks/__tests__/compactHooks.test.ts +214 -0
- package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
- package/src/hooks/__tests__/integration.test.ts +337 -0
- package/src/hooks/__tests__/matchers.test.ts +238 -0
- package/src/hooks/__tests__/toolHooks.test.ts +669 -0
- package/src/hooks/executeHooks.ts +375 -0
- package/src/hooks/index.ts +57 -0
- package/src/hooks/matchers.ts +280 -0
- package/src/hooks/types.ts +404 -0
- package/src/index.ts +10 -0
- package/src/messages/format.ts +74 -4
- package/src/messages/formatAgentMessages.skills.test.ts +334 -0
- package/src/run.ts +126 -0
- package/src/scripts/multi-agent-subagent.ts +246 -0
- package/src/scripts/subagent-event-driven-debug.ts +190 -0
- package/src/scripts/subagent-tools-debug.ts +160 -0
- package/src/specs/subagent.test.ts +305 -0
- package/src/summarization/node.ts +53 -0
- package/src/tools/BashExecutor.ts +205 -0
- package/src/tools/BashProgrammaticToolCalling.ts +397 -0
- package/src/tools/ReadFile.ts +39 -0
- package/src/tools/SkillTool.ts +46 -0
- package/src/tools/SubagentTool.ts +100 -0
- package/src/tools/ToolNode.ts +391 -169
- package/src/tools/__tests__/ReadFile.test.ts +44 -0
- package/src/tools/__tests__/SkillTool.test.ts +442 -0
- package/src/tools/__tests__/SubagentExecutor.test.ts +1148 -0
- package/src/tools/__tests__/SubagentTool.test.ts +149 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
- package/src/tools/__tests__/skillCatalog.test.ts +161 -0
- package/src/tools/__tests__/subagentHooks.test.ts +215 -0
- package/src/tools/skillCatalog.ts +126 -0
- package/src/tools/subagent/SubagentExecutor.ts +676 -0
- package/src/tools/subagent/index.ts +13 -0
- package/src/types/graph.ts +80 -1
- package/src/types/index.ts +1 -0
- package/src/types/llm.ts +16 -2
- package/src/types/run.ts +20 -0
- package/src/types/skill.ts +11 -0
- package/src/types/tools.ts +41 -1
package/src/tools/ToolNode.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ToolCall } from '@langchain/core/messages/tool';
|
|
2
2
|
import {
|
|
3
3
|
ToolMessage,
|
|
4
|
+
HumanMessage,
|
|
4
5
|
isAIMessage,
|
|
5
6
|
isBaseMessage,
|
|
6
7
|
} from '@langchain/core/messages';
|
|
@@ -19,13 +20,15 @@ import type {
|
|
|
19
20
|
import type { BaseMessage, AIMessage } from '@langchain/core/messages';
|
|
20
21
|
import type { StructuredToolInterface } from '@langchain/core/tools';
|
|
21
22
|
import type * as t from '@/types';
|
|
23
|
+
import type { HookRegistry, AggregatedHookResult } from '@/hooks';
|
|
22
24
|
import { RunnableCallable } from '@/utils';
|
|
23
25
|
import {
|
|
24
26
|
calculateMaxToolResultChars,
|
|
25
27
|
truncateToolResultContent,
|
|
26
28
|
} from '@/utils/truncation';
|
|
27
29
|
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
28
|
-
import {
|
|
30
|
+
import { executeHooks } from '@/hooks';
|
|
31
|
+
import { Constants, GraphEvents, CODE_EXECUTION_TOOLS } from '@/common';
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
34
|
* Helper to check if a value is a Send object
|
|
@@ -34,6 +37,41 @@ function isSend(value: unknown): value is Send {
|
|
|
34
37
|
return value instanceof Send;
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
/** Merges code execution session context into the sessions map. */
|
|
41
|
+
function updateCodeSession(
|
|
42
|
+
sessions: t.ToolSessionMap,
|
|
43
|
+
sessionId: string,
|
|
44
|
+
files: t.FileRefs | undefined
|
|
45
|
+
): void {
|
|
46
|
+
const newFiles = files ?? [];
|
|
47
|
+
const existingSession = sessions.get(Constants.EXECUTE_CODE) as
|
|
48
|
+
| t.CodeSessionContext
|
|
49
|
+
| undefined;
|
|
50
|
+
const existingFiles = existingSession?.files ?? [];
|
|
51
|
+
|
|
52
|
+
if (newFiles.length > 0) {
|
|
53
|
+
const filesWithSession: t.FileRefs = newFiles.map((file) => ({
|
|
54
|
+
...file,
|
|
55
|
+
session_id: sessionId,
|
|
56
|
+
}));
|
|
57
|
+
const newFileNames = new Set(filesWithSession.map((f) => f.name));
|
|
58
|
+
const filteredExisting = existingFiles.filter(
|
|
59
|
+
(f) => !newFileNames.has(f.name)
|
|
60
|
+
);
|
|
61
|
+
sessions.set(Constants.EXECUTE_CODE, {
|
|
62
|
+
session_id: sessionId,
|
|
63
|
+
files: [...filteredExisting, ...filesWithSession],
|
|
64
|
+
lastUpdated: Date.now(),
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
sessions.set(Constants.EXECUTE_CODE, {
|
|
68
|
+
session_id: sessionId,
|
|
69
|
+
files: existingFiles,
|
|
70
|
+
lastUpdated: Date.now(),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
37
75
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
76
|
export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
39
77
|
private toolMap: Map<string, StructuredToolInterface | RunnableToolLike>;
|
|
@@ -59,6 +97,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
59
97
|
private directToolNames?: Set<string>;
|
|
60
98
|
/** Maximum characters allowed in a single tool result before truncation. */
|
|
61
99
|
private maxToolResultChars: number;
|
|
100
|
+
/** Hook registry for PreToolUse/PostToolUse lifecycle hooks */
|
|
101
|
+
private hookRegistry?: HookRegistry;
|
|
62
102
|
|
|
63
103
|
constructor({
|
|
64
104
|
tools,
|
|
@@ -76,6 +116,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
76
116
|
directToolNames,
|
|
77
117
|
maxContextTokens,
|
|
78
118
|
maxToolResultChars,
|
|
119
|
+
hookRegistry,
|
|
79
120
|
}: t.ToolNodeConstructorParams) {
|
|
80
121
|
super({ name, tags, func: (input, config) => this.run(input, config) });
|
|
81
122
|
this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
|
|
@@ -91,6 +132,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
91
132
|
this.directToolNames = directToolNames;
|
|
92
133
|
this.maxToolResultChars =
|
|
93
134
|
maxToolResultChars ?? calculateMaxToolResultChars(maxContextTokens);
|
|
135
|
+
this.hookRegistry = hookRegistry;
|
|
94
136
|
}
|
|
95
137
|
|
|
96
138
|
/**
|
|
@@ -157,7 +199,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
157
199
|
};
|
|
158
200
|
|
|
159
201
|
// Inject runtime data for special tools (becomes available at config.toolCall)
|
|
160
|
-
if (
|
|
202
|
+
if (
|
|
203
|
+
call.name === Constants.PROGRAMMATIC_TOOL_CALLING ||
|
|
204
|
+
call.name === Constants.BASH_PROGRAMMATIC_TOOL_CALLING
|
|
205
|
+
) {
|
|
161
206
|
const { toolMap, toolDefs } = this.getProgrammaticTools();
|
|
162
207
|
invokeParams = {
|
|
163
208
|
...invokeParams,
|
|
@@ -180,10 +225,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
180
225
|
* session_id is always injected when available (even without tracked files)
|
|
181
226
|
* so the CodeExecutor can fall back to the /files endpoint for session continuity.
|
|
182
227
|
*/
|
|
183
|
-
if (
|
|
184
|
-
call.name === Constants.EXECUTE_CODE ||
|
|
185
|
-
call.name === Constants.PROGRAMMATIC_TOOL_CALLING
|
|
186
|
-
) {
|
|
228
|
+
if (CODE_EXECUTION_TOOLS.has(call.name)) {
|
|
187
229
|
const codeSession = this.sessions?.get(Constants.EXECUTE_CODE) as
|
|
188
230
|
| t.CodeSessionContext
|
|
189
231
|
| undefined;
|
|
@@ -313,7 +355,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
313
355
|
*/
|
|
314
356
|
private storeCodeSessionFromResults(
|
|
315
357
|
results: t.ToolExecuteResult[],
|
|
316
|
-
|
|
358
|
+
requestMap: Map<string, t.ToolCallRequest>
|
|
317
359
|
): void {
|
|
318
360
|
if (!this.sessions) {
|
|
319
361
|
return;
|
|
@@ -325,10 +367,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
325
367
|
continue;
|
|
326
368
|
}
|
|
327
369
|
|
|
328
|
-
const request =
|
|
370
|
+
const request = requestMap.get(result.toolCallId);
|
|
329
371
|
if (
|
|
330
|
-
request?.name
|
|
331
|
-
request
|
|
372
|
+
!request?.name ||
|
|
373
|
+
(!CODE_EXECUTION_TOOLS.has(request.name) &&
|
|
374
|
+
request.name !== Constants.SKILL_TOOL)
|
|
332
375
|
) {
|
|
333
376
|
continue;
|
|
334
377
|
}
|
|
@@ -338,35 +381,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
338
381
|
continue;
|
|
339
382
|
}
|
|
340
383
|
|
|
341
|
-
|
|
342
|
-
const existingSession = this.sessions.get(Constants.EXECUTE_CODE) as
|
|
343
|
-
| t.CodeSessionContext
|
|
344
|
-
| undefined;
|
|
345
|
-
const existingFiles = existingSession?.files ?? [];
|
|
346
|
-
|
|
347
|
-
if (newFiles.length > 0) {
|
|
348
|
-
const filesWithSession: t.FileRefs = newFiles.map((file) => ({
|
|
349
|
-
...file,
|
|
350
|
-
session_id: artifact.session_id,
|
|
351
|
-
}));
|
|
352
|
-
|
|
353
|
-
const newFileNames = new Set(filesWithSession.map((f) => f.name));
|
|
354
|
-
const filteredExisting = existingFiles.filter(
|
|
355
|
-
(f) => !newFileNames.has(f.name)
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
359
|
-
session_id: artifact.session_id,
|
|
360
|
-
files: [...filteredExisting, ...filesWithSession],
|
|
361
|
-
lastUpdated: Date.now(),
|
|
362
|
-
});
|
|
363
|
-
} else {
|
|
364
|
-
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
365
|
-
session_id: artifact.session_id,
|
|
366
|
-
files: existingFiles,
|
|
367
|
-
lastUpdated: Date.now(),
|
|
368
|
-
});
|
|
369
|
-
}
|
|
384
|
+
updateCodeSession(this.sessions, artifact.session_id!, artifact.files);
|
|
370
385
|
}
|
|
371
386
|
}
|
|
372
387
|
|
|
@@ -402,43 +417,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
402
417
|
continue;
|
|
403
418
|
}
|
|
404
419
|
|
|
405
|
-
|
|
406
|
-
if (
|
|
407
|
-
this.sessions &&
|
|
408
|
-
(call.name === Constants.EXECUTE_CODE ||
|
|
409
|
-
call.name === Constants.PROGRAMMATIC_TOOL_CALLING)
|
|
410
|
-
) {
|
|
420
|
+
if (this.sessions && CODE_EXECUTION_TOOLS.has(call.name)) {
|
|
411
421
|
const artifact = toolMessage.artifact as
|
|
412
422
|
| t.CodeExecutionArtifact
|
|
413
423
|
| undefined;
|
|
414
424
|
if (artifact?.session_id != null && artifact.session_id !== '') {
|
|
415
|
-
|
|
416
|
-
const existingSession = this.sessions.get(Constants.EXECUTE_CODE) as
|
|
417
|
-
| t.CodeSessionContext
|
|
418
|
-
| undefined;
|
|
419
|
-
const existingFiles = existingSession?.files ?? [];
|
|
420
|
-
|
|
421
|
-
if (newFiles.length > 0) {
|
|
422
|
-
const filesWithSession: t.FileRefs = newFiles.map((file) => ({
|
|
423
|
-
...file,
|
|
424
|
-
session_id: artifact.session_id,
|
|
425
|
-
}));
|
|
426
|
-
const newFileNames = new Set(filesWithSession.map((f) => f.name));
|
|
427
|
-
const filteredExisting = existingFiles.filter(
|
|
428
|
-
(f) => !newFileNames.has(f.name)
|
|
429
|
-
);
|
|
430
|
-
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
431
|
-
session_id: artifact.session_id,
|
|
432
|
-
files: [...filteredExisting, ...filesWithSession],
|
|
433
|
-
lastUpdated: Date.now(),
|
|
434
|
-
});
|
|
435
|
-
} else {
|
|
436
|
-
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
437
|
-
session_id: artifact.session_id,
|
|
438
|
-
files: existingFiles,
|
|
439
|
-
lastUpdated: Date.now(),
|
|
440
|
-
});
|
|
441
|
-
}
|
|
425
|
+
updateCodeSession(this.sessions, artifact.session_id, artifact.files);
|
|
442
426
|
}
|
|
443
427
|
}
|
|
444
428
|
|
|
@@ -482,128 +466,355 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
482
466
|
/**
|
|
483
467
|
* Dispatches tool calls to the host via ON_TOOL_EXECUTE event and returns raw ToolMessages.
|
|
484
468
|
* Core logic for event-driven execution, separated from output shaping.
|
|
469
|
+
*
|
|
470
|
+
* Hook lifecycle (when `hookRegistry` is set):
|
|
471
|
+
* 1. **PreToolUse** fires per call in parallel before dispatch. Denied
|
|
472
|
+
* calls produce error ToolMessages and fire **PermissionDenied**;
|
|
473
|
+
* surviving calls proceed with optional `updatedInput`.
|
|
474
|
+
* 2. Surviving calls are dispatched to the host via `ON_TOOL_EXECUTE`.
|
|
475
|
+
* 3. **PostToolUse** / **PostToolUseFailure** fire per result. Post hooks
|
|
476
|
+
* can replace tool output via `updatedOutput`.
|
|
477
|
+
* 4. Injected messages from results are collected and returned alongside
|
|
478
|
+
* ToolMessages (appended AFTER to respect provider ordering).
|
|
485
479
|
*/
|
|
486
480
|
private async dispatchToolEvents(
|
|
487
481
|
toolCalls: ToolCall[],
|
|
488
482
|
config: RunnableConfig
|
|
489
|
-
): Promise<ToolMessage[]> {
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
483
|
+
): Promise<{ toolMessages: ToolMessage[]; injected: BaseMessage[] }> {
|
|
484
|
+
const runId = (config.configurable?.run_id as string | undefined) ?? '';
|
|
485
|
+
const threadId = config.configurable?.thread_id as string | undefined;
|
|
486
|
+
|
|
487
|
+
const preToolCalls = toolCalls.map((call) => ({
|
|
488
|
+
call,
|
|
489
|
+
stepId: this.toolCallStepIds?.get(call.id!) ?? '',
|
|
490
|
+
args: call.args as Record<string, unknown>,
|
|
491
|
+
}));
|
|
492
|
+
|
|
493
|
+
const messageByCallId = new Map<string, ToolMessage>();
|
|
494
|
+
const approvedEntries: typeof preToolCalls = [];
|
|
495
|
+
const HOOK_FALLBACK: AggregatedHookResult = Object.freeze({
|
|
496
|
+
additionalContexts: [] as string[],
|
|
497
|
+
errors: [] as string[],
|
|
498
|
+
});
|
|
493
499
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
500
|
+
if (this.hookRegistry?.hasHookFor('PreToolUse', runId) === true) {
|
|
501
|
+
const preResults = await Promise.all(
|
|
502
|
+
preToolCalls.map((entry) =>
|
|
503
|
+
executeHooks({
|
|
504
|
+
registry: this.hookRegistry!,
|
|
505
|
+
input: {
|
|
506
|
+
hook_event_name: 'PreToolUse',
|
|
507
|
+
runId,
|
|
508
|
+
threadId,
|
|
509
|
+
agentId: this.agentId,
|
|
510
|
+
toolName: entry.call.name,
|
|
511
|
+
toolInput: entry.args,
|
|
512
|
+
toolUseId: entry.call.id!,
|
|
513
|
+
stepId: entry.stepId,
|
|
514
|
+
turn: this.toolUsageCount.get(entry.call.name) ?? 0,
|
|
515
|
+
},
|
|
516
|
+
sessionId: runId,
|
|
517
|
+
matchQuery: entry.call.name,
|
|
518
|
+
}).catch((): AggregatedHookResult => HOOK_FALLBACK)
|
|
519
|
+
)
|
|
520
|
+
);
|
|
501
521
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
522
|
+
for (let i = 0; i < preToolCalls.length; i++) {
|
|
523
|
+
const hookResult = preResults[i];
|
|
524
|
+
const entry = preToolCalls[i];
|
|
525
|
+
const isDenied =
|
|
526
|
+
hookResult.decision === 'deny' || hookResult.decision === 'ask';
|
|
527
|
+
if (isDenied) {
|
|
528
|
+
const reason = hookResult.reason ?? 'Blocked by hook';
|
|
529
|
+
const contentString = `Blocked: ${reason}`;
|
|
530
|
+
messageByCallId.set(
|
|
531
|
+
entry.call.id!,
|
|
532
|
+
new ToolMessage({
|
|
533
|
+
status: 'error',
|
|
534
|
+
content: contentString,
|
|
535
|
+
name: entry.call.name,
|
|
536
|
+
tool_call_id: entry.call.id!,
|
|
537
|
+
})
|
|
538
|
+
);
|
|
539
|
+
this.dispatchStepCompleted(
|
|
540
|
+
entry.call.id!,
|
|
541
|
+
entry.call.name,
|
|
542
|
+
entry.args,
|
|
543
|
+
contentString,
|
|
544
|
+
config
|
|
545
|
+
);
|
|
546
|
+
if (this.hookRegistry.hasHookFor('PermissionDenied', runId)) {
|
|
547
|
+
executeHooks({
|
|
548
|
+
registry: this.hookRegistry,
|
|
549
|
+
input: {
|
|
550
|
+
hook_event_name: 'PermissionDenied',
|
|
551
|
+
runId,
|
|
552
|
+
threadId,
|
|
553
|
+
agentId: this.agentId,
|
|
554
|
+
toolName: entry.call.name,
|
|
555
|
+
toolInput: entry.args,
|
|
556
|
+
toolUseId: entry.call.id!,
|
|
557
|
+
reason,
|
|
558
|
+
},
|
|
559
|
+
sessionId: runId,
|
|
560
|
+
matchQuery: entry.call.name,
|
|
561
|
+
}).catch(() => {
|
|
562
|
+
/* PermissionDenied is observational — swallow errors */
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (hookResult.updatedInput != null) {
|
|
568
|
+
entry.args = hookResult.updatedInput;
|
|
569
|
+
}
|
|
570
|
+
approvedEntries.push(entry);
|
|
507
571
|
}
|
|
572
|
+
} else {
|
|
573
|
+
approvedEntries.push(...preToolCalls);
|
|
574
|
+
}
|
|
508
575
|
|
|
509
|
-
|
|
510
|
-
});
|
|
576
|
+
const injected: BaseMessage[] = [];
|
|
511
577
|
|
|
512
|
-
|
|
513
|
-
(
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
reject,
|
|
578
|
+
if (approvedEntries.length > 0) {
|
|
579
|
+
const requests: t.ToolCallRequest[] = approvedEntries.map((entry) => {
|
|
580
|
+
const turn = this.toolUsageCount.get(entry.call.name) ?? 0;
|
|
581
|
+
this.toolUsageCount.set(entry.call.name, turn + 1);
|
|
582
|
+
|
|
583
|
+
const request: t.ToolCallRequest = {
|
|
584
|
+
id: entry.call.id!,
|
|
585
|
+
name: entry.call.name,
|
|
586
|
+
args: entry.args,
|
|
587
|
+
stepId: entry.stepId,
|
|
588
|
+
turn,
|
|
524
589
|
};
|
|
525
590
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
591
|
+
if (
|
|
592
|
+
CODE_EXECUTION_TOOLS.has(entry.call.name) ||
|
|
593
|
+
entry.call.name === Constants.SKILL_TOOL
|
|
594
|
+
) {
|
|
595
|
+
request.codeSessionContext = this.getCodeSessionContext();
|
|
596
|
+
}
|
|
529
597
|
|
|
530
|
-
|
|
598
|
+
return request;
|
|
599
|
+
});
|
|
531
600
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
601
|
+
const requestMap = new Map(requests.map((r) => [r.id, r]));
|
|
602
|
+
|
|
603
|
+
const results = await new Promise<t.ToolExecuteResult[]>(
|
|
604
|
+
(resolve, reject) => {
|
|
605
|
+
const batchRequest: t.ToolExecuteBatchRequest = {
|
|
606
|
+
toolCalls: requests,
|
|
607
|
+
userId: config.configurable?.user_id as string | undefined,
|
|
608
|
+
agentId: this.agentId,
|
|
609
|
+
configurable: config.configurable as
|
|
610
|
+
| Record<string, unknown>
|
|
611
|
+
| undefined,
|
|
612
|
+
metadata: config.metadata as Record<string, unknown> | undefined,
|
|
613
|
+
resolve,
|
|
614
|
+
reject,
|
|
615
|
+
};
|
|
544
616
|
|
|
545
|
-
|
|
546
|
-
|
|
617
|
+
safeDispatchCustomEvent(
|
|
618
|
+
GraphEvents.ON_TOOL_EXECUTE,
|
|
619
|
+
batchRequest,
|
|
620
|
+
config
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
);
|
|
547
624
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
625
|
+
this.storeCodeSessionFromResults(results, requestMap);
|
|
626
|
+
|
|
627
|
+
const hasPostHook =
|
|
628
|
+
this.hookRegistry?.hasHookFor('PostToolUse', runId) === true;
|
|
629
|
+
const hasFailureHook =
|
|
630
|
+
this.hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
|
|
631
|
+
|
|
632
|
+
for (const result of results) {
|
|
633
|
+
if (result.injectedMessages && result.injectedMessages.length > 0) {
|
|
634
|
+
try {
|
|
635
|
+
injected.push(
|
|
636
|
+
...this.convertInjectedMessages(result.injectedMessages)
|
|
637
|
+
);
|
|
638
|
+
} catch (e) {
|
|
639
|
+
// eslint-disable-next-line no-console
|
|
640
|
+
console.warn(
|
|
641
|
+
`[ToolNode] Failed to convert injectedMessages for toolCallId=${result.toolCallId}:`,
|
|
642
|
+
e instanceof Error ? e.message : e
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const request = requestMap.get(result.toolCallId);
|
|
647
|
+
const toolName = request?.name ?? 'unknown';
|
|
648
|
+
|
|
649
|
+
let contentString: string;
|
|
650
|
+
let toolMessage: ToolMessage;
|
|
651
|
+
|
|
652
|
+
if (result.status === 'error') {
|
|
653
|
+
contentString = `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`;
|
|
654
|
+
toolMessage = new ToolMessage({
|
|
655
|
+
status: 'error',
|
|
656
|
+
content: contentString,
|
|
657
|
+
name: toolName,
|
|
658
|
+
tool_call_id: result.toolCallId,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
if (hasFailureHook) {
|
|
662
|
+
await executeHooks({
|
|
663
|
+
registry: this.hookRegistry!,
|
|
664
|
+
input: {
|
|
665
|
+
hook_event_name: 'PostToolUseFailure',
|
|
666
|
+
runId,
|
|
667
|
+
threadId,
|
|
668
|
+
agentId: this.agentId,
|
|
669
|
+
toolName,
|
|
670
|
+
toolInput: request?.args ?? {},
|
|
671
|
+
toolUseId: result.toolCallId,
|
|
672
|
+
error: result.errorMessage ?? 'Unknown error',
|
|
673
|
+
stepId: request?.stepId,
|
|
674
|
+
turn: request?.turn,
|
|
675
|
+
},
|
|
676
|
+
sessionId: runId,
|
|
677
|
+
matchQuery: toolName,
|
|
678
|
+
}).catch(() => {
|
|
679
|
+
/* PostToolUseFailure is observational — swallow errors */
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
} else {
|
|
683
|
+
const rawContent =
|
|
684
|
+
typeof result.content === 'string'
|
|
685
|
+
? result.content
|
|
686
|
+
: JSON.stringify(result.content);
|
|
687
|
+
contentString = truncateToolResultContent(
|
|
688
|
+
rawContent,
|
|
689
|
+
this.maxToolResultChars
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
if (hasPostHook) {
|
|
693
|
+
const hookResult = await executeHooks({
|
|
694
|
+
registry: this.hookRegistry!,
|
|
695
|
+
input: {
|
|
696
|
+
hook_event_name: 'PostToolUse',
|
|
697
|
+
runId,
|
|
698
|
+
threadId,
|
|
699
|
+
agentId: this.agentId,
|
|
700
|
+
toolName,
|
|
701
|
+
toolInput: request?.args ?? {},
|
|
702
|
+
toolOutput: result.content,
|
|
703
|
+
toolUseId: result.toolCallId,
|
|
704
|
+
stepId: request?.stepId,
|
|
705
|
+
turn: request?.turn,
|
|
706
|
+
},
|
|
707
|
+
sessionId: runId,
|
|
708
|
+
matchQuery: toolName,
|
|
709
|
+
}).catch((): undefined => undefined);
|
|
710
|
+
if (hookResult?.updatedOutput != null) {
|
|
711
|
+
const replaced =
|
|
712
|
+
typeof hookResult.updatedOutput === 'string'
|
|
713
|
+
? hookResult.updatedOutput
|
|
714
|
+
: JSON.stringify(hookResult.updatedOutput);
|
|
715
|
+
contentString = truncateToolResultContent(
|
|
716
|
+
replaced,
|
|
717
|
+
this.maxToolResultChars
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
toolMessage = new ToolMessage({
|
|
723
|
+
status: 'success',
|
|
724
|
+
name: toolName,
|
|
725
|
+
content: contentString,
|
|
726
|
+
artifact: result.artifact,
|
|
727
|
+
tool_call_id: result.toolCallId,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
this.dispatchStepCompleted(
|
|
732
|
+
result.toolCallId,
|
|
733
|
+
toolName,
|
|
734
|
+
request?.args ?? {},
|
|
735
|
+
contentString,
|
|
736
|
+
config,
|
|
737
|
+
request?.turn
|
|
564
738
|
);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
name: toolName,
|
|
568
|
-
content: contentString,
|
|
569
|
-
artifact: result.artifact,
|
|
570
|
-
tool_call_id: result.toolCallId,
|
|
571
|
-
});
|
|
739
|
+
|
|
740
|
+
messageByCallId.set(result.toolCallId, toolMessage);
|
|
572
741
|
}
|
|
742
|
+
}
|
|
573
743
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
name: toolName,
|
|
580
|
-
id: result.toolCallId,
|
|
581
|
-
output: contentString,
|
|
582
|
-
progress: 1,
|
|
583
|
-
};
|
|
744
|
+
const toolMessages = toolCalls
|
|
745
|
+
.map((call) => messageByCallId.get(call.id!))
|
|
746
|
+
.filter((m): m is ToolMessage => m != null);
|
|
747
|
+
return { toolMessages, injected };
|
|
748
|
+
}
|
|
584
749
|
|
|
585
|
-
|
|
750
|
+
private dispatchStepCompleted(
|
|
751
|
+
toolCallId: string,
|
|
752
|
+
toolName: string,
|
|
753
|
+
args: Record<string, unknown>,
|
|
754
|
+
output: string,
|
|
755
|
+
config: RunnableConfig,
|
|
756
|
+
turn?: number
|
|
757
|
+
): void {
|
|
758
|
+
const stepId = this.toolCallStepIds?.get(toolCallId) ?? '';
|
|
759
|
+
if (!stepId) {
|
|
760
|
+
// eslint-disable-next-line no-console
|
|
761
|
+
console.warn(
|
|
762
|
+
`[ToolNode] toolCallStepIds missing entry for toolCallId=${toolCallId} (tool=${toolName}). ` +
|
|
763
|
+
'This indicates a race between the stream consumer and graph execution. ' +
|
|
764
|
+
`Map size: ${this.toolCallStepIds?.size ?? 0}`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
safeDispatchCustomEvent(
|
|
769
|
+
GraphEvents.ON_RUN_STEP_COMPLETED,
|
|
770
|
+
{
|
|
586
771
|
result: {
|
|
587
772
|
id: stepId,
|
|
588
|
-
index:
|
|
773
|
+
index: turn ?? this.toolUsageCount.get(toolName) ?? 0,
|
|
589
774
|
type: 'tool_call' as const,
|
|
590
|
-
tool_call
|
|
775
|
+
tool_call: {
|
|
776
|
+
args: JSON.stringify(args),
|
|
777
|
+
name: toolName,
|
|
778
|
+
id: toolCallId,
|
|
779
|
+
output,
|
|
780
|
+
progress: 1,
|
|
781
|
+
} as t.ProcessedToolCall,
|
|
591
782
|
},
|
|
783
|
+
},
|
|
784
|
+
config
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Converts InjectedMessage instances to LangChain HumanMessage objects.
|
|
790
|
+
* Both 'user' and 'system' roles become HumanMessage to avoid provider
|
|
791
|
+
* rejections (Anthropic/Google reject non-leading SystemMessages).
|
|
792
|
+
* The original role is preserved in additional_kwargs for downstream consumers.
|
|
793
|
+
*/
|
|
794
|
+
private convertInjectedMessages(
|
|
795
|
+
messages: t.InjectedMessage[]
|
|
796
|
+
): BaseMessage[] {
|
|
797
|
+
const converted: BaseMessage[] = [];
|
|
798
|
+
for (const msg of messages) {
|
|
799
|
+
const additional_kwargs: Record<string, unknown> = {
|
|
800
|
+
role: msg.role,
|
|
592
801
|
};
|
|
802
|
+
if (msg.isMeta != null) additional_kwargs.isMeta = msg.isMeta;
|
|
803
|
+
if (msg.source != null) additional_kwargs.source = msg.source;
|
|
804
|
+
if (msg.skillName != null) additional_kwargs.skillName = msg.skillName;
|
|
593
805
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
runStepCompletedData,
|
|
597
|
-
config
|
|
806
|
+
converted.push(
|
|
807
|
+
new HumanMessage({ content: msg.content, additional_kwargs })
|
|
598
808
|
);
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
});
|
|
809
|
+
}
|
|
810
|
+
return converted;
|
|
602
811
|
}
|
|
603
812
|
|
|
604
813
|
/**
|
|
605
814
|
* Execute all tool calls via ON_TOOL_EXECUTE event dispatch.
|
|
606
|
-
*
|
|
815
|
+
* Injected messages are placed AFTER ToolMessages to respect provider
|
|
816
|
+
* message ordering (AIMessage tool_calls must be immediately followed
|
|
817
|
+
* by their ToolMessage results).
|
|
607
818
|
*/
|
|
608
819
|
private async executeViaEvent(
|
|
609
820
|
toolCalls: ToolCall[],
|
|
@@ -611,7 +822,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
611
822
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
612
823
|
input: any
|
|
613
824
|
): Promise<T> {
|
|
614
|
-
const
|
|
825
|
+
const { toolMessages, injected } = await this.dispatchToolEvents(
|
|
826
|
+
toolCalls,
|
|
827
|
+
config
|
|
828
|
+
);
|
|
829
|
+
const outputs: BaseMessage[] = [...toolMessages, ...injected];
|
|
615
830
|
return (Array.isArray(input) ? outputs : { messages: outputs }) as T;
|
|
616
831
|
}
|
|
617
832
|
|
|
@@ -707,12 +922,19 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
707
922
|
this.handleRunToolCompletions(directCalls, directOutputs, config);
|
|
708
923
|
}
|
|
709
924
|
|
|
710
|
-
const
|
|
925
|
+
const eventResult =
|
|
711
926
|
eventCalls.length > 0
|
|
712
927
|
? await this.dispatchToolEvents(eventCalls, config)
|
|
713
|
-
:
|
|
714
|
-
|
|
715
|
-
|
|
928
|
+
: {
|
|
929
|
+
toolMessages: [] as ToolMessage[],
|
|
930
|
+
injected: [] as BaseMessage[],
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
outputs = [
|
|
934
|
+
...directOutputs,
|
|
935
|
+
...eventResult.toolMessages,
|
|
936
|
+
...eventResult.injected,
|
|
937
|
+
];
|
|
716
938
|
} else {
|
|
717
939
|
outputs = await Promise.all(
|
|
718
940
|
filteredCalls.map((call) => this.runTool(call, config))
|