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