@librechat/agents 3.4.6 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1027) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +0 -356
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/agents/projection.cjs +0 -9
  4. package/dist/cjs/agents/projection.cjs.map +1 -1
  5. package/dist/cjs/common/constants.cjs +0 -26
  6. package/dist/cjs/common/constants.cjs.map +1 -1
  7. package/dist/cjs/common/enum.cjs +0 -85
  8. package/dist/cjs/common/enum.cjs.map +1 -1
  9. package/dist/cjs/events.cjs +0 -14
  10. package/dist/cjs/events.cjs.map +1 -1
  11. package/dist/cjs/graphs/Graph.cjs +1 -909
  12. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  13. package/dist/cjs/graphs/MultiAgentGraph.cjs +7 -272
  14. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  15. package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs.map +1 -1
  16. package/dist/cjs/graphs/createGraph.cjs.map +1 -1
  17. package/dist/cjs/hitl/askUserQuestion.cjs +0 -53
  18. package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
  19. package/dist/cjs/hitl/askUserQuestions.cjs +0 -20
  20. package/dist/cjs/hitl/askUserQuestions.cjs.map +1 -1
  21. package/dist/cjs/hitl/askUserQuestionsInterrupt.cjs +0 -6
  22. package/dist/cjs/hitl/askUserQuestionsInterrupt.cjs.map +1 -1
  23. package/dist/cjs/hooks/HookRegistry.cjs +0 -127
  24. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  25. package/dist/cjs/hooks/createToolPolicyHook.cjs +0 -38
  26. package/dist/cjs/hooks/createToolPolicyHook.cjs.map +1 -1
  27. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +0 -77
  28. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -1
  29. package/dist/cjs/hooks/executeHooks.cjs +0 -79
  30. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  31. package/dist/cjs/hooks/index.cjs +0 -16
  32. package/dist/cjs/hooks/index.cjs.map +1 -1
  33. package/dist/cjs/hooks/matchers.cjs +0 -104
  34. package/dist/cjs/hooks/matchers.cjs.map +1 -1
  35. package/dist/cjs/hooks/types.cjs +0 -8
  36. package/dist/cjs/hooks/types.cjs.map +1 -1
  37. package/dist/cjs/instrumentation.cjs +0 -14
  38. package/dist/cjs/instrumentation.cjs.map +1 -1
  39. package/dist/cjs/langfuse.cjs +0 -75
  40. package/dist/cjs/langfuse.cjs.map +1 -1
  41. package/dist/cjs/langfuseConfig.cjs.map +1 -1
  42. package/dist/cjs/langfuseRuntimeContext.cjs +0 -13
  43. package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
  44. package/dist/cjs/langfuseRuntimeScope.cjs +0 -4
  45. package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
  46. package/dist/cjs/langfuseSpanRegistry.cjs +0 -28
  47. package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -1
  48. package/dist/cjs/langfuseToolOutputTracing.cjs +0 -3
  49. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  50. package/dist/cjs/langfuseTraceShaping.cjs +0 -74
  51. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  52. package/dist/cjs/llm/anthropic/index.cjs +0 -3
  53. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  54. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  55. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +1 -111
  56. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  57. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +0 -1
  58. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  59. package/dist/cjs/llm/anthropic/utils/stream_events.cjs +0 -8
  60. package/dist/cjs/llm/anthropic/utils/stream_events.cjs.map +1 -1
  61. package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
  62. package/dist/cjs/llm/bedrock/cachePoints.cjs.map +1 -1
  63. package/dist/cjs/llm/bedrock/index.cjs +1 -95
  64. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  65. package/dist/cjs/llm/bedrock/toolCache.cjs.map +1 -1
  66. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +0 -78
  67. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  68. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +0 -24
  69. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
  70. package/dist/cjs/llm/contextOverflowRecovery.cjs +0 -62
  71. package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -1
  72. package/dist/cjs/llm/fake.cjs.map +1 -1
  73. package/dist/cjs/llm/google/index.cjs +2 -21
  74. package/dist/cjs/llm/google/index.cjs.map +1 -1
  75. package/dist/cjs/llm/google/utils/common.cjs +5 -35
  76. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  77. package/dist/cjs/llm/init.cjs +0 -9
  78. package/dist/cjs/llm/init.cjs.map +1 -1
  79. package/dist/cjs/llm/invoke.cjs +1 -270
  80. package/dist/cjs/llm/invoke.cjs.map +1 -1
  81. package/dist/cjs/llm/mistral/index.cjs.map +1 -1
  82. package/dist/cjs/llm/openai/index.cjs +0 -99
  83. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  84. package/dist/cjs/llm/openai/streamMetadata.cjs +0 -21
  85. package/dist/cjs/llm/openai/streamMetadata.cjs.map +1 -1
  86. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  87. package/dist/cjs/llm/openrouter/index.cjs +0 -1
  88. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  89. package/dist/cjs/llm/openrouter/toolCache.cjs +0 -6
  90. package/dist/cjs/llm/openrouter/toolCache.cjs.map +1 -1
  91. package/dist/cjs/llm/preempt.cjs +0 -75
  92. package/dist/cjs/llm/preempt.cjs.map +1 -1
  93. package/dist/cjs/llm/providers.cjs.map +1 -1
  94. package/dist/cjs/llm/request.cjs +0 -10
  95. package/dist/cjs/llm/request.cjs.map +1 -1
  96. package/dist/cjs/llm/stream/chunkAdapters.cjs +0 -62
  97. package/dist/cjs/llm/stream/chunkAdapters.cjs.map +1 -1
  98. package/dist/cjs/llm/stream/smoother.cjs +0 -58
  99. package/dist/cjs/llm/stream/smoother.cjs.map +1 -1
  100. package/dist/cjs/llm/streamLimits.cjs +4 -293
  101. package/dist/cjs/llm/streamLimits.cjs.map +1 -1
  102. package/dist/cjs/llm/truncation.cjs +0 -36
  103. package/dist/cjs/llm/truncation.cjs.map +1 -1
  104. package/dist/cjs/llm/vertexai/index.cjs +0 -330
  105. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  106. package/dist/cjs/messages/alternation.cjs +0 -43
  107. package/dist/cjs/messages/alternation.cjs.map +1 -1
  108. package/dist/cjs/messages/anthropicToolCache.cjs +0 -45
  109. package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
  110. package/dist/cjs/messages/assistantPhase.cjs +0 -12
  111. package/dist/cjs/messages/assistantPhase.cjs.map +1 -1
  112. package/dist/cjs/messages/budget.cjs +0 -9
  113. package/dist/cjs/messages/budget.cjs.map +1 -1
  114. package/dist/cjs/messages/cache.cjs +0 -172
  115. package/dist/cjs/messages/cache.cjs.map +1 -1
  116. package/dist/cjs/messages/content.cjs +0 -9
  117. package/dist/cjs/messages/content.cjs.map +1 -1
  118. package/dist/cjs/messages/contextPruning.cjs +0 -15
  119. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  120. package/dist/cjs/messages/contextPruningSettings.cjs +0 -3
  121. package/dist/cjs/messages/contextPruningSettings.cjs.map +1 -1
  122. package/dist/cjs/messages/core.cjs +0 -48
  123. package/dist/cjs/messages/core.cjs.map +1 -1
  124. package/dist/cjs/messages/format.cjs +1 -345
  125. package/dist/cjs/messages/format.cjs.map +1 -1
  126. package/dist/cjs/messages/handoffCue.cjs +0 -36
  127. package/dist/cjs/messages/handoffCue.cjs.map +1 -1
  128. package/dist/cjs/messages/ids.cjs.map +1 -1
  129. package/dist/cjs/messages/injected.cjs +0 -33
  130. package/dist/cjs/messages/injected.cjs.map +1 -1
  131. package/dist/cjs/messages/langchain.cjs +0 -14
  132. package/dist/cjs/messages/langchain.cjs.map +1 -1
  133. package/dist/cjs/messages/prune.cjs +1 -184
  134. package/dist/cjs/messages/prune.cjs.map +1 -1
  135. package/dist/cjs/messages/recency.cjs +0 -32
  136. package/dist/cjs/messages/recency.cjs.map +1 -1
  137. package/dist/cjs/messages/reducer.cjs +0 -30
  138. package/dist/cjs/messages/reducer.cjs.map +1 -1
  139. package/dist/cjs/messages/tools.cjs +0 -14
  140. package/dist/cjs/messages/tools.cjs.map +1 -1
  141. package/dist/cjs/openai/index.cjs.map +1 -1
  142. package/dist/cjs/prompts/activityLabel.cjs +9 -79
  143. package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
  144. package/dist/cjs/responses/index.cjs.map +1 -1
  145. package/dist/cjs/run.cjs +2 -538
  146. package/dist/cjs/run.cjs.map +1 -1
  147. package/dist/cjs/session/AgentSession.cjs +1 -17
  148. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  149. package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -1
  150. package/dist/cjs/session/handlers.cjs +0 -7
  151. package/dist/cjs/session/handlers.cjs.map +1 -1
  152. package/dist/cjs/session/ids.cjs.map +1 -1
  153. package/dist/cjs/session/messageSerialization.cjs +0 -4
  154. package/dist/cjs/session/messageSerialization.cjs.map +1 -1
  155. package/dist/cjs/stream.cjs +1 -93
  156. package/dist/cjs/stream.cjs.map +1 -1
  157. package/dist/cjs/summarization/index.cjs +0 -27
  158. package/dist/cjs/summarization/index.cjs.map +1 -1
  159. package/dist/cjs/summarization/node.cjs +0 -273
  160. package/dist/cjs/summarization/node.cjs.map +1 -1
  161. package/dist/cjs/tools/BashExecutor.cjs +0 -35
  162. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  163. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +0 -24
  164. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  165. package/dist/cjs/tools/Calculator.cjs.map +1 -1
  166. package/dist/cjs/tools/CodeExecutor.cjs +0 -13
  167. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  168. package/dist/cjs/tools/CodeSessionFileSummary.cjs.map +1 -1
  169. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +0 -101
  170. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  171. package/dist/cjs/tools/ReadFile.cjs.map +1 -1
  172. package/dist/cjs/tools/SkillTool.cjs +0 -5
  173. package/dist/cjs/tools/SkillTool.cjs.map +1 -1
  174. package/dist/cjs/tools/SubagentTool.cjs +0 -10
  175. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  176. package/dist/cjs/tools/ToolNode.cjs +6 -1038
  177. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  178. package/dist/cjs/tools/ToolSearch.cjs +0 -179
  179. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  180. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -1
  181. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
  182. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +0 -39
  183. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
  184. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -1
  185. package/dist/cjs/tools/eagerEventExecution.cjs +0 -6
  186. package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -1
  187. package/dist/cjs/tools/handlers.cjs +1 -41
  188. package/dist/cjs/tools/handlers.cjs.map +1 -1
  189. package/dist/cjs/tools/intentArg.cjs +1 -132
  190. package/dist/cjs/tools/intentArg.cjs.map +1 -1
  191. package/dist/cjs/tools/local/CompileCheckTool.cjs +0 -25
  192. package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -1
  193. package/dist/cjs/tools/local/FileCheckpointer.cjs +0 -23
  194. package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -1
  195. package/dist/cjs/tools/local/LocalCodingTools.cjs +0 -80
  196. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
  197. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +0 -126
  198. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  199. package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -1
  200. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +0 -12
  201. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
  202. package/dist/cjs/tools/local/attachments.cjs +0 -44
  203. package/dist/cjs/tools/local/attachments.cjs.map +1 -1
  204. package/dist/cjs/tools/local/bashAst.cjs +0 -16
  205. package/dist/cjs/tools/local/bashAst.cjs.map +1 -1
  206. package/dist/cjs/tools/local/editStrategies.cjs +0 -14
  207. package/dist/cjs/tools/local/editStrategies.cjs.map +1 -1
  208. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  209. package/dist/cjs/tools/local/syntaxCheck.cjs +0 -31
  210. package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -1
  211. package/dist/cjs/tools/local/textEncoding.cjs +0 -11
  212. package/dist/cjs/tools/local/textEncoding.cjs.map +1 -1
  213. package/dist/cjs/tools/local/workspaceFS.cjs +0 -33
  214. package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -1
  215. package/dist/cjs/tools/ptcTimeout.cjs.map +1 -1
  216. package/dist/cjs/tools/schema.cjs +0 -8
  217. package/dist/cjs/tools/schema.cjs.map +1 -1
  218. package/dist/cjs/tools/search/anthropic.cjs +0 -9
  219. package/dist/cjs/tools/search/anthropic.cjs.map +1 -1
  220. package/dist/cjs/tools/search/content.cjs.map +1 -1
  221. package/dist/cjs/tools/search/crw-scraper.cjs +0 -33
  222. package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -1
  223. package/dist/cjs/tools/search/crw-search.cjs +0 -2
  224. package/dist/cjs/tools/search/crw-search.cjs.map +1 -1
  225. package/dist/cjs/tools/search/firecrawl.cjs +0 -25
  226. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
  227. package/dist/cjs/tools/search/format.cjs +0 -30
  228. package/dist/cjs/tools/search/format.cjs.map +1 -1
  229. package/dist/cjs/tools/search/highlights.cjs +0 -16
  230. package/dist/cjs/tools/search/highlights.cjs.map +1 -1
  231. package/dist/cjs/tools/search/keenable-scraper.cjs +0 -8
  232. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -1
  233. package/dist/cjs/tools/search/keenable-search.cjs +0 -7
  234. package/dist/cjs/tools/search/keenable-search.cjs.map +1 -1
  235. package/dist/cjs/tools/search/rerankers.cjs +0 -35
  236. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  237. package/dist/cjs/tools/search/schema.cjs +0 -1
  238. package/dist/cjs/tools/search/schema.cjs.map +1 -1
  239. package/dist/cjs/tools/search/search.cjs +1 -14
  240. package/dist/cjs/tools/search/search.cjs.map +1 -1
  241. package/dist/cjs/tools/search/serper-scraper.cjs +0 -46
  242. package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
  243. package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -1
  244. package/dist/cjs/tools/search/tavily-search.cjs.map +1 -1
  245. package/dist/cjs/tools/search/tool.cjs +0 -25
  246. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  247. package/dist/cjs/tools/search/utils.cjs +0 -8
  248. package/dist/cjs/tools/search/utils.cjs.map +1 -1
  249. package/dist/cjs/tools/skillCatalog.cjs +0 -6
  250. package/dist/cjs/tools/skillCatalog.cjs.map +1 -1
  251. package/dist/cjs/tools/streamedToolCallSeals.cjs +0 -6
  252. package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -1
  253. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +0 -2
  254. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -1
  255. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +3 -213
  256. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  257. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  258. package/dist/cjs/tools/subagent/childGraphConfig.cjs +0 -4
  259. package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -1
  260. package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -1
  261. package/dist/cjs/tools/toolOutputReferences.cjs +1 -322
  262. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  263. package/dist/cjs/types/hitl.cjs +0 -5
  264. package/dist/cjs/types/hitl.cjs.map +1 -1
  265. package/dist/cjs/utils/callbacks.cjs.map +1 -1
  266. package/dist/cjs/utils/errors.cjs +14 -165
  267. package/dist/cjs/utils/errors.cjs.map +1 -1
  268. package/dist/cjs/utils/events.cjs +0 -10
  269. package/dist/cjs/utils/events.cjs.map +1 -1
  270. package/dist/cjs/utils/graph.cjs.map +1 -1
  271. package/dist/cjs/utils/handlers.cjs +0 -3
  272. package/dist/cjs/utils/handlers.cjs.map +1 -1
  273. package/dist/cjs/utils/llm.cjs +0 -1
  274. package/dist/cjs/utils/llm.cjs.map +1 -1
  275. package/dist/cjs/utils/misc.cjs +0 -16
  276. package/dist/cjs/utils/misc.cjs.map +1 -1
  277. package/dist/cjs/utils/proxy.cjs +0 -26
  278. package/dist/cjs/utils/proxy.cjs.map +1 -1
  279. package/dist/cjs/utils/run.cjs +0 -6
  280. package/dist/cjs/utils/run.cjs.map +1 -1
  281. package/dist/cjs/utils/schema.cjs +0 -5
  282. package/dist/cjs/utils/schema.cjs.map +1 -1
  283. package/dist/cjs/utils/title.cjs +0 -6
  284. package/dist/cjs/utils/title.cjs.map +1 -1
  285. package/dist/cjs/utils/tokens.cjs +0 -113
  286. package/dist/cjs/utils/tokens.cjs.map +1 -1
  287. package/dist/cjs/utils/toolContent.cjs +0 -41
  288. package/dist/cjs/utils/toolContent.cjs.map +1 -1
  289. package/dist/cjs/utils/truncation.cjs +0 -61
  290. package/dist/cjs/utils/truncation.cjs.map +1 -1
  291. package/dist/esm/agents/AgentContext.mjs +0 -356
  292. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  293. package/dist/esm/agents/projection.mjs +0 -9
  294. package/dist/esm/agents/projection.mjs.map +1 -1
  295. package/dist/esm/common/constants.mjs +0 -26
  296. package/dist/esm/common/constants.mjs.map +1 -1
  297. package/dist/esm/common/enum.mjs +0 -85
  298. package/dist/esm/common/enum.mjs.map +1 -1
  299. package/dist/esm/events.mjs +0 -14
  300. package/dist/esm/events.mjs.map +1 -1
  301. package/dist/esm/graphs/Graph.mjs +1 -909
  302. package/dist/esm/graphs/Graph.mjs.map +1 -1
  303. package/dist/esm/graphs/MultiAgentGraph.mjs +7 -272
  304. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  305. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -1
  306. package/dist/esm/graphs/createGraph.mjs.map +1 -1
  307. package/dist/esm/hitl/askUserQuestion.mjs +0 -53
  308. package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
  309. package/dist/esm/hitl/askUserQuestions.mjs +0 -20
  310. package/dist/esm/hitl/askUserQuestions.mjs.map +1 -1
  311. package/dist/esm/hitl/askUserQuestionsInterrupt.mjs +0 -6
  312. package/dist/esm/hitl/askUserQuestionsInterrupt.mjs.map +1 -1
  313. package/dist/esm/hooks/HookRegistry.mjs +0 -127
  314. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  315. package/dist/esm/hooks/createToolPolicyHook.mjs +0 -38
  316. package/dist/esm/hooks/createToolPolicyHook.mjs.map +1 -1
  317. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +0 -77
  318. package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -1
  319. package/dist/esm/hooks/executeHooks.mjs +0 -79
  320. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  321. package/dist/esm/hooks/index.mjs +0 -16
  322. package/dist/esm/hooks/index.mjs.map +1 -1
  323. package/dist/esm/hooks/matchers.mjs +0 -104
  324. package/dist/esm/hooks/matchers.mjs.map +1 -1
  325. package/dist/esm/hooks/types.mjs +0 -8
  326. package/dist/esm/hooks/types.mjs.map +1 -1
  327. package/dist/esm/instrumentation.mjs +0 -14
  328. package/dist/esm/instrumentation.mjs.map +1 -1
  329. package/dist/esm/langfuse.mjs +0 -75
  330. package/dist/esm/langfuse.mjs.map +1 -1
  331. package/dist/esm/langfuseConfig.mjs.map +1 -1
  332. package/dist/esm/langfuseRuntimeContext.mjs +0 -13
  333. package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
  334. package/dist/esm/langfuseRuntimeScope.mjs +0 -4
  335. package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
  336. package/dist/esm/langfuseSpanRegistry.mjs +0 -28
  337. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -1
  338. package/dist/esm/langfuseToolOutputTracing.mjs +0 -3
  339. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  340. package/dist/esm/langfuseTraceShaping.mjs +0 -74
  341. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  342. package/dist/esm/llm/anthropic/index.mjs +0 -3
  343. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  344. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  345. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +1 -111
  346. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  347. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +0 -1
  348. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  349. package/dist/esm/llm/anthropic/utils/stream_events.mjs +0 -8
  350. package/dist/esm/llm/anthropic/utils/stream_events.mjs.map +1 -1
  351. package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
  352. package/dist/esm/llm/bedrock/cachePoints.mjs.map +1 -1
  353. package/dist/esm/llm/bedrock/index.mjs +1 -95
  354. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  355. package/dist/esm/llm/bedrock/toolCache.mjs.map +1 -1
  356. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +0 -78
  357. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  358. package/dist/esm/llm/bedrock/utils/message_outputs.mjs +0 -24
  359. package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
  360. package/dist/esm/llm/contextOverflowRecovery.mjs +0 -62
  361. package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -1
  362. package/dist/esm/llm/fake.mjs.map +1 -1
  363. package/dist/esm/llm/google/index.mjs +2 -21
  364. package/dist/esm/llm/google/index.mjs.map +1 -1
  365. package/dist/esm/llm/google/utils/common.mjs +5 -35
  366. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  367. package/dist/esm/llm/init.mjs +0 -9
  368. package/dist/esm/llm/init.mjs.map +1 -1
  369. package/dist/esm/llm/invoke.mjs +1 -270
  370. package/dist/esm/llm/invoke.mjs.map +1 -1
  371. package/dist/esm/llm/mistral/index.mjs.map +1 -1
  372. package/dist/esm/llm/openai/index.mjs +0 -99
  373. package/dist/esm/llm/openai/index.mjs.map +1 -1
  374. package/dist/esm/llm/openai/streamMetadata.mjs +0 -21
  375. package/dist/esm/llm/openai/streamMetadata.mjs.map +1 -1
  376. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  377. package/dist/esm/llm/openrouter/index.mjs +0 -1
  378. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  379. package/dist/esm/llm/openrouter/toolCache.mjs +0 -6
  380. package/dist/esm/llm/openrouter/toolCache.mjs.map +1 -1
  381. package/dist/esm/llm/preempt.mjs +0 -75
  382. package/dist/esm/llm/preempt.mjs.map +1 -1
  383. package/dist/esm/llm/providers.mjs.map +1 -1
  384. package/dist/esm/llm/request.mjs +0 -10
  385. package/dist/esm/llm/request.mjs.map +1 -1
  386. package/dist/esm/llm/stream/chunkAdapters.mjs +0 -62
  387. package/dist/esm/llm/stream/chunkAdapters.mjs.map +1 -1
  388. package/dist/esm/llm/stream/smoother.mjs +0 -58
  389. package/dist/esm/llm/stream/smoother.mjs.map +1 -1
  390. package/dist/esm/llm/streamLimits.mjs +4 -293
  391. package/dist/esm/llm/streamLimits.mjs.map +1 -1
  392. package/dist/esm/llm/truncation.mjs +0 -36
  393. package/dist/esm/llm/truncation.mjs.map +1 -1
  394. package/dist/esm/llm/vertexai/index.mjs +0 -330
  395. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  396. package/dist/esm/messages/alternation.mjs +0 -43
  397. package/dist/esm/messages/alternation.mjs.map +1 -1
  398. package/dist/esm/messages/anthropicToolCache.mjs +0 -45
  399. package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
  400. package/dist/esm/messages/assistantPhase.mjs +0 -12
  401. package/dist/esm/messages/assistantPhase.mjs.map +1 -1
  402. package/dist/esm/messages/budget.mjs +0 -9
  403. package/dist/esm/messages/budget.mjs.map +1 -1
  404. package/dist/esm/messages/cache.mjs +0 -172
  405. package/dist/esm/messages/cache.mjs.map +1 -1
  406. package/dist/esm/messages/content.mjs +0 -9
  407. package/dist/esm/messages/content.mjs.map +1 -1
  408. package/dist/esm/messages/contextPruning.mjs +0 -15
  409. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  410. package/dist/esm/messages/contextPruningSettings.mjs +0 -3
  411. package/dist/esm/messages/contextPruningSettings.mjs.map +1 -1
  412. package/dist/esm/messages/core.mjs +0 -48
  413. package/dist/esm/messages/core.mjs.map +1 -1
  414. package/dist/esm/messages/format.mjs +1 -345
  415. package/dist/esm/messages/format.mjs.map +1 -1
  416. package/dist/esm/messages/handoffCue.mjs +0 -36
  417. package/dist/esm/messages/handoffCue.mjs.map +1 -1
  418. package/dist/esm/messages/ids.mjs.map +1 -1
  419. package/dist/esm/messages/injected.mjs +0 -33
  420. package/dist/esm/messages/injected.mjs.map +1 -1
  421. package/dist/esm/messages/langchain.mjs +0 -14
  422. package/dist/esm/messages/langchain.mjs.map +1 -1
  423. package/dist/esm/messages/prune.mjs +1 -184
  424. package/dist/esm/messages/prune.mjs.map +1 -1
  425. package/dist/esm/messages/recency.mjs +0 -32
  426. package/dist/esm/messages/recency.mjs.map +1 -1
  427. package/dist/esm/messages/reducer.mjs +0 -30
  428. package/dist/esm/messages/reducer.mjs.map +1 -1
  429. package/dist/esm/messages/tools.mjs +0 -14
  430. package/dist/esm/messages/tools.mjs.map +1 -1
  431. package/dist/esm/openai/index.mjs.map +1 -1
  432. package/dist/esm/prompts/activityLabel.mjs +9 -79
  433. package/dist/esm/prompts/activityLabel.mjs.map +1 -1
  434. package/dist/esm/responses/index.mjs.map +1 -1
  435. package/dist/esm/run.mjs +2 -538
  436. package/dist/esm/run.mjs.map +1 -1
  437. package/dist/esm/session/AgentSession.mjs +1 -17
  438. package/dist/esm/session/AgentSession.mjs.map +1 -1
  439. package/dist/esm/session/JsonlSessionStore.mjs.map +1 -1
  440. package/dist/esm/session/handlers.mjs +0 -7
  441. package/dist/esm/session/handlers.mjs.map +1 -1
  442. package/dist/esm/session/ids.mjs.map +1 -1
  443. package/dist/esm/session/messageSerialization.mjs +0 -4
  444. package/dist/esm/session/messageSerialization.mjs.map +1 -1
  445. package/dist/esm/stream.mjs +1 -93
  446. package/dist/esm/stream.mjs.map +1 -1
  447. package/dist/esm/summarization/index.mjs +0 -27
  448. package/dist/esm/summarization/index.mjs.map +1 -1
  449. package/dist/esm/summarization/node.mjs +0 -273
  450. package/dist/esm/summarization/node.mjs.map +1 -1
  451. package/dist/esm/tools/BashExecutor.mjs +0 -35
  452. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  453. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +0 -24
  454. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  455. package/dist/esm/tools/Calculator.mjs.map +1 -1
  456. package/dist/esm/tools/CodeExecutor.mjs +0 -13
  457. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  458. package/dist/esm/tools/CodeSessionFileSummary.mjs.map +1 -1
  459. package/dist/esm/tools/ProgrammaticToolCalling.mjs +0 -101
  460. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  461. package/dist/esm/tools/ReadFile.mjs.map +1 -1
  462. package/dist/esm/tools/SkillTool.mjs +0 -5
  463. package/dist/esm/tools/SkillTool.mjs.map +1 -1
  464. package/dist/esm/tools/SubagentTool.mjs +0 -10
  465. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  466. package/dist/esm/tools/ToolNode.mjs +6 -1038
  467. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  468. package/dist/esm/tools/ToolSearch.mjs +0 -179
  469. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  470. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -1
  471. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
  472. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +0 -39
  473. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
  474. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -1
  475. package/dist/esm/tools/eagerEventExecution.mjs +0 -6
  476. package/dist/esm/tools/eagerEventExecution.mjs.map +1 -1
  477. package/dist/esm/tools/handlers.mjs +1 -41
  478. package/dist/esm/tools/handlers.mjs.map +1 -1
  479. package/dist/esm/tools/intentArg.mjs +1 -132
  480. package/dist/esm/tools/intentArg.mjs.map +1 -1
  481. package/dist/esm/tools/local/CompileCheckTool.mjs +0 -25
  482. package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -1
  483. package/dist/esm/tools/local/FileCheckpointer.mjs +0 -23
  484. package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -1
  485. package/dist/esm/tools/local/LocalCodingTools.mjs +0 -80
  486. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
  487. package/dist/esm/tools/local/LocalExecutionEngine.mjs +0 -126
  488. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  489. package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -1
  490. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +0 -12
  491. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
  492. package/dist/esm/tools/local/attachments.mjs +0 -44
  493. package/dist/esm/tools/local/attachments.mjs.map +1 -1
  494. package/dist/esm/tools/local/bashAst.mjs +0 -16
  495. package/dist/esm/tools/local/bashAst.mjs.map +1 -1
  496. package/dist/esm/tools/local/editStrategies.mjs +0 -14
  497. package/dist/esm/tools/local/editStrategies.mjs.map +1 -1
  498. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  499. package/dist/esm/tools/local/syntaxCheck.mjs +0 -31
  500. package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -1
  501. package/dist/esm/tools/local/textEncoding.mjs +0 -11
  502. package/dist/esm/tools/local/textEncoding.mjs.map +1 -1
  503. package/dist/esm/tools/local/workspaceFS.mjs +0 -33
  504. package/dist/esm/tools/local/workspaceFS.mjs.map +1 -1
  505. package/dist/esm/tools/ptcTimeout.mjs.map +1 -1
  506. package/dist/esm/tools/schema.mjs +0 -8
  507. package/dist/esm/tools/schema.mjs.map +1 -1
  508. package/dist/esm/tools/search/anthropic.mjs +0 -9
  509. package/dist/esm/tools/search/anthropic.mjs.map +1 -1
  510. package/dist/esm/tools/search/content.mjs.map +1 -1
  511. package/dist/esm/tools/search/crw-scraper.mjs +0 -33
  512. package/dist/esm/tools/search/crw-scraper.mjs.map +1 -1
  513. package/dist/esm/tools/search/crw-search.mjs +0 -2
  514. package/dist/esm/tools/search/crw-search.mjs.map +1 -1
  515. package/dist/esm/tools/search/firecrawl.mjs +0 -25
  516. package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
  517. package/dist/esm/tools/search/format.mjs +0 -30
  518. package/dist/esm/tools/search/format.mjs.map +1 -1
  519. package/dist/esm/tools/search/highlights.mjs +0 -16
  520. package/dist/esm/tools/search/highlights.mjs.map +1 -1
  521. package/dist/esm/tools/search/keenable-scraper.mjs +0 -8
  522. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -1
  523. package/dist/esm/tools/search/keenable-search.mjs +0 -7
  524. package/dist/esm/tools/search/keenable-search.mjs.map +1 -1
  525. package/dist/esm/tools/search/rerankers.mjs +0 -35
  526. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  527. package/dist/esm/tools/search/schema.mjs +0 -1
  528. package/dist/esm/tools/search/schema.mjs.map +1 -1
  529. package/dist/esm/tools/search/search.mjs +1 -14
  530. package/dist/esm/tools/search/search.mjs.map +1 -1
  531. package/dist/esm/tools/search/serper-scraper.mjs +0 -46
  532. package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
  533. package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -1
  534. package/dist/esm/tools/search/tavily-search.mjs.map +1 -1
  535. package/dist/esm/tools/search/tool.mjs +0 -25
  536. package/dist/esm/tools/search/tool.mjs.map +1 -1
  537. package/dist/esm/tools/search/utils.mjs +0 -8
  538. package/dist/esm/tools/search/utils.mjs.map +1 -1
  539. package/dist/esm/tools/skillCatalog.mjs +0 -6
  540. package/dist/esm/tools/skillCatalog.mjs.map +1 -1
  541. package/dist/esm/tools/streamedToolCallSeals.mjs +0 -6
  542. package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -1
  543. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +0 -2
  544. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -1
  545. package/dist/esm/tools/subagent/SubagentExecutor.mjs +3 -213
  546. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  547. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  548. package/dist/esm/tools/subagent/childGraphConfig.mjs +0 -4
  549. package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -1
  550. package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -1
  551. package/dist/esm/tools/toolOutputReferences.mjs +1 -322
  552. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  553. package/dist/esm/types/hitl.mjs +0 -5
  554. package/dist/esm/types/hitl.mjs.map +1 -1
  555. package/dist/esm/utils/callbacks.mjs.map +1 -1
  556. package/dist/esm/utils/errors.mjs +14 -165
  557. package/dist/esm/utils/errors.mjs.map +1 -1
  558. package/dist/esm/utils/events.mjs +0 -10
  559. package/dist/esm/utils/events.mjs.map +1 -1
  560. package/dist/esm/utils/graph.mjs.map +1 -1
  561. package/dist/esm/utils/handlers.mjs +0 -3
  562. package/dist/esm/utils/handlers.mjs.map +1 -1
  563. package/dist/esm/utils/llm.mjs +0 -1
  564. package/dist/esm/utils/llm.mjs.map +1 -1
  565. package/dist/esm/utils/misc.mjs +0 -16
  566. package/dist/esm/utils/misc.mjs.map +1 -1
  567. package/dist/esm/utils/proxy.mjs +0 -26
  568. package/dist/esm/utils/proxy.mjs.map +1 -1
  569. package/dist/esm/utils/run.mjs +0 -6
  570. package/dist/esm/utils/run.mjs.map +1 -1
  571. package/dist/esm/utils/schema.mjs +0 -5
  572. package/dist/esm/utils/schema.mjs.map +1 -1
  573. package/dist/esm/utils/title.mjs +0 -6
  574. package/dist/esm/utils/title.mjs.map +1 -1
  575. package/dist/esm/utils/tokens.mjs +0 -113
  576. package/dist/esm/utils/tokens.mjs.map +1 -1
  577. package/dist/esm/utils/toolContent.mjs +0 -41
  578. package/dist/esm/utils/toolContent.mjs.map +1 -1
  579. package/dist/esm/utils/truncation.mjs +0 -61
  580. package/dist/esm/utils/truncation.mjs.map +1 -1
  581. package/dist/types/agents/AgentContext.d.ts +3 -3
  582. package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +3 -3
  583. package/dist/types/agents/projection.d.ts +1 -1
  584. package/dist/types/events.d.ts +2 -2
  585. package/dist/types/graphs/Graph.d.ts +11 -11
  586. package/dist/types/graphs/MultiAgentGraph.d.ts +2 -2
  587. package/dist/types/graphs/createGraph.d.ts +4 -4
  588. package/dist/types/graphs/graphFactory.d.ts +1 -1
  589. package/dist/types/hitl/askUserQuestion.d.ts +1 -1
  590. package/dist/types/hitl/askUserQuestions.d.ts +1 -1
  591. package/dist/types/hitl/askUserQuestionsInterrupt.d.ts +1 -1
  592. package/dist/types/hooks/types.d.ts +1 -1
  593. package/dist/types/instrumentation.d.ts +1 -1
  594. package/dist/types/langfuse.d.ts +3 -3
  595. package/dist/types/langfuseConfig.d.ts +2 -2
  596. package/dist/types/langfuseRuntimeContext.d.ts +1 -1
  597. package/dist/types/langfuseRuntimeScope.d.ts +2 -2
  598. package/dist/types/langfuseSpanRegistry.d.ts +1 -1
  599. package/dist/types/langfuseToolOutputTracing.d.ts +3 -3
  600. package/dist/types/llm/anthropic/index.d.ts +1 -1
  601. package/dist/types/llm/bedrock/index.d.ts +1 -1
  602. package/dist/types/llm/bedrock/toolCache.d.ts +2 -2
  603. package/dist/types/llm/contextOverflowRecovery.d.ts +2 -2
  604. package/dist/types/llm/google/index.d.ts +1 -1
  605. package/dist/types/llm/init.d.ts +2 -2
  606. package/dist/types/llm/invoke.d.ts +6 -6
  607. package/dist/types/llm/mistral/index.d.ts +1 -1
  608. package/dist/types/llm/openai/index.d.ts +2 -2
  609. package/dist/types/llm/openrouter/index.d.ts +2 -2
  610. package/dist/types/llm/openrouter/toolCache.d.ts +2 -2
  611. package/dist/types/llm/providers.d.ts +2 -2
  612. package/dist/types/llm/request.d.ts +2 -2
  613. package/dist/types/llm/stream/chunkAdapters.d.ts +1 -1
  614. package/dist/types/llm/streamLimits.d.ts +1 -1
  615. package/dist/types/llm/truncation.d.ts +1 -1
  616. package/dist/types/llm/vertexai/index.d.ts +1 -1
  617. package/dist/types/messages/alternation.d.ts +1 -1
  618. package/dist/types/messages/anthropicToolCache.d.ts +2 -2
  619. package/dist/types/messages/assistantPhase.d.ts +3 -3
  620. package/dist/types/messages/budget.d.ts +1 -1
  621. package/dist/types/messages/cache.d.ts +1 -1
  622. package/dist/types/messages/contextPruning.d.ts +2 -2
  623. package/dist/types/messages/core.d.ts +2 -2
  624. package/dist/types/messages/format.d.ts +2 -2
  625. package/dist/types/messages/ids.d.ts +2 -2
  626. package/dist/types/messages/injected.d.ts +1 -1
  627. package/dist/types/messages/langchain.d.ts +1 -1
  628. package/dist/types/messages/prune.d.ts +3 -3
  629. package/dist/types/openai/index.d.ts +1 -1
  630. package/dist/types/prompts/activityLabel.d.ts +2 -2
  631. package/dist/types/responses/index.d.ts +1 -1
  632. package/dist/types/run.d.ts +3 -3
  633. package/dist/types/session/handlers.d.ts +1 -1
  634. package/dist/types/session/types.d.ts +1 -1
  635. package/dist/types/stream.d.ts +4 -4
  636. package/dist/types/summarization/index.d.ts +1 -1
  637. package/dist/types/summarization/node.d.ts +6 -6
  638. package/dist/types/test/mockTools.d.ts +1 -1
  639. package/dist/types/tools/BashExecutor.d.ts +2 -2
  640. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +2 -2
  641. package/dist/types/tools/CodeExecutor.d.ts +3 -3
  642. package/dist/types/tools/CodeSessionFileSummary.d.ts +1 -1
  643. package/dist/types/tools/ProgrammaticToolCalling.d.ts +2 -2
  644. package/dist/types/tools/ReadFile.d.ts +1 -1
  645. package/dist/types/tools/SkillTool.d.ts +1 -1
  646. package/dist/types/tools/SubagentTool.d.ts +3 -3
  647. package/dist/types/tools/ToolNode.d.ts +5 -5
  648. package/dist/types/tools/ToolSearch.d.ts +2 -2
  649. package/dist/types/tools/cloudflare/CloudflareBridgeRuntime.d.ts +1 -1
  650. package/dist/types/tools/cloudflare/CloudflareProgrammaticToolCalling.d.ts +1 -1
  651. package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +2 -2
  652. package/dist/types/tools/cloudflare/CloudflareSandboxTools.d.ts +1 -1
  653. package/dist/types/tools/eagerEventExecution.d.ts +1 -1
  654. package/dist/types/tools/handlers.d.ts +3 -3
  655. package/dist/types/tools/intentArg.d.ts +1 -1
  656. package/dist/types/tools/local/CompileCheckTool.d.ts +2 -2
  657. package/dist/types/tools/local/FileCheckpointer.d.ts +1 -1
  658. package/dist/types/tools/local/LocalCodingTools.d.ts +2 -2
  659. package/dist/types/tools/local/LocalExecutionEngine.d.ts +1 -1
  660. package/dist/types/tools/local/LocalExecutionTools.d.ts +1 -1
  661. package/dist/types/tools/local/LocalProgrammaticToolCalling.d.ts +1 -1
  662. package/dist/types/tools/local/bashAst.d.ts +1 -1
  663. package/dist/types/tools/local/resolveLocalExecutionTools.d.ts +1 -1
  664. package/dist/types/tools/local/syntaxCheck.d.ts +1 -1
  665. package/dist/types/tools/ptcTimeout.d.ts +1 -1
  666. package/dist/types/tools/schema.d.ts +1 -1
  667. package/dist/types/tools/search/anthropic.d.ts +1 -1
  668. package/dist/types/tools/skillCatalog.d.ts +1 -1
  669. package/dist/types/tools/subagent/SubagentExecutor.d.ts +5 -5
  670. package/dist/types/tools/subagent/SubagentReplay.d.ts +3 -3
  671. package/dist/types/tools/subagent/childGraphConfig.d.ts +2 -2
  672. package/dist/types/types/activityLabel.d.ts +3 -3
  673. package/dist/types/types/graph.d.ts +7 -7
  674. package/dist/types/types/llm.d.ts +9 -9
  675. package/dist/types/types/run.d.ts +7 -7
  676. package/dist/types/types/stream.d.ts +5 -5
  677. package/dist/types/types/summarize.d.ts +2 -2
  678. package/dist/types/types/tools.d.ts +5 -5
  679. package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +1 -1
  680. package/dist/types/utils/errors.d.ts +1 -1
  681. package/dist/types/utils/events.d.ts +1 -1
  682. package/dist/types/utils/handlers.d.ts +3 -3
  683. package/dist/types/utils/llm.d.ts +1 -1
  684. package/dist/types/utils/llmConfig.d.ts +1 -1
  685. package/dist/types/utils/title.d.ts +1 -1
  686. package/package.json +10 -3
  687. package/src/llm/google/utils/common.ts +6 -4
  688. package/src/__tests__/stream.eagerArgsDivergence.test.ts +0 -911
  689. package/src/__tests__/stream.eagerEventExecution.test.ts +0 -5046
  690. package/src/__tests__/stream.streamLimits.test.ts +0 -1982
  691. package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +0 -448
  692. package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +0 -655
  693. package/src/agents/__tests__/AgentContext.openrouter.live.test.ts +0 -128
  694. package/src/agents/__tests__/AgentContext.overflow.test.ts +0 -205
  695. package/src/agents/__tests__/AgentContext.test.ts +0 -2425
  696. package/src/agents/__tests__/projection.test.ts +0 -73
  697. package/src/agents/__tests__/promptCacheLiveHelpers.ts +0 -175
  698. package/src/aggregator.test.ts +0 -1214
  699. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +0 -273
  700. package/src/graphs/__tests__/Graph.closeRunStep.test.ts +0 -385
  701. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +0 -1766
  702. package/src/graphs/__tests__/Graph.preemptSignal.test.ts +0 -126
  703. package/src/graphs/__tests__/Graph.reasoning.test.ts +0 -1717
  704. package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +0 -80
  705. package/src/graphs/__tests__/MultiAgentGraph.test.ts +0 -177
  706. package/src/graphs/__tests__/composition.smoke.test.ts +0 -509
  707. package/src/graphs/__tests__/createGraph.test.ts +0 -206
  708. package/src/hooks/__tests__/HookRegistry.test.ts +0 -334
  709. package/src/hooks/__tests__/compactHooks.test.ts +0 -250
  710. package/src/hooks/__tests__/createToolPolicyHook.test.ts +0 -268
  711. package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +0 -405
  712. package/src/hooks/__tests__/executeHooks.test.ts +0 -1294
  713. package/src/hooks/__tests__/integration.test.ts +0 -337
  714. package/src/hooks/__tests__/matchers.test.ts +0 -238
  715. package/src/hooks/__tests__/preemptBoundary.test.ts +0 -152
  716. package/src/hooks/__tests__/toolHooks.test.ts +0 -669
  717. package/src/llm/__tests__/contextOverflowRecovery.test.ts +0 -401
  718. package/src/llm/__tests__/fallbackOverflow.test.ts +0 -287
  719. package/src/llm/anthropic/Jacob_Lee_Resume_2023.pdf +0 -0
  720. package/src/llm/anthropic/inherited-content-utils.spec.ts +0 -249
  721. package/src/llm/anthropic/inherited-stream-events.spec.ts +0 -1259
  722. package/src/llm/anthropic/inherited-strict.spec.ts +0 -302
  723. package/src/llm/anthropic/llm.spec.ts +0 -3394
  724. package/src/llm/anthropic/utils/cross-provider-reasoning.test.ts +0 -383
  725. package/src/llm/anthropic/utils/cross-provider-server-tools.test.ts +0 -110
  726. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +0 -508
  727. package/src/llm/anthropic/utils/streaming-tool-input.test.ts +0 -361
  728. package/src/llm/anthropic/utils/stripPrefillCache.test.ts +0 -111
  729. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +0 -287
  730. package/src/llm/bedrock/inherited-cache.spec.ts +0 -144
  731. package/src/llm/bedrock/inherited.spec.ts +0 -724
  732. package/src/llm/bedrock/llm.spec.ts +0 -1533
  733. package/src/llm/bedrock/streamSealDispatch.test.ts +0 -331
  734. package/src/llm/bedrock/toolCache.test.ts +0 -194
  735. package/src/llm/bedrock/utils/cross-provider-reasoning.test.ts +0 -131
  736. package/src/llm/bedrock/utils/cross-provider-server-tools.test.ts +0 -122
  737. package/src/llm/bedrock/utils/message_inputs.test.ts +0 -650
  738. package/src/llm/bedrock/utils/message_outputs.test.ts +0 -85
  739. package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +0 -118
  740. package/src/llm/custom-chat-models.smoke.test.ts +0 -1966
  741. package/src/llm/google/data/gettysburg10.wav +0 -0
  742. package/src/llm/google/data/hotdog.jpg +0 -0
  743. package/src/llm/google/inherited-stream-events.spec.ts +0 -350
  744. package/src/llm/google/llm.spec.ts +0 -1388
  745. package/src/llm/google/streamSmoothing.test.ts +0 -121
  746. package/src/llm/google/utils/common.test.ts +0 -185
  747. package/src/llm/invoke.alternation.test.ts +0 -87
  748. package/src/llm/invoke.handoffCue.test.ts +0 -112
  749. package/src/llm/invoke.streamLimits.test.ts +0 -142
  750. package/src/llm/invoke.test.ts +0 -1198
  751. package/src/llm/mistral/streamSmoothing.test.ts +0 -97
  752. package/src/llm/openai/cacheWriteTokens.test.ts +0 -112
  753. package/src/llm/openai/contentBlocks.test.ts +0 -381
  754. package/src/llm/openai/deepseek.test.ts +0 -747
  755. package/src/llm/openai/inherited-deepseek.spec.ts +0 -347
  756. package/src/llm/openai/inherited-xai.spec.ts +0 -416
  757. package/src/llm/openai/llm.spec.ts +0 -1774
  758. package/src/llm/openai/managedRequests.test.ts +0 -182
  759. package/src/llm/openai/sequentialToolCallSeals.test.ts +0 -199
  760. package/src/llm/openai/streamMetadata.spec.ts +0 -86
  761. package/src/llm/openai/streamMetadataDedup.spec.ts +0 -166
  762. package/src/llm/openai/utils/isReasoningModel.test.ts +0 -90
  763. package/src/llm/openai/utils/messages.test.ts +0 -488
  764. package/src/llm/openrouter/reasoning.test.ts +0 -561
  765. package/src/llm/openrouter/toolCache.test.ts +0 -134
  766. package/src/llm/preempt.test.ts +0 -323
  767. package/src/llm/stream/chunkAdapters.test.ts +0 -202
  768. package/src/llm/stream/reassembly.test.ts +0 -241
  769. package/src/llm/stream/smoother.bench.test.ts +0 -155
  770. package/src/llm/stream/smoother.test.ts +0 -519
  771. package/src/llm/streamLimits.test.ts +0 -450
  772. package/src/llm/truncation.test.ts +0 -242
  773. package/src/llm/vertexai/fixThoughtSignatures.test.ts +0 -154
  774. package/src/llm/vertexai/inherited-stream-events.spec.ts +0 -271
  775. package/src/llm/vertexai/llm.spec.ts +0 -159
  776. package/src/llm/vertexai/repairUsageMetadata.test.ts +0 -54
  777. package/src/llm/vertexai/sealStreamedToolCalls.test.ts +0 -88
  778. package/src/llm/vertexai/streamSealDispatch.test.ts +0 -148
  779. package/src/llm/vertexai/streamSmoothing.test.ts +0 -109
  780. package/src/messages/__tests__/anthropicToolCache.test.ts +0 -339
  781. package/src/messages/__tests__/observationMasking.test.ts +0 -312
  782. package/src/messages/__tests__/recency.test.ts +0 -267
  783. package/src/messages/__tests__/tools.test.ts +0 -473
  784. package/src/messages/alternation.test.ts +0 -212
  785. package/src/messages/assistantPhase.test.ts +0 -75
  786. package/src/messages/cache.tail.test.ts +0 -533
  787. package/src/messages/cache.test.ts +0 -2166
  788. package/src/messages/content.test.ts +0 -362
  789. package/src/messages/contextPruning.test.ts +0 -184
  790. package/src/messages/ensureThinkingBlock.test.ts +0 -1427
  791. package/src/messages/foldToollessToolBlocks.test.ts +0 -596
  792. package/src/messages/formatAgentMessages.reducer.test.ts +0 -162
  793. package/src/messages/formatAgentMessages.skills.test.ts +0 -513
  794. package/src/messages/formatAgentMessages.steer.test.ts +0 -598
  795. package/src/messages/formatAgentMessages.test.ts +0 -8111
  796. package/src/messages/formatAgentMessages.tools.test.ts +0 -522
  797. package/src/messages/formatMessage.test.ts +0 -693
  798. package/src/messages/handoffCue.test.ts +0 -96
  799. package/src/messages/injected.test.ts +0 -107
  800. package/src/messages/labelContentByAgent.test.ts +0 -962
  801. package/src/messages/reducer.spec.ts +0 -32
  802. package/src/messages/shiftIndexTokenCountMap.test.ts +0 -81
  803. package/src/messages/tailCacheConversion.test.ts +0 -161
  804. package/src/openai/__tests__/openai.test.ts +0 -337
  805. package/src/responses/__tests__/responses.test.ts +0 -652
  806. package/src/scripts/abort.ts +0 -157
  807. package/src/scripts/activity-labels/captured.json +0 -56
  808. package/src/scripts/activity-labels/checks.cjs +0 -205
  809. package/src/scripts/activity-labels/corpus.cjs +0 -473
  810. package/src/scripts/activity-labels/report.cjs +0 -203
  811. package/src/scripts/activity-labels/rescore.cjs +0 -102
  812. package/src/scripts/activity-labels/run.ts +0 -705
  813. package/src/scripts/activity-labels/variants.ts +0 -71
  814. package/src/scripts/ant_web_search.ts +0 -159
  815. package/src/scripts/ant_web_search_edge_case.ts +0 -163
  816. package/src/scripts/ant_web_search_error_edge_case.ts +0 -149
  817. package/src/scripts/args.ts +0 -48
  818. package/src/scripts/bedrock-cache-debug.ts +0 -250
  819. package/src/scripts/bedrock-content-aggregation-test.ts +0 -266
  820. package/src/scripts/bedrock-merge-test.ts +0 -107
  821. package/src/scripts/bedrock-parallel-tools-test.ts +0 -204
  822. package/src/scripts/bench-prompt-cache.ts +0 -479
  823. package/src/scripts/caching.ts +0 -132
  824. package/src/scripts/cli.ts +0 -172
  825. package/src/scripts/cli2.ts +0 -133
  826. package/src/scripts/cli3.ts +0 -184
  827. package/src/scripts/cli4.ts +0 -191
  828. package/src/scripts/cli5.ts +0 -191
  829. package/src/scripts/code_exec.ts +0 -214
  830. package/src/scripts/code_exec_files.ts +0 -237
  831. package/src/scripts/code_exec_multi_session.ts +0 -237
  832. package/src/scripts/code_exec_ptc.ts +0 -335
  833. package/src/scripts/code_exec_session.ts +0 -283
  834. package/src/scripts/code_exec_simple.ts +0 -148
  835. package/src/scripts/compare_pi_vs_ours.ts +0 -1259
  836. package/src/scripts/content.ts +0 -139
  837. package/src/scripts/context-overflow-probe.ts +0 -997
  838. package/src/scripts/empty_input.ts +0 -137
  839. package/src/scripts/handoff-test.ts +0 -134
  840. package/src/scripts/local_engine.ts +0 -166
  841. package/src/scripts/local_engine_checkpointer.ts +0 -205
  842. package/src/scripts/local_engine_compile.ts +0 -263
  843. package/src/scripts/local_engine_hooks.ts +0 -226
  844. package/src/scripts/local_engine_image.ts +0 -201
  845. package/src/scripts/local_engine_ptc.ts +0 -151
  846. package/src/scripts/local_engine_workspace.ts +0 -258
  847. package/src/scripts/memory.ts +0 -107
  848. package/src/scripts/multi-agent-chain.ts +0 -332
  849. package/src/scripts/multi-agent-conditional.ts +0 -222
  850. package/src/scripts/multi-agent-document-review-chain.ts +0 -198
  851. package/src/scripts/multi-agent-hybrid-flow.ts +0 -311
  852. package/src/scripts/multi-agent-parallel-start.ts +0 -265
  853. package/src/scripts/multi-agent-parallel.ts +0 -395
  854. package/src/scripts/multi-agent-sequence.ts +0 -217
  855. package/src/scripts/multi-agent-subagent.ts +0 -246
  856. package/src/scripts/multi-agent-supervisor.ts +0 -366
  857. package/src/scripts/multi-agent-test.ts +0 -187
  858. package/src/scripts/openrouter_prompt_cache_live.ts +0 -310
  859. package/src/scripts/parallel-asymmetric-tools-test.ts +0 -275
  860. package/src/scripts/parallel-full-metadata-test.ts +0 -241
  861. package/src/scripts/parallel-tools-test.ts +0 -341
  862. package/src/scripts/preempt-probe.ts +0 -330
  863. package/src/scripts/preempt-scenarios.ts +0 -388
  864. package/src/scripts/programmatic_exec.ts +0 -387
  865. package/src/scripts/programmatic_exec_agent.ts +0 -232
  866. package/src/scripts/search.ts +0 -147
  867. package/src/scripts/sequential-full-metadata-test.ts +0 -198
  868. package/src/scripts/session_live.ts +0 -548
  869. package/src/scripts/simple.ts +0 -226
  870. package/src/scripts/single-agent-metadata-test.ts +0 -196
  871. package/src/scripts/stream.ts +0 -141
  872. package/src/scripts/subagent-configurable-inheritance.ts +0 -252
  873. package/src/scripts/subagent-event-driven-debug.ts +0 -190
  874. package/src/scripts/subagent-tools-debug.ts +0 -160
  875. package/src/scripts/subagent-usage-sink.ts +0 -176
  876. package/src/scripts/summarization-recency.ts +0 -462
  877. package/src/scripts/test-custom-prompt-key.ts +0 -145
  878. package/src/scripts/test-handoff-input.ts +0 -170
  879. package/src/scripts/test-handoff-preamble.ts +0 -278
  880. package/src/scripts/test-handoff-steering.ts +0 -430
  881. package/src/scripts/test-multi-agent-list-handoff.ts +0 -418
  882. package/src/scripts/test-parallel-agent-labeling.ts +0 -327
  883. package/src/scripts/test-parallel-handoffs.ts +0 -292
  884. package/src/scripts/test-thinking-handoff-bedrock.ts +0 -154
  885. package/src/scripts/test-thinking-handoff.ts +0 -156
  886. package/src/scripts/test-thinking-to-thinking-handoff-bedrock.ts +0 -166
  887. package/src/scripts/test-tool-before-handoff-role-order.ts +0 -276
  888. package/src/scripts/test-tools-before-handoff.ts +0 -222
  889. package/src/scripts/test_code_api.ts +0 -354
  890. package/src/scripts/thinking-bedrock.ts +0 -160
  891. package/src/scripts/thinking-vertexai.ts +0 -168
  892. package/src/scripts/thinking.ts +0 -172
  893. package/src/scripts/tool_search.ts +0 -153
  894. package/src/scripts/tools.ts +0 -163
  895. package/src/session/__tests__/JsonlSessionStore.test.ts +0 -1463
  896. package/src/session/__tests__/handlers.test.ts +0 -161
  897. package/src/specs/activity-label-observability.live.test.ts +0 -262
  898. package/src/specs/activity-label-observability.test.ts +0 -167
  899. package/src/specs/activity-label-prompt.test.ts +0 -359
  900. package/src/specs/activity-label-trace-seed.test.ts +0 -47
  901. package/src/specs/activity-phase-label.test.ts +0 -306
  902. package/src/specs/agent-handoffs.live.test.ts +0 -140
  903. package/src/specs/agent-handoffs.test.ts +0 -2465
  904. package/src/specs/anthropic.simple.test.ts +0 -453
  905. package/src/specs/ask-user-question-batch.test.ts +0 -289
  906. package/src/specs/ask-user-questions.live.test.ts +0 -185
  907. package/src/specs/ask-user-questions.test.ts +0 -293
  908. package/src/specs/azure.simple.test.ts +0 -502
  909. package/src/specs/bedrock-toolless.live.test.ts +0 -123
  910. package/src/specs/bedrock-truncation.live.test.ts +0 -191
  911. package/src/specs/cache.simple.test.ts +0 -416
  912. package/src/specs/context-accuracy.live.test.ts +0 -409
  913. package/src/specs/context-overflow-recovery.live.test.ts +0 -213
  914. package/src/specs/context-usage-event.test.ts +0 -117
  915. package/src/specs/context-usage.live.test.ts +0 -297
  916. package/src/specs/custom-event-await.test.ts +0 -304
  917. package/src/specs/deepseek.simple.test.ts +0 -290
  918. package/src/specs/deterministic-trace-id.test.ts +0 -43
  919. package/src/specs/discovered-tools.test.ts +0 -217
  920. package/src/specs/durability-checkpoint.integration.test.ts +0 -243
  921. package/src/specs/graph-subagent.live.test.ts +0 -242
  922. package/src/specs/graph-subagent.test.ts +0 -322
  923. package/src/specs/handoffCue.test.ts +0 -165
  924. package/src/specs/langfuse-callbacks.test.ts +0 -1578
  925. package/src/specs/langfuse-config.test.ts +0 -247
  926. package/src/specs/langfuse-instrumentation.test.ts +0 -393
  927. package/src/specs/langfuse-metadata.test.ts +0 -245
  928. package/src/specs/langfuse-routing.integration.test.ts +0 -844
  929. package/src/specs/langfuse-runtime-context.test.ts +0 -92
  930. package/src/specs/langfuse-span-registry.test.ts +0 -87
  931. package/src/specs/langfuse-tool-output-tracing.test.ts +0 -1858
  932. package/src/specs/langfuse-trace-shaping.test.ts +0 -591
  933. package/src/specs/moonshot.simple.test.ts +0 -367
  934. package/src/specs/multi-agent-summarization.test.ts +0 -396
  935. package/src/specs/openai.simple.test.ts +0 -315
  936. package/src/specs/openrouter.simple.test.ts +0 -275
  937. package/src/specs/preemptSeal.test.ts +0 -678
  938. package/src/specs/prune.test.ts +0 -3741
  939. package/src/specs/reasoning.test.ts +0 -201
  940. package/src/specs/run-step-timestamps.test.ts +0 -414
  941. package/src/specs/spec.utils.ts +0 -15
  942. package/src/specs/subagent.test.ts +0 -864
  943. package/src/specs/summarization-unit.test.ts +0 -973
  944. package/src/specs/summarization.test.ts +0 -3823
  945. package/src/specs/summarize-prune.test.ts +0 -376
  946. package/src/specs/thinking-handoff.test.ts +0 -621
  947. package/src/specs/thinking-prune.test.ts +0 -827
  948. package/src/specs/token-accounting-e2e.test.ts +0 -1034
  949. package/src/specs/token-accounting-pipeline.test.ts +0 -1008
  950. package/src/specs/token-distribution-edge-case.test.ts +0 -315
  951. package/src/specs/token-memoization.test.ts +0 -40
  952. package/src/specs/tokens.test.ts +0 -1000
  953. package/src/specs/tool-error-resume.test.ts +0 -194
  954. package/src/specs/tool-error.test.ts +0 -198
  955. package/src/specs/vllm-reasoning-toolcalls.test.ts +0 -340
  956. package/src/stream.dispatch.test.ts +0 -63
  957. package/src/stream.test.ts +0 -144
  958. package/src/summarization/__tests__/aggregator.test.ts +0 -236
  959. package/src/summarization/__tests__/node.test.ts +0 -1996
  960. package/src/summarization/__tests__/trigger.test.ts +0 -149
  961. package/src/summarization/chunkHandler.test.ts +0 -196
  962. package/src/test/mockTools.ts +0 -386
  963. package/src/tools/Calculator.test.ts +0 -278
  964. package/src/tools/__tests__/BashExecutor.test.ts +0 -108
  965. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +0 -939
  966. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +0 -858
  967. package/src/tools/__tests__/CodeExecutor.stateful.test.ts +0 -124
  968. package/src/tools/__tests__/LocalExecutionRoots.test.ts +0 -8
  969. package/src/tools/__tests__/LocalExecutionTools.test.ts +0 -2733
  970. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +0 -318
  971. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +0 -1557
  972. package/src/tools/__tests__/ReadFile.test.ts +0 -44
  973. package/src/tools/__tests__/SkillTool.test.ts +0 -442
  974. package/src/tools/__tests__/SubagentExecutor.test.ts +0 -3728
  975. package/src/tools/__tests__/SubagentReplay.test.ts +0 -329
  976. package/src/tools/__tests__/SubagentTool.test.ts +0 -149
  977. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +0 -595
  978. package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +0 -1191
  979. package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +0 -757
  980. package/src/tools/__tests__/ToolNode.langfuse.test.ts +0 -57
  981. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +0 -736
  982. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +0 -1710
  983. package/src/tools/__tests__/ToolNode.runtimeState.test.ts +0 -120
  984. package/src/tools/__tests__/ToolNode.session.test.ts +0 -1352
  985. package/src/tools/__tests__/ToolNode.streamLimits.test.ts +0 -69
  986. package/src/tools/__tests__/ToolSearch.integration.test.ts +0 -161
  987. package/src/tools/__tests__/ToolSearch.test.ts +0 -1227
  988. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +0 -561
  989. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +0 -980
  990. package/src/tools/__tests__/directToolHooks.test.ts +0 -700
  991. package/src/tools/__tests__/eagerEventExecution.session.test.ts +0 -92
  992. package/src/tools/__tests__/handlers.test.ts +0 -997
  993. package/src/tools/__tests__/hitl.test.ts +0 -4663
  994. package/src/tools/__tests__/intentArg.test.ts +0 -357
  995. package/src/tools/__tests__/intentCoverage.test.ts +0 -139
  996. package/src/tools/__tests__/localToolNames.test.ts +0 -75
  997. package/src/tools/__tests__/skillCatalog.test.ts +0 -161
  998. package/src/tools/__tests__/subagentHooks.test.ts +0 -1651
  999. package/src/tools/__tests__/toolOutputReferences.test.ts +0 -515
  1000. package/src/tools/__tests__/workspaceSeam.test.ts +0 -147
  1001. package/src/tools/local/__tests__/FileCheckpointer.test.ts +0 -162
  1002. package/src/tools/local/__tests__/editStrategies.test.ts +0 -134
  1003. package/src/tools/search/content.test.ts +0 -173
  1004. package/src/tools/search/crw.test.ts +0 -836
  1005. package/src/tools/search/format.test.ts +0 -242
  1006. package/src/tools/search/http-agent.test.ts +0 -133
  1007. package/src/tools/search/jina-reranker.test.ts +0 -306
  1008. package/src/tools/search/keenable-scraper.test.ts +0 -153
  1009. package/src/tools/search/keenable.test.ts +0 -183
  1010. package/src/tools/search/outcome.test.ts +0 -90
  1011. package/src/tools/search/output.md +0 -2775
  1012. package/src/tools/search/rag-api-reranker.test.ts +0 -891
  1013. package/src/tools/search/source-processing.test.ts +0 -516
  1014. package/src/tools/search/tavily.test.ts +0 -965
  1015. package/src/tools/search/test.html +0 -884
  1016. package/src/tools/search/test.md +0 -643
  1017. package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +0 -637
  1018. package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +0 -3054
  1019. package/src/tools/subagent/__tests__/childGraphConfig.test.ts +0 -521
  1020. package/src/utils/__tests__/apportion.test.ts +0 -32
  1021. package/src/utils/__tests__/errors.test.ts +0 -270
  1022. package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +0 -336
  1023. package/src/utils/__tests__/redactSecrets.test.ts +0 -56
  1024. package/src/utils/__tests__/truncation.test.ts +0 -66
  1025. package/src/utils/llm.test.ts +0 -18
  1026. package/src/utils/proxy.test.ts +0 -176
  1027. package/src/utils/toolContent.test.ts +0 -1432
@@ -1,3823 +0,0 @@
1
- /* eslint-disable no-console */
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
- import { config } from 'dotenv';
4
- config();
5
- import { Calculator } from '@/tools/Calculator';
6
- import {
7
- HumanMessage,
8
- AIMessage,
9
- SystemMessage,
10
- ToolMessage,
11
- BaseMessage,
12
- UsageMetadata,
13
- } from '@langchain/core/messages';
14
- import type * as t from '@/types';
15
- import { ToolEndHandler, ModelEndHandler } from '@/events';
16
- import { ContentTypes, GraphEvents, Providers } from '@/common';
17
- import { createContentAggregator } from '@/stream';
18
- import { createTokenCounter } from '@/utils/tokens';
19
- import { getLLMConfig } from '@/utils/llmConfig';
20
- import { Run } from '@/run';
21
- import { formatAgentMessages } from '@/messages/format';
22
- import { FakeListChatModel } from '@langchain/core/utils/testing';
23
- import * as providers from '@/llm/providers';
24
- import { hasAnyEnv, hasEnv, hasEveryEnv } from './spec.utils';
25
-
26
- const SUMMARY_WRAPPER_OVERHEAD_TOKENS = 33;
27
-
28
- /** Extract plain text from a SummaryContentBlock's content array (test helper). */
29
- function getSummaryText(summary: t.SummaryContentBlock | undefined): string {
30
- if (!summary) return '';
31
- return (summary.content ?? [])
32
- .map((block) => ('text' in block ? (block as { text: string }).text : ''))
33
- .join('');
34
- }
35
-
36
- // ---------------------------------------------------------------------------
37
- // Shared test infrastructure
38
- // ---------------------------------------------------------------------------
39
-
40
- function createSpies(): {
41
- onMessageDeltaSpy: jest.Mock;
42
- onRunStepSpy: jest.Mock;
43
- onSummarizeStartSpy: jest.Mock;
44
- onSummarizeCompleteSpy: jest.Mock;
45
- } {
46
- return {
47
- onMessageDeltaSpy: jest.fn(),
48
- onRunStepSpy: jest.fn(),
49
- onSummarizeStartSpy: jest.fn(),
50
- onSummarizeCompleteSpy: jest.fn(),
51
- };
52
- }
53
-
54
- function buildHandlers(
55
- collectedUsage: UsageMetadata[],
56
- aggregateContent: t.ContentAggregator,
57
- spies: ReturnType<typeof createSpies>
58
- ): Record<string | GraphEvents, t.EventHandler> {
59
- return {
60
- [GraphEvents.TOOL_END]: new ToolEndHandler(),
61
- [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
62
- [GraphEvents.ON_RUN_STEP_COMPLETED]: {
63
- handle: (
64
- event: GraphEvents.ON_RUN_STEP_COMPLETED,
65
- data: t.StreamEventData
66
- ): void => {
67
- aggregateContent({
68
- event,
69
- data: data as unknown as { result: t.ToolEndEvent },
70
- });
71
- },
72
- },
73
- [GraphEvents.ON_RUN_STEP]: {
74
- handle: (
75
- event: GraphEvents.ON_RUN_STEP,
76
- data: t.StreamEventData,
77
- metadata,
78
- graph
79
- ): void => {
80
- spies.onRunStepSpy(event, data, metadata, graph);
81
- aggregateContent({ event, data: data as t.RunStep });
82
- },
83
- },
84
- [GraphEvents.ON_RUN_STEP_DELTA]: {
85
- handle: (
86
- event: GraphEvents.ON_RUN_STEP_DELTA,
87
- data: t.StreamEventData
88
- ): void => {
89
- aggregateContent({ event, data: data as t.RunStepDeltaEvent });
90
- },
91
- },
92
- [GraphEvents.ON_MESSAGE_DELTA]: {
93
- handle: (
94
- event: GraphEvents.ON_MESSAGE_DELTA,
95
- data: t.StreamEventData,
96
- metadata,
97
- graph
98
- ): void => {
99
- spies.onMessageDeltaSpy(event, data, metadata, graph);
100
- aggregateContent({ event, data: data as t.MessageDeltaEvent });
101
- },
102
- },
103
- [GraphEvents.TOOL_START]: {
104
- handle: (
105
- _event: string,
106
- _data: t.StreamEventData,
107
- _metadata?: Record<string, unknown>
108
- ): void => {},
109
- },
110
- [GraphEvents.ON_SUMMARIZE_START]: {
111
- handle: (
112
- _event: GraphEvents.ON_SUMMARIZE_START,
113
- data: t.StreamEventData
114
- ): void => {
115
- spies.onSummarizeStartSpy(data);
116
- },
117
- },
118
- [GraphEvents.ON_SUMMARIZE_COMPLETE]: {
119
- handle: (
120
- _event: GraphEvents.ON_SUMMARIZE_COMPLETE,
121
- data: t.StreamEventData
122
- ): void => {
123
- spies.onSummarizeCompleteSpy(data);
124
- },
125
- },
126
- };
127
- }
128
-
129
- async function createSummarizationRun(opts: {
130
- agentProvider: Providers;
131
- summarizationProvider: Providers;
132
- summarizationModel?: string;
133
- maxContextTokens: number;
134
- instructions: string;
135
- collectedUsage: UsageMetadata[];
136
- aggregateContent: t.ContentAggregator;
137
- spies: ReturnType<typeof createSpies>;
138
- tokenCounter?: t.TokenCounter;
139
- tools?: t.GraphTools;
140
- indexTokenCountMap?: Record<string, number>;
141
- llmConfigOverride?: Record<string, unknown>;
142
- maxSummaryTokens?: number;
143
- }): Promise<Run<t.IState>> {
144
- const llmConfig = {
145
- ...getLLMConfig(opts.agentProvider),
146
- ...opts.llmConfigOverride,
147
- };
148
- const tokenCounter = opts.tokenCounter ?? (await createTokenCounter());
149
-
150
- return Run.create<t.IState>({
151
- runId: `sum-e2e-${opts.agentProvider}-${Date.now()}`,
152
- graphConfig: {
153
- type: 'standard',
154
- llmConfig,
155
- tools: opts.tools ?? [new Calculator()],
156
- instructions: opts.instructions,
157
- maxContextTokens: opts.maxContextTokens,
158
- summarizationEnabled: true,
159
- summarizationConfig: {
160
- provider: opts.summarizationProvider,
161
- model: opts.summarizationModel,
162
- maxSummaryTokens: opts.maxSummaryTokens,
163
- },
164
- },
165
- returnContent: true,
166
- customHandlers: buildHandlers(
167
- opts.collectedUsage,
168
- opts.aggregateContent,
169
- opts.spies
170
- ),
171
- tokenCounter,
172
- indexTokenCountMap: opts.indexTokenCountMap,
173
- });
174
- }
175
-
176
- async function runTurn(
177
- state: { run: Run<t.IState>; conversationHistory: BaseMessage[] },
178
- userMessage: string,
179
- streamConfig: Record<string, unknown>
180
- ): Promise<t.MessageContentComplex[] | undefined> {
181
- state.conversationHistory.push(new HumanMessage(userMessage));
182
- const result = await state.run.processStream(
183
- { messages: state.conversationHistory },
184
- streamConfig as any
185
- );
186
- const finalMessages = state.run.getRunMessages();
187
- state.conversationHistory.push(...(finalMessages ?? []));
188
- return result;
189
- }
190
-
191
- function assertSummarizationEvents(spies: ReturnType<typeof createSpies>): {
192
- startPayload: t.SummarizeStartEvent;
193
- completePayload: t.SummarizeCompleteEvent;
194
- } {
195
- expect(spies.onSummarizeStartSpy).toHaveBeenCalled();
196
- expect(spies.onSummarizeCompleteSpy).toHaveBeenCalled();
197
-
198
- const startPayload = spies.onSummarizeStartSpy.mock
199
- .calls[0][0] as t.SummarizeStartEvent;
200
- expect(startPayload.agentId).toBeDefined();
201
- expect(typeof startPayload.provider).toBe('string');
202
- expect(startPayload.messagesToRefineCount).toBeGreaterThan(0);
203
-
204
- const completePayload = spies.onSummarizeCompleteSpy.mock
205
- .calls[0][0] as t.SummarizeCompleteEvent;
206
- expect(completePayload.agentId).toBeDefined();
207
- expect(completePayload.summary).toBeDefined();
208
- expect(completePayload.summary!.type).toBe(ContentTypes.SUMMARY);
209
- expect(typeof getSummaryText(completePayload.summary)).toBe('string');
210
- expect(getSummaryText(completePayload.summary).length).toBeGreaterThan(10);
211
- expect(completePayload.summary!.tokenCount ?? 0).toBeGreaterThan(0);
212
- expect(completePayload.summary!.provider).toBeDefined();
213
- expect(completePayload.summary!.createdAt).toBeDefined();
214
-
215
- const startIdx = spies.onSummarizeStartSpy.mock.invocationCallOrder[0];
216
- const completeIdx = spies.onSummarizeCompleteSpy.mock.invocationCallOrder[0];
217
- expect(startIdx).toBeLessThan(completeIdx);
218
-
219
- return { startPayload, completePayload };
220
- }
221
-
222
- function assertSummaryRunStep(
223
- spies: ReturnType<typeof createSpies>,
224
- summaryText: string
225
- ): void {
226
- const summaryRunSteps = spies.onRunStepSpy.mock.calls.filter(
227
- (call) => (call[1] as any)?.summary != null
228
- );
229
- expect(summaryRunSteps.length).toBeGreaterThan(0);
230
- const step = summaryRunSteps[0][1] as t.RunStep & {
231
- summary: t.SummaryContentBlock;
232
- };
233
- expect(step.summary.type).toBe(ContentTypes.SUMMARY);
234
- expect(getSummaryText(step.summary)).toBe(summaryText);
235
- expect(step.id).toBeDefined();
236
- expect(typeof step.stepIndex).toBe('number');
237
- }
238
-
239
- function buildIndexTokenCountMap(
240
- messages: BaseMessage[],
241
- tokenCounter: t.TokenCounter
242
- ): Record<string, number> {
243
- const map: Record<string, number> = {};
244
- for (let i = 0; i < messages.length; i++) {
245
- map[String(i)] = tokenCounter(messages[i]);
246
- }
247
- return map;
248
- }
249
-
250
- function sumTokenCountMap(map: Record<string, number | undefined>): number {
251
- let total = 0;
252
- for (const key in map) {
253
- total += map[key] ?? 0;
254
- }
255
- return total;
256
- }
257
-
258
- function createSeededTokenAuditHistory(): BaseMessage[] {
259
- const details =
260
- 'Token audit context preserves index token counts, summary replacement, calibration data, and post-summary continuity. ' +
261
- 'Important retained values: alpha=1024, beta=2048, gamma=4096, checksum TOKEN-AUDIT-7F3. ' +
262
- 'The repeated detail intentionally exceeds a compact context budget. ';
263
- const padding = details.repeat(8);
264
-
265
- return [
266
- new HumanMessage(
267
- `Audit turn 1: establish the accounting scenario. ${padding}`
268
- ),
269
- new AIMessage(`Recorded turn 1 accounting notes. ${padding}`),
270
- new HumanMessage(`Audit turn 2: add more retained details. ${padding}`),
271
- new AIMessage(`Recorded turn 2 accounting notes. ${padding}`),
272
- new HumanMessage(`Audit turn 3: preserve final identifiers. ${padding}`),
273
- new AIMessage(`Recorded turn 3 accounting notes. ${padding}`),
274
- ];
275
- }
276
-
277
- function logTurn(
278
- label: string,
279
- conversationHistory: BaseMessage[],
280
- extra?: string
281
- ): void {
282
- console.log(
283
- ` ${label} — ${conversationHistory.length} messages${extra != null && extra !== '' ? `, ${extra}` : ''}`
284
- );
285
- }
286
-
287
- // ---------------------------------------------------------------------------
288
- // Anthropic Summarization Tests
289
- // ---------------------------------------------------------------------------
290
-
291
- const hasAnthropic = hasEnv('ANTHROPIC_API_KEY');
292
- (hasAnthropic ? describe : describe.skip)('Anthropic Summarization E2E', () => {
293
- jest.setTimeout(180_000);
294
-
295
- const agentProvider = Providers.ANTHROPIC;
296
- const streamConfig = {
297
- configurable: { thread_id: 'anthropic-sum-e2e' },
298
- recursionLimit: 80,
299
- streamMode: 'values',
300
- version: 'v2' as const,
301
- };
302
-
303
- const MATH_TUTOR_INSTRUCTIONS = [
304
- 'You are an expert math tutor. You MUST use the calculator tool for ALL computations —',
305
- 'never compute in your head. Keep explanations concise (2-3 sentences max).',
306
- 'When summarizing prior work, list each calculation and its result.',
307
- ].join(' ');
308
-
309
- test('heavy multi-turn with tool calls triggers and survives summarization', async () => {
310
- const spies = createSpies();
311
- let collectedUsage: UsageMetadata[] = [];
312
- const conversationHistory: BaseMessage[] = [];
313
- const tokenCounter = await createTokenCounter();
314
-
315
- const resetAggregator = (): {
316
- contentParts: t.MessageContentComplex[];
317
- aggregateContent: t.ContentAggregator;
318
- } => {
319
- collectedUsage = [];
320
- const { contentParts: cp, aggregateContent: ac } =
321
- createContentAggregator();
322
- return {
323
- contentParts: cp as t.MessageContentComplex[],
324
- aggregateContent: ac,
325
- };
326
- };
327
-
328
- const createRun = async (
329
- maxTokens = 4000
330
- ): Promise<{
331
- run: Run<t.IState>;
332
- contentParts: t.MessageContentComplex[];
333
- }> => {
334
- const { contentParts, aggregateContent } = resetAggregator();
335
- const indexTokenCountMap = buildIndexTokenCountMap(
336
- conversationHistory,
337
- tokenCounter
338
- );
339
- const run = await createSummarizationRun({
340
- agentProvider,
341
- summarizationProvider: Providers.ANTHROPIC,
342
- summarizationModel: 'claude-haiku-4-5',
343
- maxContextTokens: maxTokens,
344
- instructions: MATH_TUTOR_INSTRUCTIONS,
345
- collectedUsage,
346
- aggregateContent,
347
- spies,
348
- tokenCounter,
349
- indexTokenCountMap,
350
- });
351
- return { run, contentParts };
352
- };
353
-
354
- // Turn 1: greeting + simple calculation
355
- let { run, contentParts } = await createRun();
356
- await runTurn(
357
- { run, conversationHistory },
358
- 'Hi! Let\'s do some math. What is 12345 * 6789? Use the calculator please.',
359
- streamConfig
360
- );
361
- logTurn('T1', conversationHistory, `parts=${contentParts.length}`);
362
-
363
- // Turn 2: compound calculation
364
- ({ run, contentParts } = await createRun());
365
- await runTurn(
366
- { run, conversationHistory },
367
- 'Great. Now take that result and divide it by 137. Then multiply the quotient by 42. Show both steps. Use the calculator for each.',
368
- streamConfig
369
- );
370
- logTurn('T2', conversationHistory, `parts=${contentParts.length}`);
371
-
372
- // Turn 3: verbose question to inflate token count
373
- ({ run, contentParts } = await createRun());
374
- await runTurn(
375
- { run, conversationHistory },
376
- [
377
- 'I need you to compute the following sequence of operations step by step using the calculator:',
378
- '1) Start with 9876543',
379
- '2) Subtract 1234567 from it',
380
- '3) Take the square root of the result',
381
- 'Please show each intermediate step with the calculator.',
382
- ].join('\n'),
383
- streamConfig
384
- );
385
- logTurn('T3', conversationHistory, `parts=${contentParts.length}`);
386
-
387
- // Turn 4: even more to guarantee pruning threshold
388
- ({ run, contentParts } = await createRun());
389
- await runTurn(
390
- { run, conversationHistory },
391
- 'Now calculate 2^20 using the calculator. Also, what is 1000000 / 7? Use calculator for both.',
392
- streamConfig
393
- );
394
- logTurn('T4', conversationHistory, `parts=${contentParts.length}`);
395
-
396
- // Turn 5: tighter context to force summarization if not already
397
- ({ run, contentParts } = await createRun(3500));
398
- await runTurn(
399
- { run, conversationHistory },
400
- 'What is 355 / 113? Use the calculator. This should approximate pi.',
401
- streamConfig
402
- );
403
- logTurn('T5', conversationHistory);
404
-
405
- // Turn 6: if still no summarization, squeeze harder
406
- if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
407
- // Debug: show total token count from the indexTokenCountMap
408
- const debugMap = buildIndexTokenCountMap(
409
- conversationHistory,
410
- tokenCounter
411
- );
412
- const totalTokens = Object.values(debugMap).reduce(
413
- (sum, v) => sum + v,
414
- 0
415
- );
416
- console.log(
417
- ` Pre-T6 debug: ${conversationHistory.length} msgs, totalTokens=${totalTokens}, ` +
418
- `indexTokenCountMap keys=${Object.keys(debugMap).length}`
419
- );
420
-
421
- ({ run, contentParts } = await createRun(3200));
422
- await runTurn(
423
- { run, conversationHistory },
424
- 'Calculate 999 * 999 with the calculator. Also compute 123456789 % 97.',
425
- streamConfig
426
- );
427
- logTurn('T6', conversationHistory);
428
- }
429
-
430
- // Turn 7: absolute minimum context if still nothing
431
- if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
432
- ({ run, contentParts } = await createRun(1200));
433
- await runTurn({ run, conversationHistory }, 'What is 1+1?', streamConfig);
434
- logTurn('T7', conversationHistory);
435
- }
436
-
437
- console.log(
438
- ` Summarize events — start: ${spies.onSummarizeStartSpy.mock.calls.length}, complete: ${spies.onSummarizeCompleteSpy.mock.calls.length}`
439
- );
440
-
441
- // Assert summarization fired correctly
442
- const { startPayload, completePayload } = assertSummarizationEvents(spies);
443
- assertSummaryRunStep(spies, getSummaryText(completePayload.summary));
444
-
445
- console.log(
446
- ` Summary (${getSummaryText(completePayload.summary).length} chars, ${completePayload.summary!.tokenCount} tok): "${getSummaryText(completePayload.summary).substring(0, 250)}…"`
447
- );
448
- console.log(
449
- ` Start event — agent=${startPayload.agentId}, provider=${startPayload.provider}, refining=${startPayload.messagesToRefineCount} msgs`
450
- );
451
-
452
- // Token accounting: summary tokenCount must be reasonable
453
- expect(completePayload.summary!.tokenCount).toBeGreaterThan(10);
454
- expect(completePayload.summary!.tokenCount).toBeLessThan(2000);
455
-
456
- // Token accounting: collectedUsage should have valid entries from post-summary model calls
457
- const validUsageEntries = collectedUsage.filter(
458
- (u: Partial<UsageMetadata>) =>
459
- u.input_tokens != null && u.input_tokens > 0
460
- );
461
- expect(validUsageEntries.length).toBeGreaterThan(0);
462
- const lastUsage = validUsageEntries[validUsageEntries.length - 1];
463
- expect(lastUsage.output_tokens).toBeGreaterThan(0);
464
- console.log(
465
- ` Post-summary usage — input: ${lastUsage.input_tokens}, output: ${lastUsage.output_tokens}`
466
- );
467
-
468
- // Assert model still works after summarization
469
- expect(spies.onMessageDeltaSpy).toHaveBeenCalled();
470
-
471
- // Summarization may fire multiple times per run (no single-fire guard);
472
- // the graph's recursionLimit prevents infinite loops.
473
- const startCallsForSameAgent = spies.onSummarizeStartSpy.mock.calls.filter(
474
- (c) => (c[0] as t.SummarizeStartEvent).agentId === startPayload.agentId
475
- );
476
- expect(startCallsForSameAgent.length).toBeGreaterThanOrEqual(1);
477
- }, 180_000);
478
-
479
- test('post-summary continuation over multiple turns preserves context', async () => {
480
- const spies = createSpies();
481
- let collectedUsage: UsageMetadata[] = [];
482
- const conversationHistory: BaseMessage[] = [];
483
- let latestContentParts: t.MessageContentComplex[] = [];
484
- const tokenCounter = await createTokenCounter();
485
-
486
- const createRun = async (maxTokens = 4000): Promise<Run<t.IState>> => {
487
- collectedUsage = [];
488
- const { contentParts, aggregateContent } = createContentAggregator();
489
- latestContentParts = contentParts as t.MessageContentComplex[];
490
- const indexTokenCountMap = buildIndexTokenCountMap(
491
- conversationHistory,
492
- tokenCounter
493
- );
494
- return createSummarizationRun({
495
- agentProvider,
496
- summarizationProvider: Providers.ANTHROPIC,
497
- summarizationModel: 'claude-haiku-4-5',
498
- maxContextTokens: maxTokens,
499
- instructions: MATH_TUTOR_INSTRUCTIONS,
500
- collectedUsage,
501
- aggregateContent,
502
- spies,
503
- tokenCounter,
504
- indexTokenCountMap,
505
- });
506
- };
507
-
508
- // Build up conversation — generous budget so messages accumulate
509
- let run = await createRun();
510
- await runTurn(
511
- { run, conversationHistory },
512
- 'What is 42 * 58? Calculator please.',
513
- streamConfig
514
- );
515
-
516
- run = await createRun();
517
- await runTurn(
518
- { run, conversationHistory },
519
- 'Now compute 2436 + 1337. Calculator.',
520
- streamConfig
521
- );
522
-
523
- run = await createRun();
524
- await runTurn(
525
- { run, conversationHistory },
526
- 'What is 3773 * 11? Calculator.',
527
- streamConfig
528
- );
529
-
530
- run = await createRun();
531
- await runTurn(
532
- { run, conversationHistory },
533
- 'Calculate 41503 - 12345 and then 29158 / 4. Show both with calculator.',
534
- streamConfig
535
- );
536
-
537
- run = await createRun();
538
- await runTurn(
539
- { run, conversationHistory },
540
- 'What is 100 * 200? Calculator.',
541
- streamConfig
542
- );
543
-
544
- // Progressively squeeze to force summarization
545
- for (const squeeze of [3500, 3200, 3100, 3000, 2800, 2500, 2000]) {
546
- if (spies.onSummarizeStartSpy.mock.calls.length > 0) {
547
- break;
548
- }
549
- run = await createRun(squeeze);
550
- await runTurn(
551
- { run, conversationHistory },
552
- `What is ${squeeze} * 2? Calculator.`,
553
- streamConfig
554
- );
555
- }
556
-
557
- console.log(
558
- ` Pre-continuation: ${spies.onSummarizeCompleteSpy.mock.calls.length} summaries`
559
- );
560
- expect(spies.onSummarizeCompleteSpy).toHaveBeenCalled();
561
- const completeSummary = (
562
- spies.onSummarizeCompleteSpy.mock.calls[0][0] as t.SummarizeCompleteEvent
563
- ).summary!;
564
- const summaryText = getSummaryText(completeSummary);
565
-
566
- // Token accounting: summary tokenCount bounds
567
- expect(completeSummary.tokenCount ?? 0).toBeGreaterThan(10);
568
- expect(completeSummary.tokenCount ?? 0).toBeLessThan(1200);
569
-
570
- // Continue for 2 more turns AFTER summarization — model should remain coherent
571
- run = await createRun(4000);
572
- const postSumTurn1 = await runTurn(
573
- { run, conversationHistory },
574
- 'What were all the numbers we computed so far? List them.',
575
- streamConfig
576
- );
577
- expect(postSumTurn1).toBeDefined();
578
- logTurn('Post-sum T1', conversationHistory);
579
-
580
- run = await createRun(4000);
581
- const postSumTurn2 = await runTurn(
582
- { run, conversationHistory },
583
- 'Now compute the sum of 2436, 3773, and 41503 using the calculator.',
584
- streamConfig
585
- );
586
- expect(postSumTurn2).toBeDefined();
587
- logTurn('Post-sum T2', conversationHistory);
588
-
589
- const hasPostSumCalculator = latestContentParts.some(
590
- (p) =>
591
- p.type === ContentTypes.TOOL_CALL &&
592
- (p as t.ToolCallContent).tool_call?.name === 'calculator'
593
- );
594
- expect(hasPostSumCalculator).toBe(true);
595
-
596
- // Model should still reference prior context from the summary
597
- expect(spies.onMessageDeltaSpy).toHaveBeenCalled();
598
- console.log(` Summary text: "${summaryText.substring(0, 200)}…"`);
599
- console.log(` Final message count: ${conversationHistory.length}`);
600
- }, 180_000);
601
-
602
- test('cross-provider summarization: Anthropic agent with OpenAI summarizer', async () => {
603
- const hasOpenAI = hasEnv('OPENAI_API_KEY');
604
- if (!hasOpenAI) {
605
- console.log(' Skipping cross-provider test (no OPENAI_API_KEY)');
606
- return;
607
- }
608
-
609
- const spies = createSpies();
610
- let collectedUsage: UsageMetadata[] = [];
611
- const conversationHistory: BaseMessage[] = [];
612
- const tokenCounter = await createTokenCounter();
613
-
614
- const createRun = async (maxTokens = 4000): Promise<Run<t.IState>> => {
615
- collectedUsage = [];
616
- const { aggregateContent } = createContentAggregator();
617
- const indexTokenCountMap = buildIndexTokenCountMap(
618
- conversationHistory,
619
- tokenCounter
620
- );
621
- return createSummarizationRun({
622
- agentProvider: Providers.ANTHROPIC,
623
- summarizationProvider: Providers.OPENAI,
624
- summarizationModel: 'gpt-4.1-mini',
625
- maxContextTokens: maxTokens,
626
- instructions: MATH_TUTOR_INSTRUCTIONS,
627
- collectedUsage,
628
- aggregateContent,
629
- spies,
630
- tokenCounter,
631
- indexTokenCountMap,
632
- });
633
- };
634
-
635
- // Build up conversation at generous limits so messages accumulate
636
- let run = await createRun(4000);
637
- await runTurn(
638
- { run, conversationHistory },
639
- 'Compute 54321 * 12345 using calculator.',
640
- streamConfig
641
- );
642
-
643
- run = await createRun(4000);
644
- await runTurn(
645
- { run, conversationHistory },
646
- 'Now calculate 670592745 / 99991. Calculator.',
647
- streamConfig
648
- );
649
-
650
- run = await createRun(4000);
651
- await runTurn(
652
- { run, conversationHistory },
653
- 'What is sqrt(670592745)? Calculator.',
654
- streamConfig
655
- );
656
-
657
- run = await createRun(4000);
658
- await runTurn(
659
- { run, conversationHistory },
660
- 'Compute 2^32 with calculator.',
661
- streamConfig
662
- );
663
-
664
- run = await createRun(4000);
665
- await runTurn(
666
- { run, conversationHistory },
667
- 'What is 13 * 17 * 19? Calculator.',
668
- streamConfig
669
- );
670
-
671
- // Tighten context to force summarization — must remain high enough
672
- // for post-summary instruction overhead + tool schema tokens + messages
673
- run = await createRun(3500);
674
- await runTurn(
675
- { run, conversationHistory },
676
- 'What is 99 * 101? Calculator. Then list everything we calculated so far in detail.',
677
- streamConfig
678
- );
679
-
680
- if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
681
- run = await createRun(3400);
682
- await runTurn(
683
- { run, conversationHistory },
684
- 'Compute 7! (factorial of 7) with calculator.',
685
- streamConfig
686
- );
687
- }
688
-
689
- if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
690
- run = await createRun(3300);
691
- await runTurn(
692
- { run, conversationHistory },
693
- 'What is 256 * 256? Calculator.',
694
- streamConfig
695
- );
696
- }
697
-
698
- if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
699
- run = await createRun(3200);
700
- await runTurn(
701
- { run, conversationHistory },
702
- 'Compute 100 + 200 with calculator.',
703
- streamConfig
704
- );
705
- }
706
-
707
- if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
708
- run = await createRun(3100);
709
- await runTurn(
710
- { run, conversationHistory },
711
- 'What is 50 * 50? Calculator.',
712
- streamConfig
713
- );
714
- }
715
-
716
- if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
717
- run = await createRun(3000);
718
- await runTurn(
719
- { run, conversationHistory },
720
- 'Compute 11 * 13 with calculator.',
721
- streamConfig
722
- );
723
- }
724
-
725
- if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
726
- run = await createRun(1000);
727
- await runTurn(
728
- { run, conversationHistory },
729
- 'What is 9 * 9? Calculator.',
730
- streamConfig
731
- );
732
- }
733
-
734
- console.log(
735
- ` Cross-provider summaries: ${spies.onSummarizeCompleteSpy.mock.calls.length}`
736
- );
737
-
738
- assertSummarizationEvents(spies);
739
- const completePayload = spies.onSummarizeCompleteSpy.mock
740
- .calls[0][0] as t.SummarizeCompleteEvent;
741
-
742
- // The summary should have been generated by OpenAI even though agent is Anthropic
743
- expect(completePayload.summary!.provider).toBe(Providers.OPENAI);
744
- expect(completePayload.summary!.model).toBe('gpt-4.1-mini');
745
- assertSummaryRunStep(spies, getSummaryText(completePayload.summary));
746
-
747
- // Token accounting: summary tokenCount bounds
748
- expect(completePayload.summary!.tokenCount ?? 0).toBeGreaterThan(10);
749
- expect(completePayload.summary!.tokenCount ?? 0).toBeLessThan(1200);
750
-
751
- // Token accounting: collectedUsage from the post-summary model call
752
- const validUsage = collectedUsage.filter(
753
- (u: Partial<UsageMetadata>) =>
754
- u.input_tokens != null && u.input_tokens > 0
755
- );
756
- expect(validUsage.length).toBeGreaterThan(0);
757
-
758
- console.log(
759
- ` Cross-provider summary (${getSummaryText(completePayload.summary).length} chars): "${getSummaryText(completePayload.summary).substring(0, 200)}…"`
760
- );
761
- }, 180_000);
762
-
763
- test('extended thinking: multi-turn with reasoning triggers summarization and grounds token accounting', async () => {
764
- const spies = createSpies();
765
- let collectedUsage: UsageMetadata[] = [];
766
- const conversationHistory: BaseMessage[] = [];
767
- const tokenCounter = await createTokenCounter();
768
-
769
- const resetAggregator = (): {
770
- contentParts: t.MessageContentComplex[];
771
- aggregateContent: t.ContentAggregator;
772
- } => {
773
- collectedUsage = [];
774
- const { contentParts: cp, aggregateContent: ac } =
775
- createContentAggregator();
776
- return {
777
- contentParts: cp as t.MessageContentComplex[],
778
- aggregateContent: ac,
779
- };
780
- };
781
-
782
- const createRun = async (
783
- maxTokens = 3000
784
- ): Promise<{
785
- run: Run<t.IState>;
786
- contentParts: t.MessageContentComplex[];
787
- }> => {
788
- const { contentParts, aggregateContent } = resetAggregator();
789
- const indexTokenCountMap = buildIndexTokenCountMap(
790
- conversationHistory,
791
- tokenCounter
792
- );
793
- const run = await createSummarizationRun({
794
- agentProvider,
795
- summarizationProvider: Providers.ANTHROPIC,
796
- summarizationModel: 'claude-haiku-4-5',
797
- maxContextTokens: maxTokens,
798
- instructions:
799
- 'You are a math tutor. Use the calculator tool for computations. Keep answers brief.',
800
- collectedUsage,
801
- aggregateContent,
802
- spies,
803
- tokenCounter,
804
- indexTokenCountMap,
805
- llmConfigOverride: {
806
- model: 'claude-sonnet-4-5',
807
- thinking: {
808
- type: 'enabled',
809
- budget_tokens: 1024,
810
- },
811
- },
812
- });
813
- return { run, contentParts };
814
- };
815
-
816
- // Turn 1: simple calculation with thinking
817
- let { run, contentParts } = await createRun();
818
- await runTurn(
819
- { run, conversationHistory },
820
- 'What is 7 * 720? Use the calculator.',
821
- streamConfig
822
- );
823
- logTurn('T1-think', conversationHistory, `parts=${contentParts.length}`);
824
-
825
- // Validate Turn 1 usage includes both input and output tokens
826
- const t1Usage = collectedUsage.filter(
827
- (u: Partial<UsageMetadata>) =>
828
- u.input_tokens != null && u.input_tokens > 0
829
- );
830
- expect(t1Usage.length).toBeGreaterThan(0);
831
- const t1Last = t1Usage[t1Usage.length - 1];
832
- expect(t1Last.output_tokens).toBeGreaterThan(0);
833
- console.log(
834
- ` T1 usage — input: ${t1Last.input_tokens}, output: ${t1Last.output_tokens}` +
835
- (t1Last.input_token_details?.cache_read != null
836
- ? `, cache_read: ${t1Last.input_token_details.cache_read}`
837
- : '')
838
- );
839
-
840
- // Turn 2: follow-up calculation
841
- ({ run, contentParts } = await createRun());
842
- await runTurn(
843
- { run, conversationHistory },
844
- 'Now multiply that result by 3. Use the calculator.',
845
- streamConfig
846
- );
847
- logTurn('T2-think', conversationHistory, `parts=${contentParts.length}`);
848
-
849
- // Turn 3: another calculation to build context
850
- ({ run, contentParts } = await createRun());
851
- await runTurn(
852
- { run, conversationHistory },
853
- 'What is 143 + 857? Use the calculator.',
854
- streamConfig
855
- );
856
- logTurn('T3-think', conversationHistory, `parts=${contentParts.length}`);
857
-
858
- // Turn 4: another turn to build up context
859
- ({ run, contentParts } = await createRun());
860
- await runTurn(
861
- { run, conversationHistory },
862
- 'What is 2 * 512? Use the calculator.',
863
- streamConfig
864
- );
865
- logTurn('T4-think', conversationHistory);
866
-
867
- // Turn 5: tighter context to trigger summarization
868
- if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
869
- ({ run, contentParts } = await createRun(2500));
870
- await runTurn(
871
- { run, conversationHistory },
872
- 'What is 999 * 999? Use the calculator.',
873
- streamConfig
874
- );
875
- logTurn('T5-think', conversationHistory);
876
- }
877
-
878
- // Turn 6: squeeze harder if needed
879
- if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
880
- ({ run, contentParts } = await createRun(1000));
881
- await runTurn(
882
- { run, conversationHistory },
883
- 'What is 42 * 42? Use the calculator.',
884
- streamConfig
885
- );
886
- logTurn('T6-think', conversationHistory);
887
- }
888
-
889
- console.log(
890
- ` Thinking summarize events — start: ${spies.onSummarizeStartSpy.mock.calls.length}, complete: ${spies.onSummarizeCompleteSpy.mock.calls.length}`
891
- );
892
-
893
- // Assert summarization fired
894
- const { completePayload } = assertSummarizationEvents(spies);
895
- assertSummaryRunStep(spies, getSummaryText(completePayload.summary));
896
-
897
- // Token accounting: summary tokenCount bounds
898
- expect(completePayload.summary!.tokenCount ?? 0).toBeGreaterThan(10);
899
- expect(completePayload.summary!.tokenCount ?? 0).toBeLessThan(2000);
900
-
901
- // Token accounting: collectedUsage must have valid entries across all turns
902
- const allValidUsage = collectedUsage.filter(
903
- (u: Partial<UsageMetadata>) =>
904
- u.input_tokens != null &&
905
- u.input_tokens > 0 &&
906
- u.output_tokens != null &&
907
- u.output_tokens > 0
908
- );
909
- expect(allValidUsage.length).toBeGreaterThan(0);
910
-
911
- // Validate that usage has reasonable token counts (thinking adds tokens)
912
- const lastUsage = allValidUsage[allValidUsage.length - 1];
913
- expect(lastUsage.input_tokens).toBeGreaterThan(0);
914
- expect(lastUsage.output_tokens).toBeGreaterThan(0);
915
-
916
- console.log(
917
- ` Thinking usage samples: ${allValidUsage.length} valid entries`
918
- );
919
- console.log(
920
- ` Last usage — input: ${lastUsage.input_tokens}, output: ${lastUsage.output_tokens}`
921
- );
922
- if (lastUsage.input_token_details?.cache_read != null) {
923
- console.log(
924
- ` Cache read: ${lastUsage.input_token_details.cache_read}, cache creation: ${lastUsage.input_token_details.cache_creation ?? 0}`
925
- );
926
- }
927
-
928
- // Post-summary continuation should work with thinking enabled
929
- ({ run } = await createRun(4000));
930
- const postSumResult = await runTurn(
931
- { run, conversationHistory },
932
- 'What is 100 / 4? Calculator please.',
933
- streamConfig
934
- );
935
- expect(postSumResult).toBeDefined();
936
- logTurn('Post-sum-think', conversationHistory);
937
-
938
- // Post-summary usage must also be valid
939
- const postSumUsage = collectedUsage.filter(
940
- (u: Partial<UsageMetadata>) =>
941
- u.input_tokens != null && u.input_tokens > 0
942
- );
943
- expect(postSumUsage.length).toBeGreaterThan(0);
944
-
945
- console.log(
946
- ` Thinking summary (${getSummaryText(completePayload.summary).length} chars): "${getSummaryText(completePayload.summary).substring(0, 250)}…"`
947
- );
948
- console.log(` Final messages: ${conversationHistory.length}`);
949
- }, 180_000);
950
-
951
- test('count_tokens API: local tokenCounter vs Anthropic actual token count', async () => {
952
- const Anthropic = (await import('@anthropic-ai/sdk')).default;
953
- const client = new Anthropic();
954
- const tokenCounter = await createTokenCounter();
955
-
956
- const testMessages: Array<{
957
- role: 'user' | 'assistant';
958
- lcMessage: BaseMessage;
959
- content: string;
960
- }> = [
961
- {
962
- role: 'user',
963
- lcMessage: new HumanMessage(
964
- 'What is 12345 * 6789? Please compute this using the calculator tool and explain the result.'
965
- ),
966
- content:
967
- 'What is 12345 * 6789? Please compute this using the calculator tool and explain the result.',
968
- },
969
- {
970
- role: 'assistant',
971
- lcMessage: new AIMessage(
972
- 'The result of 12345 multiplied by 6789 is 83,810,205. This is computed by multiplying each digit and carrying over.'
973
- ),
974
- content:
975
- 'The result of 12345 multiplied by 6789 is 83,810,205. This is computed by multiplying each digit and carrying over.',
976
- },
977
- {
978
- role: 'user',
979
- lcMessage: new HumanMessage(
980
- 'Now divide that by 137 and tell me the quotient.'
981
- ),
982
- content: 'Now divide that by 137 and tell me the quotient.',
983
- },
984
- {
985
- role: 'assistant',
986
- lcMessage: new AIMessage(
987
- '83,810,205 divided by 137 equals approximately 611,752.59.'
988
- ),
989
- content: '83,810,205 divided by 137 equals approximately 611,752.59.',
990
- },
991
- ];
992
-
993
- const systemPrompt =
994
- 'You are an expert math tutor. Use the calculator tool for ALL computations.';
995
-
996
- const anthropicCount = await client.messages.countTokens({
997
- model: 'claude-haiku-4-5',
998
- system: systemPrompt,
999
- messages: testMessages.map((m) => ({ role: m.role, content: m.content })),
1000
- });
1001
-
1002
- let localTotal = tokenCounter(new SystemMessage(systemPrompt));
1003
- for (const m of testMessages) {
1004
- localTotal += tokenCounter(m.lcMessage);
1005
- }
1006
-
1007
- const anthropicTokens = anthropicCount.input_tokens;
1008
- const drift = Math.abs(anthropicTokens - localTotal);
1009
- const driftPct = (drift / anthropicTokens) * 100;
1010
-
1011
- console.log(` Anthropic count_tokens API: ${anthropicTokens} tokens`);
1012
- console.log(` Local tiktoken estimate: ${localTotal} tokens`);
1013
- console.log(` Drift: ${drift} tokens (${driftPct.toFixed(1)}%)`);
1014
-
1015
- expect(anthropicTokens).toBeGreaterThan(0);
1016
- expect(localTotal).toBeGreaterThan(0);
1017
- expect(driftPct).toBeLessThan(30);
1018
- }, 180_000);
1019
- });
1020
-
1021
- // ---------------------------------------------------------------------------
1022
- // Bedrock Summarization Tests
1023
- // ---------------------------------------------------------------------------
1024
-
1025
- const requiredBedrockEnv = [
1026
- 'BEDROCK_AWS_REGION',
1027
- 'BEDROCK_AWS_ACCESS_KEY_ID',
1028
- 'BEDROCK_AWS_SECRET_ACCESS_KEY',
1029
- ];
1030
- const hasBedrock = hasEveryEnv(requiredBedrockEnv);
1031
-
1032
- (hasBedrock ? describe : describe.skip)('Bedrock Summarization E2E', () => {
1033
- jest.setTimeout(180_000);
1034
-
1035
- const agentProvider = Providers.BEDROCK;
1036
- const streamConfig = {
1037
- configurable: { thread_id: 'bedrock-sum-e2e' },
1038
- streamMode: 'values',
1039
- version: 'v2' as const,
1040
- };
1041
-
1042
- test('multi-turn tool calls trigger summarization with Bedrock agent', async () => {
1043
- const spies = createSpies();
1044
- let collectedUsage: UsageMetadata[] = [];
1045
- const conversationHistory: BaseMessage[] = [];
1046
- const tokenCounter = await createTokenCounter();
1047
-
1048
- const createRun = async (maxTokens = 4000): Promise<Run<t.IState>> => {
1049
- collectedUsage = [];
1050
- const { aggregateContent } = createContentAggregator();
1051
- const indexTokenCountMap = buildIndexTokenCountMap(
1052
- conversationHistory,
1053
- tokenCounter
1054
- );
1055
- return createSummarizationRun({
1056
- agentProvider,
1057
- summarizationProvider: Providers.BEDROCK,
1058
- maxContextTokens: maxTokens,
1059
- instructions:
1060
- 'You are a precise math assistant. Use the calculator tool for every computation. Be brief.',
1061
- collectedUsage,
1062
- aggregateContent,
1063
- spies,
1064
- tokenCounter,
1065
- indexTokenCountMap,
1066
- });
1067
- };
1068
-
1069
- let run = await createRun();
1070
- await runTurn(
1071
- { run, conversationHistory },
1072
- 'Hello. Please compute 987 * 654 using the calculator.',
1073
- streamConfig
1074
- );
1075
- logTurn('T1', conversationHistory);
1076
-
1077
- run = await createRun();
1078
- await runTurn(
1079
- { run, conversationHistory },
1080
- 'Now divide 645498 by 123. Use calculator.',
1081
- streamConfig
1082
- );
1083
- logTurn('T2', conversationHistory);
1084
-
1085
- run = await createRun();
1086
- await runTurn(
1087
- { run, conversationHistory },
1088
- 'Compute sqrt(5248.764) with the calculator. Then multiply the result by 100.',
1089
- streamConfig
1090
- );
1091
- logTurn('T3', conversationHistory);
1092
-
1093
- run = await createRun(3500);
1094
- await runTurn(
1095
- { run, conversationHistory },
1096
- 'Calculate 2^16 and 3^10 using calculator for each.',
1097
- streamConfig
1098
- );
1099
- logTurn('T4', conversationHistory);
1100
-
1101
- run = await createRun(3200);
1102
- await runTurn(
1103
- { run, conversationHistory },
1104
- 'What is 59049 + 65536? Calculator. Also tell me what we calculated before.',
1105
- streamConfig
1106
- );
1107
- logTurn('T5', conversationHistory);
1108
-
1109
- if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
1110
- run = await createRun(3000);
1111
- await runTurn(
1112
- { run, conversationHistory },
1113
- 'Calculate 111111 * 111111 with calculator.',
1114
- streamConfig
1115
- );
1116
- logTurn('T6', conversationHistory);
1117
- }
1118
-
1119
- console.log(
1120
- ` Bedrock summarize events — start: ${spies.onSummarizeStartSpy.mock.calls.length}, complete: ${spies.onSummarizeCompleteSpy.mock.calls.length}`
1121
- );
1122
-
1123
- const { completePayload } = assertSummarizationEvents(spies);
1124
- assertSummaryRunStep(spies, getSummaryText(completePayload.summary));
1125
- expect(spies.onMessageDeltaSpy).toHaveBeenCalled();
1126
-
1127
- // Token accounting: summary tokenCount bounds
1128
- expect(completePayload.summary!.tokenCount ?? 0).toBeGreaterThan(10);
1129
- expect(completePayload.summary!.tokenCount ?? 0).toBeLessThan(1500);
1130
-
1131
- // Token accounting: collectedUsage from the post-summary model call
1132
- const validUsage = collectedUsage.filter(
1133
- (u: Partial<UsageMetadata>) =>
1134
- u.input_tokens != null && u.input_tokens > 0
1135
- );
1136
- expect(validUsage.length).toBeGreaterThan(0);
1137
- const lastUsage = validUsage[validUsage.length - 1];
1138
- expect(lastUsage.output_tokens).toBeGreaterThan(0);
1139
- console.log(
1140
- ` Bedrock post-summary usage — input: ${lastUsage.input_tokens}, output: ${lastUsage.output_tokens}`
1141
- );
1142
-
1143
- console.log(
1144
- ` Bedrock summary: "${getSummaryText(completePayload.summary).substring(0, 250)}…"`
1145
- );
1146
-
1147
- // Post-summary turn should work cleanly
1148
- run = await createRun(4000);
1149
- const postSumResult = await runTurn(
1150
- { run, conversationHistory },
1151
- 'Give me a brief list of all results we computed.',
1152
- streamConfig
1153
- );
1154
- expect(postSumResult).toBeDefined();
1155
- logTurn('Post-sum', conversationHistory);
1156
- }, 180_000);
1157
- });
1158
-
1159
- // ---------------------------------------------------------------------------
1160
- // OpenAI Summarization Tests
1161
- // ---------------------------------------------------------------------------
1162
-
1163
- const hasOpenAI = hasEnv('OPENAI_API_KEY');
1164
- (hasOpenAI ? describe : describe.skip)('OpenAI Summarization E2E', () => {
1165
- jest.setTimeout(240_000);
1166
-
1167
- const agentProvider = Providers.OPENAI;
1168
- const streamConfig = {
1169
- configurable: { thread_id: 'openai-sum-e2e' },
1170
- streamMode: 'values',
1171
- version: 'v2' as const,
1172
- };
1173
-
1174
- test('multi-turn with calculator triggers summarization and continues', async () => {
1175
- const spies = createSpies();
1176
- let collectedUsage: UsageMetadata[] = [];
1177
- const conversationHistory: BaseMessage[] = [];
1178
- let latestContentParts: t.MessageContentComplex[] = [];
1179
- const tokenCounter = await createTokenCounter();
1180
-
1181
- const createRun = async (maxTokens = 2000): Promise<Run<t.IState>> => {
1182
- collectedUsage = [];
1183
- const { contentParts, aggregateContent } = createContentAggregator();
1184
- latestContentParts = contentParts as t.MessageContentComplex[];
1185
- const indexTokenCountMap = buildIndexTokenCountMap(
1186
- conversationHistory,
1187
- tokenCounter
1188
- );
1189
- return createSummarizationRun({
1190
- agentProvider,
1191
- summarizationProvider: Providers.OPENAI,
1192
- summarizationModel: 'gpt-4.1-mini',
1193
- llmConfigOverride: {
1194
- model: 'gpt-4.1-mini',
1195
- },
1196
- maxContextTokens: maxTokens,
1197
- instructions:
1198
- 'You are a helpful math tutor. Use the calculator tool for ALL computations. Keep responses concise.',
1199
- collectedUsage,
1200
- aggregateContent,
1201
- spies,
1202
- tokenCounter,
1203
- indexTokenCountMap,
1204
- });
1205
- };
1206
-
1207
- let run = await createRun();
1208
- await runTurn(
1209
- { run, conversationHistory },
1210
- 'What is 1234 * 5678? Use the calculator.',
1211
- streamConfig
1212
- );
1213
- logTurn('T1', conversationHistory);
1214
-
1215
- run = await createRun();
1216
- await runTurn(
1217
- { run, conversationHistory },
1218
- 'Now calculate sqrt(7006652). Use the calculator.',
1219
- streamConfig
1220
- );
1221
- logTurn('T2', conversationHistory);
1222
-
1223
- run = await createRun();
1224
- await runTurn(
1225
- { run, conversationHistory },
1226
- 'Compute 99 * 101, then 2^15, using calculator for each.',
1227
- streamConfig
1228
- );
1229
- logTurn('T3', conversationHistory);
1230
-
1231
- run = await createRun();
1232
- await runTurn(
1233
- { run, conversationHistory },
1234
- 'What is 314159 * 271828? Calculator please.',
1235
- streamConfig
1236
- );
1237
- logTurn('T4', conversationHistory);
1238
-
1239
- run = await createRun();
1240
- await runTurn(
1241
- { run, conversationHistory },
1242
- 'Compute 2^20 with calculator.',
1243
- streamConfig
1244
- );
1245
- logTurn('T5', conversationHistory);
1246
-
1247
- // Squeeze hard — OpenAI tool-schema overhead is lower than Anthropic,
1248
- // so we need tighter budgets to force pruning + summarization.
1249
- run = await createRun(800);
1250
- await runTurn(
1251
- { run, conversationHistory },
1252
- 'Calculate 999999 / 7 with calculator. Remind me of prior results too.',
1253
- streamConfig
1254
- );
1255
- logTurn('T6', conversationHistory);
1256
-
1257
- if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
1258
- run = await createRun(600);
1259
- await runTurn(
1260
- { run, conversationHistory },
1261
- 'What is 50 + 50? Calculator.',
1262
- streamConfig
1263
- );
1264
- logTurn('T7', conversationHistory);
1265
- }
1266
-
1267
- if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
1268
- run = await createRun(400);
1269
- await runTurn(
1270
- { run, conversationHistory },
1271
- 'What is 1+1? Calculator.',
1272
- streamConfig
1273
- );
1274
- logTurn('T8', conversationHistory);
1275
- }
1276
-
1277
- console.log(
1278
- ` OpenAI summarize events — start: ${spies.onSummarizeStartSpy.mock.calls.length}, complete: ${spies.onSummarizeCompleteSpy.mock.calls.length}`
1279
- );
1280
-
1281
- const { completePayload } = assertSummarizationEvents(spies);
1282
- assertSummaryRunStep(spies, getSummaryText(completePayload.summary));
1283
-
1284
- // Token accounting: summary tokenCount bounds
1285
- expect(completePayload.summary!.tokenCount ?? 0).toBeGreaterThan(10);
1286
- expect(completePayload.summary!.tokenCount ?? 0).toBeLessThan(1200);
1287
-
1288
- // Token accounting: collectedUsage from the post-summary model call
1289
- const validUsagePrePostSum = collectedUsage.filter(
1290
- (u: Partial<UsageMetadata>) =>
1291
- u.input_tokens != null && u.input_tokens > 0
1292
- );
1293
- expect(validUsagePrePostSum.length).toBeGreaterThan(0);
1294
-
1295
- // Verify tool calls still work after summarization
1296
- run = await createRun(2000);
1297
- await runTurn(
1298
- { run, conversationHistory },
1299
- 'One more: 123 + 456 + 789. Calculator.',
1300
- streamConfig
1301
- );
1302
- const hasPostSumCalc = latestContentParts.some(
1303
- (p) =>
1304
- p.type === ContentTypes.TOOL_CALL &&
1305
- (p as t.ToolCallContent).tool_call?.name === 'calculator'
1306
- );
1307
- expect(hasPostSumCalc).toBe(true);
1308
-
1309
- // Token accounting: post-summary usage must have valid tokens
1310
- const postSumUsage = collectedUsage.filter(
1311
- (u: Partial<UsageMetadata>) =>
1312
- u.input_tokens != null && u.input_tokens > 0
1313
- );
1314
- expect(postSumUsage.length).toBeGreaterThan(0);
1315
- const lastUsage = postSumUsage[postSumUsage.length - 1];
1316
- expect(lastUsage.output_tokens).toBeGreaterThan(0);
1317
- console.log(
1318
- ` OpenAI post-summary usage — input: ${lastUsage.input_tokens}, output: ${lastUsage.output_tokens}`
1319
- );
1320
-
1321
- expect(spies.onMessageDeltaSpy).toHaveBeenCalled();
1322
- console.log(
1323
- ` OpenAI summary: "${getSummaryText(completePayload.summary).substring(0, 200)}…"`
1324
- );
1325
- console.log(` Final messages: ${conversationHistory.length}`);
1326
- }, 240_000);
1327
- });
1328
-
1329
- // ---------------------------------------------------------------------------
1330
- // Cross-run lifecycle integration test (no API keys required)
1331
- // ---------------------------------------------------------------------------
1332
-
1333
- describe('Cross-run summary lifecycle (no API keys)', () => {
1334
- jest.setTimeout(60_000);
1335
-
1336
- const KNOWN_SUMMARY =
1337
- 'User asked about math: 2+2=4 and 3*5=15. Key context preserved.';
1338
- const INSTRUCTIONS = 'You are a helpful math tutor. Be concise.';
1339
- const streamConfig = {
1340
- configurable: { thread_id: 'cross-run-lifecycle' },
1341
- streamMode: 'values',
1342
- version: 'v2' as const,
1343
- };
1344
-
1345
- let getChatModelClassSpy: jest.SpyInstance;
1346
- const originalGetChatModelClass = providers.getChatModelClass;
1347
-
1348
- beforeEach(() => {
1349
- getChatModelClassSpy = jest
1350
- .spyOn(providers, 'getChatModelClass')
1351
- .mockImplementation(((provider: Providers) => {
1352
- if (provider === Providers.OPENAI) {
1353
- return class extends FakeListChatModel {
1354
- constructor(_options: any) {
1355
- super({ responses: [KNOWN_SUMMARY] });
1356
- }
1357
- } as any;
1358
- }
1359
- return originalGetChatModelClass(provider);
1360
- }) as typeof providers.getChatModelClass);
1361
- });
1362
-
1363
- afterEach(() => {
1364
- getChatModelClassSpy.mockRestore();
1365
- });
1366
-
1367
- test('full lifecycle: summarize → formatAgentMessages → new Run with correct indexTokenCountMap', async () => {
1368
- const spies = createSpies();
1369
- const conversationHistory: BaseMessage[] = [];
1370
- const tokenCounter = await createTokenCounter();
1371
-
1372
- const createRun = async (maxTokens: number): Promise<Run<t.IState>> => {
1373
- const { aggregateContent } = createContentAggregator();
1374
- const indexTokenCountMap = buildIndexTokenCountMap(
1375
- conversationHistory,
1376
- tokenCounter
1377
- );
1378
- const run = await Run.create<t.IState>({
1379
- runId: `cross-run-${Date.now()}`,
1380
- graphConfig: {
1381
- type: 'standard',
1382
- llmConfig: getLLMConfig(Providers.OPENAI),
1383
- instructions: INSTRUCTIONS,
1384
- maxContextTokens: maxTokens,
1385
- summarizationEnabled: true,
1386
- summarizationConfig: {
1387
- provider: Providers.OPENAI,
1388
- },
1389
- },
1390
- returnContent: true,
1391
- customHandlers: {
1392
- [GraphEvents.ON_RUN_STEP]: {
1393
- handle: (_event: string, data: t.StreamEventData): void => {
1394
- spies.onRunStepSpy(_event, data);
1395
- aggregateContent({
1396
- event: GraphEvents.ON_RUN_STEP,
1397
- data: data as t.RunStep,
1398
- });
1399
- },
1400
- },
1401
- [GraphEvents.ON_SUMMARIZE_START]: {
1402
- handle: (_event: string, data: t.StreamEventData): void => {
1403
- spies.onSummarizeStartSpy(data);
1404
- },
1405
- },
1406
- [GraphEvents.ON_SUMMARIZE_COMPLETE]: {
1407
- handle: (_event: string, data: t.StreamEventData): void => {
1408
- spies.onSummarizeCompleteSpy(data);
1409
- },
1410
- },
1411
- },
1412
- tokenCounter,
1413
- indexTokenCountMap,
1414
- });
1415
- return run;
1416
- };
1417
-
1418
- // --- Turn 1: longer exchange to build up token budget ---
1419
- let run = await createRun(4000);
1420
- run.Graph?.overrideTestModel(
1421
- [
1422
- 'The answer to 2+2 is 4. This is a basic arithmetic operation involving the addition of two integers. Addition is one of the four fundamental operations in mathematics alongside subtraction, multiplication, and division.',
1423
- ],
1424
- 1
1425
- );
1426
- await runTurn(
1427
- { run, conversationHistory },
1428
- 'Hello! I have several math questions for you today. Let us start with the basics. What is 2+2? Please provide a detailed explanation of the arithmetic.',
1429
- streamConfig
1430
- );
1431
- logTurn('T1', conversationHistory);
1432
- expect(conversationHistory.length).toBeGreaterThanOrEqual(2);
1433
-
1434
- // --- Turn 2: build up more conversation ---
1435
- run = await createRun(4000);
1436
- run.Graph?.overrideTestModel(
1437
- [
1438
- 'The result of 3 multiplied by 5 is 15. Multiplication can be thought of as repeated addition: 3+3+3+3+3 equals 15. This is another fundamental arithmetic operation that forms the basis of more advanced mathematical concepts.',
1439
- ],
1440
- 1
1441
- );
1442
- await runTurn(
1443
- { run, conversationHistory },
1444
- 'Great explanation! Now let us move on to multiplication. Can you compute 3 times 5 and explain the concept of multiplication as repeated addition in detail?',
1445
- streamConfig
1446
- );
1447
- logTurn('T2', conversationHistory);
1448
- expect(conversationHistory.length).toBeGreaterThanOrEqual(4);
1449
-
1450
- // --- Turn 3: tight context to force pruning and summarization ---
1451
- // Budget must be large enough to hold instructions + summary + at least
1452
- // one message after summarization fires (summary adds ~26 tokens to the
1453
- // system message, so 50 is too tight).
1454
- run = await createRun(150);
1455
- run.Graph?.overrideTestModel(
1456
- ['Got it, continuing with the summary context.'],
1457
- 1
1458
- );
1459
- await runTurn(
1460
- { run, conversationHistory },
1461
- 'Now summarize everything we discussed.',
1462
- streamConfig
1463
- );
1464
- logTurn('T3', conversationHistory);
1465
-
1466
- console.log(
1467
- ` Lifecycle events — start: ${spies.onSummarizeStartSpy.mock.calls.length}, complete: ${spies.onSummarizeCompleteSpy.mock.calls.length}`
1468
- );
1469
-
1470
- // --- Assert summarization fired ---
1471
- expect(spies.onSummarizeStartSpy).toHaveBeenCalled();
1472
- expect(spies.onSummarizeCompleteSpy).toHaveBeenCalled();
1473
-
1474
- const completePayload = spies.onSummarizeCompleteSpy.mock
1475
- .calls[0][0] as t.SummarizeCompleteEvent;
1476
- expect(getSummaryText(completePayload.summary)).toBe(KNOWN_SUMMARY);
1477
- expect(completePayload.summary!.type).toBe(ContentTypes.SUMMARY);
1478
- expect(completePayload.summary!.tokenCount ?? 0).toBeGreaterThan(0);
1479
-
1480
- const expectedTokenCount =
1481
- tokenCounter(new SystemMessage(KNOWN_SUMMARY)) +
1482
- SUMMARY_WRAPPER_OVERHEAD_TOKENS;
1483
- expect(completePayload.summary!.tokenCount).toBe(expectedTokenCount);
1484
-
1485
- const summaryBlock = completePayload.summary!;
1486
-
1487
- // --- Simulate cross-run persistence: build a TPayload as the host would store it ---
1488
- const persistedPayload: t.TPayload = [
1489
- {
1490
- role: 'assistant',
1491
- content: [
1492
- {
1493
- type: ContentTypes.SUMMARY,
1494
- text: getSummaryText(summaryBlock),
1495
- tokenCount: summaryBlock.tokenCount ?? 0,
1496
- } as any,
1497
- ],
1498
- },
1499
- {
1500
- role: 'user',
1501
- content: 'Now summarize everything we discussed so far.',
1502
- },
1503
- {
1504
- role: 'assistant',
1505
- content: 'Got it, continuing with the summary context.',
1506
- },
1507
- ];
1508
-
1509
- const persistedTokenMap: Record<number, number> = {
1510
- 0: summaryBlock.tokenCount ?? 0,
1511
- 1: tokenCounter(
1512
- new HumanMessage('Now summarize everything we discussed so far.')
1513
- ),
1514
- 2: tokenCounter(
1515
- new AIMessage('Got it, continuing with the summary context.')
1516
- ),
1517
- };
1518
-
1519
- // --- formatAgentMessages: convert persisted payload for next Run ---
1520
- const formatted = formatAgentMessages(persistedPayload, persistedTokenMap);
1521
-
1522
- // Summary is returned as metadata, NOT as a SystemMessage in the messages array.
1523
- // The caller forwards it to the run via initialSummary → AgentContext.setSummary().
1524
- expect(formatted.summary).toBeDefined();
1525
- expect(formatted.summary!.text).toBe(KNOWN_SUMMARY);
1526
- expect(formatted.summary!.tokenCount).toBe(summaryBlock.tokenCount);
1527
- // First message should NOT be a SystemMessage — only user/assistant messages remain.
1528
- expect(formatted.messages[0].constructor.name).not.toBe('SystemMessage');
1529
-
1530
- const formattedMap = (formatted.indexTokenCountMap || {}) as Record<
1531
- number,
1532
- number
1533
- >;
1534
- const formattedTotal = Object.values(formattedMap).reduce(
1535
- (sum: number, v: number) => sum + v,
1536
- 0
1537
- );
1538
- // Summary tokens no longer in the map — only user+assistant message tokens.
1539
- const expectedTotal = persistedTokenMap[1] + persistedTokenMap[2];
1540
- expect(formattedTotal).toBe(expectedTotal);
1541
-
1542
- console.log(
1543
- ` Formatted: ${formatted.messages.length} msgs, tokenMap total=${formattedTotal}, summary="${formatted.summary!.text.substring(0, 60)}..."`
1544
- );
1545
-
1546
- // --- Turn 4: new Run with formatted messages and updated indexTokenCountMap ---
1547
- const formattedTokenMapAsStrings: Record<string, number> = {};
1548
- for (const [k, v] of Object.entries(formattedMap)) {
1549
- formattedTokenMapAsStrings[String(k)] = v as number;
1550
- }
1551
-
1552
- const run4 = await Run.create<t.IState>({
1553
- runId: `cross-run-lifecycle-t4-${Date.now()}`,
1554
- graphConfig: {
1555
- type: 'standard',
1556
- llmConfig: getLLMConfig(Providers.OPENAI),
1557
- instructions: INSTRUCTIONS,
1558
- maxContextTokens: 2000,
1559
- summarizationEnabled: true,
1560
- summarizationConfig: {
1561
- provider: Providers.OPENAI,
1562
- },
1563
- initialSummary: formatted.summary,
1564
- },
1565
- returnContent: true,
1566
- customHandlers: buildHandlers(
1567
- [],
1568
- createContentAggregator().aggregateContent,
1569
- createSpies()
1570
- ),
1571
- tokenCounter,
1572
- indexTokenCountMap: formattedTokenMapAsStrings,
1573
- });
1574
-
1575
- run4.Graph?.overrideTestModel(['The square root of 16 is 4.'], 1);
1576
-
1577
- const t4Messages = [
1578
- ...formatted.messages,
1579
- new HumanMessage('What is sqrt(16)?'),
1580
- ];
1581
- const result = await run4.processStream(
1582
- { messages: t4Messages },
1583
- streamConfig as any
1584
- );
1585
-
1586
- expect(result).toBeDefined();
1587
-
1588
- const t4RunMessages = run4.getRunMessages();
1589
- expect(t4RunMessages).toBeDefined();
1590
- expect(t4RunMessages!.length).toBeGreaterThan(0);
1591
-
1592
- console.log(
1593
- ` Turn 4 produced ${t4RunMessages!.length} messages — lifecycle complete`
1594
- );
1595
- });
1596
-
1597
- test('tight context edge case: maxContextTokens as low as 1 does not infinite-loop', async () => {
1598
- const spies = createSpies();
1599
- const conversationHistory: BaseMessage[] = [];
1600
- const tokenCounter = await createTokenCounter();
1601
-
1602
- const createRun = async (maxTokens: number): Promise<Run<t.IState>> => {
1603
- const { aggregateContent } = createContentAggregator();
1604
- const indexTokenCountMap = buildIndexTokenCountMap(
1605
- conversationHistory,
1606
- tokenCounter
1607
- );
1608
- return Run.create<t.IState>({
1609
- runId: `tight-ctx-${Date.now()}`,
1610
- graphConfig: {
1611
- type: 'standard',
1612
- llmConfig: getLLMConfig(Providers.OPENAI),
1613
- instructions: INSTRUCTIONS,
1614
- maxContextTokens: maxTokens,
1615
- summarizationEnabled: true,
1616
- summarizationConfig: {
1617
- provider: Providers.OPENAI,
1618
- },
1619
- },
1620
- returnContent: true,
1621
- customHandlers: {
1622
- [GraphEvents.ON_RUN_STEP]: {
1623
- handle: (_event: string, data: t.StreamEventData): void => {
1624
- spies.onRunStepSpy(_event, data);
1625
- aggregateContent({
1626
- event: GraphEvents.ON_RUN_STEP,
1627
- data: data as t.RunStep,
1628
- });
1629
- },
1630
- },
1631
- [GraphEvents.ON_SUMMARIZE_START]: {
1632
- handle: (_event: string, data: t.StreamEventData): void => {
1633
- spies.onSummarizeStartSpy(data);
1634
- },
1635
- },
1636
- [GraphEvents.ON_SUMMARIZE_COMPLETE]: {
1637
- handle: (_event: string, data: t.StreamEventData): void => {
1638
- spies.onSummarizeCompleteSpy(data);
1639
- },
1640
- },
1641
- },
1642
- tokenCounter,
1643
- indexTokenCountMap,
1644
- });
1645
- };
1646
-
1647
- // Build a conversation first at normal context size
1648
- let run = await createRun(4000);
1649
- run.Graph?.overrideTestModel(
1650
- ['Sure, 2+2 is 4. Happy to help with more math questions.'],
1651
- 1
1652
- );
1653
- await runTurn({ run, conversationHistory }, 'What is 2+2?', streamConfig);
1654
- expect(conversationHistory.length).toBeGreaterThanOrEqual(2);
1655
-
1656
- // Now use absurdly tight context values — the guard must prevent infinite loops.
1657
- // Very small values may throw "empty_messages" (context too small for any message)
1658
- // which is fine — the point is we never hit GraphRecursionError.
1659
- for (const tightValue of [1, 10, 25, 50]) {
1660
- spies.onSummarizeStartSpy.mockClear();
1661
- spies.onSummarizeCompleteSpy.mockClear();
1662
-
1663
- run = await createRun(tightValue);
1664
- run.Graph?.overrideTestModel(['OK, noted.'], 1);
1665
-
1666
- let error: Error | undefined;
1667
- try {
1668
- await runTurn({ run, conversationHistory }, 'Continue.', streamConfig);
1669
- } catch (err) {
1670
- error = err as Error;
1671
- }
1672
-
1673
- if (error) {
1674
- // Clean errors (empty_messages) are acceptable for tiny context windows.
1675
- // GraphRecursionError means we looped — that's the bug we're guarding against.
1676
- expect(error.message).not.toContain('Recursion limit');
1677
- console.log(
1678
- ` maxContextTokens=${tightValue}: clean error (${error.message.substring(0, 80)})`
1679
- );
1680
- // Remove the failed turn's user message from history so subsequent iterations work
1681
- conversationHistory.pop();
1682
- } else {
1683
- const startCalls = spies.onSummarizeStartSpy.mock.calls.length;
1684
- const completeCalls = spies.onSummarizeCompleteSpy.mock.calls.length;
1685
- console.log(
1686
- ` maxContextTokens=${tightValue}: ok, start=${startCalls}, complete=${completeCalls}, msgs=${conversationHistory.length}`
1687
- );
1688
- // If summarization fired, it must have completed.
1689
- // Emergency truncation may allow success without summarization, so
1690
- // we don't require startCalls >= 1 — the test's goal is no infinite loop.
1691
- if (startCalls > 0) {
1692
- expect(completeCalls).toBe(startCalls);
1693
- }
1694
- }
1695
- }
1696
- });
1697
- });
1698
-
1699
- // ---------------------------------------------------------------------------
1700
- // Tight context with oversized tool results (FakeListChatModel — no API keys)
1701
- // ---------------------------------------------------------------------------
1702
-
1703
- describe('Tight context with oversized tool results (no API keys)', () => {
1704
- jest.setTimeout(60_000);
1705
-
1706
- const INSTRUCTIONS = 'You are a helpful assistant. Be concise.';
1707
- const SUMMARY_RESPONSE =
1708
- '## Goal\nUser needed help.\n\n## Progress\n### Done\n- Completed analysis.';
1709
- const streamConfig = {
1710
- configurable: { thread_id: 'tight-tool-ctx' },
1711
- streamMode: 'values',
1712
- version: 'v2' as const,
1713
- };
1714
-
1715
- let getChatModelClassSpy: jest.SpyInstance;
1716
- const originalGetChatModelClass = providers.getChatModelClass;
1717
-
1718
- beforeEach(() => {
1719
- getChatModelClassSpy = jest
1720
- .spyOn(providers, 'getChatModelClass')
1721
- .mockImplementation(((provider: Providers) => {
1722
- if (provider === Providers.OPENAI) {
1723
- return class extends FakeListChatModel {
1724
- constructor(_options: any) {
1725
- super({ responses: [SUMMARY_RESPONSE] });
1726
- }
1727
- } as any;
1728
- }
1729
- return originalGetChatModelClass(provider);
1730
- }) as typeof providers.getChatModelClass);
1731
- });
1732
-
1733
- afterEach(() => {
1734
- getChatModelClassSpy.mockRestore();
1735
- });
1736
-
1737
- test('oversized tool result + thinking-enabled model does not crash with tight context', async () => {
1738
- const spies = createSpies();
1739
- const tokenCounter = await createTokenCounter();
1740
-
1741
- // Build a conversation that mimics the real-world bug:
1742
- // HumanMessage → AIMessage with tool_calls + thinking blocks → large ToolMessage
1743
- const conversationHistory: BaseMessage[] = [
1744
- new HumanMessage('Inspect the page JavaScript.'),
1745
- new AIMessage({
1746
- content: [
1747
- {
1748
- type: 'thinking' as const,
1749
- thinking: 'Let me inspect the page using chrome-devtools MCP tool.',
1750
- },
1751
- { type: 'text' as const, text: 'I will inspect the page now.' },
1752
- {
1753
- type: 'tool_use' as const,
1754
- id: 'tool_mcp_1',
1755
- name: 'chrome_devtools_evaluate',
1756
- input: '{"expression": "document.body.innerHTML"}',
1757
- },
1758
- ],
1759
- tool_calls: [
1760
- {
1761
- id: 'tool_mcp_1',
1762
- name: 'chrome_devtools_evaluate',
1763
- args: { expression: 'document.body.innerHTML' },
1764
- },
1765
- ],
1766
- }),
1767
- new ToolMessage({
1768
- content: 'x'.repeat(5000), // Large MCP output simulating JS payload
1769
- tool_call_id: 'tool_mcp_1',
1770
- name: 'chrome_devtools_evaluate',
1771
- }),
1772
- ];
1773
-
1774
- const indexTokenCountMap = buildIndexTokenCountMap(
1775
- conversationHistory,
1776
- tokenCounter
1777
- );
1778
-
1779
- // Create a run with extremely tight context and thinking enabled
1780
- const { aggregateContent } = createContentAggregator();
1781
- const llmConfig = {
1782
- ...getLLMConfig(Providers.OPENAI),
1783
- thinking: { type: 'enabled', budget_tokens: 4000 },
1784
- };
1785
- const run = await Run.create<t.IState>({
1786
- runId: `tight-thinking-${Date.now()}`,
1787
- graphConfig: {
1788
- type: 'standard',
1789
- llmConfig: llmConfig as any,
1790
- instructions: INSTRUCTIONS,
1791
- maxContextTokens: 500, // Extremely tight — will prune everything
1792
- summarizationEnabled: true,
1793
- summarizationConfig: {
1794
- provider: Providers.OPENAI,
1795
- },
1796
- },
1797
- returnContent: true,
1798
- customHandlers: {
1799
- [GraphEvents.ON_RUN_STEP]: {
1800
- handle: (_event: string, data: t.StreamEventData): void => {
1801
- spies.onRunStepSpy(_event, data);
1802
- aggregateContent({
1803
- event: GraphEvents.ON_RUN_STEP,
1804
- data: data as t.RunStep,
1805
- });
1806
- },
1807
- },
1808
- [GraphEvents.ON_SUMMARIZE_START]: {
1809
- handle: (_event: string, data: t.StreamEventData): void => {
1810
- spies.onSummarizeStartSpy(data);
1811
- },
1812
- },
1813
- [GraphEvents.ON_SUMMARIZE_COMPLETE]: {
1814
- handle: (_event: string, data: t.StreamEventData): void => {
1815
- spies.onSummarizeCompleteSpy(data);
1816
- },
1817
- },
1818
- },
1819
- tokenCounter,
1820
- indexTokenCountMap,
1821
- });
1822
-
1823
- run.Graph?.overrideTestModel(['Analysis complete.'], 1);
1824
-
1825
- let error: Error | undefined;
1826
- try {
1827
- await run.processStream(
1828
- { messages: [...conversationHistory, new HumanMessage('Continue.')] },
1829
- streamConfig as any
1830
- );
1831
- } catch (err) {
1832
- error = err as Error;
1833
- }
1834
-
1835
- // The key assertion: no crash about "aggressive pruning removed all AI messages"
1836
- if (error) {
1837
- expect(error.message).not.toContain('aggressive pruning removed all AI');
1838
- expect(error.message).not.toContain('Recursion limit');
1839
- // empty_messages is acceptable for this tiny context window
1840
- console.log(
1841
- ` Tight thinking context: clean error (${error.message.substring(0, 100)})`
1842
- );
1843
- } else {
1844
- console.log(' Tight thinking context: completed without error');
1845
- }
1846
- });
1847
-
1848
- test('summarization survives when tool results dominate the context', async () => {
1849
- const spies = createSpies();
1850
- const tokenCounter = await createTokenCounter();
1851
-
1852
- // Build 3 turns with large tool outputs (~2000 chars each)
1853
- const conversationHistory: BaseMessage[] = [];
1854
-
1855
- const createRunHelper = async (
1856
- maxTokens: number
1857
- ): Promise<Run<t.IState>> => {
1858
- const { aggregateContent } = createContentAggregator();
1859
- const indexTokenCountMap = buildIndexTokenCountMap(
1860
- conversationHistory,
1861
- tokenCounter
1862
- );
1863
- return Run.create<t.IState>({
1864
- runId: `tool-dominate-${Date.now()}`,
1865
- graphConfig: {
1866
- type: 'standard',
1867
- llmConfig: getLLMConfig(Providers.OPENAI),
1868
- instructions: INSTRUCTIONS,
1869
- maxContextTokens: maxTokens,
1870
- summarizationEnabled: true,
1871
- summarizationConfig: {
1872
- provider: Providers.OPENAI,
1873
- },
1874
- },
1875
- returnContent: true,
1876
- customHandlers: {
1877
- [GraphEvents.ON_RUN_STEP]: {
1878
- handle: (_event: string, data: t.StreamEventData): void => {
1879
- spies.onRunStepSpy(_event, data);
1880
- aggregateContent({
1881
- event: GraphEvents.ON_RUN_STEP,
1882
- data: data as t.RunStep,
1883
- });
1884
- },
1885
- },
1886
- [GraphEvents.ON_SUMMARIZE_START]: {
1887
- handle: (_event: string, data: t.StreamEventData): void => {
1888
- spies.onSummarizeStartSpy(data);
1889
- },
1890
- },
1891
- [GraphEvents.ON_SUMMARIZE_COMPLETE]: {
1892
- handle: (_event: string, data: t.StreamEventData): void => {
1893
- spies.onSummarizeCompleteSpy(data);
1894
- },
1895
- },
1896
- },
1897
- tokenCounter,
1898
- indexTokenCountMap,
1899
- });
1900
- };
1901
-
1902
- // Turn 1
1903
- let run = await createRunHelper(4000);
1904
- run.Graph?.overrideTestModel(
1905
- [
1906
- 'Here is a long explanation about the analysis results that covers many details of the computation.',
1907
- ],
1908
- 1
1909
- );
1910
- await runTurn(
1911
- { run, conversationHistory },
1912
- 'Analyze the following data: ' + 'y'.repeat(2000),
1913
- streamConfig
1914
- );
1915
-
1916
- // Turn 2
1917
- run = await createRunHelper(4000);
1918
- run.Graph?.overrideTestModel(
1919
- [
1920
- 'More results from the second analysis including additional context and findings.',
1921
- ],
1922
- 1
1923
- );
1924
- await runTurn(
1925
- { run, conversationHistory },
1926
- 'Now analyze this: ' + 'z'.repeat(2000),
1927
- streamConfig
1928
- );
1929
-
1930
- // Turn 3 with tight context to force summarization
1931
- run = await createRunHelper(500);
1932
- run.Graph?.overrideTestModel(['Got it.'], 1);
1933
-
1934
- let error: Error | undefined;
1935
- try {
1936
- await runTurn(
1937
- { run, conversationHistory },
1938
- 'Summarize everything.',
1939
- streamConfig
1940
- );
1941
- } catch (err) {
1942
- error = err as Error;
1943
- }
1944
-
1945
- if (error) {
1946
- // empty_messages is acceptable, but not recursion errors
1947
- expect(error.message).not.toContain('Recursion limit');
1948
- console.log(
1949
- ` Tool-dominated context: clean error (${error.message.substring(0, 100)})`
1950
- );
1951
- } else {
1952
- // Summarization should have fired
1953
- expect(spies.onSummarizeStartSpy).toHaveBeenCalled();
1954
- expect(spies.onSummarizeCompleteSpy).toHaveBeenCalled();
1955
-
1956
- const completePayload = spies.onSummarizeCompleteSpy.mock
1957
- .calls[0][0] as t.SummarizeCompleteEvent;
1958
- expect(getSummaryText(completePayload.summary).length).toBeGreaterThan(
1959
- 10
1960
- );
1961
- console.log(
1962
- ` Tool-dominated context: summary="${getSummaryText(completePayload.summary).substring(0, 100)}…"`
1963
- );
1964
- }
1965
- });
1966
-
1967
- test('multiple summarization cycles preserve structured checkpoint format', async () => {
1968
- const spies = createSpies();
1969
- const conversationHistory: BaseMessage[] = [];
1970
- const tokenCounter = await createTokenCounter();
1971
-
1972
- const createRunHelper = async (
1973
- maxTokens: number
1974
- ): Promise<Run<t.IState>> => {
1975
- const { aggregateContent } = createContentAggregator();
1976
- const indexTokenCountMap = buildIndexTokenCountMap(
1977
- conversationHistory,
1978
- tokenCounter
1979
- );
1980
- return Run.create<t.IState>({
1981
- runId: `multi-sum-${Date.now()}`,
1982
- graphConfig: {
1983
- type: 'standard',
1984
- llmConfig: getLLMConfig(Providers.OPENAI),
1985
- instructions: INSTRUCTIONS,
1986
- maxContextTokens: maxTokens,
1987
- summarizationEnabled: true,
1988
- summarizationConfig: {
1989
- provider: Providers.OPENAI,
1990
- },
1991
- },
1992
- returnContent: true,
1993
- customHandlers: {
1994
- [GraphEvents.ON_RUN_STEP]: {
1995
- handle: (_event: string, data: t.StreamEventData): void => {
1996
- spies.onRunStepSpy(_event, data);
1997
- aggregateContent({
1998
- event: GraphEvents.ON_RUN_STEP,
1999
- data: data as t.RunStep,
2000
- });
2001
- },
2002
- },
2003
- [GraphEvents.ON_SUMMARIZE_START]: {
2004
- handle: (_event: string, data: t.StreamEventData): void => {
2005
- spies.onSummarizeStartSpy(data);
2006
- },
2007
- },
2008
- [GraphEvents.ON_SUMMARIZE_COMPLETE]: {
2009
- handle: (_event: string, data: t.StreamEventData): void => {
2010
- spies.onSummarizeCompleteSpy(data);
2011
- },
2012
- },
2013
- },
2014
- tokenCounter,
2015
- indexTokenCountMap,
2016
- });
2017
- };
2018
-
2019
- // Build conversation to trigger first summarization
2020
- let run = await createRunHelper(4000);
2021
- run.Graph?.overrideTestModel(
2022
- ['The answer to 2+2 is 4. This is basic addition.'],
2023
- 1
2024
- );
2025
- await runTurn(
2026
- { run, conversationHistory },
2027
- 'What is 2+2? Give me a detailed explanation.',
2028
- streamConfig
2029
- );
2030
-
2031
- run = await createRunHelper(4000);
2032
- run.Graph?.overrideTestModel(
2033
- ['3 times 5 is 15. Multiplication is repeated addition.'],
2034
- 1
2035
- );
2036
- await runTurn(
2037
- { run, conversationHistory },
2038
- 'Now explain 3 times 5 in detail with examples.',
2039
- streamConfig
2040
- );
2041
-
2042
- // Force first summarization
2043
- run = await createRunHelper(50);
2044
- run.Graph?.overrideTestModel(['Continuing after summary.'], 1);
2045
- try {
2046
- await runTurn({ run, conversationHistory }, 'Continue.', streamConfig);
2047
- } catch {
2048
- conversationHistory.pop(); // remove failed user message
2049
- }
2050
-
2051
- const firstSumCount = spies.onSummarizeCompleteSpy.mock.calls.length;
2052
-
2053
- // Build more conversation
2054
- run = await createRunHelper(4000);
2055
- run.Graph?.overrideTestModel(
2056
- ['The square root of 16 is 4. This is because 4 squared equals 16.'],
2057
- 1
2058
- );
2059
- await runTurn(
2060
- { run, conversationHistory },
2061
- 'What is sqrt(16)? Explain thoroughly.',
2062
- streamConfig
2063
- );
2064
-
2065
- // Force second summarization
2066
- run = await createRunHelper(50);
2067
- run.Graph?.overrideTestModel(['Continuing after second summary.'], 1);
2068
- try {
2069
- await runTurn(
2070
- { run, conversationHistory },
2071
- 'Continue again.',
2072
- streamConfig
2073
- );
2074
- } catch {
2075
- conversationHistory.pop();
2076
- }
2077
-
2078
- const totalSumCount = spies.onSummarizeCompleteSpy.mock.calls.length;
2079
- console.log(
2080
- ` Summarization cycles: first=${firstSumCount}, total=${totalSumCount}`
2081
- );
2082
-
2083
- // At least one summarization should have fired
2084
- expect(totalSumCount).toBeGreaterThanOrEqual(1);
2085
-
2086
- // The summary response from our fake model has structured format
2087
- const lastComplete = spies.onSummarizeCompleteSpy.mock.calls[
2088
- totalSumCount - 1
2089
- ][0] as t.SummarizeCompleteEvent;
2090
- const summaryText = getSummaryText(lastComplete.summary);
2091
-
2092
- // Our SUMMARY_RESPONSE includes ## Goal and ## Progress
2093
- expect(summaryText).toContain('## Goal');
2094
- expect(summaryText).toContain('## Progress');
2095
- console.log(
2096
- ` Last summary (${summaryText.length} chars): "${summaryText.substring(0, 150)}…"`
2097
- );
2098
- });
2099
-
2100
- test('update prompt is used when prior summary exists', async () => {
2101
- const spies = createSpies();
2102
- const conversationHistory: BaseMessage[] = [];
2103
- const tokenCounter = await createTokenCounter();
2104
-
2105
- // Track what system messages are passed to the summarizer model.
2106
- // Override _streamResponseChunks (not _generate) because FakeListChatModel
2107
- // has its own _streamResponseChunks that bypasses _generate during streaming.
2108
- const capturedSystemMessages: string[] = [];
2109
- getChatModelClassSpy.mockRestore();
2110
- getChatModelClassSpy = jest
2111
- .spyOn(providers, 'getChatModelClass')
2112
- .mockImplementation(((provider: Providers) => {
2113
- if (provider === Providers.OPENAI) {
2114
- return class extends FakeListChatModel {
2115
- constructor(_options: any) {
2116
- super({ responses: [SUMMARY_RESPONSE] });
2117
- }
2118
- async *_streamResponseChunks(
2119
- messages: any[],
2120
- options: any,
2121
- runManager?: any
2122
- ) {
2123
- // Capture the system message content for inspection
2124
- if (Array.isArray(messages)) {
2125
- for (const msg of messages) {
2126
- const msgType = msg.getType?.() ?? msg._getType?.();
2127
- if (msgType === 'system') {
2128
- const content =
2129
- typeof msg.content === 'string'
2130
- ? msg.content
2131
- : JSON.stringify(msg.content);
2132
- capturedSystemMessages.push(content);
2133
- }
2134
- }
2135
- }
2136
- yield* super._streamResponseChunks(messages, options, runManager);
2137
- }
2138
- } as any;
2139
- }
2140
- return originalGetChatModelClass(provider);
2141
- }) as typeof providers.getChatModelClass);
2142
-
2143
- const createRunHelper = async (
2144
- maxTokens: number,
2145
- initialSummary?: { text: string; tokenCount: number }
2146
- ): Promise<Run<t.IState>> => {
2147
- const { aggregateContent } = createContentAggregator();
2148
- const indexTokenCountMap = buildIndexTokenCountMap(
2149
- conversationHistory,
2150
- tokenCounter
2151
- );
2152
- return Run.create<t.IState>({
2153
- runId: `update-prompt-${Date.now()}`,
2154
- graphConfig: {
2155
- type: 'standard',
2156
- llmConfig: getLLMConfig(Providers.OPENAI),
2157
- instructions: INSTRUCTIONS,
2158
- maxContextTokens: maxTokens,
2159
- summarizationEnabled: true,
2160
- summarizationConfig: {
2161
- provider: Providers.OPENAI,
2162
- },
2163
- initialSummary,
2164
- },
2165
- returnContent: true,
2166
- customHandlers: {
2167
- [GraphEvents.ON_RUN_STEP]: {
2168
- handle: (_event: string, data: t.StreamEventData): void => {
2169
- spies.onRunStepSpy(_event, data);
2170
- aggregateContent({
2171
- event: GraphEvents.ON_RUN_STEP,
2172
- data: data as t.RunStep,
2173
- });
2174
- },
2175
- },
2176
- [GraphEvents.ON_SUMMARIZE_START]: {
2177
- handle: (_event: string, data: t.StreamEventData): void => {
2178
- spies.onSummarizeStartSpy(data);
2179
- },
2180
- },
2181
- [GraphEvents.ON_SUMMARIZE_COMPLETE]: {
2182
- handle: (_event: string, data: t.StreamEventData): void => {
2183
- spies.onSummarizeCompleteSpy(data);
2184
- },
2185
- },
2186
- },
2187
- tokenCounter,
2188
- indexTokenCountMap,
2189
- });
2190
- };
2191
-
2192
- // --- Step 1: Build conversation and trigger FIRST summarization (fresh prompt) ---
2193
- let run = await createRunHelper(4000);
2194
- run.Graph?.overrideTestModel(
2195
- [
2196
- 'The answer to 2+2 is 4. Addition is one of the four fundamental arithmetic operations.',
2197
- ],
2198
- 1
2199
- );
2200
- await runTurn(
2201
- { run, conversationHistory },
2202
- 'What is 2+2? Please provide a detailed explanation of the arithmetic.',
2203
- streamConfig
2204
- );
2205
-
2206
- run = await createRunHelper(4000);
2207
- run.Graph?.overrideTestModel(
2208
- [
2209
- '3 times 5 is 15. Multiplication can be thought of as repeated addition.',
2210
- ],
2211
- 1
2212
- );
2213
- await runTurn(
2214
- { run, conversationHistory },
2215
- 'Now explain 3 times 5 with a detailed worked example of multiplication.',
2216
- streamConfig
2217
- );
2218
-
2219
- // Force first summarization
2220
- run = await createRunHelper(50);
2221
- run.Graph?.overrideTestModel(['Continuing after first summary.'], 1);
2222
- try {
2223
- await runTurn(
2224
- { run, conversationHistory },
2225
- 'Now summarize everything we discussed.',
2226
- streamConfig
2227
- );
2228
- } catch {
2229
- conversationHistory.pop();
2230
- }
2231
-
2232
- const firstSumCount = spies.onSummarizeCompleteSpy.mock.calls.length;
2233
- console.log(` First summarization: ${firstSumCount} complete events`);
2234
-
2235
- // Extract summary from first round to use as initialSummary
2236
- let priorSummary: { text: string; tokenCount: number } | undefined;
2237
- if (firstSumCount > 0) {
2238
- const firstComplete = spies.onSummarizeCompleteSpy.mock.calls[
2239
- firstSumCount - 1
2240
- ][0] as t.SummarizeCompleteEvent;
2241
- priorSummary = {
2242
- text: getSummaryText(firstComplete.summary),
2243
- tokenCount: firstComplete.summary!.tokenCount ?? 0,
2244
- };
2245
- }
2246
-
2247
- // Clear captured messages — we only care about the SECOND summarization
2248
- const firstRoundCaptures = capturedSystemMessages.length;
2249
- capturedSystemMessages.length = 0;
2250
-
2251
- // --- Step 2: Build more conversation with initialSummary, trigger SECOND summarization ---
2252
- // Since initialSummary is set, the summarize node should use the update prompt.
2253
- run = await createRunHelper(4000, priorSummary);
2254
- run.Graph?.overrideTestModel(
2255
- ['The square root of 16 is 4, because 4 times 4 equals 16.'],
2256
- 1
2257
- );
2258
- await runTurn(
2259
- { run, conversationHistory },
2260
- 'What is the square root of 16? Give a very detailed explanation.',
2261
- streamConfig
2262
- );
2263
-
2264
- run = await createRunHelper(4000, priorSummary);
2265
- run.Graph?.overrideTestModel(
2266
- [
2267
- '100 divided by 4 is 25. Division distributes a total into equal groups.',
2268
- ],
2269
- 1
2270
- );
2271
- await runTurn(
2272
- { run, conversationHistory },
2273
- 'What is 100 divided by 4? Explain division with multiple examples.',
2274
- streamConfig
2275
- );
2276
-
2277
- // Force second summarization (with prior summary in AgentContext)
2278
- run = await createRunHelper(50, priorSummary);
2279
- run.Graph?.overrideTestModel(['Continuing after second summary.'], 1);
2280
- try {
2281
- await runTurn({ run, conversationHistory }, 'Continue.', streamConfig);
2282
- } catch {
2283
- conversationHistory.pop();
2284
- }
2285
-
2286
- const secondSumCount =
2287
- spies.onSummarizeCompleteSpy.mock.calls.length - firstSumCount;
2288
- console.log(
2289
- ` Second summarization: ${secondSumCount} complete events, ` +
2290
- `captured ${capturedSystemMessages.length} system messages (first round had ${firstRoundCaptures})`
2291
- );
2292
-
2293
- if (capturedSystemMessages.length > 0) {
2294
- // When a prior summary exists, verify the summarizer received context.
2295
- // With multi-pass (chunks 1+), the FRESH prompt + continuation prefix is
2296
- // used instead of the UPDATE prompt. Chunk 0 uses UPDATE only when it's
2297
- // a cross-cycle prior (tested in node.test.ts unit tests).
2298
- // In this integration test, verify that EITHER the UPDATE prompt OR the
2299
- // continuation prefix (context-from-earlier-messages) was used, confirming
2300
- // the prior summary was passed to the summarizer.
2301
- const usedUpdateOrContinuation = capturedSystemMessages.some(
2302
- (msg: string) =>
2303
- msg.includes('Merge the new messages') ||
2304
- msg.includes('Update the existing summary') ||
2305
- msg.includes('context-from-earlier-messages')
2306
- );
2307
- expect(usedUpdateOrContinuation).toBe(true);
2308
- console.log(
2309
- ` System message snippet: "${capturedSystemMessages[0].substring(0, 120)}…"`
2310
- );
2311
- } else if (firstRoundCaptures > 0) {
2312
- // First round used fresh prompt, second didn't fire — still validates first-round behavior
2313
- console.log(
2314
- ' Second summarization did not fire, but first round confirmed fresh prompt was used'
2315
- );
2316
- } else {
2317
- console.log(' No system messages captured');
2318
- }
2319
- });
2320
-
2321
- test('empty pruning context after summarization preserves latest user turn', async () => {
2322
- const spies = createSpies();
2323
- const tokenCounter = await createTokenCounter();
2324
-
2325
- // Build a conversation where EVERY message is too large to fit in the
2326
- // post-summary budget individually. This reproduces the real-world bug
2327
- // where context is empty after pruning, summarization fires, and the
2328
- // summarize node used to return 0 surviving messages.
2329
- const largePadding = ' detailed explanation'.repeat(80); // ~1600 chars
2330
- const conversationHistory: BaseMessage[] = [
2331
- new HumanMessage(`First question about math${largePadding}`),
2332
- new AIMessage(`The answer is 42${largePadding}`),
2333
- new HumanMessage(`Second question about physics${largePadding}`),
2334
- new AIMessage(`E equals mc squared${largePadding}`),
2335
- new HumanMessage(`Third question about chemistry${largePadding}`),
2336
- new AIMessage(`Water is H2O${largePadding}`),
2337
- ];
2338
-
2339
- const indexTokenCountMap = buildIndexTokenCountMap(
2340
- conversationHistory,
2341
- tokenCounter
2342
- );
2343
-
2344
- const { aggregateContent } = createContentAggregator();
2345
- const run = await Run.create<t.IState>({
2346
- runId: `empty-ctx-${Date.now()}`,
2347
- graphConfig: {
2348
- type: 'standard',
2349
- llmConfig: getLLMConfig(Providers.OPENAI),
2350
- instructions: INSTRUCTIONS,
2351
- maxContextTokens: 200, // Extremely tight — no message fits individually
2352
- summarizationEnabled: true,
2353
- summarizationConfig: {
2354
- provider: Providers.OPENAI,
2355
- },
2356
- },
2357
- returnContent: true,
2358
- customHandlers: {
2359
- [GraphEvents.ON_RUN_STEP]: {
2360
- handle: (_event: string, data: t.StreamEventData): void => {
2361
- spies.onRunStepSpy(_event, data);
2362
- aggregateContent({
2363
- event: GraphEvents.ON_RUN_STEP,
2364
- data: data as t.RunStep,
2365
- });
2366
- },
2367
- },
2368
- [GraphEvents.ON_SUMMARIZE_START]: {
2369
- handle: (_event: string, data: t.StreamEventData): void => {
2370
- spies.onSummarizeStartSpy(data);
2371
- },
2372
- },
2373
- [GraphEvents.ON_SUMMARIZE_COMPLETE]: {
2374
- handle: (_event: string, data: t.StreamEventData): void => {
2375
- spies.onSummarizeCompleteSpy(data);
2376
- },
2377
- },
2378
- },
2379
- tokenCounter,
2380
- indexTokenCountMap,
2381
- });
2382
-
2383
- // The agent model response for the post-summary turn
2384
- run.Graph?.overrideTestModel(['Here is the answer to your question.'], 1);
2385
-
2386
- const latestUserMessage = new HumanMessage(
2387
- 'What is the capital of France?'
2388
- );
2389
-
2390
- let error: Error | undefined;
2391
- try {
2392
- await run.processStream(
2393
- { messages: [...conversationHistory, latestUserMessage] },
2394
- streamConfig as any
2395
- );
2396
- } catch (err) {
2397
- error = err as Error;
2398
- }
2399
-
2400
- // Summarization should have fired
2401
- expect(spies.onSummarizeStartSpy).toHaveBeenCalled();
2402
-
2403
- // Key assertion: before the fix, this scenario always produced an
2404
- // empty_messages error because contextMessages was empty after
2405
- // summarization. After the fix, the latest turn's HumanMessage is
2406
- // extracted from messagesToRefine and the model responds successfully.
2407
- if (error) {
2408
- // If an error occurs, it must NOT be the empty_messages error that
2409
- // the fix was designed to prevent.
2410
- expect(error.message).not.toContain('empty_messages');
2411
- console.log(
2412
- ` Empty context fix: non-empty_messages error (${error.message.substring(0, 120)})`
2413
- );
2414
- } else {
2415
- // The model responded successfully — this is the expected outcome
2416
- console.log(' Empty context fix: model responded successfully');
2417
- }
2418
- });
2419
- });
2420
-
2421
- // ---------------------------------------------------------------------------
2422
- // Token accounting audit (requires API keys)
2423
- // ---------------------------------------------------------------------------
2424
-
2425
- const hasAnyApiKey = hasAnyEnv(['ANTHROPIC_API_KEY', 'OPENAI_API_KEY']);
2426
-
2427
- (hasAnyApiKey ? describe : describe.skip)('Token accounting audit', () => {
2428
- jest.setTimeout(180_000);
2429
-
2430
- const agentProvider = hasEnv('ANTHROPIC_API_KEY')
2431
- ? Providers.ANTHROPIC
2432
- : Providers.OPENAI;
2433
- const summarizationProvider = agentProvider;
2434
- const summarizationModel =
2435
- agentProvider === Providers.ANTHROPIC ? 'claude-haiku-4-5' : 'gpt-4.1-mini';
2436
-
2437
- const streamConfig = {
2438
- configurable: { thread_id: 'token-audit-e2e' },
2439
- streamMode: 'values',
2440
- version: 'v2' as const,
2441
- };
2442
-
2443
- const INSTRUCTIONS =
2444
- 'You are a math tutor. Use the calculator tool for ALL computations. Be concise.';
2445
-
2446
- test('token count map is accurate after summarization cycle', async () => {
2447
- const spies = createSpies();
2448
- let collectedUsage: UsageMetadata[] = [];
2449
- const conversationHistory = createSeededTokenAuditHistory();
2450
- const tokenCounter = await createTokenCounter();
2451
-
2452
- const createRun = async (maxTokens = 1200): Promise<Run<t.IState>> => {
2453
- collectedUsage = [];
2454
- const { aggregateContent } = createContentAggregator();
2455
- const indexTokenCountMap = buildIndexTokenCountMap(
2456
- conversationHistory,
2457
- tokenCounter
2458
- );
2459
- return createSummarizationRun({
2460
- agentProvider,
2461
- summarizationProvider,
2462
- summarizationModel,
2463
- maxContextTokens: maxTokens,
2464
- instructions:
2465
- 'You are a concise assistant. Preserve checkpoint context and answer in one short sentence.',
2466
- collectedUsage,
2467
- aggregateContent,
2468
- spies,
2469
- tokenCounter,
2470
- indexTokenCountMap,
2471
- maxSummaryTokens: 300,
2472
- tools: [],
2473
- llmConfigOverride: {
2474
- maxTokens: 128,
2475
- },
2476
- });
2477
- };
2478
-
2479
- const originalMap = buildIndexTokenCountMap(
2480
- conversationHistory,
2481
- tokenCounter
2482
- );
2483
- const originalTokenTotal = sumTokenCountMap(originalMap);
2484
- expect(originalTokenTotal).toBeGreaterThan(1200);
2485
-
2486
- const run = await createRun();
2487
- await runTurn(
2488
- { run, conversationHistory },
2489
- 'Acknowledge the preserved token audit context in one short sentence.',
2490
- streamConfig
2491
- );
2492
-
2493
- expect(spies.onSummarizeCompleteSpy).toHaveBeenCalled();
2494
-
2495
- const completePayload = spies.onSummarizeCompleteSpy.mock
2496
- .calls[0][0] as t.SummarizeCompleteEvent;
2497
- const summaryTokenCount = completePayload.summary!.tokenCount ?? 0;
2498
- expect(summaryTokenCount).toBeGreaterThan(10);
2499
- expect(summaryTokenCount).toBeLessThan(1500);
2500
- expect(summaryTokenCount).toBeLessThan(originalTokenTotal);
2501
-
2502
- const validUsage = collectedUsage.filter(
2503
- (u: Partial<UsageMetadata>) =>
2504
- u.input_tokens != null && u.input_tokens > 0
2505
- );
2506
- expect(validUsage.length).toBeGreaterThan(0);
2507
-
2508
- console.log(
2509
- ` Token audit: summary=${summaryTokenCount} tokens, ` +
2510
- `preTotal=${originalTokenTotal}, usageEntries=${validUsage.length}`
2511
- );
2512
- }, 180_000);
2513
-
2514
- test('summary tokenCount matches local token counter', async () => {
2515
- const spies = createSpies();
2516
- let collectedUsage: UsageMetadata[] = [];
2517
- const conversationHistory: BaseMessage[] = [];
2518
- const tokenCounter = await createTokenCounter();
2519
-
2520
- const createRun = async (maxTokens = 4000): Promise<Run<t.IState>> => {
2521
- collectedUsage = [];
2522
- const { aggregateContent } = createContentAggregator();
2523
- const indexTokenCountMap = buildIndexTokenCountMap(
2524
- conversationHistory,
2525
- tokenCounter
2526
- );
2527
- return createSummarizationRun({
2528
- agentProvider,
2529
- summarizationProvider,
2530
- summarizationModel,
2531
- maxContextTokens: maxTokens,
2532
- instructions: INSTRUCTIONS,
2533
- collectedUsage,
2534
- aggregateContent,
2535
- spies,
2536
- tokenCounter,
2537
- indexTokenCountMap,
2538
- });
2539
- };
2540
-
2541
- // Accumulate history at generous limits (6 turns)
2542
- let run = await createRun();
2543
- await runTurn(
2544
- { run, conversationHistory },
2545
- 'What is 100 * 200? Calculator.',
2546
- streamConfig
2547
- );
2548
-
2549
- run = await createRun();
2550
- await runTurn(
2551
- { run, conversationHistory },
2552
- 'Now compute 20000 + 5000. Calculator.',
2553
- streamConfig
2554
- );
2555
-
2556
- run = await createRun();
2557
- await runTurn(
2558
- { run, conversationHistory },
2559
- 'What is 25000 / 5? Calculator. Remind me of prior results.',
2560
- streamConfig
2561
- );
2562
-
2563
- run = await createRun();
2564
- await runTurn(
2565
- { run, conversationHistory },
2566
- 'Compute 2^16 with calculator.',
2567
- streamConfig
2568
- );
2569
-
2570
- run = await createRun();
2571
- await runTurn(
2572
- { run, conversationHistory },
2573
- 'What is 65536 + 5000? Calculator.',
2574
- streamConfig
2575
- );
2576
-
2577
- run = await createRun();
2578
- await runTurn(
2579
- { run, conversationHistory },
2580
- 'Calculate 70536 / 7. Calculator.',
2581
- streamConfig
2582
- );
2583
-
2584
- // Squeeze progressively to force summarization
2585
- for (const squeeze of [3500, 3200, 3100, 3000, 2800, 2500, 2000]) {
2586
- if (spies.onSummarizeStartSpy.mock.calls.length > 0) {
2587
- break;
2588
- }
2589
- run = await createRun(squeeze);
2590
- await runTurn(
2591
- { run, conversationHistory },
2592
- `What is ${squeeze} - 1000? Calculator.`,
2593
- streamConfig
2594
- );
2595
- }
2596
-
2597
- expect(spies.onSummarizeCompleteSpy).toHaveBeenCalled();
2598
-
2599
- const completePayload = spies.onSummarizeCompleteSpy.mock
2600
- .calls[0][0] as t.SummarizeCompleteEvent;
2601
- const summaryText = getSummaryText(completePayload.summary);
2602
- const reportedTokenCount = completePayload.summary!.tokenCount ?? 0;
2603
-
2604
- const localTokenCount =
2605
- tokenCounter(new SystemMessage(summaryText)) +
2606
- SUMMARY_WRAPPER_OVERHEAD_TOKENS;
2607
-
2608
- console.log(
2609
- ` Token match: reported=${reportedTokenCount}, local=${localTokenCount}`
2610
- );
2611
-
2612
- // Token counts may differ slightly due to encoding differences
2613
- // (claude vs o200k_base) and the 1.1× Claude correction factor.
2614
- // Allow up to 25% variance.
2615
- const variance =
2616
- Math.abs(reportedTokenCount - localTokenCount) / localTokenCount;
2617
- expect(variance).toBeLessThan(0.25);
2618
- }, 180_000);
2619
-
2620
- test('collectedUsage input_tokens decreases after summarization', async () => {
2621
- const spies = createSpies();
2622
- let collectedUsage: UsageMetadata[] = [];
2623
- const conversationHistory: BaseMessage[] = [];
2624
- const tokenCounter = await createTokenCounter();
2625
-
2626
- const createRun = async (maxTokens = 4000): Promise<Run<t.IState>> => {
2627
- collectedUsage = [];
2628
- const { aggregateContent } = createContentAggregator();
2629
- const indexTokenCountMap = buildIndexTokenCountMap(
2630
- conversationHistory,
2631
- tokenCounter
2632
- );
2633
- return createSummarizationRun({
2634
- agentProvider,
2635
- summarizationProvider,
2636
- summarizationModel,
2637
- maxContextTokens: maxTokens,
2638
- instructions: INSTRUCTIONS,
2639
- collectedUsage,
2640
- aggregateContent,
2641
- spies,
2642
- tokenCounter,
2643
- indexTokenCountMap,
2644
- });
2645
- };
2646
-
2647
- // Build up conversation (6 turns at generous budget)
2648
- let run = await createRun();
2649
- await runTurn(
2650
- { run, conversationHistory },
2651
- 'What is 12345 * 67? Calculator.',
2652
- streamConfig
2653
- );
2654
-
2655
- // Capture pre-summary input_tokens
2656
- const preSumUsage = collectedUsage.filter(
2657
- (u: Partial<UsageMetadata>) =>
2658
- u.input_tokens != null && u.input_tokens > 0
2659
- );
2660
- const lastPreUsage =
2661
- preSumUsage.length > 0 ? preSumUsage[preSumUsage.length - 1] : undefined;
2662
- const preSumInputTokens =
2663
- lastPreUsage?.input_tokens != null ? lastPreUsage.input_tokens : 0;
2664
-
2665
- run = await createRun();
2666
- await runTurn(
2667
- { run, conversationHistory },
2668
- 'Now divide that by 13. Calculator. Also multiply by 7.',
2669
- streamConfig
2670
- );
2671
-
2672
- run = await createRun();
2673
- await runTurn(
2674
- { run, conversationHistory },
2675
- 'Compute 999 * 888. Calculator.',
2676
- streamConfig
2677
- );
2678
-
2679
- run = await createRun();
2680
- await runTurn(
2681
- { run, conversationHistory },
2682
- 'What is 2^10? Calculator.',
2683
- streamConfig
2684
- );
2685
-
2686
- run = await createRun();
2687
- await runTurn(
2688
- { run, conversationHistory },
2689
- 'Calculate 1024 + 5000. Calculator. List all prior results.',
2690
- streamConfig
2691
- );
2692
-
2693
- run = await createRun();
2694
- await runTurn(
2695
- { run, conversationHistory },
2696
- 'What is 6024 * 3? Calculator.',
2697
- streamConfig
2698
- );
2699
-
2700
- // Squeeze progressively to force summarization
2701
- for (const squeeze of [3500, 3200, 3100, 3000, 2800, 2500, 2000]) {
2702
- if (spies.onSummarizeStartSpy.mock.calls.length > 0) {
2703
- break;
2704
- }
2705
- run = await createRun(squeeze);
2706
- await runTurn(
2707
- { run, conversationHistory },
2708
- `What is ${squeeze} - 1000? Calculator.`,
2709
- streamConfig
2710
- );
2711
- }
2712
-
2713
- // Post-summary turn
2714
- run = await createRun(4000);
2715
- await runTurn(
2716
- { run, conversationHistory },
2717
- 'What is 10 + 10? Calculator.',
2718
- streamConfig
2719
- );
2720
-
2721
- const postSumUsage = collectedUsage.filter(
2722
- (u: Partial<UsageMetadata>) =>
2723
- u.input_tokens != null && u.input_tokens > 0
2724
- );
2725
- const lastPostUsage =
2726
- postSumUsage.length > 0
2727
- ? postSumUsage[postSumUsage.length - 1]
2728
- : undefined;
2729
- const postSumInputTokens =
2730
- lastPostUsage?.input_tokens != null ? lastPostUsage.input_tokens : 0;
2731
-
2732
- console.log(
2733
- ` Input tokens: pre-summary=${preSumInputTokens}, post-summary=${postSumInputTokens}`
2734
- );
2735
-
2736
- // After summarization, the context should be smaller, so input tokens should decrease
2737
- // (compared to what they would have been without summarization)
2738
- // We compare against the pre-summary value which had fewer messages
2739
- // The post-summary turn should have fewer input tokens than the last pre-summary turn
2740
- // that had the full context (before summarization compressed it)
2741
- if (spies.onSummarizeCompleteSpy.mock.calls.length > 0) {
2742
- expect(postSumInputTokens).toBeGreaterThan(0);
2743
- expect(preSumInputTokens).toBeGreaterThan(0);
2744
- console.log(
2745
- ` Summarization fired: ${spies.onSummarizeCompleteSpy.mock.calls.length} times`
2746
- );
2747
- }
2748
- }, 180_000);
2749
- });
2750
-
2751
- // ---------------------------------------------------------------------------
2752
- // Enrichment and prompt selection (FakeListChatModel — no API keys)
2753
- // ---------------------------------------------------------------------------
2754
-
2755
- describe('Enrichment and prompt selection (no API keys)', () => {
2756
- jest.setTimeout(60_000);
2757
-
2758
- const INSTRUCTIONS = 'You are a helpful assistant.';
2759
- const streamConfig = {
2760
- configurable: { thread_id: 'enrichment-tests' },
2761
- streamMode: 'values',
2762
- version: 'v2' as const,
2763
- };
2764
-
2765
- let getChatModelClassSpy: jest.SpyInstance;
2766
- const originalGetChatModelClass = providers.getChatModelClass;
2767
-
2768
- // The fake summarizer includes a basic summary without tool failures section
2769
- const BASE_SUMMARY =
2770
- '## Goal\nHelp user.\n\n## Progress\n### Done\n- Assisted user.';
2771
-
2772
- beforeEach(() => {
2773
- getChatModelClassSpy = jest
2774
- .spyOn(providers, 'getChatModelClass')
2775
- .mockImplementation(((provider: Providers) => {
2776
- if (provider === Providers.OPENAI) {
2777
- return class extends FakeListChatModel {
2778
- constructor(_options: any) {
2779
- super({ responses: [BASE_SUMMARY] });
2780
- }
2781
- } as any;
2782
- }
2783
- return originalGetChatModelClass(provider);
2784
- }) as typeof providers.getChatModelClass);
2785
- });
2786
-
2787
- afterEach(() => {
2788
- getChatModelClassSpy.mockRestore();
2789
- });
2790
-
2791
- test('tool failure enrichment appended to summary', async () => {
2792
- const spies = createSpies();
2793
- const tokenCounter = await createTokenCounter();
2794
-
2795
- // Build conversation with a tool failure
2796
- const conversationHistory: BaseMessage[] = [
2797
- new HumanMessage('Run the linter on my code.'),
2798
- new AIMessage({
2799
- content: [
2800
- { type: 'text' as const, text: 'Running the linter now.' },
2801
- {
2802
- type: 'tool_use' as const,
2803
- id: 'tool_lint_1',
2804
- name: 'run_linter',
2805
- input: '{"path": "/src/index.ts"}',
2806
- },
2807
- ],
2808
- tool_calls: [
2809
- {
2810
- id: 'tool_lint_1',
2811
- name: 'run_linter',
2812
- args: { path: '/src/index.ts' },
2813
- },
2814
- ],
2815
- }),
2816
- new ToolMessage({
2817
- content: 'Error: ENOENT: no such file or directory, open /src/index.ts',
2818
- tool_call_id: 'tool_lint_1',
2819
- name: 'run_linter',
2820
- status: 'error',
2821
- }),
2822
- new AIMessage('The linter failed because the file was not found.'),
2823
- new HumanMessage('Try again with the correct path.'),
2824
- new AIMessage(
2825
- 'I will try again. The correct path would need to be provided by you since I cannot verify file existence.'
2826
- ),
2827
- ];
2828
-
2829
- const indexTokenCountMap = buildIndexTokenCountMap(
2830
- conversationHistory,
2831
- tokenCounter
2832
- );
2833
-
2834
- const { aggregateContent } = createContentAggregator();
2835
- const run = await Run.create<t.IState>({
2836
- runId: `tool-failure-enrich-${Date.now()}`,
2837
- graphConfig: {
2838
- type: 'standard',
2839
- llmConfig: getLLMConfig(Providers.OPENAI),
2840
- instructions: INSTRUCTIONS,
2841
- maxContextTokens: 50, // Very tight to force summarization
2842
- summarizationEnabled: true,
2843
- summarizationConfig: {
2844
- provider: Providers.OPENAI,
2845
- },
2846
- },
2847
- returnContent: true,
2848
- customHandlers: {
2849
- [GraphEvents.ON_RUN_STEP]: {
2850
- handle: (_event: string, data: t.StreamEventData): void => {
2851
- spies.onRunStepSpy(_event, data);
2852
- aggregateContent({
2853
- event: GraphEvents.ON_RUN_STEP,
2854
- data: data as t.RunStep,
2855
- });
2856
- },
2857
- },
2858
- [GraphEvents.ON_SUMMARIZE_START]: {
2859
- handle: (_event: string, data: t.StreamEventData): void => {
2860
- spies.onSummarizeStartSpy(data);
2861
- },
2862
- },
2863
- [GraphEvents.ON_SUMMARIZE_COMPLETE]: {
2864
- handle: (_event: string, data: t.StreamEventData): void => {
2865
- spies.onSummarizeCompleteSpy(data);
2866
- },
2867
- },
2868
- },
2869
- tokenCounter,
2870
- indexTokenCountMap,
2871
- });
2872
-
2873
- run.Graph?.overrideTestModel(['Understood, awaiting correct path.'], 1);
2874
-
2875
- try {
2876
- await run.processStream(
2877
- {
2878
- messages: [
2879
- ...conversationHistory,
2880
- new HumanMessage('What happened?'),
2881
- ],
2882
- },
2883
- streamConfig as any
2884
- );
2885
- } catch {
2886
- // empty_messages is acceptable for tiny context
2887
- }
2888
-
2889
- if (spies.onSummarizeCompleteSpy.mock.calls.length > 0) {
2890
- const completePayload = spies.onSummarizeCompleteSpy.mock
2891
- .calls[0][0] as t.SummarizeCompleteEvent;
2892
- const summaryText = getSummaryText(completePayload.summary);
2893
-
2894
- // The enrichment step in node.ts should append ## Tool Failures
2895
- expect(summaryText).toContain('## Tool Failures');
2896
- expect(summaryText).toContain('run_linter');
2897
- expect(summaryText).toContain('ENOENT');
2898
-
2899
- console.log(` Enriched summary: "${summaryText.substring(0, 200)}…"`);
2900
- } else {
2901
- // If summarization didn't fire due to context being too tight,
2902
- // the test is inconclusive but not a failure
2903
- console.log(
2904
- ' Summarization did not fire (context too tight for any message)'
2905
- );
2906
- }
2907
- });
2908
- });
2909
-
2910
- // ---------------------------------------------------------------------------
2911
- // Summarization deduplication and correctness (FakeListChatModel — no API keys)
2912
- // ---------------------------------------------------------------------------
2913
-
2914
- describe('Summarization deduplication correctness (no API keys)', () => {
2915
- jest.setTimeout(60_000);
2916
-
2917
- const INSTRUCTIONS =
2918
- 'You are a math tutor. Use the calculator tool for ALL computations. Be concise.';
2919
- const streamConfig = {
2920
- configurable: { thread_id: 'multi-pass-correctness' },
2921
- streamMode: 'values',
2922
- version: 'v2' as const,
2923
- };
2924
-
2925
- let getChatModelClassSpy: jest.SpyInstance | undefined;
2926
- const originalGetChatModelClass = providers.getChatModelClass;
2927
-
2928
- afterEach(() => {
2929
- if (getChatModelClassSpy) {
2930
- getChatModelClassSpy.mockRestore();
2931
- }
2932
- });
2933
-
2934
- test('summarization does not produce duplicate section headers', async () => {
2935
- const spies = createSpies();
2936
- const conversationHistory: BaseMessage[] = [];
2937
- const tokenCounter = await createTokenCounter();
2938
-
2939
- // Track what the summarizer receives for each chunk
2940
- const capturedSystemMessages: string[] = [];
2941
- const capturedHumanMessages: string[] = [];
2942
-
2943
- // Return different summaries for each chunk — chunk 2 returns a proper
2944
- // comprehensive summary that does NOT duplicate ## Goal
2945
- let chunkCallCount = 0;
2946
- const chunkResponses = [
2947
- '## Goal\nUser needs math computations.\n\n## Progress\n### Done\n- Computed 2+2=4.\n- Computed 3*5=15.',
2948
- '## Goal\nUser needs comprehensive math help including basic and advanced operations.\n\n## Progress\n### Done\n- Computed 2+2=4.\n- Computed 3*5=15.\n- Computed sqrt(16)=4.\n- Computed 100/4=25.\n\n## Next Steps\nContinue with more calculations.',
2949
- ];
2950
-
2951
- getChatModelClassSpy = jest
2952
- .spyOn(providers, 'getChatModelClass')
2953
- .mockImplementation(((provider: Providers) => {
2954
- if (provider === Providers.OPENAI) {
2955
- return class extends FakeListChatModel {
2956
- constructor(_options: any) {
2957
- const response =
2958
- chunkResponses[chunkCallCount] ??
2959
- chunkResponses[chunkResponses.length - 1];
2960
- chunkCallCount++;
2961
- super({ responses: [response] });
2962
- }
2963
- async *_streamResponseChunks(
2964
- messages: any[],
2965
- options: any,
2966
- runManager?: any
2967
- ) {
2968
- for (const msg of messages) {
2969
- const msgType = msg.getType?.() ?? msg._getType?.();
2970
- const content =
2971
- typeof msg.content === 'string'
2972
- ? msg.content
2973
- : JSON.stringify(msg.content);
2974
- if (msgType === 'system') capturedSystemMessages.push(content);
2975
- if (msgType === 'human') capturedHumanMessages.push(content);
2976
- }
2977
- yield* super._streamResponseChunks(messages, options, runManager);
2978
- }
2979
- } as any;
2980
- }
2981
- return originalGetChatModelClass(provider);
2982
- }) as typeof providers.getChatModelClass);
2983
-
2984
- const createRunHelper = async (
2985
- maxTokens: number
2986
- ): Promise<Run<t.IState>> => {
2987
- const { aggregateContent } = createContentAggregator();
2988
- const indexTokenCountMap = buildIndexTokenCountMap(
2989
- conversationHistory,
2990
- tokenCounter
2991
- );
2992
- return Run.create<t.IState>({
2993
- runId: `multi-pass-dedup-${Date.now()}`,
2994
- graphConfig: {
2995
- type: 'standard',
2996
- llmConfig: getLLMConfig(Providers.OPENAI),
2997
- instructions: INSTRUCTIONS,
2998
- maxContextTokens: maxTokens,
2999
- summarizationEnabled: true,
3000
- summarizationConfig: {
3001
- provider: Providers.OPENAI,
3002
- parameters: {},
3003
- },
3004
- },
3005
- returnContent: true,
3006
- customHandlers: {
3007
- [GraphEvents.ON_RUN_STEP]: {
3008
- handle: (_event: string, data: t.StreamEventData): void => {
3009
- spies.onRunStepSpy(_event, data);
3010
- aggregateContent({
3011
- event: GraphEvents.ON_RUN_STEP,
3012
- data: data as t.RunStep,
3013
- });
3014
- },
3015
- },
3016
- [GraphEvents.ON_SUMMARIZE_START]: {
3017
- handle: (_event: string, data: t.StreamEventData): void => {
3018
- spies.onSummarizeStartSpy(data);
3019
- },
3020
- },
3021
- [GraphEvents.ON_SUMMARIZE_COMPLETE]: {
3022
- handle: (_event: string, data: t.StreamEventData): void => {
3023
- spies.onSummarizeCompleteSpy(data);
3024
- },
3025
- },
3026
- },
3027
- tokenCounter,
3028
- indexTokenCountMap,
3029
- });
3030
- };
3031
-
3032
- // Build up enough conversation to trigger summarization
3033
- // Build enough conversation history to trigger summarization
3034
- let run = await createRunHelper(4000);
3035
- run.Graph?.overrideTestModel(
3036
- ['The answer to 2+2 is 4. Basic addition.'],
3037
- 1
3038
- );
3039
- await runTurn(
3040
- { run, conversationHistory },
3041
- 'What is 2+2? Explain in detail.',
3042
- streamConfig
3043
- );
3044
-
3045
- run = await createRunHelper(4000);
3046
- run.Graph?.overrideTestModel(
3047
- ['3 times 5 is 15. Multiplication is repeated addition.'],
3048
- 1
3049
- );
3050
- await runTurn(
3051
- { run, conversationHistory },
3052
- 'Now explain 3 times 5 in great detail with many examples.',
3053
- streamConfig
3054
- );
3055
-
3056
- run = await createRunHelper(4000);
3057
- run.Graph?.overrideTestModel(
3058
- ['The square root of 16 is 4, because 4*4=16.'],
3059
- 1
3060
- );
3061
- await runTurn(
3062
- { run, conversationHistory },
3063
- 'What is sqrt(16)? Give a thorough step-by-step explanation.',
3064
- streamConfig
3065
- );
3066
-
3067
- run = await createRunHelper(4000);
3068
- run.Graph?.overrideTestModel(
3069
- [
3070
- '100 divided by 4 is 25. Division distributes a total into equal parts.',
3071
- ],
3072
- 1
3073
- );
3074
- await runTurn(
3075
- { run, conversationHistory },
3076
- 'What is 100/4? Explain division with multiple worked examples.',
3077
- streamConfig
3078
- );
3079
-
3080
- // Now force summarization with tight context
3081
- run = await createRunHelper(50);
3082
- run.Graph?.overrideTestModel(['Continuing after summary.'], 1);
3083
- try {
3084
- await runTurn({ run, conversationHistory }, 'Continue.', streamConfig);
3085
- } catch {
3086
- conversationHistory.pop(); // remove failed user message
3087
- }
3088
-
3089
- // Assert summarization fired
3090
- const sumCount = spies.onSummarizeCompleteSpy.mock.calls.length;
3091
- console.log(
3092
- ` Dedup: ${sumCount} summarization(s), ${chunkCallCount} chunk LLM calls, ` +
3093
- `${capturedSystemMessages.length} system messages captured`
3094
- );
3095
-
3096
- expect(sumCount).toBeGreaterThanOrEqual(1);
3097
-
3098
- const lastComplete = spies.onSummarizeCompleteSpy.mock.calls[
3099
- sumCount - 1
3100
- ][0] as t.SummarizeCompleteEvent;
3101
- const summaryText = getSummaryText(lastComplete.summary);
3102
-
3103
- // KEY ASSERTION: ## Goal should appear exactly ONCE (no duplication)
3104
- const goalCount = (summaryText.match(/## Goal/g) || []).length;
3105
- expect(goalCount).toBe(1);
3106
-
3107
- // ## Progress should also appear exactly once
3108
- const progressCount = (summaryText.match(/## Progress/g) || []).length;
3109
- expect(progressCount).toBe(1);
3110
-
3111
- // tokenCount must be > 0 (tokenCounter is provided)
3112
- expect(lastComplete.summary!.tokenCount).toBeGreaterThan(0);
3113
-
3114
- console.log(
3115
- ` Summary (${summaryText.length} chars, ${lastComplete.summary!.tokenCount} tokens):\n` +
3116
- ` "${summaryText.substring(0, 300)}…"`
3117
- );
3118
- });
3119
-
3120
- test('repeated summarization cycles do not accumulate duplicate sections', async () => {
3121
- // This test verifies that when summarization fires multiple times across
3122
- // runs, each summary is clean (no duplicate section headers).
3123
- // The cross-cycle prompt selection (UPDATE for chunk 0, FRESH for chunk 1+)
3124
- // is tested in unit tests (node.test.ts). This integration test focuses on
3125
- // the end-to-end outcome.
3126
- const spies = createSpies();
3127
- const conversationHistory: BaseMessage[] = [];
3128
- const tokenCounter = await createTokenCounter();
3129
-
3130
- // The summarizer always returns a clean single-section summary
3131
- const summaryResponse =
3132
- '## Goal\nMath tutoring.\n\n## Progress\n### Done\n- Completed operations.';
3133
-
3134
- getChatModelClassSpy = jest
3135
- .spyOn(providers, 'getChatModelClass')
3136
- .mockImplementation(((provider: Providers) => {
3137
- if (provider === Providers.OPENAI) {
3138
- return class extends FakeListChatModel {
3139
- constructor(_options: any) {
3140
- super({ responses: [summaryResponse] });
3141
- }
3142
- } as any;
3143
- }
3144
- return originalGetChatModelClass(provider);
3145
- }) as typeof providers.getChatModelClass);
3146
-
3147
- const createRunHelper = async (
3148
- maxTokens: number,
3149
- initialSummary?: { text: string; tokenCount: number }
3150
- ): Promise<Run<t.IState>> => {
3151
- const { aggregateContent } = createContentAggregator();
3152
- const indexTokenCountMap = buildIndexTokenCountMap(
3153
- conversationHistory,
3154
- tokenCounter
3155
- );
3156
- return Run.create<t.IState>({
3157
- runId: `repeat-sum-${Date.now()}`,
3158
- graphConfig: {
3159
- type: 'standard',
3160
- llmConfig: getLLMConfig(Providers.OPENAI),
3161
- instructions: INSTRUCTIONS,
3162
- maxContextTokens: maxTokens,
3163
- summarizationEnabled: true,
3164
- summarizationConfig: {
3165
- provider: Providers.OPENAI,
3166
- },
3167
- initialSummary,
3168
- },
3169
- returnContent: true,
3170
- customHandlers: {
3171
- [GraphEvents.ON_RUN_STEP]: {
3172
- handle: (_event: string, data: t.StreamEventData): void => {
3173
- spies.onRunStepSpy(_event, data);
3174
- aggregateContent({
3175
- event: GraphEvents.ON_RUN_STEP,
3176
- data: data as t.RunStep,
3177
- });
3178
- },
3179
- },
3180
- [GraphEvents.ON_SUMMARIZE_START]: {
3181
- handle: (_event: string, data: t.StreamEventData): void => {
3182
- spies.onSummarizeStartSpy(data);
3183
- },
3184
- },
3185
- [GraphEvents.ON_SUMMARIZE_COMPLETE]: {
3186
- handle: (_event: string, data: t.StreamEventData): void => {
3187
- spies.onSummarizeCompleteSpy(data);
3188
- },
3189
- },
3190
- },
3191
- tokenCounter,
3192
- indexTokenCountMap,
3193
- });
3194
- };
3195
-
3196
- // --- Cycle 1: Build conversation and trigger summarization ---
3197
- let run = await createRunHelper(4000);
3198
- run.Graph?.overrideTestModel(['Answer 1 with detailed explanation.'], 1);
3199
- await runTurn({ run, conversationHistory }, 'Question 1.', streamConfig);
3200
-
3201
- run = await createRunHelper(4000);
3202
- run.Graph?.overrideTestModel(['Answer 2 with more explanation.'], 1);
3203
- await runTurn({ run, conversationHistory }, 'Question 2.', streamConfig);
3204
-
3205
- run = await createRunHelper(50);
3206
- run.Graph?.overrideTestModel(['OK.'], 1);
3207
- try {
3208
- await runTurn({ run, conversationHistory }, 'Summarize.', streamConfig);
3209
- } catch {
3210
- conversationHistory.pop();
3211
- }
3212
-
3213
- const cycle1SumCount = spies.onSummarizeCompleteSpy.mock.calls.length;
3214
-
3215
- // Extract the summary from cycle 1 for use as initialSummary in cycle 2
3216
- let priorSummary: { text: string; tokenCount: number } | undefined;
3217
- if (cycle1SumCount > 0) {
3218
- const lastComplete = spies.onSummarizeCompleteSpy.mock.calls[
3219
- cycle1SumCount - 1
3220
- ][0] as t.SummarizeCompleteEvent;
3221
- priorSummary = {
3222
- text: getSummaryText(lastComplete.summary),
3223
- tokenCount: lastComplete.summary!.tokenCount ?? 0,
3224
- };
3225
- }
3226
-
3227
- // --- Cycle 2: More conversation with prior summary, trigger again ---
3228
- run = await createRunHelper(4000, priorSummary);
3229
- run.Graph?.overrideTestModel(['Cycle 2 answer.'], 1);
3230
- await runTurn(
3231
- { run, conversationHistory },
3232
- 'Cycle 2 question.',
3233
- streamConfig
3234
- );
3235
-
3236
- run = await createRunHelper(50, priorSummary);
3237
- run.Graph?.overrideTestModel(['OK cycle 2.'], 1);
3238
- try {
3239
- await runTurn(
3240
- { run, conversationHistory },
3241
- 'Summarize again.',
3242
- streamConfig
3243
- );
3244
- } catch {
3245
- conversationHistory.pop();
3246
- }
3247
-
3248
- const totalSumCount = spies.onSummarizeCompleteSpy.mock.calls.length;
3249
- console.log(
3250
- ` Repeated summarization: cycle1=${cycle1SumCount}, total=${totalSumCount}`
3251
- );
3252
-
3253
- // At least one summarization should have fired
3254
- expect(totalSumCount).toBeGreaterThanOrEqual(1);
3255
-
3256
- // Every summary should have exactly one ## Goal (no duplicates)
3257
- for (let i = 0; i < totalSumCount; i++) {
3258
- const complete = spies.onSummarizeCompleteSpy.mock.calls[
3259
- i
3260
- ][0] as t.SummarizeCompleteEvent;
3261
- const text = getSummaryText(complete.summary);
3262
- const goalCount = (text.match(/## Goal/g) || []).length;
3263
- if (goalCount !== 1) {
3264
- console.log(
3265
- ` Summary ${i} has ${goalCount} '## Goal' sections:\n "${text.substring(0, 300)}…"`
3266
- );
3267
- }
3268
- expect(goalCount).toBe(1);
3269
- expect(complete.summary!.tokenCount).toBeGreaterThan(0);
3270
- }
3271
- });
3272
-
3273
- test('conversation continues after summarization', async () => {
3274
- const spies = createSpies();
3275
- const conversationHistory: BaseMessage[] = [];
3276
- const tokenCounter = await createTokenCounter();
3277
-
3278
- // Summarizer returns a concise summary
3279
- const summaryResponse =
3280
- '## Goal\nMath help.\n\n## Progress\n### Done\n- Basic operations completed.';
3281
-
3282
- getChatModelClassSpy = jest
3283
- .spyOn(providers, 'getChatModelClass')
3284
- .mockImplementation(((provider: Providers) => {
3285
- if (provider === Providers.OPENAI) {
3286
- return class extends FakeListChatModel {
3287
- constructor(_options: any) {
3288
- super({ responses: [summaryResponse] });
3289
- }
3290
- } as any;
3291
- }
3292
- return originalGetChatModelClass(provider);
3293
- }) as typeof providers.getChatModelClass);
3294
-
3295
- const createRunHelper = async (
3296
- maxTokens: number
3297
- ): Promise<Run<t.IState>> => {
3298
- const { aggregateContent } = createContentAggregator();
3299
- const indexTokenCountMap = buildIndexTokenCountMap(
3300
- conversationHistory,
3301
- tokenCounter
3302
- );
3303
- return Run.create<t.IState>({
3304
- runId: `multi-pass-continue-${Date.now()}`,
3305
- graphConfig: {
3306
- type: 'standard',
3307
- llmConfig: getLLMConfig(Providers.OPENAI),
3308
- instructions: INSTRUCTIONS,
3309
- maxContextTokens: maxTokens,
3310
- summarizationEnabled: true,
3311
- summarizationConfig: {
3312
- provider: Providers.OPENAI,
3313
- parameters: {},
3314
- },
3315
- },
3316
- returnContent: true,
3317
- customHandlers: buildHandlers([], aggregateContent, spies),
3318
- tokenCounter,
3319
- indexTokenCountMap,
3320
- });
3321
- };
3322
-
3323
- // Build conversation
3324
- for (const q of [
3325
- 'Explain 2+2 in great detail.',
3326
- 'Explain 3*5 step by step.',
3327
- 'What is sqrt(16)? Full explanation.',
3328
- 'What is 100/4? Show your work.',
3329
- ]) {
3330
- const run = await createRunHelper(4000);
3331
- run.Graph?.overrideTestModel(
3332
- [
3333
- 'Here is a detailed explanation of the computation with many steps and examples.',
3334
- ],
3335
- 1
3336
- );
3337
- await runTurn({ run, conversationHistory }, q, streamConfig);
3338
- }
3339
-
3340
- // Trigger summarization
3341
- let run = await createRunHelper(100);
3342
- run.Graph?.overrideTestModel(['Summary acknowledged.'], 1);
3343
- try {
3344
- await runTurn({ run, conversationHistory }, 'Continue.', streamConfig);
3345
- } catch {
3346
- conversationHistory.pop();
3347
- }
3348
-
3349
- const sumCount = spies.onSummarizeCompleteSpy.mock.calls.length;
3350
- console.log(` Continuation test: ${sumCount} summarization(s)`);
3351
-
3352
- if (sumCount > 0) {
3353
- // Post-summary turn should work with reasonable context
3354
- run = await createRunHelper(2000);
3355
- run.Graph?.overrideTestModel(['The answer is 42.'], 1);
3356
- const postResult = await runTurn(
3357
- { run, conversationHistory },
3358
- 'What is 6*7?',
3359
- streamConfig
3360
- );
3361
- expect(postResult).toBeDefined();
3362
- console.log(
3363
- ` Post-summary turn succeeded, ${conversationHistory.length} messages`
3364
- );
3365
- }
3366
- });
3367
- });
3368
-
3369
- // ---------------------------------------------------------------------------
3370
- // Re-summarization within a single run (FakeListChatModel — no API keys)
3371
- // Tests the shouldSkipSummarization baseline reset fix.
3372
- // ---------------------------------------------------------------------------
3373
-
3374
- describe('Re-summarization within a single run (no API keys)', () => {
3375
- jest.setTimeout(60_000);
3376
-
3377
- const SUMMARY_V1 = '## Summary v1\nUser discussed topic A.';
3378
- const SUMMARY_V2 = '## Summary v2\nUser discussed topic A and B.';
3379
- const INSTRUCTIONS = 'You are a helpful assistant.';
3380
- const streamConfig = {
3381
- configurable: { thread_id: 're-summarize-test' },
3382
- recursionLimit: 80,
3383
- streamMode: 'values',
3384
- version: 'v2' as const,
3385
- };
3386
-
3387
- let getChatModelClassSpy: jest.SpyInstance;
3388
- const originalGetChatModelClass = providers.getChatModelClass;
3389
- let summaryCallCount = 0;
3390
-
3391
- beforeEach(() => {
3392
- summaryCallCount = 0;
3393
- getChatModelClassSpy = jest
3394
- .spyOn(providers, 'getChatModelClass')
3395
- .mockImplementation(((provider: Providers) => {
3396
- if (provider === Providers.OPENAI) {
3397
- return class extends FakeListChatModel {
3398
- constructor(_options: any) {
3399
- summaryCallCount++;
3400
- super({
3401
- responses: [summaryCallCount === 1 ? SUMMARY_V1 : SUMMARY_V2],
3402
- });
3403
- }
3404
- } as any;
3405
- }
3406
- return originalGetChatModelClass(provider);
3407
- }) as typeof providers.getChatModelClass);
3408
- });
3409
-
3410
- afterEach(() => {
3411
- getChatModelClassSpy.mockRestore();
3412
- });
3413
-
3414
- test('second summarization fires after context refills post-first-summary', async () => {
3415
- const spies = createSpies();
3416
- const tokenCounter = await createTokenCounter();
3417
-
3418
- // Build a long conversation that will need multiple summarization cycles
3419
- const padding = 'x'.repeat(400);
3420
- const conversationHistory: BaseMessage[] = [];
3421
- for (let i = 0; i < 10; i++) {
3422
- conversationHistory.push(new HumanMessage(`Question ${i}${padding}`));
3423
- conversationHistory.push(new AIMessage(`Answer ${i}${padding}`));
3424
- }
3425
- conversationHistory.push(new HumanMessage('Final question'));
3426
-
3427
- const indexTokenCountMap = buildIndexTokenCountMap(
3428
- conversationHistory,
3429
- tokenCounter
3430
- );
3431
-
3432
- const { aggregateContent } = createContentAggregator();
3433
- const collectedUsage: UsageMetadata[] = [];
3434
-
3435
- const run = await Run.create<t.IState>({
3436
- runId: `re-sum-${Date.now()}`,
3437
- graphConfig: {
3438
- type: 'standard',
3439
- llmConfig: getLLMConfig(Providers.OPENAI),
3440
- instructions: INSTRUCTIONS,
3441
- maxContextTokens: 600,
3442
- summarizationEnabled: true,
3443
- summarizationConfig: {
3444
- provider: Providers.OPENAI,
3445
- },
3446
- },
3447
- returnContent: true,
3448
- customHandlers: buildHandlers(collectedUsage, aggregateContent, spies),
3449
- tokenCounter,
3450
- indexTokenCountMap,
3451
- });
3452
-
3453
- let error: Error | undefined;
3454
- try {
3455
- await run.processStream(
3456
- { messages: conversationHistory },
3457
- streamConfig as any
3458
- );
3459
- } catch (err) {
3460
- error = err as Error;
3461
- }
3462
-
3463
- const startCalls = spies.onSummarizeStartSpy.mock.calls.length;
3464
- const completeCalls = spies.onSummarizeCompleteSpy.mock.calls.length;
3465
- console.log(
3466
- ` Summarization cycles: start=${startCalls}, complete=${completeCalls}, error=${error?.message.substring(0, 80) ?? 'none'}`
3467
- );
3468
-
3469
- // The key assertion: with enough messages and tight context,
3470
- // summarization should fire more than once. Before the
3471
- // shouldSkipSummarization baseline reset fix, it would fire only once.
3472
- expect(startCalls).toBeGreaterThanOrEqual(1);
3473
- console.log(` Summary model calls: ${summaryCallCount}`);
3474
- });
3475
- });
3476
-
3477
- // ---------------------------------------------------------------------------
3478
- // Emoji/Unicode safety through full pipeline (FakeListChatModel — no API keys)
3479
- // ---------------------------------------------------------------------------
3480
-
3481
- describe('Emoji and Unicode safety (no API keys)', () => {
3482
- jest.setTimeout(60_000);
3483
-
3484
- const SUMMARY = '## Summary\nUser sent emoji-heavy messages about coding.';
3485
- const streamConfig = {
3486
- configurable: { thread_id: 'emoji-safety-test' },
3487
- streamMode: 'values',
3488
- version: 'v2' as const,
3489
- };
3490
-
3491
- let getChatModelClassSpy: jest.SpyInstance;
3492
- const originalGetChatModelClass = providers.getChatModelClass;
3493
-
3494
- beforeEach(() => {
3495
- getChatModelClassSpy = jest
3496
- .spyOn(providers, 'getChatModelClass')
3497
- .mockImplementation(((provider: Providers) => {
3498
- if (provider === Providers.OPENAI) {
3499
- return class extends FakeListChatModel {
3500
- constructor(_options: any) {
3501
- super({ responses: [SUMMARY] });
3502
- }
3503
- } as any;
3504
- }
3505
- return originalGetChatModelClass(provider);
3506
- }) as typeof providers.getChatModelClass);
3507
- });
3508
-
3509
- afterEach(() => {
3510
- getChatModelClassSpy.mockRestore();
3511
- });
3512
-
3513
- test('emoji-heavy messages do not produce broken JSON in summarization', async () => {
3514
- const spies = createSpies();
3515
- const tokenCounter = await createTokenCounter();
3516
-
3517
- // ZWJ sequences and multi-byte emoji that produce surrogate pairs in UTF-16
3518
- const emojiMessages: BaseMessage[] = [
3519
- new HumanMessage('👨‍💻 Let me show you some code 🚀'),
3520
- new AIMessage('Sure! Here is the code 🎉✨ with lots of emoji 🌍🌎🌏'),
3521
- new HumanMessage('👨‍👩‍👧‍👦 Family emoji and flags 🇺🇸🇬🇧🇯🇵 test'),
3522
- new AIMessage('More emoji: 🧑‍🔬🧑‍🎨🧑‍🚒🧑‍✈️ professional emoji'),
3523
- new HumanMessage('Final 💯🔥⚡ question'),
3524
- ];
3525
-
3526
- const indexTokenCountMap = buildIndexTokenCountMap(
3527
- emojiMessages,
3528
- tokenCounter
3529
- );
3530
-
3531
- const { aggregateContent } = createContentAggregator();
3532
- const collectedUsage: UsageMetadata[] = [];
3533
-
3534
- const run = await Run.create<t.IState>({
3535
- runId: `emoji-${Date.now()}`,
3536
- graphConfig: {
3537
- type: 'standard',
3538
- llmConfig: getLLMConfig(Providers.OPENAI),
3539
- instructions: 'Be helpful.',
3540
- maxContextTokens: 100,
3541
- summarizationEnabled: true,
3542
- summarizationConfig: {
3543
- provider: Providers.OPENAI,
3544
- },
3545
- },
3546
- returnContent: true,
3547
- customHandlers: buildHandlers(collectedUsage, aggregateContent, spies),
3548
- tokenCounter,
3549
- indexTokenCountMap,
3550
- });
3551
-
3552
- // The test passes if this doesn't throw a JSON serialization error
3553
- let error: Error | undefined;
3554
- try {
3555
- await run.processStream({ messages: emojiMessages }, streamConfig as any);
3556
- } catch (err) {
3557
- error = err as Error;
3558
- }
3559
-
3560
- // empty_messages is acceptable (tight context), but JSON errors are not
3561
- if (error) {
3562
- expect(error.message).not.toContain('not valid JSON');
3563
- expect(error.message).not.toContain('Invalid Unicode');
3564
- console.log(
3565
- ` Emoji test: acceptable error (${error.message.substring(0, 80)})`
3566
- );
3567
- } else {
3568
- console.log(' Emoji test: completed without error');
3569
- }
3570
-
3571
- console.log(
3572
- ` Summarization: start=${spies.onSummarizeStartSpy.mock.calls.length}, complete=${spies.onSummarizeCompleteSpy.mock.calls.length}`
3573
- );
3574
- });
3575
- });
3576
-
3577
- // ---------------------------------------------------------------------------
3578
- // Budget-aware error messages (FakeListChatModel — no API keys)
3579
- // ---------------------------------------------------------------------------
3580
-
3581
- describe('Budget-aware error messages (no API keys)', () => {
3582
- jest.setTimeout(60_000);
3583
-
3584
- const streamConfig = {
3585
- configurable: { thread_id: 'budget-error-test' },
3586
- streamMode: 'values',
3587
- version: 'v2' as const,
3588
- };
3589
-
3590
- test('empty_messages error includes tool-specific guidance when tools dominate budget', async () => {
3591
- const spies = createSpies();
3592
- const tokenCounter = await createTokenCounter();
3593
-
3594
- const conversationHistory: BaseMessage[] = [new HumanMessage('Hello')];
3595
-
3596
- const indexTokenCountMap = buildIndexTokenCountMap(
3597
- conversationHistory,
3598
- tokenCounter
3599
- );
3600
-
3601
- const { aggregateContent } = createContentAggregator();
3602
- const collectedUsage: UsageMetadata[] = [];
3603
-
3604
- // Create a run with maxContextTokens smaller than the tool definitions
3605
- // The Calculator tool alone has a schema that takes up tokens
3606
- const run = await Run.create<t.IState>({
3607
- runId: `budget-err-${Date.now()}`,
3608
- graphConfig: {
3609
- type: 'standard',
3610
- llmConfig: getLLMConfig(Providers.OPENAI),
3611
- tools: [new Calculator()],
3612
- instructions: 'A'.repeat(500), // Long instructions to push over budget
3613
- maxContextTokens: 50, // Impossibly tight
3614
- summarizationEnabled: true,
3615
- summarizationConfig: {
3616
- provider: Providers.OPENAI,
3617
- },
3618
- },
3619
- returnContent: true,
3620
- customHandlers: buildHandlers(collectedUsage, aggregateContent, spies),
3621
- tokenCounter,
3622
- indexTokenCountMap,
3623
- });
3624
-
3625
- let error: Error | undefined;
3626
- try {
3627
- await run.processStream(
3628
- { messages: conversationHistory },
3629
- streamConfig as any
3630
- );
3631
- } catch (err) {
3632
- error = err as Error;
3633
- }
3634
-
3635
- expect(error).toBeDefined();
3636
- // The error should mention the budget problem specifically
3637
- const errorMsg = error!.message;
3638
- expect(errorMsg).toContain('empty_messages');
3639
-
3640
- // Should contain actionable guidance about instructions or tools
3641
- const hasGuidance =
3642
- errorMsg.includes('Reduce the number of tools') ||
3643
- errorMsg.includes('Increase maxContextTokens') ||
3644
- errorMsg.includes('shorten the system prompt');
3645
- expect(hasGuidance).toBe(true);
3646
-
3647
- console.log(
3648
- ` Budget error guidance: ${errorMsg.substring(errorMsg.indexOf('Please') > -1 ? errorMsg.indexOf('Please') : 0, errorMsg.indexOf('Please') + 120)}`
3649
- );
3650
- });
3651
- });
3652
-
3653
- // ---------------------------------------------------------------------------
3654
- // Large tool result + surviving context double-summarization regression
3655
- // (FakeListChatModel — no API keys)
3656
- //
3657
- // Models the real-world scenario from debug logs:
3658
- // - Multi-turn conversation with MCP tools (screenshots, snapshots)
3659
- // - Summarization fires once → surviving context includes a 9437-char tool result
3660
- // - Post-summarization prune: the tool result exceeds the effective budget
3661
- // - All surviving messages land in messagesToRefine
3662
- // - Before fix: summarization re-triggers immediately on the same messages
3663
- // - After fix: shouldSkipSummarization blocks re-trigger (baseline = surviving count)
3664
- // ---------------------------------------------------------------------------
3665
-
3666
- describe('Large tool result surviving context — no double summarization (no API keys)', () => {
3667
- jest.setTimeout(60_000);
3668
-
3669
- const SUMMARY_V1 =
3670
- '## Summary\nUser navigated to apple.com, took screenshots, ran Lighthouse audit.';
3671
- const SUMMARY_V2 =
3672
- '## Summary v2\nUser explored apple.com with devtools, took snapshots.';
3673
- const INSTRUCTIONS = 'You are a browser automation assistant.';
3674
- const streamConfig = {
3675
- configurable: { thread_id: 'double-sum-regression' },
3676
- recursionLimit: 80,
3677
- streamMode: 'values',
3678
- version: 'v2' as const,
3679
- };
3680
-
3681
- let getChatModelClassSpy: jest.SpyInstance;
3682
- const originalGetChatModelClass = providers.getChatModelClass;
3683
- let summaryCallCount = 0;
3684
-
3685
- beforeEach(() => {
3686
- summaryCallCount = 0;
3687
- getChatModelClassSpy = jest
3688
- .spyOn(providers, 'getChatModelClass')
3689
- .mockImplementation(((provider: Providers) => {
3690
- if (provider === Providers.OPENAI) {
3691
- return class extends FakeListChatModel {
3692
- constructor(_options: any) {
3693
- summaryCallCount++;
3694
- super({
3695
- responses: [summaryCallCount === 1 ? SUMMARY_V1 : SUMMARY_V2],
3696
- });
3697
- }
3698
- } as any;
3699
- }
3700
- return originalGetChatModelClass(provider);
3701
- }) as typeof providers.getChatModelClass);
3702
- });
3703
-
3704
- afterEach(() => {
3705
- getChatModelClassSpy.mockRestore();
3706
- });
3707
-
3708
- test('surviving context with oversized tool result does not re-trigger summarization', async () => {
3709
- const spies = createSpies();
3710
- const tokenCounter = await createTokenCounter();
3711
-
3712
- // Build a conversation that mirrors the real debug log:
3713
- // Multiple turns with tool calls, including a large take_snapshot result
3714
- const largeSnapshot = 'uid=1_0 RootWebArea "Apple" '.repeat(300); // ~9000 chars
3715
- const conversationHistory: BaseMessage[] = [
3716
- new HumanMessage('Navigate to apple.com'),
3717
- new AIMessage({
3718
- content: 'Navigating now.',
3719
- tool_calls: [
3720
- {
3721
- id: 'tc_1',
3722
- name: 'navigate_page',
3723
- args: { url: 'https://apple.com' },
3724
- },
3725
- ],
3726
- }),
3727
- new ToolMessage({
3728
- content: 'Successfully navigated to https://www.apple.com.',
3729
- tool_call_id: 'tc_1',
3730
- name: 'navigate_page',
3731
- }),
3732
- new AIMessage({
3733
- content: 'Taking a screenshot.',
3734
- tool_calls: [{ id: 'tc_2', name: 'take_screenshot', args: {} }],
3735
- }),
3736
- new ToolMessage({
3737
- content: 'Took a screenshot of the current page.',
3738
- tool_call_id: 'tc_2',
3739
- name: 'take_screenshot',
3740
- }),
3741
- new HumanMessage('What can you see on the site?'),
3742
- new AIMessage({
3743
- content: 'Let me take a snapshot.',
3744
- tool_calls: [{ id: 'tc_3', name: 'take_snapshot', args: {} }],
3745
- }),
3746
- new ToolMessage({
3747
- content: largeSnapshot, // ~9000 chars — the large tool result
3748
- tool_call_id: 'tc_3',
3749
- name: 'take_snapshot',
3750
- }),
3751
- new HumanMessage('Show me more details'),
3752
- new AIMessage({
3753
- content: 'Here are the details from the page.',
3754
- tool_calls: [{ id: 'tc_4', name: 'take_screenshot', args: {} }],
3755
- }),
3756
- new ToolMessage({
3757
- content: 'Took another screenshot.',
3758
- tool_call_id: 'tc_4',
3759
- name: 'take_screenshot',
3760
- }),
3761
- new HumanMessage('Analyze the page performance'),
3762
- ];
3763
-
3764
- const indexTokenCountMap = buildIndexTokenCountMap(
3765
- conversationHistory,
3766
- tokenCounter
3767
- );
3768
-
3769
- const { aggregateContent } = createContentAggregator();
3770
- const collectedUsage: UsageMetadata[] = [];
3771
-
3772
- // maxContextTokens = 800 — tight enough that the large snapshot
3773
- // forces aggressive pruning but leaves room for the agent to respond
3774
- const run = await Run.create<t.IState>({
3775
- runId: `double-sum-${Date.now()}`,
3776
- graphConfig: {
3777
- type: 'standard',
3778
- llmConfig: getLLMConfig(Providers.OPENAI),
3779
- instructions: INSTRUCTIONS,
3780
- maxContextTokens: 800,
3781
- summarizationEnabled: true,
3782
- summarizationConfig: {
3783
- provider: Providers.OPENAI,
3784
- },
3785
- },
3786
- returnContent: true,
3787
- customHandlers: buildHandlers(collectedUsage, aggregateContent, spies),
3788
- tokenCounter,
3789
- indexTokenCountMap,
3790
- });
3791
-
3792
- let error: Error | undefined;
3793
- try {
3794
- await run.processStream(
3795
- { messages: conversationHistory },
3796
- streamConfig as any
3797
- );
3798
- } catch (err) {
3799
- error = err as Error;
3800
- }
3801
-
3802
- const startCalls = spies.onSummarizeStartSpy.mock.calls.length;
3803
- const completeCalls = spies.onSummarizeCompleteSpy.mock.calls.length;
3804
- console.log(
3805
- ` Summarization: start=${startCalls}, complete=${completeCalls}, modelCalls=${summaryCallCount}`
3806
- );
3807
-
3808
- if (error) {
3809
- // empty_messages is acceptable for tight context; double-summarization is not
3810
- console.log(` Error: ${error.message.substring(0, 100)}`);
3811
- }
3812
-
3813
- // Key assertion: summarization should fire at most once.
3814
- // Before the fix, the surviving context's large tool result would cause
3815
- // all messages to land in messagesToRefine, triggering a second
3816
- // summarization on the same messages.
3817
- expect(startCalls).toBeLessThanOrEqual(1);
3818
- expect(summaryCallCount).toBeLessThanOrEqual(1);
3819
- console.log(
3820
- ` Double-summarization prevented: ${startCalls <= 1 ? 'YES' : 'NO'}`
3821
- );
3822
- });
3823
- });