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