@librechat/agents 3.2.32 → 3.2.33

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 (573) hide show
  1. package/dist/cjs/_virtual/_rolldown/runtime.cjs +23 -0
  2. package/dist/cjs/agents/AgentContext.cjs +844 -1046
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  4. package/dist/cjs/common/constants.cjs +13 -13
  5. package/dist/cjs/common/constants.cjs.map +1 -1
  6. package/dist/cjs/common/enum.cjs +233 -240
  7. package/dist/cjs/common/enum.cjs.map +1 -1
  8. package/dist/cjs/common/index.cjs +2 -0
  9. package/dist/cjs/events.cjs +121 -169
  10. package/dist/cjs/events.cjs.map +1 -1
  11. package/dist/cjs/graphs/Graph.cjs +1389 -1807
  12. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  13. package/dist/cjs/graphs/MultiAgentGraph.cjs +713 -945
  14. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  15. package/dist/cjs/graphs/index.cjs +2 -0
  16. package/dist/cjs/hitl/askUserQuestion.cjs +60 -62
  17. package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
  18. package/dist/cjs/hitl/index.cjs +1 -0
  19. package/dist/cjs/hooks/HookRegistry.cjs +176 -202
  20. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  21. package/dist/cjs/hooks/createToolPolicyHook.cjs +71 -101
  22. package/dist/cjs/hooks/createToolPolicyHook.cjs.map +1 -1
  23. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +170 -273
  24. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -1
  25. package/dist/cjs/hooks/executeHooks.cjs +227 -282
  26. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  27. package/dist/cjs/hooks/index.cjs +6 -0
  28. package/dist/cjs/hooks/matchers.cjs +196 -230
  29. package/dist/cjs/hooks/matchers.cjs.map +1 -1
  30. package/dist/cjs/hooks/types.cjs +24 -24
  31. package/dist/cjs/hooks/types.cjs.map +1 -1
  32. package/dist/cjs/instrumentation.cjs +110 -137
  33. package/dist/cjs/instrumentation.cjs.map +1 -1
  34. package/dist/cjs/langchain/google-common.cjs +0 -3
  35. package/dist/cjs/langchain/index.cjs +80 -43
  36. package/dist/cjs/langchain/language_models/chat_models.cjs +0 -3
  37. package/dist/cjs/langchain/messages/tool.cjs +0 -3
  38. package/dist/cjs/langchain/messages.cjs +35 -18
  39. package/dist/cjs/langchain/openai.cjs +0 -3
  40. package/dist/cjs/langchain/prompts.cjs +5 -8
  41. package/dist/cjs/langchain/runnables.cjs +11 -10
  42. package/dist/cjs/langchain/tools.cjs +14 -11
  43. package/dist/cjs/langchain/utils/env.cjs +5 -8
  44. package/dist/cjs/langfuse.cjs +60 -79
  45. package/dist/cjs/langfuse.cjs.map +1 -1
  46. package/dist/cjs/langfuseToolOutputTracing.cjs +267 -399
  47. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  48. package/dist/cjs/llm/anthropic/index.cjs +432 -562
  49. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  50. package/dist/cjs/llm/anthropic/types.cjs +23 -47
  51. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  52. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +441 -731
  53. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  54. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +171 -256
  55. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  56. package/dist/cjs/llm/anthropic/utils/output_parsers.cjs +2 -0
  57. package/dist/cjs/llm/anthropic/utils/tools.cjs +12 -26
  58. package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
  59. package/dist/cjs/llm/bedrock/index.cjs +195 -240
  60. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  61. package/dist/cjs/llm/bedrock/toolCache.cjs +84 -106
  62. package/dist/cjs/llm/bedrock/toolCache.cjs.map +1 -1
  63. package/dist/cjs/llm/bedrock/utils/index.cjs +2 -0
  64. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +357 -620
  65. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  66. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +105 -149
  67. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
  68. package/dist/cjs/llm/fake.cjs +86 -96
  69. package/dist/cjs/llm/fake.cjs.map +1 -1
  70. package/dist/cjs/llm/google/index.cjs +183 -237
  71. package/dist/cjs/llm/google/index.cjs.map +1 -1
  72. package/dist/cjs/llm/google/utils/common.cjs +398 -674
  73. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  74. package/dist/cjs/llm/google/utils/zod_to_genai_parameters.cjs +2 -0
  75. package/dist/cjs/llm/init.cjs +44 -53
  76. package/dist/cjs/llm/init.cjs.map +1 -1
  77. package/dist/cjs/llm/invoke.cjs +142 -182
  78. package/dist/cjs/llm/invoke.cjs.map +1 -1
  79. package/dist/cjs/llm/openai/index.cjs +991 -1276
  80. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  81. package/dist/cjs/llm/openai/utils/index.cjs +189 -316
  82. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  83. package/dist/cjs/llm/openrouter/index.cjs +102 -153
  84. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  85. package/dist/cjs/llm/openrouter/toolCache.cjs +35 -44
  86. package/dist/cjs/llm/openrouter/toolCache.cjs.map +1 -1
  87. package/dist/cjs/llm/providers.cjs +29 -37
  88. package/dist/cjs/llm/providers.cjs.map +1 -1
  89. package/dist/cjs/llm/request.cjs +20 -33
  90. package/dist/cjs/llm/request.cjs.map +1 -1
  91. package/dist/cjs/llm/vertexai/index.cjs +427 -453
  92. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  93. package/dist/cjs/main.cjs +547 -528
  94. package/dist/cjs/messages/anthropicToolCache.cjs +68 -119
  95. package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
  96. package/dist/cjs/messages/cache.cjs +305 -418
  97. package/dist/cjs/messages/cache.cjs.map +1 -1
  98. package/dist/cjs/messages/content.cjs +36 -49
  99. package/dist/cjs/messages/content.cjs.map +1 -1
  100. package/dist/cjs/messages/contextPruning.cjs +112 -145
  101. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  102. package/dist/cjs/messages/contextPruningSettings.cjs +36 -46
  103. package/dist/cjs/messages/contextPruningSettings.cjs.map +1 -1
  104. package/dist/cjs/messages/core.cjs +256 -397
  105. package/dist/cjs/messages/core.cjs.map +1 -1
  106. package/dist/cjs/messages/format.cjs +904 -1387
  107. package/dist/cjs/messages/format.cjs.map +1 -1
  108. package/dist/cjs/messages/ids.cjs +16 -20
  109. package/dist/cjs/messages/ids.cjs.map +1 -1
  110. package/dist/cjs/messages/index.cjs +12 -0
  111. package/dist/cjs/messages/langchain.cjs +18 -18
  112. package/dist/cjs/messages/langchain.cjs.map +1 -1
  113. package/dist/cjs/messages/prune.cjs +1054 -1517
  114. package/dist/cjs/messages/prune.cjs.map +1 -1
  115. package/dist/cjs/messages/recency.cjs +77 -95
  116. package/dist/cjs/messages/recency.cjs.map +1 -1
  117. package/dist/cjs/messages/reducer.cjs +63 -78
  118. package/dist/cjs/messages/reducer.cjs.map +1 -1
  119. package/dist/cjs/messages/tools.cjs +51 -79
  120. package/dist/cjs/messages/tools.cjs.map +1 -1
  121. package/dist/cjs/openai/index.cjs +171 -217
  122. package/dist/cjs/openai/index.cjs.map +1 -1
  123. package/dist/cjs/responses/index.cjs +302 -391
  124. package/dist/cjs/responses/index.cjs.map +1 -1
  125. package/dist/cjs/run.cjs +903 -1113
  126. package/dist/cjs/run.cjs.map +1 -1
  127. package/dist/cjs/session/AgentSession.cjs +805 -986
  128. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  129. package/dist/cjs/session/JsonlSessionStore.cjs +327 -410
  130. package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -1
  131. package/dist/cjs/session/handlers.cjs +192 -208
  132. package/dist/cjs/session/handlers.cjs.map +1 -1
  133. package/dist/cjs/session/ids.cjs +9 -10
  134. package/dist/cjs/session/ids.cjs.map +1 -1
  135. package/dist/cjs/session/index.cjs +4 -0
  136. package/dist/cjs/session/messageSerialization.cjs +94 -156
  137. package/dist/cjs/session/messageSerialization.cjs.map +1 -1
  138. package/dist/cjs/splitStream.cjs +147 -206
  139. package/dist/cjs/splitStream.cjs.map +1 -1
  140. package/dist/cjs/stream.cjs +856 -1344
  141. package/dist/cjs/stream.cjs.map +1 -1
  142. package/dist/cjs/summarization/index.cjs +57 -101
  143. package/dist/cjs/summarization/index.cjs.map +1 -1
  144. package/dist/cjs/summarization/node.cjs +643 -796
  145. package/dist/cjs/summarization/node.cjs.map +1 -1
  146. package/dist/cjs/tools/BashExecutor.cjs +110 -136
  147. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  148. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +165 -245
  149. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  150. package/dist/cjs/tools/Calculator.cjs +36 -57
  151. package/dist/cjs/tools/Calculator.cjs.map +1 -1
  152. package/dist/cjs/tools/CodeExecutor.cjs +126 -168
  153. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  154. package/dist/cjs/tools/CodeSessionFileSummary.cjs +36 -46
  155. package/dist/cjs/tools/CodeSessionFileSummary.cjs.map +1 -1
  156. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +459 -649
  157. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  158. package/dist/cjs/tools/ReadFile.cjs +17 -20
  159. package/dist/cjs/tools/ReadFile.cjs.map +1 -1
  160. package/dist/cjs/tools/SkillTool.cjs +26 -27
  161. package/dist/cjs/tools/SkillTool.cjs.map +1 -1
  162. package/dist/cjs/tools/SubagentTool.cjs +59 -61
  163. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  164. package/dist/cjs/tools/ToolNode.cjs +2109 -2686
  165. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  166. package/dist/cjs/tools/ToolSearch.cjs +663 -825
  167. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  168. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs +248 -340
  169. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -1
  170. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +170 -197
  171. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
  172. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +425 -520
  173. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
  174. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +91 -124
  175. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -1
  176. package/dist/cjs/tools/cloudflare/index.cjs +4 -0
  177. package/dist/cjs/tools/eagerEventExecution.cjs +75 -99
  178. package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -1
  179. package/dist/cjs/tools/handlers.cjs +200 -262
  180. package/dist/cjs/tools/handlers.cjs.map +1 -1
  181. package/dist/cjs/tools/local/CompileCheckTool.cjs +150 -212
  182. package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -1
  183. package/dist/cjs/tools/local/FileCheckpointer.cjs +77 -85
  184. package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -1
  185. package/dist/cjs/tools/local/LocalCodingTools.cjs +763 -1022
  186. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
  187. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +666 -941
  188. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  189. package/dist/cjs/tools/local/LocalExecutionTools.cjs +49 -92
  190. package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -1
  191. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +286 -354
  192. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
  193. package/dist/cjs/tools/local/attachments.cjs +108 -165
  194. package/dist/cjs/tools/local/attachments.cjs.map +1 -1
  195. package/dist/cjs/tools/local/bashAst.cjs +99 -113
  196. package/dist/cjs/tools/local/bashAst.cjs.map +1 -1
  197. package/dist/cjs/tools/local/editStrategies.cjs +126 -169
  198. package/dist/cjs/tools/local/editStrategies.cjs.map +1 -1
  199. package/dist/cjs/tools/local/index.cjs +12 -0
  200. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +136 -218
  201. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  202. package/dist/cjs/tools/local/syntaxCheck.cjs +142 -161
  203. package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -1
  204. package/dist/cjs/tools/local/textEncoding.cjs +25 -23
  205. package/dist/cjs/tools/local/textEncoding.cjs.map +1 -1
  206. package/dist/cjs/tools/local/workspaceFS.cjs +38 -46
  207. package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -1
  208. package/dist/cjs/tools/ptcTimeout.cjs +27 -47
  209. package/dist/cjs/tools/ptcTimeout.cjs.map +1 -1
  210. package/dist/cjs/tools/schema.cjs +24 -23
  211. package/dist/cjs/tools/schema.cjs.map +1 -1
  212. package/dist/cjs/tools/search/anthropic.cjs +24 -33
  213. package/dist/cjs/tools/search/anthropic.cjs.map +1 -1
  214. package/dist/cjs/tools/search/content.cjs +95 -137
  215. package/dist/cjs/tools/search/content.cjs.map +1 -1
  216. package/dist/cjs/tools/search/firecrawl.cjs +141 -172
  217. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
  218. package/dist/cjs/tools/search/format.cjs +128 -196
  219. package/dist/cjs/tools/search/format.cjs.map +1 -1
  220. package/dist/cjs/tools/search/highlights.cjs +165 -232
  221. package/dist/cjs/tools/search/highlights.cjs.map +1 -1
  222. package/dist/cjs/tools/search/index.cjs +2 -0
  223. package/dist/cjs/tools/search/rerankers.cjs +151 -174
  224. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  225. package/dist/cjs/tools/search/schema.cjs +40 -39
  226. package/dist/cjs/tools/search/schema.cjs.map +1 -1
  227. package/dist/cjs/tools/search/search.cjs +428 -530
  228. package/dist/cjs/tools/search/search.cjs.map +1 -1
  229. package/dist/cjs/tools/search/serper-scraper.cjs +106 -127
  230. package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
  231. package/dist/cjs/tools/search/tavily-scraper.cjs +129 -181
  232. package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -1
  233. package/dist/cjs/tools/search/tavily-search.cjs +295 -359
  234. package/dist/cjs/tools/search/tavily-search.cjs.map +1 -1
  235. package/dist/cjs/tools/search/tool.cjs +260 -299
  236. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  237. package/dist/cjs/tools/search/utils.cjs +74 -117
  238. package/dist/cjs/tools/search/utils.cjs.map +1 -1
  239. package/dist/cjs/tools/skillCatalog.cjs +54 -72
  240. package/dist/cjs/tools/skillCatalog.cjs.map +1 -1
  241. package/dist/cjs/tools/streamedToolCallSeals.cjs +19 -36
  242. package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -1
  243. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +612 -771
  244. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  245. package/dist/cjs/tools/subagent/index.cjs +1 -0
  246. package/dist/cjs/tools/toolOutputReferences.cjs +523 -630
  247. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  248. package/dist/cjs/utils/callbacks.cjs +11 -21
  249. package/dist/cjs/utils/callbacks.cjs.map +1 -1
  250. package/dist/cjs/utils/errors.cjs +70 -95
  251. package/dist/cjs/utils/errors.cjs.map +1 -1
  252. package/dist/cjs/utils/events.cjs +32 -42
  253. package/dist/cjs/utils/events.cjs.map +1 -1
  254. package/dist/cjs/utils/graph.cjs +8 -12
  255. package/dist/cjs/utils/graph.cjs.map +1 -1
  256. package/dist/cjs/utils/handlers.cjs +60 -82
  257. package/dist/cjs/utils/handlers.cjs.map +1 -1
  258. package/dist/cjs/utils/index.cjs +9 -0
  259. package/dist/cjs/utils/llm.cjs +19 -27
  260. package/dist/cjs/utils/llm.cjs.map +1 -1
  261. package/dist/cjs/utils/misc.cjs +30 -46
  262. package/dist/cjs/utils/misc.cjs.map +1 -1
  263. package/dist/cjs/utils/run.cjs +50 -66
  264. package/dist/cjs/utils/run.cjs.map +1 -1
  265. package/dist/cjs/utils/schema.cjs +11 -19
  266. package/dist/cjs/utils/schema.cjs.map +1 -1
  267. package/dist/cjs/utils/title.cjs +71 -106
  268. package/dist/cjs/utils/title.cjs.map +1 -1
  269. package/dist/cjs/utils/tokens.cjs +186 -283
  270. package/dist/cjs/utils/tokens.cjs.map +1 -1
  271. package/dist/cjs/utils/truncation.cjs +95 -114
  272. package/dist/cjs/utils/truncation.cjs.map +1 -1
  273. package/dist/esm/agents/AgentContext.mjs +844 -1044
  274. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  275. package/dist/esm/common/constants.mjs +13 -11
  276. package/dist/esm/common/constants.mjs.map +1 -1
  277. package/dist/esm/common/enum.mjs +221 -238
  278. package/dist/esm/common/enum.mjs.map +1 -1
  279. package/dist/esm/common/index.mjs +3 -0
  280. package/dist/esm/events.mjs +121 -167
  281. package/dist/esm/events.mjs.map +1 -1
  282. package/dist/esm/graphs/Graph.mjs +1388 -1804
  283. package/dist/esm/graphs/Graph.mjs.map +1 -1
  284. package/dist/esm/graphs/MultiAgentGraph.mjs +713 -943
  285. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  286. package/dist/esm/graphs/index.mjs +3 -0
  287. package/dist/esm/hitl/askUserQuestion.mjs +60 -60
  288. package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
  289. package/dist/esm/hitl/index.mjs +2 -0
  290. package/dist/esm/hooks/HookRegistry.mjs +176 -200
  291. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  292. package/dist/esm/hooks/createToolPolicyHook.mjs +71 -99
  293. package/dist/esm/hooks/createToolPolicyHook.mjs.map +1 -1
  294. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +170 -271
  295. package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -1
  296. package/dist/esm/hooks/executeHooks.mjs +227 -280
  297. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  298. package/dist/esm/hooks/index.mjs +7 -0
  299. package/dist/esm/hooks/matchers.mjs +196 -228
  300. package/dist/esm/hooks/matchers.mjs.map +1 -1
  301. package/dist/esm/hooks/types.mjs +24 -22
  302. package/dist/esm/hooks/types.mjs.map +1 -1
  303. package/dist/esm/instrumentation.mjs +109 -132
  304. package/dist/esm/instrumentation.mjs.map +1 -1
  305. package/dist/esm/langchain/google-common.mjs +1 -2
  306. package/dist/esm/langchain/index.mjs +5 -5
  307. package/dist/esm/langchain/language_models/chat_models.mjs +1 -2
  308. package/dist/esm/langchain/messages/tool.mjs +1 -2
  309. package/dist/esm/langchain/messages.mjs +2 -2
  310. package/dist/esm/langchain/openai.mjs +1 -2
  311. package/dist/esm/langchain/prompts.mjs +2 -2
  312. package/dist/esm/langchain/runnables.mjs +2 -2
  313. package/dist/esm/langchain/tools.mjs +2 -2
  314. package/dist/esm/langchain/utils/env.mjs +2 -2
  315. package/dist/esm/langfuse.mjs +60 -76
  316. package/dist/esm/langfuse.mjs.map +1 -1
  317. package/dist/esm/langfuseToolOutputTracing.mjs +267 -395
  318. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  319. package/dist/esm/llm/anthropic/index.mjs +432 -559
  320. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  321. package/dist/esm/llm/anthropic/types.mjs +23 -45
  322. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  323. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +439 -725
  324. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  325. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +171 -253
  326. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  327. package/dist/esm/llm/anthropic/utils/output_parsers.mjs +3 -0
  328. package/dist/esm/llm/anthropic/utils/tools.mjs +12 -24
  329. package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
  330. package/dist/esm/llm/bedrock/index.mjs +195 -238
  331. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  332. package/dist/esm/llm/bedrock/toolCache.mjs +84 -104
  333. package/dist/esm/llm/bedrock/toolCache.mjs.map +1 -1
  334. package/dist/esm/llm/bedrock/utils/index.mjs +3 -0
  335. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +357 -618
  336. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  337. package/dist/esm/llm/bedrock/utils/message_outputs.mjs +105 -147
  338. package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
  339. package/dist/esm/llm/fake.mjs +86 -94
  340. package/dist/esm/llm/fake.mjs.map +1 -1
  341. package/dist/esm/llm/google/index.mjs +183 -235
  342. package/dist/esm/llm/google/index.mjs.map +1 -1
  343. package/dist/esm/llm/google/utils/common.mjs +397 -666
  344. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  345. package/dist/esm/llm/google/utils/zod_to_genai_parameters.mjs +3 -0
  346. package/dist/esm/llm/init.mjs +44 -51
  347. package/dist/esm/llm/init.mjs.map +1 -1
  348. package/dist/esm/llm/invoke.mjs +142 -180
  349. package/dist/esm/llm/invoke.mjs.map +1 -1
  350. package/dist/esm/llm/openai/index.mjs +991 -1271
  351. package/dist/esm/llm/openai/index.mjs.map +1 -1
  352. package/dist/esm/llm/openai/utils/index.mjs +188 -312
  353. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  354. package/dist/esm/llm/openrouter/index.mjs +102 -151
  355. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  356. package/dist/esm/llm/openrouter/toolCache.mjs +35 -42
  357. package/dist/esm/llm/openrouter/toolCache.mjs.map +1 -1
  358. package/dist/esm/llm/providers.mjs +29 -34
  359. package/dist/esm/llm/providers.mjs.map +1 -1
  360. package/dist/esm/llm/request.mjs +20 -31
  361. package/dist/esm/llm/request.mjs.map +1 -1
  362. package/dist/esm/llm/vertexai/index.mjs +427 -449
  363. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  364. package/dist/esm/main.mjs +99 -87
  365. package/dist/esm/messages/anthropicToolCache.mjs +68 -117
  366. package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
  367. package/dist/esm/messages/cache.mjs +305 -416
  368. package/dist/esm/messages/cache.mjs.map +1 -1
  369. package/dist/esm/messages/content.mjs +36 -47
  370. package/dist/esm/messages/content.mjs.map +1 -1
  371. package/dist/esm/messages/contextPruning.mjs +112 -143
  372. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  373. package/dist/esm/messages/contextPruningSettings.mjs +36 -44
  374. package/dist/esm/messages/contextPruningSettings.mjs.map +1 -1
  375. package/dist/esm/messages/core.mjs +254 -393
  376. package/dist/esm/messages/core.mjs.map +1 -1
  377. package/dist/esm/messages/format.mjs +902 -1383
  378. package/dist/esm/messages/format.mjs.map +1 -1
  379. package/dist/esm/messages/ids.mjs +16 -18
  380. package/dist/esm/messages/ids.mjs.map +1 -1
  381. package/dist/esm/messages/index.mjs +13 -0
  382. package/dist/esm/messages/langchain.mjs +18 -16
  383. package/dist/esm/messages/langchain.mjs.map +1 -1
  384. package/dist/esm/messages/prune.mjs +1053 -1514
  385. package/dist/esm/messages/prune.mjs.map +1 -1
  386. package/dist/esm/messages/recency.mjs +77 -93
  387. package/dist/esm/messages/recency.mjs.map +1 -1
  388. package/dist/esm/messages/reducer.mjs +63 -76
  389. package/dist/esm/messages/reducer.mjs.map +1 -1
  390. package/dist/esm/messages/tools.mjs +49 -75
  391. package/dist/esm/messages/tools.mjs.map +1 -1
  392. package/dist/esm/openai/index.mjs +170 -215
  393. package/dist/esm/openai/index.mjs.map +1 -1
  394. package/dist/esm/responses/index.mjs +301 -389
  395. package/dist/esm/responses/index.mjs.map +1 -1
  396. package/dist/esm/run.mjs +903 -1111
  397. package/dist/esm/run.mjs.map +1 -1
  398. package/dist/esm/session/AgentSession.mjs +806 -985
  399. package/dist/esm/session/AgentSession.mjs.map +1 -1
  400. package/dist/esm/session/JsonlSessionStore.mjs +326 -407
  401. package/dist/esm/session/JsonlSessionStore.mjs.map +1 -1
  402. package/dist/esm/session/handlers.mjs +192 -206
  403. package/dist/esm/session/handlers.mjs.map +1 -1
  404. package/dist/esm/session/ids.mjs +9 -8
  405. package/dist/esm/session/ids.mjs.map +1 -1
  406. package/dist/esm/session/index.mjs +5 -0
  407. package/dist/esm/session/messageSerialization.mjs +94 -154
  408. package/dist/esm/session/messageSerialization.mjs.map +1 -1
  409. package/dist/esm/splitStream.mjs +147 -204
  410. package/dist/esm/splitStream.mjs.map +1 -1
  411. package/dist/esm/stream.mjs +854 -1341
  412. package/dist/esm/stream.mjs.map +1 -1
  413. package/dist/esm/summarization/index.mjs +57 -99
  414. package/dist/esm/summarization/index.mjs.map +1 -1
  415. package/dist/esm/summarization/node.mjs +640 -790
  416. package/dist/esm/summarization/node.mjs.map +1 -1
  417. package/dist/esm/tools/BashExecutor.mjs +103 -129
  418. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  419. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +162 -239
  420. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  421. package/dist/esm/tools/Calculator.mjs +34 -36
  422. package/dist/esm/tools/Calculator.mjs.map +1 -1
  423. package/dist/esm/tools/CodeExecutor.mjs +123 -164
  424. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  425. package/dist/esm/tools/CodeSessionFileSummary.mjs +36 -44
  426. package/dist/esm/tools/CodeSessionFileSummary.mjs.map +1 -1
  427. package/dist/esm/tools/ProgrammaticToolCalling.mjs +454 -644
  428. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  429. package/dist/esm/tools/ReadFile.mjs +17 -18
  430. package/dist/esm/tools/ReadFile.mjs.map +1 -1
  431. package/dist/esm/tools/SkillTool.mjs +26 -25
  432. package/dist/esm/tools/SkillTool.mjs.map +1 -1
  433. package/dist/esm/tools/SubagentTool.mjs +59 -59
  434. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  435. package/dist/esm/tools/ToolNode.mjs +2107 -2684
  436. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  437. package/dist/esm/tools/ToolSearch.mjs +659 -804
  438. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  439. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs +248 -338
  440. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -1
  441. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +170 -195
  442. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
  443. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +424 -517
  444. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
  445. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +91 -122
  446. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -1
  447. package/dist/esm/tools/cloudflare/index.mjs +5 -0
  448. package/dist/esm/tools/eagerEventExecution.mjs +75 -96
  449. package/dist/esm/tools/eagerEventExecution.mjs.map +1 -1
  450. package/dist/esm/tools/handlers.mjs +200 -260
  451. package/dist/esm/tools/handlers.mjs.map +1 -1
  452. package/dist/esm/tools/local/CompileCheckTool.mjs +150 -210
  453. package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -1
  454. package/dist/esm/tools/local/FileCheckpointer.mjs +77 -83
  455. package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -1
  456. package/dist/esm/tools/local/LocalCodingTools.mjs +760 -1017
  457. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
  458. package/dist/esm/tools/local/LocalExecutionEngine.mjs +663 -936
  459. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  460. package/dist/esm/tools/local/LocalExecutionTools.mjs +49 -90
  461. package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -1
  462. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +283 -349
  463. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
  464. package/dist/esm/tools/local/attachments.mjs +108 -163
  465. package/dist/esm/tools/local/attachments.mjs.map +1 -1
  466. package/dist/esm/tools/local/bashAst.mjs +99 -111
  467. package/dist/esm/tools/local/bashAst.mjs.map +1 -1
  468. package/dist/esm/tools/local/editStrategies.mjs +126 -167
  469. package/dist/esm/tools/local/editStrategies.mjs.map +1 -1
  470. package/dist/esm/tools/local/index.mjs +13 -0
  471. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +136 -216
  472. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  473. package/dist/esm/tools/local/syntaxCheck.mjs +138 -155
  474. package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -1
  475. package/dist/esm/tools/local/textEncoding.mjs +25 -21
  476. package/dist/esm/tools/local/textEncoding.mjs.map +1 -1
  477. package/dist/esm/tools/local/workspaceFS.mjs +38 -44
  478. package/dist/esm/tools/local/workspaceFS.mjs.map +1 -1
  479. package/dist/esm/tools/ptcTimeout.mjs +27 -42
  480. package/dist/esm/tools/ptcTimeout.mjs.map +1 -1
  481. package/dist/esm/tools/schema.mjs +24 -21
  482. package/dist/esm/tools/schema.mjs.map +1 -1
  483. package/dist/esm/tools/search/anthropic.mjs +24 -31
  484. package/dist/esm/tools/search/anthropic.mjs.map +1 -1
  485. package/dist/esm/tools/search/content.mjs +93 -116
  486. package/dist/esm/tools/search/content.mjs.map +1 -1
  487. package/dist/esm/tools/search/firecrawl.mjs +139 -169
  488. package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
  489. package/dist/esm/tools/search/format.mjs +128 -194
  490. package/dist/esm/tools/search/format.mjs.map +1 -1
  491. package/dist/esm/tools/search/highlights.mjs +165 -230
  492. package/dist/esm/tools/search/highlights.mjs.map +1 -1
  493. package/dist/esm/tools/search/index.mjs +3 -0
  494. package/dist/esm/tools/search/rerankers.mjs +149 -168
  495. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  496. package/dist/esm/tools/search/schema.mjs +39 -37
  497. package/dist/esm/tools/search/schema.mjs.map +1 -1
  498. package/dist/esm/tools/search/search.mjs +426 -528
  499. package/dist/esm/tools/search/search.mjs.map +1 -1
  500. package/dist/esm/tools/search/serper-scraper.mjs +104 -124
  501. package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
  502. package/dist/esm/tools/search/tavily-scraper.mjs +127 -178
  503. package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -1
  504. package/dist/esm/tools/search/tavily-search.mjs +293 -357
  505. package/dist/esm/tools/search/tavily-search.mjs.map +1 -1
  506. package/dist/esm/tools/search/tool.mjs +259 -297
  507. package/dist/esm/tools/search/tool.mjs.map +1 -1
  508. package/dist/esm/tools/search/utils.mjs +74 -115
  509. package/dist/esm/tools/search/utils.mjs.map +1 -1
  510. package/dist/esm/tools/skillCatalog.mjs +54 -70
  511. package/dist/esm/tools/skillCatalog.mjs.map +1 -1
  512. package/dist/esm/tools/streamedToolCallSeals.mjs +19 -31
  513. package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -1
  514. package/dist/esm/tools/subagent/SubagentExecutor.mjs +612 -768
  515. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  516. package/dist/esm/tools/subagent/index.mjs +2 -0
  517. package/dist/esm/tools/toolOutputReferences.mjs +523 -624
  518. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  519. package/dist/esm/utils/callbacks.mjs +11 -19
  520. package/dist/esm/utils/callbacks.mjs.map +1 -1
  521. package/dist/esm/utils/errors.mjs +70 -93
  522. package/dist/esm/utils/errors.mjs.map +1 -1
  523. package/dist/esm/utils/events.mjs +32 -40
  524. package/dist/esm/utils/events.mjs.map +1 -1
  525. package/dist/esm/utils/graph.mjs +8 -10
  526. package/dist/esm/utils/graph.mjs.map +1 -1
  527. package/dist/esm/utils/handlers.mjs +60 -80
  528. package/dist/esm/utils/handlers.mjs.map +1 -1
  529. package/dist/esm/utils/index.mjs +10 -0
  530. package/dist/esm/utils/llm.mjs +19 -25
  531. package/dist/esm/utils/llm.mjs.map +1 -1
  532. package/dist/esm/utils/misc.mjs +30 -44
  533. package/dist/esm/utils/misc.mjs.map +1 -1
  534. package/dist/esm/utils/run.mjs +50 -64
  535. package/dist/esm/utils/run.mjs.map +1 -1
  536. package/dist/esm/utils/schema.mjs +11 -17
  537. package/dist/esm/utils/schema.mjs.map +1 -1
  538. package/dist/esm/utils/title.mjs +71 -104
  539. package/dist/esm/utils/title.mjs.map +1 -1
  540. package/dist/esm/utils/tokens.mjs +186 -281
  541. package/dist/esm/utils/tokens.mjs.map +1 -1
  542. package/dist/esm/utils/truncation.mjs +95 -112
  543. package/dist/esm/utils/truncation.mjs.map +1 -1
  544. package/dist/types/tools/search/tool.d.ts +17 -0
  545. package/dist/types/tools/search/types.d.ts +4 -0
  546. package/package.json +4 -10
  547. package/src/tools/search/highlights.ts +9 -1
  548. package/src/tools/search/search.ts +41 -3
  549. package/src/tools/search/source-processing.test.ts +373 -0
  550. package/src/tools/search/tool.ts +22 -2
  551. package/src/tools/search/types.ts +4 -0
  552. package/dist/cjs/langchain/google-common.cjs.map +0 -1
  553. package/dist/cjs/langchain/index.cjs.map +0 -1
  554. package/dist/cjs/langchain/language_models/chat_models.cjs.map +0 -1
  555. package/dist/cjs/langchain/messages/tool.cjs.map +0 -1
  556. package/dist/cjs/langchain/messages.cjs.map +0 -1
  557. package/dist/cjs/langchain/openai.cjs.map +0 -1
  558. package/dist/cjs/langchain/prompts.cjs.map +0 -1
  559. package/dist/cjs/langchain/runnables.cjs.map +0 -1
  560. package/dist/cjs/langchain/tools.cjs.map +0 -1
  561. package/dist/cjs/langchain/utils/env.cjs.map +0 -1
  562. package/dist/cjs/main.cjs.map +0 -1
  563. package/dist/esm/langchain/google-common.mjs.map +0 -1
  564. package/dist/esm/langchain/index.mjs.map +0 -1
  565. package/dist/esm/langchain/language_models/chat_models.mjs.map +0 -1
  566. package/dist/esm/langchain/messages/tool.mjs.map +0 -1
  567. package/dist/esm/langchain/messages.mjs.map +0 -1
  568. package/dist/esm/langchain/openai.mjs.map +0 -1
  569. package/dist/esm/langchain/prompts.mjs.map +0 -1
  570. package/dist/esm/langchain/runnables.mjs.map +0 -1
  571. package/dist/esm/langchain/tools.mjs.map +0 -1
  572. package/dist/esm/langchain/utils/env.mjs.map +0 -1
  573. package/dist/esm/main.mjs.map +0 -1
@@ -1,2711 +1,2134 @@
1
- import { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';
2
- import { isBaseMessage, ToolMessage, HumanMessage, isAIMessage } from '@langchain/core/messages';
3
- import { isCommand, isGraphInterrupt, interrupt, Command, Send, END } from '@langchain/langgraph';
4
- import { ToolOutputReferenceRegistry, buildReferenceKey } from './toolOutputReferences.mjs';
5
- import { buildToolExecutionRequestPlan, recordArgsEqual } from './eagerEventExecution.mjs';
6
- import { calculateMaxToolResultChars, truncateToolResultContent } from '../utils/truncation.mjs';
7
- import './local/CompileCheckTool.mjs';
8
- import 'path';
9
- import 'fs/promises';
10
- import './local/LocalCodingTools.mjs';
11
- import './local/LocalExecutionEngine.mjs';
12
- import '@langchain/core/tools';
13
- import './BashExecutor.mjs';
14
- import './CodeExecutor.mjs';
15
- import { Constants, CODE_EXECUTION_TOOLS, GraphEvents } from '../common/enum.mjs';
16
- import 'http';
17
- import 'crypto';
18
- import './ProgrammaticToolCalling.mjs';
19
- import './BashProgrammaticToolCalling.mjs';
20
- import { executeHooks } from '../hooks/executeHooks.mjs';
21
- import '../hooks/createWorkspacePolicyHook.mjs';
22
- import { resolveLocalToolRegistry, resolveLocalExecutionTools } from './local/resolveLocalExecutionTools.mjs';
23
- import { withLangfuseToolOutputTracingConfig } from '../langfuseToolOutputTracing.mjs';
24
- import { stripCodeSessionFileSummary } from './CodeSessionFileSummary.mjs';
25
- import { toLangChainContent } from '../messages/langchain.mjs';
26
- import { safeDispatchCustomEvent } from '../utils/events.mjs';
27
- import '../stream.mjs';
28
- import { RunnableCallable } from '../utils/run.mjs';
29
- import 'ai-tokenizer';
30
- import 'zod-to-json-schema';
31
-
32
- const TOOL_NODE_RUN_NAME = 'tool_batch';
1
+ import { withLangfuseToolOutputTracingConfig } from "../langfuseToolOutputTracing.mjs";
2
+ import { CODE_EXECUTION_TOOLS } from "../common/enum.mjs";
3
+ import "../common/index.mjs";
4
+ import { toLangChainContent } from "../messages/langchain.mjs";
5
+ import { calculateMaxToolResultChars, truncateToolResultContent } from "../utils/truncation.mjs";
6
+ import { safeDispatchCustomEvent } from "../utils/events.mjs";
7
+ import { buildToolExecutionRequestPlan, recordArgsEqual } from "./eagerEventExecution.mjs";
8
+ import { ToolOutputReferenceRegistry, buildReferenceKey } from "./toolOutputReferences.mjs";
9
+ import { RunnableCallable } from "../utils/run.mjs";
10
+ import "../utils/index.mjs";
11
+ import { stripCodeSessionFileSummary } from "./CodeSessionFileSummary.mjs";
12
+ import { executeHooks } from "../hooks/executeHooks.mjs";
13
+ import "../hooks/index.mjs";
14
+ import { resolveLocalExecutionTools, resolveLocalToolRegistry } from "./local/resolveLocalExecutionTools.mjs";
15
+ import "./local/index.mjs";
16
+ import { HumanMessage, ToolMessage, isAIMessage, isBaseMessage } from "@langchain/core/messages";
17
+ import { Command, END, Send, interrupt, isCommand, isGraphInterrupt } from "@langchain/langgraph";
18
+ import { AsyncLocalStorageProviderSingleton } from "@langchain/core/singletons";
19
+ //#region src/tools/ToolNode.ts
20
+ const TOOL_NODE_RUN_NAME = "tool_batch";
33
21
  /**
34
- * Helper to check if a value is a Send object
35
- */
22
+ * Helper to check if a value is a Send object
23
+ */
36
24
  function isSend(value) {
37
- return value instanceof Send;
25
+ return value instanceof Send;
38
26
  }
39
27
  function isHandoffToolName(name) {
40
- return name.startsWith(Constants.LC_TRANSFER_TO_);
28
+ return name.startsWith("lc_transfer_to_");
41
29
  }
42
30
  /**
43
- * Format a fail-closed diagnostic for malformed approval-decision
44
- * fields. Hosts deserialize resume payloads from untyped JSON, so
45
- * `responseText` and `updatedInput` can land here as anything; the
46
- * blocking ToolMessage carries this string so the host can debug the
47
- * exact wire shape that was rejected.
48
- */
31
+ * Format a fail-closed diagnostic for malformed approval-decision
32
+ * fields. Hosts deserialize resume payloads from untyped JSON, so
33
+ * `responseText` and `updatedInput` can land here as anything; the
34
+ * blocking ToolMessage carries this string so the host can debug the
35
+ * exact wire shape that was rejected.
36
+ */
49
37
  function describeOfferedShape(value) {
50
- if (value === undefined) {
51
- return '<missing>';
52
- }
53
- if (value === null) {
54
- return 'null';
55
- }
56
- if (Array.isArray(value)) {
57
- return 'array';
58
- }
59
- return typeof value;
38
+ if (value === void 0) return "<missing>";
39
+ if (value === null) return "null";
40
+ if (Array.isArray(value)) return "array";
41
+ return typeof value;
60
42
  }
61
43
  /**
62
- * Build the `tool_approval` interrupt payload from the set of pending
63
- * `ask`-decision entries collected during PreToolUse hook handling.
64
- * Pure function — doesn't touch ToolNode state — so it lives at module
65
- * scope. The interrupt itself is raised by the caller (which still
66
- * needs `interrupt()` plus the AsyncLocalStorage anchoring shim).
67
- */
44
+ * Build the `tool_approval` interrupt payload from the set of pending
45
+ * `ask`-decision entries collected during PreToolUse hook handling.
46
+ * Pure function — doesn't touch ToolNode state — so it lives at module
47
+ * scope. The interrupt itself is raised by the caller (which still
48
+ * needs `interrupt()` plus the AsyncLocalStorage anchoring shim).
49
+ */
68
50
  function buildToolApprovalInterruptPayload(askEntries) {
69
- return {
70
- type: 'tool_approval',
71
- action_requests: askEntries.map(({ entry, reason }) => {
72
- const request = {
73
- tool_call_id: entry.call.id,
74
- name: entry.call.name,
75
- arguments: entry.args,
76
- };
77
- if (reason != null) {
78
- request.description = reason;
79
- }
80
- return request;
81
- }),
82
- review_configs: askEntries.map(({ entry, allowedDecisions }) => ({
83
- action_name: entry.call.name,
84
- tool_call_id: entry.call.id,
85
- allowed_decisions: (allowedDecisions ?? [
86
- 'approve',
87
- 'reject',
88
- 'edit',
89
- 'respond',
90
- ]),
91
- })),
92
- };
51
+ return {
52
+ type: "tool_approval",
53
+ action_requests: askEntries.map(({ entry, reason }) => {
54
+ const request = {
55
+ tool_call_id: entry.call.id,
56
+ name: entry.call.name,
57
+ arguments: entry.args
58
+ };
59
+ if (reason != null) request.description = reason;
60
+ return request;
61
+ }),
62
+ review_configs: askEntries.map(({ entry, allowedDecisions }) => ({
63
+ action_name: entry.call.name,
64
+ tool_call_id: entry.call.id,
65
+ allowed_decisions: allowedDecisions ?? [
66
+ "approve",
67
+ "reject",
68
+ "edit",
69
+ "respond"
70
+ ]
71
+ }))
72
+ };
93
73
  }
94
74
  /**
95
- * Build a `tool_call_id → ToolApprovalDecision` map from the host's
96
- * resume value. Hosts may return decisions either as an array (one per
97
- * action_request, in order) or as a record keyed by `tool_call_id`. Any
98
- * unrecognized shape (or a decision missing for a given call id) is
99
- * treated as "no decision" by callers — typically rejected so the run
100
- * doesn't silently invoke a tool the human never approved.
101
- */
75
+ * Build a `tool_call_id → ToolApprovalDecision` map from the host's
76
+ * resume value. Hosts may return decisions either as an array (one per
77
+ * action_request, in order) or as a record keyed by `tool_call_id`. Any
78
+ * unrecognized shape (or a decision missing for a given call id) is
79
+ * treated as "no decision" by callers — typically rejected so the run
80
+ * doesn't silently invoke a tool the human never approved.
81
+ */
102
82
  function normalizeApprovalDecisions(callIds, resumeValue) {
103
- const map = new Map();
104
- if (resumeValue == null) {
105
- return map;
106
- }
107
- if (Array.isArray(resumeValue)) {
108
- const limit = Math.min(callIds.length, resumeValue.length);
109
- for (let i = 0; i < limit; i++) {
110
- map.set(callIds[i], resumeValue[i]);
111
- }
112
- return map;
113
- }
114
- if (typeof resumeValue === 'object') {
115
- for (const callId of callIds) {
116
- const decision = resumeValue[callId];
117
- if (decision !== undefined) {
118
- map.set(callId, decision);
119
- }
120
- }
121
- }
122
- return map;
83
+ const map = /* @__PURE__ */ new Map();
84
+ if (resumeValue == null) return map;
85
+ if (Array.isArray(resumeValue)) {
86
+ const limit = Math.min(callIds.length, resumeValue.length);
87
+ for (let i = 0; i < limit; i++) map.set(callIds[i], resumeValue[i]);
88
+ return map;
89
+ }
90
+ if (typeof resumeValue === "object") for (const callId of callIds) {
91
+ const decision = resumeValue[callId];
92
+ if (decision !== void 0) map.set(callId, decision);
93
+ }
94
+ return map;
123
95
  }
124
96
  /**
125
- * Merges code execution session context into the sessions map.
126
- *
127
- * The codeapi worker reports two distinct ids on a code-execution result:
128
- * - `artifact.session_id` (the `sessionId` arg here) is the EXEC session
129
- * — the sandbox VM that ran the code. It's transient and torn down
130
- * post-execution; subsequent calls cannot reuse it as a sandbox.
131
- * - `file.storage_session_id` on each `artifact.files[i]` is the STORAGE
132
- * session — the file-server bucket prefix where the artifact actually
133
- * lives and is served from.
134
- *
135
- * Per-file `storage_session_id` is preserved (not overwritten with the
136
- * exec id) because `_injected_files` are looked up against the
137
- * file-server's storage path on subsequent tool calls. Stomping the
138
- * storage id with the exec id silently 404s every follow-up tool call
139
- * within the same run — `cat /mnt/data/foo.txt` reports "No such file
140
- * or directory" because the worker can't mount a file at a path the
141
- * storage doesn't know about. Fall back to the exec id only when the
142
- * per-file id is absent (e.g. inline `content` files have no persistent
143
- * storage location).
144
- */
97
+ * Merges code execution session context into the sessions map.
98
+ *
99
+ * The codeapi worker reports two distinct ids on a code-execution result:
100
+ * - `artifact.session_id` (the `sessionId` arg here) is the EXEC session
101
+ * — the sandbox VM that ran the code. It's transient and torn down
102
+ * post-execution; subsequent calls cannot reuse it as a sandbox.
103
+ * - `file.storage_session_id` on each `artifact.files[i]` is the STORAGE
104
+ * session — the file-server bucket prefix where the artifact actually
105
+ * lives and is served from.
106
+ *
107
+ * Per-file `storage_session_id` is preserved (not overwritten with the
108
+ * exec id) because `_injected_files` are looked up against the
109
+ * file-server's storage path on subsequent tool calls. Stomping the
110
+ * storage id with the exec id silently 404s every follow-up tool call
111
+ * within the same run — `cat /mnt/data/foo.txt` reports "No such file
112
+ * or directory" because the worker can't mount a file at a path the
113
+ * storage doesn't know about. Fall back to the exec id only when the
114
+ * per-file id is absent (e.g. inline `content` files have no persistent
115
+ * storage location).
116
+ */
145
117
  /**
146
- * Builds a `CodeEnvFile` ref from an arbitrary `FileRef`-like input,
147
- * narrowing onto the discriminated union: `kind: 'skill'` requires
148
- * `version`, other kinds forbid it.
149
- *
150
- * Defaults `kind` to `'user'` when unset — most ad-hoc files are
151
- * user-private; shared resources (skills/agents) populate their kind
152
- * upstream. A skill ref missing `version` falls back to `'user'` so
153
- * the upstream contract bug surfaces as a degraded sessionKey rather
154
- * than a runtime crash; primeSkillFiles is the only writer, and it
155
- * always sets `version` — see LC packages/api/src/agents/skillFiles.ts.
156
- *
157
- * `resource_id` carries the entity-that-owns-this-file's-session
158
- * identity (skill `_id` etc.); falls back to `id` (the storage
159
- * file_id) for inputs that haven't been updated to send the field
160
- * explicitly. The fallback degrades sessionKey resolution on the
161
- * codeapi side for shared kinds (it'll match the storage nanoid
162
- * against a skill _id and 403) — but won't crash, so an unmigrated
163
- * client still produces a diagnosable error instead of a stack
164
- * trace.
165
- */
118
+ * Builds a `CodeEnvFile` ref from an arbitrary `FileRef`-like input,
119
+ * narrowing onto the discriminated union: `kind: 'skill'` requires
120
+ * `version`, other kinds forbid it.
121
+ *
122
+ * Defaults `kind` to `'user'` when unset — most ad-hoc files are
123
+ * user-private; shared resources (skills/agents) populate their kind
124
+ * upstream. A skill ref missing `version` falls back to `'user'` so
125
+ * the upstream contract bug surfaces as a degraded sessionKey rather
126
+ * than a runtime crash; primeSkillFiles is the only writer, and it
127
+ * always sets `version` — see LC packages/api/src/agents/skillFiles.ts.
128
+ *
129
+ * `resource_id` carries the entity-that-owns-this-file's-session
130
+ * identity (skill `_id` etc.); falls back to `id` (the storage
131
+ * file_id) for inputs that haven't been updated to send the field
132
+ * explicitly. The fallback degrades sessionKey resolution on the
133
+ * codeapi side for shared kinds (it'll match the storage nanoid
134
+ * against a skill _id and 403) — but won't crash, so an unmigrated
135
+ * client still produces a diagnosable error instead of a stack
136
+ * trace.
137
+ */
166
138
  function toInjectedFileRef(file, execSessionId) {
167
- const base = {
168
- id: file.id,
169
- resource_id: file.resource_id ?? file.id,
170
- name: file.name,
171
- /* Inline `content` files have no persistent storage location;
172
- * fall back to the execution session id for those entries. */
173
- storage_session_id: file.storage_session_id ?? execSessionId,
174
- };
175
- const kind = file.kind ?? 'user';
176
- if (kind === 'skill' && file.version != null) {
177
- return { ...base, kind: 'skill', version: file.version };
178
- }
179
- if (kind === 'agent') {
180
- return { ...base, kind: 'agent' };
181
- }
182
- return { ...base, kind: 'user' };
139
+ const base = {
140
+ id: file.id,
141
+ resource_id: file.resource_id ?? file.id,
142
+ name: file.name,
143
+ storage_session_id: file.storage_session_id ?? execSessionId
144
+ };
145
+ const kind = file.kind ?? "user";
146
+ if (kind === "skill" && file.version != null) return {
147
+ ...base,
148
+ kind: "skill",
149
+ version: file.version
150
+ };
151
+ if (kind === "agent") return {
152
+ ...base,
153
+ kind: "agent"
154
+ };
155
+ return {
156
+ ...base,
157
+ kind: "user"
158
+ };
183
159
  }
184
- /* Stable file identity = `(storage_session_id, id)`. Same name in
185
- * different storage sessions are distinct files. */
186
160
  function fileIdentityKey(file) {
187
- return `${file.storage_session_id ?? ''}\0${file.id}`;
161
+ return `${file.storage_session_id ?? ""}\0${file.id}`;
188
162
  }
189
163
  function updateCodeSession(sessions, execSessionId, files) {
190
- const newFiles = files ?? [];
191
- const existingSession = sessions.get(Constants.EXECUTE_CODE);
192
- const existingFiles = existingSession?.files ?? [];
193
- if (newFiles.length === 0) {
194
- sessions.set(Constants.EXECUTE_CODE, {
195
- session_id: execSessionId,
196
- files: existingFiles,
197
- lastUpdated: Date.now(),
198
- });
199
- return;
200
- }
201
- /* Worker echoes lack ownership identity (kind/resource_id/version)
202
- * sandbox doesn't re-attest; that's signed at upload. Merge by
203
- * (storage_session_id, id) so prior identity survives the echo. */
204
- const filesWithSession = [];
205
- const newFileNames = new Set();
206
- const incomingByIdentity = new Map();
207
- for (const file of newFiles) {
208
- const withSession = {
209
- ...file,
210
- storage_session_id: file.storage_session_id ?? execSessionId,
211
- };
212
- incomingByIdentity.set(fileIdentityKey(withSession), filesWithSession.length);
213
- newFileNames.add(withSession.name);
214
- filesWithSession.push(withSession);
215
- }
216
- const filteredExisting = [];
217
- for (const e of existingFiles) {
218
- const idx = incomingByIdentity.get(fileIdentityKey(e));
219
- if (idx !== undefined) {
220
- filesWithSession[idx] = { ...e, ...filesWithSession[idx] };
221
- }
222
- if (!newFileNames.has(e.name)) {
223
- filteredExisting.push(e);
224
- }
225
- }
226
- sessions.set(Constants.EXECUTE_CODE, {
227
- session_id: execSessionId,
228
- files: [...filteredExisting, ...filesWithSession],
229
- lastUpdated: Date.now(),
230
- });
231
- }
232
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
233
- class ToolNode extends RunnableCallable {
234
- toolMap;
235
- loadRuntimeTools;
236
- handleToolErrors = true;
237
- trace = false;
238
- runLangfuse;
239
- agentLangfuse;
240
- toolCallStepIds;
241
- errorHandler;
242
- toolUsageCount;
243
- /** Maps toolCallId → turn captured in runTool, used by handleRunToolCompletions */
244
- toolCallTurns = new Map();
245
- /**
246
- * `call.id → turn` map dedicated to the direct-path lifecycle so the
247
- * turn assigned on first entry is REUSED on LangGraph resume.
248
- * Distinct from `toolCallTurns` (which is cleared at the start of
249
- * every `run()` to keep per-batch event-dispatch metadata fresh) —
250
- * the direct path needs stability across re-entries triggered by
251
- * `interrupt()` resumes (Codex P2 #30). Cleared with the rest of
252
- * the per-Run state in `clearHeavyState`-equivalent flushes when
253
- * the Run ends.
254
- */
255
- directPathTurns = new Map();
256
- /** Tool registry for filtering (lazy computation of programmatic maps) */
257
- toolRegistry;
258
- /** Cached programmatic tools (computed once on first PTC call) */
259
- programmaticCache;
260
- /** Reference to Graph's sessions map for automatic session injection */
261
- sessions;
262
- /** When true, dispatches ON_TOOL_EXECUTE events instead of invoking tools directly */
263
- eventDrivenMode = false;
264
- /** Opt-in stream-layer prestart config for event-driven tools. */
265
- eagerEventToolExecution;
266
- /** Shared per-run prestarted tool registry populated by ChatModelStreamHandler. */
267
- eagerEventToolExecutions;
268
- /** Shared per-run per-tool turn counter used by eager and normal event dispatch. */
269
- eagerEventToolUsageCount;
270
- /** Agent ID for event-driven mode */
271
- agentId;
272
- /**
273
- * ID of the agent that owns this tool node, whenever the graph knows it
274
- * (including top-level agents in a multi-agent graph). Surfaced to hooks as
275
- * `executingAgentId` so they can attribute a tool batch to a specific agent
276
- * even where `agentId` (the subagent-scope marker) is undefined.
277
- */
278
- executingAgentId;
279
- /** Tool names that bypass event dispatch and execute directly (e.g., graph-managed handoff tools) */
280
- directToolNames;
281
- /**
282
- * File checkpointer extracted from the local coding tool bundle when
283
- * `toolExecution.local.fileCheckpointing === true`. Exposed via
284
- * {@link getFileCheckpointer}. Undefined when checkpointing is off
285
- * or the local coding suite isn't bound to this node.
286
- */
287
- fileCheckpointer;
288
- /** Maximum characters allowed in a single tool result before truncation. */
289
- maxToolResultChars;
290
- /** Hook registry for PreToolUse/PostToolUse lifecycle hooks */
291
- hookRegistry;
292
- /**
293
- * Run-scoped HITL config. When `enabled`, `ask` decisions from
294
- * PreToolUse hooks raise a LangGraph `interrupt()` instead of being
295
- * treated as fail-closed denies.
296
- */
297
- humanInTheLoop;
298
- /**
299
- * Registry of tool outputs keyed by `tool<idx>turn<turn>`.
300
- *
301
- * Populated only when `toolOutputReferences.enabled` is true. The
302
- * registry owns the run-scoped state (turn counter, last-seen runId,
303
- * warn-once memo, stored outputs), so sharing a single instance
304
- * across multiple ToolNodes in a run lets cross-agent `{{…}}`
305
- * references resolve — which is why multi-agent graphs pass the
306
- * *same* instance to every ToolNode they compile rather than each
307
- * ToolNode building its own.
308
- */
309
- toolOutputRegistry;
310
- /** Run-scoped selection for swapping remote code tools to local executors. */
311
- toolExecution;
312
- /**
313
- * Monotonic counter used to mint a unique scope id for anonymous
314
- * batches (ones invoked without a `run_id` in
315
- * `config.configurable`). Each such batch gets its own registry
316
- * partition so concurrent anonymous invocations can't delete each
317
- * other's in-flight state.
318
- */
319
- anonBatchCounter = 0;
320
- constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, agentId, executingAgentId, directToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, }) {
321
- super({
322
- name: name ?? TOOL_NODE_RUN_NAME,
323
- tags,
324
- func: (input, config) => this.run(input, config),
325
- });
326
- this.trace = trace ?? this.trace;
327
- this.runLangfuse = runLangfuse;
328
- this.agentLangfuse = agentLangfuse;
329
- this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
330
- this.toolCallStepIds = toolCallStepIds;
331
- this.handleToolErrors = handleToolErrors ?? this.handleToolErrors;
332
- this.loadRuntimeTools = loadRuntimeTools;
333
- this.errorHandler = errorHandler;
334
- this.toolUsageCount = new Map();
335
- this.toolRegistry = resolveLocalToolRegistry({
336
- toolRegistry,
337
- toolExecution,
338
- });
339
- this.sessions = sessions;
340
- this.eventDrivenMode = eventDrivenMode ?? false;
341
- this.eagerEventToolExecution = eagerEventToolExecution;
342
- this.eagerEventToolExecutions = eagerEventToolExecutions;
343
- this.eagerEventToolUsageCount = eagerEventToolUsageCount;
344
- this.agentId = agentId;
345
- // Default to agentId so callers constructing ToolNode directly (who pass the
346
- // existing agentId option) still get attribution without knowing the new option.
347
- this.executingAgentId = executingAgentId ?? agentId;
348
- this.directToolNames = directToolNames;
349
- this.maxToolResultChars =
350
- maxToolResultChars ?? calculateMaxToolResultChars(maxContextTokens);
351
- this.hookRegistry = hookRegistry;
352
- this.humanInTheLoop = humanInTheLoop;
353
- this.toolExecution = toolExecution;
354
- // Caller-provided checkpointer wins. Graphs use this to share a
355
- // single per-Run instance across every ToolNode they compile so
356
- // `Run.rewindFiles()` reaches the same snapshot store regardless
357
- // of which agent's tool batch ran. Falls through to the bundle's
358
- // auto-created one when undefined (direct ToolNode construction).
359
- this.fileCheckpointer = fileCheckpointer;
360
- this.applyToolExecutionOverrides();
361
- /**
362
- * Precedence: an explicitly passed `toolOutputRegistry` instance
363
- * wins over a config object so a host (`Graph`) can share one
364
- * registry across many ToolNodes. When only the config is
365
- * provided (direct ToolNode usage), build a local registry so
366
- * the feature still works without graph-level plumbing. Registry
367
- * caps are intentionally decoupled from `maxToolResultChars`:
368
- * the registry stores the raw untruncated output so a later
369
- * `{{…}}` substitution pipes the full payload into the next
370
- * tool, even when the LLM saw a truncated preview.
371
- */
372
- if (toolOutputRegistry != null) {
373
- this.toolOutputRegistry = toolOutputRegistry;
374
- }
375
- else if (toolOutputReferences?.enabled === true) {
376
- this.toolOutputRegistry = new ToolOutputReferenceRegistry({
377
- maxOutputSize: toolOutputReferences.maxOutputSize,
378
- maxTotalSize: toolOutputReferences.maxTotalSize,
379
- });
380
- }
381
- }
382
- async invoke(
383
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
384
- input, options
385
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
386
- ) {
387
- return withLangfuseToolOutputTracingConfig(this.runLangfuse, () => super.invoke(input, options), this.agentLangfuse);
388
- }
389
- /**
390
- * Returns the run-scoped tool output registry, or `undefined` when
391
- * the feature is disabled.
392
- *
393
- * @internal Exposed for test observation only. Host code should rely
394
- * on `{{tool<i>turn<n>}}` substitution at tool-invocation time and
395
- * not mutate the registry directly.
396
- */
397
- _unsafeGetToolOutputRegistry() {
398
- return this.toolOutputRegistry;
399
- }
400
- /**
401
- * Replaces known remote Code API tools with local-process tools when
402
- * `RunConfig.toolExecution.engine === 'local'`. In event-driven mode those
403
- * names are also marked direct so the SDK executes them locally instead of
404
- * dispatching the batch to a host-side remote sandbox handler. When the
405
- * local coding suite is enabled, this also injects file/search/edit tools.
406
- */
407
- applyToolExecutionOverrides() {
408
- const resolved = resolveLocalExecutionTools({
409
- toolMap: this.toolMap,
410
- toolExecution: this.toolExecution,
411
- fileCheckpointer: this.fileCheckpointer,
412
- });
413
- this.toolMap = resolved.toolMap;
414
- if (resolved.fileCheckpointer != null) {
415
- this.fileCheckpointer = resolved.fileCheckpointer;
416
- }
417
- if (resolved.directToolNames.size === 0) {
418
- return;
419
- }
420
- this.directToolNames = new Set([
421
- ...(this.directToolNames ?? new Set()),
422
- ...resolved.directToolNames,
423
- ]);
424
- this.programmaticCache = undefined;
425
- }
426
- /**
427
- * Returns the per-Run file checkpointer when
428
- * `toolExecution.local.fileCheckpointing === true`. Hosts call
429
- * `rewind()` on the returned object to restore captured pre-write
430
- * file contents — the standard "undo a tool batch" pattern. Returns
431
- * undefined when checkpointing is disabled or the local coding suite
432
- * isn't bound. Manual review (finding E): without this getter, the
433
- * config flag was a silent no-op outside of direct
434
- * `createLocalCodingToolBundle()` use.
435
- */
436
- getFileCheckpointer() {
437
- return this.fileCheckpointer;
438
- }
439
- *getRegisteredHandoffNames() {
440
- if (this.directToolNames != null) {
441
- for (const toolName of this.directToolNames) {
442
- yield toolName;
443
- }
444
- }
445
- for (const toolName of this.toolMap.keys()) {
446
- if (this.directToolNames?.has(toolName) === true) {
447
- continue;
448
- }
449
- yield toolName;
450
- }
451
- }
452
- hasRegisteredHandoffTool() {
453
- for (const toolName of this.getRegisteredHandoffNames()) {
454
- if (isHandoffToolName(toolName)) {
455
- return true;
456
- }
457
- }
458
- return false;
459
- }
460
- getHandoffToolNameSuggestion(callName) {
461
- if (!isHandoffToolName(callName)) {
462
- return undefined;
463
- }
464
- let suggestion;
465
- for (const toolName of this.getRegisteredHandoffNames()) {
466
- if (!isHandoffToolName(toolName) ||
467
- toolName.length >= callName.length ||
468
- !callName.startsWith(toolName)) {
469
- continue;
470
- }
471
- if (suggestion == null || toolName.length > suggestion.length) {
472
- suggestion = toolName;
473
- }
474
- }
475
- return suggestion;
476
- }
477
- shouldHandleUnknownHandoffLocally(callName, hasRegisteredHandoffTool) {
478
- if (!isHandoffToolName(callName) || this.toolMap.has(callName)) {
479
- return false;
480
- }
481
- return hasRegisteredHandoffTool ?? this.hasRegisteredHandoffTool();
482
- }
483
- getUnknownToolErrorMessage(callName) {
484
- const suggestion = this.getHandoffToolNameSuggestion(callName);
485
- if (suggestion == null) {
486
- return `Tool "${callName}" not found.`;
487
- }
488
- return (`Tool "${callName}" not found. Did you mean "${suggestion}"? ` +
489
- 'Handoff tool names must match exactly.');
490
- }
491
- /**
492
- * Flush the per-Run direct-path turn cache. Called by the Graph at
493
- * end-of-Run via `clearHeavyState`. The map intentionally survives
494
- * `run()` re-entry so an interrupt + resume reuses the original
495
- * slot (Codex P2 #30), but it would otherwise grow linearly with
496
- * tool calls and could collide across Runs if a provider reused
497
- * call IDs (Codex P2 #33). Hosts can also call this directly if
498
- * they reuse a ToolNode across batches outside of a Graph.
499
- */
500
- clearDirectPathTurns() {
501
- this.directPathTurns.clear();
502
- }
503
- /**
504
- * Returns cached programmatic tools, computing once on first access.
505
- * Single iteration builds both toolMap and toolDefs simultaneously.
506
- */
507
- getProgrammaticTools() {
508
- if (this.programmaticCache)
509
- return this.programmaticCache;
510
- const toolMap = new Map();
511
- const toolDefs = [];
512
- if (this.toolRegistry) {
513
- for (const [name, toolDef] of this.toolRegistry) {
514
- if ((toolDef.allowed_callers ?? ['direct']).includes('code_execution')) {
515
- toolDefs.push(toolDef);
516
- const tool = this.toolMap.get(name);
517
- if (tool)
518
- toolMap.set(name, tool);
519
- }
520
- }
521
- }
522
- this.programmaticCache = { toolMap, toolDefs };
523
- return this.programmaticCache;
524
- }
525
- /**
526
- * Returns a snapshot of the current tool usage counts.
527
- * @returns A ReadonlyMap where keys are tool names and values are their usage counts.
528
- */
529
- getToolUsageCounts() {
530
- return new Map(this.toolUsageCount); // Return a copy
531
- }
532
- recordToolUsageTurn(toolName, turn, callId) {
533
- this.toolUsageCount.set(toolName, Math.max(this.toolUsageCount.get(toolName) ?? 0, turn + 1));
534
- if (callId != null && callId !== '') {
535
- this.toolCallTurns.set(callId, turn);
536
- }
537
- }
538
- recordEventToolPlanningTurn(toolName, turn, callId) {
539
- this.recordToolUsageTurn(toolName, turn, callId);
540
- if (this.canConsumeEagerEventExecution()) {
541
- this.eagerEventToolUsageCount?.set(toolName, Math.max(this.eagerEventToolUsageCount.get(toolName) ?? 0, turn + 1));
542
- }
543
- }
544
- /**
545
- * Runs a single tool call with error handling.
546
- *
547
- * `batchIndex` is the tool's position within the current ToolNode
548
- * batch and, together with `this.currentTurn`, forms the key used to
549
- * register the output for future `{{tool<idx>turn<turn>}}`
550
- * substitutions. Omit when no registration should occur.
551
- */
552
- async runTool(call, config, batchContext = {}) {
553
- const { batchIndex, turn, batchScopeId, resolvedArgsByCallId, preBatchSnapshot, } = batchContext;
554
- const tool = this.toolMap.get(call.name);
555
- const registry = this.toolOutputRegistry;
556
- let resolveFn;
557
- if (preBatchSnapshot != null) {
558
- resolveFn = (_runId, args) => preBatchSnapshot.resolve(args);
559
- }
560
- else if (registry != null) {
561
- resolveFn = (runIdArg, args) => registry.resolve(runIdArg, args);
562
- }
563
- /**
564
- * Precompute the reference key once per call — captured locally
565
- * so concurrent `invoke()` calls on the same ToolNode cannot race
566
- * on a shared turn field.
567
- */
568
- const refKey = registry != null && batchIndex != null && turn != null
569
- ? buildReferenceKey(batchIndex, turn)
570
- : undefined;
571
- /**
572
- * Hoisted outside the try so the catch branch can append
573
- * `[unresolved refs: …]` to error messages — otherwise the LLM
574
- * only sees a generic error when it references a bad key, losing
575
- * the self-correction signal this feature is meant to provide.
576
- */
577
- let unresolvedRefs = [];
578
- /**
579
- * Use the caller-provided `batchScopeId` when threaded from
580
- * `run()` (so anonymous batches get their own unique scope).
581
- * Fall back to the config's `run_id` when runTool is invoked
582
- * from a context that doesn't thread it — that still preserves
583
- * the runId-based partitioning for named runs.
584
- */
585
- const runId = batchScopeId ?? config.configurable?.run_id;
586
- try {
587
- if (tool === undefined) {
588
- throw new Error(this.getUnknownToolErrorMessage(call.name));
589
- }
590
- /**
591
- * `usageCount` is the per-tool-name invocation index that
592
- * web-search and other tools observe via `invokeParams.turn`.
593
- * It is intentionally distinct from the outer `turn` parameter
594
- * (the batch turn used for ref keys); the latter is captured
595
- * before the try block when constructing `refKey`.
596
- *
597
- * Prefer the value `runDirectToolWithLifecycleHooks` already
598
- * incremented (Codex P2 #27) — its hook wants the SAME turn
599
- * the tool will execute under. When called from a path that
600
- * doesn't pre-increment (event dispatch, the no-hooks
601
- * shortcut), do the read+increment here.
602
- */
603
- const usageCount = batchContext.usageCount ??
604
- (() => {
605
- const next = this.toolUsageCount.get(call.name) ?? 0;
606
- this.toolUsageCount.set(call.name, next + 1);
607
- if (call.id != null && call.id !== '') {
608
- this.toolCallTurns.set(call.id, next);
609
- }
610
- return next;
611
- })();
612
- let args = call.args;
613
- if (resolveFn != null) {
614
- const { resolved, unresolved } = resolveFn(runId, args);
615
- args = resolved;
616
- unresolvedRefs = unresolved;
617
- /**
618
- * Expose the post-substitution args to downstream completion
619
- * events so audit logs / host-side `ON_RUN_STEP_COMPLETED`
620
- * handlers observe what actually ran, not the `{{…}}`
621
- * template. Only string/object args are worth recording.
622
- */
623
- if (resolvedArgsByCallId != null &&
624
- call.id != null &&
625
- call.id !== '' &&
626
- resolved !== call.args &&
627
- typeof resolved === 'object') {
628
- resolvedArgsByCallId.set(call.id, resolved);
629
- }
630
- }
631
- const stepId = this.toolCallStepIds?.get(call.id);
632
- // Build invoke params - LangChain extracts non-schema fields to config.toolCall
633
- // `turn` here is the per-tool usage count (matches what tools have
634
- // observed historically via config.toolCall.turn — e.g. web search).
635
- let invokeParams = {
636
- ...call,
637
- args,
638
- type: 'tool_call',
639
- stepId,
640
- turn: usageCount,
641
- };
642
- // Inject runtime data for special tools (becomes available at config.toolCall)
643
- if (call.name === Constants.PROGRAMMATIC_TOOL_CALLING ||
644
- call.name === Constants.BASH_PROGRAMMATIC_TOOL_CALLING) {
645
- const { toolMap, toolDefs } = this.getProgrammaticTools();
646
- invokeParams = {
647
- ...invokeParams,
648
- toolMap,
649
- toolDefs,
650
- // Plumb the hook context into the programmatic-tool path so
651
- // inner tool calls made via the in-process bridge can run
652
- // through `PreToolUse` (deny / updatedInput) before reaching
653
- // the underlying tool. Without this, programmatic tool calls
654
- // bypass every PreToolUse hook the host registered for the tools
655
- // they dispatch — including HITL gates on `write_file` / `edit_file`.
656
- hookContext: {
657
- registry: this.hookRegistry,
658
- runId: config.configurable?.run_id ?? '',
659
- threadId: config.configurable?.thread_id,
660
- agentId: this.agentId,
661
- executingAgentId: this.executingAgentId,
662
- },
663
- };
664
- }
665
- else if (call.name === Constants.TOOL_SEARCH) {
666
- invokeParams = {
667
- ...invokeParams,
668
- toolRegistry: this.toolRegistry,
669
- };
670
- }
671
- /**
672
- * Inject session context for code execution tools when available.
673
- * Each file uses its own session_id (supporting multi-session file tracking).
674
- * Both session_id and _injected_files are injected directly to invokeParams
675
- * (not inside args) so they bypass Zod schema validation and reach config.toolCall.
676
- *
677
- * session_id is always injected when available, but concrete file refs
678
- * still need to travel through `_injected_files`; the legacy
679
- * `/files/<session_id>` fallback was removed from the executors.
680
- */
681
- if (CODE_EXECUTION_TOOLS.has(call.name)) {
682
- const codeSession = this.sessions?.get(Constants.EXECUTE_CODE);
683
- const execSessionId = codeSession?.session_id;
684
- if (execSessionId != null && execSessionId !== '') {
685
- invokeParams = {
686
- ...invokeParams,
687
- session_id: execSessionId,
688
- };
689
- if (codeSession?.files != null && codeSession.files.length > 0) {
690
- invokeParams._injected_files = codeSession.files.map((file) => toInjectedFileRef(file, execSessionId));
691
- }
692
- }
693
- }
694
- const output = await tool.invoke(invokeParams, config);
695
- if (isCommand(output)) {
696
- return output;
697
- }
698
- if (isBaseMessage(output) && output._getType() === 'tool') {
699
- const toolMsg = output;
700
- const isError = toolMsg.status === 'error';
701
- if (isError) {
702
- /**
703
- * Error ToolMessages bypass registration but still stamp the
704
- * unresolved-refs hint into `additional_kwargs` so the lazy
705
- * annotation transform surfaces it to the LLM, letting the
706
- * model self-correct when its reference key caused the
707
- * failure. Persisted `content` stays clean.
708
- */
709
- if (unresolvedRefs.length > 0) {
710
- toolMsg.additional_kwargs = {
711
- ...toolMsg.additional_kwargs,
712
- _unresolvedRefs: unresolvedRefs,
713
- };
714
- }
715
- return toolMsg;
716
- }
717
- if (this.toolOutputRegistry != null || unresolvedRefs.length > 0) {
718
- if (typeof toolMsg.content === 'string') {
719
- const rawContent = toolMsg.content;
720
- const registryContent = stripCodeSessionFileSummary(rawContent);
721
- const llmContent = truncateToolResultContent(rawContent, this.maxToolResultChars);
722
- toolMsg.content = llmContent;
723
- const refMeta = this.recordOutputReference(runId, registryContent, refKey, unresolvedRefs);
724
- if (refMeta != null) {
725
- toolMsg.additional_kwargs = {
726
- ...toolMsg.additional_kwargs,
727
- ...refMeta,
728
- };
729
- }
730
- }
731
- else {
732
- /**
733
- * Non-string content (multi-part content blocks — text +
734
- * image). Known limitation: we cannot register under a
735
- * reference key because there's no canonical serialized
736
- * form. Warn once per tool per run when the caller
737
- * intended to register. The unresolved-refs hint is still
738
- * stamped as metadata; the lazy transform prepends a text
739
- * block at request time so the LLM gets the self-correction
740
- * signal.
741
- */
742
- if (unresolvedRefs.length > 0) {
743
- toolMsg.additional_kwargs = {
744
- ...toolMsg.additional_kwargs,
745
- _unresolvedRefs: unresolvedRefs,
746
- };
747
- }
748
- if (refKey != null &&
749
- this.toolOutputRegistry.claimWarnOnce(runId, call.name)) {
750
- // eslint-disable-next-line no-console
751
- console.warn(`[ToolNode] Skipping tool output reference for "${call.name}": ` +
752
- 'ToolMessage content is not a string (further occurrences for this tool in the same run are suppressed).');
753
- }
754
- }
755
- }
756
- return toolMsg;
757
- }
758
- const rawContent = typeof output === 'string' ? output : JSON.stringify(output);
759
- const truncated = truncateToolResultContent(rawContent, this.maxToolResultChars);
760
- const refMeta = this.recordOutputReference(runId, stripCodeSessionFileSummary(rawContent), refKey, unresolvedRefs);
761
- return new ToolMessage({
762
- status: 'success',
763
- name: tool.name,
764
- content: truncated,
765
- tool_call_id: call.id,
766
- ...(refMeta != null && {
767
- additional_kwargs: refMeta,
768
- }),
769
- });
770
- }
771
- catch (_e) {
772
- const e = _e;
773
- if (!this.handleToolErrors) {
774
- throw e;
775
- }
776
- if (isGraphInterrupt(e)) {
777
- throw e;
778
- }
779
- if (this.errorHandler) {
780
- try {
781
- await this.errorHandler({
782
- error: e,
783
- id: call.id,
784
- name: call.name,
785
- input: call.args,
786
- }, config.metadata);
787
- }
788
- catch (handlerError) {
789
- // eslint-disable-next-line no-console
790
- console.error('Error in errorHandler:', {
791
- toolName: call.name,
792
- toolCallId: call.id,
793
- toolArgs: call.args,
794
- stepId: this.toolCallStepIds?.get(call.id),
795
- turn: this.toolUsageCount.get(call.name),
796
- originalError: {
797
- message: e.message,
798
- stack: e.stack ?? undefined,
799
- },
800
- handlerError: handlerError instanceof Error
801
- ? {
802
- message: handlerError.message,
803
- stack: handlerError.stack ?? undefined,
804
- }
805
- : {
806
- message: String(handlerError),
807
- stack: undefined,
808
- },
809
- });
810
- }
811
- }
812
- const errorContent = `Error: ${e.message}\n Please fix your mistakes.`;
813
- const refMeta = unresolvedRefs.length > 0
814
- ? this.recordOutputReference(runId, errorContent, undefined, unresolvedRefs)
815
- : undefined;
816
- return new ToolMessage({
817
- status: 'error',
818
- content: errorContent,
819
- name: call.name,
820
- tool_call_id: call.id ?? '',
821
- ...(refMeta != null && {
822
- additional_kwargs: refMeta,
823
- }),
824
- });
825
- }
826
- }
827
- /**
828
- * Runs a single in-process tool call with the same lifecycle hooks
829
- * the event-dispatch path fires (`PreToolUse`, `PermissionDenied`,
830
- * `PostToolUse`, `PostToolUseFailure`). Used for any tool whose
831
- * implementation lives in the SDK process — i.e. every entry in
832
- * `directToolNames` — so host-supplied policy hooks gate
833
- * direct-invoked tools the same way they gate dispatched ones.
834
- *
835
- * Fast path: when the registry has none of the relevant events
836
- * registered for this run, falls through to `runTool` with zero
837
- * extra work. The hook list is also checked via
838
- * `hasHookFor(event, runId)`, which performs the registry's own
839
- * O(1) shortcut.
840
- *
841
- * Hook semantics intentionally mirror `dispatchToolEvents` for the
842
- * single-call case:
843
- * - `PreToolUse` returning `decision: 'deny'` synthesizes an error
844
- * `ToolMessage` and fires `PermissionDenied` (observational).
845
- * - `PreToolUse` returning `decision: 'ask'`:
846
- * • When `humanInTheLoop.enabled === true`: raises a real
847
- * `tool_approval` interrupt for this single tool call (the
848
- * same payload shape the event path produces). On resume:
849
- * `approve` runs the tool, `reject` blocks via
850
- * `blockDirectCall`, `respond` returns the host-supplied
851
- * `responseText` as a synthetic success ToolMessage,
852
- * `edit` re-runs with edited args. LangGraph re-enters
853
- * ToolNode.run from the start on resume; the hook fires
854
- * again and the resume value distinguishes "first ask" from
855
- * "second pass with decision".
856
- * • When HITL is off: collapses to a fail-closed deny (matches
857
- * the rest of the SDK's HITL-disabled default). One-time
858
- * warning logged so hosts notice the gap.
859
- * - `PreToolUse.updatedInput` is applied to the call before
860
- * `runTool` runs; placeholder resolution inside `runTool` is
861
- * idempotent on already-resolved args.
862
- * - `PostToolUse.updatedOutput` replaces the returned
863
- * `ToolMessage` content (preserving id/name/status).
864
- * - `PostToolUseFailure` fires when `runTool` returns a
865
- * `ToolMessage` whose `status === 'error'`. Observational only;
866
- * the error message stays the source of truth.
867
- *
868
- * `PostToolBatch` aggregation across direct + dispatched outcomes is
869
- * a separate concern: `dispatchToolEvents` accumulates batch entries
870
- * locally and fires `PostToolBatch` at the end of its scope. Wiring
871
- * direct-call entries into that aggregation crosses the two paths'
872
- * scopes and is left to a follow-up.
873
- */
874
- async runDirectToolWithLifecycleHooks(call, config, batchContext = {}) {
875
- const runId = config.configurable?.run_id ?? '';
876
- const hookRegistry = this.hookRegistry;
877
- const hasPreHook = hookRegistry?.hasHookFor('PreToolUse', runId) === true;
878
- const hasPostHook = hookRegistry?.hasHookFor('PostToolUse', runId) === true;
879
- const hasFailureHook = hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
880
- if (hookRegistry == null ||
881
- (!hasPreHook && !hasPostHook && !hasFailureHook)) {
882
- return this.runTool(call, config, batchContext);
883
- }
884
- const threadId = config.configurable?.thread_id;
885
- const registryRunId = batchContext.batchScopeId ??
886
- config.configurable?.run_id;
887
- // Slot reservation, synchronous, before any await:
888
- // 1. If this call.id already has a recorded turn (from a prior
889
- // entry that asked / interrupted), REUSE it. LangGraph
890
- // re-runs the entire ToolNode on resume, so the same call
891
- // can hit this code multiple times — incrementing on each
892
- // pass would push the eventual approved execution to
893
- // `turn=N` instead of `turn=0` (Codex P2 #30: the fix from
894
- // P2 #27 over-incremented across re-entries).
895
- // 2. Otherwise reserve the next slot from the counter. Done
896
- // synchronously so concurrent same-tool calls in a single
897
- // Promise.all batch get distinct turns (the original P2 #27
898
- // requirement still holds).
899
- // Net: turns are stable per call.id across interrupt/resume,
900
- // unique per call within a batch.
901
- let usageCount;
902
- // Look in the resume-stable map first; fall back to the
903
- // per-batch one. (`directPathTurns` is set on first entry and
904
- // survives `run()`'s clear, so a resume sees the original
905
- // assignment.)
906
- const cachedTurn = call.id != null && call.id !== ''
907
- ? (this.directPathTurns.get(call.id) ?? this.toolCallTurns.get(call.id))
908
- : undefined;
909
- if (cachedTurn != null) {
910
- usageCount = cachedTurn;
911
- }
912
- else {
913
- usageCount = this.toolUsageCount.get(call.name) ?? 0;
914
- this.toolUsageCount.set(call.name, usageCount + 1);
915
- if (call.id != null && call.id !== '') {
916
- this.toolCallTurns.set(call.id, usageCount);
917
- // Dedicated direct-path map that SURVIVES `run()`'s
918
- // toolCallTurns.clear() — so a re-entry triggered by
919
- // LangGraph interrupt resume reuses this slot instead of
920
- // re-incrementing. Codex P2 #30.
921
- this.directPathTurns.set(call.id, usageCount);
922
- }
923
- }
924
- const turn = usageCount;
925
- const stepId = this.toolCallStepIds?.get(call.id ?? '') ?? '';
926
- // Use the caller-threaded snapshot when available (P1 #18) so the
927
- // value the PreToolUse hook observes matches the value the
928
- // (later-awaited) `runTool` will actually run with — both are
929
- // anchored to the pre-batch registry state.
930
- let resolvedArgs = call.args;
931
- if (batchContext.preBatchSnapshot != null) {
932
- const { resolved } = batchContext.preBatchSnapshot.resolve(call.args);
933
- resolvedArgs = resolved;
934
- }
935
- else if (this.toolOutputRegistry != null) {
936
- const { resolved } = this.toolOutputRegistry.resolve(registryRunId, call.args);
937
- resolvedArgs = resolved;
938
- }
939
- let effectiveCall = call;
940
- if (hasPreHook) {
941
- const preResult = await executeHooks({
942
- registry: hookRegistry,
943
- input: {
944
- hook_event_name: 'PreToolUse',
945
- runId,
946
- threadId,
947
- agentId: this.agentId,
948
- executingAgentId: this.executingAgentId,
949
- toolName: call.name,
950
- toolInput: resolvedArgs,
951
- toolUseId: call.id ?? '',
952
- stepId,
953
- turn,
954
- },
955
- sessionId: runId,
956
- matchQuery: call.name,
957
- }).catch(() => undefined);
958
- if (preResult != null) {
959
- // Forward any additionalContext strings hooks returned into
960
- // the per-batch sink so the caller materializes them as a
961
- // HumanMessage for the next model turn — same shape as the
962
- // event-driven path's `injected[]`. Codex P2 #39.
963
- if (batchContext.additionalContextsSink != null &&
964
- preResult.additionalContexts.length > 0) {
965
- batchContext.additionalContextsSink.push(...preResult.additionalContexts);
966
- }
967
- // Apply any input rewrite first — `ask`-with-`updatedInput` is
968
- // a valid combination (one matcher sanitises args, another asks
969
- // for approval); the reviewer should see the sanitised args.
970
- if (preResult.updatedInput != null) {
971
- effectiveCall = {
972
- ...call,
973
- args: preResult.updatedInput,
974
- };
975
- }
976
- if (preResult.decision === 'deny') {
977
- return this.blockDirectCall({
978
- call,
979
- resolvedArgs,
980
- reason: preResult.reason ?? 'Blocked by hook',
981
- hookRegistry,
982
- runId,
983
- threadId,
984
- });
985
- }
986
- if (preResult.decision === 'ask') {
987
- if (this.humanInTheLoop?.enabled !== true) {
988
- // Fail-closed: no HITL UI configured, so we can't actually
989
- // ask. Logged once via the existing helper.
990
- const reason = this.resolveAskDecisionForDirectTool(preResult.reason, call.name);
991
- return this.blockDirectCall({
992
- call,
993
- resolvedArgs,
994
- reason,
995
- hookRegistry,
996
- runId,
997
- threadId,
998
- });
999
- }
1000
- // Raise a single-tool tool_approval interrupt. LangGraph
1001
- // throws on the first execution (host gets the interrupt)
1002
- // and returns the resume value on re-entry. Because direct
1003
- // tools re-enter the entire ToolNode.run on resume, the
1004
- // PreToolUse hook fires AGAIN — which is fine: the hook is
1005
- // expected to be deterministic, and the resume value is what
1006
- // distinguishes "first call asking" from "second call after
1007
- // approve/reject". We anchor `interrupt()` against the
1008
- // node's RunnableConfig the same way `dispatchToolEvents`
1009
- // does (ToolNode disables LangSmith tracing, so the
1010
- // AsyncLocalStorage frame must be re-established here).
1011
- const askEntry = {
1012
- entry: {
1013
- call: effectiveCall,
1014
- args: effectiveCall.args,
1015
- stepId,
1016
- },
1017
- reason: preResult.reason,
1018
- allowedDecisions: preResult.allowedDecisions,
1019
- };
1020
- const payload = buildToolApprovalInterruptPayload([askEntry]);
1021
- const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(config, () => interrupt(payload));
1022
- const decisionByCallId = normalizeApprovalDecisions([call.id], resumeValue);
1023
- const decision = decisionByCallId.get(call.id) ?? {
1024
- type: 'reject',
1025
- reason: 'No decision provided for tool approval',
1026
- };
1027
- const declaredType = decision.type;
1028
- if (preResult.allowedDecisions != null &&
1029
- (typeof declaredType !== 'string' ||
1030
- !preResult.allowedDecisions.includes(declaredType))) {
1031
- return this.blockDirectCall({
1032
- call,
1033
- resolvedArgs,
1034
- reason: `Decision "${typeof declaredType === 'string' ? declaredType : '<missing>'}" not in allowedDecisions [${preResult.allowedDecisions.join(', ')}] — failing closed`,
1035
- hookRegistry,
1036
- runId,
1037
- threadId,
1038
- });
1039
- }
1040
- if (decision.type === 'reject') {
1041
- return this.blockDirectCall({
1042
- call,
1043
- resolvedArgs,
1044
- reason: decision.reason ?? preResult.reason ?? 'Rejected by user',
1045
- hookRegistry,
1046
- runId,
1047
- threadId,
1048
- });
1049
- }
1050
- if (decision.type === 'respond') {
1051
- const responseText = decision
1052
- .responseText;
1053
- if (typeof responseText !== 'string') {
1054
- return this.blockDirectCall({
1055
- call,
1056
- resolvedArgs,
1057
- reason: 'Approval payload `respond` was missing a string `responseText`',
1058
- hookRegistry,
1059
- runId,
1060
- threadId,
1061
- });
1062
- }
1063
- return new ToolMessage({
1064
- status: 'success',
1065
- content: responseText,
1066
- name: call.name,
1067
- tool_call_id: call.id ?? '',
1068
- });
1069
- }
1070
- if (decision.type === 'edit') {
1071
- // Mirror the event-driven path's validation
1072
- // (see `dispatchToolEvents`'s edit branch). The wire
1073
- // field is `updatedInput`, NOT `args` — hosts following
1074
- // the documented `ToolApprovalDecision` shape were
1075
- // silently ignored before, so the tool ran with the
1076
- // original (un-edited) arguments. Fail closed on
1077
- // malformed payloads instead of falling through with
1078
- // undefined args.
1079
- const updatedInput = decision
1080
- .updatedInput;
1081
- if (updatedInput === null ||
1082
- typeof updatedInput !== 'object' ||
1083
- Array.isArray(updatedInput)) {
1084
- return new ToolMessage({
1085
- status: 'error',
1086
- content: 'Decision "edit" missing object updatedInput — failing closed.',
1087
- name: call.name,
1088
- tool_call_id: call.id ?? '',
1089
- });
1090
- }
1091
- effectiveCall = {
1092
- ...call,
1093
- args: updatedInput,
1094
- };
1095
- // fall through to executing the edited call
1096
- }
1097
- // 'approve' (or 'edit' after applying edits) → fall through
1098
- }
1099
- }
1100
- }
1101
- const output = await this.runTool(effectiveCall, config, {
1102
- ...batchContext,
1103
- usageCount,
1104
- });
1105
- if (!(output instanceof ToolMessage)) {
1106
- return output;
1107
- }
1108
- if (output.status === 'error' && hasFailureHook) {
1109
- // Await the failure hook (instead of fire-and-forget) so we
1110
- // can capture additionalContexts before returning. The hook is
1111
- // still observational w.r.t. the tool result itself — we don't
1112
- // mutate `output`, just plumb the contexts. Codex P2 #39.
1113
- const failureResult = await executeHooks({
1114
- registry: hookRegistry,
1115
- input: {
1116
- hook_event_name: 'PostToolUseFailure',
1117
- runId,
1118
- threadId,
1119
- agentId: this.agentId,
1120
- executingAgentId: this.executingAgentId,
1121
- toolName: call.name,
1122
- toolInput: effectiveCall.args,
1123
- toolUseId: call.id ?? '',
1124
- error: typeof output.content === 'string'
1125
- ? output.content
1126
- : JSON.stringify(output.content),
1127
- stepId,
1128
- turn,
1129
- },
1130
- sessionId: runId,
1131
- matchQuery: call.name,
1132
- }).catch(() => undefined);
1133
- if (failureResult != null &&
1134
- batchContext.additionalContextsSink != null &&
1135
- failureResult.additionalContexts.length > 0) {
1136
- batchContext.additionalContextsSink.push(...failureResult.additionalContexts);
1137
- }
1138
- return output;
1139
- }
1140
- if (output.status !== 'error' && hasPostHook) {
1141
- const postResult = await executeHooks({
1142
- registry: hookRegistry,
1143
- input: {
1144
- hook_event_name: 'PostToolUse',
1145
- runId,
1146
- threadId,
1147
- agentId: this.agentId,
1148
- executingAgentId: this.executingAgentId,
1149
- toolName: call.name,
1150
- toolInput: effectiveCall.args,
1151
- toolOutput: output.content,
1152
- toolUseId: call.id ?? '',
1153
- stepId,
1154
- turn,
1155
- },
1156
- sessionId: runId,
1157
- matchQuery: call.name,
1158
- }).catch(() => undefined);
1159
- // Forward additionalContexts from the PostToolUse hook into
1160
- // the per-batch sink (Codex P2 #39).
1161
- if (postResult != null &&
1162
- batchContext.additionalContextsSink != null &&
1163
- postResult.additionalContexts.length > 0) {
1164
- batchContext.additionalContextsSink.push(...postResult.additionalContexts);
1165
- }
1166
- if (postResult?.updatedOutput != null) {
1167
- const replaced = typeof postResult.updatedOutput === 'string'
1168
- ? postResult.updatedOutput
1169
- : JSON.stringify(postResult.updatedOutput);
1170
- // Keep the tool-output registry in sync with what the model
1171
- // actually sees. Without this, `runTool` already registered
1172
- // the PRE-hook content under `_refKey`, and a later
1173
- // `{{tool<i>turn<n>}}` substitution would deliver the stale
1174
- // pre-hook bytes while the model (and downstream tools)
1175
- // observed the post-hook replacement. Read `_refKey` /
1176
- // `_refScope` straight off the message metadata that
1177
- // `recordOutputReference` stamped — no need to re-derive
1178
- // (and we couldn't, for anonymous-batch synthetic scopes).
1179
- const refMeta = output.additional_kwargs;
1180
- const refKey = refMeta?._refKey;
1181
- const refScope = refMeta?._refScope;
1182
- if (this.toolOutputRegistry != null && refKey != null) {
1183
- this.toolOutputRegistry.set(refScope, refKey, replaced);
1184
- }
1185
- return new ToolMessage({
1186
- status: output.status,
1187
- name: output.name,
1188
- content: replaced,
1189
- artifact: output.artifact,
1190
- tool_call_id: output.tool_call_id,
1191
- additional_kwargs: output.additional_kwargs,
1192
- });
1193
- }
1194
- }
1195
- return output;
1196
- }
1197
- /**
1198
- * `ask` decisions on direct-path tools collapse to fail-closed deny
1199
- * only when `humanInTheLoop.enabled !== true` (i.e. there's no host
1200
- * UI configured to actually prompt the user). Logged once per process
1201
- * so the gap is visible. When HITL IS enabled, `ask` raises a real
1202
- * LangGraph `interrupt()` instead — see `runDirectToolWithLifecycleHooks`.
1203
- */
1204
- askDirectWarningEmitted = false;
1205
- resolveAskDecisionForDirectTool(reason, toolName) {
1206
- if (!this.askDirectWarningEmitted) {
1207
- this.askDirectWarningEmitted = true;
1208
- // eslint-disable-next-line no-console
1209
- console.warn(`[ToolNode] PreToolUse returned 'ask' for direct-path tool "${toolName}" but ` +
1210
- 'humanInTheLoop is not enabled — failing closed. Set humanInTheLoop.enabled=true ' +
1211
- 'to raise a tool_approval interrupt the host can resolve.');
1212
- }
1213
- return reason ?? 'Blocked by hook';
1214
- }
1215
- /**
1216
- * Synthesize a Blocked ToolMessage AND fire `PermissionDenied`
1217
- * (observational) for a direct-path tool call. Centralised so the
1218
- * deny path looks identical whether the block came from `'deny'` or
1219
- * from a fail-closed/`'reject'`/policy-violation path.
1220
- */
1221
- blockDirectCall(args) {
1222
- const { call, resolvedArgs, reason, hookRegistry, runId, threadId } = args;
1223
- if (hookRegistry.hasHookFor('PermissionDenied', runId) === true) {
1224
- executeHooks({
1225
- registry: hookRegistry,
1226
- input: {
1227
- hook_event_name: 'PermissionDenied',
1228
- runId,
1229
- threadId,
1230
- agentId: this.agentId,
1231
- executingAgentId: this.executingAgentId,
1232
- toolName: call.name,
1233
- toolInput: resolvedArgs,
1234
- toolUseId: call.id ?? '',
1235
- reason,
1236
- },
1237
- sessionId: runId,
1238
- matchQuery: call.name,
1239
- }).catch(() => {
1240
- /* observational */
1241
- });
1242
- }
1243
- return new ToolMessage({
1244
- status: 'error',
1245
- content: `Blocked: ${reason}`,
1246
- name: call.name,
1247
- tool_call_id: call.id ?? '',
1248
- });
1249
- }
1250
- /**
1251
- * Registers the full, raw output under `refKey` (when provided) and
1252
- * builds the per-message ref metadata stamped onto the resulting
1253
- * `ToolMessage.additional_kwargs`. The metadata is read at LLM-
1254
- * request time by `annotateMessagesForLLM` to produce a transient
1255
- * annotated copy of the message — the persisted `content` itself
1256
- * stays clean.
1257
- *
1258
- * @param registryContent The full, untruncated output to store in
1259
- * the registry so `{{tool<i>turn<n>}}` substitutions deliver the
1260
- * complete payload. Ignored when `refKey` is undefined.
1261
- * @param refKey Precomputed `tool<i>turn<n>` key, or undefined when
1262
- * the output is not to be registered (errors, disabled feature,
1263
- * unavailable batch/turn).
1264
- * @param unresolved Placeholder keys that did not resolve; surfaced
1265
- * to the LLM lazily so it can self-correct.
1266
- * @returns A `ToolMessageRefMetadata` object when there is anything
1267
- * to stamp, otherwise `undefined`.
1268
- */
1269
- recordOutputReference(runId, registryContent, refKey, unresolved) {
1270
- if (this.toolOutputRegistry != null && refKey != null) {
1271
- this.toolOutputRegistry.set(runId, refKey, registryContent);
1272
- }
1273
- if (refKey == null && unresolved.length === 0)
1274
- return undefined;
1275
- const meta = {};
1276
- if (refKey != null) {
1277
- meta._refKey = refKey;
1278
- /**
1279
- * Stamp the registry scope alongside the key so the lazy
1280
- * annotation transform can look up the right bucket. Anonymous
1281
- * invocations get a synthetic per-batch scope (`\0anon-<n>`)
1282
- * that `attemptInvoke` cannot derive from
1283
- * `config.configurable.run_id` — without this, anonymous-run
1284
- * refs would silently fail registry lookup and the LLM would
1285
- * never see `[ref: …]` markers for outputs that were registered.
1286
- */
1287
- if (runId != null)
1288
- meta._refScope = runId;
1289
- }
1290
- if (unresolved.length > 0)
1291
- meta._unresolvedRefs = unresolved;
1292
- return meta;
1293
- }
1294
- /**
1295
- * Builds code session context for injection into event-driven tool calls.
1296
- * Mirrors the session injection logic in runTool() for direct execution.
1297
- */
1298
- getCodeSessionContext() {
1299
- if (!this.sessions) {
1300
- return undefined;
1301
- }
1302
- const codeSession = this.sessions.get(Constants.EXECUTE_CODE);
1303
- if (!codeSession) {
1304
- return undefined;
1305
- }
1306
- const execSessionId = codeSession.session_id;
1307
- const context = {
1308
- session_id: execSessionId,
1309
- };
1310
- if (codeSession.files && codeSession.files.length > 0) {
1311
- context.files = codeSession.files.map((file) => toInjectedFileRef(file, execSessionId));
1312
- }
1313
- return context;
1314
- }
1315
- /**
1316
- * Extracts code execution session context from tool results and stores in Graph.sessions.
1317
- * Mirrors the session storage logic in handleRunToolCompletions for direct execution.
1318
- */
1319
- storeCodeSessionFromResults(results, requestMap) {
1320
- if (!this.sessions) {
1321
- return;
1322
- }
1323
- for (let i = 0; i < results.length; i++) {
1324
- const result = results[i];
1325
- if (result.status !== 'success' || result.artifact == null) {
1326
- continue;
1327
- }
1328
- const request = requestMap.get(result.toolCallId);
1329
- if (request?.name == null ||
1330
- request.name === '' ||
1331
- (!CODE_EXECUTION_TOOLS.has(request.name) &&
1332
- request.name !== Constants.SKILL_TOOL)) {
1333
- continue;
1334
- }
1335
- const artifact = result.artifact;
1336
- const execSessionId = artifact?.session_id;
1337
- if (execSessionId == null || execSessionId === '') {
1338
- continue;
1339
- }
1340
- updateCodeSession(this.sessions, execSessionId, artifact?.files);
1341
- }
1342
- }
1343
- /**
1344
- * Post-processes standard runTool outputs: dispatches ON_RUN_STEP_COMPLETED
1345
- * and stores code session context. Mirrors the completion handling in
1346
- * dispatchToolEvents for the event-driven path.
1347
- *
1348
- * By handling completions here in graph context (rather than in the
1349
- * stream consumer via ToolEndHandler), the race between the stream
1350
- * consumer and graph execution is eliminated.
1351
- *
1352
- * @param resolvedArgsByCallId Per-batch resolved-args sink populated
1353
- * by `runTool`. Threaded as a local map (instead of instance state)
1354
- * so concurrent batches cannot read each other's entries.
1355
- */
1356
- async handleRunToolCompletions(calls, outputs, config, resolvedArgsByCallId) {
1357
- for (let i = 0; i < calls.length; i++) {
1358
- const call = calls[i];
1359
- const output = outputs[i];
1360
- const turn = this.toolCallTurns.get(call.id) ?? 0;
1361
- if (isCommand(output)) {
1362
- continue;
1363
- }
1364
- const toolMessage = output;
1365
- const toolCallId = call.id ?? '';
1366
- // Skip error ToolMessages when errorHandler already dispatched ON_RUN_STEP_COMPLETED
1367
- // via handleToolCallErrorStatic. Without this check, errors would be double-dispatched.
1368
- if (toolMessage.status === 'error' && this.errorHandler != null) {
1369
- continue;
1370
- }
1371
- if (this.sessions && CODE_EXECUTION_TOOLS.has(call.name)) {
1372
- const artifact = toolMessage.artifact;
1373
- const execSessionId = artifact?.session_id;
1374
- if (execSessionId != null && execSessionId !== '') {
1375
- updateCodeSession(this.sessions, execSessionId, artifact?.files);
1376
- }
1377
- }
1378
- // Dispatch ON_RUN_STEP_COMPLETED via custom event (same path as dispatchToolEvents)
1379
- const stepId = this.toolCallStepIds?.get(toolCallId) ?? '';
1380
- if (!stepId) {
1381
- continue;
1382
- }
1383
- const contentString = typeof toolMessage.content === 'string'
1384
- ? toolMessage.content
1385
- : JSON.stringify(toolMessage.content);
1386
- /**
1387
- * Prefer the post-substitution args when a `{{…}}` placeholder
1388
- * was resolved in `runTool`. This keeps
1389
- * `ON_RUN_STEP_COMPLETED.tool_call.args` consistent with what
1390
- * the tool actually received rather than leaking the template.
1391
- */
1392
- const effectiveArgs = resolvedArgsByCallId?.get(toolCallId) ?? call.args;
1393
- const tool_call = {
1394
- args: typeof effectiveArgs === 'string'
1395
- ? effectiveArgs
1396
- : JSON.stringify(effectiveArgs ?? {}),
1397
- name: call.name,
1398
- id: toolCallId,
1399
- output: contentString,
1400
- progress: 1,
1401
- };
1402
- await safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP_COMPLETED, {
1403
- result: {
1404
- id: stepId,
1405
- index: turn,
1406
- type: 'tool_call',
1407
- tool_call,
1408
- },
1409
- }, config);
1410
- }
1411
- }
1412
- /**
1413
- * Dispatches tool calls to the host via ON_TOOL_EXECUTE event and returns raw ToolMessages.
1414
- * Core logic for event-driven execution, separated from output shaping.
1415
- *
1416
- * Hook lifecycle (when `hookRegistry` is set):
1417
- * 1. **PreToolUse** fires per call in parallel before dispatch. Denied
1418
- * calls produce error ToolMessages and fire **PermissionDenied**;
1419
- * surviving calls proceed with optional `updatedInput`.
1420
- * 2. Surviving calls are dispatched to the host via `ON_TOOL_EXECUTE`.
1421
- * 3. **PostToolUse** / **PostToolUseFailure** fire per result. Post hooks
1422
- * can replace tool output via `updatedOutput`.
1423
- * 4. Injected messages from results are collected and returned alongside
1424
- * ToolMessages (appended AFTER to respect provider ordering).
1425
- */
1426
- async dispatchToolEvents(toolCalls, config, batchContext = {}) {
1427
- const { batchIndices, turn, batchScopeId, preResolvedArgs, preBatchSnapshot, } = batchContext;
1428
- const runId = config.configurable?.run_id ?? '';
1429
- /**
1430
- * Registry-facing scope id — prefers the caller-threaded
1431
- * `batchScopeId` so anonymous batches target their own unique
1432
- * bucket and don't step on concurrent anonymous invocations.
1433
- * Hooks and event payloads keep using the empty-string coerced
1434
- * `runId` for backward compat.
1435
- */
1436
- const registryRunId = batchScopeId ?? config.configurable?.run_id;
1437
- const threadId = config.configurable?.thread_id;
1438
- const registry = this.toolOutputRegistry;
1439
- const unresolvedByCallId = new Map();
1440
- const preToolCalls = toolCalls.map((call, i) => {
1441
- const originalArgs = call.args;
1442
- let resolvedArgs = originalArgs;
1443
- /**
1444
- * When the caller provided a pre-resolved map (the mixed
1445
- * direct+event path snapshots event args synchronously before
1446
- * awaiting directs so they can't accidentally resolve
1447
- * same-turn direct outputs), use those entries verbatim instead
1448
- * of re-resolving against a registry that may have changed
1449
- * since the batch started.
1450
- */
1451
- const pre = call.id != null ? preResolvedArgs?.get(call.id) : undefined;
1452
- if (pre != null) {
1453
- resolvedArgs = pre.resolved;
1454
- if (pre.unresolved.length > 0 && call.id != null) {
1455
- unresolvedByCallId.set(call.id, pre.unresolved);
1456
- }
1457
- }
1458
- else if (registry != null) {
1459
- const { resolved, unresolved } = registry.resolve(registryRunId, originalArgs);
1460
- resolvedArgs = resolved;
1461
- if (unresolved.length > 0 && call.id != null) {
1462
- unresolvedByCallId.set(call.id, unresolved);
1463
- }
1464
- }
1465
- return {
1466
- call,
1467
- stepId: this.toolCallStepIds?.get(call.id) ?? '',
1468
- args: resolvedArgs,
1469
- batchIndex: batchIndices?.[i],
1470
- };
1471
- });
1472
- const messageByCallId = new Map();
1473
- const approvedEntries = [];
1474
- /**
1475
- * Batch-level accumulator for `additionalContext` strings returned
1476
- * by any PreToolUse / PostToolUse / PostToolUseFailure hook in this
1477
- * dispatch. We emit one consolidated `HumanMessage` after all tool
1478
- * results land so the next model turn sees the injected context
1479
- * exactly once, ordered after the ToolMessages.
1480
- */
1481
- const batchAdditionalContexts = [];
1482
- /**
1483
- * Batch-level outcome record keyed by `tool_call_id`. Captures
1484
- * every tool call's final result (success / error from the host,
1485
- * blocked from HITL deny / reject, substituted from HITL respond)
1486
- * across the three call sites that touch it. We materialize the
1487
- * `PostToolBatch` entry array in `toolCalls` order at dispatch
1488
- * time so hooks correlating outcomes by position see exactly the
1489
- * same sequence the model emitted — independent of when each
1490
- * outcome was recorded (deny entries land synchronously in the
1491
- * hook loop, approved entries land after host execution, respond
1492
- * entries land in the resume branch).
1493
- */
1494
- const postToolBatchEntryByCallId = new Map();
1495
- const HOOK_FALLBACK = Object.freeze({
1496
- additionalContexts: [],
1497
- errors: [],
1498
- });
1499
- if (this.hookRegistry?.hasHookFor('PreToolUse', runId) === true) {
1500
- /**
1501
- * Capture as a non-null local so the inner `blockEntry` closure
1502
- * doesn't lose narrowing on `this.hookRegistry` and we don't have
1503
- * to defensively `?.` it across every reference inside.
1504
- */
1505
- const hookRegistry = this.hookRegistry;
1506
- const preResults = await Promise.all(preToolCalls.map((entry) => executeHooks({
1507
- registry: hookRegistry,
1508
- input: {
1509
- hook_event_name: 'PreToolUse',
1510
- runId,
1511
- threadId,
1512
- agentId: this.agentId,
1513
- executingAgentId: this.executingAgentId,
1514
- toolName: entry.call.name,
1515
- toolInput: entry.args,
1516
- toolUseId: entry.call.id,
1517
- stepId: entry.stepId,
1518
- turn: this.toolUsageCount.get(entry.call.name) ?? 0,
1519
- },
1520
- sessionId: runId,
1521
- matchQuery: entry.call.name,
1522
- }).catch(() => HOOK_FALLBACK)));
1523
- /**
1524
- * Side effects deferred from `blockEntry` until after any pending
1525
- * `interrupt()` resolves. Without deferral, a batch that mixes a
1526
- * `deny` decision with an `ask` decision would dispatch
1527
- * `ON_RUN_STEP_COMPLETED` for the denied tool on the FIRST node
1528
- * execution (before `interrupt()` throws), then dispatch the
1529
- * same event AGAIN on the resume re-execution — hosts would
1530
- * observe two completion events for one logical denial. By
1531
- * queueing the dispatch + PermissionDenied hook here and
1532
- * flushing after the interrupt block, we ensure each side effect
1533
- * fires exactly once: never on the first pass when interrupt
1534
- * throws (the flush is unreachable), once on resume / no-ask
1535
- * passes when control reaches the flush.
1536
- */
1537
- const deferredBlockedSideEffects = [];
1538
- const blockEntry = (entry, reason) => {
1539
- const contentString = `Blocked: ${reason}`;
1540
- messageByCallId.set(entry.call.id, new ToolMessage({
1541
- status: 'error',
1542
- content: contentString,
1543
- name: entry.call.name,
1544
- tool_call_id: entry.call.id,
1545
- }));
1546
- postToolBatchEntryByCallId.set(entry.call.id, {
1547
- toolName: entry.call.name,
1548
- toolInput: entry.args,
1549
- toolUseId: entry.call.id,
1550
- stepId: entry.stepId,
1551
- /**
1552
- * Records the pre-invocation turn count — the same value the
1553
- * executed path captures before incrementing `toolUsageCount`.
1554
- * For a blocked tool the counter is never incremented (no
1555
- * invocation happened), so this is always the count of prior
1556
- * successful invocations of this tool name in earlier batches.
1557
- * Surfaces in the `PostToolBatch` entry so batch hooks see
1558
- * a uniform shape regardless of outcome.
1559
- */
1560
- turn: this.toolUsageCount.get(entry.call.name) ?? 0,
1561
- status: 'error',
1562
- error: contentString,
1563
- });
1564
- deferredBlockedSideEffects.push({
1565
- callId: entry.call.id,
1566
- toolName: entry.call.name,
1567
- args: entry.args,
1568
- contentString,
1569
- reason,
1570
- });
1571
- };
1572
- const flushDeferredBlockedSideEffects = async () => {
1573
- for (const item of deferredBlockedSideEffects) {
1574
- await this.dispatchStepCompleted(item.callId, item.toolName, item.args, item.contentString, config);
1575
- if (hookRegistry.hasHookFor('PermissionDenied', runId)) {
1576
- executeHooks({
1577
- registry: hookRegistry,
1578
- input: {
1579
- hook_event_name: 'PermissionDenied',
1580
- runId,
1581
- threadId,
1582
- agentId: this.agentId,
1583
- executingAgentId: this.executingAgentId,
1584
- toolName: item.toolName,
1585
- toolInput: item.args,
1586
- toolUseId: item.callId,
1587
- reason: item.reason,
1588
- },
1589
- sessionId: runId,
1590
- matchQuery: item.toolName,
1591
- }).catch(() => {
1592
- /* PermissionDenied is observational — swallow errors */
1593
- });
1594
- }
1595
- }
1596
- deferredBlockedSideEffects.length = 0;
1597
- };
1598
- /**
1599
- * Apply a hook-supplied or host-supplied input override to a pending
1600
- * entry, re-running the `{{tool<i>turn<n>}}` resolver so any new
1601
- * placeholders introduced by the override are substituted (and any
1602
- * formerly-unresolved refs cleared from the unresolved set).
1603
- *
1604
- * Mixed direct+event batches must use the pre-batch snapshot so a
1605
- * hook-introduced placeholder cannot accidentally resolve to a
1606
- * same-turn direct output that has just registered. Pure event
1607
- * batches don't have a snapshot and resolve against the live
1608
- * registry — safe because no event-side registrations have happened
1609
- * yet.
1610
- */
1611
- const applyInputOverride = (entry, nextArgs) => {
1612
- if (registry != null) {
1613
- const view = preBatchSnapshot ?? {
1614
- resolve: (args) => registry.resolve(registryRunId, args),
1615
- };
1616
- const { resolved, unresolved } = view.resolve(nextArgs);
1617
- entry.args = resolved;
1618
- if (entry.call.id != null) {
1619
- if (unresolved.length > 0) {
1620
- unresolvedByCallId.set(entry.call.id, unresolved);
1621
- }
1622
- else {
1623
- unresolvedByCallId.delete(entry.call.id);
1624
- }
1625
- }
1626
- return;
1627
- }
1628
- entry.args = nextArgs;
1629
- };
1630
- const askEntries = [];
1631
- for (let i = 0; i < preToolCalls.length; i++) {
1632
- const hookResult = preResults[i];
1633
- const entry = preToolCalls[i];
1634
- for (const ctx of hookResult.additionalContexts) {
1635
- batchAdditionalContexts.push(ctx);
1636
- }
1637
- if (hookResult.decision === 'deny') {
1638
- blockEntry(entry, hookResult.reason ?? 'Blocked by hook');
1639
- continue;
1640
- }
1641
- if (hookResult.decision === 'ask') {
1642
- /**
1643
- * HITL is OFF by default — hosts must explicitly opt in via
1644
- * `humanInTheLoop: { enabled: true }` to engage the
1645
- * `interrupt()` path. When opted out (or omitted), `ask`
1646
- * collapses into the pre-HITL fail-closed path: a blocked
1647
- * tool with an error `ToolMessage`. The default stays
1648
- * conservative until host UIs are ready to render
1649
- * `tool_approval` interrupts; see `HumanInTheLoopConfig`
1650
- * JSDoc for the full rationale and the migration plan.
1651
- */
1652
- if (this.humanInTheLoop?.enabled !== true) {
1653
- blockEntry(entry, hookResult.reason ?? 'Blocked by hook');
1654
- continue;
1655
- }
1656
- /**
1657
- * Apply `updatedInput` BEFORE queuing into `askEntries` —
1658
- * a hook is allowed to return both a sanitization rewrite
1659
- * and an `ask` decision (e.g. one matcher redacts secrets,
1660
- * another matcher requires approval). Without this, the
1661
- * interrupt payload would surface the original args to the
1662
- * reviewer AND the post-approve execution would run with
1663
- * the original args, silently dropping the hook's rewrite.
1664
- */
1665
- if (hookResult.updatedInput != null) {
1666
- applyInputOverride(entry, hookResult.updatedInput);
1667
- }
1668
- askEntries.push({
1669
- entry,
1670
- reason: hookResult.reason,
1671
- allowedDecisions: hookResult.allowedDecisions,
1672
- });
1673
- continue;
1674
- }
1675
- if (hookResult.updatedInput != null) {
1676
- applyInputOverride(entry, hookResult.updatedInput);
1677
- }
1678
- approvedEntries.push(entry);
1679
- }
1680
- /**
1681
- * If any entries asked for approval, raise a single LangGraph
1682
- * `interrupt()` carrying every pending request together. The host
1683
- * pauses, gathers human input, and resumes the run with one
1684
- * decision per request. On resume LangGraph re-executes this node
1685
- * from the start; `interrupt()` then returns the resume value
1686
- * instead of throwing, so the loop above re-runs and the same
1687
- * `askEntries` list is rebuilt deterministically (assuming hooks
1688
- * are pure — see `humanInTheLoop` docs).
1689
- */
1690
- if (askEntries.length > 0) {
1691
- const payload = buildToolApprovalInterruptPayload(askEntries);
1692
- /**
1693
- * `interrupt()` reads the current `RunnableConfig` from
1694
- * AsyncLocalStorage. ToolNode usually runs with tracing disabled
1695
- * (unless Langfuse explicitly enables it), so the upstream
1696
- * `runWithConfig` frame may not exist. Re-anchor here using the
1697
- * node's own `config` — Pregel hands us a config that already
1698
- * carries every checkpoint/scratchpad key `interrupt()` needs to
1699
- * suspend and resume.
1700
- */
1701
- const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(config, () => interrupt(payload));
1702
- const decisionByCallId = normalizeApprovalDecisions(askEntries.map(({ entry }) => entry.call.id), resumeValue);
1703
- for (const { entry, reason: askReason, allowedDecisions, } of askEntries) {
1704
- const decision = decisionByCallId.get(entry.call.id) ?? {
1705
- type: 'reject',
1706
- reason: 'No decision provided for tool approval',
1707
- };
1708
- /**
1709
- * Read `decision.type` through a widened view once: hosts
1710
- * deserialize resume payloads from untyped JSON, so the
1711
- * runtime value can be a typo, the wrong type, or missing
1712
- * entirely. Both the `allowedDecisions` enforcement
1713
- * immediately below and the unknown-type fallthrough at the
1714
- * end of this loop body share this single read so the
1715
- * fail-closed checks compare against the same source.
1716
- */
1717
- const declaredType = decision.type;
1718
- /**
1719
- * Enforce the per-tool `allowedDecisions` allowlist that the
1720
- * `PreToolUse` hook surfaced in `review_configs`. The host
1721
- * UI is supposed to honor this when collecting the user's
1722
- * decision, but the wire is untrusted: a buggy or hostile
1723
- * host could submit a decision type the policy explicitly
1724
- * forbids (e.g. `'edit'` when the hook restricted to
1725
- * `['approve', 'reject']`), bypassing argument-mutation /
1726
- * response-substitution safeguards. Fail closed when the
1727
- * declared type isn't in the allowlist.
1728
- */
1729
- if (allowedDecisions != null &&
1730
- (typeof declaredType !== 'string' ||
1731
- !allowedDecisions.includes(declaredType))) {
1732
- const offered = typeof declaredType === 'string' ? declaredType : '<missing>';
1733
- blockEntry(entry, `Decision "${offered}" not in allowedDecisions [${allowedDecisions.join(', ')}] — failing closed`);
1734
- continue;
1735
- }
1736
- if (decision.type === 'reject') {
1737
- blockEntry(entry, decision.reason ?? askReason ?? 'Rejected by user');
1738
- continue;
1739
- }
1740
- /**
1741
- * `respond` short-circuits tool execution: the human supplies
1742
- * the result the model should see in place of running the
1743
- * tool. We emit a successful `ToolMessage` directly and skip
1744
- * dispatch — no host event fires, no real tool side effect
1745
- * occurs. Mirrors LangChain HITL middleware semantics.
1746
- */
1747
- if (decision.type === 'respond') {
1748
- /**
1749
- * Validate the wire shape before touching it: hosts
1750
- * deserialize resume payloads from untyped JSON, so a
1751
- * malformed `{ type: 'respond' }` (no `responseText`) or
1752
- * `{ type: 'respond', responseText: 42 }` would crash
1753
- * `truncateToolResultContent` (which calls
1754
- * `content.length`) and turn a fail-closed approval path
1755
- * into a hard run failure. Route bad shapes through
1756
- * `blockEntry` like any other unusable decision.
1757
- */
1758
- const responseText = decision
1759
- .responseText;
1760
- if (typeof responseText !== 'string') {
1761
- blockEntry(entry, `Decision "respond" missing string responseText (got ${describeOfferedShape(responseText)}) — failing closed`);
1762
- continue;
1763
- }
1764
- /**
1765
- * Truncate the human-supplied text just like the success
1766
- * path does for real tool output. Without this, a user
1767
- * pasting a large document as a manual response bypasses
1768
- * `maxToolResultChars` and can blow past the model's
1769
- * context window. The PostToolBatch entry surfaces the
1770
- * truncated text too so batch hooks see what the model
1771
- * will actually see.
1772
- */
1773
- const truncatedResponse = truncateToolResultContent(responseText, this.maxToolResultChars);
1774
- messageByCallId.set(entry.call.id, new ToolMessage({
1775
- status: 'success',
1776
- content: truncatedResponse,
1777
- name: entry.call.name,
1778
- tool_call_id: entry.call.id,
1779
- }));
1780
- postToolBatchEntryByCallId.set(entry.call.id, {
1781
- toolName: entry.call.name,
1782
- toolInput: entry.args,
1783
- toolUseId: entry.call.id,
1784
- stepId: entry.stepId,
1785
- turn: this.toolUsageCount.get(entry.call.name) ?? 0,
1786
- status: 'success',
1787
- toolOutput: truncatedResponse,
1788
- });
1789
- /**
1790
- * Safe to dispatch immediately — unlike `blockEntry` which
1791
- * defers, `respond` only executes inside the decision-
1792
- * processing loop, which is reachable only AFTER
1793
- * `interrupt()` has returned (the resume pass). There is
1794
- * no risk of being rolled back by a subsequent throw, so
1795
- * no risk of a duplicate `ON_RUN_STEP_COMPLETED` event.
1796
- */
1797
- await this.dispatchStepCompleted(entry.call.id, entry.call.name, entry.args, truncatedResponse, config);
1798
- continue;
1799
- }
1800
- if (decision.type === 'edit') {
1801
- /**
1802
- * Validate the wire shape before touching it: hosts
1803
- * deserialize resume payloads from untyped JSON, so a
1804
- * malformed `{ type: 'edit' }` (no `updatedInput`),
1805
- * `{ type: 'edit', updatedInput: 'string' }` (non-object),
1806
- * or `{ type: 'edit', updatedInput: [...] }` (array, not a
1807
- * plain object) would feed garbage into
1808
- * `applyInputOverride` and silently approve a tool with
1809
- * undefined / wrong-shape args. Same trust boundary as
1810
- * the `respond` validation above — fail closed via
1811
- * `blockEntry` with a diagnostic.
1812
- */
1813
- const updatedInput = decision
1814
- .updatedInput;
1815
- if (updatedInput === null ||
1816
- typeof updatedInput !== 'object' ||
1817
- Array.isArray(updatedInput)) {
1818
- blockEntry(entry, `Decision "edit" missing object updatedInput (got ${describeOfferedShape(updatedInput)}) — failing closed`);
1819
- continue;
1820
- }
1821
- applyInputOverride(entry, updatedInput);
1822
- approvedEntries.push(entry);
1823
- continue;
1824
- }
1825
- /**
1826
- * Defensive type widening: hosts deserialize resume payloads
1827
- * from untyped JSON, so the `decision.type` value at runtime
1828
- * is whatever string the wire sent — not necessarily one of
1829
- * the four union variants TS knows about. We compare against
1830
- * the literal `'approve'` through the widened `declaredType`
1831
- * captured at the top of this iteration, so a typo or schema
1832
- * drift (`'aproved'`, `null`, `undefined`) hits the fail-
1833
- * closed branch below instead of silently approving the
1834
- * tool. Without this widening, TS narrows the union after
1835
- * the three earlier branches and treats `=== 'approve'` as
1836
- * trivially true.
1837
- */
1838
- if (declaredType === 'approve') {
1839
- approvedEntries.push(entry);
1840
- continue;
1841
- }
1842
- /**
1843
- * Unknown / missing decision type — fail closed. The whole
1844
- * point of an approval gate is that "no decision" or
1845
- * "garbled decision" deny by default.
1846
- */
1847
- const unknownType = typeof declaredType === 'string' ? declaredType : '<missing>';
1848
- blockEntry(entry, `Unknown approval decision type "${unknownType}" — failing closed`);
1849
- }
1850
- }
1851
- /**
1852
- * Flush deferred denial side effects exactly once. On the FIRST
1853
- * pass through a batch that contains an `ask`, `interrupt()`
1854
- * threw above and we never reach this line — so no
1855
- * `ON_RUN_STEP_COMPLETED` / `PermissionDenied` events fire
1856
- * for blocked tools yet. On resume the node re-executes from
1857
- * scratch, `blockEntry` re-queues the same entries, and the
1858
- * flush below dispatches them once. For batches without any
1859
- * `ask` (deny-only or empty), the flush still runs here and
1860
- * dispatches in the same relative position as the pre-deferral
1861
- * code did (after hook processing, before tool execution).
1862
- */
1863
- await flushDeferredBlockedSideEffects();
1864
- }
1865
- else {
1866
- approvedEntries.push(...preToolCalls);
1867
- }
1868
- const injected = [];
1869
- const batchIndexByCallId = new Map();
1870
- if (approvedEntries.length > 0) {
1871
- const plan = buildToolExecutionRequestPlan({
1872
- toolCalls: approvedEntries.map((entry) => {
1873
- const codeSessionContext = CODE_EXECUTION_TOOLS.has(entry.call.name) ||
1874
- entry.call.name === Constants.SKILL_TOOL ||
1875
- entry.call.name === Constants.READ_FILE
1876
- ? this.getCodeSessionContext()
1877
- : undefined;
1878
- return {
1879
- id: entry.call.id,
1880
- name: entry.call.name,
1881
- args: entry.args,
1882
- stepId: entry.stepId,
1883
- codeSessionContext,
1884
- };
1885
- }),
1886
- usageCount: this.toolUsageCount,
1887
- invalidArgsBehavior: 'error-result',
1888
- recordTurn: (toolName, reservedTurn, callId) => {
1889
- this.recordEventToolPlanningTurn(toolName, reservedTurn, callId);
1890
- },
1891
- });
1892
- if (plan == null) {
1893
- throw new Error('Unable to build event tool execution request plan');
1894
- }
1895
- const requests = plan.requests;
1896
- for (const entry of approvedEntries) {
1897
- if (entry.batchIndex != null && entry.call.id != null) {
1898
- batchIndexByCallId.set(entry.call.id, entry.batchIndex);
1899
- }
1900
- }
1901
- for (const result of plan.rejectedResults) {
1902
- this.eagerEventToolExecutions?.delete(result.toolCallId);
1903
- }
1904
- const requestMap = new Map(plan.allRequests.map((r) => [r.id, r]));
1905
- const eagerExecutions = [];
1906
- const dispatchRequests = [];
1907
- for (const request of requests) {
1908
- const eagerExecution = this.takeMatchingEagerEventExecution(request);
1909
- if (eagerExecution != null) {
1910
- eagerExecutions.push({ request, execution: eagerExecution });
1911
- }
1912
- else {
1913
- dispatchRequests.push(request);
1914
- }
1915
- }
1916
- const dispatchPromise = dispatchRequests.length === 0
1917
- ? Promise.resolve([])
1918
- : new Promise((resolve, reject) => {
1919
- let dispatchSettled = false;
1920
- let resultSettled = false;
1921
- let settledResults;
1922
- const maybeResolve = () => {
1923
- if (dispatchSettled && resultSettled) {
1924
- resolve(settledResults ?? []);
1925
- }
1926
- };
1927
- const batchRequest = {
1928
- toolCalls: dispatchRequests,
1929
- userId: config.configurable?.user_id,
1930
- agentId: this.agentId,
1931
- configurable: config.configurable,
1932
- metadata: config.metadata,
1933
- resolve: (results) => {
1934
- resultSettled = true;
1935
- settledResults = results;
1936
- maybeResolve();
1937
- },
1938
- reject,
1939
- };
1940
- void safeDispatchCustomEvent(GraphEvents.ON_TOOL_EXECUTE, batchRequest, config)
1941
- .then(() => {
1942
- dispatchSettled = true;
1943
- maybeResolve();
1944
- })
1945
- .catch(reject);
1946
- });
1947
- const eagerResultsPromise = Promise.all(eagerExecutions.map(async ({ request, execution }) => {
1948
- const results = await this.resolveEagerEventExecution(request, execution);
1949
- return {
1950
- results,
1951
- completionDispatched: execution.completionDispatched === true &&
1952
- execution.request.turn === request.turn,
1953
- toolCallId: request.id,
1954
- };
1955
- }));
1956
- const [eagerResults, dispatchedResults] = await Promise.all([
1957
- eagerResultsPromise,
1958
- dispatchPromise,
1959
- ]);
1960
- const eagerCompletionDispatchedIds = new Set(eagerResults
1961
- .filter((result) => result.completionDispatched)
1962
- .map((result) => result.toolCallId));
1963
- const flattenedEagerResults = eagerResults.flatMap((result) => result.results);
1964
- const results = [
1965
- ...plan.rejectedResults,
1966
- ...flattenedEagerResults,
1967
- ...dispatchedResults,
1968
- ];
1969
- this.storeCodeSessionFromResults(results, requestMap);
1970
- const hasPostHook = this.hookRegistry?.hasHookFor('PostToolUse', runId) === true;
1971
- const hasFailureHook = this.hookRegistry?.hasHookFor('PostToolUseFailure', runId) === true;
1972
- for (const result of results) {
1973
- if (result.injectedMessages && result.injectedMessages.length > 0) {
1974
- try {
1975
- injected.push(...this.convertInjectedMessages(result.injectedMessages));
1976
- }
1977
- catch (e) {
1978
- // eslint-disable-next-line no-console
1979
- console.warn(`[ToolNode] Failed to convert injectedMessages for toolCallId=${result.toolCallId}:`, e instanceof Error ? e.message : e);
1980
- }
1981
- }
1982
- const request = requestMap.get(result.toolCallId);
1983
- const toolName = request?.name ?? 'unknown';
1984
- let contentString;
1985
- let toolMessage;
1986
- /**
1987
- * Tracks the post-PostToolUse-hook output so the
1988
- * `PostToolBatch` entry below sees the final transformed value
1989
- * even when a hook replaced the original via `updatedOutput`.
1990
- * Lives at the loop-iteration scope so the success branch can
1991
- * mutate it; the error branch leaves it unset (and the batch
1992
- * entry uses `error` instead of `toolOutput` in that case).
1993
- */
1994
- let finalToolOutput = result.content;
1995
- if (result.status === 'error') {
1996
- contentString = `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`;
1997
- /**
1998
- * Error results bypass registration but stamp the
1999
- * unresolved-refs hint into `additional_kwargs` so the lazy
2000
- * annotation transform surfaces it to the LLM at request
2001
- * time, letting the model self-correct when its reference
2002
- * key caused the failure. Persisted `content` stays clean.
2003
- */
2004
- const unresolved = unresolvedByCallId.get(result.toolCallId) ?? [];
2005
- const errorRefMeta = unresolved.length > 0
2006
- ? this.recordOutputReference(registryRunId, contentString, undefined, unresolved)
2007
- : undefined;
2008
- toolMessage = new ToolMessage({
2009
- status: 'error',
2010
- content: contentString,
2011
- name: toolName,
2012
- tool_call_id: result.toolCallId,
2013
- ...(errorRefMeta != null && {
2014
- additional_kwargs: errorRefMeta,
2015
- }),
2016
- });
2017
- if (hasFailureHook) {
2018
- const failureHookResult = await executeHooks({
2019
- registry: this.hookRegistry,
2020
- input: {
2021
- hook_event_name: 'PostToolUseFailure',
2022
- runId,
2023
- threadId,
2024
- agentId: this.agentId,
2025
- executingAgentId: this.executingAgentId,
2026
- toolName,
2027
- toolInput: request?.args ?? {},
2028
- toolUseId: result.toolCallId,
2029
- error: result.errorMessage ?? 'Unknown error',
2030
- stepId: request?.stepId,
2031
- turn: request?.turn,
2032
- },
2033
- sessionId: runId,
2034
- matchQuery: toolName,
2035
- }).catch(() => undefined);
2036
- /**
2037
- * Collect `additionalContext` from failure hooks too. Without
2038
- * this, recovery guidance returned on tool errors (e.g.
2039
- * "if this tool errors with X, suggest Y to the user") is
2040
- * silently dropped even though the API surface advertises
2041
- * `additionalContext` for this event. PostToolUseFailure
2042
- * remains observational for errors thrown by the hook
2043
- * itself, but a successfully-returned result is honored.
2044
- */
2045
- if (failureHookResult != null) {
2046
- for (const ctx of failureHookResult.additionalContexts) {
2047
- batchAdditionalContexts.push(ctx);
2048
- }
2049
- }
2050
- }
2051
- }
2052
- else {
2053
- let registryRaw = typeof result.content === 'string'
2054
- ? result.content
2055
- : JSON.stringify(result.content);
2056
- contentString = truncateToolResultContent(registryRaw, this.maxToolResultChars);
2057
- if (hasPostHook) {
2058
- const hookResult = await executeHooks({
2059
- registry: this.hookRegistry,
2060
- input: {
2061
- hook_event_name: 'PostToolUse',
2062
- runId,
2063
- threadId,
2064
- agentId: this.agentId,
2065
- executingAgentId: this.executingAgentId,
2066
- toolName,
2067
- toolInput: request?.args ?? {},
2068
- toolOutput: result.content,
2069
- toolUseId: result.toolCallId,
2070
- stepId: request?.stepId,
2071
- turn: request?.turn,
2072
- },
2073
- sessionId: runId,
2074
- matchQuery: toolName,
2075
- }).catch(() => undefined);
2076
- if (hookResult != null) {
2077
- for (const ctx of hookResult.additionalContexts) {
2078
- batchAdditionalContexts.push(ctx);
2079
- }
2080
- }
2081
- if (hookResult?.updatedOutput != null) {
2082
- const replaced = typeof hookResult.updatedOutput === 'string'
2083
- ? hookResult.updatedOutput
2084
- : JSON.stringify(hookResult.updatedOutput);
2085
- registryRaw = replaced;
2086
- contentString = truncateToolResultContent(replaced, this.maxToolResultChars);
2087
- finalToolOutput = hookResult.updatedOutput;
2088
- }
2089
- }
2090
- const batchIndex = batchIndexByCallId.get(result.toolCallId);
2091
- const unresolved = unresolvedByCallId.get(result.toolCallId) ?? [];
2092
- const refKey = this.toolOutputRegistry != null &&
2093
- batchIndex != null &&
2094
- turn != null
2095
- ? buildReferenceKey(batchIndex, turn)
2096
- : undefined;
2097
- const successRefMeta = this.recordOutputReference(registryRunId, stripCodeSessionFileSummary(registryRaw), refKey, unresolved);
2098
- toolMessage = new ToolMessage({
2099
- status: 'success',
2100
- name: toolName,
2101
- content: contentString,
2102
- artifact: result.artifact,
2103
- tool_call_id: result.toolCallId,
2104
- ...(successRefMeta != null && {
2105
- additional_kwargs: successRefMeta,
2106
- }),
2107
- });
2108
- }
2109
- if (!eagerCompletionDispatchedIds.has(result.toolCallId)) {
2110
- await this.dispatchStepCompleted(result.toolCallId, toolName, request?.args ?? {}, contentString, config, request?.turn);
2111
- }
2112
- postToolBatchEntryByCallId.set(result.toolCallId, {
2113
- toolName,
2114
- toolInput: request?.args ?? {},
2115
- toolUseId: result.toolCallId,
2116
- stepId: request?.stepId,
2117
- turn: request?.turn,
2118
- status: result.status === 'error' ? 'error' : 'success',
2119
- ...(result.status === 'error'
2120
- ? { error: result.errorMessage ?? 'Unknown error' }
2121
- : { toolOutput: finalToolOutput }),
2122
- });
2123
- messageByCallId.set(result.toolCallId, toolMessage);
2124
- }
2125
- }
2126
- const toolMessages = toolCalls
2127
- .map((call) => messageByCallId.get(call.id))
2128
- .filter((m) => m != null);
2129
- await this.dispatchPostToolBatchAndInjectContext({
2130
- toolCalls,
2131
- entriesByCallId: postToolBatchEntryByCallId,
2132
- batchAdditionalContexts,
2133
- injected,
2134
- runId,
2135
- threadId,
2136
- });
2137
- return { toolMessages, injected };
2138
- }
2139
- canConsumeEagerEventExecution() {
2140
- return (this.eventDrivenMode &&
2141
- this.eagerEventToolExecution?.enabled === true &&
2142
- this.hookRegistry == null &&
2143
- this.humanInTheLoop?.enabled !== true);
2144
- }
2145
- takeMatchingEagerEventExecution(request) {
2146
- if (!this.canConsumeEagerEventExecution()) {
2147
- return undefined;
2148
- }
2149
- const execution = this.eagerEventToolExecutions?.get(request.id);
2150
- if (execution == null) {
2151
- return undefined;
2152
- }
2153
- this.eagerEventToolExecutions?.delete(request.id);
2154
- // Only tool identity + canonical args define side-effect identity here.
2155
- // `request.turn` is final-planning metadata; if it drifts between the
2156
- // streamed eager reservation and model-end materialization, consume the
2157
- // same-name/same-args eager result and let the final request drive refs,
2158
- // completion metadata, and PostToolBatch state.
2159
- if (execution.toolName !== request.name ||
2160
- !recordArgsEqual(execution.args, request.args)) {
2161
- return {
2162
- toolCallId: request.id,
2163
- toolName: request.name,
2164
- args: request.args,
2165
- request,
2166
- promise: Promise.resolve({
2167
- results: [
2168
- {
2169
- toolCallId: request.id,
2170
- status: 'error',
2171
- content: '',
2172
- errorMessage: 'Tool call changed after eager execution started; refusing to re-run the tool to avoid duplicate side effects.',
2173
- },
2174
- ],
2175
- }),
2176
- };
2177
- }
2178
- return execution;
2179
- }
2180
- async resolveEagerEventExecution(request, execution) {
2181
- const outcome = await execution.promise;
2182
- if (outcome.error != null) {
2183
- throw outcome.error;
2184
- }
2185
- const results = outcome.results.filter((result) => result.toolCallId === request.id);
2186
- if (results.length > 0) {
2187
- return results;
2188
- }
2189
- return [
2190
- {
2191
- toolCallId: request.id,
2192
- status: 'error',
2193
- content: '',
2194
- errorMessage: 'Tool execution completed without a result for this tool call',
2195
- },
2196
- ];
2197
- }
2198
- /**
2199
- * Fires the `PostToolBatch` hook (if registered) and appends the
2200
- * accumulated batch-level `additionalContext` strings to `injected`
2201
- * as a single `HumanMessage`. Entries are materialized in the
2202
- * original `toolCalls` order so hooks correlating outcomes by
2203
- * position (as the type docs promise) see exactly the sequence
2204
- * the model emitted, regardless of when each individual outcome
2205
- * was recorded into the map (deny synchronous, approved
2206
- * post-execution, respond on resume).
2207
- *
2208
- * The PostToolBatch hook's `additionalContexts` flow into the same
2209
- * batch accumulator per-tool hooks already use, so a single
2210
- * batch-level convention message can be injected through one path.
2211
- *
2212
- * Mutates `batchAdditionalContexts` (push from batch hook) and
2213
- * `injected` (push the consolidated HumanMessage). The caller owns
2214
- * those arrays and consumes them right after this returns.
2215
- */
2216
- async dispatchPostToolBatchAndInjectContext(args) {
2217
- const { toolCalls, entriesByCallId, batchAdditionalContexts, injected, runId, threadId, } = args;
2218
- const orderedBatchEntries = [];
2219
- for (const call of toolCalls) {
2220
- const callId = call.id;
2221
- if (callId == null) {
2222
- continue;
2223
- }
2224
- const entry = entriesByCallId.get(callId);
2225
- if (entry != null) {
2226
- orderedBatchEntries.push(entry);
2227
- }
2228
- }
2229
- if (this.hookRegistry?.hasHookFor('PostToolBatch', runId) === true &&
2230
- orderedBatchEntries.length > 0) {
2231
- const batchHookResult = await executeHooks({
2232
- registry: this.hookRegistry,
2233
- input: {
2234
- hook_event_name: 'PostToolBatch',
2235
- runId,
2236
- threadId,
2237
- agentId: this.agentId,
2238
- executingAgentId: this.executingAgentId,
2239
- entries: orderedBatchEntries,
2240
- },
2241
- sessionId: runId,
2242
- }).catch(() => undefined);
2243
- if (batchHookResult != null) {
2244
- for (const ctx of batchHookResult.additionalContexts) {
2245
- batchAdditionalContexts.push(ctx);
2246
- }
2247
- }
2248
- }
2249
- if (batchAdditionalContexts.length > 0) {
2250
- /**
2251
- * `HumanMessage` carrying a metadata `role: 'system'` marker —
2252
- * see `convertInjectedMessages` for the wider rationale. Anthropic
2253
- * and Google reject mid-conversation `SystemMessage`s, so we use
2254
- * a user-role message and surface the system intent through
2255
- * `additional_kwargs` for hosts inspecting state. The model sees
2256
- * a user message; `role` is metadata only.
2257
- */
2258
- injected.push(new HumanMessage({
2259
- content: batchAdditionalContexts.join('\n\n'),
2260
- additional_kwargs: { role: 'system', source: 'hook' },
2261
- }));
2262
- }
2263
- }
2264
- async dispatchStepCompleted(toolCallId, toolName, args, output, config, turn) {
2265
- const stepId = this.toolCallStepIds?.get(toolCallId) ?? '';
2266
- if (!stepId) {
2267
- // eslint-disable-next-line no-console
2268
- console.warn(`[ToolNode] toolCallStepIds missing entry for toolCallId=${toolCallId} (tool=${toolName}). ` +
2269
- 'This indicates a race between the stream consumer and graph execution. ' +
2270
- `Map size: ${this.toolCallStepIds?.size ?? 0}`);
2271
- }
2272
- await safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP_COMPLETED, {
2273
- result: {
2274
- id: stepId,
2275
- index: turn ?? this.toolUsageCount.get(toolName) ?? 0,
2276
- type: 'tool_call',
2277
- tool_call: {
2278
- args: JSON.stringify(args),
2279
- name: toolName,
2280
- id: toolCallId,
2281
- output,
2282
- progress: 1,
2283
- },
2284
- },
2285
- }, config);
2286
- }
2287
- /**
2288
- * Converts InjectedMessage instances to LangChain HumanMessage objects.
2289
- * Both 'user' and 'system' roles become HumanMessage to avoid provider
2290
- * rejections (Anthropic/Google reject non-leading SystemMessages).
2291
- * The original role is preserved in additional_kwargs for downstream consumers.
2292
- */
2293
- convertInjectedMessages(messages) {
2294
- const converted = [];
2295
- for (const msg of messages) {
2296
- const additional_kwargs = {
2297
- role: msg.role,
2298
- };
2299
- if (msg.isMeta != null)
2300
- additional_kwargs.isMeta = msg.isMeta;
2301
- if (msg.source != null)
2302
- additional_kwargs.source = msg.source;
2303
- if (msg.skillName != null)
2304
- additional_kwargs.skillName = msg.skillName;
2305
- converted.push(new HumanMessage({
2306
- content: toLangChainContent(msg.content),
2307
- additional_kwargs,
2308
- }));
2309
- }
2310
- return converted;
2311
- }
2312
- /**
2313
- * Execute all tool calls via ON_TOOL_EXECUTE event dispatch.
2314
- * Injected messages are placed AFTER ToolMessages to respect provider
2315
- * message ordering (AIMessage tool_calls must be immediately followed
2316
- * by their ToolMessage results).
2317
- *
2318
- * `batchIndices` mirrors `toolCalls` and carries each call's position
2319
- * within the parent batch. `turn` is the per-`run()` batch index
2320
- * captured locally by the caller. Both are threaded so concurrent
2321
- * invocations cannot race on shared mutable state.
2322
- */
2323
- async executeViaEvent(toolCalls, config,
2324
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2325
- input, batchContext = {}) {
2326
- const { toolMessages, injected } = await this.dispatchToolEvents(toolCalls, config, batchContext);
2327
- const outputs = [...toolMessages, ...injected];
2328
- return (Array.isArray(input) ? outputs : { messages: outputs });
2329
- }
2330
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2331
- async run(input, config) {
2332
- this.toolCallTurns.clear();
2333
- /**
2334
- * Per-batch local map for resolved (post-substitution) args.
2335
- * Lives on the stack so concurrent `run()` calls on the same
2336
- * ToolNode cannot read or wipe each other's entries.
2337
- */
2338
- const resolvedArgsByCallId = new Map();
2339
- /**
2340
- * Claim this batch's turn synchronously from the registry (or
2341
- * fall back to 0 when the feature is disabled). The registry is
2342
- * partitioned by scope id so overlapping batches cannot
2343
- * overwrite each other's state even under a shared registry.
2344
- *
2345
- * For anonymous callers (no `run_id` in config), mint a unique
2346
- * per-batch scope id so two concurrent anonymous invocations
2347
- * don't target the same bucket. The scope is threaded down to
2348
- * every subsequent registry call on this batch.
2349
- */
2350
- const incomingRunId = config.configurable?.run_id;
2351
- const batchScopeId = incomingRunId ?? `\0anon-${this.anonBatchCounter++}`;
2352
- const turn = this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
2353
- let outputs;
2354
- if (this.isSendInput(input)) {
2355
- const isLocalTool = this.directToolNames?.has(input.lg_tool_call.name) === true ||
2356
- this.shouldHandleUnknownHandoffLocally(input.lg_tool_call.name);
2357
- if (this.eventDrivenMode && !isLocalTool) {
2358
- return this.executeViaEvent([input.lg_tool_call], config, input, {
2359
- batchIndices: [0],
2360
- turn,
2361
- batchScopeId,
2362
- });
2363
- }
2364
- // Same per-batch sink the message-state branches use so
2365
- // direct-path PreToolUse/PostToolUse/Failure additionalContexts
2366
- // surface here too. Codex P2 [44] — round 14 added the sink to
2367
- // both message-state branches but missed this Send-input
2368
- // branch, so direct tools dispatched via Send (a supported
2369
- // input shape) still silently dropped hook context.
2370
- const directAdditionalContexts = [];
2371
- const sendOutput = await this.runDirectToolWithLifecycleHooks(input.lg_tool_call, config, {
2372
- batchIndex: 0,
2373
- turn,
2374
- batchScopeId,
2375
- resolvedArgsByCallId,
2376
- additionalContextsSink: directAdditionalContexts,
2377
- });
2378
- outputs =
2379
- directAdditionalContexts.length > 0
2380
- ? [
2381
- sendOutput,
2382
- new HumanMessage({
2383
- content: directAdditionalContexts.join('\n\n'),
2384
- // Match the event-driven path's marker so hosts /
2385
- // model-side annotators treat this as system intent
2386
- // rather than ordinary user text. Codex P2 [46].
2387
- additional_kwargs: { role: 'system', source: 'hook' },
2388
- }),
2389
- ]
2390
- : [sendOutput];
2391
- await this.handleRunToolCompletions([input.lg_tool_call],
2392
- // Pass only the tool output to completion handling; the
2393
- // HumanMessage isn't a tool result.
2394
- [sendOutput], config, resolvedArgsByCallId);
2395
- }
2396
- else {
2397
- let messages;
2398
- if (Array.isArray(input)) {
2399
- messages = input;
2400
- }
2401
- else if (this.isMessagesState(input)) {
2402
- messages = input.messages;
2403
- }
2404
- else {
2405
- throw new Error('ToolNode only accepts BaseMessage[] or { messages: BaseMessage[] } as input.');
2406
- }
2407
- const toolMessageIds = new Set(messages
2408
- .filter((msg) => msg._getType() === 'tool')
2409
- .map((msg) => msg.tool_call_id));
2410
- let aiMessage;
2411
- for (let i = messages.length - 1; i >= 0; i--) {
2412
- const message = messages[i];
2413
- if (isAIMessage(message)) {
2414
- aiMessage = message;
2415
- break;
2416
- }
2417
- }
2418
- if (aiMessage == null || !isAIMessage(aiMessage)) {
2419
- throw new Error('ToolNode only accepts AIMessages as input.');
2420
- }
2421
- if (this.loadRuntimeTools) {
2422
- const { tools, toolMap } = this.loadRuntimeTools(aiMessage.tool_calls ?? []);
2423
- this.toolMap =
2424
- toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
2425
- this.applyToolExecutionOverrides();
2426
- this.programmaticCache = undefined; // Invalidate cache on toolMap change
2427
- }
2428
- const filteredCalls = aiMessage.tool_calls?.filter((call) => {
2429
- /**
2430
- * Filter out:
2431
- * 1. Already processed tool calls (present in toolMessageIds)
2432
- * 2. Server tool calls (e.g., web_search with IDs starting with 'srvtoolu_')
2433
- * which are executed by the provider's API and don't require invocation
2434
- */
2435
- return ((call.id == null || !toolMessageIds.has(call.id)) &&
2436
- !(call.id?.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX) ??
2437
- false));
2438
- }) ?? [];
2439
- if (this.eventDrivenMode && filteredCalls.length > 0) {
2440
- const directToolNames = this.directToolNames;
2441
- const hasRegisteredHandoffTool = this.hasRegisteredHandoffTool();
2442
- const directEntries = [];
2443
- const eventEntries = [];
2444
- for (let i = 0; i < filteredCalls.length; i++) {
2445
- const call = filteredCalls[i];
2446
- const entry = { call, batchIndex: i };
2447
- if (directToolNames?.has(call.name) === true ||
2448
- this.shouldHandleUnknownHandoffLocally(call.name, hasRegisteredHandoffTool)) {
2449
- directEntries.push(entry);
2450
- }
2451
- else {
2452
- eventEntries.push(entry);
2453
- }
2454
- }
2455
- if (directEntries.length === 0) {
2456
- return this.executeViaEvent(filteredCalls, config, input, {
2457
- batchIndices: eventEntries.map((entry) => entry.batchIndex),
2458
- turn,
2459
- batchScopeId,
2460
- });
2461
- }
2462
- const directCalls = directEntries.map((e) => e.call);
2463
- const directIndices = directEntries.map((e) => e.batchIndex);
2464
- const eventCalls = eventEntries.map((e) => e.call);
2465
- const eventIndices = eventEntries.map((e) => e.batchIndex);
2466
- /**
2467
- * Snapshot the event calls' args against the *pre-batch*
2468
- * registry state synchronously, before any await runs. The
2469
- * directs are then awaited first (preserving fail-fast
2470
- * semantics — a thrown error in a direct tool, e.g. with
2471
- * `handleToolErrors=false` or a `GraphInterrupt`, aborts
2472
- * before we dispatch any event-driven tools to the host).
2473
- * Because the event args were captured pre-await, they stay
2474
- * isolated from same-turn direct outputs that register
2475
- * during the await.
2476
- */
2477
- const preResolvedEventArgs = new Map();
2478
- /**
2479
- * Take a frozen snapshot of the registry state before any
2480
- * direct registrations land. The snapshot resolves
2481
- * placeholders against this point-in-time view, so a
2482
- * `PreToolUse` hook later rewriting event args via
2483
- * `updatedInput` can introduce placeholders that resolve
2484
- * cross-batch (against prior runs) without ever picking up
2485
- * same-turn direct outputs.
2486
- */
2487
- const preBatchSnapshot = this.toolOutputRegistry?.snapshot(batchScopeId);
2488
- if (preBatchSnapshot != null) {
2489
- for (const entry of eventEntries) {
2490
- if (entry.call.id != null) {
2491
- const { resolved, unresolved } = preBatchSnapshot.resolve(entry.call.args);
2492
- preResolvedEventArgs.set(entry.call.id, {
2493
- resolved: resolved,
2494
- unresolved,
2495
- });
2496
- }
2497
- }
2498
- }
2499
- // Per-batch sink for direct-path hook additionalContexts
2500
- // (Codex P2 #39). Materialized as a HumanMessage at end-of-
2501
- // batch so the next model turn sees the injected context,
2502
- // matching the event path's `injected[]` shape.
2503
- const directAdditionalContexts = [];
2504
- const directOutputs = directCalls.length > 0
2505
- ? await Promise.all(directCalls.map((call, i) => this.runDirectToolWithLifecycleHooks(call, config, {
2506
- batchIndex: directIndices[i],
2507
- turn,
2508
- batchScopeId,
2509
- resolvedArgsByCallId,
2510
- preBatchSnapshot,
2511
- additionalContextsSink: directAdditionalContexts,
2512
- })))
2513
- : [];
2514
- if (directCalls.length > 0 && directOutputs.length > 0) {
2515
- await this.handleRunToolCompletions(directCalls, directOutputs, config, resolvedArgsByCallId);
2516
- }
2517
- const eventResult = eventCalls.length > 0
2518
- ? await this.dispatchToolEvents(eventCalls, config, {
2519
- batchIndices: eventIndices,
2520
- turn,
2521
- batchScopeId,
2522
- preResolvedArgs: preResolvedEventArgs,
2523
- preBatchSnapshot,
2524
- })
2525
- : {
2526
- toolMessages: [],
2527
- injected: [],
2528
- };
2529
- const directInjected = directAdditionalContexts.length > 0
2530
- ? [
2531
- new HumanMessage({
2532
- content: directAdditionalContexts.join('\n\n'),
2533
- // System-role metadata to match the event-driven
2534
- // path so policy/recovery guidance is treated
2535
- // consistently regardless of whether the tool ran
2536
- // direct or dispatched. Codex P2 [46].
2537
- additional_kwargs: { role: 'system', source: 'hook' },
2538
- }),
2539
- ]
2540
- : [];
2541
- outputs = [
2542
- ...directOutputs,
2543
- ...eventResult.toolMessages,
2544
- ...directInjected,
2545
- ...eventResult.injected,
2546
- ];
2547
- }
2548
- else {
2549
- // Same per-batch pre-snapshot as the mixed path, applied to
2550
- // the all-direct case so `Promise.all`-induced ordering can't
2551
- // leak a sibling's just-registered output into a sister
2552
- // call's args mid-await (Codex P1 #18).
2553
- const preBatchSnapshot = this.toolOutputRegistry?.snapshot(batchScopeId);
2554
- const directAdditionalContexts = [];
2555
- const toolOutputs = await Promise.all(filteredCalls.map((call, i) => this.runDirectToolWithLifecycleHooks(call, config, {
2556
- batchIndex: i,
2557
- turn,
2558
- batchScopeId,
2559
- resolvedArgsByCallId,
2560
- preBatchSnapshot,
2561
- additionalContextsSink: directAdditionalContexts,
2562
- })));
2563
- await this.handleRunToolCompletions(filteredCalls, toolOutputs, config, resolvedArgsByCallId);
2564
- // Append accumulated additionalContexts as a single
2565
- // HumanMessage so the next model turn sees them. Codex P2 #39.
2566
- outputs =
2567
- directAdditionalContexts.length > 0
2568
- ? [
2569
- ...toolOutputs,
2570
- new HumanMessage({
2571
- content: directAdditionalContexts.join('\n\n'),
2572
- // Same system-role marker the event-driven path
2573
- // uses so direct vs dispatched is invisible to
2574
- // downstream consumers. Codex P2 [46].
2575
- additional_kwargs: { role: 'system', source: 'hook' },
2576
- }),
2577
- ]
2578
- : toolOutputs;
2579
- }
2580
- }
2581
- if (!outputs.some(isCommand)) {
2582
- return (Array.isArray(input) ? outputs : { messages: outputs });
2583
- }
2584
- const combinedOutputs = [];
2585
- let parentCommand = null;
2586
- /**
2587
- * Collect handoff commands (Commands with string goto and Command.PARENT)
2588
- * for potential parallel handoff aggregation
2589
- */
2590
- const handoffCommands = [];
2591
- for (const output of outputs) {
2592
- if (isCommand(output)) {
2593
- if (output.graph === Command.PARENT &&
2594
- Array.isArray(output.goto) &&
2595
- output.goto.every((send) => isSend(send))) {
2596
- /** Aggregate Send-based commands */
2597
- if (parentCommand) {
2598
- parentCommand.goto.push(...output.goto);
2599
- }
2600
- else {
2601
- parentCommand = new Command({
2602
- graph: Command.PARENT,
2603
- goto: output.goto,
2604
- });
2605
- }
2606
- }
2607
- else if (output.graph === Command.PARENT) {
2608
- /**
2609
- * Handoff Command with destination.
2610
- * Handle both string ('agent') and array (['agent']) formats.
2611
- * Collect for potential parallel aggregation.
2612
- */
2613
- const goto = output.goto;
2614
- const isSingleStringDest = typeof goto === 'string';
2615
- const isSingleArrayDest = Array.isArray(goto) &&
2616
- goto.length === 1 &&
2617
- typeof goto[0] === 'string';
2618
- if (isSingleStringDest || isSingleArrayDest) {
2619
- handoffCommands.push(output);
2620
- }
2621
- else {
2622
- /** Multi-destination or other command - pass through */
2623
- combinedOutputs.push(output);
2624
- }
2625
- }
2626
- else {
2627
- /** Other commands - pass through */
2628
- combinedOutputs.push(output);
2629
- }
2630
- }
2631
- else {
2632
- combinedOutputs.push(Array.isArray(input) ? [output] : { messages: [output] });
2633
- }
2634
- }
2635
- /**
2636
- * Handle handoff commands - convert to Send objects for parallel execution
2637
- * when multiple handoffs are requested
2638
- */
2639
- if (handoffCommands.length > 1) {
2640
- /**
2641
- * Multiple parallel handoffs - convert to Send objects.
2642
- * Each Send carries its own state with the appropriate messages.
2643
- * This enables LLM-initiated parallel execution when calling multiple
2644
- * transfer tools simultaneously.
2645
- */
2646
- /** Collect all destinations for sibling tracking */
2647
- const allDestinations = handoffCommands.map((cmd) => {
2648
- const goto = cmd.goto;
2649
- return typeof goto === 'string' ? goto : goto[0];
2650
- });
2651
- const sends = handoffCommands.map((cmd, idx) => {
2652
- const destination = allDestinations[idx];
2653
- /** Get siblings (other destinations, not this one) */
2654
- const siblings = allDestinations.filter((d) => d !== destination);
2655
- /** Add siblings to ToolMessage additional_kwargs */
2656
- const update = cmd.update;
2657
- if (update && update.messages) {
2658
- for (const msg of update.messages) {
2659
- if (msg.getType() === 'tool') {
2660
- msg.additional_kwargs.handoff_parallel_siblings =
2661
- siblings;
2662
- }
2663
- }
2664
- }
2665
- return new Send(destination, cmd.update);
2666
- });
2667
- const parallelCommand = new Command({
2668
- graph: Command.PARENT,
2669
- goto: sends,
2670
- });
2671
- combinedOutputs.push(parallelCommand);
2672
- }
2673
- else if (handoffCommands.length === 1) {
2674
- /** Single handoff - pass through as-is */
2675
- combinedOutputs.push(handoffCommands[0]);
2676
- }
2677
- if (parentCommand) {
2678
- combinedOutputs.push(parentCommand);
2679
- }
2680
- return combinedOutputs;
2681
- }
2682
- isSendInput(input) {
2683
- return (typeof input === 'object' && input != null && 'lg_tool_call' in input);
2684
- }
2685
- isMessagesState(input) {
2686
- return (typeof input === 'object' &&
2687
- input != null &&
2688
- 'messages' in input &&
2689
- Array.isArray(input.messages) &&
2690
- input.messages.every(isBaseMessage));
2691
- }
164
+ const newFiles = files ?? [];
165
+ const existingFiles = sessions.get("execute_code")?.files ?? [];
166
+ if (newFiles.length === 0) {
167
+ sessions.set("execute_code", {
168
+ session_id: execSessionId,
169
+ files: existingFiles,
170
+ lastUpdated: Date.now()
171
+ });
172
+ return;
173
+ }
174
+ const filesWithSession = [];
175
+ const newFileNames = /* @__PURE__ */ new Set();
176
+ const incomingByIdentity = /* @__PURE__ */ new Map();
177
+ for (const file of newFiles) {
178
+ const withSession = {
179
+ ...file,
180
+ storage_session_id: file.storage_session_id ?? execSessionId
181
+ };
182
+ incomingByIdentity.set(fileIdentityKey(withSession), filesWithSession.length);
183
+ newFileNames.add(withSession.name);
184
+ filesWithSession.push(withSession);
185
+ }
186
+ const filteredExisting = [];
187
+ for (const e of existingFiles) {
188
+ const idx = incomingByIdentity.get(fileIdentityKey(e));
189
+ if (idx !== void 0) filesWithSession[idx] = {
190
+ ...e,
191
+ ...filesWithSession[idx]
192
+ };
193
+ if (!newFileNames.has(e.name)) filteredExisting.push(e);
194
+ }
195
+ sessions.set("execute_code", {
196
+ session_id: execSessionId,
197
+ files: [...filteredExisting, ...filesWithSession],
198
+ lastUpdated: Date.now()
199
+ });
2692
200
  }
201
+ var ToolNode = class extends RunnableCallable {
202
+ toolMap;
203
+ loadRuntimeTools;
204
+ handleToolErrors = true;
205
+ trace = false;
206
+ runLangfuse;
207
+ agentLangfuse;
208
+ toolCallStepIds;
209
+ errorHandler;
210
+ toolUsageCount;
211
+ /** Maps toolCallId → turn captured in runTool, used by handleRunToolCompletions */
212
+ toolCallTurns = /* @__PURE__ */ new Map();
213
+ /**
214
+ * `call.id → turn` map dedicated to the direct-path lifecycle so the
215
+ * turn assigned on first entry is REUSED on LangGraph resume.
216
+ * Distinct from `toolCallTurns` (which is cleared at the start of
217
+ * every `run()` to keep per-batch event-dispatch metadata fresh) —
218
+ * the direct path needs stability across re-entries triggered by
219
+ * `interrupt()` resumes (Codex P2 #30). Cleared with the rest of
220
+ * the per-Run state in `clearHeavyState`-equivalent flushes when
221
+ * the Run ends.
222
+ */
223
+ directPathTurns = /* @__PURE__ */ new Map();
224
+ /** Tool registry for filtering (lazy computation of programmatic maps) */
225
+ toolRegistry;
226
+ /** Cached programmatic tools (computed once on first PTC call) */
227
+ programmaticCache;
228
+ /** Reference to Graph's sessions map for automatic session injection */
229
+ sessions;
230
+ /** When true, dispatches ON_TOOL_EXECUTE events instead of invoking tools directly */
231
+ eventDrivenMode = false;
232
+ /** Opt-in stream-layer prestart config for event-driven tools. */
233
+ eagerEventToolExecution;
234
+ /** Shared per-run prestarted tool registry populated by ChatModelStreamHandler. */
235
+ eagerEventToolExecutions;
236
+ /** Shared per-run per-tool turn counter used by eager and normal event dispatch. */
237
+ eagerEventToolUsageCount;
238
+ /** Agent ID for event-driven mode */
239
+ agentId;
240
+ /**
241
+ * ID of the agent that owns this tool node, whenever the graph knows it
242
+ * (including top-level agents in a multi-agent graph). Surfaced to hooks as
243
+ * `executingAgentId` so they can attribute a tool batch to a specific agent
244
+ * even where `agentId` (the subagent-scope marker) is undefined.
245
+ */
246
+ executingAgentId;
247
+ /** Tool names that bypass event dispatch and execute directly (e.g., graph-managed handoff tools) */
248
+ directToolNames;
249
+ /**
250
+ * File checkpointer extracted from the local coding tool bundle when
251
+ * `toolExecution.local.fileCheckpointing === true`. Exposed via
252
+ * {@link getFileCheckpointer}. Undefined when checkpointing is off
253
+ * or the local coding suite isn't bound to this node.
254
+ */
255
+ fileCheckpointer;
256
+ /** Maximum characters allowed in a single tool result before truncation. */
257
+ maxToolResultChars;
258
+ /** Hook registry for PreToolUse/PostToolUse lifecycle hooks */
259
+ hookRegistry;
260
+ /**
261
+ * Run-scoped HITL config. When `enabled`, `ask` decisions from
262
+ * PreToolUse hooks raise a LangGraph `interrupt()` instead of being
263
+ * treated as fail-closed denies.
264
+ */
265
+ humanInTheLoop;
266
+ /**
267
+ * Registry of tool outputs keyed by `tool<idx>turn<turn>`.
268
+ *
269
+ * Populated only when `toolOutputReferences.enabled` is true. The
270
+ * registry owns the run-scoped state (turn counter, last-seen runId,
271
+ * warn-once memo, stored outputs), so sharing a single instance
272
+ * across multiple ToolNodes in a run lets cross-agent `{{…}}`
273
+ * references resolve — which is why multi-agent graphs pass the
274
+ * *same* instance to every ToolNode they compile rather than each
275
+ * ToolNode building its own.
276
+ */
277
+ toolOutputRegistry;
278
+ /** Run-scoped selection for swapping remote code tools to local executors. */
279
+ toolExecution;
280
+ /**
281
+ * Monotonic counter used to mint a unique scope id for anonymous
282
+ * batches (ones invoked without a `run_id` in
283
+ * `config.configurable`). Each such batch gets its own registry
284
+ * partition so concurrent anonymous invocations can't delete each
285
+ * other's in-flight state.
286
+ */
287
+ anonBatchCounter = 0;
288
+ constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, agentId, executingAgentId, directToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer }) {
289
+ super({
290
+ name: name ?? TOOL_NODE_RUN_NAME,
291
+ tags,
292
+ func: (input, config) => this.run(input, config)
293
+ });
294
+ this.trace = trace ?? this.trace;
295
+ this.runLangfuse = runLangfuse;
296
+ this.agentLangfuse = agentLangfuse;
297
+ this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
298
+ this.toolCallStepIds = toolCallStepIds;
299
+ this.handleToolErrors = handleToolErrors ?? this.handleToolErrors;
300
+ this.loadRuntimeTools = loadRuntimeTools;
301
+ this.errorHandler = errorHandler;
302
+ this.toolUsageCount = /* @__PURE__ */ new Map();
303
+ this.toolRegistry = resolveLocalToolRegistry({
304
+ toolRegistry,
305
+ toolExecution
306
+ });
307
+ this.sessions = sessions;
308
+ this.eventDrivenMode = eventDrivenMode ?? false;
309
+ this.eagerEventToolExecution = eagerEventToolExecution;
310
+ this.eagerEventToolExecutions = eagerEventToolExecutions;
311
+ this.eagerEventToolUsageCount = eagerEventToolUsageCount;
312
+ this.agentId = agentId;
313
+ this.executingAgentId = executingAgentId ?? agentId;
314
+ this.directToolNames = directToolNames;
315
+ this.maxToolResultChars = maxToolResultChars ?? calculateMaxToolResultChars(maxContextTokens);
316
+ this.hookRegistry = hookRegistry;
317
+ this.humanInTheLoop = humanInTheLoop;
318
+ this.toolExecution = toolExecution;
319
+ this.fileCheckpointer = fileCheckpointer;
320
+ this.applyToolExecutionOverrides();
321
+ /**
322
+ * Precedence: an explicitly passed `toolOutputRegistry` instance
323
+ * wins over a config object so a host (`Graph`) can share one
324
+ * registry across many ToolNodes. When only the config is
325
+ * provided (direct ToolNode usage), build a local registry so
326
+ * the feature still works without graph-level plumbing. Registry
327
+ * caps are intentionally decoupled from `maxToolResultChars`:
328
+ * the registry stores the raw untruncated output so a later
329
+ * `{{…}}` substitution pipes the full payload into the next
330
+ * tool, even when the LLM saw a truncated preview.
331
+ */
332
+ if (toolOutputRegistry != null) this.toolOutputRegistry = toolOutputRegistry;
333
+ else if (toolOutputReferences?.enabled === true) this.toolOutputRegistry = new ToolOutputReferenceRegistry({
334
+ maxOutputSize: toolOutputReferences.maxOutputSize,
335
+ maxTotalSize: toolOutputReferences.maxTotalSize
336
+ });
337
+ }
338
+ async invoke(input, options) {
339
+ return withLangfuseToolOutputTracingConfig(this.runLangfuse, () => super.invoke(input, options), this.agentLangfuse);
340
+ }
341
+ /**
342
+ * Returns the run-scoped tool output registry, or `undefined` when
343
+ * the feature is disabled.
344
+ *
345
+ * @internal Exposed for test observation only. Host code should rely
346
+ * on `{{tool<i>turn<n>}}` substitution at tool-invocation time and
347
+ * not mutate the registry directly.
348
+ */
349
+ _unsafeGetToolOutputRegistry() {
350
+ return this.toolOutputRegistry;
351
+ }
352
+ /**
353
+ * Replaces known remote Code API tools with local-process tools when
354
+ * `RunConfig.toolExecution.engine === 'local'`. In event-driven mode those
355
+ * names are also marked direct so the SDK executes them locally instead of
356
+ * dispatching the batch to a host-side remote sandbox handler. When the
357
+ * local coding suite is enabled, this also injects file/search/edit tools.
358
+ */
359
+ applyToolExecutionOverrides() {
360
+ const resolved = resolveLocalExecutionTools({
361
+ toolMap: this.toolMap,
362
+ toolExecution: this.toolExecution,
363
+ fileCheckpointer: this.fileCheckpointer
364
+ });
365
+ this.toolMap = resolved.toolMap;
366
+ if (resolved.fileCheckpointer != null) this.fileCheckpointer = resolved.fileCheckpointer;
367
+ if (resolved.directToolNames.size === 0) return;
368
+ this.directToolNames = new Set([...this.directToolNames ?? /* @__PURE__ */ new Set(), ...resolved.directToolNames]);
369
+ this.programmaticCache = void 0;
370
+ }
371
+ /**
372
+ * Returns the per-Run file checkpointer when
373
+ * `toolExecution.local.fileCheckpointing === true`. Hosts call
374
+ * `rewind()` on the returned object to restore captured pre-write
375
+ * file contents — the standard "undo a tool batch" pattern. Returns
376
+ * undefined when checkpointing is disabled or the local coding suite
377
+ * isn't bound. Manual review (finding E): without this getter, the
378
+ * config flag was a silent no-op outside of direct
379
+ * `createLocalCodingToolBundle()` use.
380
+ */
381
+ getFileCheckpointer() {
382
+ return this.fileCheckpointer;
383
+ }
384
+ *getRegisteredHandoffNames() {
385
+ if (this.directToolNames != null) for (const toolName of this.directToolNames) yield toolName;
386
+ for (const toolName of this.toolMap.keys()) {
387
+ if (this.directToolNames?.has(toolName) === true) continue;
388
+ yield toolName;
389
+ }
390
+ }
391
+ hasRegisteredHandoffTool() {
392
+ for (const toolName of this.getRegisteredHandoffNames()) if (isHandoffToolName(toolName)) return true;
393
+ return false;
394
+ }
395
+ getHandoffToolNameSuggestion(callName) {
396
+ if (!isHandoffToolName(callName)) return;
397
+ let suggestion;
398
+ for (const toolName of this.getRegisteredHandoffNames()) {
399
+ if (!isHandoffToolName(toolName) || toolName.length >= callName.length || !callName.startsWith(toolName)) continue;
400
+ if (suggestion == null || toolName.length > suggestion.length) suggestion = toolName;
401
+ }
402
+ return suggestion;
403
+ }
404
+ shouldHandleUnknownHandoffLocally(callName, hasRegisteredHandoffTool) {
405
+ if (!isHandoffToolName(callName) || this.toolMap.has(callName)) return false;
406
+ return hasRegisteredHandoffTool ?? this.hasRegisteredHandoffTool();
407
+ }
408
+ getUnknownToolErrorMessage(callName) {
409
+ const suggestion = this.getHandoffToolNameSuggestion(callName);
410
+ if (suggestion == null) return `Tool "${callName}" not found.`;
411
+ return `Tool "${callName}" not found. Did you mean "${suggestion}"? Handoff tool names must match exactly.`;
412
+ }
413
+ /**
414
+ * Flush the per-Run direct-path turn cache. Called by the Graph at
415
+ * end-of-Run via `clearHeavyState`. The map intentionally survives
416
+ * `run()` re-entry so an interrupt + resume reuses the original
417
+ * slot (Codex P2 #30), but it would otherwise grow linearly with
418
+ * tool calls and could collide across Runs if a provider reused
419
+ * call IDs (Codex P2 #33). Hosts can also call this directly if
420
+ * they reuse a ToolNode across batches outside of a Graph.
421
+ */
422
+ clearDirectPathTurns() {
423
+ this.directPathTurns.clear();
424
+ }
425
+ /**
426
+ * Returns cached programmatic tools, computing once on first access.
427
+ * Single iteration builds both toolMap and toolDefs simultaneously.
428
+ */
429
+ getProgrammaticTools() {
430
+ if (this.programmaticCache) return this.programmaticCache;
431
+ const toolMap = /* @__PURE__ */ new Map();
432
+ const toolDefs = [];
433
+ if (this.toolRegistry) {
434
+ for (const [name, toolDef] of this.toolRegistry) if ((toolDef.allowed_callers ?? ["direct"]).includes("code_execution")) {
435
+ toolDefs.push(toolDef);
436
+ const tool = this.toolMap.get(name);
437
+ if (tool) toolMap.set(name, tool);
438
+ }
439
+ }
440
+ this.programmaticCache = {
441
+ toolMap,
442
+ toolDefs
443
+ };
444
+ return this.programmaticCache;
445
+ }
446
+ /**
447
+ * Returns a snapshot of the current tool usage counts.
448
+ * @returns A ReadonlyMap where keys are tool names and values are their usage counts.
449
+ */
450
+ getToolUsageCounts() {
451
+ return new Map(this.toolUsageCount);
452
+ }
453
+ recordToolUsageTurn(toolName, turn, callId) {
454
+ this.toolUsageCount.set(toolName, Math.max(this.toolUsageCount.get(toolName) ?? 0, turn + 1));
455
+ if (callId != null && callId !== "") this.toolCallTurns.set(callId, turn);
456
+ }
457
+ recordEventToolPlanningTurn(toolName, turn, callId) {
458
+ this.recordToolUsageTurn(toolName, turn, callId);
459
+ if (this.canConsumeEagerEventExecution()) this.eagerEventToolUsageCount?.set(toolName, Math.max(this.eagerEventToolUsageCount.get(toolName) ?? 0, turn + 1));
460
+ }
461
+ /**
462
+ * Runs a single tool call with error handling.
463
+ *
464
+ * `batchIndex` is the tool's position within the current ToolNode
465
+ * batch and, together with `this.currentTurn`, forms the key used to
466
+ * register the output for future `{{tool<idx>turn<turn>}}`
467
+ * substitutions. Omit when no registration should occur.
468
+ */
469
+ async runTool(call, config, batchContext = {}) {
470
+ const { batchIndex, turn, batchScopeId, resolvedArgsByCallId, preBatchSnapshot } = batchContext;
471
+ const tool = this.toolMap.get(call.name);
472
+ const registry = this.toolOutputRegistry;
473
+ let resolveFn;
474
+ if (preBatchSnapshot != null) resolveFn = (_runId, args) => preBatchSnapshot.resolve(args);
475
+ else if (registry != null) resolveFn = (runIdArg, args) => registry.resolve(runIdArg, args);
476
+ /**
477
+ * Precompute the reference key once per call — captured locally
478
+ * so concurrent `invoke()` calls on the same ToolNode cannot race
479
+ * on a shared turn field.
480
+ */
481
+ const refKey = registry != null && batchIndex != null && turn != null ? buildReferenceKey(batchIndex, turn) : void 0;
482
+ /**
483
+ * Hoisted outside the try so the catch branch can append
484
+ * `[unresolved refs: …]` to error messages — otherwise the LLM
485
+ * only sees a generic error when it references a bad key, losing
486
+ * the self-correction signal this feature is meant to provide.
487
+ */
488
+ let unresolvedRefs = [];
489
+ /**
490
+ * Use the caller-provided `batchScopeId` when threaded from
491
+ * `run()` (so anonymous batches get their own unique scope).
492
+ * Fall back to the config's `run_id` when runTool is invoked
493
+ * from a context that doesn't thread it — that still preserves
494
+ * the runId-based partitioning for named runs.
495
+ */
496
+ const runId = batchScopeId ?? config.configurable?.run_id;
497
+ try {
498
+ if (tool === void 0) throw new Error(this.getUnknownToolErrorMessage(call.name));
499
+ /**
500
+ * `usageCount` is the per-tool-name invocation index that
501
+ * web-search and other tools observe via `invokeParams.turn`.
502
+ * It is intentionally distinct from the outer `turn` parameter
503
+ * (the batch turn used for ref keys); the latter is captured
504
+ * before the try block when constructing `refKey`.
505
+ *
506
+ * Prefer the value `runDirectToolWithLifecycleHooks` already
507
+ * incremented (Codex P2 #27) — its hook wants the SAME turn
508
+ * the tool will execute under. When called from a path that
509
+ * doesn't pre-increment (event dispatch, the no-hooks
510
+ * shortcut), do the read+increment here.
511
+ */
512
+ const usageCount = batchContext.usageCount ?? (() => {
513
+ const next = this.toolUsageCount.get(call.name) ?? 0;
514
+ this.toolUsageCount.set(call.name, next + 1);
515
+ if (call.id != null && call.id !== "") this.toolCallTurns.set(call.id, next);
516
+ return next;
517
+ })();
518
+ let args = call.args;
519
+ if (resolveFn != null) {
520
+ const { resolved, unresolved } = resolveFn(runId, args);
521
+ args = resolved;
522
+ unresolvedRefs = unresolved;
523
+ /**
524
+ * Expose the post-substitution args to downstream completion
525
+ * events so audit logs / host-side `ON_RUN_STEP_COMPLETED`
526
+ * handlers observe what actually ran, not the `{{…}}`
527
+ * template. Only string/object args are worth recording.
528
+ */
529
+ if (resolvedArgsByCallId != null && call.id != null && call.id !== "" && resolved !== call.args && typeof resolved === "object") resolvedArgsByCallId.set(call.id, resolved);
530
+ }
531
+ const stepId = this.toolCallStepIds?.get(call.id);
532
+ let invokeParams = {
533
+ ...call,
534
+ args,
535
+ type: "tool_call",
536
+ stepId,
537
+ turn: usageCount
538
+ };
539
+ if (call.name === "run_tools_with_code" || call.name === "run_tools_with_bash") {
540
+ const { toolMap, toolDefs } = this.getProgrammaticTools();
541
+ invokeParams = {
542
+ ...invokeParams,
543
+ toolMap,
544
+ toolDefs,
545
+ hookContext: {
546
+ registry: this.hookRegistry,
547
+ runId: config.configurable?.run_id ?? "",
548
+ threadId: config.configurable?.thread_id,
549
+ agentId: this.agentId,
550
+ executingAgentId: this.executingAgentId
551
+ }
552
+ };
553
+ } else if (call.name === "tool_search") invokeParams = {
554
+ ...invokeParams,
555
+ toolRegistry: this.toolRegistry
556
+ };
557
+ /**
558
+ * Inject session context for code execution tools when available.
559
+ * Each file uses its own session_id (supporting multi-session file tracking).
560
+ * Both session_id and _injected_files are injected directly to invokeParams
561
+ * (not inside args) so they bypass Zod schema validation and reach config.toolCall.
562
+ *
563
+ * session_id is always injected when available, but concrete file refs
564
+ * still need to travel through `_injected_files`; the legacy
565
+ * `/files/<session_id>` fallback was removed from the executors.
566
+ */
567
+ if (CODE_EXECUTION_TOOLS.has(call.name)) {
568
+ const codeSession = this.sessions?.get("execute_code");
569
+ const execSessionId = codeSession?.session_id;
570
+ if (execSessionId != null && execSessionId !== "") {
571
+ invokeParams = {
572
+ ...invokeParams,
573
+ session_id: execSessionId
574
+ };
575
+ if (codeSession?.files != null && codeSession.files.length > 0) invokeParams._injected_files = codeSession.files.map((file) => toInjectedFileRef(file, execSessionId));
576
+ }
577
+ }
578
+ const output = await tool.invoke(invokeParams, config);
579
+ if (isCommand(output)) return output;
580
+ if (isBaseMessage(output) && output._getType() === "tool") {
581
+ const toolMsg = output;
582
+ if (toolMsg.status === "error") {
583
+ /**
584
+ * Error ToolMessages bypass registration but still stamp the
585
+ * unresolved-refs hint into `additional_kwargs` so the lazy
586
+ * annotation transform surfaces it to the LLM, letting the
587
+ * model self-correct when its reference key caused the
588
+ * failure. Persisted `content` stays clean.
589
+ */
590
+ if (unresolvedRefs.length > 0) toolMsg.additional_kwargs = {
591
+ ...toolMsg.additional_kwargs,
592
+ _unresolvedRefs: unresolvedRefs
593
+ };
594
+ return toolMsg;
595
+ }
596
+ if (this.toolOutputRegistry != null || unresolvedRefs.length > 0) if (typeof toolMsg.content === "string") {
597
+ const rawContent = toolMsg.content;
598
+ const registryContent = stripCodeSessionFileSummary(rawContent);
599
+ toolMsg.content = truncateToolResultContent(rawContent, this.maxToolResultChars);
600
+ const refMeta = this.recordOutputReference(runId, registryContent, refKey, unresolvedRefs);
601
+ if (refMeta != null) toolMsg.additional_kwargs = {
602
+ ...toolMsg.additional_kwargs,
603
+ ...refMeta
604
+ };
605
+ } else {
606
+ /**
607
+ * Non-string content (multi-part content blocks — text +
608
+ * image). Known limitation: we cannot register under a
609
+ * reference key because there's no canonical serialized
610
+ * form. Warn once per tool per run when the caller
611
+ * intended to register. The unresolved-refs hint is still
612
+ * stamped as metadata; the lazy transform prepends a text
613
+ * block at request time so the LLM gets the self-correction
614
+ * signal.
615
+ */
616
+ if (unresolvedRefs.length > 0) toolMsg.additional_kwargs = {
617
+ ...toolMsg.additional_kwargs,
618
+ _unresolvedRefs: unresolvedRefs
619
+ };
620
+ if (refKey != null && this.toolOutputRegistry.claimWarnOnce(runId, call.name)) console.warn(`[ToolNode] Skipping tool output reference for "${call.name}": ToolMessage content is not a string (further occurrences for this tool in the same run are suppressed).`);
621
+ }
622
+ return toolMsg;
623
+ }
624
+ const rawContent = typeof output === "string" ? output : JSON.stringify(output);
625
+ const truncated = truncateToolResultContent(rawContent, this.maxToolResultChars);
626
+ const refMeta = this.recordOutputReference(runId, stripCodeSessionFileSummary(rawContent), refKey, unresolvedRefs);
627
+ return new ToolMessage({
628
+ status: "success",
629
+ name: tool.name,
630
+ content: truncated,
631
+ tool_call_id: call.id,
632
+ ...refMeta != null && { additional_kwargs: refMeta }
633
+ });
634
+ } catch (_e) {
635
+ const e = _e;
636
+ if (!this.handleToolErrors) throw e;
637
+ if (isGraphInterrupt(e)) throw e;
638
+ if (this.errorHandler) try {
639
+ await this.errorHandler({
640
+ error: e,
641
+ id: call.id,
642
+ name: call.name,
643
+ input: call.args
644
+ }, config.metadata);
645
+ } catch (handlerError) {
646
+ console.error("Error in errorHandler:", {
647
+ toolName: call.name,
648
+ toolCallId: call.id,
649
+ toolArgs: call.args,
650
+ stepId: this.toolCallStepIds?.get(call.id),
651
+ turn: this.toolUsageCount.get(call.name),
652
+ originalError: {
653
+ message: e.message,
654
+ stack: e.stack ?? void 0
655
+ },
656
+ handlerError: handlerError instanceof Error ? {
657
+ message: handlerError.message,
658
+ stack: handlerError.stack ?? void 0
659
+ } : {
660
+ message: String(handlerError),
661
+ stack: void 0
662
+ }
663
+ });
664
+ }
665
+ const errorContent = `Error: ${e.message}\n Please fix your mistakes.`;
666
+ const refMeta = unresolvedRefs.length > 0 ? this.recordOutputReference(runId, errorContent, void 0, unresolvedRefs) : void 0;
667
+ return new ToolMessage({
668
+ status: "error",
669
+ content: errorContent,
670
+ name: call.name,
671
+ tool_call_id: call.id ?? "",
672
+ ...refMeta != null && { additional_kwargs: refMeta }
673
+ });
674
+ }
675
+ }
676
+ /**
677
+ * Runs a single in-process tool call with the same lifecycle hooks
678
+ * the event-dispatch path fires (`PreToolUse`, `PermissionDenied`,
679
+ * `PostToolUse`, `PostToolUseFailure`). Used for any tool whose
680
+ * implementation lives in the SDK process — i.e. every entry in
681
+ * `directToolNames` — so host-supplied policy hooks gate
682
+ * direct-invoked tools the same way they gate dispatched ones.
683
+ *
684
+ * Fast path: when the registry has none of the relevant events
685
+ * registered for this run, falls through to `runTool` with zero
686
+ * extra work. The hook list is also checked via
687
+ * `hasHookFor(event, runId)`, which performs the registry's own
688
+ * O(1) shortcut.
689
+ *
690
+ * Hook semantics intentionally mirror `dispatchToolEvents` for the
691
+ * single-call case:
692
+ * - `PreToolUse` returning `decision: 'deny'` synthesizes an error
693
+ * `ToolMessage` and fires `PermissionDenied` (observational).
694
+ * - `PreToolUse` returning `decision: 'ask'`:
695
+ * • When `humanInTheLoop.enabled === true`: raises a real
696
+ * `tool_approval` interrupt for this single tool call (the
697
+ * same payload shape the event path produces). On resume:
698
+ * `approve` runs the tool, `reject` blocks via
699
+ * `blockDirectCall`, `respond` returns the host-supplied
700
+ * `responseText` as a synthetic success ToolMessage,
701
+ * `edit` re-runs with edited args. LangGraph re-enters
702
+ * ToolNode.run from the start on resume; the hook fires
703
+ * again and the resume value distinguishes "first ask" from
704
+ * "second pass with decision".
705
+ * • When HITL is off: collapses to a fail-closed deny (matches
706
+ * the rest of the SDK's HITL-disabled default). One-time
707
+ * warning logged so hosts notice the gap.
708
+ * - `PreToolUse.updatedInput` is applied to the call before
709
+ * `runTool` runs; placeholder resolution inside `runTool` is
710
+ * idempotent on already-resolved args.
711
+ * - `PostToolUse.updatedOutput` replaces the returned
712
+ * `ToolMessage` content (preserving id/name/status).
713
+ * - `PostToolUseFailure` fires when `runTool` returns a
714
+ * `ToolMessage` whose `status === 'error'`. Observational only;
715
+ * the error message stays the source of truth.
716
+ *
717
+ * `PostToolBatch` aggregation across direct + dispatched outcomes is
718
+ * a separate concern: `dispatchToolEvents` accumulates batch entries
719
+ * locally and fires `PostToolBatch` at the end of its scope. Wiring
720
+ * direct-call entries into that aggregation crosses the two paths'
721
+ * scopes and is left to a follow-up.
722
+ */
723
+ async runDirectToolWithLifecycleHooks(call, config, batchContext = {}) {
724
+ const runId = config.configurable?.run_id ?? "";
725
+ const hookRegistry = this.hookRegistry;
726
+ const hasPreHook = hookRegistry?.hasHookFor("PreToolUse", runId) === true;
727
+ const hasPostHook = hookRegistry?.hasHookFor("PostToolUse", runId) === true;
728
+ const hasFailureHook = hookRegistry?.hasHookFor("PostToolUseFailure", runId) === true;
729
+ if (hookRegistry == null || !hasPreHook && !hasPostHook && !hasFailureHook) return this.runTool(call, config, batchContext);
730
+ const threadId = config.configurable?.thread_id;
731
+ const registryRunId = batchContext.batchScopeId ?? config.configurable?.run_id;
732
+ let usageCount;
733
+ const cachedTurn = call.id != null && call.id !== "" ? this.directPathTurns.get(call.id) ?? this.toolCallTurns.get(call.id) : void 0;
734
+ if (cachedTurn != null) usageCount = cachedTurn;
735
+ else {
736
+ usageCount = this.toolUsageCount.get(call.name) ?? 0;
737
+ this.toolUsageCount.set(call.name, usageCount + 1);
738
+ if (call.id != null && call.id !== "") {
739
+ this.toolCallTurns.set(call.id, usageCount);
740
+ this.directPathTurns.set(call.id, usageCount);
741
+ }
742
+ }
743
+ const turn = usageCount;
744
+ const stepId = this.toolCallStepIds?.get(call.id ?? "") ?? "";
745
+ let resolvedArgs = call.args;
746
+ if (batchContext.preBatchSnapshot != null) {
747
+ const { resolved } = batchContext.preBatchSnapshot.resolve(call.args);
748
+ resolvedArgs = resolved;
749
+ } else if (this.toolOutputRegistry != null) {
750
+ const { resolved } = this.toolOutputRegistry.resolve(registryRunId, call.args);
751
+ resolvedArgs = resolved;
752
+ }
753
+ let effectiveCall = call;
754
+ if (hasPreHook) {
755
+ const preResult = await executeHooks({
756
+ registry: hookRegistry,
757
+ input: {
758
+ hook_event_name: "PreToolUse",
759
+ runId,
760
+ threadId,
761
+ agentId: this.agentId,
762
+ executingAgentId: this.executingAgentId,
763
+ toolName: call.name,
764
+ toolInput: resolvedArgs,
765
+ toolUseId: call.id ?? "",
766
+ stepId,
767
+ turn
768
+ },
769
+ sessionId: runId,
770
+ matchQuery: call.name
771
+ }).catch(() => void 0);
772
+ if (preResult != null) {
773
+ if (batchContext.additionalContextsSink != null && preResult.additionalContexts.length > 0) batchContext.additionalContextsSink.push(...preResult.additionalContexts);
774
+ if (preResult.updatedInput != null) effectiveCall = {
775
+ ...call,
776
+ args: preResult.updatedInput
777
+ };
778
+ if (preResult.decision === "deny") return this.blockDirectCall({
779
+ call,
780
+ resolvedArgs,
781
+ reason: preResult.reason ?? "Blocked by hook",
782
+ hookRegistry,
783
+ runId,
784
+ threadId
785
+ });
786
+ if (preResult.decision === "ask") {
787
+ if (this.humanInTheLoop?.enabled !== true) {
788
+ const reason = this.resolveAskDecisionForDirectTool(preResult.reason, call.name);
789
+ return this.blockDirectCall({
790
+ call,
791
+ resolvedArgs,
792
+ reason,
793
+ hookRegistry,
794
+ runId,
795
+ threadId
796
+ });
797
+ }
798
+ const payload = buildToolApprovalInterruptPayload([{
799
+ entry: {
800
+ call: effectiveCall,
801
+ args: effectiveCall.args,
802
+ stepId
803
+ },
804
+ reason: preResult.reason,
805
+ allowedDecisions: preResult.allowedDecisions
806
+ }]);
807
+ const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(config, () => interrupt(payload));
808
+ const decision = normalizeApprovalDecisions([call.id], resumeValue).get(call.id) ?? {
809
+ type: "reject",
810
+ reason: "No decision provided for tool approval"
811
+ };
812
+ const declaredType = decision.type;
813
+ if (preResult.allowedDecisions != null && (typeof declaredType !== "string" || !preResult.allowedDecisions.includes(declaredType))) return this.blockDirectCall({
814
+ call,
815
+ resolvedArgs,
816
+ reason: `Decision "${typeof declaredType === "string" ? declaredType : "<missing>"}" not in allowedDecisions [${preResult.allowedDecisions.join(", ")}] — failing closed`,
817
+ hookRegistry,
818
+ runId,
819
+ threadId
820
+ });
821
+ if (decision.type === "reject") return this.blockDirectCall({
822
+ call,
823
+ resolvedArgs,
824
+ reason: decision.reason ?? preResult.reason ?? "Rejected by user",
825
+ hookRegistry,
826
+ runId,
827
+ threadId
828
+ });
829
+ if (decision.type === "respond") {
830
+ const responseText = decision.responseText;
831
+ if (typeof responseText !== "string") return this.blockDirectCall({
832
+ call,
833
+ resolvedArgs,
834
+ reason: "Approval payload `respond` was missing a string `responseText`",
835
+ hookRegistry,
836
+ runId,
837
+ threadId
838
+ });
839
+ return new ToolMessage({
840
+ status: "success",
841
+ content: responseText,
842
+ name: call.name,
843
+ tool_call_id: call.id ?? ""
844
+ });
845
+ }
846
+ if (decision.type === "edit") {
847
+ const updatedInput = decision.updatedInput;
848
+ if (updatedInput === null || typeof updatedInput !== "object" || Array.isArray(updatedInput)) return new ToolMessage({
849
+ status: "error",
850
+ content: "Decision \"edit\" missing object updatedInput — failing closed.",
851
+ name: call.name,
852
+ tool_call_id: call.id ?? ""
853
+ });
854
+ effectiveCall = {
855
+ ...call,
856
+ args: updatedInput
857
+ };
858
+ }
859
+ }
860
+ }
861
+ }
862
+ const output = await this.runTool(effectiveCall, config, {
863
+ ...batchContext,
864
+ usageCount
865
+ });
866
+ if (!(output instanceof ToolMessage)) return output;
867
+ if (output.status === "error" && hasFailureHook) {
868
+ const failureResult = await executeHooks({
869
+ registry: hookRegistry,
870
+ input: {
871
+ hook_event_name: "PostToolUseFailure",
872
+ runId,
873
+ threadId,
874
+ agentId: this.agentId,
875
+ executingAgentId: this.executingAgentId,
876
+ toolName: call.name,
877
+ toolInput: effectiveCall.args,
878
+ toolUseId: call.id ?? "",
879
+ error: typeof output.content === "string" ? output.content : JSON.stringify(output.content),
880
+ stepId,
881
+ turn
882
+ },
883
+ sessionId: runId,
884
+ matchQuery: call.name
885
+ }).catch(() => void 0);
886
+ if (failureResult != null && batchContext.additionalContextsSink != null && failureResult.additionalContexts.length > 0) batchContext.additionalContextsSink.push(...failureResult.additionalContexts);
887
+ return output;
888
+ }
889
+ if (output.status !== "error" && hasPostHook) {
890
+ const postResult = await executeHooks({
891
+ registry: hookRegistry,
892
+ input: {
893
+ hook_event_name: "PostToolUse",
894
+ runId,
895
+ threadId,
896
+ agentId: this.agentId,
897
+ executingAgentId: this.executingAgentId,
898
+ toolName: call.name,
899
+ toolInput: effectiveCall.args,
900
+ toolOutput: output.content,
901
+ toolUseId: call.id ?? "",
902
+ stepId,
903
+ turn
904
+ },
905
+ sessionId: runId,
906
+ matchQuery: call.name
907
+ }).catch(() => void 0);
908
+ if (postResult != null && batchContext.additionalContextsSink != null && postResult.additionalContexts.length > 0) batchContext.additionalContextsSink.push(...postResult.additionalContexts);
909
+ if (postResult?.updatedOutput != null) {
910
+ const replaced = typeof postResult.updatedOutput === "string" ? postResult.updatedOutput : JSON.stringify(postResult.updatedOutput);
911
+ const refMeta = output.additional_kwargs;
912
+ const refKey = refMeta?._refKey;
913
+ const refScope = refMeta?._refScope;
914
+ if (this.toolOutputRegistry != null && refKey != null) this.toolOutputRegistry.set(refScope, refKey, replaced);
915
+ return new ToolMessage({
916
+ status: output.status,
917
+ name: output.name,
918
+ content: replaced,
919
+ artifact: output.artifact,
920
+ tool_call_id: output.tool_call_id,
921
+ additional_kwargs: output.additional_kwargs
922
+ });
923
+ }
924
+ }
925
+ return output;
926
+ }
927
+ /**
928
+ * `ask` decisions on direct-path tools collapse to fail-closed deny
929
+ * only when `humanInTheLoop.enabled !== true` (i.e. there's no host
930
+ * UI configured to actually prompt the user). Logged once per process
931
+ * so the gap is visible. When HITL IS enabled, `ask` raises a real
932
+ * LangGraph `interrupt()` instead — see `runDirectToolWithLifecycleHooks`.
933
+ */
934
+ askDirectWarningEmitted = false;
935
+ resolveAskDecisionForDirectTool(reason, toolName) {
936
+ if (!this.askDirectWarningEmitted) {
937
+ this.askDirectWarningEmitted = true;
938
+ console.warn(`[ToolNode] PreToolUse returned 'ask' for direct-path tool "${toolName}" but humanInTheLoop is not enabled — failing closed. Set humanInTheLoop.enabled=true to raise a tool_approval interrupt the host can resolve.`);
939
+ }
940
+ return reason ?? "Blocked by hook";
941
+ }
942
+ /**
943
+ * Synthesize a Blocked ToolMessage AND fire `PermissionDenied`
944
+ * (observational) for a direct-path tool call. Centralised so the
945
+ * deny path looks identical whether the block came from `'deny'` or
946
+ * from a fail-closed/`'reject'`/policy-violation path.
947
+ */
948
+ blockDirectCall(args) {
949
+ const { call, resolvedArgs, reason, hookRegistry, runId, threadId } = args;
950
+ if (hookRegistry.hasHookFor("PermissionDenied", runId) === true) executeHooks({
951
+ registry: hookRegistry,
952
+ input: {
953
+ hook_event_name: "PermissionDenied",
954
+ runId,
955
+ threadId,
956
+ agentId: this.agentId,
957
+ executingAgentId: this.executingAgentId,
958
+ toolName: call.name,
959
+ toolInput: resolvedArgs,
960
+ toolUseId: call.id ?? "",
961
+ reason
962
+ },
963
+ sessionId: runId,
964
+ matchQuery: call.name
965
+ }).catch(() => {});
966
+ return new ToolMessage({
967
+ status: "error",
968
+ content: `Blocked: ${reason}`,
969
+ name: call.name,
970
+ tool_call_id: call.id ?? ""
971
+ });
972
+ }
973
+ /**
974
+ * Registers the full, raw output under `refKey` (when provided) and
975
+ * builds the per-message ref metadata stamped onto the resulting
976
+ * `ToolMessage.additional_kwargs`. The metadata is read at LLM-
977
+ * request time by `annotateMessagesForLLM` to produce a transient
978
+ * annotated copy of the message — the persisted `content` itself
979
+ * stays clean.
980
+ *
981
+ * @param registryContent The full, untruncated output to store in
982
+ * the registry so `{{tool<i>turn<n>}}` substitutions deliver the
983
+ * complete payload. Ignored when `refKey` is undefined.
984
+ * @param refKey Precomputed `tool<i>turn<n>` key, or undefined when
985
+ * the output is not to be registered (errors, disabled feature,
986
+ * unavailable batch/turn).
987
+ * @param unresolved Placeholder keys that did not resolve; surfaced
988
+ * to the LLM lazily so it can self-correct.
989
+ * @returns A `ToolMessageRefMetadata` object when there is anything
990
+ * to stamp, otherwise `undefined`.
991
+ */
992
+ recordOutputReference(runId, registryContent, refKey, unresolved) {
993
+ if (this.toolOutputRegistry != null && refKey != null) this.toolOutputRegistry.set(runId, refKey, registryContent);
994
+ if (refKey == null && unresolved.length === 0) return void 0;
995
+ const meta = {};
996
+ if (refKey != null) {
997
+ meta._refKey = refKey;
998
+ /**
999
+ * Stamp the registry scope alongside the key so the lazy
1000
+ * annotation transform can look up the right bucket. Anonymous
1001
+ * invocations get a synthetic per-batch scope (`\0anon-<n>`)
1002
+ * that `attemptInvoke` cannot derive from
1003
+ * `config.configurable.run_id` — without this, anonymous-run
1004
+ * refs would silently fail registry lookup and the LLM would
1005
+ * never see `[ref: …]` markers for outputs that were registered.
1006
+ */
1007
+ if (runId != null) meta._refScope = runId;
1008
+ }
1009
+ if (unresolved.length > 0) meta._unresolvedRefs = unresolved;
1010
+ return meta;
1011
+ }
1012
+ /**
1013
+ * Builds code session context for injection into event-driven tool calls.
1014
+ * Mirrors the session injection logic in runTool() for direct execution.
1015
+ */
1016
+ getCodeSessionContext() {
1017
+ if (!this.sessions) return;
1018
+ const codeSession = this.sessions.get("execute_code");
1019
+ if (!codeSession) return;
1020
+ const execSessionId = codeSession.session_id;
1021
+ const context = { session_id: execSessionId };
1022
+ if (codeSession.files && codeSession.files.length > 0) context.files = codeSession.files.map((file) => toInjectedFileRef(file, execSessionId));
1023
+ return context;
1024
+ }
1025
+ /**
1026
+ * Extracts code execution session context from tool results and stores in Graph.sessions.
1027
+ * Mirrors the session storage logic in handleRunToolCompletions for direct execution.
1028
+ */
1029
+ storeCodeSessionFromResults(results, requestMap) {
1030
+ if (!this.sessions) return;
1031
+ for (let i = 0; i < results.length; i++) {
1032
+ const result = results[i];
1033
+ if (result.status !== "success" || result.artifact == null) continue;
1034
+ const request = requestMap.get(result.toolCallId);
1035
+ if (request?.name == null || request.name === "" || !CODE_EXECUTION_TOOLS.has(request.name) && request.name !== "skill") continue;
1036
+ const artifact = result.artifact;
1037
+ const execSessionId = artifact?.session_id;
1038
+ if (execSessionId == null || execSessionId === "") continue;
1039
+ updateCodeSession(this.sessions, execSessionId, artifact?.files);
1040
+ }
1041
+ }
1042
+ /**
1043
+ * Post-processes standard runTool outputs: dispatches ON_RUN_STEP_COMPLETED
1044
+ * and stores code session context. Mirrors the completion handling in
1045
+ * dispatchToolEvents for the event-driven path.
1046
+ *
1047
+ * By handling completions here in graph context (rather than in the
1048
+ * stream consumer via ToolEndHandler), the race between the stream
1049
+ * consumer and graph execution is eliminated.
1050
+ *
1051
+ * @param resolvedArgsByCallId Per-batch resolved-args sink populated
1052
+ * by `runTool`. Threaded as a local map (instead of instance state)
1053
+ * so concurrent batches cannot read each other's entries.
1054
+ */
1055
+ async handleRunToolCompletions(calls, outputs, config, resolvedArgsByCallId) {
1056
+ for (let i = 0; i < calls.length; i++) {
1057
+ const call = calls[i];
1058
+ const output = outputs[i];
1059
+ const turn = this.toolCallTurns.get(call.id) ?? 0;
1060
+ if (isCommand(output)) continue;
1061
+ const toolMessage = output;
1062
+ const toolCallId = call.id ?? "";
1063
+ if (toolMessage.status === "error" && this.errorHandler != null) continue;
1064
+ if (this.sessions && CODE_EXECUTION_TOOLS.has(call.name)) {
1065
+ const artifact = toolMessage.artifact;
1066
+ const execSessionId = artifact?.session_id;
1067
+ if (execSessionId != null && execSessionId !== "") updateCodeSession(this.sessions, execSessionId, artifact?.files);
1068
+ }
1069
+ const stepId = this.toolCallStepIds?.get(toolCallId) ?? "";
1070
+ if (!stepId) continue;
1071
+ const contentString = typeof toolMessage.content === "string" ? toolMessage.content : JSON.stringify(toolMessage.content);
1072
+ /**
1073
+ * Prefer the post-substitution args when a `{{…}}` placeholder
1074
+ * was resolved in `runTool`. This keeps
1075
+ * `ON_RUN_STEP_COMPLETED.tool_call.args` consistent with what
1076
+ * the tool actually received rather than leaking the template.
1077
+ */
1078
+ const effectiveArgs = resolvedArgsByCallId?.get(toolCallId) ?? call.args;
1079
+ await safeDispatchCustomEvent("on_run_step_completed", { result: {
1080
+ id: stepId,
1081
+ index: turn,
1082
+ type: "tool_call",
1083
+ tool_call: {
1084
+ args: typeof effectiveArgs === "string" ? effectiveArgs : JSON.stringify(effectiveArgs ?? {}),
1085
+ name: call.name,
1086
+ id: toolCallId,
1087
+ output: contentString,
1088
+ progress: 1
1089
+ }
1090
+ } }, config);
1091
+ }
1092
+ }
1093
+ /**
1094
+ * Dispatches tool calls to the host via ON_TOOL_EXECUTE event and returns raw ToolMessages.
1095
+ * Core logic for event-driven execution, separated from output shaping.
1096
+ *
1097
+ * Hook lifecycle (when `hookRegistry` is set):
1098
+ * 1. **PreToolUse** fires per call in parallel before dispatch. Denied
1099
+ * calls produce error ToolMessages and fire **PermissionDenied**;
1100
+ * surviving calls proceed with optional `updatedInput`.
1101
+ * 2. Surviving calls are dispatched to the host via `ON_TOOL_EXECUTE`.
1102
+ * 3. **PostToolUse** / **PostToolUseFailure** fire per result. Post hooks
1103
+ * can replace tool output via `updatedOutput`.
1104
+ * 4. Injected messages from results are collected and returned alongside
1105
+ * ToolMessages (appended AFTER to respect provider ordering).
1106
+ */
1107
+ async dispatchToolEvents(toolCalls, config, batchContext = {}) {
1108
+ const { batchIndices, turn, batchScopeId, preResolvedArgs, preBatchSnapshot } = batchContext;
1109
+ const runId = config.configurable?.run_id ?? "";
1110
+ /**
1111
+ * Registry-facing scope id — prefers the caller-threaded
1112
+ * `batchScopeId` so anonymous batches target their own unique
1113
+ * bucket and don't step on concurrent anonymous invocations.
1114
+ * Hooks and event payloads keep using the empty-string coerced
1115
+ * `runId` for backward compat.
1116
+ */
1117
+ const registryRunId = batchScopeId ?? config.configurable?.run_id;
1118
+ const threadId = config.configurable?.thread_id;
1119
+ const registry = this.toolOutputRegistry;
1120
+ const unresolvedByCallId = /* @__PURE__ */ new Map();
1121
+ const preToolCalls = toolCalls.map((call, i) => {
1122
+ const originalArgs = call.args;
1123
+ let resolvedArgs = originalArgs;
1124
+ /**
1125
+ * When the caller provided a pre-resolved map (the mixed
1126
+ * direct+event path snapshots event args synchronously before
1127
+ * awaiting directs so they can't accidentally resolve
1128
+ * same-turn direct outputs), use those entries verbatim instead
1129
+ * of re-resolving against a registry that may have changed
1130
+ * since the batch started.
1131
+ */
1132
+ const pre = call.id != null ? preResolvedArgs?.get(call.id) : void 0;
1133
+ if (pre != null) {
1134
+ resolvedArgs = pre.resolved;
1135
+ if (pre.unresolved.length > 0 && call.id != null) unresolvedByCallId.set(call.id, pre.unresolved);
1136
+ } else if (registry != null) {
1137
+ const { resolved, unresolved } = registry.resolve(registryRunId, originalArgs);
1138
+ resolvedArgs = resolved;
1139
+ if (unresolved.length > 0 && call.id != null) unresolvedByCallId.set(call.id, unresolved);
1140
+ }
1141
+ return {
1142
+ call,
1143
+ stepId: this.toolCallStepIds?.get(call.id) ?? "",
1144
+ args: resolvedArgs,
1145
+ batchIndex: batchIndices?.[i]
1146
+ };
1147
+ });
1148
+ const messageByCallId = /* @__PURE__ */ new Map();
1149
+ const approvedEntries = [];
1150
+ /**
1151
+ * Batch-level accumulator for `additionalContext` strings returned
1152
+ * by any PreToolUse / PostToolUse / PostToolUseFailure hook in this
1153
+ * dispatch. We emit one consolidated `HumanMessage` after all tool
1154
+ * results land so the next model turn sees the injected context
1155
+ * exactly once, ordered after the ToolMessages.
1156
+ */
1157
+ const batchAdditionalContexts = [];
1158
+ /**
1159
+ * Batch-level outcome record keyed by `tool_call_id`. Captures
1160
+ * every tool call's final result (success / error from the host,
1161
+ * blocked from HITL deny / reject, substituted from HITL respond)
1162
+ * across the three call sites that touch it. We materialize the
1163
+ * `PostToolBatch` entry array in `toolCalls` order at dispatch
1164
+ * time so hooks correlating outcomes by position see exactly the
1165
+ * same sequence the model emitted — independent of when each
1166
+ * outcome was recorded (deny entries land synchronously in the
1167
+ * hook loop, approved entries land after host execution, respond
1168
+ * entries land in the resume branch).
1169
+ */
1170
+ const postToolBatchEntryByCallId = /* @__PURE__ */ new Map();
1171
+ const HOOK_FALLBACK = Object.freeze({
1172
+ additionalContexts: [],
1173
+ errors: []
1174
+ });
1175
+ if (this.hookRegistry?.hasHookFor("PreToolUse", runId) === true) {
1176
+ /**
1177
+ * Capture as a non-null local so the inner `blockEntry` closure
1178
+ * doesn't lose narrowing on `this.hookRegistry` and we don't have
1179
+ * to defensively `?.` it across every reference inside.
1180
+ */
1181
+ const hookRegistry = this.hookRegistry;
1182
+ const preResults = await Promise.all(preToolCalls.map((entry) => executeHooks({
1183
+ registry: hookRegistry,
1184
+ input: {
1185
+ hook_event_name: "PreToolUse",
1186
+ runId,
1187
+ threadId,
1188
+ agentId: this.agentId,
1189
+ executingAgentId: this.executingAgentId,
1190
+ toolName: entry.call.name,
1191
+ toolInput: entry.args,
1192
+ toolUseId: entry.call.id,
1193
+ stepId: entry.stepId,
1194
+ turn: this.toolUsageCount.get(entry.call.name) ?? 0
1195
+ },
1196
+ sessionId: runId,
1197
+ matchQuery: entry.call.name
1198
+ }).catch(() => HOOK_FALLBACK)));
1199
+ /**
1200
+ * Side effects deferred from `blockEntry` until after any pending
1201
+ * `interrupt()` resolves. Without deferral, a batch that mixes a
1202
+ * `deny` decision with an `ask` decision would dispatch
1203
+ * `ON_RUN_STEP_COMPLETED` for the denied tool on the FIRST node
1204
+ * execution (before `interrupt()` throws), then dispatch the
1205
+ * same event AGAIN on the resume re-execution — hosts would
1206
+ * observe two completion events for one logical denial. By
1207
+ * queueing the dispatch + PermissionDenied hook here and
1208
+ * flushing after the interrupt block, we ensure each side effect
1209
+ * fires exactly once: never on the first pass when interrupt
1210
+ * throws (the flush is unreachable), once on resume / no-ask
1211
+ * passes when control reaches the flush.
1212
+ */
1213
+ const deferredBlockedSideEffects = [];
1214
+ const blockEntry = (entry, reason) => {
1215
+ const contentString = `Blocked: ${reason}`;
1216
+ messageByCallId.set(entry.call.id, new ToolMessage({
1217
+ status: "error",
1218
+ content: contentString,
1219
+ name: entry.call.name,
1220
+ tool_call_id: entry.call.id
1221
+ }));
1222
+ postToolBatchEntryByCallId.set(entry.call.id, {
1223
+ toolName: entry.call.name,
1224
+ toolInput: entry.args,
1225
+ toolUseId: entry.call.id,
1226
+ stepId: entry.stepId,
1227
+ /**
1228
+ * Records the pre-invocation turn count — the same value the
1229
+ * executed path captures before incrementing `toolUsageCount`.
1230
+ * For a blocked tool the counter is never incremented (no
1231
+ * invocation happened), so this is always the count of prior
1232
+ * successful invocations of this tool name in earlier batches.
1233
+ * Surfaces in the `PostToolBatch` entry so batch hooks see
1234
+ * a uniform shape regardless of outcome.
1235
+ */
1236
+ turn: this.toolUsageCount.get(entry.call.name) ?? 0,
1237
+ status: "error",
1238
+ error: contentString
1239
+ });
1240
+ deferredBlockedSideEffects.push({
1241
+ callId: entry.call.id,
1242
+ toolName: entry.call.name,
1243
+ args: entry.args,
1244
+ contentString,
1245
+ reason
1246
+ });
1247
+ };
1248
+ const flushDeferredBlockedSideEffects = async () => {
1249
+ for (const item of deferredBlockedSideEffects) {
1250
+ await this.dispatchStepCompleted(item.callId, item.toolName, item.args, item.contentString, config);
1251
+ if (hookRegistry.hasHookFor("PermissionDenied", runId)) executeHooks({
1252
+ registry: hookRegistry,
1253
+ input: {
1254
+ hook_event_name: "PermissionDenied",
1255
+ runId,
1256
+ threadId,
1257
+ agentId: this.agentId,
1258
+ executingAgentId: this.executingAgentId,
1259
+ toolName: item.toolName,
1260
+ toolInput: item.args,
1261
+ toolUseId: item.callId,
1262
+ reason: item.reason
1263
+ },
1264
+ sessionId: runId,
1265
+ matchQuery: item.toolName
1266
+ }).catch(() => {});
1267
+ }
1268
+ deferredBlockedSideEffects.length = 0;
1269
+ };
1270
+ /**
1271
+ * Apply a hook-supplied or host-supplied input override to a pending
1272
+ * entry, re-running the `{{tool<i>turn<n>}}` resolver so any new
1273
+ * placeholders introduced by the override are substituted (and any
1274
+ * formerly-unresolved refs cleared from the unresolved set).
1275
+ *
1276
+ * Mixed direct+event batches must use the pre-batch snapshot so a
1277
+ * hook-introduced placeholder cannot accidentally resolve to a
1278
+ * same-turn direct output that has just registered. Pure event
1279
+ * batches don't have a snapshot and resolve against the live
1280
+ * registry — safe because no event-side registrations have happened
1281
+ * yet.
1282
+ */
1283
+ const applyInputOverride = (entry, nextArgs) => {
1284
+ if (registry != null) {
1285
+ const { resolved, unresolved } = (preBatchSnapshot ?? { resolve: (args) => registry.resolve(registryRunId, args) }).resolve(nextArgs);
1286
+ entry.args = resolved;
1287
+ if (entry.call.id != null) if (unresolved.length > 0) unresolvedByCallId.set(entry.call.id, unresolved);
1288
+ else unresolvedByCallId.delete(entry.call.id);
1289
+ return;
1290
+ }
1291
+ entry.args = nextArgs;
1292
+ };
1293
+ const askEntries = [];
1294
+ for (let i = 0; i < preToolCalls.length; i++) {
1295
+ const hookResult = preResults[i];
1296
+ const entry = preToolCalls[i];
1297
+ for (const ctx of hookResult.additionalContexts) batchAdditionalContexts.push(ctx);
1298
+ if (hookResult.decision === "deny") {
1299
+ blockEntry(entry, hookResult.reason ?? "Blocked by hook");
1300
+ continue;
1301
+ }
1302
+ if (hookResult.decision === "ask") {
1303
+ /**
1304
+ * HITL is OFF by default — hosts must explicitly opt in via
1305
+ * `humanInTheLoop: { enabled: true }` to engage the
1306
+ * `interrupt()` path. When opted out (or omitted), `ask`
1307
+ * collapses into the pre-HITL fail-closed path: a blocked
1308
+ * tool with an error `ToolMessage`. The default stays
1309
+ * conservative until host UIs are ready to render
1310
+ * `tool_approval` interrupts; see `HumanInTheLoopConfig`
1311
+ * JSDoc for the full rationale and the migration plan.
1312
+ */
1313
+ if (this.humanInTheLoop?.enabled !== true) {
1314
+ blockEntry(entry, hookResult.reason ?? "Blocked by hook");
1315
+ continue;
1316
+ }
1317
+ /**
1318
+ * Apply `updatedInput` BEFORE queuing into `askEntries` —
1319
+ * a hook is allowed to return both a sanitization rewrite
1320
+ * and an `ask` decision (e.g. one matcher redacts secrets,
1321
+ * another matcher requires approval). Without this, the
1322
+ * interrupt payload would surface the original args to the
1323
+ * reviewer AND the post-approve execution would run with
1324
+ * the original args, silently dropping the hook's rewrite.
1325
+ */
1326
+ if (hookResult.updatedInput != null) applyInputOverride(entry, hookResult.updatedInput);
1327
+ askEntries.push({
1328
+ entry,
1329
+ reason: hookResult.reason,
1330
+ allowedDecisions: hookResult.allowedDecisions
1331
+ });
1332
+ continue;
1333
+ }
1334
+ if (hookResult.updatedInput != null) applyInputOverride(entry, hookResult.updatedInput);
1335
+ approvedEntries.push(entry);
1336
+ }
1337
+ /**
1338
+ * If any entries asked for approval, raise a single LangGraph
1339
+ * `interrupt()` carrying every pending request together. The host
1340
+ * pauses, gathers human input, and resumes the run with one
1341
+ * decision per request. On resume LangGraph re-executes this node
1342
+ * from the start; `interrupt()` then returns the resume value
1343
+ * instead of throwing, so the loop above re-runs and the same
1344
+ * `askEntries` list is rebuilt deterministically (assuming hooks
1345
+ * are pure — see `humanInTheLoop` docs).
1346
+ */
1347
+ if (askEntries.length > 0) {
1348
+ const payload = buildToolApprovalInterruptPayload(askEntries);
1349
+ /**
1350
+ * `interrupt()` reads the current `RunnableConfig` from
1351
+ * AsyncLocalStorage. ToolNode usually runs with tracing disabled
1352
+ * (unless Langfuse explicitly enables it), so the upstream
1353
+ * `runWithConfig` frame may not exist. Re-anchor here using the
1354
+ * node's own `config` — Pregel hands us a config that already
1355
+ * carries every checkpoint/scratchpad key `interrupt()` needs to
1356
+ * suspend and resume.
1357
+ */
1358
+ const resumeValue = AsyncLocalStorageProviderSingleton.runWithConfig(config, () => interrupt(payload));
1359
+ const decisionByCallId = normalizeApprovalDecisions(askEntries.map(({ entry }) => entry.call.id), resumeValue);
1360
+ for (const { entry, reason: askReason, allowedDecisions } of askEntries) {
1361
+ const decision = decisionByCallId.get(entry.call.id) ?? {
1362
+ type: "reject",
1363
+ reason: "No decision provided for tool approval"
1364
+ };
1365
+ /**
1366
+ * Read `decision.type` through a widened view once: hosts
1367
+ * deserialize resume payloads from untyped JSON, so the
1368
+ * runtime value can be a typo, the wrong type, or missing
1369
+ * entirely. Both the `allowedDecisions` enforcement
1370
+ * immediately below and the unknown-type fallthrough at the
1371
+ * end of this loop body share this single read so the
1372
+ * fail-closed checks compare against the same source.
1373
+ */
1374
+ const declaredType = decision.type;
1375
+ /**
1376
+ * Enforce the per-tool `allowedDecisions` allowlist that the
1377
+ * `PreToolUse` hook surfaced in `review_configs`. The host
1378
+ * UI is supposed to honor this when collecting the user's
1379
+ * decision, but the wire is untrusted: a buggy or hostile
1380
+ * host could submit a decision type the policy explicitly
1381
+ * forbids (e.g. `'edit'` when the hook restricted to
1382
+ * `['approve', 'reject']`), bypassing argument-mutation /
1383
+ * response-substitution safeguards. Fail closed when the
1384
+ * declared type isn't in the allowlist.
1385
+ */
1386
+ if (allowedDecisions != null && (typeof declaredType !== "string" || !allowedDecisions.includes(declaredType))) {
1387
+ blockEntry(entry, `Decision "${typeof declaredType === "string" ? declaredType : "<missing>"}" not in allowedDecisions [${allowedDecisions.join(", ")}] — failing closed`);
1388
+ continue;
1389
+ }
1390
+ if (decision.type === "reject") {
1391
+ blockEntry(entry, decision.reason ?? askReason ?? "Rejected by user");
1392
+ continue;
1393
+ }
1394
+ /**
1395
+ * `respond` short-circuits tool execution: the human supplies
1396
+ * the result the model should see in place of running the
1397
+ * tool. We emit a successful `ToolMessage` directly and skip
1398
+ * dispatch — no host event fires, no real tool side effect
1399
+ * occurs. Mirrors LangChain HITL middleware semantics.
1400
+ */
1401
+ if (decision.type === "respond") {
1402
+ /**
1403
+ * Validate the wire shape before touching it: hosts
1404
+ * deserialize resume payloads from untyped JSON, so a
1405
+ * malformed `{ type: 'respond' }` (no `responseText`) or
1406
+ * `{ type: 'respond', responseText: 42 }` would crash
1407
+ * `truncateToolResultContent` (which calls
1408
+ * `content.length`) and turn a fail-closed approval path
1409
+ * into a hard run failure. Route bad shapes through
1410
+ * `blockEntry` like any other unusable decision.
1411
+ */
1412
+ const responseText = decision.responseText;
1413
+ if (typeof responseText !== "string") {
1414
+ blockEntry(entry, `Decision "respond" missing string responseText (got ${describeOfferedShape(responseText)}) — failing closed`);
1415
+ continue;
1416
+ }
1417
+ /**
1418
+ * Truncate the human-supplied text just like the success
1419
+ * path does for real tool output. Without this, a user
1420
+ * pasting a large document as a manual response bypasses
1421
+ * `maxToolResultChars` and can blow past the model's
1422
+ * context window. The PostToolBatch entry surfaces the
1423
+ * truncated text too so batch hooks see what the model
1424
+ * will actually see.
1425
+ */
1426
+ const truncatedResponse = truncateToolResultContent(responseText, this.maxToolResultChars);
1427
+ messageByCallId.set(entry.call.id, new ToolMessage({
1428
+ status: "success",
1429
+ content: truncatedResponse,
1430
+ name: entry.call.name,
1431
+ tool_call_id: entry.call.id
1432
+ }));
1433
+ postToolBatchEntryByCallId.set(entry.call.id, {
1434
+ toolName: entry.call.name,
1435
+ toolInput: entry.args,
1436
+ toolUseId: entry.call.id,
1437
+ stepId: entry.stepId,
1438
+ turn: this.toolUsageCount.get(entry.call.name) ?? 0,
1439
+ status: "success",
1440
+ toolOutput: truncatedResponse
1441
+ });
1442
+ /**
1443
+ * Safe to dispatch immediately — unlike `blockEntry` which
1444
+ * defers, `respond` only executes inside the decision-
1445
+ * processing loop, which is reachable only AFTER
1446
+ * `interrupt()` has returned (the resume pass). There is
1447
+ * no risk of being rolled back by a subsequent throw, so
1448
+ * no risk of a duplicate `ON_RUN_STEP_COMPLETED` event.
1449
+ */
1450
+ await this.dispatchStepCompleted(entry.call.id, entry.call.name, entry.args, truncatedResponse, config);
1451
+ continue;
1452
+ }
1453
+ if (decision.type === "edit") {
1454
+ /**
1455
+ * Validate the wire shape before touching it: hosts
1456
+ * deserialize resume payloads from untyped JSON, so a
1457
+ * malformed `{ type: 'edit' }` (no `updatedInput`),
1458
+ * `{ type: 'edit', updatedInput: 'string' }` (non-object),
1459
+ * or `{ type: 'edit', updatedInput: [...] }` (array, not a
1460
+ * plain object) would feed garbage into
1461
+ * `applyInputOverride` and silently approve a tool with
1462
+ * undefined / wrong-shape args. Same trust boundary as
1463
+ * the `respond` validation above — fail closed via
1464
+ * `blockEntry` with a diagnostic.
1465
+ */
1466
+ const updatedInput = decision.updatedInput;
1467
+ if (updatedInput === null || typeof updatedInput !== "object" || Array.isArray(updatedInput)) {
1468
+ blockEntry(entry, `Decision "edit" missing object updatedInput (got ${describeOfferedShape(updatedInput)}) — failing closed`);
1469
+ continue;
1470
+ }
1471
+ applyInputOverride(entry, updatedInput);
1472
+ approvedEntries.push(entry);
1473
+ continue;
1474
+ }
1475
+ /**
1476
+ * Defensive type widening: hosts deserialize resume payloads
1477
+ * from untyped JSON, so the `decision.type` value at runtime
1478
+ * is whatever string the wire sent — not necessarily one of
1479
+ * the four union variants TS knows about. We compare against
1480
+ * the literal `'approve'` through the widened `declaredType`
1481
+ * captured at the top of this iteration, so a typo or schema
1482
+ * drift (`'aproved'`, `null`, `undefined`) hits the fail-
1483
+ * closed branch below instead of silently approving the
1484
+ * tool. Without this widening, TS narrows the union after
1485
+ * the three earlier branches and treats `=== 'approve'` as
1486
+ * trivially true.
1487
+ */
1488
+ if (declaredType === "approve") {
1489
+ approvedEntries.push(entry);
1490
+ continue;
1491
+ }
1492
+ blockEntry(entry, `Unknown approval decision type "${typeof declaredType === "string" ? declaredType : "<missing>"}" — failing closed`);
1493
+ }
1494
+ }
1495
+ /**
1496
+ * Flush deferred denial side effects exactly once. On the FIRST
1497
+ * pass through a batch that contains an `ask`, `interrupt()`
1498
+ * threw above and we never reach this line — so no
1499
+ * `ON_RUN_STEP_COMPLETED` / `PermissionDenied` events fire
1500
+ * for blocked tools yet. On resume the node re-executes from
1501
+ * scratch, `blockEntry` re-queues the same entries, and the
1502
+ * flush below dispatches them once. For batches without any
1503
+ * `ask` (deny-only or empty), the flush still runs here and
1504
+ * dispatches in the same relative position as the pre-deferral
1505
+ * code did (after hook processing, before tool execution).
1506
+ */
1507
+ await flushDeferredBlockedSideEffects();
1508
+ } else approvedEntries.push(...preToolCalls);
1509
+ const injected = [];
1510
+ const batchIndexByCallId = /* @__PURE__ */ new Map();
1511
+ if (approvedEntries.length > 0) {
1512
+ const plan = buildToolExecutionRequestPlan({
1513
+ toolCalls: approvedEntries.map((entry) => {
1514
+ const codeSessionContext = CODE_EXECUTION_TOOLS.has(entry.call.name) || entry.call.name === "skill" || entry.call.name === "read_file" ? this.getCodeSessionContext() : void 0;
1515
+ return {
1516
+ id: entry.call.id,
1517
+ name: entry.call.name,
1518
+ args: entry.args,
1519
+ stepId: entry.stepId,
1520
+ codeSessionContext
1521
+ };
1522
+ }),
1523
+ usageCount: this.toolUsageCount,
1524
+ invalidArgsBehavior: "error-result",
1525
+ recordTurn: (toolName, reservedTurn, callId) => {
1526
+ this.recordEventToolPlanningTurn(toolName, reservedTurn, callId);
1527
+ }
1528
+ });
1529
+ if (plan == null) throw new Error("Unable to build event tool execution request plan");
1530
+ const requests = plan.requests;
1531
+ for (const entry of approvedEntries) if (entry.batchIndex != null && entry.call.id != null) batchIndexByCallId.set(entry.call.id, entry.batchIndex);
1532
+ for (const result of plan.rejectedResults) this.eagerEventToolExecutions?.delete(result.toolCallId);
1533
+ const requestMap = new Map(plan.allRequests.map((r) => [r.id, r]));
1534
+ const eagerExecutions = [];
1535
+ const dispatchRequests = [];
1536
+ for (const request of requests) {
1537
+ const eagerExecution = this.takeMatchingEagerEventExecution(request);
1538
+ if (eagerExecution != null) eagerExecutions.push({
1539
+ request,
1540
+ execution: eagerExecution
1541
+ });
1542
+ else dispatchRequests.push(request);
1543
+ }
1544
+ const dispatchPromise = dispatchRequests.length === 0 ? Promise.resolve([]) : new Promise((resolve, reject) => {
1545
+ let dispatchSettled = false;
1546
+ let resultSettled = false;
1547
+ let settledResults;
1548
+ const maybeResolve = () => {
1549
+ if (dispatchSettled && resultSettled) resolve(settledResults ?? []);
1550
+ };
1551
+ safeDispatchCustomEvent("on_tool_execute", {
1552
+ toolCalls: dispatchRequests,
1553
+ userId: config.configurable?.user_id,
1554
+ agentId: this.agentId,
1555
+ configurable: config.configurable,
1556
+ metadata: config.metadata,
1557
+ resolve: (results) => {
1558
+ resultSettled = true;
1559
+ settledResults = results;
1560
+ maybeResolve();
1561
+ },
1562
+ reject
1563
+ }, config).then(() => {
1564
+ dispatchSettled = true;
1565
+ maybeResolve();
1566
+ }).catch(reject);
1567
+ });
1568
+ const eagerResultsPromise = Promise.all(eagerExecutions.map(async ({ request, execution }) => {
1569
+ return {
1570
+ results: await this.resolveEagerEventExecution(request, execution),
1571
+ completionDispatched: execution.completionDispatched === true && execution.request.turn === request.turn,
1572
+ toolCallId: request.id
1573
+ };
1574
+ }));
1575
+ const [eagerResults, dispatchedResults] = await Promise.all([eagerResultsPromise, dispatchPromise]);
1576
+ const eagerCompletionDispatchedIds = new Set(eagerResults.filter((result) => result.completionDispatched).map((result) => result.toolCallId));
1577
+ const flattenedEagerResults = eagerResults.flatMap((result) => result.results);
1578
+ const results = [
1579
+ ...plan.rejectedResults,
1580
+ ...flattenedEagerResults,
1581
+ ...dispatchedResults
1582
+ ];
1583
+ this.storeCodeSessionFromResults(results, requestMap);
1584
+ const hasPostHook = this.hookRegistry?.hasHookFor("PostToolUse", runId) === true;
1585
+ const hasFailureHook = this.hookRegistry?.hasHookFor("PostToolUseFailure", runId) === true;
1586
+ for (const result of results) {
1587
+ if (result.injectedMessages && result.injectedMessages.length > 0) try {
1588
+ injected.push(...this.convertInjectedMessages(result.injectedMessages));
1589
+ } catch (e) {
1590
+ console.warn(`[ToolNode] Failed to convert injectedMessages for toolCallId=${result.toolCallId}:`, e instanceof Error ? e.message : e);
1591
+ }
1592
+ const request = requestMap.get(result.toolCallId);
1593
+ const toolName = request?.name ?? "unknown";
1594
+ let contentString;
1595
+ let toolMessage;
1596
+ /**
1597
+ * Tracks the post-PostToolUse-hook output so the
1598
+ * `PostToolBatch` entry below sees the final transformed value
1599
+ * even when a hook replaced the original via `updatedOutput`.
1600
+ * Lives at the loop-iteration scope so the success branch can
1601
+ * mutate it; the error branch leaves it unset (and the batch
1602
+ * entry uses `error` instead of `toolOutput` in that case).
1603
+ */
1604
+ let finalToolOutput = result.content;
1605
+ if (result.status === "error") {
1606
+ contentString = `Error: ${result.errorMessage ?? "Unknown error"}\n Please fix your mistakes.`;
1607
+ /**
1608
+ * Error results bypass registration but stamp the
1609
+ * unresolved-refs hint into `additional_kwargs` so the lazy
1610
+ * annotation transform surfaces it to the LLM at request
1611
+ * time, letting the model self-correct when its reference
1612
+ * key caused the failure. Persisted `content` stays clean.
1613
+ */
1614
+ const unresolved = unresolvedByCallId.get(result.toolCallId) ?? [];
1615
+ const errorRefMeta = unresolved.length > 0 ? this.recordOutputReference(registryRunId, contentString, void 0, unresolved) : void 0;
1616
+ toolMessage = new ToolMessage({
1617
+ status: "error",
1618
+ content: contentString,
1619
+ name: toolName,
1620
+ tool_call_id: result.toolCallId,
1621
+ ...errorRefMeta != null && { additional_kwargs: errorRefMeta }
1622
+ });
1623
+ if (hasFailureHook) {
1624
+ const failureHookResult = await executeHooks({
1625
+ registry: this.hookRegistry,
1626
+ input: {
1627
+ hook_event_name: "PostToolUseFailure",
1628
+ runId,
1629
+ threadId,
1630
+ agentId: this.agentId,
1631
+ executingAgentId: this.executingAgentId,
1632
+ toolName,
1633
+ toolInput: request?.args ?? {},
1634
+ toolUseId: result.toolCallId,
1635
+ error: result.errorMessage ?? "Unknown error",
1636
+ stepId: request?.stepId,
1637
+ turn: request?.turn
1638
+ },
1639
+ sessionId: runId,
1640
+ matchQuery: toolName
1641
+ }).catch(() => void 0);
1642
+ /**
1643
+ * Collect `additionalContext` from failure hooks too. Without
1644
+ * this, recovery guidance returned on tool errors (e.g.
1645
+ * "if this tool errors with X, suggest Y to the user") is
1646
+ * silently dropped even though the API surface advertises
1647
+ * `additionalContext` for this event. PostToolUseFailure
1648
+ * remains observational for errors thrown by the hook
1649
+ * itself, but a successfully-returned result is honored.
1650
+ */
1651
+ if (failureHookResult != null) for (const ctx of failureHookResult.additionalContexts) batchAdditionalContexts.push(ctx);
1652
+ }
1653
+ } else {
1654
+ let registryRaw = typeof result.content === "string" ? result.content : JSON.stringify(result.content);
1655
+ contentString = truncateToolResultContent(registryRaw, this.maxToolResultChars);
1656
+ if (hasPostHook) {
1657
+ const hookResult = await executeHooks({
1658
+ registry: this.hookRegistry,
1659
+ input: {
1660
+ hook_event_name: "PostToolUse",
1661
+ runId,
1662
+ threadId,
1663
+ agentId: this.agentId,
1664
+ executingAgentId: this.executingAgentId,
1665
+ toolName,
1666
+ toolInput: request?.args ?? {},
1667
+ toolOutput: result.content,
1668
+ toolUseId: result.toolCallId,
1669
+ stepId: request?.stepId,
1670
+ turn: request?.turn
1671
+ },
1672
+ sessionId: runId,
1673
+ matchQuery: toolName
1674
+ }).catch(() => void 0);
1675
+ if (hookResult != null) for (const ctx of hookResult.additionalContexts) batchAdditionalContexts.push(ctx);
1676
+ if (hookResult?.updatedOutput != null) {
1677
+ const replaced = typeof hookResult.updatedOutput === "string" ? hookResult.updatedOutput : JSON.stringify(hookResult.updatedOutput);
1678
+ registryRaw = replaced;
1679
+ contentString = truncateToolResultContent(replaced, this.maxToolResultChars);
1680
+ finalToolOutput = hookResult.updatedOutput;
1681
+ }
1682
+ }
1683
+ const batchIndex = batchIndexByCallId.get(result.toolCallId);
1684
+ const unresolved = unresolvedByCallId.get(result.toolCallId) ?? [];
1685
+ const refKey = this.toolOutputRegistry != null && batchIndex != null && turn != null ? buildReferenceKey(batchIndex, turn) : void 0;
1686
+ const successRefMeta = this.recordOutputReference(registryRunId, stripCodeSessionFileSummary(registryRaw), refKey, unresolved);
1687
+ toolMessage = new ToolMessage({
1688
+ status: "success",
1689
+ name: toolName,
1690
+ content: contentString,
1691
+ artifact: result.artifact,
1692
+ tool_call_id: result.toolCallId,
1693
+ ...successRefMeta != null && { additional_kwargs: successRefMeta }
1694
+ });
1695
+ }
1696
+ if (!eagerCompletionDispatchedIds.has(result.toolCallId)) await this.dispatchStepCompleted(result.toolCallId, toolName, request?.args ?? {}, contentString, config, request?.turn);
1697
+ postToolBatchEntryByCallId.set(result.toolCallId, {
1698
+ toolName,
1699
+ toolInput: request?.args ?? {},
1700
+ toolUseId: result.toolCallId,
1701
+ stepId: request?.stepId,
1702
+ turn: request?.turn,
1703
+ status: result.status === "error" ? "error" : "success",
1704
+ ...result.status === "error" ? { error: result.errorMessage ?? "Unknown error" } : { toolOutput: finalToolOutput }
1705
+ });
1706
+ messageByCallId.set(result.toolCallId, toolMessage);
1707
+ }
1708
+ }
1709
+ const toolMessages = toolCalls.map((call) => messageByCallId.get(call.id)).filter((m) => m != null);
1710
+ await this.dispatchPostToolBatchAndInjectContext({
1711
+ toolCalls,
1712
+ entriesByCallId: postToolBatchEntryByCallId,
1713
+ batchAdditionalContexts,
1714
+ injected,
1715
+ runId,
1716
+ threadId
1717
+ });
1718
+ return {
1719
+ toolMessages,
1720
+ injected
1721
+ };
1722
+ }
1723
+ canConsumeEagerEventExecution() {
1724
+ return this.eventDrivenMode && this.eagerEventToolExecution?.enabled === true && this.hookRegistry == null && this.humanInTheLoop?.enabled !== true;
1725
+ }
1726
+ takeMatchingEagerEventExecution(request) {
1727
+ if (!this.canConsumeEagerEventExecution()) return;
1728
+ const execution = this.eagerEventToolExecutions?.get(request.id);
1729
+ if (execution == null) return;
1730
+ this.eagerEventToolExecutions?.delete(request.id);
1731
+ if (execution.toolName !== request.name || !recordArgsEqual(execution.args, request.args)) return {
1732
+ toolCallId: request.id,
1733
+ toolName: request.name,
1734
+ args: request.args,
1735
+ request,
1736
+ promise: Promise.resolve({ results: [{
1737
+ toolCallId: request.id,
1738
+ status: "error",
1739
+ content: "",
1740
+ errorMessage: "Tool call changed after eager execution started; refusing to re-run the tool to avoid duplicate side effects."
1741
+ }] })
1742
+ };
1743
+ return execution;
1744
+ }
1745
+ async resolveEagerEventExecution(request, execution) {
1746
+ const outcome = await execution.promise;
1747
+ if (outcome.error != null) throw outcome.error;
1748
+ const results = outcome.results.filter((result) => result.toolCallId === request.id);
1749
+ if (results.length > 0) return results;
1750
+ return [{
1751
+ toolCallId: request.id,
1752
+ status: "error",
1753
+ content: "",
1754
+ errorMessage: "Tool execution completed without a result for this tool call"
1755
+ }];
1756
+ }
1757
+ /**
1758
+ * Fires the `PostToolBatch` hook (if registered) and appends the
1759
+ * accumulated batch-level `additionalContext` strings to `injected`
1760
+ * as a single `HumanMessage`. Entries are materialized in the
1761
+ * original `toolCalls` order so hooks correlating outcomes by
1762
+ * position (as the type docs promise) see exactly the sequence
1763
+ * the model emitted, regardless of when each individual outcome
1764
+ * was recorded into the map (deny synchronous, approved
1765
+ * post-execution, respond on resume).
1766
+ *
1767
+ * The PostToolBatch hook's `additionalContexts` flow into the same
1768
+ * batch accumulator per-tool hooks already use, so a single
1769
+ * batch-level convention message can be injected through one path.
1770
+ *
1771
+ * Mutates `batchAdditionalContexts` (push from batch hook) and
1772
+ * `injected` (push the consolidated HumanMessage). The caller owns
1773
+ * those arrays and consumes them right after this returns.
1774
+ */
1775
+ async dispatchPostToolBatchAndInjectContext(args) {
1776
+ const { toolCalls, entriesByCallId, batchAdditionalContexts, injected, runId, threadId } = args;
1777
+ const orderedBatchEntries = [];
1778
+ for (const call of toolCalls) {
1779
+ const callId = call.id;
1780
+ if (callId == null) continue;
1781
+ const entry = entriesByCallId.get(callId);
1782
+ if (entry != null) orderedBatchEntries.push(entry);
1783
+ }
1784
+ if (this.hookRegistry?.hasHookFor("PostToolBatch", runId) === true && orderedBatchEntries.length > 0) {
1785
+ const batchHookResult = await executeHooks({
1786
+ registry: this.hookRegistry,
1787
+ input: {
1788
+ hook_event_name: "PostToolBatch",
1789
+ runId,
1790
+ threadId,
1791
+ agentId: this.agentId,
1792
+ executingAgentId: this.executingAgentId,
1793
+ entries: orderedBatchEntries
1794
+ },
1795
+ sessionId: runId
1796
+ }).catch(() => void 0);
1797
+ if (batchHookResult != null) for (const ctx of batchHookResult.additionalContexts) batchAdditionalContexts.push(ctx);
1798
+ }
1799
+ if (batchAdditionalContexts.length > 0)
1800
+ /**
1801
+ * `HumanMessage` carrying a metadata `role: 'system'` marker —
1802
+ * see `convertInjectedMessages` for the wider rationale. Anthropic
1803
+ * and Google reject mid-conversation `SystemMessage`s, so we use
1804
+ * a user-role message and surface the system intent through
1805
+ * `additional_kwargs` for hosts inspecting state. The model sees
1806
+ * a user message; `role` is metadata only.
1807
+ */
1808
+ injected.push(new HumanMessage({
1809
+ content: batchAdditionalContexts.join("\n\n"),
1810
+ additional_kwargs: {
1811
+ role: "system",
1812
+ source: "hook"
1813
+ }
1814
+ }));
1815
+ }
1816
+ async dispatchStepCompleted(toolCallId, toolName, args, output, config, turn) {
1817
+ const stepId = this.toolCallStepIds?.get(toolCallId) ?? "";
1818
+ if (!stepId) console.warn(`[ToolNode] toolCallStepIds missing entry for toolCallId=${toolCallId} (tool=${toolName}). This indicates a race between the stream consumer and graph execution. Map size: ${this.toolCallStepIds?.size ?? 0}`);
1819
+ await safeDispatchCustomEvent("on_run_step_completed", { result: {
1820
+ id: stepId,
1821
+ index: turn ?? this.toolUsageCount.get(toolName) ?? 0,
1822
+ type: "tool_call",
1823
+ tool_call: {
1824
+ args: JSON.stringify(args),
1825
+ name: toolName,
1826
+ id: toolCallId,
1827
+ output,
1828
+ progress: 1
1829
+ }
1830
+ } }, config);
1831
+ }
1832
+ /**
1833
+ * Converts InjectedMessage instances to LangChain HumanMessage objects.
1834
+ * Both 'user' and 'system' roles become HumanMessage to avoid provider
1835
+ * rejections (Anthropic/Google reject non-leading SystemMessages).
1836
+ * The original role is preserved in additional_kwargs for downstream consumers.
1837
+ */
1838
+ convertInjectedMessages(messages) {
1839
+ const converted = [];
1840
+ for (const msg of messages) {
1841
+ const additional_kwargs = { role: msg.role };
1842
+ if (msg.isMeta != null) additional_kwargs.isMeta = msg.isMeta;
1843
+ if (msg.source != null) additional_kwargs.source = msg.source;
1844
+ if (msg.skillName != null) additional_kwargs.skillName = msg.skillName;
1845
+ converted.push(new HumanMessage({
1846
+ content: toLangChainContent(msg.content),
1847
+ additional_kwargs
1848
+ }));
1849
+ }
1850
+ return converted;
1851
+ }
1852
+ /**
1853
+ * Execute all tool calls via ON_TOOL_EXECUTE event dispatch.
1854
+ * Injected messages are placed AFTER ToolMessages to respect provider
1855
+ * message ordering (AIMessage tool_calls must be immediately followed
1856
+ * by their ToolMessage results).
1857
+ *
1858
+ * `batchIndices` mirrors `toolCalls` and carries each call's position
1859
+ * within the parent batch. `turn` is the per-`run()` batch index
1860
+ * captured locally by the caller. Both are threaded so concurrent
1861
+ * invocations cannot race on shared mutable state.
1862
+ */
1863
+ async executeViaEvent(toolCalls, config, input, batchContext = {}) {
1864
+ const { toolMessages, injected } = await this.dispatchToolEvents(toolCalls, config, batchContext);
1865
+ const outputs = [...toolMessages, ...injected];
1866
+ return Array.isArray(input) ? outputs : { messages: outputs };
1867
+ }
1868
+ async run(input, config) {
1869
+ this.toolCallTurns.clear();
1870
+ /**
1871
+ * Per-batch local map for resolved (post-substitution) args.
1872
+ * Lives on the stack so concurrent `run()` calls on the same
1873
+ * ToolNode cannot read or wipe each other's entries.
1874
+ */
1875
+ const resolvedArgsByCallId = /* @__PURE__ */ new Map();
1876
+ const batchScopeId = config.configurable?.run_id ?? `\0anon-${this.anonBatchCounter++}`;
1877
+ const turn = this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
1878
+ let outputs;
1879
+ if (this.isSendInput(input)) {
1880
+ const isLocalTool = this.directToolNames?.has(input.lg_tool_call.name) === true || this.shouldHandleUnknownHandoffLocally(input.lg_tool_call.name);
1881
+ if (this.eventDrivenMode && !isLocalTool) return this.executeViaEvent([input.lg_tool_call], config, input, {
1882
+ batchIndices: [0],
1883
+ turn,
1884
+ batchScopeId
1885
+ });
1886
+ const directAdditionalContexts = [];
1887
+ const sendOutput = await this.runDirectToolWithLifecycleHooks(input.lg_tool_call, config, {
1888
+ batchIndex: 0,
1889
+ turn,
1890
+ batchScopeId,
1891
+ resolvedArgsByCallId,
1892
+ additionalContextsSink: directAdditionalContexts
1893
+ });
1894
+ outputs = directAdditionalContexts.length > 0 ? [sendOutput, new HumanMessage({
1895
+ content: directAdditionalContexts.join("\n\n"),
1896
+ additional_kwargs: {
1897
+ role: "system",
1898
+ source: "hook"
1899
+ }
1900
+ })] : [sendOutput];
1901
+ await this.handleRunToolCompletions([input.lg_tool_call], [sendOutput], config, resolvedArgsByCallId);
1902
+ } else {
1903
+ let messages;
1904
+ if (Array.isArray(input)) messages = input;
1905
+ else if (this.isMessagesState(input)) messages = input.messages;
1906
+ else throw new Error("ToolNode only accepts BaseMessage[] or { messages: BaseMessage[] } as input.");
1907
+ const toolMessageIds = new Set(messages.filter((msg) => msg._getType() === "tool").map((msg) => msg.tool_call_id));
1908
+ let aiMessage;
1909
+ for (let i = messages.length - 1; i >= 0; i--) {
1910
+ const message = messages[i];
1911
+ if (isAIMessage(message)) {
1912
+ aiMessage = message;
1913
+ break;
1914
+ }
1915
+ }
1916
+ if (aiMessage == null || !isAIMessage(aiMessage)) throw new Error("ToolNode only accepts AIMessages as input.");
1917
+ if (this.loadRuntimeTools) {
1918
+ const { tools, toolMap } = this.loadRuntimeTools(aiMessage.tool_calls ?? []);
1919
+ this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
1920
+ this.applyToolExecutionOverrides();
1921
+ this.programmaticCache = void 0;
1922
+ }
1923
+ const filteredCalls = aiMessage.tool_calls?.filter((call) => {
1924
+ /**
1925
+ * Filter out:
1926
+ * 1. Already processed tool calls (present in toolMessageIds)
1927
+ * 2. Server tool calls (e.g., web_search with IDs starting with 'srvtoolu_')
1928
+ * which are executed by the provider's API and don't require invocation
1929
+ */
1930
+ return (call.id == null || !toolMessageIds.has(call.id)) && !(call.id?.startsWith("srvtoolu_") ?? false);
1931
+ }) ?? [];
1932
+ if (this.eventDrivenMode && filteredCalls.length > 0) {
1933
+ const directToolNames = this.directToolNames;
1934
+ const hasRegisteredHandoffTool = this.hasRegisteredHandoffTool();
1935
+ const directEntries = [];
1936
+ const eventEntries = [];
1937
+ for (let i = 0; i < filteredCalls.length; i++) {
1938
+ const call = filteredCalls[i];
1939
+ const entry = {
1940
+ call,
1941
+ batchIndex: i
1942
+ };
1943
+ if (directToolNames?.has(call.name) === true || this.shouldHandleUnknownHandoffLocally(call.name, hasRegisteredHandoffTool)) directEntries.push(entry);
1944
+ else eventEntries.push(entry);
1945
+ }
1946
+ if (directEntries.length === 0) return this.executeViaEvent(filteredCalls, config, input, {
1947
+ batchIndices: eventEntries.map((entry) => entry.batchIndex),
1948
+ turn,
1949
+ batchScopeId
1950
+ });
1951
+ const directCalls = directEntries.map((e) => e.call);
1952
+ const directIndices = directEntries.map((e) => e.batchIndex);
1953
+ const eventCalls = eventEntries.map((e) => e.call);
1954
+ const eventIndices = eventEntries.map((e) => e.batchIndex);
1955
+ /**
1956
+ * Snapshot the event calls' args against the *pre-batch*
1957
+ * registry state synchronously, before any await runs. The
1958
+ * directs are then awaited first (preserving fail-fast
1959
+ * semantics — a thrown error in a direct tool, e.g. with
1960
+ * `handleToolErrors=false` or a `GraphInterrupt`, aborts
1961
+ * before we dispatch any event-driven tools to the host).
1962
+ * Because the event args were captured pre-await, they stay
1963
+ * isolated from same-turn direct outputs that register
1964
+ * during the await.
1965
+ */
1966
+ const preResolvedEventArgs = /* @__PURE__ */ new Map();
1967
+ /**
1968
+ * Take a frozen snapshot of the registry state before any
1969
+ * direct registrations land. The snapshot resolves
1970
+ * placeholders against this point-in-time view, so a
1971
+ * `PreToolUse` hook later rewriting event args via
1972
+ * `updatedInput` can introduce placeholders that resolve
1973
+ * cross-batch (against prior runs) without ever picking up
1974
+ * same-turn direct outputs.
1975
+ */
1976
+ const preBatchSnapshot = this.toolOutputRegistry?.snapshot(batchScopeId);
1977
+ if (preBatchSnapshot != null) {
1978
+ for (const entry of eventEntries) if (entry.call.id != null) {
1979
+ const { resolved, unresolved } = preBatchSnapshot.resolve(entry.call.args);
1980
+ preResolvedEventArgs.set(entry.call.id, {
1981
+ resolved,
1982
+ unresolved
1983
+ });
1984
+ }
1985
+ }
1986
+ const directAdditionalContexts = [];
1987
+ const directOutputs = directCalls.length > 0 ? await Promise.all(directCalls.map((call, i) => this.runDirectToolWithLifecycleHooks(call, config, {
1988
+ batchIndex: directIndices[i],
1989
+ turn,
1990
+ batchScopeId,
1991
+ resolvedArgsByCallId,
1992
+ preBatchSnapshot,
1993
+ additionalContextsSink: directAdditionalContexts
1994
+ }))) : [];
1995
+ if (directCalls.length > 0 && directOutputs.length > 0) await this.handleRunToolCompletions(directCalls, directOutputs, config, resolvedArgsByCallId);
1996
+ const eventResult = eventCalls.length > 0 ? await this.dispatchToolEvents(eventCalls, config, {
1997
+ batchIndices: eventIndices,
1998
+ turn,
1999
+ batchScopeId,
2000
+ preResolvedArgs: preResolvedEventArgs,
2001
+ preBatchSnapshot
2002
+ }) : {
2003
+ toolMessages: [],
2004
+ injected: []
2005
+ };
2006
+ const directInjected = directAdditionalContexts.length > 0 ? [new HumanMessage({
2007
+ content: directAdditionalContexts.join("\n\n"),
2008
+ additional_kwargs: {
2009
+ role: "system",
2010
+ source: "hook"
2011
+ }
2012
+ })] : [];
2013
+ outputs = [
2014
+ ...directOutputs,
2015
+ ...eventResult.toolMessages,
2016
+ ...directInjected,
2017
+ ...eventResult.injected
2018
+ ];
2019
+ } else {
2020
+ const preBatchSnapshot = this.toolOutputRegistry?.snapshot(batchScopeId);
2021
+ const directAdditionalContexts = [];
2022
+ const toolOutputs = await Promise.all(filteredCalls.map((call, i) => this.runDirectToolWithLifecycleHooks(call, config, {
2023
+ batchIndex: i,
2024
+ turn,
2025
+ batchScopeId,
2026
+ resolvedArgsByCallId,
2027
+ preBatchSnapshot,
2028
+ additionalContextsSink: directAdditionalContexts
2029
+ })));
2030
+ await this.handleRunToolCompletions(filteredCalls, toolOutputs, config, resolvedArgsByCallId);
2031
+ outputs = directAdditionalContexts.length > 0 ? [...toolOutputs, new HumanMessage({
2032
+ content: directAdditionalContexts.join("\n\n"),
2033
+ additional_kwargs: {
2034
+ role: "system",
2035
+ source: "hook"
2036
+ }
2037
+ })] : toolOutputs;
2038
+ }
2039
+ }
2040
+ if (!outputs.some(isCommand)) return Array.isArray(input) ? outputs : { messages: outputs };
2041
+ const combinedOutputs = [];
2042
+ let parentCommand = null;
2043
+ /**
2044
+ * Collect handoff commands (Commands with string goto and Command.PARENT)
2045
+ * for potential parallel handoff aggregation
2046
+ */
2047
+ const handoffCommands = [];
2048
+ const nonCommandOutputs = [];
2049
+ for (const output of outputs) if (isCommand(output)) if (output.graph === Command.PARENT && Array.isArray(output.goto) && output.goto.every((send) => isSend(send)))
2050
+ /** Aggregate Send-based commands */
2051
+ if (parentCommand) parentCommand.goto.push(...output.goto);
2052
+ else parentCommand = new Command({
2053
+ graph: Command.PARENT,
2054
+ goto: output.goto
2055
+ });
2056
+ else if (output.graph === Command.PARENT) {
2057
+ /**
2058
+ * Handoff Command with destination.
2059
+ * Handle both string ('agent') and array (['agent']) formats.
2060
+ * Collect for potential parallel aggregation.
2061
+ */
2062
+ const goto = output.goto;
2063
+ const isSingleStringDest = typeof goto === "string";
2064
+ const isSingleArrayDest = Array.isArray(goto) && goto.length === 1 && typeof goto[0] === "string";
2065
+ if (isSingleStringDest || isSingleArrayDest) handoffCommands.push(output);
2066
+ else
2067
+ /** Multi-destination or other command - pass through */
2068
+ combinedOutputs.push(output);
2069
+ } else
2070
+ /** Other commands - pass through */
2071
+ combinedOutputs.push(output);
2072
+ else {
2073
+ nonCommandOutputs.push(output);
2074
+ combinedOutputs.push(Array.isArray(input) ? [output] : { messages: [output] });
2075
+ }
2076
+ /**
2077
+ * Handle handoff commands - convert to Send objects for parallel execution
2078
+ * when multiple handoffs are requested
2079
+ */
2080
+ if (handoffCommands.length > 1) {
2081
+ /**
2082
+ * Multiple parallel handoffs - convert to Send objects.
2083
+ * Each Send carries its own state with the appropriate messages.
2084
+ * This enables LLM-initiated parallel execution when calling multiple
2085
+ * transfer tools simultaneously.
2086
+ */
2087
+ /** Collect all destinations for sibling tracking */
2088
+ const allDestinations = handoffCommands.map((cmd) => {
2089
+ const goto = cmd.goto;
2090
+ return typeof goto === "string" ? goto : goto[0];
2091
+ });
2092
+ const sends = handoffCommands.map((cmd, idx) => {
2093
+ const destination = allDestinations[idx];
2094
+ /** Get siblings (other destinations, not this one) */
2095
+ const siblings = allDestinations.filter((d) => d !== destination);
2096
+ /** Add siblings to ToolMessage additional_kwargs */
2097
+ const update = cmd.update;
2098
+ if (update && update.messages) {
2099
+ for (const msg of update.messages) if (msg.getType() === "tool") msg.additional_kwargs.handoff_parallel_siblings = siblings;
2100
+ }
2101
+ return new Send(destination, cmd.update);
2102
+ });
2103
+ const parallelCommand = new Command({
2104
+ graph: Command.PARENT,
2105
+ goto: sends
2106
+ });
2107
+ combinedOutputs.push(parallelCommand);
2108
+ } else if (handoffCommands.length === 1)
2109
+ /** Single handoff - pass through as-is */
2110
+ combinedOutputs.push(handoffCommands[0]);
2111
+ if (parentCommand) combinedOutputs.push(parentCommand);
2112
+ return combinedOutputs;
2113
+ }
2114
+ isSendInput(input) {
2115
+ return typeof input === "object" && input != null && "lg_tool_call" in input;
2116
+ }
2117
+ isMessagesState(input) {
2118
+ return typeof input === "object" && input != null && "messages" in input && Array.isArray(input.messages) && input.messages.every(isBaseMessage);
2119
+ }
2120
+ };
2693
2121
  function areToolCallsInvoked(message, invokedToolIds) {
2694
- if (!invokedToolIds || invokedToolIds.size === 0)
2695
- return false;
2696
- return (message.tool_calls?.every((toolCall) => toolCall.id != null && invokedToolIds.has(toolCall.id)) ?? false);
2122
+ if (!invokedToolIds || invokedToolIds.size === 0) return false;
2123
+ return message.tool_calls?.every((toolCall) => toolCall.id != null && invokedToolIds.has(toolCall.id)) ?? false;
2697
2124
  }
2698
2125
  function toolsCondition(state, toolNode, invokedToolIds) {
2699
- const messages = Array.isArray(state) ? state : state.messages;
2700
- const message = messages[messages.length - 1];
2701
- if (message &&
2702
- 'tool_calls' in message &&
2703
- (message.tool_calls?.length ?? 0) > 0 &&
2704
- !areToolCallsInvoked(message, invokedToolIds)) {
2705
- return toolNode;
2706
- }
2707
- return END;
2126
+ const messages = Array.isArray(state) ? state : state.messages;
2127
+ const message = messages[messages.length - 1];
2128
+ if (message && "tool_calls" in message && (message.tool_calls?.length ?? 0) > 0 && !areToolCallsInvoked(message, invokedToolIds)) return toolNode;
2129
+ return END;
2708
2130
  }
2709
-
2131
+ //#endregion
2710
2132
  export { ToolNode, toolsCondition };
2711
- //# sourceMappingURL=ToolNode.mjs.map
2133
+
2134
+ //# sourceMappingURL=ToolNode.mjs.map