@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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { isBaseMessage, ToolMessage, isAIMessage } from '@langchain/core/messages';
|
|
1
|
+
import { isBaseMessage, ToolMessage, HumanMessage, isAIMessage } from '@langchain/core/messages';
|
|
2
2
|
import { isCommand, isGraphInterrupt, Command, Send, END } from '@langchain/langgraph';
|
|
3
|
-
import { Constants, GraphEvents } from '../common/enum.mjs';
|
|
3
|
+
import { Constants, CODE_EXECUTION_TOOLS, GraphEvents } from '../common/enum.mjs';
|
|
4
4
|
import 'nanoid';
|
|
5
5
|
import '../messages/core.mjs';
|
|
6
6
|
import { calculateMaxToolResultChars, truncateToolResultContent } from '../utils/truncation.mjs';
|
|
@@ -9,6 +9,7 @@ import 'uuid';
|
|
|
9
9
|
import { RunnableCallable } from '../utils/run.mjs';
|
|
10
10
|
import 'ai-tokenizer';
|
|
11
11
|
import 'zod-to-json-schema';
|
|
12
|
+
import { executeHooks } from '../hooks/executeHooks.mjs';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Helper to check if a value is a Send object
|
|
@@ -16,6 +17,32 @@ import 'zod-to-json-schema';
|
|
|
16
17
|
function isSend(value) {
|
|
17
18
|
return value instanceof Send;
|
|
18
19
|
}
|
|
20
|
+
/** Merges code execution session context into the sessions map. */
|
|
21
|
+
function updateCodeSession(sessions, sessionId, files) {
|
|
22
|
+
const newFiles = files ?? [];
|
|
23
|
+
const existingSession = sessions.get(Constants.EXECUTE_CODE);
|
|
24
|
+
const existingFiles = existingSession?.files ?? [];
|
|
25
|
+
if (newFiles.length > 0) {
|
|
26
|
+
const filesWithSession = newFiles.map((file) => ({
|
|
27
|
+
...file,
|
|
28
|
+
session_id: sessionId,
|
|
29
|
+
}));
|
|
30
|
+
const newFileNames = new Set(filesWithSession.map((f) => f.name));
|
|
31
|
+
const filteredExisting = existingFiles.filter((f) => !newFileNames.has(f.name));
|
|
32
|
+
sessions.set(Constants.EXECUTE_CODE, {
|
|
33
|
+
session_id: sessionId,
|
|
34
|
+
files: [...filteredExisting, ...filesWithSession],
|
|
35
|
+
lastUpdated: Date.now(),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
sessions.set(Constants.EXECUTE_CODE, {
|
|
40
|
+
session_id: sessionId,
|
|
41
|
+
files: existingFiles,
|
|
42
|
+
lastUpdated: Date.now(),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
19
46
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
47
|
class ToolNode extends RunnableCallable {
|
|
21
48
|
toolMap;
|
|
@@ -41,7 +68,9 @@ class ToolNode extends RunnableCallable {
|
|
|
41
68
|
directToolNames;
|
|
42
69
|
/** Maximum characters allowed in a single tool result before truncation. */
|
|
43
70
|
maxToolResultChars;
|
|
44
|
-
|
|
71
|
+
/** Hook registry for PreToolUse/PostToolUse lifecycle hooks */
|
|
72
|
+
hookRegistry;
|
|
73
|
+
constructor({ tools, toolMap, name, tags, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, agentId, directToolNames, maxContextTokens, maxToolResultChars, hookRegistry, }) {
|
|
45
74
|
super({ name, tags, func: (input, config) => this.run(input, config) });
|
|
46
75
|
this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
|
|
47
76
|
this.toolCallStepIds = toolCallStepIds;
|
|
@@ -56,6 +85,7 @@ class ToolNode extends RunnableCallable {
|
|
|
56
85
|
this.directToolNames = directToolNames;
|
|
57
86
|
this.maxToolResultChars =
|
|
58
87
|
maxToolResultChars ?? calculateMaxToolResultChars(maxContextTokens);
|
|
88
|
+
this.hookRegistry = hookRegistry;
|
|
59
89
|
}
|
|
60
90
|
/**
|
|
61
91
|
* Returns cached programmatic tools, computing once on first access.
|
|
@@ -111,7 +141,8 @@ class ToolNode extends RunnableCallable {
|
|
|
111
141
|
turn,
|
|
112
142
|
};
|
|
113
143
|
// Inject runtime data for special tools (becomes available at config.toolCall)
|
|
114
|
-
if (call.name === Constants.PROGRAMMATIC_TOOL_CALLING
|
|
144
|
+
if (call.name === Constants.PROGRAMMATIC_TOOL_CALLING ||
|
|
145
|
+
call.name === Constants.BASH_PROGRAMMATIC_TOOL_CALLING) {
|
|
115
146
|
const { toolMap, toolDefs } = this.getProgrammaticTools();
|
|
116
147
|
invokeParams = {
|
|
117
148
|
...invokeParams,
|
|
@@ -134,8 +165,7 @@ class ToolNode extends RunnableCallable {
|
|
|
134
165
|
* session_id is always injected when available (even without tracked files)
|
|
135
166
|
* so the CodeExecutor can fall back to the /files endpoint for session continuity.
|
|
136
167
|
*/
|
|
137
|
-
if (call.name
|
|
138
|
-
call.name === Constants.PROGRAMMATIC_TOOL_CALLING) {
|
|
168
|
+
if (CODE_EXECUTION_TOOLS.has(call.name)) {
|
|
139
169
|
const codeSession = this.sessions?.get(Constants.EXECUTE_CODE);
|
|
140
170
|
if (codeSession?.session_id != null && codeSession.session_id !== '') {
|
|
141
171
|
invokeParams = {
|
|
@@ -244,7 +274,7 @@ class ToolNode extends RunnableCallable {
|
|
|
244
274
|
* Extracts code execution session context from tool results and stores in Graph.sessions.
|
|
245
275
|
* Mirrors the session storage logic in handleRunToolCompletions for direct execution.
|
|
246
276
|
*/
|
|
247
|
-
storeCodeSessionFromResults(results,
|
|
277
|
+
storeCodeSessionFromResults(results, requestMap) {
|
|
248
278
|
if (!this.sessions) {
|
|
249
279
|
return;
|
|
250
280
|
}
|
|
@@ -253,38 +283,17 @@ class ToolNode extends RunnableCallable {
|
|
|
253
283
|
if (result.status !== 'success' || result.artifact == null) {
|
|
254
284
|
continue;
|
|
255
285
|
}
|
|
256
|
-
const request =
|
|
257
|
-
if (request?.name
|
|
258
|
-
request
|
|
286
|
+
const request = requestMap.get(result.toolCallId);
|
|
287
|
+
if (!request?.name ||
|
|
288
|
+
(!CODE_EXECUTION_TOOLS.has(request.name) &&
|
|
289
|
+
request.name !== Constants.SKILL_TOOL)) {
|
|
259
290
|
continue;
|
|
260
291
|
}
|
|
261
292
|
const artifact = result.artifact;
|
|
262
293
|
if (artifact?.session_id == null || artifact.session_id === '') {
|
|
263
294
|
continue;
|
|
264
295
|
}
|
|
265
|
-
|
|
266
|
-
const existingSession = this.sessions.get(Constants.EXECUTE_CODE);
|
|
267
|
-
const existingFiles = existingSession?.files ?? [];
|
|
268
|
-
if (newFiles.length > 0) {
|
|
269
|
-
const filesWithSession = newFiles.map((file) => ({
|
|
270
|
-
...file,
|
|
271
|
-
session_id: artifact.session_id,
|
|
272
|
-
}));
|
|
273
|
-
const newFileNames = new Set(filesWithSession.map((f) => f.name));
|
|
274
|
-
const filteredExisting = existingFiles.filter((f) => !newFileNames.has(f.name));
|
|
275
|
-
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
276
|
-
session_id: artifact.session_id,
|
|
277
|
-
files: [...filteredExisting, ...filesWithSession],
|
|
278
|
-
lastUpdated: Date.now(),
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
283
|
-
session_id: artifact.session_id,
|
|
284
|
-
files: existingFiles,
|
|
285
|
-
lastUpdated: Date.now(),
|
|
286
|
-
});
|
|
287
|
-
}
|
|
296
|
+
updateCodeSession(this.sessions, artifact.session_id, artifact.files);
|
|
288
297
|
}
|
|
289
298
|
}
|
|
290
299
|
/**
|
|
@@ -311,35 +320,10 @@ class ToolNode extends RunnableCallable {
|
|
|
311
320
|
if (toolMessage.status === 'error' && this.errorHandler != null) {
|
|
312
321
|
continue;
|
|
313
322
|
}
|
|
314
|
-
|
|
315
|
-
if (this.sessions &&
|
|
316
|
-
(call.name === Constants.EXECUTE_CODE ||
|
|
317
|
-
call.name === Constants.PROGRAMMATIC_TOOL_CALLING)) {
|
|
323
|
+
if (this.sessions && CODE_EXECUTION_TOOLS.has(call.name)) {
|
|
318
324
|
const artifact = toolMessage.artifact;
|
|
319
325
|
if (artifact?.session_id != null && artifact.session_id !== '') {
|
|
320
|
-
|
|
321
|
-
const existingSession = this.sessions.get(Constants.EXECUTE_CODE);
|
|
322
|
-
const existingFiles = existingSession?.files ?? [];
|
|
323
|
-
if (newFiles.length > 0) {
|
|
324
|
-
const filesWithSession = newFiles.map((file) => ({
|
|
325
|
-
...file,
|
|
326
|
-
session_id: artifact.session_id,
|
|
327
|
-
}));
|
|
328
|
-
const newFileNames = new Set(filesWithSession.map((f) => f.name));
|
|
329
|
-
const filteredExisting = existingFiles.filter((f) => !newFileNames.has(f.name));
|
|
330
|
-
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
331
|
-
session_id: artifact.session_id,
|
|
332
|
-
files: [...filteredExisting, ...filesWithSession],
|
|
333
|
-
lastUpdated: Date.now(),
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
338
|
-
session_id: artifact.session_id,
|
|
339
|
-
files: existingFiles,
|
|
340
|
-
lastUpdated: Date.now(),
|
|
341
|
-
});
|
|
342
|
-
}
|
|
326
|
+
updateCodeSession(this.sessions, artifact.session_id, artifact.files);
|
|
343
327
|
}
|
|
344
328
|
}
|
|
345
329
|
// Dispatch ON_RUN_STEP_COMPLETED via custom event (same path as dispatchToolEvents)
|
|
@@ -372,100 +356,273 @@ class ToolNode extends RunnableCallable {
|
|
|
372
356
|
/**
|
|
373
357
|
* Dispatches tool calls to the host via ON_TOOL_EXECUTE event and returns raw ToolMessages.
|
|
374
358
|
* Core logic for event-driven execution, separated from output shaping.
|
|
359
|
+
*
|
|
360
|
+
* Hook lifecycle (when `hookRegistry` is set):
|
|
361
|
+
* 1. **PreToolUse** fires per call in parallel before dispatch. Denied
|
|
362
|
+
* calls produce error ToolMessages and fire **PermissionDenied**;
|
|
363
|
+
* surviving calls proceed with optional `updatedInput`.
|
|
364
|
+
* 2. Surviving calls are dispatched to the host via `ON_TOOL_EXECUTE`.
|
|
365
|
+
* 3. **PostToolUse** / **PostToolUseFailure** fire per result. Post hooks
|
|
366
|
+
* can replace tool output via `updatedOutput`.
|
|
367
|
+
* 4. Injected messages from results are collected and returned alongside
|
|
368
|
+
* ToolMessages (appended AFTER to respect provider ordering).
|
|
375
369
|
*/
|
|
376
370
|
async dispatchToolEvents(toolCalls, config) {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
request.codeSessionContext = this.getCodeSessionContext();
|
|
390
|
-
}
|
|
391
|
-
return request;
|
|
392
|
-
});
|
|
393
|
-
const results = await new Promise((resolve, reject) => {
|
|
394
|
-
const request = {
|
|
395
|
-
toolCalls: requests,
|
|
396
|
-
userId: config.configurable?.user_id,
|
|
397
|
-
agentId: this.agentId,
|
|
398
|
-
configurable: config.configurable,
|
|
399
|
-
metadata: config.metadata,
|
|
400
|
-
resolve,
|
|
401
|
-
reject,
|
|
402
|
-
};
|
|
403
|
-
safeDispatchCustomEvent(GraphEvents.ON_TOOL_EXECUTE, request, config);
|
|
371
|
+
const runId = config.configurable?.run_id ?? '';
|
|
372
|
+
const threadId = config.configurable?.thread_id;
|
|
373
|
+
const preToolCalls = toolCalls.map((call) => ({
|
|
374
|
+
call,
|
|
375
|
+
stepId: this.toolCallStepIds?.get(call.id) ?? '',
|
|
376
|
+
args: call.args,
|
|
377
|
+
}));
|
|
378
|
+
const messageByCallId = new Map();
|
|
379
|
+
const approvedEntries = [];
|
|
380
|
+
const HOOK_FALLBACK = Object.freeze({
|
|
381
|
+
additionalContexts: [],
|
|
382
|
+
errors: [],
|
|
404
383
|
});
|
|
405
|
-
this.
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
384
|
+
if (this.hookRegistry?.hasHookFor('PreToolUse', runId) === true) {
|
|
385
|
+
const preResults = await Promise.all(preToolCalls.map((entry) => executeHooks({
|
|
386
|
+
registry: this.hookRegistry,
|
|
387
|
+
input: {
|
|
388
|
+
hook_event_name: 'PreToolUse',
|
|
389
|
+
runId,
|
|
390
|
+
threadId,
|
|
391
|
+
agentId: this.agentId,
|
|
392
|
+
toolName: entry.call.name,
|
|
393
|
+
toolInput: entry.args,
|
|
394
|
+
toolUseId: entry.call.id,
|
|
395
|
+
stepId: entry.stepId,
|
|
396
|
+
turn: this.toolUsageCount.get(entry.call.name) ?? 0,
|
|
397
|
+
},
|
|
398
|
+
sessionId: runId,
|
|
399
|
+
matchQuery: entry.call.name,
|
|
400
|
+
}).catch(() => HOOK_FALLBACK)));
|
|
401
|
+
for (let i = 0; i < preToolCalls.length; i++) {
|
|
402
|
+
const hookResult = preResults[i];
|
|
403
|
+
const entry = preToolCalls[i];
|
|
404
|
+
const isDenied = hookResult.decision === 'deny' || hookResult.decision === 'ask';
|
|
405
|
+
if (isDenied) {
|
|
406
|
+
const reason = hookResult.reason ?? 'Blocked by hook';
|
|
407
|
+
const contentString = `Blocked: ${reason}`;
|
|
408
|
+
messageByCallId.set(entry.call.id, new ToolMessage({
|
|
409
|
+
status: 'error',
|
|
410
|
+
content: contentString,
|
|
411
|
+
name: entry.call.name,
|
|
412
|
+
tool_call_id: entry.call.id,
|
|
413
|
+
}));
|
|
414
|
+
this.dispatchStepCompleted(entry.call.id, entry.call.name, entry.args, contentString, config);
|
|
415
|
+
if (this.hookRegistry.hasHookFor('PermissionDenied', runId)) {
|
|
416
|
+
executeHooks({
|
|
417
|
+
registry: this.hookRegistry,
|
|
418
|
+
input: {
|
|
419
|
+
hook_event_name: 'PermissionDenied',
|
|
420
|
+
runId,
|
|
421
|
+
threadId,
|
|
422
|
+
agentId: this.agentId,
|
|
423
|
+
toolName: entry.call.name,
|
|
424
|
+
toolInput: entry.args,
|
|
425
|
+
toolUseId: entry.call.id,
|
|
426
|
+
reason,
|
|
427
|
+
},
|
|
428
|
+
sessionId: runId,
|
|
429
|
+
matchQuery: entry.call.name,
|
|
430
|
+
}).catch(() => {
|
|
431
|
+
/* PermissionDenied is observational — swallow errors */
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (hookResult.updatedInput != null) {
|
|
437
|
+
entry.args = hookResult.updatedInput;
|
|
438
|
+
}
|
|
439
|
+
approvedEntries.push(entry);
|
|
415
440
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
approvedEntries.push(...preToolCalls);
|
|
444
|
+
}
|
|
445
|
+
const injected = [];
|
|
446
|
+
if (approvedEntries.length > 0) {
|
|
447
|
+
const requests = approvedEntries.map((entry) => {
|
|
448
|
+
const turn = this.toolUsageCount.get(entry.call.name) ?? 0;
|
|
449
|
+
this.toolUsageCount.set(entry.call.name, turn + 1);
|
|
450
|
+
const request = {
|
|
451
|
+
id: entry.call.id,
|
|
452
|
+
name: entry.call.name,
|
|
453
|
+
args: entry.args,
|
|
454
|
+
stepId: entry.stepId,
|
|
455
|
+
turn,
|
|
456
|
+
};
|
|
457
|
+
if (CODE_EXECUTION_TOOLS.has(entry.call.name) ||
|
|
458
|
+
entry.call.name === Constants.SKILL_TOOL) {
|
|
459
|
+
request.codeSessionContext = this.getCodeSessionContext();
|
|
460
|
+
}
|
|
461
|
+
return request;
|
|
462
|
+
});
|
|
463
|
+
const requestMap = new Map(requests.map((r) => [r.id, r]));
|
|
464
|
+
const results = await new Promise((resolve, reject) => {
|
|
465
|
+
const batchRequest = {
|
|
466
|
+
toolCalls: requests,
|
|
467
|
+
userId: config.configurable?.user_id,
|
|
468
|
+
agentId: this.agentId,
|
|
469
|
+
configurable: config.configurable,
|
|
470
|
+
metadata: config.metadata,
|
|
471
|
+
resolve,
|
|
472
|
+
reject,
|
|
473
|
+
};
|
|
474
|
+
safeDispatchCustomEvent(GraphEvents.ON_TOOL_EXECUTE, batchRequest, config);
|
|
475
|
+
});
|
|
476
|
+
this.storeCodeSessionFromResults(results, requestMap);
|
|
477
|
+
const hasPostHook = this.hookRegistry?.hasHookFor('PostToolUse', runId) === true;
|
|
478
|
+
const hasFailureHook = this.hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
|
|
479
|
+
for (const result of results) {
|
|
480
|
+
if (result.injectedMessages && result.injectedMessages.length > 0) {
|
|
481
|
+
try {
|
|
482
|
+
injected.push(...this.convertInjectedMessages(result.injectedMessages));
|
|
483
|
+
}
|
|
484
|
+
catch (e) {
|
|
485
|
+
// eslint-disable-next-line no-console
|
|
486
|
+
console.warn(`[ToolNode] Failed to convert injectedMessages for toolCallId=${result.toolCallId}:`, e instanceof Error ? e.message : e);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const request = requestMap.get(result.toolCallId);
|
|
490
|
+
const toolName = request?.name ?? 'unknown';
|
|
491
|
+
let contentString;
|
|
492
|
+
let toolMessage;
|
|
493
|
+
if (result.status === 'error') {
|
|
494
|
+
contentString = `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`;
|
|
495
|
+
toolMessage = new ToolMessage({
|
|
496
|
+
status: 'error',
|
|
497
|
+
content: contentString,
|
|
498
|
+
name: toolName,
|
|
499
|
+
tool_call_id: result.toolCallId,
|
|
500
|
+
});
|
|
501
|
+
if (hasFailureHook) {
|
|
502
|
+
await executeHooks({
|
|
503
|
+
registry: this.hookRegistry,
|
|
504
|
+
input: {
|
|
505
|
+
hook_event_name: 'PostToolUseFailure',
|
|
506
|
+
runId,
|
|
507
|
+
threadId,
|
|
508
|
+
agentId: this.agentId,
|
|
509
|
+
toolName,
|
|
510
|
+
toolInput: request?.args ?? {},
|
|
511
|
+
toolUseId: result.toolCallId,
|
|
512
|
+
error: result.errorMessage ?? 'Unknown error',
|
|
513
|
+
stepId: request?.stepId,
|
|
514
|
+
turn: request?.turn,
|
|
515
|
+
},
|
|
516
|
+
sessionId: runId,
|
|
517
|
+
matchQuery: toolName,
|
|
518
|
+
}).catch(() => {
|
|
519
|
+
/* PostToolUseFailure is observational — swallow errors */
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
const rawContent = typeof result.content === 'string'
|
|
525
|
+
? result.content
|
|
526
|
+
: JSON.stringify(result.content);
|
|
527
|
+
contentString = truncateToolResultContent(rawContent, this.maxToolResultChars);
|
|
528
|
+
if (hasPostHook) {
|
|
529
|
+
const hookResult = await executeHooks({
|
|
530
|
+
registry: this.hookRegistry,
|
|
531
|
+
input: {
|
|
532
|
+
hook_event_name: 'PostToolUse',
|
|
533
|
+
runId,
|
|
534
|
+
threadId,
|
|
535
|
+
agentId: this.agentId,
|
|
536
|
+
toolName,
|
|
537
|
+
toolInput: request?.args ?? {},
|
|
538
|
+
toolOutput: result.content,
|
|
539
|
+
toolUseId: result.toolCallId,
|
|
540
|
+
stepId: request?.stepId,
|
|
541
|
+
turn: request?.turn,
|
|
542
|
+
},
|
|
543
|
+
sessionId: runId,
|
|
544
|
+
matchQuery: toolName,
|
|
545
|
+
}).catch(() => undefined);
|
|
546
|
+
if (hookResult?.updatedOutput != null) {
|
|
547
|
+
const replaced = typeof hookResult.updatedOutput === 'string'
|
|
548
|
+
? hookResult.updatedOutput
|
|
549
|
+
: JSON.stringify(hookResult.updatedOutput);
|
|
550
|
+
contentString = truncateToolResultContent(replaced, this.maxToolResultChars);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
toolMessage = new ToolMessage({
|
|
554
|
+
status: 'success',
|
|
555
|
+
name: toolName,
|
|
556
|
+
content: contentString,
|
|
557
|
+
artifact: result.artifact,
|
|
558
|
+
tool_call_id: result.toolCallId,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
this.dispatchStepCompleted(result.toolCallId, toolName, request?.args ?? {}, contentString, config, request?.turn);
|
|
562
|
+
messageByCallId.set(result.toolCallId, toolMessage);
|
|
426
563
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
564
|
+
}
|
|
565
|
+
const toolMessages = toolCalls
|
|
566
|
+
.map((call) => messageByCallId.get(call.id))
|
|
567
|
+
.filter((m) => m != null);
|
|
568
|
+
return { toolMessages, injected };
|
|
569
|
+
}
|
|
570
|
+
dispatchStepCompleted(toolCallId, toolName, args, output, config, turn) {
|
|
571
|
+
const stepId = this.toolCallStepIds?.get(toolCallId) ?? '';
|
|
572
|
+
if (!stepId) {
|
|
573
|
+
// eslint-disable-next-line no-console
|
|
574
|
+
console.warn(`[ToolNode] toolCallStepIds missing entry for toolCallId=${toolCallId} (tool=${toolName}). ` +
|
|
575
|
+
'This indicates a race between the stream consumer and graph execution. ' +
|
|
576
|
+
`Map size: ${this.toolCallStepIds?.size ?? 0}`);
|
|
577
|
+
}
|
|
578
|
+
safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP_COMPLETED, {
|
|
579
|
+
result: {
|
|
580
|
+
id: stepId,
|
|
581
|
+
index: turn ?? this.toolUsageCount.get(toolName) ?? 0,
|
|
582
|
+
type: 'tool_call',
|
|
583
|
+
tool_call: {
|
|
584
|
+
args: JSON.stringify(args),
|
|
434
585
|
name: toolName,
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
const tool_call = {
|
|
441
|
-
args: typeof request?.args === 'string'
|
|
442
|
-
? request.args
|
|
443
|
-
: JSON.stringify(request?.args ?? {}),
|
|
444
|
-
name: toolName,
|
|
445
|
-
id: result.toolCallId,
|
|
446
|
-
output: contentString,
|
|
447
|
-
progress: 1,
|
|
448
|
-
};
|
|
449
|
-
const runStepCompletedData = {
|
|
450
|
-
result: {
|
|
451
|
-
id: stepId,
|
|
452
|
-
index: request?.turn ?? 0,
|
|
453
|
-
type: 'tool_call',
|
|
454
|
-
tool_call,
|
|
586
|
+
id: toolCallId,
|
|
587
|
+
output,
|
|
588
|
+
progress: 1,
|
|
455
589
|
},
|
|
590
|
+
},
|
|
591
|
+
}, config);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Converts InjectedMessage instances to LangChain HumanMessage objects.
|
|
595
|
+
* Both 'user' and 'system' roles become HumanMessage to avoid provider
|
|
596
|
+
* rejections (Anthropic/Google reject non-leading SystemMessages).
|
|
597
|
+
* The original role is preserved in additional_kwargs for downstream consumers.
|
|
598
|
+
*/
|
|
599
|
+
convertInjectedMessages(messages) {
|
|
600
|
+
const converted = [];
|
|
601
|
+
for (const msg of messages) {
|
|
602
|
+
const additional_kwargs = {
|
|
603
|
+
role: msg.role,
|
|
456
604
|
};
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
605
|
+
if (msg.isMeta != null)
|
|
606
|
+
additional_kwargs.isMeta = msg.isMeta;
|
|
607
|
+
if (msg.source != null)
|
|
608
|
+
additional_kwargs.source = msg.source;
|
|
609
|
+
if (msg.skillName != null)
|
|
610
|
+
additional_kwargs.skillName = msg.skillName;
|
|
611
|
+
converted.push(new HumanMessage({ content: msg.content, additional_kwargs }));
|
|
612
|
+
}
|
|
613
|
+
return converted;
|
|
460
614
|
}
|
|
461
615
|
/**
|
|
462
616
|
* Execute all tool calls via ON_TOOL_EXECUTE event dispatch.
|
|
463
|
-
*
|
|
617
|
+
* Injected messages are placed AFTER ToolMessages to respect provider
|
|
618
|
+
* message ordering (AIMessage tool_calls must be immediately followed
|
|
619
|
+
* by their ToolMessage results).
|
|
464
620
|
*/
|
|
465
621
|
async executeViaEvent(toolCalls, config,
|
|
466
622
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
467
623
|
input) {
|
|
468
|
-
const
|
|
624
|
+
const { toolMessages, injected } = await this.dispatchToolEvents(toolCalls, config);
|
|
625
|
+
const outputs = [...toolMessages, ...injected];
|
|
469
626
|
return (Array.isArray(input) ? outputs : { messages: outputs });
|
|
470
627
|
}
|
|
471
628
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -534,10 +691,17 @@ class ToolNode extends RunnableCallable {
|
|
|
534
691
|
if (directCalls.length > 0 && directOutputs.length > 0) {
|
|
535
692
|
this.handleRunToolCompletions(directCalls, directOutputs, config);
|
|
536
693
|
}
|
|
537
|
-
const
|
|
694
|
+
const eventResult = eventCalls.length > 0
|
|
538
695
|
? await this.dispatchToolEvents(eventCalls, config)
|
|
539
|
-
:
|
|
540
|
-
|
|
696
|
+
: {
|
|
697
|
+
toolMessages: [],
|
|
698
|
+
injected: [],
|
|
699
|
+
};
|
|
700
|
+
outputs = [
|
|
701
|
+
...directOutputs,
|
|
702
|
+
...eventResult.toolMessages,
|
|
703
|
+
...eventResult.injected,
|
|
704
|
+
];
|
|
541
705
|
}
|
|
542
706
|
else {
|
|
543
707
|
outputs = await Promise.all(filteredCalls.map((call) => this.runTool(call, config)));
|