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