@librechat/agents 3.4.7 → 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 (1026) 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 +0 -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 +0 -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/__tests__/stream.eagerArgsDivergence.test.ts +0 -911
  688. package/src/__tests__/stream.eagerEventExecution.test.ts +0 -5046
  689. package/src/__tests__/stream.streamLimits.test.ts +0 -1982
  690. package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +0 -448
  691. package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +0 -655
  692. package/src/agents/__tests__/AgentContext.openrouter.live.test.ts +0 -128
  693. package/src/agents/__tests__/AgentContext.overflow.test.ts +0 -205
  694. package/src/agents/__tests__/AgentContext.test.ts +0 -2425
  695. package/src/agents/__tests__/projection.test.ts +0 -73
  696. package/src/agents/__tests__/promptCacheLiveHelpers.ts +0 -175
  697. package/src/aggregator.test.ts +0 -1214
  698. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +0 -273
  699. package/src/graphs/__tests__/Graph.closeRunStep.test.ts +0 -385
  700. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +0 -1766
  701. package/src/graphs/__tests__/Graph.preemptSignal.test.ts +0 -126
  702. package/src/graphs/__tests__/Graph.reasoning.test.ts +0 -1717
  703. package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +0 -80
  704. package/src/graphs/__tests__/MultiAgentGraph.test.ts +0 -177
  705. package/src/graphs/__tests__/composition.smoke.test.ts +0 -509
  706. package/src/graphs/__tests__/createGraph.test.ts +0 -206
  707. package/src/hooks/__tests__/HookRegistry.test.ts +0 -334
  708. package/src/hooks/__tests__/compactHooks.test.ts +0 -250
  709. package/src/hooks/__tests__/createToolPolicyHook.test.ts +0 -268
  710. package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +0 -405
  711. package/src/hooks/__tests__/executeHooks.test.ts +0 -1294
  712. package/src/hooks/__tests__/integration.test.ts +0 -337
  713. package/src/hooks/__tests__/matchers.test.ts +0 -238
  714. package/src/hooks/__tests__/preemptBoundary.test.ts +0 -152
  715. package/src/hooks/__tests__/toolHooks.test.ts +0 -669
  716. package/src/llm/__tests__/contextOverflowRecovery.test.ts +0 -401
  717. package/src/llm/__tests__/fallbackOverflow.test.ts +0 -287
  718. package/src/llm/anthropic/Jacob_Lee_Resume_2023.pdf +0 -0
  719. package/src/llm/anthropic/inherited-content-utils.spec.ts +0 -249
  720. package/src/llm/anthropic/inherited-stream-events.spec.ts +0 -1259
  721. package/src/llm/anthropic/inherited-strict.spec.ts +0 -302
  722. package/src/llm/anthropic/llm.spec.ts +0 -3394
  723. package/src/llm/anthropic/utils/cross-provider-reasoning.test.ts +0 -383
  724. package/src/llm/anthropic/utils/cross-provider-server-tools.test.ts +0 -110
  725. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +0 -508
  726. package/src/llm/anthropic/utils/streaming-tool-input.test.ts +0 -361
  727. package/src/llm/anthropic/utils/stripPrefillCache.test.ts +0 -111
  728. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +0 -287
  729. package/src/llm/bedrock/inherited-cache.spec.ts +0 -144
  730. package/src/llm/bedrock/inherited.spec.ts +0 -724
  731. package/src/llm/bedrock/llm.spec.ts +0 -1533
  732. package/src/llm/bedrock/streamSealDispatch.test.ts +0 -331
  733. package/src/llm/bedrock/toolCache.test.ts +0 -194
  734. package/src/llm/bedrock/utils/cross-provider-reasoning.test.ts +0 -131
  735. package/src/llm/bedrock/utils/cross-provider-server-tools.test.ts +0 -122
  736. package/src/llm/bedrock/utils/message_inputs.test.ts +0 -650
  737. package/src/llm/bedrock/utils/message_outputs.test.ts +0 -85
  738. package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +0 -118
  739. package/src/llm/custom-chat-models.smoke.test.ts +0 -1966
  740. package/src/llm/google/data/gettysburg10.wav +0 -0
  741. package/src/llm/google/data/hotdog.jpg +0 -0
  742. package/src/llm/google/inherited-stream-events.spec.ts +0 -350
  743. package/src/llm/google/llm.spec.ts +0 -1388
  744. package/src/llm/google/streamSmoothing.test.ts +0 -121
  745. package/src/llm/google/utils/common.test.ts +0 -196
  746. package/src/llm/invoke.alternation.test.ts +0 -87
  747. package/src/llm/invoke.handoffCue.test.ts +0 -112
  748. package/src/llm/invoke.streamLimits.test.ts +0 -142
  749. package/src/llm/invoke.test.ts +0 -1198
  750. package/src/llm/mistral/streamSmoothing.test.ts +0 -97
  751. package/src/llm/openai/cacheWriteTokens.test.ts +0 -112
  752. package/src/llm/openai/contentBlocks.test.ts +0 -381
  753. package/src/llm/openai/deepseek.test.ts +0 -747
  754. package/src/llm/openai/inherited-deepseek.spec.ts +0 -347
  755. package/src/llm/openai/inherited-xai.spec.ts +0 -416
  756. package/src/llm/openai/llm.spec.ts +0 -1774
  757. package/src/llm/openai/managedRequests.test.ts +0 -182
  758. package/src/llm/openai/sequentialToolCallSeals.test.ts +0 -199
  759. package/src/llm/openai/streamMetadata.spec.ts +0 -86
  760. package/src/llm/openai/streamMetadataDedup.spec.ts +0 -166
  761. package/src/llm/openai/utils/isReasoningModel.test.ts +0 -90
  762. package/src/llm/openai/utils/messages.test.ts +0 -488
  763. package/src/llm/openrouter/reasoning.test.ts +0 -561
  764. package/src/llm/openrouter/toolCache.test.ts +0 -134
  765. package/src/llm/preempt.test.ts +0 -323
  766. package/src/llm/stream/chunkAdapters.test.ts +0 -202
  767. package/src/llm/stream/reassembly.test.ts +0 -241
  768. package/src/llm/stream/smoother.bench.test.ts +0 -155
  769. package/src/llm/stream/smoother.test.ts +0 -519
  770. package/src/llm/streamLimits.test.ts +0 -450
  771. package/src/llm/truncation.test.ts +0 -242
  772. package/src/llm/vertexai/fixThoughtSignatures.test.ts +0 -154
  773. package/src/llm/vertexai/inherited-stream-events.spec.ts +0 -271
  774. package/src/llm/vertexai/llm.spec.ts +0 -159
  775. package/src/llm/vertexai/repairUsageMetadata.test.ts +0 -54
  776. package/src/llm/vertexai/sealStreamedToolCalls.test.ts +0 -88
  777. package/src/llm/vertexai/streamSealDispatch.test.ts +0 -148
  778. package/src/llm/vertexai/streamSmoothing.test.ts +0 -109
  779. package/src/messages/__tests__/anthropicToolCache.test.ts +0 -339
  780. package/src/messages/__tests__/observationMasking.test.ts +0 -312
  781. package/src/messages/__tests__/recency.test.ts +0 -267
  782. package/src/messages/__tests__/tools.test.ts +0 -473
  783. package/src/messages/alternation.test.ts +0 -212
  784. package/src/messages/assistantPhase.test.ts +0 -75
  785. package/src/messages/cache.tail.test.ts +0 -533
  786. package/src/messages/cache.test.ts +0 -2166
  787. package/src/messages/content.test.ts +0 -362
  788. package/src/messages/contextPruning.test.ts +0 -184
  789. package/src/messages/ensureThinkingBlock.test.ts +0 -1427
  790. package/src/messages/foldToollessToolBlocks.test.ts +0 -596
  791. package/src/messages/formatAgentMessages.reducer.test.ts +0 -162
  792. package/src/messages/formatAgentMessages.skills.test.ts +0 -513
  793. package/src/messages/formatAgentMessages.steer.test.ts +0 -598
  794. package/src/messages/formatAgentMessages.test.ts +0 -8111
  795. package/src/messages/formatAgentMessages.tools.test.ts +0 -522
  796. package/src/messages/formatMessage.test.ts +0 -693
  797. package/src/messages/handoffCue.test.ts +0 -96
  798. package/src/messages/injected.test.ts +0 -107
  799. package/src/messages/labelContentByAgent.test.ts +0 -962
  800. package/src/messages/reducer.spec.ts +0 -32
  801. package/src/messages/shiftIndexTokenCountMap.test.ts +0 -81
  802. package/src/messages/tailCacheConversion.test.ts +0 -161
  803. package/src/openai/__tests__/openai.test.ts +0 -337
  804. package/src/responses/__tests__/responses.test.ts +0 -652
  805. package/src/scripts/abort.ts +0 -157
  806. package/src/scripts/activity-labels/captured.json +0 -56
  807. package/src/scripts/activity-labels/checks.cjs +0 -205
  808. package/src/scripts/activity-labels/corpus.cjs +0 -473
  809. package/src/scripts/activity-labels/report.cjs +0 -203
  810. package/src/scripts/activity-labels/rescore.cjs +0 -102
  811. package/src/scripts/activity-labels/run.ts +0 -705
  812. package/src/scripts/activity-labels/variants.ts +0 -71
  813. package/src/scripts/ant_web_search.ts +0 -159
  814. package/src/scripts/ant_web_search_edge_case.ts +0 -163
  815. package/src/scripts/ant_web_search_error_edge_case.ts +0 -149
  816. package/src/scripts/args.ts +0 -48
  817. package/src/scripts/bedrock-cache-debug.ts +0 -250
  818. package/src/scripts/bedrock-content-aggregation-test.ts +0 -266
  819. package/src/scripts/bedrock-merge-test.ts +0 -107
  820. package/src/scripts/bedrock-parallel-tools-test.ts +0 -204
  821. package/src/scripts/bench-prompt-cache.ts +0 -479
  822. package/src/scripts/caching.ts +0 -132
  823. package/src/scripts/cli.ts +0 -172
  824. package/src/scripts/cli2.ts +0 -133
  825. package/src/scripts/cli3.ts +0 -184
  826. package/src/scripts/cli4.ts +0 -191
  827. package/src/scripts/cli5.ts +0 -191
  828. package/src/scripts/code_exec.ts +0 -214
  829. package/src/scripts/code_exec_files.ts +0 -237
  830. package/src/scripts/code_exec_multi_session.ts +0 -237
  831. package/src/scripts/code_exec_ptc.ts +0 -335
  832. package/src/scripts/code_exec_session.ts +0 -283
  833. package/src/scripts/code_exec_simple.ts +0 -148
  834. package/src/scripts/compare_pi_vs_ours.ts +0 -1259
  835. package/src/scripts/content.ts +0 -139
  836. package/src/scripts/context-overflow-probe.ts +0 -997
  837. package/src/scripts/empty_input.ts +0 -137
  838. package/src/scripts/handoff-test.ts +0 -134
  839. package/src/scripts/local_engine.ts +0 -166
  840. package/src/scripts/local_engine_checkpointer.ts +0 -205
  841. package/src/scripts/local_engine_compile.ts +0 -263
  842. package/src/scripts/local_engine_hooks.ts +0 -226
  843. package/src/scripts/local_engine_image.ts +0 -201
  844. package/src/scripts/local_engine_ptc.ts +0 -151
  845. package/src/scripts/local_engine_workspace.ts +0 -258
  846. package/src/scripts/memory.ts +0 -107
  847. package/src/scripts/multi-agent-chain.ts +0 -332
  848. package/src/scripts/multi-agent-conditional.ts +0 -222
  849. package/src/scripts/multi-agent-document-review-chain.ts +0 -198
  850. package/src/scripts/multi-agent-hybrid-flow.ts +0 -311
  851. package/src/scripts/multi-agent-parallel-start.ts +0 -265
  852. package/src/scripts/multi-agent-parallel.ts +0 -395
  853. package/src/scripts/multi-agent-sequence.ts +0 -217
  854. package/src/scripts/multi-agent-subagent.ts +0 -246
  855. package/src/scripts/multi-agent-supervisor.ts +0 -366
  856. package/src/scripts/multi-agent-test.ts +0 -187
  857. package/src/scripts/openrouter_prompt_cache_live.ts +0 -310
  858. package/src/scripts/parallel-asymmetric-tools-test.ts +0 -275
  859. package/src/scripts/parallel-full-metadata-test.ts +0 -241
  860. package/src/scripts/parallel-tools-test.ts +0 -341
  861. package/src/scripts/preempt-probe.ts +0 -330
  862. package/src/scripts/preempt-scenarios.ts +0 -388
  863. package/src/scripts/programmatic_exec.ts +0 -387
  864. package/src/scripts/programmatic_exec_agent.ts +0 -232
  865. package/src/scripts/search.ts +0 -147
  866. package/src/scripts/sequential-full-metadata-test.ts +0 -198
  867. package/src/scripts/session_live.ts +0 -548
  868. package/src/scripts/simple.ts +0 -226
  869. package/src/scripts/single-agent-metadata-test.ts +0 -196
  870. package/src/scripts/stream.ts +0 -141
  871. package/src/scripts/subagent-configurable-inheritance.ts +0 -252
  872. package/src/scripts/subagent-event-driven-debug.ts +0 -190
  873. package/src/scripts/subagent-tools-debug.ts +0 -160
  874. package/src/scripts/subagent-usage-sink.ts +0 -176
  875. package/src/scripts/summarization-recency.ts +0 -462
  876. package/src/scripts/test-custom-prompt-key.ts +0 -145
  877. package/src/scripts/test-handoff-input.ts +0 -170
  878. package/src/scripts/test-handoff-preamble.ts +0 -278
  879. package/src/scripts/test-handoff-steering.ts +0 -430
  880. package/src/scripts/test-multi-agent-list-handoff.ts +0 -418
  881. package/src/scripts/test-parallel-agent-labeling.ts +0 -327
  882. package/src/scripts/test-parallel-handoffs.ts +0 -292
  883. package/src/scripts/test-thinking-handoff-bedrock.ts +0 -154
  884. package/src/scripts/test-thinking-handoff.ts +0 -156
  885. package/src/scripts/test-thinking-to-thinking-handoff-bedrock.ts +0 -166
  886. package/src/scripts/test-tool-before-handoff-role-order.ts +0 -276
  887. package/src/scripts/test-tools-before-handoff.ts +0 -222
  888. package/src/scripts/test_code_api.ts +0 -354
  889. package/src/scripts/thinking-bedrock.ts +0 -160
  890. package/src/scripts/thinking-vertexai.ts +0 -168
  891. package/src/scripts/thinking.ts +0 -172
  892. package/src/scripts/tool_search.ts +0 -153
  893. package/src/scripts/tools.ts +0 -163
  894. package/src/session/__tests__/JsonlSessionStore.test.ts +0 -1463
  895. package/src/session/__tests__/handlers.test.ts +0 -161
  896. package/src/specs/activity-label-observability.live.test.ts +0 -262
  897. package/src/specs/activity-label-observability.test.ts +0 -167
  898. package/src/specs/activity-label-prompt.test.ts +0 -359
  899. package/src/specs/activity-label-trace-seed.test.ts +0 -47
  900. package/src/specs/activity-phase-label.test.ts +0 -306
  901. package/src/specs/agent-handoffs.live.test.ts +0 -140
  902. package/src/specs/agent-handoffs.test.ts +0 -2465
  903. package/src/specs/anthropic.simple.test.ts +0 -453
  904. package/src/specs/ask-user-question-batch.test.ts +0 -289
  905. package/src/specs/ask-user-questions.live.test.ts +0 -185
  906. package/src/specs/ask-user-questions.test.ts +0 -293
  907. package/src/specs/azure.simple.test.ts +0 -502
  908. package/src/specs/bedrock-toolless.live.test.ts +0 -123
  909. package/src/specs/bedrock-truncation.live.test.ts +0 -191
  910. package/src/specs/cache.simple.test.ts +0 -416
  911. package/src/specs/context-accuracy.live.test.ts +0 -409
  912. package/src/specs/context-overflow-recovery.live.test.ts +0 -213
  913. package/src/specs/context-usage-event.test.ts +0 -117
  914. package/src/specs/context-usage.live.test.ts +0 -297
  915. package/src/specs/custom-event-await.test.ts +0 -304
  916. package/src/specs/deepseek.simple.test.ts +0 -290
  917. package/src/specs/deterministic-trace-id.test.ts +0 -43
  918. package/src/specs/discovered-tools.test.ts +0 -217
  919. package/src/specs/durability-checkpoint.integration.test.ts +0 -243
  920. package/src/specs/graph-subagent.live.test.ts +0 -242
  921. package/src/specs/graph-subagent.test.ts +0 -322
  922. package/src/specs/handoffCue.test.ts +0 -165
  923. package/src/specs/langfuse-callbacks.test.ts +0 -1578
  924. package/src/specs/langfuse-config.test.ts +0 -247
  925. package/src/specs/langfuse-instrumentation.test.ts +0 -393
  926. package/src/specs/langfuse-metadata.test.ts +0 -245
  927. package/src/specs/langfuse-routing.integration.test.ts +0 -844
  928. package/src/specs/langfuse-runtime-context.test.ts +0 -92
  929. package/src/specs/langfuse-span-registry.test.ts +0 -87
  930. package/src/specs/langfuse-tool-output-tracing.test.ts +0 -1858
  931. package/src/specs/langfuse-trace-shaping.test.ts +0 -591
  932. package/src/specs/moonshot.simple.test.ts +0 -367
  933. package/src/specs/multi-agent-summarization.test.ts +0 -396
  934. package/src/specs/openai.simple.test.ts +0 -315
  935. package/src/specs/openrouter.simple.test.ts +0 -275
  936. package/src/specs/preemptSeal.test.ts +0 -678
  937. package/src/specs/prune.test.ts +0 -3741
  938. package/src/specs/reasoning.test.ts +0 -201
  939. package/src/specs/run-step-timestamps.test.ts +0 -414
  940. package/src/specs/spec.utils.ts +0 -15
  941. package/src/specs/subagent.test.ts +0 -864
  942. package/src/specs/summarization-unit.test.ts +0 -973
  943. package/src/specs/summarization.test.ts +0 -3823
  944. package/src/specs/summarize-prune.test.ts +0 -376
  945. package/src/specs/thinking-handoff.test.ts +0 -621
  946. package/src/specs/thinking-prune.test.ts +0 -827
  947. package/src/specs/token-accounting-e2e.test.ts +0 -1034
  948. package/src/specs/token-accounting-pipeline.test.ts +0 -1008
  949. package/src/specs/token-distribution-edge-case.test.ts +0 -315
  950. package/src/specs/token-memoization.test.ts +0 -40
  951. package/src/specs/tokens.test.ts +0 -1000
  952. package/src/specs/tool-error-resume.test.ts +0 -194
  953. package/src/specs/tool-error.test.ts +0 -198
  954. package/src/specs/vllm-reasoning-toolcalls.test.ts +0 -340
  955. package/src/stream.dispatch.test.ts +0 -63
  956. package/src/stream.test.ts +0 -144
  957. package/src/summarization/__tests__/aggregator.test.ts +0 -236
  958. package/src/summarization/__tests__/node.test.ts +0 -1996
  959. package/src/summarization/__tests__/trigger.test.ts +0 -149
  960. package/src/summarization/chunkHandler.test.ts +0 -196
  961. package/src/test/mockTools.ts +0 -386
  962. package/src/tools/Calculator.test.ts +0 -278
  963. package/src/tools/__tests__/BashExecutor.test.ts +0 -108
  964. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +0 -939
  965. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +0 -858
  966. package/src/tools/__tests__/CodeExecutor.stateful.test.ts +0 -124
  967. package/src/tools/__tests__/LocalExecutionRoots.test.ts +0 -8
  968. package/src/tools/__tests__/LocalExecutionTools.test.ts +0 -2733
  969. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +0 -318
  970. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +0 -1557
  971. package/src/tools/__tests__/ReadFile.test.ts +0 -44
  972. package/src/tools/__tests__/SkillTool.test.ts +0 -442
  973. package/src/tools/__tests__/SubagentExecutor.test.ts +0 -3728
  974. package/src/tools/__tests__/SubagentReplay.test.ts +0 -329
  975. package/src/tools/__tests__/SubagentTool.test.ts +0 -149
  976. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +0 -595
  977. package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +0 -1191
  978. package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +0 -757
  979. package/src/tools/__tests__/ToolNode.langfuse.test.ts +0 -57
  980. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +0 -736
  981. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +0 -1710
  982. package/src/tools/__tests__/ToolNode.runtimeState.test.ts +0 -120
  983. package/src/tools/__tests__/ToolNode.session.test.ts +0 -1352
  984. package/src/tools/__tests__/ToolNode.streamLimits.test.ts +0 -69
  985. package/src/tools/__tests__/ToolSearch.integration.test.ts +0 -161
  986. package/src/tools/__tests__/ToolSearch.test.ts +0 -1227
  987. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +0 -561
  988. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +0 -980
  989. package/src/tools/__tests__/directToolHooks.test.ts +0 -700
  990. package/src/tools/__tests__/eagerEventExecution.session.test.ts +0 -92
  991. package/src/tools/__tests__/handlers.test.ts +0 -997
  992. package/src/tools/__tests__/hitl.test.ts +0 -4663
  993. package/src/tools/__tests__/intentArg.test.ts +0 -357
  994. package/src/tools/__tests__/intentCoverage.test.ts +0 -139
  995. package/src/tools/__tests__/localToolNames.test.ts +0 -75
  996. package/src/tools/__tests__/skillCatalog.test.ts +0 -161
  997. package/src/tools/__tests__/subagentHooks.test.ts +0 -1651
  998. package/src/tools/__tests__/toolOutputReferences.test.ts +0 -515
  999. package/src/tools/__tests__/workspaceSeam.test.ts +0 -147
  1000. package/src/tools/local/__tests__/FileCheckpointer.test.ts +0 -162
  1001. package/src/tools/local/__tests__/editStrategies.test.ts +0 -134
  1002. package/src/tools/search/content.test.ts +0 -173
  1003. package/src/tools/search/crw.test.ts +0 -836
  1004. package/src/tools/search/format.test.ts +0 -242
  1005. package/src/tools/search/http-agent.test.ts +0 -133
  1006. package/src/tools/search/jina-reranker.test.ts +0 -306
  1007. package/src/tools/search/keenable-scraper.test.ts +0 -153
  1008. package/src/tools/search/keenable.test.ts +0 -183
  1009. package/src/tools/search/outcome.test.ts +0 -90
  1010. package/src/tools/search/output.md +0 -2775
  1011. package/src/tools/search/rag-api-reranker.test.ts +0 -891
  1012. package/src/tools/search/source-processing.test.ts +0 -516
  1013. package/src/tools/search/tavily.test.ts +0 -965
  1014. package/src/tools/search/test.html +0 -884
  1015. package/src/tools/search/test.md +0 -643
  1016. package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +0 -637
  1017. package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +0 -3054
  1018. package/src/tools/subagent/__tests__/childGraphConfig.test.ts +0 -521
  1019. package/src/utils/__tests__/apportion.test.ts +0 -32
  1020. package/src/utils/__tests__/errors.test.ts +0 -270
  1021. package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +0 -336
  1022. package/src/utils/__tests__/redactSecrets.test.ts +0 -56
  1023. package/src/utils/__tests__/truncation.test.ts +0 -66
  1024. package/src/utils/llm.test.ts +0 -18
  1025. package/src/utils/proxy.test.ts +0 -176
  1026. 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
- });