@librechat/agents 3.1.68 → 3.1.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +23 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +16 -1
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +91 -0
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/hooks/HookRegistry.cjs +162 -0
  8. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
  9. package/dist/cjs/hooks/executeHooks.cjs +276 -0
  10. package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
  11. package/dist/cjs/hooks/matchers.cjs +256 -0
  12. package/dist/cjs/hooks/matchers.cjs.map +1 -0
  13. package/dist/cjs/hooks/types.cjs +27 -0
  14. package/dist/cjs/hooks/types.cjs.map +1 -0
  15. package/dist/cjs/main.cjs +53 -0
  16. package/dist/cjs/main.cjs.map +1 -1
  17. package/dist/cjs/messages/format.cjs +74 -12
  18. package/dist/cjs/messages/format.cjs.map +1 -1
  19. package/dist/cjs/run.cjs +111 -0
  20. package/dist/cjs/run.cjs.map +1 -1
  21. package/dist/cjs/summarization/node.cjs +44 -0
  22. package/dist/cjs/summarization/node.cjs.map +1 -1
  23. package/dist/cjs/tools/BashExecutor.cjs +165 -0
  24. package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
  25. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +287 -0
  26. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
  27. package/dist/cjs/tools/CodeExecutor.cjs +0 -9
  28. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  29. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +7 -23
  30. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  31. package/dist/cjs/tools/ReadFile.cjs +43 -0
  32. package/dist/cjs/tools/ReadFile.cjs.map +1 -0
  33. package/dist/cjs/tools/SkillTool.cjs +50 -0
  34. package/dist/cjs/tools/SkillTool.cjs.map +1 -0
  35. package/dist/cjs/tools/SubagentTool.cjs +92 -0
  36. package/dist/cjs/tools/SubagentTool.cjs.map +1 -0
  37. package/dist/cjs/tools/ToolNode.cjs +304 -140
  38. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolSearch.cjs +2 -13
  40. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  41. package/dist/cjs/tools/skillCatalog.cjs +84 -0
  42. package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
  43. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +511 -0
  44. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -0
  45. package/dist/esm/agents/AgentContext.mjs +23 -3
  46. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  47. package/dist/esm/common/enum.mjs +15 -2
  48. package/dist/esm/common/enum.mjs.map +1 -1
  49. package/dist/esm/graphs/Graph.mjs +91 -0
  50. package/dist/esm/graphs/Graph.mjs.map +1 -1
  51. package/dist/esm/hooks/HookRegistry.mjs +160 -0
  52. package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
  53. package/dist/esm/hooks/executeHooks.mjs +273 -0
  54. package/dist/esm/hooks/executeHooks.mjs.map +1 -0
  55. package/dist/esm/hooks/matchers.mjs +251 -0
  56. package/dist/esm/hooks/matchers.mjs.map +1 -0
  57. package/dist/esm/hooks/types.mjs +25 -0
  58. package/dist/esm/hooks/types.mjs.map +1 -0
  59. package/dist/esm/main.mjs +12 -1
  60. package/dist/esm/main.mjs.map +1 -1
  61. package/dist/esm/messages/format.mjs +66 -4
  62. package/dist/esm/messages/format.mjs.map +1 -1
  63. package/dist/esm/run.mjs +111 -0
  64. package/dist/esm/run.mjs.map +1 -1
  65. package/dist/esm/summarization/node.mjs +44 -0
  66. package/dist/esm/summarization/node.mjs.map +1 -1
  67. package/dist/esm/tools/BashExecutor.mjs +159 -0
  68. package/dist/esm/tools/BashExecutor.mjs.map +1 -0
  69. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +278 -0
  70. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
  71. package/dist/esm/tools/CodeExecutor.mjs +0 -9
  72. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  73. package/dist/esm/tools/ProgrammaticToolCalling.mjs +8 -24
  74. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  75. package/dist/esm/tools/ReadFile.mjs +38 -0
  76. package/dist/esm/tools/ReadFile.mjs.map +1 -0
  77. package/dist/esm/tools/SkillTool.mjs +45 -0
  78. package/dist/esm/tools/SkillTool.mjs.map +1 -0
  79. package/dist/esm/tools/SubagentTool.mjs +85 -0
  80. package/dist/esm/tools/SubagentTool.mjs.map +1 -0
  81. package/dist/esm/tools/ToolNode.mjs +306 -142
  82. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  83. package/dist/esm/tools/ToolSearch.mjs +3 -14
  84. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  85. package/dist/esm/tools/skillCatalog.mjs +82 -0
  86. package/dist/esm/tools/skillCatalog.mjs.map +1 -0
  87. package/dist/esm/tools/subagent/SubagentExecutor.mjs +505 -0
  88. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -0
  89. package/dist/types/agents/AgentContext.d.ts +6 -0
  90. package/dist/types/common/enum.d.ts +10 -2
  91. package/dist/types/graphs/Graph.d.ts +2 -0
  92. package/dist/types/hooks/HookRegistry.d.ts +56 -0
  93. package/dist/types/hooks/executeHooks.d.ts +79 -0
  94. package/dist/types/hooks/index.d.ts +6 -0
  95. package/dist/types/hooks/matchers.d.ts +95 -0
  96. package/dist/types/hooks/types.d.ts +320 -0
  97. package/dist/types/index.d.ts +8 -0
  98. package/dist/types/messages/format.d.ts +2 -1
  99. package/dist/types/run.d.ts +1 -0
  100. package/dist/types/summarization/node.d.ts +2 -0
  101. package/dist/types/tools/BashExecutor.d.ts +45 -0
  102. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
  103. package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -9
  104. package/dist/types/tools/ReadFile.d.ts +28 -0
  105. package/dist/types/tools/SkillTool.d.ts +40 -0
  106. package/dist/types/tools/SubagentTool.d.ts +36 -0
  107. package/dist/types/tools/ToolNode.d.ts +24 -2
  108. package/dist/types/tools/ToolSearch.d.ts +2 -2
  109. package/dist/types/tools/skillCatalog.d.ts +19 -0
  110. package/dist/types/tools/subagent/SubagentExecutor.d.ts +137 -0
  111. package/dist/types/tools/subagent/index.d.ts +2 -0
  112. package/dist/types/types/graph.d.ts +61 -2
  113. package/dist/types/types/index.d.ts +1 -0
  114. package/dist/types/types/run.d.ts +20 -0
  115. package/dist/types/types/skill.d.ts +9 -0
  116. package/dist/types/types/tools.d.ts +38 -10
  117. package/package.json +5 -1
  118. package/src/agents/AgentContext.ts +26 -2
  119. package/src/common/enum.ts +15 -1
  120. package/src/graphs/Graph.ts +113 -0
  121. package/src/hooks/HookRegistry.ts +208 -0
  122. package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
  123. package/src/hooks/__tests__/compactHooks.test.ts +214 -0
  124. package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
  125. package/src/hooks/__tests__/integration.test.ts +337 -0
  126. package/src/hooks/__tests__/matchers.test.ts +238 -0
  127. package/src/hooks/__tests__/toolHooks.test.ts +669 -0
  128. package/src/hooks/executeHooks.ts +375 -0
  129. package/src/hooks/index.ts +57 -0
  130. package/src/hooks/matchers.ts +280 -0
  131. package/src/hooks/types.ts +404 -0
  132. package/src/index.ts +10 -0
  133. package/src/messages/format.ts +74 -4
  134. package/src/messages/formatAgentMessages.skills.test.ts +334 -0
  135. package/src/run.ts +126 -0
  136. package/src/scripts/multi-agent-subagent.ts +246 -0
  137. package/src/scripts/programmatic_exec.ts +1 -10
  138. package/src/scripts/subagent-event-driven-debug.ts +190 -0
  139. package/src/scripts/subagent-tools-debug.ts +160 -0
  140. package/src/scripts/test_code_api.ts +0 -7
  141. package/src/scripts/tool_search.ts +1 -10
  142. package/src/specs/subagent.test.ts +305 -0
  143. package/src/summarization/node.ts +53 -0
  144. package/src/tools/BashExecutor.ts +193 -0
  145. package/src/tools/BashProgrammaticToolCalling.ts +381 -0
  146. package/src/tools/CodeExecutor.ts +0 -11
  147. package/src/tools/ProgrammaticToolCalling.ts +4 -29
  148. package/src/tools/ReadFile.ts +39 -0
  149. package/src/tools/SkillTool.ts +46 -0
  150. package/src/tools/SubagentTool.ts +100 -0
  151. package/src/tools/ToolNode.ts +391 -169
  152. package/src/tools/ToolSearch.ts +3 -19
  153. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +7 -8
  154. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +0 -1
  155. package/src/tools/__tests__/ReadFile.test.ts +44 -0
  156. package/src/tools/__tests__/SkillTool.test.ts +442 -0
  157. package/src/tools/__tests__/SubagentExecutor.test.ts +1148 -0
  158. package/src/tools/__tests__/SubagentTool.test.ts +149 -0
  159. package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
  160. package/src/tools/__tests__/ToolSearch.integration.test.ts +7 -8
  161. package/src/tools/__tests__/skillCatalog.test.ts +161 -0
  162. package/src/tools/__tests__/subagentHooks.test.ts +215 -0
  163. package/src/tools/skillCatalog.ts +126 -0
  164. package/src/tools/subagent/SubagentExecutor.ts +676 -0
  165. package/src/tools/subagent/index.ts +13 -0
  166. package/src/types/graph.ts +80 -1
  167. package/src/types/index.ts +1 -0
  168. package/src/types/run.ts +20 -0
  169. package/src/types/skill.ts +11 -0
  170. package/src/types/tools.ts +41 -10
@@ -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
- constructor({ tools, toolMap, name, tags, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, agentId, directToolNames, maxContextTokens, maxToolResultChars, }) {
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 === Constants.EXECUTE_CODE ||
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, requests) {
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 = requests.find((r) => r.id === result.toolCallId);
257
- if (request?.name !== Constants.EXECUTE_CODE &&
258
- request?.name !== Constants.PROGRAMMATIC_TOOL_CALLING) {
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
- const newFiles = artifact.files ?? [];
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
- // Store code session context from tool results
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
- const newFiles = artifact.files ?? [];
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 requests = toolCalls.map((call) => {
378
- const turn = this.toolUsageCount.get(call.name) ?? 0;
379
- this.toolUsageCount.set(call.name, turn + 1);
380
- const request = {
381
- id: call.id,
382
- name: call.name,
383
- args: call.args,
384
- stepId: this.toolCallStepIds?.get(call.id),
385
- turn,
386
- };
387
- if (call.name === Constants.EXECUTE_CODE ||
388
- call.name === Constants.PROGRAMMATIC_TOOL_CALLING) {
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.storeCodeSessionFromResults(results, requests);
406
- return results.map((result) => {
407
- const request = requests.find((r) => r.id === result.toolCallId);
408
- const toolName = request?.name ?? 'unknown';
409
- const stepId = this.toolCallStepIds?.get(result.toolCallId) ?? '';
410
- if (!stepId) {
411
- // eslint-disable-next-line no-console
412
- console.warn(`[ToolNode] toolCallStepIds missing entry for toolCallId=${result.toolCallId} (tool=${toolName}). ` +
413
- 'This indicates a race between the stream consumer and graph execution. ' +
414
- `Map size: ${this.toolCallStepIds?.size ?? 0}`);
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
- let toolMessage;
417
- let contentString;
418
- if (result.status === 'error') {
419
- contentString = `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`;
420
- toolMessage = new ToolMessage({
421
- status: 'error',
422
- content: contentString,
423
- name: toolName,
424
- tool_call_id: result.toolCallId,
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
- else {
428
- const rawContent = typeof result.content === 'string'
429
- ? result.content
430
- : JSON.stringify(result.content);
431
- contentString = truncateToolResultContent(rawContent, this.maxToolResultChars);
432
- toolMessage = new ToolMessage({
433
- status: 'success',
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
- content: contentString,
436
- artifact: result.artifact,
437
- tool_call_id: result.toolCallId,
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
- safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP_COMPLETED, runStepCompletedData, config);
458
- return toolMessage;
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
- * Used in event-driven mode where the host handles actual tool execution.
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 outputs = await this.dispatchToolEvents(toolCalls, config);
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 eventOutputs = eventCalls.length > 0
694
+ const eventResult = eventCalls.length > 0
538
695
  ? await this.dispatchToolEvents(eventCalls, config)
539
- : [];
540
- outputs = [...directOutputs, ...eventOutputs];
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)));