@librechat/agents 3.2.21 → 3.2.31

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 (398) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +3 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/events.cjs.map +1 -1
  4. package/dist/cjs/graphs/Graph.cjs +200 -54
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +13 -7
  8. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -1
  9. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  10. package/dist/cjs/hooks/types.cjs.map +1 -1
  11. package/dist/cjs/instrumentation.cjs +2 -2
  12. package/dist/cjs/instrumentation.cjs.map +1 -1
  13. package/dist/cjs/langfuse.cjs +17 -1
  14. package/dist/cjs/langfuse.cjs.map +1 -1
  15. package/dist/cjs/langfuseToolOutputTracing.cjs +2 -2
  16. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  17. package/dist/cjs/llm/anthropic/index.cjs +1 -1
  18. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  19. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  20. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +1 -1
  21. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  22. package/dist/cjs/llm/bedrock/index.cjs +2 -2
  23. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  24. package/dist/cjs/llm/bedrock/toolCache.cjs +8 -5
  25. package/dist/cjs/llm/bedrock/toolCache.cjs.map +1 -1
  26. package/dist/cjs/llm/fake.cjs +16 -14
  27. package/dist/cjs/llm/fake.cjs.map +1 -1
  28. package/dist/cjs/llm/google/index.cjs +22 -0
  29. package/dist/cjs/llm/google/index.cjs.map +1 -1
  30. package/dist/cjs/llm/google/utils/common.cjs +88 -27
  31. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  32. package/dist/cjs/llm/init.cjs +2 -2
  33. package/dist/cjs/llm/invoke.cjs +108 -11
  34. package/dist/cjs/llm/invoke.cjs.map +1 -1
  35. package/dist/cjs/llm/openai/index.cjs +1 -1
  36. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  37. package/dist/cjs/llm/openai/utils/index.cjs +1 -1
  38. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  39. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  40. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  41. package/dist/cjs/main.cjs +1 -0
  42. package/dist/cjs/main.cjs.map +1 -1
  43. package/dist/cjs/messages/cache.cjs +8 -7
  44. package/dist/cjs/messages/cache.cjs.map +1 -1
  45. package/dist/cjs/messages/content.cjs.map +1 -1
  46. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  47. package/dist/cjs/messages/format.cjs +124 -17
  48. package/dist/cjs/messages/format.cjs.map +1 -1
  49. package/dist/cjs/messages/prune.cjs.map +1 -1
  50. package/dist/cjs/messages/reducer.cjs +1 -1
  51. package/dist/cjs/messages/reducer.cjs.map +1 -1
  52. package/dist/cjs/messages/tools.cjs +1 -1
  53. package/dist/cjs/messages/tools.cjs.map +1 -1
  54. package/dist/cjs/openai/index.cjs.map +1 -1
  55. package/dist/cjs/responses/index.cjs.map +1 -1
  56. package/dist/cjs/run.cjs +41 -20
  57. package/dist/cjs/run.cjs.map +1 -1
  58. package/dist/cjs/session/AgentSession.cjs +4 -4
  59. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  60. package/dist/cjs/session/JsonlSessionStore.cjs +2 -2
  61. package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -1
  62. package/dist/cjs/session/handlers.cjs +2 -2
  63. package/dist/cjs/session/handlers.cjs.map +1 -1
  64. package/dist/cjs/stream.cjs +248 -25
  65. package/dist/cjs/stream.cjs.map +1 -1
  66. package/dist/cjs/summarization/node.cjs.map +1 -1
  67. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +1 -1
  68. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  69. package/dist/cjs/tools/Calculator.cjs +1 -1
  70. package/dist/cjs/tools/Calculator.cjs.map +1 -1
  71. package/dist/cjs/tools/CodeExecutor.cjs +1 -1
  72. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  73. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  74. package/dist/cjs/tools/ToolNode.cjs +37 -18
  75. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  76. package/dist/cjs/tools/ToolSearch.cjs +1 -1
  77. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  78. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +7 -4
  79. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
  80. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +4 -4
  81. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -1
  82. package/dist/cjs/tools/handlers.cjs +2 -1
  83. package/dist/cjs/tools/handlers.cjs.map +1 -1
  84. package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -1
  85. package/dist/cjs/tools/local/FileCheckpointer.cjs +2 -1
  86. package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -1
  87. package/dist/cjs/tools/local/LocalCodingTools.cjs +45 -19
  88. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
  89. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +3 -3
  90. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  91. package/dist/cjs/tools/local/LocalExecutionTools.cjs +2 -2
  92. package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -1
  93. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +4 -3
  94. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
  95. package/dist/cjs/tools/local/attachments.cjs +0 -5
  96. package/dist/cjs/tools/local/attachments.cjs.map +1 -1
  97. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +4 -4
  98. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  99. package/dist/cjs/tools/search/firecrawl.cjs +1 -1
  100. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
  101. package/dist/cjs/tools/search/rerankers.cjs +7 -3
  102. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  103. package/dist/cjs/tools/search/tavily-search.cjs +1 -1
  104. package/dist/cjs/tools/search/tavily-search.cjs.map +1 -1
  105. package/dist/cjs/tools/search/utils.cjs +76 -8
  106. package/dist/cjs/tools/search/utils.cjs.map +1 -1
  107. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  108. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  109. package/dist/cjs/utils/handlers.cjs +1 -1
  110. package/dist/cjs/utils/handlers.cjs.map +1 -1
  111. package/dist/cjs/utils/run.cjs +1 -1
  112. package/dist/cjs/utils/run.cjs.map +1 -1
  113. package/dist/esm/agents/AgentContext.mjs +3 -2
  114. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  115. package/dist/esm/events.mjs.map +1 -1
  116. package/dist/esm/graphs/Graph.mjs +200 -54
  117. package/dist/esm/graphs/Graph.mjs.map +1 -1
  118. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  119. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +13 -7
  120. package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -1
  121. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  122. package/dist/esm/hooks/types.mjs.map +1 -1
  123. package/dist/esm/instrumentation.mjs +2 -2
  124. package/dist/esm/instrumentation.mjs.map +1 -1
  125. package/dist/esm/langfuse.mjs +17 -2
  126. package/dist/esm/langfuse.mjs.map +1 -1
  127. package/dist/esm/langfuseToolOutputTracing.mjs +2 -2
  128. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  129. package/dist/esm/llm/anthropic/index.mjs +1 -1
  130. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  131. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  132. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +1 -1
  133. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  134. package/dist/esm/llm/bedrock/index.mjs +2 -2
  135. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  136. package/dist/esm/llm/bedrock/toolCache.mjs +8 -5
  137. package/dist/esm/llm/bedrock/toolCache.mjs.map +1 -1
  138. package/dist/esm/llm/fake.mjs +16 -14
  139. package/dist/esm/llm/fake.mjs.map +1 -1
  140. package/dist/esm/llm/google/index.mjs +23 -1
  141. package/dist/esm/llm/google/index.mjs.map +1 -1
  142. package/dist/esm/llm/google/utils/common.mjs +88 -27
  143. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  144. package/dist/esm/llm/init.mjs +2 -2
  145. package/dist/esm/llm/invoke.mjs +104 -7
  146. package/dist/esm/llm/invoke.mjs.map +1 -1
  147. package/dist/esm/llm/openai/index.mjs +1 -1
  148. package/dist/esm/llm/openai/index.mjs.map +1 -1
  149. package/dist/esm/llm/openai/utils/index.mjs +1 -1
  150. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  151. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  152. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  153. package/dist/esm/main.mjs +1 -1
  154. package/dist/esm/messages/cache.mjs +8 -7
  155. package/dist/esm/messages/cache.mjs.map +1 -1
  156. package/dist/esm/messages/content.mjs.map +1 -1
  157. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  158. package/dist/esm/messages/format.mjs +124 -18
  159. package/dist/esm/messages/format.mjs.map +1 -1
  160. package/dist/esm/messages/prune.mjs.map +1 -1
  161. package/dist/esm/messages/reducer.mjs +1 -1
  162. package/dist/esm/messages/reducer.mjs.map +1 -1
  163. package/dist/esm/messages/tools.mjs +1 -1
  164. package/dist/esm/messages/tools.mjs.map +1 -1
  165. package/dist/esm/openai/index.mjs.map +1 -1
  166. package/dist/esm/responses/index.mjs.map +1 -1
  167. package/dist/esm/run.mjs +41 -20
  168. package/dist/esm/run.mjs.map +1 -1
  169. package/dist/esm/session/AgentSession.mjs +4 -4
  170. package/dist/esm/session/AgentSession.mjs.map +1 -1
  171. package/dist/esm/session/JsonlSessionStore.mjs +2 -2
  172. package/dist/esm/session/JsonlSessionStore.mjs.map +1 -1
  173. package/dist/esm/session/handlers.mjs +2 -2
  174. package/dist/esm/session/handlers.mjs.map +1 -1
  175. package/dist/esm/stream.mjs +248 -25
  176. package/dist/esm/stream.mjs.map +1 -1
  177. package/dist/esm/summarization/node.mjs.map +1 -1
  178. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +1 -1
  179. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  180. package/dist/esm/tools/Calculator.mjs +1 -1
  181. package/dist/esm/tools/Calculator.mjs.map +1 -1
  182. package/dist/esm/tools/CodeExecutor.mjs +1 -1
  183. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  184. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  185. package/dist/esm/tools/ToolNode.mjs +37 -18
  186. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  187. package/dist/esm/tools/ToolSearch.mjs +1 -1
  188. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  189. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +7 -4
  190. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
  191. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +4 -4
  192. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -1
  193. package/dist/esm/tools/handlers.mjs +2 -1
  194. package/dist/esm/tools/handlers.mjs.map +1 -1
  195. package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -1
  196. package/dist/esm/tools/local/FileCheckpointer.mjs +2 -1
  197. package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -1
  198. package/dist/esm/tools/local/LocalCodingTools.mjs +45 -19
  199. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
  200. package/dist/esm/tools/local/LocalExecutionEngine.mjs +3 -3
  201. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  202. package/dist/esm/tools/local/LocalExecutionTools.mjs +2 -2
  203. package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -1
  204. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +4 -3
  205. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
  206. package/dist/esm/tools/local/attachments.mjs +0 -5
  207. package/dist/esm/tools/local/attachments.mjs.map +1 -1
  208. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +4 -4
  209. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  210. package/dist/esm/tools/search/firecrawl.mjs +1 -1
  211. package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
  212. package/dist/esm/tools/search/rerankers.mjs +8 -4
  213. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  214. package/dist/esm/tools/search/tavily-search.mjs +1 -1
  215. package/dist/esm/tools/search/tavily-search.mjs.map +1 -1
  216. package/dist/esm/tools/search/utils.mjs +76 -9
  217. package/dist/esm/tools/search/utils.mjs.map +1 -1
  218. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  219. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  220. package/dist/esm/utils/handlers.mjs +1 -1
  221. package/dist/esm/utils/handlers.mjs.map +1 -1
  222. package/dist/esm/utils/run.mjs +1 -1
  223. package/dist/esm/utils/run.mjs.map +1 -1
  224. package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +1 -1
  225. package/dist/types/events.d.ts +1 -1
  226. package/dist/types/graphs/Graph.d.ts +7 -1
  227. package/dist/types/hooks/executeHooks.d.ts +1 -1
  228. package/dist/types/hooks/types.d.ts +5 -0
  229. package/dist/types/langfuse.d.ts +4 -0
  230. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -1
  231. package/dist/types/llm/anthropic/utils/message_outputs.d.ts +1 -1
  232. package/dist/types/llm/anthropic/utils/output_parsers.d.ts +2 -2
  233. package/dist/types/llm/bedrock/index.d.ts +2 -2
  234. package/dist/types/llm/fake.d.ts +3 -3
  235. package/dist/types/llm/google/index.d.ts +1 -0
  236. package/dist/types/llm/google/types.d.ts +1 -1
  237. package/dist/types/llm/google/utils/common.d.ts +2 -2
  238. package/dist/types/llm/google/utils/tools.d.ts +1 -1
  239. package/dist/types/llm/google/utils/zod_to_genai_parameters.d.ts +1 -1
  240. package/dist/types/llm/openai/index.d.ts +2 -2
  241. package/dist/types/llm/openai/utils/index.d.ts +1 -1
  242. package/dist/types/llm/openrouter/index.d.ts +4 -4
  243. package/dist/types/messages/contextPruning.d.ts +1 -1
  244. package/dist/types/messages/format.d.ts +9 -4
  245. package/dist/types/messages/prune.d.ts +1 -1
  246. package/dist/types/session/JsonlSessionStore.d.ts +1 -1
  247. package/dist/types/session/handlers.d.ts +1 -1
  248. package/dist/types/session/types.d.ts +1 -1
  249. package/dist/types/summarization/node.d.ts +1 -1
  250. package/dist/types/tools/SubagentTool.d.ts +2 -2
  251. package/dist/types/tools/ToolNode.d.ts +9 -2
  252. package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +1 -1
  253. package/dist/types/tools/search/types.d.ts +1 -1
  254. package/dist/types/tools/search/utils.d.ts +11 -0
  255. package/dist/types/types/graph.d.ts +4 -4
  256. package/dist/types/types/llm.d.ts +4 -3
  257. package/dist/types/types/messages.d.ts +1 -1
  258. package/dist/types/types/run.d.ts +6 -6
  259. package/dist/types/types/stream.d.ts +2 -2
  260. package/dist/types/types/tools.d.ts +5 -1
  261. package/dist/types/utils/handlers.d.ts +2 -2
  262. package/dist/types/utils/run.d.ts +1 -1
  263. package/package.json +6 -3
  264. package/src/__tests__/stream.eagerEventExecution.test.ts +543 -6
  265. package/src/agents/AgentContext.ts +2 -2
  266. package/src/agents/__tests__/AgentContext.test.ts +3 -3
  267. package/src/agents/__tests__/promptCacheLiveHelpers.ts +1 -1
  268. package/src/events.ts +1 -1
  269. package/src/graphs/Graph.ts +329 -72
  270. package/src/graphs/MultiAgentGraph.ts +1 -1
  271. package/src/graphs/__tests__/Graph.reasoning.test.ts +919 -6
  272. package/src/graphs/__tests__/MultiAgentGraph.test.ts +1 -1
  273. package/src/graphs/__tests__/composition.smoke.test.ts +1 -1
  274. package/src/hooks/__tests__/HookRegistry.test.ts +1 -1
  275. package/src/hooks/__tests__/compactHooks.test.ts +8 -8
  276. package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +34 -22
  277. package/src/hooks/__tests__/executeHooks.test.ts +3 -3
  278. package/src/hooks/__tests__/integration.test.ts +3 -3
  279. package/src/hooks/__tests__/toolHooks.test.ts +10 -10
  280. package/src/hooks/createWorkspacePolicyHook.ts +17 -14
  281. package/src/hooks/executeHooks.ts +1 -1
  282. package/src/hooks/types.ts +5 -0
  283. package/src/instrumentation.ts +11 -11
  284. package/src/langfuse.ts +35 -1
  285. package/src/langfuseToolOutputTracing.ts +2 -2
  286. package/src/llm/anthropic/index.ts +1 -1
  287. package/src/llm/anthropic/utils/message_inputs.ts +1 -1
  288. package/src/llm/anthropic/utils/message_outputs.ts +3 -5
  289. package/src/llm/anthropic/utils/output_parsers.ts +5 -5
  290. package/src/llm/bedrock/index.ts +4 -4
  291. package/src/llm/bedrock/toolCache.test.ts +48 -9
  292. package/src/llm/bedrock/toolCache.ts +11 -6
  293. package/src/llm/fake.ts +30 -25
  294. package/src/llm/google/index.ts +43 -1
  295. package/src/llm/google/llm.spec.ts +173 -1
  296. package/src/llm/google/types.ts +1 -1
  297. package/src/llm/google/utils/common.ts +154 -45
  298. package/src/llm/google/utils/tools.ts +8 -8
  299. package/src/llm/google/utils/zod_to_genai_parameters.ts +4 -4
  300. package/src/llm/invoke.test.ts +3 -3
  301. package/src/llm/invoke.ts +170 -10
  302. package/src/llm/openai/index.ts +4 -4
  303. package/src/llm/openai/utils/index.ts +14 -14
  304. package/src/llm/openrouter/index.ts +4 -4
  305. package/src/llm/openrouter/reasoning.test.ts +2 -2
  306. package/src/llm/vertexai/fixThoughtSignatures.test.ts +1 -1
  307. package/src/llm/vertexai/index.ts +1 -1
  308. package/src/messages/cache.test.ts +22 -0
  309. package/src/messages/cache.ts +25 -12
  310. package/src/messages/content.ts +1 -1
  311. package/src/messages/contextPruning.ts +1 -1
  312. package/src/messages/format.ts +227 -43
  313. package/src/messages/formatAgentMessages.skills.test.ts +105 -26
  314. package/src/messages/formatAgentMessages.test.ts +841 -10
  315. package/src/messages/labelContentByAgent.test.ts +2 -2
  316. package/src/messages/prune.ts +1 -1
  317. package/src/messages/reducer.ts +1 -1
  318. package/src/messages/tools.ts +1 -1
  319. package/src/openai/__tests__/openai.test.ts +2 -2
  320. package/src/openai/index.ts +1 -1
  321. package/src/responses/__tests__/responses.test.ts +2 -2
  322. package/src/responses/index.ts +1 -1
  323. package/src/run.ts +68 -41
  324. package/src/session/AgentSession.ts +6 -6
  325. package/src/session/JsonlSessionStore.ts +3 -3
  326. package/src/session/__tests__/JsonlSessionStore.test.ts +5 -5
  327. package/src/session/__tests__/handlers.test.ts +2 -2
  328. package/src/session/handlers.ts +5 -5
  329. package/src/session/types.ts +1 -1
  330. package/src/specs/agent-handoffs.test.ts +1 -1
  331. package/src/specs/langfuse-callbacks.test.ts +2 -2
  332. package/src/specs/langfuse-metadata.test.ts +39 -0
  333. package/src/specs/langfuse-tool-output-tracing.test.ts +1 -1
  334. package/src/specs/multi-agent-summarization.test.ts +4 -4
  335. package/src/specs/subagent.test.ts +3 -3
  336. package/src/specs/summarization-unit.test.ts +1 -1
  337. package/src/specs/thinking-handoff.test.ts +1 -1
  338. package/src/splitStream.test.ts +48 -0
  339. package/src/stream.test.ts +53 -3
  340. package/src/stream.ts +450 -39
  341. package/src/summarization/__tests__/aggregator.test.ts +2 -2
  342. package/src/summarization/__tests__/node.test.ts +2 -2
  343. package/src/summarization/node.ts +1 -1
  344. package/src/tools/BashProgrammaticToolCalling.ts +5 -5
  345. package/src/tools/Calculator.ts +1 -1
  346. package/src/tools/CodeExecutor.ts +2 -4
  347. package/src/tools/SubagentTool.ts +2 -2
  348. package/src/tools/ToolNode.ts +37 -16
  349. package/src/tools/ToolSearch.ts +1 -1
  350. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +4 -4
  351. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +12 -12
  352. package/src/tools/__tests__/LocalExecutionTools.test.ts +125 -93
  353. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +29 -5
  354. package/src/tools/__tests__/ReadFile.test.ts +1 -1
  355. package/src/tools/__tests__/SkillTool.test.ts +4 -4
  356. package/src/tools/__tests__/SubagentExecutor.test.ts +17 -13
  357. package/src/tools/__tests__/SubagentTool.test.ts +2 -2
  358. package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +1 -1
  359. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +2 -5
  360. package/src/tools/__tests__/ToolNode.session.test.ts +1 -1
  361. package/src/tools/__tests__/ToolSearch.test.ts +1 -1
  362. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +1 -1
  363. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +35 -32
  364. package/src/tools/__tests__/directToolHooks.test.ts +41 -0
  365. package/src/tools/__tests__/handlers.test.ts +2 -2
  366. package/src/tools/__tests__/hitl.test.ts +11 -11
  367. package/src/tools/__tests__/localToolNames.test.ts +8 -6
  368. package/src/tools/__tests__/skillCatalog.test.ts +1 -1
  369. package/src/tools/__tests__/subagentHooks.test.ts +20 -10
  370. package/src/tools/__tests__/workspaceSeam.test.ts +20 -7
  371. package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +9 -6
  372. package/src/tools/cloudflare/CloudflareSandboxTools.ts +19 -19
  373. package/src/tools/handlers.ts +5 -5
  374. package/src/tools/local/CompileCheckTool.ts +4 -7
  375. package/src/tools/local/FileCheckpointer.ts +6 -5
  376. package/src/tools/local/LocalCodingTools.ts +100 -45
  377. package/src/tools/local/LocalExecutionEngine.ts +5 -5
  378. package/src/tools/local/LocalExecutionTools.ts +9 -9
  379. package/src/tools/local/LocalProgrammaticToolCalling.ts +5 -4
  380. package/src/tools/local/attachments.ts +0 -6
  381. package/src/tools/local/resolveLocalExecutionTools.ts +15 -15
  382. package/src/tools/search/firecrawl.ts +1 -1
  383. package/src/tools/search/jina-reranker.test.ts +148 -37
  384. package/src/tools/search/rerankers.ts +14 -4
  385. package/src/tools/search/tavily-search.ts +2 -2
  386. package/src/tools/search/types.ts +1 -1
  387. package/src/tools/search/utils.ts +99 -9
  388. package/src/tools/subagent/SubagentExecutor.ts +12 -6
  389. package/src/types/graph.ts +12 -12
  390. package/src/types/llm.ts +7 -6
  391. package/src/types/messages.ts +1 -1
  392. package/src/types/run.ts +7 -7
  393. package/src/types/stream.ts +2 -2
  394. package/src/types/tools.ts +5 -1
  395. package/src/utils/handlers.ts +2 -2
  396. package/src/utils/llmConfig.ts +1 -1
  397. package/src/utils/logging.ts +20 -10
  398. package/src/utils/run.ts +2 -2
@@ -1,16 +1,26 @@
1
- import { AIMessageChunk, HumanMessage } from '@langchain/core/messages';
2
1
  import { ChatGenerationChunk } from '@langchain/core/outputs';
3
2
  import { FakeListChatModel } from '@langchain/core/utils/testing';
3
+ import { AIMessageChunk, HumanMessage } from '@langchain/core/messages';
4
4
  import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
5
- import type { RunnableConfig } from '@langchain/core/runnables';
6
5
  import type { BaseMessage, UsageMetadata } from '@langchain/core/messages';
6
+ import type { RunnableConfig } from '@langchain/core/runnables';
7
+ import type { OpenAIClient } from '@langchain/openai';
7
8
  import type * as t from '@/types';
8
- import { ContentTypes, GraphEvents, Providers } from '@/common';
9
- import { createContentAggregator } from '@/stream';
9
+ import { ContentTypes, GraphEvents, Providers, StepTypes } from '@/common';
10
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
10
11
  import { ModelEndHandler, ToolEndHandler } from '@/events';
12
+ import { toLangChainContent } from '@/messages/langchain';
13
+ import { ChatOpenRouter } from '@/llm/openrouter';
11
14
  import { Run } from '@/run';
12
15
 
13
16
  type ReasoningKey = 'reasoning_content' | 'reasoning';
17
+ type StreamingCompletionBackedModel = {
18
+ completions: {
19
+ completionWithRetry: () => Promise<
20
+ AsyncIterable<OpenAIClient.Chat.Completions.ChatCompletionChunk>
21
+ >;
22
+ };
23
+ };
14
24
 
15
25
  class InvokeOnlyReasoningModel implements t.ChatModel {
16
26
  constructor(
@@ -93,6 +103,25 @@ class CallbackStreamingReasoningModel extends FakeListChatModel {
93
103
  }
94
104
  }
95
105
 
106
+ function createOpenRouterStreamChunk(
107
+ content: string,
108
+ finishReason: OpenAIClient.Chat.Completions.ChatCompletionChunk.Choice['finish_reason'] = null
109
+ ): OpenAIClient.Chat.Completions.ChatCompletionChunk {
110
+ return {
111
+ id: 'chatcmpl-openrouter-test',
112
+ object: 'chat.completion.chunk',
113
+ created: 0,
114
+ model: 'google/gemini-test',
115
+ choices: [
116
+ {
117
+ index: 0,
118
+ delta: { content },
119
+ finish_reason: finishReason,
120
+ },
121
+ ],
122
+ };
123
+ }
124
+
96
125
  function createReasoningChunk(
97
126
  reasoningKey: ReasoningKey,
98
127
  reasoningText: string
@@ -105,7 +134,9 @@ function createReasoningChunk(
105
134
  });
106
135
  }
107
136
 
108
- function createOpenAIReasoningSummaryChunk(reasoningText: string): AIMessageChunk {
137
+ function createOpenAIReasoningSummaryChunk(
138
+ reasoningText: string
139
+ ): AIMessageChunk {
109
140
  return new AIMessageChunk({
110
141
  content: '',
111
142
  additional_kwargs: {
@@ -123,7 +154,10 @@ function createReasoningHandlers(
123
154
  ): Record<string | GraphEvents, t.EventHandler> {
124
155
  return {
125
156
  [GraphEvents.ON_RUN_STEP]: {
126
- handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData): void => {
157
+ handle: (
158
+ event: GraphEvents.ON_RUN_STEP,
159
+ data: t.StreamEventData
160
+ ): void => {
127
161
  aggregateContent({ event, data: data as t.RunStep });
128
162
  },
129
163
  },
@@ -326,6 +360,176 @@ describe('StandardGraph final response reasoning fallback', () => {
326
360
  ]);
327
361
  });
328
362
 
363
+ it('emits final text from invoke-only Gemini server-side context responses', async () => {
364
+ const text = 'Search complete.';
365
+ const { contentParts, aggregateContent } = createContentAggregator();
366
+ const run = await Run.create<t.IState>({
367
+ runId: 'reasoning-fallback-gemini-server-side-context',
368
+ graphConfig: {
369
+ type: 'standard',
370
+ llmConfig: {
371
+ provider: Providers.GOOGLE,
372
+ disableStreaming: true,
373
+ streamUsage: false,
374
+ },
375
+ },
376
+ returnContent: true,
377
+ skipCleanup: true,
378
+ customHandlers: createReasoningHandlers(aggregateContent, []),
379
+ });
380
+
381
+ if (!run.Graph) {
382
+ throw new Error('Expected graph to be initialized');
383
+ }
384
+
385
+ const messageContent: t.MessageContentComplex[] = [
386
+ {
387
+ type: 'toolCall',
388
+ toolCall: {
389
+ id: 'server-search-1',
390
+ name: 'google_search',
391
+ args: {},
392
+ },
393
+ },
394
+ { type: ContentTypes.TEXT, text },
395
+ {
396
+ type: 'toolResponse',
397
+ toolResponse: {
398
+ id: 'server-search-1',
399
+ name: 'google_search',
400
+ response: { results: [] },
401
+ },
402
+ },
403
+ ];
404
+ run.Graph.overrideModel = new InvokeOnlyMessageModel(
405
+ new AIMessageChunk({
406
+ content: toLangChainContent(messageContent),
407
+ })
408
+ );
409
+
410
+ const finalContentParts = await run.processStream(
411
+ { messages: [new HumanMessage('search and answer')] },
412
+ {
413
+ ...config,
414
+ configurable: {
415
+ thread_id: 'reasoning-fallback-gemini-server-side-context',
416
+ },
417
+ }
418
+ );
419
+
420
+ expect(finalContentParts).toEqual(messageContent);
421
+ expect(contentParts).toEqual(messageContent);
422
+ });
423
+
424
+ it('does not preserve Gemini server-side context blocks for non-Google invoke fallbacks', async () => {
425
+ const text = 'Search complete.';
426
+ const { contentParts, aggregateContent } = createContentAggregator();
427
+ const run = await Run.create<t.IState>({
428
+ runId: 'reasoning-fallback-non-google-server-side-shape',
429
+ graphConfig: {
430
+ type: 'standard',
431
+ llmConfig,
432
+ },
433
+ returnContent: true,
434
+ skipCleanup: true,
435
+ customHandlers: createReasoningHandlers(aggregateContent, []),
436
+ });
437
+
438
+ if (!run.Graph) {
439
+ throw new Error('Expected graph to be initialized');
440
+ }
441
+
442
+ const messageContent: t.MessageContentComplex[] = [
443
+ {
444
+ type: 'toolCall',
445
+ toolCall: {
446
+ id: 'server-search-1',
447
+ name: 'google_search',
448
+ args: {},
449
+ },
450
+ },
451
+ { type: ContentTypes.TEXT, text },
452
+ {
453
+ type: 'toolResponse',
454
+ toolResponse: {
455
+ id: 'server-search-1',
456
+ name: 'google_search',
457
+ response: { results: [] },
458
+ },
459
+ },
460
+ ];
461
+ run.Graph.overrideModel = new InvokeOnlyMessageModel(
462
+ new AIMessageChunk({
463
+ content: toLangChainContent(messageContent),
464
+ })
465
+ );
466
+
467
+ const finalContentParts = await run.processStream(
468
+ { messages: [new HumanMessage('search and answer')] },
469
+ {
470
+ ...config,
471
+ configurable: {
472
+ thread_id: 'reasoning-fallback-non-google-server-side-shape',
473
+ },
474
+ }
475
+ );
476
+
477
+ expect(finalContentParts).toEqual(messageContent);
478
+ expect(contentParts).toEqual([]);
479
+ });
480
+
481
+ it('uses the final fallback for one-shot disableStreaming mixed chunks', async () => {
482
+ const text = 'Cloudflare content check.';
483
+ const reasoningText = 'Plan the exact final wording.';
484
+ const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
485
+ const messageDeltas: t.MessageDeltaEvent[] = [];
486
+ const { contentParts, aggregateContent } = createContentAggregator();
487
+ const run = await Run.create<t.IState>({
488
+ runId: 'reasoning-fallback-disable-streaming-mixed-final-chunk',
489
+ graphConfig: {
490
+ type: 'standard',
491
+ llmConfig,
492
+ },
493
+ returnContent: true,
494
+ skipCleanup: true,
495
+ customHandlers: createReasoningHandlers(
496
+ aggregateContent,
497
+ reasoningDeltas,
498
+ messageDeltas
499
+ ),
500
+ });
501
+
502
+ if (!run.Graph) {
503
+ throw new Error('Expected graph to be initialized');
504
+ }
505
+
506
+ run.Graph.overrideModel = new StreamingReasoningModel([
507
+ new AIMessageChunk({
508
+ content: text,
509
+ additional_kwargs: {
510
+ reasoning_content: reasoningText,
511
+ },
512
+ }),
513
+ ]);
514
+
515
+ await run.processStream(
516
+ { messages: [new HumanMessage('return mixed final chunk')] },
517
+ {
518
+ ...config,
519
+ configurable: {
520
+ thread_id: 'reasoning-fallback-disable-streaming-mixed-final-chunk',
521
+ },
522
+ }
523
+ );
524
+
525
+ expect(reasoningDeltas).toHaveLength(1);
526
+ expect(messageDeltas).toHaveLength(1);
527
+ expect(contentParts).toEqual([
528
+ { type: ContentTypes.THINK, think: reasoningText },
529
+ { type: ContentTypes.TEXT, text },
530
+ ]);
531
+ });
532
+
329
533
  it('returns reasoning content without a custom aggregator', async () => {
330
534
  const reasoningText = 'Reasoning should persist for returnContent.';
331
535
  const run = await Run.create<t.IState>({
@@ -555,6 +759,715 @@ describe('StandardGraph final response reasoning fallback', () => {
555
759
  }
556
760
  );
557
761
 
762
+ it('streams OpenRouter reasoning_content before visible text', async () => {
763
+ const text = '391.';
764
+ const reasoningText = 'Use the difference of squares.';
765
+ const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
766
+ const messageDeltas: t.MessageDeltaEvent[] = [];
767
+ const { contentParts, aggregateContent } = createContentAggregator();
768
+ const run = await Run.create<t.IState>({
769
+ runId: 'reasoning-fallback-openrouter-reasoning-content-stream',
770
+ graphConfig: {
771
+ type: 'standard',
772
+ llmConfig: {
773
+ provider: Providers.OPENROUTER,
774
+ streamUsage: false,
775
+ },
776
+ reasoningKey: 'reasoning',
777
+ },
778
+ returnContent: true,
779
+ skipCleanup: true,
780
+ customHandlers: createReasoningHandlers(
781
+ aggregateContent,
782
+ reasoningDeltas,
783
+ messageDeltas
784
+ ),
785
+ });
786
+
787
+ if (!run.Graph) {
788
+ throw new Error('Expected graph to be initialized');
789
+ }
790
+
791
+ run.Graph.overrideModel = new StreamingReasoningModel([
792
+ createReasoningChunk('reasoning_content', reasoningText),
793
+ new AIMessageChunk({ content: text }),
794
+ ]);
795
+
796
+ await run.processStream(
797
+ { messages: [new HumanMessage('stream OpenRouter reasoning_content')] },
798
+ {
799
+ ...config,
800
+ configurable: {
801
+ thread_id: 'reasoning-fallback-openrouter-reasoning-content-stream',
802
+ },
803
+ }
804
+ );
805
+
806
+ expect(reasoningDeltas).toHaveLength(1);
807
+ expect(messageDeltas).toHaveLength(1);
808
+ expect(contentParts).toEqual([
809
+ { type: ContentTypes.THINK, think: reasoningText },
810
+ { type: ContentTypes.TEXT, text },
811
+ ]);
812
+ });
813
+
814
+ it('does not replay streamed OpenRouter text when the final chunk has reasoning_details', async () => {
815
+ const text = '391.';
816
+ const reasoningText = '17 times 23 equals 391.';
817
+ const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
818
+ const messageDeltas: t.MessageDeltaEvent[] = [];
819
+ const { contentParts, aggregateContent } = createContentAggregator();
820
+ const run = await Run.create<t.IState>({
821
+ runId: 'reasoning-fallback-openrouter-final-details-stream',
822
+ graphConfig: {
823
+ type: 'standard',
824
+ llmConfig: {
825
+ provider: Providers.OPENROUTER,
826
+ streamUsage: false,
827
+ },
828
+ reasoningKey: 'reasoning',
829
+ },
830
+ returnContent: true,
831
+ skipCleanup: true,
832
+ customHandlers: createReasoningHandlers(
833
+ aggregateContent,
834
+ reasoningDeltas,
835
+ messageDeltas
836
+ ),
837
+ });
838
+
839
+ if (!run.Graph) {
840
+ throw new Error('Expected graph to be initialized');
841
+ }
842
+
843
+ run.Graph.overrideModel = new StreamingReasoningModel([
844
+ new AIMessageChunk({
845
+ content: text,
846
+ additional_kwargs: {
847
+ reasoning_details: [{ type: 'reasoning.text', text: reasoningText }],
848
+ },
849
+ }),
850
+ ]);
851
+
852
+ await run.processStream(
853
+ { messages: [new HumanMessage('think briefly, what is 17 x 23?')] },
854
+ {
855
+ ...config,
856
+ configurable: {
857
+ thread_id: 'reasoning-fallback-openrouter-final-details-stream',
858
+ },
859
+ }
860
+ );
861
+
862
+ expect(reasoningDeltas).toHaveLength(1);
863
+ expect(messageDeltas).toHaveLength(1);
864
+ expect(contentParts).toEqual([
865
+ { type: ContentTypes.THINK, think: reasoningText },
866
+ { type: ContentTypes.TEXT, text },
867
+ ]);
868
+ });
869
+
870
+ it('does not keep an OpenRouter streamed text replay before final reasoning_details fallback', async () => {
871
+ const text = '391.';
872
+ const reasoningText = '17 times 23 equals 391.';
873
+ const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
874
+ const messageDeltas: t.MessageDeltaEvent[] = [];
875
+ const { contentParts, aggregateContent } = createContentAggregator();
876
+ const run = await Run.create<t.IState>({
877
+ runId: 'reasoning-fallback-openrouter-streamed-text-final-details',
878
+ graphConfig: {
879
+ type: 'standard',
880
+ llmConfig: {
881
+ provider: Providers.OPENROUTER,
882
+ streamUsage: false,
883
+ },
884
+ reasoningKey: 'reasoning',
885
+ },
886
+ returnContent: true,
887
+ skipCleanup: true,
888
+ customHandlers: createReasoningHandlers(
889
+ aggregateContent,
890
+ reasoningDeltas,
891
+ messageDeltas
892
+ ),
893
+ });
894
+
895
+ if (!run.Graph) {
896
+ throw new Error('Expected graph to be initialized');
897
+ }
898
+
899
+ run.Graph.overrideModel = new StreamingReasoningModel([
900
+ new AIMessageChunk({ content: text }),
901
+ new AIMessageChunk({
902
+ content: text,
903
+ additional_kwargs: {
904
+ reasoning_details: [{ type: 'reasoning.text', text: reasoningText }],
905
+ },
906
+ }),
907
+ ]);
908
+
909
+ const finalContentParts = await run.processStream(
910
+ { messages: [new HumanMessage('think briefly, what is 17 x 23?')] },
911
+ {
912
+ ...config,
913
+ configurable: {
914
+ thread_id:
915
+ 'reasoning-fallback-openrouter-streamed-text-final-details',
916
+ },
917
+ }
918
+ );
919
+
920
+ expect(reasoningDeltas).toHaveLength(0);
921
+ expect(messageDeltas).toHaveLength(1);
922
+ expect(contentParts).toEqual([{ type: ContentTypes.TEXT, text }]);
923
+ expect(finalContentParts).toEqual([
924
+ { type: ContentTypes.THINK, think: reasoningText },
925
+ { type: ContentTypes.TEXT, text },
926
+ ]);
927
+ expect(run.getRunMessages()?.[0]?.content).toBe(text);
928
+ });
929
+
930
+ it('does not replay streamed text when late OpenRouter reasoning_content arrives', async () => {
931
+ const text = '391.';
932
+ const reasoningText = 'Confirm the arithmetic after visible text.';
933
+ const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
934
+ const messageDeltas: t.MessageDeltaEvent[] = [];
935
+ const { contentParts, aggregateContent } = createContentAggregator();
936
+ const run = await Run.create<t.IState>({
937
+ runId: 'reasoning-fallback-openrouter-text-before-reasoning-content',
938
+ graphConfig: {
939
+ type: 'standard',
940
+ llmConfig: {
941
+ provider: Providers.OPENROUTER,
942
+ streamUsage: false,
943
+ },
944
+ reasoningKey: 'reasoning',
945
+ },
946
+ returnContent: true,
947
+ skipCleanup: true,
948
+ customHandlers: createReasoningHandlers(
949
+ aggregateContent,
950
+ reasoningDeltas,
951
+ messageDeltas
952
+ ),
953
+ });
954
+
955
+ if (!run.Graph) {
956
+ throw new Error('Expected graph to be initialized');
957
+ }
958
+
959
+ run.Graph.overrideModel = new StreamingReasoningModel([
960
+ new AIMessageChunk({ content: text }),
961
+ createReasoningChunk('reasoning_content', reasoningText),
962
+ ]);
963
+
964
+ const finalContentParts = await run.processStream(
965
+ { messages: [new HumanMessage('think briefly, what is 17 x 23?')] },
966
+ {
967
+ ...config,
968
+ configurable: {
969
+ thread_id:
970
+ 'reasoning-fallback-openrouter-text-before-reasoning-content',
971
+ },
972
+ }
973
+ );
974
+
975
+ expect(reasoningDeltas).toHaveLength(0);
976
+ expect(messageDeltas).toHaveLength(1);
977
+ expect(contentParts).toEqual([{ type: ContentTypes.TEXT, text }]);
978
+ expect(finalContentParts).toEqual([
979
+ { type: ContentTypes.THINK, think: reasoningText },
980
+ { type: ContentTypes.TEXT, text },
981
+ ]);
982
+ expect(run.getRunMessages()?.[0]?.content).toBe(text);
983
+ });
984
+
985
+ it('does not replay streamed text when the final fallback is on the reasoning key', async () => {
986
+ const text = '391.';
987
+ const reasoningText = 'Confirm the arithmetic after visible text.';
988
+ const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
989
+ const messageDeltas: t.MessageDeltaEvent[] = [];
990
+ const { contentParts, aggregateContent } = createContentAggregator();
991
+ const run = await Run.create<t.IState>({
992
+ runId: 'reasoning-fallback-openrouter-text-before-reasoning-key',
993
+ graphConfig: {
994
+ type: 'standard',
995
+ llmConfig: {
996
+ provider: Providers.OPENROUTER,
997
+ streamUsage: false,
998
+ },
999
+ reasoningKey: 'reasoning',
1000
+ },
1001
+ returnContent: true,
1002
+ skipCleanup: true,
1003
+ customHandlers: createReasoningHandlers(
1004
+ aggregateContent,
1005
+ reasoningDeltas,
1006
+ messageDeltas
1007
+ ),
1008
+ });
1009
+
1010
+ if (!run.Graph) {
1011
+ throw new Error('Expected graph to be initialized');
1012
+ }
1013
+
1014
+ run.Graph.overrideModel = new StreamingReasoningModel([
1015
+ new AIMessageChunk({ content: text }),
1016
+ createReasoningChunk('reasoning', reasoningText),
1017
+ ]);
1018
+
1019
+ const finalContentParts = await run.processStream(
1020
+ { messages: [new HumanMessage('think briefly, what is 17 x 23?')] },
1021
+ {
1022
+ ...config,
1023
+ configurable: {
1024
+ thread_id: 'reasoning-fallback-openrouter-text-before-reasoning-key',
1025
+ },
1026
+ }
1027
+ );
1028
+
1029
+ expect(reasoningDeltas).toHaveLength(0);
1030
+ expect(messageDeltas).toHaveLength(1);
1031
+ expect(contentParts).toEqual([{ type: ContentTypes.TEXT, text }]);
1032
+ expect(finalContentParts).toEqual([
1033
+ { type: ContentTypes.THINK, think: reasoningText },
1034
+ { type: ContentTypes.TEXT, text },
1035
+ ]);
1036
+ expect(run.getRunMessages()?.[0]?.content).toBe(text);
1037
+ });
1038
+
1039
+ it('keeps new OpenRouter final text suffixes that carry reasoning_details', async () => {
1040
+ const firstText = 'Hello ';
1041
+ const replayWithSuffix = 'Hello world';
1042
+ const reasoningText = 'The greeting needs one more word.';
1043
+ const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
1044
+ const messageDeltas: t.MessageDeltaEvent[] = [];
1045
+ const { contentParts, aggregateContent } = createContentAggregator();
1046
+ const run = await Run.create<t.IState>({
1047
+ runId: 'reasoning-fallback-openrouter-final-suffix-details',
1048
+ graphConfig: {
1049
+ type: 'standard',
1050
+ llmConfig: {
1051
+ provider: Providers.OPENROUTER,
1052
+ streamUsage: false,
1053
+ },
1054
+ reasoningKey: 'reasoning',
1055
+ },
1056
+ returnContent: true,
1057
+ skipCleanup: true,
1058
+ customHandlers: createReasoningHandlers(
1059
+ aggregateContent,
1060
+ reasoningDeltas,
1061
+ messageDeltas
1062
+ ),
1063
+ });
1064
+
1065
+ if (!run.Graph) {
1066
+ throw new Error('Expected graph to be initialized');
1067
+ }
1068
+
1069
+ run.Graph.overrideModel = new StreamingReasoningModel([
1070
+ new AIMessageChunk({ content: firstText }),
1071
+ new AIMessageChunk({
1072
+ content: replayWithSuffix,
1073
+ additional_kwargs: {
1074
+ reasoning_details: [{ type: 'reasoning.text', text: reasoningText }],
1075
+ },
1076
+ }),
1077
+ ]);
1078
+
1079
+ const finalContentParts = await run.processStream(
1080
+ { messages: [new HumanMessage('finish the greeting')] },
1081
+ {
1082
+ ...config,
1083
+ configurable: {
1084
+ thread_id: 'reasoning-fallback-openrouter-final-suffix-details',
1085
+ },
1086
+ }
1087
+ );
1088
+
1089
+ expect(reasoningDeltas).toHaveLength(0);
1090
+ expect(messageDeltas).toHaveLength(2);
1091
+ expect(messageDeltas[1].delta.content?.[0]).toEqual({
1092
+ type: ContentTypes.TEXT,
1093
+ text: 'world',
1094
+ });
1095
+ expect(contentParts).toEqual([
1096
+ { type: ContentTypes.TEXT, text: replayWithSuffix },
1097
+ ]);
1098
+ expect(finalContentParts).toEqual([
1099
+ { type: ContentTypes.THINK, think: reasoningText },
1100
+ { type: ContentTypes.TEXT, text: replayWithSuffix },
1101
+ ]);
1102
+ expect(run.getRunMessages()?.[0]?.content).toBe(replayWithSuffix);
1103
+ });
1104
+
1105
+ it('keeps post-tool final text when an earlier invocation streamed text', async () => {
1106
+ const streamedBeforeTool = 'I will check that. ';
1107
+ const finalAfterTool = 'The answer is 391.';
1108
+ const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
1109
+ const messageDeltas: t.MessageDeltaEvent[] = [];
1110
+ const { aggregateContent } = createContentAggregator();
1111
+ const run = await Run.create<t.IState>({
1112
+ runId: 'reasoning-fallback-post-tool-final-text',
1113
+ graphConfig: {
1114
+ type: 'standard',
1115
+ llmConfig: {
1116
+ provider: Providers.OPENAI,
1117
+ disableStreaming: true,
1118
+ streamUsage: false,
1119
+ },
1120
+ },
1121
+ returnContent: true,
1122
+ skipCleanup: true,
1123
+ customHandlers: createReasoningHandlers(
1124
+ aggregateContent,
1125
+ reasoningDeltas,
1126
+ messageDeltas
1127
+ ),
1128
+ });
1129
+
1130
+ if (!run.Graph) {
1131
+ throw new Error('Expected graph to be initialized');
1132
+ }
1133
+
1134
+ const graph = run.Graph;
1135
+ const metadata = {
1136
+ thread_id: 'reasoning-fallback-post-tool-final-text',
1137
+ langgraph_node: 'agent=default',
1138
+ langgraph_step: 1,
1139
+ checkpoint_ns: '',
1140
+ };
1141
+ const runConfig = {
1142
+ ...config,
1143
+ configurable: {
1144
+ thread_id: 'reasoning-fallback-post-tool-final-text',
1145
+ },
1146
+ metadata,
1147
+ };
1148
+ graph.config = runConfig;
1149
+ const streamedStepKey = graph.getStepKey(metadata);
1150
+ await graph.dispatchRunStep(
1151
+ streamedStepKey,
1152
+ {
1153
+ type: StepTypes.MESSAGE_CREATION,
1154
+ message_creation: { message_id: 'msg-before-tool' },
1155
+ },
1156
+ metadata
1157
+ );
1158
+ await graph.dispatchMessageDelta(
1159
+ graph.getStepIdByKey(streamedStepKey),
1160
+ {
1161
+ content: [
1162
+ {
1163
+ type: ContentTypes.TEXT,
1164
+ text: streamedBeforeTool,
1165
+ },
1166
+ ],
1167
+ },
1168
+ metadata
1169
+ );
1170
+
1171
+ graph.invokedToolIds = new Set(['call_calculator']);
1172
+ graph.overrideModel = new InvokeOnlyMessageModel(
1173
+ new AIMessageChunk({ content: finalAfterTool })
1174
+ );
1175
+
1176
+ await graph.createCallModel('default')(
1177
+ { messages: [new HumanMessage('continue after tool')] },
1178
+ runConfig
1179
+ );
1180
+
1181
+ expect(messageDeltas.map((delta) => delta.delta.content?.[0])).toEqual([
1182
+ {
1183
+ type: ContentTypes.TEXT,
1184
+ text: streamedBeforeTool,
1185
+ },
1186
+ {
1187
+ type: ContentTypes.TEXT,
1188
+ text: finalAfterTool,
1189
+ },
1190
+ ]);
1191
+ });
1192
+
1193
+ it('does not replay streamed text block variants from the final fallback', async () => {
1194
+ const text = 'Variant text.';
1195
+ const textDeltaContent: t.MessageDelta['content'] = [
1196
+ { type: 'text_delta', text },
1197
+ ];
1198
+ const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
1199
+ const messageDeltas: t.MessageDeltaEvent[] = [];
1200
+ const { aggregateContent } = createContentAggregator();
1201
+ const run = await Run.create<t.IState>({
1202
+ runId: 'reasoning-fallback-text-delta-block',
1203
+ graphConfig: {
1204
+ type: 'standard',
1205
+ llmConfig: {
1206
+ provider: Providers.OPENAI,
1207
+ disableStreaming: true,
1208
+ streamUsage: false,
1209
+ },
1210
+ },
1211
+ returnContent: true,
1212
+ skipCleanup: true,
1213
+ customHandlers: createReasoningHandlers(
1214
+ aggregateContent,
1215
+ reasoningDeltas,
1216
+ messageDeltas
1217
+ ),
1218
+ });
1219
+
1220
+ if (!run.Graph) {
1221
+ throw new Error('Expected graph to be initialized');
1222
+ }
1223
+
1224
+ const graph = run.Graph;
1225
+ const metadata = {
1226
+ thread_id: 'reasoning-fallback-text-delta-block',
1227
+ langgraph_node: 'agent=default',
1228
+ langgraph_step: 1,
1229
+ checkpoint_ns: '',
1230
+ };
1231
+ const runConfig = {
1232
+ ...config,
1233
+ configurable: {
1234
+ thread_id: 'reasoning-fallback-text-delta-block',
1235
+ },
1236
+ metadata,
1237
+ };
1238
+ graph.config = runConfig;
1239
+ const stepKey = graph.getStepKey(metadata);
1240
+ await graph.dispatchRunStep(
1241
+ stepKey,
1242
+ {
1243
+ type: StepTypes.MESSAGE_CREATION,
1244
+ message_creation: { message_id: 'msg-text-delta-block' },
1245
+ },
1246
+ metadata
1247
+ );
1248
+ await graph.dispatchMessageDelta(
1249
+ graph.getStepIdByKey(stepKey),
1250
+ { content: textDeltaContent },
1251
+ metadata
1252
+ );
1253
+
1254
+ graph.overrideModel = new InvokeOnlyMessageModel(
1255
+ new AIMessageChunk({ content: textDeltaContent as never })
1256
+ );
1257
+
1258
+ await graph.createCallModel('default')(
1259
+ { messages: [new HumanMessage('finish text delta block')] },
1260
+ runConfig
1261
+ );
1262
+
1263
+ expect(messageDeltas.map((delta) => delta.delta.content?.[0])).toEqual([
1264
+ { type: 'text_delta', text },
1265
+ ]);
1266
+ });
1267
+
1268
+ it('sanitizes OpenRouter final reasoning chunks for registered stream handlers', async () => {
1269
+ const firstText = 'Hello ';
1270
+ const replayWithSuffix = 'Hello world';
1271
+ const reasoningText = 'The greeting needs one more word.';
1272
+ const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
1273
+ const messageDeltas: t.MessageDeltaEvent[] = [];
1274
+ const { aggregateContent } = createContentAggregator();
1275
+ const streamHandler = new ChatModelStreamHandler();
1276
+ const run = await Run.create<t.IState>({
1277
+ runId: 'reasoning-fallback-openrouter-registered-handler-suffix',
1278
+ graphConfig: {
1279
+ type: 'standard',
1280
+ llmConfig: {
1281
+ provider: Providers.OPENROUTER,
1282
+ streamUsage: false,
1283
+ },
1284
+ reasoningKey: 'reasoning',
1285
+ },
1286
+ returnContent: true,
1287
+ skipCleanup: true,
1288
+ customHandlers: {
1289
+ [GraphEvents.CHAT_MODEL_STREAM]: streamHandler,
1290
+ ...createReasoningHandlers(
1291
+ aggregateContent,
1292
+ reasoningDeltas,
1293
+ messageDeltas
1294
+ ),
1295
+ },
1296
+ });
1297
+
1298
+ if (!run.Graph) {
1299
+ throw new Error('Expected graph to be initialized');
1300
+ }
1301
+
1302
+ run.Graph.overrideModel = new StreamingReasoningModel([
1303
+ new AIMessageChunk({ content: firstText }),
1304
+ new AIMessageChunk({
1305
+ content: replayWithSuffix,
1306
+ additional_kwargs: {
1307
+ reasoning_details: [{ type: 'reasoning.text', text: reasoningText }],
1308
+ },
1309
+ }),
1310
+ ]);
1311
+
1312
+ const finalContentParts = await run.processStream(
1313
+ { messages: [new HumanMessage('stream OpenRouter suffix once')] },
1314
+ {
1315
+ ...config,
1316
+ configurable: {
1317
+ thread_id: 'reasoning-fallback-openrouter-registered-handler-suffix',
1318
+ },
1319
+ }
1320
+ );
1321
+
1322
+ expect(reasoningDeltas).toHaveLength(0);
1323
+ expect(messageDeltas.map((delta) => delta.delta.content?.[0])).toEqual([
1324
+ { type: ContentTypes.TEXT, text: 'world' },
1325
+ ]);
1326
+ expect(finalContentParts).toEqual([
1327
+ { type: ContentTypes.THINK, think: reasoningText },
1328
+ { type: ContentTypes.TEXT, text: replayWithSuffix },
1329
+ ]);
1330
+ expect(run.getRunMessages()?.[0]?.content).toBe(replayWithSuffix);
1331
+ });
1332
+
1333
+ it('composes observer chat stream handlers with default graph dispatch', async () => {
1334
+ const firstText = 'Vis';
1335
+ const secondText = 'ible answer.';
1336
+ const observedTextChunks: string[] = [];
1337
+ const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
1338
+ const messageDeltas: t.MessageDeltaEvent[] = [];
1339
+ const { contentParts, aggregateContent } = createContentAggregator();
1340
+ const run = await Run.create<t.IState>({
1341
+ runId: 'reasoning-fallback-openrouter-observer-stream',
1342
+ graphConfig: {
1343
+ type: 'standard',
1344
+ llmConfig: {
1345
+ provider: Providers.OPENROUTER,
1346
+ streamUsage: false,
1347
+ },
1348
+ reasoningKey: 'reasoning',
1349
+ },
1350
+ returnContent: true,
1351
+ skipCleanup: true,
1352
+ customHandlers: {
1353
+ [GraphEvents.CHAT_MODEL_STREAM]: {
1354
+ handle: (_event: string, data: t.StreamEventData): void => {
1355
+ const content = (data.chunk as AIMessageChunk | undefined)?.content;
1356
+ if (typeof content === 'string' && content !== '') {
1357
+ observedTextChunks.push(content);
1358
+ }
1359
+ },
1360
+ },
1361
+ ...createReasoningHandlers(
1362
+ aggregateContent,
1363
+ reasoningDeltas,
1364
+ messageDeltas
1365
+ ),
1366
+ },
1367
+ });
1368
+
1369
+ if (!run.Graph) {
1370
+ throw new Error('Expected graph to be initialized');
1371
+ }
1372
+
1373
+ const model = new ChatOpenRouter({
1374
+ model: 'google/gemini-test',
1375
+ apiKey: 'test-key',
1376
+ });
1377
+ const completions = (model as unknown as StreamingCompletionBackedModel)
1378
+ .completions;
1379
+
1380
+ async function* streamChunks(): AsyncGenerator<OpenAIClient.Chat.Completions.ChatCompletionChunk> {
1381
+ yield createOpenRouterStreamChunk(firstText);
1382
+ yield createOpenRouterStreamChunk(secondText, 'stop');
1383
+ }
1384
+
1385
+ completions.completionWithRetry = async (): Promise<
1386
+ AsyncIterable<OpenAIClient.Chat.Completions.ChatCompletionChunk>
1387
+ > => streamChunks();
1388
+ run.Graph.overrideModel = model as t.ChatModel;
1389
+
1390
+ await run.processStream(
1391
+ { messages: [new HumanMessage('stream OpenRouter text to observer')] },
1392
+ {
1393
+ ...config,
1394
+ configurable: {
1395
+ thread_id: 'reasoning-fallback-openrouter-observer-stream',
1396
+ },
1397
+ }
1398
+ );
1399
+
1400
+ expect(observedTextChunks).toEqual([firstText, secondText]);
1401
+ expect(reasoningDeltas).toHaveLength(0);
1402
+ expect(messageDeltas).toHaveLength(2);
1403
+ expect(contentParts).toEqual([
1404
+ { type: ContentTypes.TEXT, text: `${firstText}${secondText}` },
1405
+ ]);
1406
+ });
1407
+
1408
+ it('does not handle OpenRouter callback-emitted chunks twice inside graph streaming', async () => {
1409
+ const text = 'Visible answer.';
1410
+ const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
1411
+ const messageDeltas: t.MessageDeltaEvent[] = [];
1412
+ const { contentParts, aggregateContent } = createContentAggregator();
1413
+ const streamHandler = new ChatModelStreamHandler();
1414
+ const run = await Run.create<t.IState>({
1415
+ runId: 'reasoning-fallback-openrouter-callback-yield-stream',
1416
+ graphConfig: {
1417
+ type: 'standard',
1418
+ llmConfig: {
1419
+ provider: Providers.OPENROUTER,
1420
+ streamUsage: false,
1421
+ },
1422
+ reasoningKey: 'reasoning',
1423
+ },
1424
+ returnContent: true,
1425
+ skipCleanup: true,
1426
+ customHandlers: {
1427
+ [GraphEvents.CHAT_MODEL_STREAM]: streamHandler,
1428
+ ...createReasoningHandlers(
1429
+ aggregateContent,
1430
+ reasoningDeltas,
1431
+ messageDeltas
1432
+ ),
1433
+ },
1434
+ });
1435
+
1436
+ if (!run.Graph) {
1437
+ throw new Error('Expected graph to be initialized');
1438
+ }
1439
+
1440
+ const model = new ChatOpenRouter({
1441
+ model: 'google/gemini-test',
1442
+ apiKey: 'test-key',
1443
+ });
1444
+ const completions = (model as unknown as StreamingCompletionBackedModel)
1445
+ .completions;
1446
+
1447
+ async function* streamChunks(): AsyncGenerator<OpenAIClient.Chat.Completions.ChatCompletionChunk> {
1448
+ yield createOpenRouterStreamChunk(text, 'stop');
1449
+ }
1450
+
1451
+ completions.completionWithRetry = async (): Promise<
1452
+ AsyncIterable<OpenAIClient.Chat.Completions.ChatCompletionChunk>
1453
+ > => streamChunks();
1454
+ run.Graph.overrideModel = model as t.ChatModel;
1455
+
1456
+ await run.processStream(
1457
+ { messages: [new HumanMessage('stream OpenRouter text once')] },
1458
+ {
1459
+ ...config,
1460
+ configurable: {
1461
+ thread_id: 'reasoning-fallback-openrouter-callback-yield-stream',
1462
+ },
1463
+ }
1464
+ );
1465
+
1466
+ expect(reasoningDeltas).toHaveLength(0);
1467
+ expect(messageDeltas).toHaveLength(1);
1468
+ expect(contentParts).toEqual([{ type: ContentTypes.TEXT, text }]);
1469
+ });
1470
+
558
1471
  it.each([
559
1472
  {
560
1473
  providerName: 'DeepSeek',