@librechat/agents 3.1.68 → 3.1.71-dev.0

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 (192) 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 +136 -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 +57 -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/messages/prune.cjs +9 -2
  20. package/dist/cjs/messages/prune.cjs.map +1 -1
  21. package/dist/cjs/run.cjs +115 -0
  22. package/dist/cjs/run.cjs.map +1 -1
  23. package/dist/cjs/summarization/node.cjs +44 -0
  24. package/dist/cjs/summarization/node.cjs.map +1 -1
  25. package/dist/cjs/tools/BashExecutor.cjs +208 -0
  26. package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
  27. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +287 -0
  28. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
  29. package/dist/cjs/tools/CodeExecutor.cjs +0 -9
  30. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  31. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +7 -23
  32. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  33. package/dist/cjs/tools/ReadFile.cjs +43 -0
  34. package/dist/cjs/tools/ReadFile.cjs.map +1 -0
  35. package/dist/cjs/tools/SkillTool.cjs +50 -0
  36. package/dist/cjs/tools/SkillTool.cjs.map +1 -0
  37. package/dist/cjs/tools/SubagentTool.cjs +92 -0
  38. package/dist/cjs/tools/SubagentTool.cjs.map +1 -0
  39. package/dist/cjs/tools/ToolNode.cjs +746 -174
  40. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  41. package/dist/cjs/tools/ToolSearch.cjs +2 -13
  42. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  43. package/dist/cjs/tools/skillCatalog.cjs +84 -0
  44. package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
  45. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +511 -0
  46. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -0
  47. package/dist/cjs/tools/toolOutputReferences.cjs +475 -0
  48. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -0
  49. package/dist/cjs/utils/truncation.cjs +28 -0
  50. package/dist/cjs/utils/truncation.cjs.map +1 -1
  51. package/dist/esm/agents/AgentContext.mjs +23 -3
  52. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  53. package/dist/esm/common/enum.mjs +15 -2
  54. package/dist/esm/common/enum.mjs.map +1 -1
  55. package/dist/esm/graphs/Graph.mjs +136 -0
  56. package/dist/esm/graphs/Graph.mjs.map +1 -1
  57. package/dist/esm/hooks/HookRegistry.mjs +160 -0
  58. package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
  59. package/dist/esm/hooks/executeHooks.mjs +273 -0
  60. package/dist/esm/hooks/executeHooks.mjs.map +1 -0
  61. package/dist/esm/hooks/matchers.mjs +251 -0
  62. package/dist/esm/hooks/matchers.mjs.map +1 -0
  63. package/dist/esm/hooks/types.mjs +25 -0
  64. package/dist/esm/hooks/types.mjs.map +1 -0
  65. package/dist/esm/main.mjs +13 -2
  66. package/dist/esm/main.mjs.map +1 -1
  67. package/dist/esm/messages/format.mjs +66 -4
  68. package/dist/esm/messages/format.mjs.map +1 -1
  69. package/dist/esm/messages/prune.mjs +9 -2
  70. package/dist/esm/messages/prune.mjs.map +1 -1
  71. package/dist/esm/run.mjs +115 -0
  72. package/dist/esm/run.mjs.map +1 -1
  73. package/dist/esm/summarization/node.mjs +44 -0
  74. package/dist/esm/summarization/node.mjs.map +1 -1
  75. package/dist/esm/tools/BashExecutor.mjs +200 -0
  76. package/dist/esm/tools/BashExecutor.mjs.map +1 -0
  77. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +278 -0
  78. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
  79. package/dist/esm/tools/CodeExecutor.mjs +0 -9
  80. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  81. package/dist/esm/tools/ProgrammaticToolCalling.mjs +8 -24
  82. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  83. package/dist/esm/tools/ReadFile.mjs +38 -0
  84. package/dist/esm/tools/ReadFile.mjs.map +1 -0
  85. package/dist/esm/tools/SkillTool.mjs +45 -0
  86. package/dist/esm/tools/SkillTool.mjs.map +1 -0
  87. package/dist/esm/tools/SubagentTool.mjs +85 -0
  88. package/dist/esm/tools/SubagentTool.mjs.map +1 -0
  89. package/dist/esm/tools/ToolNode.mjs +748 -176
  90. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  91. package/dist/esm/tools/ToolSearch.mjs +3 -14
  92. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  93. package/dist/esm/tools/skillCatalog.mjs +82 -0
  94. package/dist/esm/tools/skillCatalog.mjs.map +1 -0
  95. package/dist/esm/tools/subagent/SubagentExecutor.mjs +505 -0
  96. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -0
  97. package/dist/esm/tools/toolOutputReferences.mjs +468 -0
  98. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -0
  99. package/dist/esm/utils/truncation.mjs +27 -1
  100. package/dist/esm/utils/truncation.mjs.map +1 -1
  101. package/dist/types/agents/AgentContext.d.ts +6 -0
  102. package/dist/types/common/enum.d.ts +10 -2
  103. package/dist/types/graphs/Graph.d.ts +23 -0
  104. package/dist/types/hooks/HookRegistry.d.ts +56 -0
  105. package/dist/types/hooks/executeHooks.d.ts +79 -0
  106. package/dist/types/hooks/index.d.ts +6 -0
  107. package/dist/types/hooks/matchers.d.ts +95 -0
  108. package/dist/types/hooks/types.d.ts +320 -0
  109. package/dist/types/index.d.ts +8 -0
  110. package/dist/types/messages/format.d.ts +2 -1
  111. package/dist/types/run.d.ts +2 -0
  112. package/dist/types/summarization/node.d.ts +2 -0
  113. package/dist/types/tools/BashExecutor.d.ts +76 -0
  114. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
  115. package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -9
  116. package/dist/types/tools/ReadFile.d.ts +28 -0
  117. package/dist/types/tools/SkillTool.d.ts +40 -0
  118. package/dist/types/tools/SubagentTool.d.ts +36 -0
  119. package/dist/types/tools/ToolNode.d.ts +109 -4
  120. package/dist/types/tools/ToolSearch.d.ts +2 -2
  121. package/dist/types/tools/skillCatalog.d.ts +19 -0
  122. package/dist/types/tools/subagent/SubagentExecutor.d.ts +137 -0
  123. package/dist/types/tools/subagent/index.d.ts +2 -0
  124. package/dist/types/tools/toolOutputReferences.d.ts +205 -0
  125. package/dist/types/types/graph.d.ts +61 -2
  126. package/dist/types/types/index.d.ts +1 -0
  127. package/dist/types/types/run.d.ts +28 -0
  128. package/dist/types/types/skill.d.ts +9 -0
  129. package/dist/types/types/tools.d.ts +108 -10
  130. package/dist/types/utils/truncation.d.ts +21 -0
  131. package/package.json +5 -1
  132. package/src/agents/AgentContext.ts +26 -2
  133. package/src/common/enum.ts +15 -1
  134. package/src/graphs/Graph.ts +161 -0
  135. package/src/hooks/HookRegistry.ts +208 -0
  136. package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
  137. package/src/hooks/__tests__/compactHooks.test.ts +214 -0
  138. package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
  139. package/src/hooks/__tests__/integration.test.ts +337 -0
  140. package/src/hooks/__tests__/matchers.test.ts +238 -0
  141. package/src/hooks/__tests__/toolHooks.test.ts +669 -0
  142. package/src/hooks/executeHooks.ts +375 -0
  143. package/src/hooks/index.ts +57 -0
  144. package/src/hooks/matchers.ts +280 -0
  145. package/src/hooks/types.ts +404 -0
  146. package/src/index.ts +10 -0
  147. package/src/messages/format.ts +74 -4
  148. package/src/messages/formatAgentMessages.skills.test.ts +334 -0
  149. package/src/messages/prune.ts +9 -2
  150. package/src/run.ts +130 -0
  151. package/src/scripts/multi-agent-subagent.ts +246 -0
  152. package/src/scripts/programmatic_exec.ts +1 -10
  153. package/src/scripts/subagent-event-driven-debug.ts +190 -0
  154. package/src/scripts/subagent-tools-debug.ts +160 -0
  155. package/src/scripts/test_code_api.ts +0 -7
  156. package/src/scripts/tool_search.ts +1 -10
  157. package/src/specs/prune.test.ts +413 -0
  158. package/src/specs/subagent.test.ts +305 -0
  159. package/src/summarization/node.ts +53 -0
  160. package/src/tools/BashExecutor.ts +238 -0
  161. package/src/tools/BashProgrammaticToolCalling.ts +381 -0
  162. package/src/tools/CodeExecutor.ts +0 -11
  163. package/src/tools/ProgrammaticToolCalling.ts +4 -29
  164. package/src/tools/ReadFile.ts +39 -0
  165. package/src/tools/SkillTool.ts +46 -0
  166. package/src/tools/SubagentTool.ts +100 -0
  167. package/src/tools/ToolNode.ts +999 -214
  168. package/src/tools/ToolSearch.ts +3 -19
  169. package/src/tools/__tests__/BashExecutor.test.ts +36 -0
  170. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +7 -8
  171. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +0 -1
  172. package/src/tools/__tests__/ReadFile.test.ts +44 -0
  173. package/src/tools/__tests__/SkillTool.test.ts +442 -0
  174. package/src/tools/__tests__/SubagentExecutor.test.ts +1148 -0
  175. package/src/tools/__tests__/SubagentTool.test.ts +149 -0
  176. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +1395 -0
  177. package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
  178. package/src/tools/__tests__/ToolSearch.integration.test.ts +7 -8
  179. package/src/tools/__tests__/skillCatalog.test.ts +161 -0
  180. package/src/tools/__tests__/subagentHooks.test.ts +215 -0
  181. package/src/tools/__tests__/toolOutputReferences.test.ts +415 -0
  182. package/src/tools/skillCatalog.ts +126 -0
  183. package/src/tools/subagent/SubagentExecutor.ts +676 -0
  184. package/src/tools/subagent/index.ts +13 -0
  185. package/src/tools/toolOutputReferences.ts +590 -0
  186. package/src/types/graph.ts +80 -1
  187. package/src/types/index.ts +1 -0
  188. package/src/types/run.ts +28 -0
  189. package/src/types/skill.ts +11 -0
  190. package/src/types/tools.ts +112 -10
  191. package/src/utils/__tests__/truncation.test.ts +66 -0
  192. package/src/utils/truncation.ts +30 -0
@@ -11,6 +11,8 @@ 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');
15
+ var toolOutputReferences = require('./toolOutputReferences.cjs');
14
16
 
15
17
  /**
16
18
  * Helper to check if a value is a Send object
@@ -18,6 +20,32 @@ require('zod-to-json-schema');
18
20
  function isSend(value) {
19
21
  return value instanceof langgraph.Send;
20
22
  }
23
+ /** Merges code execution session context into the sessions map. */
24
+ function updateCodeSession(sessions, sessionId, files) {
25
+ const newFiles = files ?? [];
26
+ const existingSession = sessions.get(_enum.Constants.EXECUTE_CODE);
27
+ const existingFiles = existingSession?.files ?? [];
28
+ if (newFiles.length > 0) {
29
+ const filesWithSession = newFiles.map((file) => ({
30
+ ...file,
31
+ session_id: sessionId,
32
+ }));
33
+ const newFileNames = new Set(filesWithSession.map((f) => f.name));
34
+ const filteredExisting = existingFiles.filter((f) => !newFileNames.has(f.name));
35
+ sessions.set(_enum.Constants.EXECUTE_CODE, {
36
+ session_id: sessionId,
37
+ files: [...filteredExisting, ...filesWithSession],
38
+ lastUpdated: Date.now(),
39
+ });
40
+ }
41
+ else {
42
+ sessions.set(_enum.Constants.EXECUTE_CODE, {
43
+ session_id: sessionId,
44
+ files: existingFiles,
45
+ lastUpdated: Date.now(),
46
+ });
47
+ }
48
+ }
21
49
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
50
  class ToolNode extends run.RunnableCallable {
23
51
  toolMap;
@@ -43,7 +71,29 @@ class ToolNode extends run.RunnableCallable {
43
71
  directToolNames;
44
72
  /** Maximum characters allowed in a single tool result before truncation. */
45
73
  maxToolResultChars;
46
- constructor({ tools, toolMap, name, tags, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, agentId, directToolNames, maxContextTokens, maxToolResultChars, }) {
74
+ /** Hook registry for PreToolUse/PostToolUse lifecycle hooks */
75
+ hookRegistry;
76
+ /**
77
+ * Registry of tool outputs keyed by `tool<idx>turn<turn>`.
78
+ *
79
+ * Populated only when `toolOutputReferences.enabled` is true. The
80
+ * registry owns the run-scoped state (turn counter, last-seen runId,
81
+ * warn-once memo, stored outputs), so sharing a single instance
82
+ * across multiple ToolNodes in a run lets cross-agent `{{…}}`
83
+ * references resolve — which is why multi-agent graphs pass the
84
+ * *same* instance to every ToolNode they compile rather than each
85
+ * ToolNode building its own.
86
+ */
87
+ toolOutputRegistry;
88
+ /**
89
+ * Monotonic counter used to mint a unique scope id for anonymous
90
+ * batches (ones invoked without a `run_id` in
91
+ * `config.configurable`). Each such batch gets its own registry
92
+ * partition so concurrent anonymous invocations can't delete each
93
+ * other's in-flight state.
94
+ */
95
+ anonBatchCounter = 0;
96
+ constructor({ tools, toolMap, name, tags, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, agentId, directToolNames, maxContextTokens, maxToolResultChars, hookRegistry, toolOutputReferences: toolOutputReferences$1, toolOutputRegistry, }) {
47
97
  super({ name, tags, func: (input, config) => this.run(input, config) });
48
98
  this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
49
99
  this.toolCallStepIds = toolCallStepIds;
@@ -58,6 +108,38 @@ class ToolNode extends run.RunnableCallable {
58
108
  this.directToolNames = directToolNames;
59
109
  this.maxToolResultChars =
60
110
  maxToolResultChars ?? truncation.calculateMaxToolResultChars(maxContextTokens);
111
+ this.hookRegistry = hookRegistry;
112
+ /**
113
+ * Precedence: an explicitly passed `toolOutputRegistry` instance
114
+ * wins over a config object so a host (`Graph`) can share one
115
+ * registry across many ToolNodes. When only the config is
116
+ * provided (direct ToolNode usage), build a local registry so
117
+ * the feature still works without graph-level plumbing. Registry
118
+ * caps are intentionally decoupled from `maxToolResultChars`:
119
+ * the registry stores the raw untruncated output so a later
120
+ * `{{…}}` substitution pipes the full payload into the next
121
+ * tool, even when the LLM saw a truncated preview.
122
+ */
123
+ if (toolOutputRegistry != null) {
124
+ this.toolOutputRegistry = toolOutputRegistry;
125
+ }
126
+ else if (toolOutputReferences$1?.enabled === true) {
127
+ this.toolOutputRegistry = new toolOutputReferences.ToolOutputReferenceRegistry({
128
+ maxOutputSize: toolOutputReferences$1.maxOutputSize,
129
+ maxTotalSize: toolOutputReferences$1.maxTotalSize,
130
+ });
131
+ }
132
+ }
133
+ /**
134
+ * Returns the run-scoped tool output registry, or `undefined` when
135
+ * the feature is disabled.
136
+ *
137
+ * @internal Exposed for test observation only. Host code should rely
138
+ * on `{{tool<i>turn<n>}}` substitution at tool-invocation time and
139
+ * not mutate the registry directly.
140
+ */
141
+ _unsafeGetToolOutputRegistry() {
142
+ return this.toolOutputRegistry;
61
143
  }
62
144
  /**
63
145
  * Returns cached programmatic tools, computing once on first access.
@@ -89,31 +171,89 @@ class ToolNode extends run.RunnableCallable {
89
171
  return new Map(this.toolUsageCount); // Return a copy
90
172
  }
91
173
  /**
92
- * Runs a single tool call with error handling
174
+ * Runs a single tool call with error handling.
175
+ *
176
+ * `batchIndex` is the tool's position within the current ToolNode
177
+ * batch and, together with `this.currentTurn`, forms the key used to
178
+ * register the output for future `{{tool<idx>turn<turn>}}`
179
+ * substitutions. Omit when no registration should occur.
93
180
  */
94
- async runTool(call, config) {
181
+ async runTool(call, config, batchContext = {}) {
182
+ const { batchIndex, turn, batchScopeId, resolvedArgsByCallId } = batchContext;
95
183
  const tool = this.toolMap.get(call.name);
184
+ const registry = this.toolOutputRegistry;
185
+ /**
186
+ * Precompute the reference key once per call — captured locally
187
+ * so concurrent `invoke()` calls on the same ToolNode cannot race
188
+ * on a shared turn field.
189
+ */
190
+ const refKey = registry != null && batchIndex != null && turn != null
191
+ ? toolOutputReferences.buildReferenceKey(batchIndex, turn)
192
+ : undefined;
193
+ /**
194
+ * Hoisted outside the try so the catch branch can append
195
+ * `[unresolved refs: …]` to error messages — otherwise the LLM
196
+ * only sees a generic error when it references a bad key, losing
197
+ * the self-correction signal this feature is meant to provide.
198
+ */
199
+ let unresolvedRefs = [];
200
+ /**
201
+ * Use the caller-provided `batchScopeId` when threaded from
202
+ * `run()` (so anonymous batches get their own unique scope).
203
+ * Fall back to the config's `run_id` when runTool is invoked
204
+ * from a context that doesn't thread it — that still preserves
205
+ * the runId-based partitioning for named runs.
206
+ */
207
+ const runId = batchScopeId ?? config.configurable?.run_id;
96
208
  try {
97
209
  if (tool === undefined) {
98
210
  throw new Error(`Tool "${call.name}" not found.`);
99
211
  }
100
- const turn = this.toolUsageCount.get(call.name) ?? 0;
101
- this.toolUsageCount.set(call.name, turn + 1);
212
+ /**
213
+ * `usageCount` is the per-tool-name invocation index that
214
+ * web-search and other tools observe via `invokeParams.turn`.
215
+ * It is intentionally distinct from the outer `turn` parameter
216
+ * (the batch turn used for ref keys); the latter is captured
217
+ * before the try block when constructing `refKey`.
218
+ */
219
+ const usageCount = this.toolUsageCount.get(call.name) ?? 0;
220
+ this.toolUsageCount.set(call.name, usageCount + 1);
102
221
  if (call.id != null && call.id !== '') {
103
- this.toolCallTurns.set(call.id, turn);
222
+ this.toolCallTurns.set(call.id, usageCount);
223
+ }
224
+ let args = call.args;
225
+ if (registry != null) {
226
+ const { resolved, unresolved } = registry.resolve(runId, args);
227
+ args = resolved;
228
+ unresolvedRefs = unresolved;
229
+ /**
230
+ * Expose the post-substitution args to downstream completion
231
+ * events so audit logs / host-side `ON_RUN_STEP_COMPLETED`
232
+ * handlers observe what actually ran, not the `{{…}}`
233
+ * template. Only string/object args are worth recording.
234
+ */
235
+ if (resolvedArgsByCallId != null &&
236
+ call.id != null &&
237
+ call.id !== '' &&
238
+ resolved !== call.args &&
239
+ typeof resolved === 'object') {
240
+ resolvedArgsByCallId.set(call.id, resolved);
241
+ }
104
242
  }
105
- const args = call.args;
106
243
  const stepId = this.toolCallStepIds?.get(call.id);
107
244
  // Build invoke params - LangChain extracts non-schema fields to config.toolCall
245
+ // `turn` here is the per-tool usage count (matches what tools have
246
+ // observed historically via config.toolCall.turn — e.g. web search).
108
247
  let invokeParams = {
109
248
  ...call,
110
249
  args,
111
250
  type: 'tool_call',
112
251
  stepId,
113
- turn,
252
+ turn: usageCount,
114
253
  };
115
254
  // Inject runtime data for special tools (becomes available at config.toolCall)
116
- if (call.name === _enum.Constants.PROGRAMMATIC_TOOL_CALLING) {
255
+ if (call.name === _enum.Constants.PROGRAMMATIC_TOOL_CALLING ||
256
+ call.name === _enum.Constants.BASH_PROGRAMMATIC_TOOL_CALLING) {
117
257
  const { toolMap, toolDefs } = this.getProgrammaticTools();
118
258
  invokeParams = {
119
259
  ...invokeParams,
@@ -136,8 +276,7 @@ class ToolNode extends run.RunnableCallable {
136
276
  * session_id is always injected when available (even without tracked files)
137
277
  * so the CodeExecutor can fall back to the /files endpoint for session continuity.
138
278
  */
139
- if (call.name === _enum.Constants.EXECUTE_CODE ||
140
- call.name === _enum.Constants.PROGRAMMATIC_TOOL_CALLING) {
279
+ if (_enum.CODE_EXECUTION_TOOLS.has(call.name)) {
141
280
  const codeSession = this.sessions?.get(_enum.Constants.EXECUTE_CODE);
142
281
  if (codeSession?.session_id != null && codeSession.session_id !== '') {
143
282
  invokeParams = {
@@ -155,19 +294,72 @@ class ToolNode extends run.RunnableCallable {
155
294
  }
156
295
  }
157
296
  const output = await tool.invoke(invokeParams, config);
158
- if ((messages.isBaseMessage(output) && output._getType() === 'tool') ||
159
- langgraph.isCommand(output)) {
297
+ if (langgraph.isCommand(output)) {
160
298
  return output;
161
299
  }
162
- else {
163
- const rawContent = typeof output === 'string' ? output : JSON.stringify(output);
164
- return new messages.ToolMessage({
165
- status: 'success',
166
- name: tool.name,
167
- content: truncation.truncateToolResultContent(rawContent, this.maxToolResultChars),
168
- tool_call_id: call.id,
169
- });
300
+ if (messages.isBaseMessage(output) && output._getType() === 'tool') {
301
+ const toolMsg = output;
302
+ const isError = toolMsg.status === 'error';
303
+ if (isError) {
304
+ /**
305
+ * Error ToolMessages bypass registration/annotation but must
306
+ * still carry the unresolved-refs hint so the LLM can
307
+ * self-correct when its reference key caused the failure.
308
+ */
309
+ if (unresolvedRefs.length > 0 &&
310
+ typeof toolMsg.content === 'string') {
311
+ toolMsg.content = this.applyOutputReference(runId, toolMsg.content, toolMsg.content, undefined, unresolvedRefs);
312
+ }
313
+ return toolMsg;
314
+ }
315
+ if (this.toolOutputRegistry != null || unresolvedRefs.length > 0) {
316
+ if (typeof toolMsg.content === 'string') {
317
+ const rawContent = toolMsg.content;
318
+ const llmContent = truncation.truncateToolResultContent(rawContent, this.maxToolResultChars);
319
+ toolMsg.content = this.applyOutputReference(runId, llmContent, rawContent, refKey, unresolvedRefs);
320
+ }
321
+ else {
322
+ /**
323
+ * Non-string content (multi-part content blocks — text +
324
+ * image). Known limitation: we cannot register under a
325
+ * reference key because there's no canonical serialized
326
+ * form. Warn once per tool per run when the caller
327
+ * intended to register.
328
+ *
329
+ * Still surface unresolved-ref warnings so the LLM gets
330
+ * the self-correction signal that the string and error
331
+ * paths already emit. Prepended as a leading text block
332
+ * to keep the original content ordering intact.
333
+ */
334
+ if (unresolvedRefs.length > 0 && Array.isArray(toolMsg.content)) {
335
+ const warningBlock = {
336
+ type: 'text',
337
+ text: `[unresolved refs: ${unresolvedRefs.join(', ')}]`,
338
+ };
339
+ toolMsg.content = [
340
+ warningBlock,
341
+ ...toolMsg.content,
342
+ ];
343
+ }
344
+ if (refKey != null &&
345
+ this.toolOutputRegistry.claimWarnOnce(runId, call.name)) {
346
+ // eslint-disable-next-line no-console
347
+ console.warn(`[ToolNode] Skipping tool output reference for "${call.name}": ` +
348
+ 'ToolMessage content is not a string (further occurrences for this tool in the same run are suppressed).');
349
+ }
350
+ }
351
+ }
352
+ return toolMsg;
170
353
  }
354
+ const rawContent = typeof output === 'string' ? output : JSON.stringify(output);
355
+ const truncated = truncation.truncateToolResultContent(rawContent, this.maxToolResultChars);
356
+ const content = this.applyOutputReference(runId, truncated, rawContent, refKey, unresolvedRefs);
357
+ return new messages.ToolMessage({
358
+ status: 'success',
359
+ name: tool.name,
360
+ content,
361
+ tool_call_id: call.id,
362
+ });
171
363
  }
172
364
  catch (_e) {
173
365
  const e = _e;
@@ -210,14 +402,52 @@ class ToolNode extends run.RunnableCallable {
210
402
  });
211
403
  }
212
404
  }
405
+ let errorContent = `Error: ${e.message}\n Please fix your mistakes.`;
406
+ if (unresolvedRefs.length > 0) {
407
+ errorContent = this.applyOutputReference(runId, errorContent, errorContent, undefined, unresolvedRefs);
408
+ }
213
409
  return new messages.ToolMessage({
214
410
  status: 'error',
215
- content: `Error: ${e.message}\n Please fix your mistakes.`,
411
+ content: errorContent,
216
412
  name: call.name,
217
413
  tool_call_id: call.id ?? '',
218
414
  });
219
415
  }
220
416
  }
417
+ /**
418
+ * Finalizes the LLM-visible content for a tool call and (when a
419
+ * `refKey` is provided) registers the full, raw output under that
420
+ * key.
421
+ *
422
+ * @param llmContent The content string the LLM will see. This is
423
+ * the already-truncated, post-hook view; the annotation is
424
+ * applied on top of it.
425
+ * @param registryContent The full, untruncated output to store in
426
+ * the registry so `{{tool<i>turn<n>}}` substitutions deliver the
427
+ * complete payload. Ignored when `refKey` is undefined.
428
+ * @param refKey Precomputed `tool<i>turn<n>` key, or undefined when
429
+ * the output is not to be registered (errors, disabled feature,
430
+ * unavailable batch/turn).
431
+ * @param unresolved Placeholder keys that did not resolve; appended
432
+ * as `[unresolved refs: …]` so the LLM can self-correct.
433
+ *
434
+ * `refKey` is passed in (rather than built from `this.currentTurn`)
435
+ * so parallel `invoke()` calls on the same ToolNode cannot race on
436
+ * the shared turn field.
437
+ */
438
+ applyOutputReference(runId, llmContent, registryContent, refKey, unresolved) {
439
+ if (this.toolOutputRegistry != null && refKey != null) {
440
+ this.toolOutputRegistry.set(runId, refKey, registryContent);
441
+ }
442
+ /**
443
+ * `annotateToolOutputWithReference` handles both the ref-key and
444
+ * unresolved-refs cases together so JSON-object outputs stay
445
+ * parseable: unresolved refs land in an `_unresolved_refs` field
446
+ * instead of as a trailing text line that would break
447
+ * `JSON.parse` for downstream consumers.
448
+ */
449
+ return toolOutputReferences.annotateToolOutputWithReference(llmContent, refKey, unresolved);
450
+ }
221
451
  /**
222
452
  * Builds code session context for injection into event-driven tool calls.
223
453
  * Mirrors the session injection logic in runTool() for direct execution.
@@ -246,7 +476,7 @@ class ToolNode extends run.RunnableCallable {
246
476
  * Extracts code execution session context from tool results and stores in Graph.sessions.
247
477
  * Mirrors the session storage logic in handleRunToolCompletions for direct execution.
248
478
  */
249
- storeCodeSessionFromResults(results, requests) {
479
+ storeCodeSessionFromResults(results, requestMap) {
250
480
  if (!this.sessions) {
251
481
  return;
252
482
  }
@@ -255,38 +485,17 @@ class ToolNode extends run.RunnableCallable {
255
485
  if (result.status !== 'success' || result.artifact == null) {
256
486
  continue;
257
487
  }
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) {
488
+ const request = requestMap.get(result.toolCallId);
489
+ if (!request?.name ||
490
+ (!_enum.CODE_EXECUTION_TOOLS.has(request.name) &&
491
+ request.name !== _enum.Constants.SKILL_TOOL)) {
261
492
  continue;
262
493
  }
263
494
  const artifact = result.artifact;
264
495
  if (artifact?.session_id == null || artifact.session_id === '') {
265
496
  continue;
266
497
  }
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
- }
498
+ updateCodeSession(this.sessions, artifact.session_id, artifact.files);
290
499
  }
291
500
  }
292
501
  /**
@@ -297,8 +506,12 @@ class ToolNode extends run.RunnableCallable {
297
506
  * By handling completions here in graph context (rather than in the
298
507
  * stream consumer via ToolEndHandler), the race between the stream
299
508
  * consumer and graph execution is eliminated.
509
+ *
510
+ * @param resolvedArgsByCallId Per-batch resolved-args sink populated
511
+ * by `runTool`. Threaded as a local map (instead of instance state)
512
+ * so concurrent batches cannot read each other's entries.
300
513
  */
301
- handleRunToolCompletions(calls, outputs, config) {
514
+ handleRunToolCompletions(calls, outputs, config, resolvedArgsByCallId) {
302
515
  for (let i = 0; i < calls.length; i++) {
303
516
  const call = calls[i];
304
517
  const output = outputs[i];
@@ -313,35 +526,10 @@ class ToolNode extends run.RunnableCallable {
313
526
  if (toolMessage.status === 'error' && this.errorHandler != null) {
314
527
  continue;
315
528
  }
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)) {
529
+ if (this.sessions && _enum.CODE_EXECUTION_TOOLS.has(call.name)) {
320
530
  const artifact = toolMessage.artifact;
321
531
  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
- }
532
+ updateCodeSession(this.sessions, artifact.session_id, artifact.files);
345
533
  }
346
534
  }
347
535
  // Dispatch ON_RUN_STEP_COMPLETED via custom event (same path as dispatchToolEvents)
@@ -352,10 +540,17 @@ class ToolNode extends run.RunnableCallable {
352
540
  const contentString = typeof toolMessage.content === 'string'
353
541
  ? toolMessage.content
354
542
  : JSON.stringify(toolMessage.content);
543
+ /**
544
+ * Prefer the post-substitution args when a `{{…}}` placeholder
545
+ * was resolved in `runTool`. This keeps
546
+ * `ON_RUN_STEP_COMPLETED.tool_call.args` consistent with what
547
+ * the tool actually received rather than leaking the template.
548
+ */
549
+ const effectiveArgs = resolvedArgsByCallId?.get(toolCallId) ?? call.args;
355
550
  const tool_call = {
356
- args: typeof call.args === 'string'
357
- ? call.args
358
- : JSON.stringify(call.args ?? {}),
551
+ args: typeof effectiveArgs === 'string'
552
+ ? effectiveArgs
553
+ : JSON.stringify(effectiveArgs ?? {}),
359
554
  name: call.name,
360
555
  id: toolCallId,
361
556
  output: contentString,
@@ -374,113 +569,415 @@ class ToolNode extends run.RunnableCallable {
374
569
  /**
375
570
  * Dispatches tool calls to the host via ON_TOOL_EXECUTE event and returns raw ToolMessages.
376
571
  * Core logic for event-driven execution, separated from output shaping.
572
+ *
573
+ * Hook lifecycle (when `hookRegistry` is set):
574
+ * 1. **PreToolUse** fires per call in parallel before dispatch. Denied
575
+ * calls produce error ToolMessages and fire **PermissionDenied**;
576
+ * surviving calls proceed with optional `updatedInput`.
577
+ * 2. Surviving calls are dispatched to the host via `ON_TOOL_EXECUTE`.
578
+ * 3. **PostToolUse** / **PostToolUseFailure** fire per result. Post hooks
579
+ * can replace tool output via `updatedOutput`.
580
+ * 4. Injected messages from results are collected and returned alongside
581
+ * ToolMessages (appended AFTER to respect provider ordering).
377
582
  */
378
- 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();
583
+ async dispatchToolEvents(toolCalls, config, batchContext = {}) {
584
+ const { batchIndices, turn, batchScopeId, preResolvedArgs, preBatchSnapshot, } = batchContext;
585
+ const runId = config.configurable?.run_id ?? '';
586
+ /**
587
+ * Registry-facing scope id — prefers the caller-threaded
588
+ * `batchScopeId` so anonymous batches target their own unique
589
+ * bucket and don't step on concurrent anonymous invocations.
590
+ * Hooks and event payloads keep using the empty-string coerced
591
+ * `runId` for backward compat.
592
+ */
593
+ const registryRunId = batchScopeId ?? config.configurable?.run_id;
594
+ const threadId = config.configurable?.thread_id;
595
+ const registry = this.toolOutputRegistry;
596
+ const unresolvedByCallId = new Map();
597
+ const preToolCalls = toolCalls.map((call, i) => {
598
+ const originalArgs = call.args;
599
+ let resolvedArgs = originalArgs;
600
+ /**
601
+ * When the caller provided a pre-resolved map (the mixed
602
+ * direct+event path snapshots event args synchronously before
603
+ * awaiting directs so they can't accidentally resolve
604
+ * same-turn direct outputs), use those entries verbatim instead
605
+ * of re-resolving against a registry that may have changed
606
+ * since the batch started.
607
+ */
608
+ const pre = call.id != null ? preResolvedArgs?.get(call.id) : undefined;
609
+ if (pre != null) {
610
+ resolvedArgs = pre.resolved;
611
+ if (pre.unresolved.length > 0 && call.id != null) {
612
+ unresolvedByCallId.set(call.id, pre.unresolved);
613
+ }
392
614
  }
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,
615
+ else if (registry != null) {
616
+ const { resolved, unresolved } = registry.resolve(registryRunId, originalArgs);
617
+ resolvedArgs = resolved;
618
+ if (unresolved.length > 0 && call.id != null) {
619
+ unresolvedByCallId.set(call.id, unresolved);
620
+ }
621
+ }
622
+ return {
623
+ call,
624
+ stepId: this.toolCallStepIds?.get(call.id) ?? '',
625
+ args: resolvedArgs,
626
+ batchIndex: batchIndices?.[i],
404
627
  };
405
- events.safeDispatchCustomEvent(_enum.GraphEvents.ON_TOOL_EXECUTE, request, config);
406
628
  });
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}`);
417
- }
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
- });
629
+ const messageByCallId = new Map();
630
+ const approvedEntries = [];
631
+ const HOOK_FALLBACK = Object.freeze({
632
+ additionalContexts: [],
633
+ errors: [],
634
+ });
635
+ if (this.hookRegistry?.hasHookFor('PreToolUse', runId) === true) {
636
+ const preResults = await Promise.all(preToolCalls.map((entry) => executeHooks.executeHooks({
637
+ registry: this.hookRegistry,
638
+ input: {
639
+ hook_event_name: 'PreToolUse',
640
+ runId,
641
+ threadId,
642
+ agentId: this.agentId,
643
+ toolName: entry.call.name,
644
+ toolInput: entry.args,
645
+ toolUseId: entry.call.id,
646
+ stepId: entry.stepId,
647
+ turn: this.toolUsageCount.get(entry.call.name) ?? 0,
648
+ },
649
+ sessionId: runId,
650
+ matchQuery: entry.call.name,
651
+ }).catch(() => HOOK_FALLBACK)));
652
+ for (let i = 0; i < preToolCalls.length; i++) {
653
+ const hookResult = preResults[i];
654
+ const entry = preToolCalls[i];
655
+ const isDenied = hookResult.decision === 'deny' || hookResult.decision === 'ask';
656
+ if (isDenied) {
657
+ const reason = hookResult.reason ?? 'Blocked by hook';
658
+ const contentString = `Blocked: ${reason}`;
659
+ messageByCallId.set(entry.call.id, new messages.ToolMessage({
660
+ status: 'error',
661
+ content: contentString,
662
+ name: entry.call.name,
663
+ tool_call_id: entry.call.id,
664
+ }));
665
+ this.dispatchStepCompleted(entry.call.id, entry.call.name, entry.args, contentString, config);
666
+ if (this.hookRegistry.hasHookFor('PermissionDenied', runId)) {
667
+ executeHooks.executeHooks({
668
+ registry: this.hookRegistry,
669
+ input: {
670
+ hook_event_name: 'PermissionDenied',
671
+ runId,
672
+ threadId,
673
+ agentId: this.agentId,
674
+ toolName: entry.call.name,
675
+ toolInput: entry.args,
676
+ toolUseId: entry.call.id,
677
+ reason,
678
+ },
679
+ sessionId: runId,
680
+ matchQuery: entry.call.name,
681
+ }).catch(() => {
682
+ /* PermissionDenied is observational — swallow errors */
683
+ });
684
+ }
685
+ continue;
686
+ }
687
+ if (hookResult.updatedInput != null) {
688
+ /**
689
+ * Re-resolve after PreToolUse replaces the input: a hook may
690
+ * introduce new `{{tool<i>turn<n>}}` placeholders (e.g., by
691
+ * copying user-supplied text) that the pre-hook pass never
692
+ * saw. Re-running the resolver on the hook-rewritten args
693
+ * keeps substitution and the unresolved-refs record in sync
694
+ * with what the tool will actually receive.
695
+ */
696
+ if (registry != null) {
697
+ /**
698
+ * Mixed direct+event batches must use the pre-batch
699
+ * snapshot so a hook-introduced placeholder cannot
700
+ * accidentally resolve to a same-turn direct output that
701
+ * has just registered. Pure event batches don't have a
702
+ * snapshot and resolve against the live registry — safe
703
+ * because no event-side registrations have happened yet.
704
+ */
705
+ const view = preBatchSnapshot ?? {
706
+ resolve: (args) => registry.resolve(registryRunId, args),
707
+ };
708
+ const { resolved, unresolved } = view.resolve(hookResult.updatedInput);
709
+ entry.args = resolved;
710
+ if (entry.call.id != null) {
711
+ if (unresolved.length > 0) {
712
+ unresolvedByCallId.set(entry.call.id, unresolved);
713
+ }
714
+ else {
715
+ unresolvedByCallId.delete(entry.call.id);
716
+ }
717
+ }
718
+ }
719
+ else {
720
+ entry.args = hookResult.updatedInput;
721
+ }
722
+ }
723
+ approvedEntries.push(entry);
428
724
  }
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',
436
- name: toolName,
437
- content: contentString,
438
- artifact: result.artifact,
439
- tool_call_id: result.toolCallId,
440
- });
725
+ }
726
+ else {
727
+ approvedEntries.push(...preToolCalls);
728
+ }
729
+ const injected = [];
730
+ const batchIndexByCallId = new Map();
731
+ if (approvedEntries.length > 0) {
732
+ const requests = approvedEntries.map((entry) => {
733
+ const turn = this.toolUsageCount.get(entry.call.name) ?? 0;
734
+ this.toolUsageCount.set(entry.call.name, turn + 1);
735
+ if (entry.batchIndex != null && entry.call.id != null) {
736
+ batchIndexByCallId.set(entry.call.id, entry.batchIndex);
737
+ }
738
+ const request = {
739
+ id: entry.call.id,
740
+ name: entry.call.name,
741
+ args: entry.args,
742
+ stepId: entry.stepId,
743
+ turn,
744
+ };
745
+ if (_enum.CODE_EXECUTION_TOOLS.has(entry.call.name) ||
746
+ entry.call.name === _enum.Constants.SKILL_TOOL) {
747
+ request.codeSessionContext = this.getCodeSessionContext();
748
+ }
749
+ return request;
750
+ });
751
+ const requestMap = new Map(requests.map((r) => [r.id, r]));
752
+ const results = await new Promise((resolve, reject) => {
753
+ const batchRequest = {
754
+ toolCalls: requests,
755
+ userId: config.configurable?.user_id,
756
+ agentId: this.agentId,
757
+ configurable: config.configurable,
758
+ metadata: config.metadata,
759
+ resolve,
760
+ reject,
761
+ };
762
+ events.safeDispatchCustomEvent(_enum.GraphEvents.ON_TOOL_EXECUTE, batchRequest, config);
763
+ });
764
+ this.storeCodeSessionFromResults(results, requestMap);
765
+ const hasPostHook = this.hookRegistry?.hasHookFor('PostToolUse', runId) === true;
766
+ const hasFailureHook = this.hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
767
+ for (const result of results) {
768
+ if (result.injectedMessages && result.injectedMessages.length > 0) {
769
+ try {
770
+ injected.push(...this.convertInjectedMessages(result.injectedMessages));
771
+ }
772
+ catch (e) {
773
+ // eslint-disable-next-line no-console
774
+ console.warn(`[ToolNode] Failed to convert injectedMessages for toolCallId=${result.toolCallId}:`, e instanceof Error ? e.message : e);
775
+ }
776
+ }
777
+ const request = requestMap.get(result.toolCallId);
778
+ const toolName = request?.name ?? 'unknown';
779
+ let contentString;
780
+ let toolMessage;
781
+ if (result.status === 'error') {
782
+ contentString = `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`;
783
+ /**
784
+ * Error results bypass registration/annotation but must still
785
+ * carry the unresolved-refs hint so the LLM can self-correct
786
+ * when its reference key caused the failure.
787
+ */
788
+ const unresolved = unresolvedByCallId.get(result.toolCallId) ?? [];
789
+ if (unresolved.length > 0) {
790
+ contentString = this.applyOutputReference(registryRunId, contentString, contentString, undefined, unresolved);
791
+ }
792
+ toolMessage = new messages.ToolMessage({
793
+ status: 'error',
794
+ content: contentString,
795
+ name: toolName,
796
+ tool_call_id: result.toolCallId,
797
+ });
798
+ if (hasFailureHook) {
799
+ await executeHooks.executeHooks({
800
+ registry: this.hookRegistry,
801
+ input: {
802
+ hook_event_name: 'PostToolUseFailure',
803
+ runId,
804
+ threadId,
805
+ agentId: this.agentId,
806
+ toolName,
807
+ toolInput: request?.args ?? {},
808
+ toolUseId: result.toolCallId,
809
+ error: result.errorMessage ?? 'Unknown error',
810
+ stepId: request?.stepId,
811
+ turn: request?.turn,
812
+ },
813
+ sessionId: runId,
814
+ matchQuery: toolName,
815
+ }).catch(() => {
816
+ /* PostToolUseFailure is observational — swallow errors */
817
+ });
818
+ }
819
+ }
820
+ else {
821
+ let registryRaw = typeof result.content === 'string'
822
+ ? result.content
823
+ : JSON.stringify(result.content);
824
+ contentString = truncation.truncateToolResultContent(registryRaw, this.maxToolResultChars);
825
+ if (hasPostHook) {
826
+ const hookResult = await executeHooks.executeHooks({
827
+ registry: this.hookRegistry,
828
+ input: {
829
+ hook_event_name: 'PostToolUse',
830
+ runId,
831
+ threadId,
832
+ agentId: this.agentId,
833
+ toolName,
834
+ toolInput: request?.args ?? {},
835
+ toolOutput: result.content,
836
+ toolUseId: result.toolCallId,
837
+ stepId: request?.stepId,
838
+ turn: request?.turn,
839
+ },
840
+ sessionId: runId,
841
+ matchQuery: toolName,
842
+ }).catch(() => undefined);
843
+ if (hookResult?.updatedOutput != null) {
844
+ const replaced = typeof hookResult.updatedOutput === 'string'
845
+ ? hookResult.updatedOutput
846
+ : JSON.stringify(hookResult.updatedOutput);
847
+ registryRaw = replaced;
848
+ contentString = truncation.truncateToolResultContent(replaced, this.maxToolResultChars);
849
+ }
850
+ }
851
+ const batchIndex = batchIndexByCallId.get(result.toolCallId);
852
+ const unresolved = unresolvedByCallId.get(result.toolCallId) ?? [];
853
+ const refKey = this.toolOutputRegistry != null &&
854
+ batchIndex != null &&
855
+ turn != null
856
+ ? toolOutputReferences.buildReferenceKey(batchIndex, turn)
857
+ : undefined;
858
+ contentString = this.applyOutputReference(registryRunId, contentString, registryRaw, refKey, unresolved);
859
+ toolMessage = new messages.ToolMessage({
860
+ status: 'success',
861
+ name: toolName,
862
+ content: contentString,
863
+ artifact: result.artifact,
864
+ tool_call_id: result.toolCallId,
865
+ });
866
+ }
867
+ this.dispatchStepCompleted(result.toolCallId, toolName, request?.args ?? {}, contentString, config, request?.turn);
868
+ messageByCallId.set(result.toolCallId, toolMessage);
441
869
  }
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,
870
+ }
871
+ const toolMessages = toolCalls
872
+ .map((call) => messageByCallId.get(call.id))
873
+ .filter((m) => m != null);
874
+ return { toolMessages, injected };
875
+ }
876
+ dispatchStepCompleted(toolCallId, toolName, args, output, config, turn) {
877
+ const stepId = this.toolCallStepIds?.get(toolCallId) ?? '';
878
+ if (!stepId) {
879
+ // eslint-disable-next-line no-console
880
+ console.warn(`[ToolNode] toolCallStepIds missing entry for toolCallId=${toolCallId} (tool=${toolName}). ` +
881
+ 'This indicates a race between the stream consumer and graph execution. ' +
882
+ `Map size: ${this.toolCallStepIds?.size ?? 0}`);
883
+ }
884
+ events.safeDispatchCustomEvent(_enum.GraphEvents.ON_RUN_STEP_COMPLETED, {
885
+ result: {
886
+ id: stepId,
887
+ index: turn ?? this.toolUsageCount.get(toolName) ?? 0,
888
+ type: 'tool_call',
889
+ tool_call: {
890
+ args: JSON.stringify(args),
891
+ name: toolName,
892
+ id: toolCallId,
893
+ output,
894
+ progress: 1,
457
895
  },
896
+ },
897
+ }, config);
898
+ }
899
+ /**
900
+ * Converts InjectedMessage instances to LangChain HumanMessage objects.
901
+ * Both 'user' and 'system' roles become HumanMessage to avoid provider
902
+ * rejections (Anthropic/Google reject non-leading SystemMessages).
903
+ * The original role is preserved in additional_kwargs for downstream consumers.
904
+ */
905
+ convertInjectedMessages(messages$1) {
906
+ const converted = [];
907
+ for (const msg of messages$1) {
908
+ const additional_kwargs = {
909
+ role: msg.role,
458
910
  };
459
- events.safeDispatchCustomEvent(_enum.GraphEvents.ON_RUN_STEP_COMPLETED, runStepCompletedData, config);
460
- return toolMessage;
461
- });
911
+ if (msg.isMeta != null)
912
+ additional_kwargs.isMeta = msg.isMeta;
913
+ if (msg.source != null)
914
+ additional_kwargs.source = msg.source;
915
+ if (msg.skillName != null)
916
+ additional_kwargs.skillName = msg.skillName;
917
+ converted.push(new messages.HumanMessage({ content: msg.content, additional_kwargs }));
918
+ }
919
+ return converted;
462
920
  }
463
921
  /**
464
922
  * Execute all tool calls via ON_TOOL_EXECUTE event dispatch.
465
- * Used in event-driven mode where the host handles actual tool execution.
923
+ * Injected messages are placed AFTER ToolMessages to respect provider
924
+ * message ordering (AIMessage tool_calls must be immediately followed
925
+ * by their ToolMessage results).
926
+ *
927
+ * `batchIndices` mirrors `toolCalls` and carries each call's position
928
+ * within the parent batch. `turn` is the per-`run()` batch index
929
+ * captured locally by the caller. Both are threaded so concurrent
930
+ * invocations cannot race on shared mutable state.
466
931
  */
467
932
  async executeViaEvent(toolCalls, config,
468
933
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
469
- input) {
470
- const outputs = await this.dispatchToolEvents(toolCalls, config);
934
+ input, batchContext = {}) {
935
+ const { toolMessages, injected } = await this.dispatchToolEvents(toolCalls, config, batchContext);
936
+ const outputs = [...toolMessages, ...injected];
471
937
  return (Array.isArray(input) ? outputs : { messages: outputs });
472
938
  }
473
939
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
474
940
  async run(input, config) {
475
941
  this.toolCallTurns.clear();
942
+ /**
943
+ * Per-batch local map for resolved (post-substitution) args.
944
+ * Lives on the stack so concurrent `run()` calls on the same
945
+ * ToolNode cannot read or wipe each other's entries.
946
+ */
947
+ const resolvedArgsByCallId = new Map();
948
+ /**
949
+ * Claim this batch's turn synchronously from the registry (or
950
+ * fall back to 0 when the feature is disabled). The registry is
951
+ * partitioned by scope id so overlapping batches cannot
952
+ * overwrite each other's state even under a shared registry.
953
+ *
954
+ * For anonymous callers (no `run_id` in config), mint a unique
955
+ * per-batch scope id so two concurrent anonymous invocations
956
+ * don't target the same bucket. The scope is threaded down to
957
+ * every subsequent registry call on this batch.
958
+ */
959
+ const incomingRunId = config.configurable?.run_id;
960
+ const batchScopeId = incomingRunId ?? `\0anon-${this.anonBatchCounter++}`;
961
+ const turn = this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
476
962
  let outputs;
477
963
  if (this.isSendInput(input)) {
478
964
  const isDirectTool = this.directToolNames?.has(input.lg_tool_call.name);
479
965
  if (this.eventDrivenMode && isDirectTool !== true) {
480
- return this.executeViaEvent([input.lg_tool_call], config, input);
966
+ return this.executeViaEvent([input.lg_tool_call], config, input, {
967
+ batchIndices: [0],
968
+ turn,
969
+ batchScopeId,
970
+ });
481
971
  }
482
- outputs = [await this.runTool(input.lg_tool_call, config)];
483
- this.handleRunToolCompletions([input.lg_tool_call], outputs, config);
972
+ outputs = [
973
+ await this.runTool(input.lg_tool_call, config, {
974
+ batchIndex: 0,
975
+ turn,
976
+ batchScopeId,
977
+ resolvedArgsByCallId,
978
+ }),
979
+ ];
980
+ this.handleRunToolCompletions([input.lg_tool_call], outputs, config, resolvedArgsByCallId);
484
981
  }
485
982
  else {
486
983
  let messages$1;
@@ -525,25 +1022,100 @@ class ToolNode extends run.RunnableCallable {
525
1022
  false));
526
1023
  }) ?? [];
527
1024
  if (this.eventDrivenMode && filteredCalls.length > 0) {
1025
+ const filteredIndices = filteredCalls.map((_, idx) => idx);
528
1026
  if (!this.directToolNames || this.directToolNames.size === 0) {
529
- return this.executeViaEvent(filteredCalls, config, input);
1027
+ return this.executeViaEvent(filteredCalls, config, input, {
1028
+ batchIndices: filteredIndices,
1029
+ turn,
1030
+ batchScopeId,
1031
+ });
1032
+ }
1033
+ const directEntries = [];
1034
+ const eventEntries = [];
1035
+ for (let i = 0; i < filteredCalls.length; i++) {
1036
+ const call = filteredCalls[i];
1037
+ const entry = { call, batchIndex: i };
1038
+ if (this.directToolNames.has(call.name)) {
1039
+ directEntries.push(entry);
1040
+ }
1041
+ else {
1042
+ eventEntries.push(entry);
1043
+ }
1044
+ }
1045
+ const directCalls = directEntries.map((e) => e.call);
1046
+ const directIndices = directEntries.map((e) => e.batchIndex);
1047
+ const eventCalls = eventEntries.map((e) => e.call);
1048
+ const eventIndices = eventEntries.map((e) => e.batchIndex);
1049
+ /**
1050
+ * Snapshot the event calls' args against the *pre-batch*
1051
+ * registry state synchronously, before any await runs. The
1052
+ * directs are then awaited first (preserving fail-fast
1053
+ * semantics — a thrown error in a direct tool, e.g. with
1054
+ * `handleToolErrors=false` or a `GraphInterrupt`, aborts
1055
+ * before we dispatch any event-driven tools to the host).
1056
+ * Because the event args were captured pre-await, they stay
1057
+ * isolated from same-turn direct outputs that register
1058
+ * during the await.
1059
+ */
1060
+ const preResolvedEventArgs = new Map();
1061
+ /**
1062
+ * Take a frozen snapshot of the registry state before any
1063
+ * direct registrations land. The snapshot resolves
1064
+ * placeholders against this point-in-time view, so a
1065
+ * `PreToolUse` hook later rewriting event args via
1066
+ * `updatedInput` can introduce placeholders that resolve
1067
+ * cross-batch (against prior runs) without ever picking up
1068
+ * same-turn direct outputs.
1069
+ */
1070
+ const preBatchSnapshot = this.toolOutputRegistry?.snapshot(batchScopeId);
1071
+ if (preBatchSnapshot != null) {
1072
+ for (const entry of eventEntries) {
1073
+ if (entry.call.id != null) {
1074
+ const { resolved, unresolved } = preBatchSnapshot.resolve(entry.call.args);
1075
+ preResolvedEventArgs.set(entry.call.id, {
1076
+ resolved: resolved,
1077
+ unresolved,
1078
+ });
1079
+ }
1080
+ }
530
1081
  }
531
- const directCalls = filteredCalls.filter((c) => this.directToolNames.has(c.name));
532
- const eventCalls = filteredCalls.filter((c) => !this.directToolNames.has(c.name));
533
1082
  const directOutputs = directCalls.length > 0
534
- ? await Promise.all(directCalls.map((call) => this.runTool(call, config)))
1083
+ ? await Promise.all(directCalls.map((call, i) => this.runTool(call, config, {
1084
+ batchIndex: directIndices[i],
1085
+ turn,
1086
+ batchScopeId,
1087
+ resolvedArgsByCallId,
1088
+ })))
535
1089
  : [];
536
1090
  if (directCalls.length > 0 && directOutputs.length > 0) {
537
- this.handleRunToolCompletions(directCalls, directOutputs, config);
1091
+ this.handleRunToolCompletions(directCalls, directOutputs, config, resolvedArgsByCallId);
538
1092
  }
539
- const eventOutputs = eventCalls.length > 0
540
- ? await this.dispatchToolEvents(eventCalls, config)
541
- : [];
542
- outputs = [...directOutputs, ...eventOutputs];
1093
+ const eventResult = eventCalls.length > 0
1094
+ ? await this.dispatchToolEvents(eventCalls, config, {
1095
+ batchIndices: eventIndices,
1096
+ turn,
1097
+ batchScopeId,
1098
+ preResolvedArgs: preResolvedEventArgs,
1099
+ preBatchSnapshot,
1100
+ })
1101
+ : {
1102
+ toolMessages: [],
1103
+ injected: [],
1104
+ };
1105
+ outputs = [
1106
+ ...directOutputs,
1107
+ ...eventResult.toolMessages,
1108
+ ...eventResult.injected,
1109
+ ];
543
1110
  }
544
1111
  else {
545
- outputs = await Promise.all(filteredCalls.map((call) => this.runTool(call, config)));
546
- this.handleRunToolCompletions(filteredCalls, outputs, config);
1112
+ outputs = await Promise.all(filteredCalls.map((call, i) => this.runTool(call, config, {
1113
+ batchIndex: i,
1114
+ turn,
1115
+ batchScopeId,
1116
+ resolvedArgsByCallId,
1117
+ })));
1118
+ this.handleRunToolCompletions(filteredCalls, outputs, config, resolvedArgsByCallId);
547
1119
  }
548
1120
  }
549
1121
  if (!outputs.some(langgraph.isCommand)) {