@machina.ai/cell-cli-core 1.6.1-rc2 → 1.10.0-rc1

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 (358) hide show
  1. package/dist/index.d.ts +4 -3
  2. package/dist/index.js +4 -3
  3. package/dist/index.js.map +1 -1
  4. package/dist/package.json +1 -1
  5. package/dist/src/agents/codebase-investigator.d.ts +46 -0
  6. package/dist/src/agents/codebase-investigator.js +135 -0
  7. package/dist/src/agents/codebase-investigator.js.map +1 -0
  8. package/dist/src/agents/executor.d.ts +92 -0
  9. package/dist/src/agents/executor.js +579 -0
  10. package/dist/src/agents/executor.js.map +1 -0
  11. package/dist/src/agents/executor.test.d.ts +6 -0
  12. package/dist/src/agents/executor.test.js +680 -0
  13. package/dist/src/agents/executor.test.js.map +1 -0
  14. package/dist/src/agents/invocation.d.ts +46 -0
  15. package/dist/src/agents/invocation.js +102 -0
  16. package/dist/src/agents/invocation.js.map +1 -0
  17. package/dist/src/agents/invocation.test.d.ts +6 -0
  18. package/dist/src/agents/invocation.test.js +215 -0
  19. package/dist/src/agents/invocation.test.js.map +1 -0
  20. package/dist/src/agents/registry.d.ts +36 -0
  21. package/dist/src/agents/registry.js +81 -0
  22. package/dist/src/agents/registry.js.map +1 -0
  23. package/dist/src/agents/registry.test.d.ts +6 -0
  24. package/dist/src/agents/registry.test.js +146 -0
  25. package/dist/src/agents/registry.test.js.map +1 -0
  26. package/dist/src/agents/schema-utils.d.ts +39 -0
  27. package/dist/src/agents/schema-utils.js +57 -0
  28. package/dist/src/agents/schema-utils.js.map +1 -0
  29. package/dist/src/agents/schema-utils.test.d.ts +6 -0
  30. package/dist/src/agents/schema-utils.test.js +144 -0
  31. package/dist/src/agents/schema-utils.test.js.map +1 -0
  32. package/dist/src/agents/subagent-tool-wrapper.d.ts +38 -0
  33. package/dist/src/agents/subagent-tool-wrapper.js +48 -0
  34. package/dist/src/agents/subagent-tool-wrapper.js.map +1 -0
  35. package/dist/src/agents/subagent-tool-wrapper.test.d.ts +6 -0
  36. package/dist/src/agents/subagent-tool-wrapper.test.js +112 -0
  37. package/dist/src/agents/subagent-tool-wrapper.test.js.map +1 -0
  38. package/dist/src/agents/types.d.ts +145 -0
  39. package/dist/src/agents/types.js +18 -0
  40. package/dist/src/agents/types.js.map +1 -0
  41. package/dist/src/agents/utils.d.ts +15 -0
  42. package/dist/src/agents/utils.js +29 -0
  43. package/dist/src/agents/utils.js.map +1 -0
  44. package/dist/src/agents/utils.test.d.ts +6 -0
  45. package/dist/src/agents/utils.test.js +87 -0
  46. package/dist/src/agents/utils.test.js.map +1 -0
  47. package/dist/src/code_assist/oauth-credential-storage.js +1 -1
  48. package/dist/src/code_assist/oauth-credential-storage.js.map +1 -1
  49. package/dist/src/code_assist/oauth-credential-storage.test.js +1 -1
  50. package/dist/src/code_assist/oauth-credential-storage.test.js.map +1 -1
  51. package/dist/src/code_assist/oauth2.test.js +14 -13
  52. package/dist/src/code_assist/oauth2.test.js.map +1 -1
  53. package/dist/src/code_assist/setup.js +4 -2
  54. package/dist/src/code_assist/setup.js.map +1 -1
  55. package/dist/src/config/config.d.ts +61 -15
  56. package/dist/src/config/config.js +132 -28
  57. package/dist/src/config/config.js.map +1 -1
  58. package/dist/src/config/config.test.js +67 -3
  59. package/dist/src/config/config.test.js.map +1 -1
  60. package/dist/src/config/constants.d.ts +11 -0
  61. package/dist/src/config/constants.js +16 -0
  62. package/dist/src/config/constants.js.map +1 -0
  63. package/dist/src/config/storage.d.ts +0 -1
  64. package/dist/src/config/storage.js +2 -2
  65. package/dist/src/config/storage.js.map +1 -1
  66. package/dist/src/config/storage.test.js +7 -6
  67. package/dist/src/config/storage.test.js.map +1 -1
  68. package/dist/src/core/baseLlmClient.d.ts +4 -0
  69. package/dist/src/core/baseLlmClient.js +24 -23
  70. package/dist/src/core/baseLlmClient.js.map +1 -1
  71. package/dist/src/core/baseLlmClient.test.js +76 -13
  72. package/dist/src/core/baseLlmClient.test.js.map +1 -1
  73. package/dist/src/core/client.d.ts +6 -3
  74. package/dist/src/core/client.js +103 -64
  75. package/dist/src/core/client.js.map +1 -1
  76. package/dist/src/core/client.test.js +470 -118
  77. package/dist/src/core/client.test.js.map +1 -1
  78. package/dist/src/core/contentGenerator.js +3 -1
  79. package/dist/src/core/contentGenerator.js.map +1 -1
  80. package/dist/src/core/coreToolScheduler.js +12 -12
  81. package/dist/src/core/coreToolScheduler.js.map +1 -1
  82. package/dist/src/core/coreToolScheduler.test.js +260 -23
  83. package/dist/src/core/coreToolScheduler.test.js.map +1 -1
  84. package/dist/src/core/geminiChat.d.ts +11 -14
  85. package/dist/src/core/geminiChat.js +80 -124
  86. package/dist/src/core/geminiChat.js.map +1 -1
  87. package/dist/src/core/geminiChat.test.js +316 -239
  88. package/dist/src/core/geminiChat.test.js.map +1 -1
  89. package/dist/src/core/logger.test.js +16 -16
  90. package/dist/src/core/logger.test.js.map +1 -1
  91. package/dist/src/core/nonInteractiveToolExecutor.d.ts +3 -2
  92. package/dist/src/core/nonInteractiveToolExecutor.js +2 -2
  93. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  94. package/dist/src/core/nonInteractiveToolExecutor.test.js +19 -19
  95. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
  96. package/dist/src/core/prompts.d.ts +2 -1
  97. package/dist/src/core/prompts.js +53 -111
  98. package/dist/src/core/prompts.js.map +1 -1
  99. package/dist/src/core/prompts.test.js +83 -29
  100. package/dist/src/core/prompts.test.js.map +1 -1
  101. package/dist/src/core/subagent.js +1 -1
  102. package/dist/src/core/subagent.js.map +1 -1
  103. package/dist/src/core/subagent.test.js +38 -12
  104. package/dist/src/core/subagent.test.js.map +1 -1
  105. package/dist/src/core/turn.d.ts +15 -6
  106. package/dist/src/core/turn.js +14 -13
  107. package/dist/src/core/turn.js.map +1 -1
  108. package/dist/src/core/turn.test.js +14 -2
  109. package/dist/src/core/turn.test.js.map +1 -1
  110. package/dist/src/generated/git-commit.d.ts +2 -2
  111. package/dist/src/generated/git-commit.js +2 -2
  112. package/dist/src/generated/git-commit.js.map +1 -1
  113. package/dist/src/ide/detect-ide.d.ts +45 -14
  114. package/dist/src/ide/detect-ide.js +32 -69
  115. package/dist/src/ide/detect-ide.js.map +1 -1
  116. package/dist/src/ide/detect-ide.test.js +40 -46
  117. package/dist/src/ide/detect-ide.test.js.map +1 -1
  118. package/dist/src/ide/ide-client.d.ts +4 -4
  119. package/dist/src/ide/ide-client.js +33 -32
  120. package/dist/src/ide/ide-client.js.map +1 -1
  121. package/dist/src/ide/ide-client.test.js +12 -25
  122. package/dist/src/ide/ide-client.test.js.map +1 -1
  123. package/dist/src/ide/ide-installer.d.ts +2 -2
  124. package/dist/src/ide/ide-installer.js +8 -10
  125. package/dist/src/ide/ide-installer.js.map +1 -1
  126. package/dist/src/ide/ide-installer.test.js +33 -14
  127. package/dist/src/ide/ide-installer.test.js.map +1 -1
  128. package/dist/src/ide/process-utils.js +85 -75
  129. package/dist/src/ide/process-utils.js.map +1 -1
  130. package/dist/src/ide/process-utils.test.js +83 -90
  131. package/dist/src/ide/process-utils.test.js.map +1 -1
  132. package/dist/src/index.d.ts +7 -2
  133. package/dist/src/index.js +7 -2
  134. package/dist/src/index.js.map +1 -1
  135. package/dist/src/mcp/oauth-provider.d.ts +4 -1
  136. package/dist/src/mcp/oauth-provider.js +31 -25
  137. package/dist/src/mcp/oauth-provider.js.map +1 -1
  138. package/dist/src/mcp/sa-impersonation-provider.d.ts +33 -0
  139. package/dist/src/mcp/sa-impersonation-provider.js +130 -0
  140. package/dist/src/mcp/sa-impersonation-provider.js.map +1 -0
  141. package/dist/src/mcp/sa-impersonation-provider.test.d.ts +6 -0
  142. package/dist/src/mcp/sa-impersonation-provider.test.js +117 -0
  143. package/dist/src/mcp/sa-impersonation-provider.test.js.map +1 -0
  144. package/dist/src/mcp/token-storage/file-token-storage.js +2 -1
  145. package/dist/src/mcp/token-storage/file-token-storage.js.map +1 -1
  146. package/dist/src/mcp/token-storage/file-token-storage.test.js +4 -3
  147. package/dist/src/mcp/token-storage/file-token-storage.test.js.map +1 -1
  148. package/dist/src/policy/policy-engine.js +11 -2
  149. package/dist/src/policy/policy-engine.js.map +1 -1
  150. package/dist/src/policy/policy-engine.test.js +45 -0
  151. package/dist/src/policy/policy-engine.test.js.map +1 -1
  152. package/dist/src/routing/strategies/compositeStrategy.js +4 -3
  153. package/dist/src/routing/strategies/compositeStrategy.js.map +1 -1
  154. package/dist/src/services/chatRecordingService.d.ts +3 -2
  155. package/dist/src/services/chatRecordingService.js +3 -2
  156. package/dist/src/services/chatRecordingService.js.map +1 -1
  157. package/dist/src/services/fileSystemService.d.ts +9 -0
  158. package/dist/src/services/fileSystemService.js +11 -0
  159. package/dist/src/services/fileSystemService.js.map +1 -1
  160. package/dist/src/services/shellExecutionService.d.ts +3 -0
  161. package/dist/src/services/shellExecutionService.js +165 -49
  162. package/dist/src/services/shellExecutionService.js.map +1 -1
  163. package/dist/src/services/shellExecutionService.test.js +74 -5
  164. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  165. package/dist/src/telemetry/activity-detector.d.ts +41 -0
  166. package/dist/src/telemetry/activity-detector.js +61 -0
  167. package/dist/src/telemetry/activity-detector.js.map +1 -0
  168. package/dist/src/telemetry/activity-detector.test.d.ts +6 -0
  169. package/dist/src/telemetry/activity-detector.test.js +136 -0
  170. package/dist/src/telemetry/activity-detector.test.js.map +1 -0
  171. package/dist/src/telemetry/activity-types.d.ts +19 -0
  172. package/dist/src/telemetry/activity-types.js +21 -0
  173. package/dist/src/telemetry/activity-types.js.map +1 -0
  174. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +18 -2
  175. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +257 -108
  176. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  177. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.d.ts +1 -0
  178. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +258 -33
  179. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  180. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +118 -100
  181. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +146 -103
  182. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  183. package/dist/src/telemetry/config.d.ts +31 -0
  184. package/dist/src/telemetry/config.js +74 -0
  185. package/dist/src/telemetry/config.js.map +1 -0
  186. package/dist/src/telemetry/config.test.d.ts +6 -0
  187. package/dist/src/telemetry/config.test.js +124 -0
  188. package/dist/src/telemetry/config.test.js.map +1 -0
  189. package/dist/src/telemetry/constants.d.ts +0 -34
  190. package/dist/src/telemetry/constants.js +0 -34
  191. package/dist/src/telemetry/constants.js.map +1 -1
  192. package/dist/src/telemetry/index.d.ts +9 -3
  193. package/dist/src/telemetry/index.js +19 -2
  194. package/dist/src/telemetry/index.js.map +1 -1
  195. package/dist/src/telemetry/loggers.d.ts +10 -2
  196. package/dist/src/telemetry/loggers.js +206 -273
  197. package/dist/src/telemetry/loggers.js.map +1 -1
  198. package/dist/src/telemetry/loggers.test.circular.js +3 -3
  199. package/dist/src/telemetry/loggers.test.circular.js.map +1 -1
  200. package/dist/src/telemetry/loggers.test.js +316 -13
  201. package/dist/src/telemetry/loggers.test.js.map +1 -1
  202. package/dist/src/telemetry/memory-monitor.d.ts +149 -0
  203. package/dist/src/telemetry/memory-monitor.js +335 -0
  204. package/dist/src/telemetry/memory-monitor.js.map +1 -0
  205. package/dist/src/telemetry/memory-monitor.test.d.ts +6 -0
  206. package/dist/src/telemetry/memory-monitor.test.js +472 -0
  207. package/dist/src/telemetry/memory-monitor.test.js.map +1 -0
  208. package/dist/src/telemetry/metrics.d.ts +436 -11
  209. package/dist/src/telemetry/metrics.js +600 -110
  210. package/dist/src/telemetry/metrics.js.map +1 -1
  211. package/dist/src/telemetry/metrics.test.js +898 -16
  212. package/dist/src/telemetry/metrics.test.js.map +1 -1
  213. package/dist/src/telemetry/sdk.js +1 -1
  214. package/dist/src/telemetry/sdk.js.map +1 -1
  215. package/dist/src/telemetry/sdk.test.js +13 -0
  216. package/dist/src/telemetry/sdk.test.js.map +1 -1
  217. package/dist/src/telemetry/telemetryAttributes.d.ts +8 -0
  218. package/dist/src/telemetry/telemetryAttributes.js +18 -0
  219. package/dist/src/telemetry/telemetryAttributes.js.map +1 -0
  220. package/dist/src/telemetry/types.d.ts +168 -5
  221. package/dist/src/telemetry/types.js +696 -34
  222. package/dist/src/telemetry/types.js.map +1 -1
  223. package/dist/src/telemetry/uiTelemetry.d.ts +2 -2
  224. package/dist/src/telemetry/uiTelemetry.js +3 -4
  225. package/dist/src/telemetry/uiTelemetry.js.map +1 -1
  226. package/dist/src/telemetry/uiTelemetry.test.js +14 -14
  227. package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
  228. package/dist/src/test-utils/mock-tool.d.ts +28 -3
  229. package/dist/src/test-utils/mock-tool.js +71 -1
  230. package/dist/src/test-utils/mock-tool.js.map +1 -1
  231. package/dist/src/tools/glob.js +4 -2
  232. package/dist/src/tools/glob.js.map +1 -1
  233. package/dist/src/tools/ls.js +1 -1
  234. package/dist/src/tools/ls.js.map +1 -1
  235. package/dist/src/tools/mcp-client.d.ts +5 -14
  236. package/dist/src/tools/mcp-client.js +51 -98
  237. package/dist/src/tools/mcp-client.js.map +1 -1
  238. package/dist/src/tools/mcp-client.test.js +175 -157
  239. package/dist/src/tools/mcp-client.test.js.map +1 -1
  240. package/dist/src/tools/memoryTool.d.ts +1 -1
  241. package/dist/src/tools/memoryTool.js +1 -2
  242. package/dist/src/tools/memoryTool.js.map +1 -1
  243. package/dist/src/tools/memoryTool.test.js +9 -8
  244. package/dist/src/tools/memoryTool.test.js.map +1 -1
  245. package/dist/src/tools/message-bus-integration.test.d.ts +6 -0
  246. package/dist/src/tools/message-bus-integration.test.js +183 -0
  247. package/dist/src/tools/message-bus-integration.test.js.map +1 -0
  248. package/dist/src/tools/shell.js +60 -4
  249. package/dist/src/tools/shell.js.map +1 -1
  250. package/dist/src/tools/shell.test.js +2 -1
  251. package/dist/src/tools/shell.test.js.map +1 -1
  252. package/dist/src/tools/smart-edit.d.ts +1 -1
  253. package/dist/src/tools/smart-edit.js +116 -12
  254. package/dist/src/tools/smart-edit.js.map +1 -1
  255. package/dist/src/tools/smart-edit.test.js +91 -29
  256. package/dist/src/tools/smart-edit.test.js.map +1 -1
  257. package/dist/src/tools/tool-error.d.ts +22 -0
  258. package/dist/src/tools/tool-error.js +28 -0
  259. package/dist/src/tools/tool-error.js.map +1 -1
  260. package/dist/src/tools/tool-names.d.ts +9 -0
  261. package/dist/src/tools/tool-names.js +18 -0
  262. package/dist/src/tools/tool-names.js.map +1 -0
  263. package/dist/src/tools/tool-registry.test.js +10 -10
  264. package/dist/src/tools/tool-registry.test.js.map +1 -1
  265. package/dist/src/tools/tools.d.ts +11 -3
  266. package/dist/src/tools/tools.js +94 -3
  267. package/dist/src/tools/tools.js.map +1 -1
  268. package/dist/src/tools/web-fetch.d.ts +7 -0
  269. package/dist/src/tools/web-fetch.js +42 -10
  270. package/dist/src/tools/web-fetch.js.map +1 -1
  271. package/dist/src/tools/web-fetch.test.js +127 -8
  272. package/dist/src/tools/web-fetch.test.js.map +1 -1
  273. package/dist/src/tools/web-search.js +2 -1
  274. package/dist/src/tools/web-search.js.map +1 -1
  275. package/dist/src/tools/write-file.js +2 -1
  276. package/dist/src/tools/write-file.js.map +1 -1
  277. package/dist/src/tools/write-todos.d.ts +25 -0
  278. package/dist/src/tools/write-todos.js +151 -0
  279. package/dist/src/tools/write-todos.js.map +1 -0
  280. package/dist/src/tools/write-todos.test.d.ts +6 -0
  281. package/dist/src/tools/write-todos.test.js +89 -0
  282. package/dist/src/tools/write-todos.test.js.map +1 -0
  283. package/dist/src/utils/bfsFileSearch.d.ts +1 -1
  284. package/dist/src/utils/editCorrector.js +2 -2
  285. package/dist/src/utils/editCorrector.js.map +1 -1
  286. package/dist/src/utils/editor.js +1 -0
  287. package/dist/src/utils/editor.js.map +1 -1
  288. package/dist/src/utils/editor.test.js +1 -0
  289. package/dist/src/utils/editor.test.js.map +1 -1
  290. package/dist/src/utils/flashFallback.test.js +2 -2
  291. package/dist/src/utils/flashFallback.test.js.map +1 -1
  292. package/dist/src/utils/formatters.d.ts +1 -0
  293. package/dist/src/utils/formatters.js +2 -1
  294. package/dist/src/utils/formatters.js.map +1 -1
  295. package/dist/src/utils/formatters.test.d.ts +6 -0
  296. package/dist/src/utils/formatters.test.js +26 -0
  297. package/dist/src/utils/formatters.test.js.map +1 -0
  298. package/dist/src/utils/getFolderStructure.d.ts +1 -1
  299. package/dist/src/utils/getFolderStructure.js +1 -1
  300. package/dist/src/utils/getFolderStructure.js.map +1 -1
  301. package/dist/src/utils/getFolderStructure.test.js +7 -6
  302. package/dist/src/utils/getFolderStructure.test.js.map +1 -1
  303. package/dist/src/utils/installationManager.test.js +2 -1
  304. package/dist/src/utils/installationManager.test.js.map +1 -1
  305. package/dist/src/utils/llm-edit-fixer.js +14 -4
  306. package/dist/src/utils/llm-edit-fixer.js.map +1 -1
  307. package/dist/src/utils/llm-edit-fixer.test.js +81 -0
  308. package/dist/src/utils/llm-edit-fixer.test.js.map +1 -1
  309. package/dist/src/utils/memoryDiscovery.d.ts +2 -1
  310. package/dist/src/utils/memoryDiscovery.js +3 -2
  311. package/dist/src/utils/memoryDiscovery.js.map +1 -1
  312. package/dist/src/utils/memoryDiscovery.test.js +99 -21
  313. package/dist/src/utils/memoryDiscovery.test.js.map +1 -1
  314. package/dist/src/utils/memoryImportProcessor.js +13 -20
  315. package/dist/src/utils/memoryImportProcessor.js.map +1 -1
  316. package/dist/src/utils/memoryImportProcessor.test.js +14 -0
  317. package/dist/src/utils/memoryImportProcessor.test.js.map +1 -1
  318. package/dist/src/utils/pathCorrector.d.ts +25 -0
  319. package/dist/src/utils/pathCorrector.js +33 -0
  320. package/dist/src/utils/pathCorrector.js.map +1 -0
  321. package/dist/src/utils/pathCorrector.test.d.ts +6 -0
  322. package/dist/src/utils/pathCorrector.test.js +83 -0
  323. package/dist/src/utils/pathCorrector.test.js.map +1 -0
  324. package/dist/src/utils/retry.d.ts +4 -1
  325. package/dist/src/utils/retry.js +40 -17
  326. package/dist/src/utils/retry.js.map +1 -1
  327. package/dist/src/utils/retry.test.js +104 -31
  328. package/dist/src/utils/retry.test.js.map +1 -1
  329. package/dist/src/utils/schemaValidator.js +11 -1
  330. package/dist/src/utils/schemaValidator.js.map +1 -1
  331. package/dist/src/utils/schemaValidator.test.d.ts +6 -0
  332. package/dist/src/utils/schemaValidator.test.js +113 -0
  333. package/dist/src/utils/schemaValidator.test.js.map +1 -0
  334. package/dist/src/utils/shell-utils.d.ts +1 -0
  335. package/dist/src/utils/shell-utils.js +6 -2
  336. package/dist/src/utils/shell-utils.js.map +1 -1
  337. package/dist/src/utils/shell-utils.test.js +5 -0
  338. package/dist/src/utils/shell-utils.test.js.map +1 -1
  339. package/dist/src/utils/terminalSerializer.d.ts +1 -4
  340. package/dist/src/utils/terminalSerializer.js +3 -3
  341. package/dist/src/utils/terminalSerializer.js.map +1 -1
  342. package/dist/src/utils/thoughtUtils.d.ts +21 -0
  343. package/dist/src/utils/thoughtUtils.js +39 -0
  344. package/dist/src/utils/thoughtUtils.js.map +1 -0
  345. package/dist/src/utils/thoughtUtils.test.d.ts +6 -0
  346. package/dist/src/utils/thoughtUtils.test.js +78 -0
  347. package/dist/src/utils/thoughtUtils.test.js.map +1 -0
  348. package/dist/src/utils/tool-utils.js +2 -2
  349. package/dist/src/utils/tool-utils.js.map +1 -1
  350. package/dist/src/utils/tool-utils.test.js +8 -0
  351. package/dist/src/utils/tool-utils.test.js.map +1 -1
  352. package/dist/src/utils/userAccountManager.test.js +2 -1
  353. package/dist/src/utils/userAccountManager.test.js.map +1 -1
  354. package/dist/tsconfig.tsbuildinfo +1 -1
  355. package/package.json +2 -2
  356. package/dist/src/test-utils/tools.d.ts +0 -45
  357. package/dist/src/test-utils/tools.js +0 -105
  358. package/dist/src/test-utils/tools.js.map +0 -1
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
7
- import { findIndexAfterFraction, isThinkingDefault, isThinkingSupported, GeminiClient, } from './client.js';
7
+ import { findCompressSplitPoint, isThinkingDefault, isThinkingSupported, GeminiClient, } from './client.js';
8
8
  import { AuthType, } from './contentGenerator.js';
9
9
  import {} from './geminiChat.js';
10
10
  import { CompressionStatus, GeminiEventType, Turn, } from './turn.js';
@@ -15,6 +15,7 @@ import { setSimulate429 } from '../utils/testUtils.js';
15
15
  import { tokenLimit } from './tokenLimits.js';
16
16
  import { ideContextStore } from '../ide/ideContext.js';
17
17
  import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
18
+ import { uiTelemetryService } from '../telemetry/uiTelemetry.js';
18
19
  // Mock fs module to prevent actual file system operations during tests
19
20
  const mockFileSystem = new Map();
20
21
  vi.mock('node:fs', () => {
@@ -76,6 +77,12 @@ vi.mock('../telemetry/index.js', () => ({
76
77
  logApiError: vi.fn(),
77
78
  }));
78
79
  vi.mock('../ide/ideContext.js');
80
+ vi.mock('../telemetry/uiTelemetry.js', () => ({
81
+ uiTelemetryService: {
82
+ setLastPromptTokenCount: vi.fn(),
83
+ getLastPromptTokenCount: vi.fn(),
84
+ },
85
+ }));
79
86
  /**
80
87
  * Array.fromAsync ponyfill, which will be available in es 2024.
81
88
  *
@@ -88,42 +95,60 @@ async function fromAsync(promise) {
88
95
  }
89
96
  return results;
90
97
  }
91
- describe('findIndexAfterFraction', () => {
92
- const history = [
93
- { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66
94
- { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68
95
- { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66
96
- { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68
97
- { role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65
98
- ];
99
- // Total length: 333
98
+ describe('findCompressSplitPoint', () => {
100
99
  it('should throw an error for non-positive numbers', () => {
101
- expect(() => findIndexAfterFraction(history, 0)).toThrow('Fraction must be between 0 and 1');
100
+ expect(() => findCompressSplitPoint([], 0)).toThrow('Fraction must be between 0 and 1');
102
101
  });
103
102
  it('should throw an error for a fraction greater than or equal to 1', () => {
104
- expect(() => findIndexAfterFraction(history, 1)).toThrow('Fraction must be between 0 and 1');
103
+ expect(() => findCompressSplitPoint([], 1)).toThrow('Fraction must be between 0 and 1');
104
+ });
105
+ it('should handle an empty history', () => {
106
+ expect(findCompressSplitPoint([], 0.5)).toBe(0);
105
107
  });
106
108
  it('should handle a fraction in the middle', () => {
107
- // 333 * 0.5 = 166.5
108
- // 0: 66
109
- // 1: 66 + 68 = 134
110
- // 2: 134 + 66 = 200
111
- // 200 >= 166.5, so index is 3
112
- expect(findIndexAfterFraction(history, 0.5)).toBe(3);
109
+ const history = [
110
+ { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66 (19%)
111
+ { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68 (40%)
112
+ { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66 (60%)
113
+ { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68 (80%)
114
+ { role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65 (100%)
115
+ ];
116
+ expect(findCompressSplitPoint(history, 0.5)).toBe(4);
117
+ });
118
+ it('should handle a fraction of last index', () => {
119
+ const history = [
120
+ { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66 (19%)
121
+ { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68 (40%)
122
+ { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66 (60%)
123
+ { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68 (80%)
124
+ { role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65 (100%)
125
+ ];
126
+ expect(findCompressSplitPoint(history, 0.9)).toBe(4);
113
127
  });
114
- it('should handle a fraction that results in the last index', () => {
115
- // 333 * 0.9 = 299.7
116
- // ...
117
- // 3: 200 + 68 = 268
118
- // 4: 268 + 65 = 333
119
- // 333 >= 299.7, so index is 5
120
- expect(findIndexAfterFraction(history, 0.9)).toBe(5);
128
+ it('should handle a fraction of after last index', () => {
129
+ const history = [
130
+ { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66 (24%%)
131
+ { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68 (50%)
132
+ { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66 (74%)
133
+ { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68 (100%)
134
+ ];
135
+ expect(findCompressSplitPoint(history, 0.8)).toBe(4);
121
136
  });
122
- it('should handle an empty history', () => {
123
- expect(findIndexAfterFraction([], 0.5)).toBe(0);
137
+ it('should return earlier splitpoint if no valid ones are after threshhold', () => {
138
+ const history = [
139
+ { role: 'user', parts: [{ text: 'This is the first message.' }] },
140
+ { role: 'model', parts: [{ text: 'This is the second message.' }] },
141
+ { role: 'user', parts: [{ text: 'This is the third message.' }] },
142
+ { role: 'model', parts: [{ functionCall: {} }] },
143
+ ];
144
+ // Can't return 4 because the previous item has a function call.
145
+ expect(findCompressSplitPoint(history, 0.99)).toBe(2);
124
146
  });
125
147
  it('should handle a history with only one item', () => {
126
- expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(1);
148
+ const historyWithEmptyParts = [
149
+ { role: 'user', parts: [{ text: 'Message 1' }] },
150
+ ];
151
+ expect(findCompressSplitPoint(historyWithEmptyParts, 0.5)).toBe(0);
127
152
  });
128
153
  it('should handle history with weird parts', () => {
129
154
  const historyWithEmptyParts = [
@@ -131,7 +156,7 @@ describe('findIndexAfterFraction', () => {
131
156
  { role: 'model', parts: [{ fileData: { fileUri: 'derp' } }] },
132
157
  { role: 'user', parts: [{ text: 'Message 2' }] },
133
158
  ];
134
- expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(2);
159
+ expect(findCompressSplitPoint(historyWithEmptyParts, 0.5)).toBe(2);
135
160
  });
136
161
  });
137
162
  describe('isThinkingSupported', () => {
@@ -168,6 +193,7 @@ describe('Gemini Client (client.ts)', () => {
168
193
  let mockGenerateContentFn;
169
194
  beforeEach(async () => {
170
195
  vi.resetAllMocks();
196
+ vi.mocked(uiTelemetryService.setLastPromptTokenCount).mockClear();
171
197
  mockGenerateContentFn = vi.fn().mockResolvedValue({
172
198
  candidates: [{ content: { parts: [{ text: '{"key": "value"}' }] } }],
173
199
  });
@@ -176,7 +202,6 @@ describe('Gemini Client (client.ts)', () => {
176
202
  mockContentGenerator = {
177
203
  generateContent: mockGenerateContentFn,
178
204
  generateContentStream: vi.fn(),
179
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 100 }),
180
205
  batchEmbedContents: vi.fn(),
181
206
  };
182
207
  // Because the GeminiClient constructor kicks off an async process (startChat)
@@ -229,6 +254,7 @@ describe('Gemini Client (client.ts)', () => {
229
254
  getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
230
255
  getUseSmartEdit: vi.fn().mockReturnValue(false),
231
256
  getUseModelRouter: vi.fn().mockReturnValue(false),
257
+ getContinueOnFailedApiCall: vi.fn(),
232
258
  getProjectRoot: vi.fn().mockReturnValue('/test/project/root'),
233
259
  storage: {
234
260
  getProjectTempDir: vi.fn().mockReturnValue('/test/temp'),
@@ -299,72 +325,128 @@ describe('Gemini Client (client.ts)', () => {
299
325
  function setup({ chatHistory = [
300
326
  { role: 'user', parts: [{ text: 'Long conversation' }] },
301
327
  { role: 'model', parts: [{ text: 'Long response' }] },
302
- ], } = {}) {
303
- const mockChat = {
304
- getHistory: vi.fn().mockReturnValue(chatHistory),
328
+ ], originalTokenCount = 1000, summaryText = 'This is a summary.', } = {}) {
329
+ const mockOriginalChat = {
330
+ getHistory: vi.fn((_curated) => chatHistory),
305
331
  setHistory: vi.fn(),
306
332
  };
307
- vi.mocked(mockContentGenerator.countTokens)
308
- .mockResolvedValueOnce({ totalTokens: 1000 })
309
- .mockResolvedValueOnce({ totalTokens: 5000 });
310
- client['chat'] = mockChat;
311
- client['startChat'] = vi.fn().mockResolvedValue({ ...mockChat });
312
- return { client, mockChat };
333
+ client['chat'] = mockOriginalChat;
334
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
335
+ mockGenerateContentFn.mockResolvedValue({
336
+ candidates: [
337
+ {
338
+ content: {
339
+ role: 'model',
340
+ parts: [{ text: summaryText }],
341
+ },
342
+ },
343
+ ],
344
+ });
345
+ // Calculate what the new history will be
346
+ const splitPoint = findCompressSplitPoint(chatHistory, 0.7); // 1 - 0.3
347
+ const historyToKeep = chatHistory.slice(splitPoint);
348
+ // This is the history that the new chat will have.
349
+ // It includes the default startChat history + the extra history from tryCompressChat
350
+ const newCompressedHistory = [
351
+ // Mocked envParts + canned response from startChat
352
+ {
353
+ role: 'user',
354
+ parts: [{ text: 'Mocked env context' }],
355
+ },
356
+ {
357
+ role: 'model',
358
+ parts: [{ text: 'Got it. Thanks for the context!' }],
359
+ },
360
+ // extraHistory from tryCompressChat
361
+ {
362
+ role: 'user',
363
+ parts: [{ text: summaryText }],
364
+ },
365
+ {
366
+ role: 'model',
367
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
368
+ },
369
+ ...historyToKeep,
370
+ ];
371
+ const mockNewChat = {
372
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
373
+ setHistory: vi.fn(),
374
+ };
375
+ client['startChat'] = vi
376
+ .fn()
377
+ .mockResolvedValue(mockNewChat);
378
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
379
+ const estimatedNewTokenCount = Math.floor(totalChars / 4);
380
+ return {
381
+ client,
382
+ mockOriginalChat,
383
+ mockNewChat,
384
+ estimatedNewTokenCount,
385
+ };
313
386
  }
314
387
  describe('when compression inflates the token count', () => {
315
388
  it('allows compression to be forced/manual after a failure', async () => {
316
- const { client } = setup();
317
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
318
- totalTokens: 1000,
389
+ // Call 1 (Fails): Setup with a long summary to inflate tokens
390
+ const longSummary = 'long summary '.repeat(100);
391
+ const { client, estimatedNewTokenCount: inflatedTokenCount } = setup({
392
+ originalTokenCount: 100,
393
+ summaryText: longSummary,
319
394
  });
395
+ expect(inflatedTokenCount).toBeGreaterThan(100); // Ensure setup is correct
320
396
  await client.tryCompressChat('prompt-id-4', false); // Fails
321
- const result = await client.tryCompressChat('prompt-id-4', true);
397
+ // Call 2 (Forced): Re-setup with a short summary
398
+ const shortSummary = 'short';
399
+ const { estimatedNewTokenCount: compressedTokenCount } = setup({
400
+ originalTokenCount: 100,
401
+ summaryText: shortSummary,
402
+ });
403
+ expect(compressedTokenCount).toBeLessThanOrEqual(100); // Ensure setup is correct
404
+ const result = await client.tryCompressChat('prompt-id-4', true); // Forced
322
405
  expect(result).toEqual({
323
406
  compressionStatus: CompressionStatus.COMPRESSED,
324
- newTokenCount: 1000,
325
- originalTokenCount: 1000,
407
+ newTokenCount: compressedTokenCount,
408
+ originalTokenCount: 100,
326
409
  });
327
410
  });
328
411
  it('yields the result even if the compression inflated the tokens', async () => {
329
- const { client } = setup();
330
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
331
- totalTokens: 1000,
412
+ const longSummary = 'long summary '.repeat(100);
413
+ const { client, estimatedNewTokenCount } = setup({
414
+ originalTokenCount: 100,
415
+ summaryText: longSummary,
332
416
  });
417
+ expect(estimatedNewTokenCount).toBeGreaterThan(100); // Ensure setup is correct
333
418
  const result = await client.tryCompressChat('prompt-id-4', false);
334
419
  expect(result).toEqual({
335
420
  compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
336
- newTokenCount: 5000,
337
- originalTokenCount: 1000,
421
+ newTokenCount: estimatedNewTokenCount,
422
+ originalTokenCount: 100,
338
423
  });
424
+ // IMPORTANT: The change in client.ts means setLastPromptTokenCount is NOT called on failure
425
+ expect(uiTelemetryService.setLastPromptTokenCount).not.toHaveBeenCalled();
339
426
  });
340
427
  it('does not manipulate the source chat', async () => {
341
- const { client, mockChat } = setup();
342
- await client.tryCompressChat('prompt-id-4', false);
343
- expect(client['chat']).toBe(mockChat); // a new chat session was not created
344
- });
345
- it('restores the history back to the original', async () => {
346
- vi.mocked(tokenLimit).mockReturnValue(1000);
347
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
348
- totalTokens: 999,
349
- });
350
- const originalHistory = [
351
- { role: 'user', parts: [{ text: 'what is your wisdom?' }] },
352
- { role: 'model', parts: [{ text: 'some wisdom' }] },
353
- { role: 'user', parts: [{ text: 'ahh that is a good a wisdom' }] },
354
- ];
355
- const { client } = setup({
356
- chatHistory: originalHistory,
428
+ const longSummary = 'long summary '.repeat(100);
429
+ const { client, mockOriginalChat, estimatedNewTokenCount } = setup({
430
+ originalTokenCount: 100,
431
+ summaryText: longSummary,
357
432
  });
358
- const { compressionStatus } = await client.tryCompressChat('prompt-id-4', false);
359
- expect(compressionStatus).toBe(CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT);
360
- expect(client['chat']?.setHistory).toHaveBeenCalledWith(originalHistory);
433
+ expect(estimatedNewTokenCount).toBeGreaterThan(100); // Ensure setup is correct
434
+ await client.tryCompressChat('prompt-id-4', false);
435
+ // On failure, the chat should NOT be replaced
436
+ expect(client['chat']).toBe(mockOriginalChat);
361
437
  });
362
438
  it('will not attempt to compress context after a failure', async () => {
363
- const { client } = setup();
364
- await client.tryCompressChat('prompt-id-4', false);
439
+ const longSummary = 'long summary '.repeat(100);
440
+ const { client, estimatedNewTokenCount } = setup({
441
+ originalTokenCount: 100,
442
+ summaryText: longSummary,
443
+ });
444
+ expect(estimatedNewTokenCount).toBeGreaterThan(100); // Ensure setup is correct
445
+ await client.tryCompressChat('prompt-id-4', false); // This fails and sets hasFailedCompressionAttempt = true
446
+ // This call should now be a NOOP
365
447
  const result = await client.tryCompressChat('prompt-id-5', false);
366
- // it counts tokens for {original, compressed} and then never again
367
- expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
448
+ // generateContent (for summary) should only have been called once
449
+ expect(mockGenerateContentFn).toHaveBeenCalledTimes(1);
368
450
  expect(result).toEqual({
369
451
  compressionStatus: CompressionStatus.NOOP,
370
452
  newTokenCount: 0,
@@ -378,43 +460,80 @@ describe('Gemini Client (client.ts)', () => {
378
460
  mockGetHistory.mockReturnValue([
379
461
  { role: 'user', parts: [{ text: '...history...' }] },
380
462
  ]);
381
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
382
- totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
383
- });
463
+ const originalTokenCount = MOCKED_TOKEN_LIMIT * 0.699;
464
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
384
465
  const initialChat = client.getChat();
385
466
  const result = await client.tryCompressChat('prompt-id-2', false);
386
467
  const newChat = client.getChat();
387
468
  expect(tokenLimit).toHaveBeenCalled();
388
469
  expect(result).toEqual({
389
470
  compressionStatus: CompressionStatus.NOOP,
390
- newTokenCount: 699,
391
- originalTokenCount: 699,
471
+ newTokenCount: originalTokenCount,
472
+ originalTokenCount,
392
473
  });
393
474
  expect(newChat).toBe(initialChat);
394
475
  });
476
+ it('should return NOOP if history is too short to compress', async () => {
477
+ const { client } = setup({
478
+ chatHistory: [{ role: 'user', parts: [{ text: 'hi' }] }],
479
+ originalTokenCount: 50,
480
+ });
481
+ const result = await client.tryCompressChat('prompt-id-noop', false);
482
+ expect(result).toEqual({
483
+ compressionStatus: CompressionStatus.NOOP,
484
+ originalTokenCount: 50,
485
+ newTokenCount: 50,
486
+ });
487
+ expect(mockGenerateContentFn).not.toHaveBeenCalled();
488
+ });
395
489
  it('logs a telemetry event when compressing', async () => {
396
490
  vi.spyOn(ClearcutLogger.prototype, 'logChatCompressionEvent');
397
491
  const MOCKED_TOKEN_LIMIT = 1000;
398
492
  const MOCKED_CONTEXT_PERCENTAGE_THRESHOLD = 0.5;
399
- vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
400
493
  vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
401
494
  contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
402
495
  });
403
- mockGetHistory.mockReturnValue([
496
+ const history = [
404
497
  { role: 'user', parts: [{ text: '...history...' }] },
405
- ]);
498
+ { role: 'model', parts: [{ text: '...history...' }] },
499
+ { role: 'user', parts: [{ text: '...history...' }] },
500
+ { role: 'model', parts: [{ text: '...history...' }] },
501
+ { role: 'user', parts: [{ text: '...history...' }] },
502
+ { role: 'model', parts: [{ text: '...history...' }] },
503
+ ];
504
+ mockGetHistory.mockReturnValue(history);
406
505
  const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
407
- const newTokenCount = 100;
408
- vi.mocked(mockContentGenerator.countTokens)
409
- .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
410
- .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
506
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
507
+ // We need to control the estimated new token count.
508
+ // We mock startChat to return a chat with a known history.
509
+ const summaryText = 'This is a summary.';
510
+ const splitPoint = findCompressSplitPoint(history, 0.7);
511
+ const historyToKeep = history.slice(splitPoint);
512
+ const newCompressedHistory = [
513
+ { role: 'user', parts: [{ text: 'Mocked env context' }] },
514
+ { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
515
+ { role: 'user', parts: [{ text: summaryText }] },
516
+ {
517
+ role: 'model',
518
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
519
+ },
520
+ ...historyToKeep,
521
+ ];
522
+ const mockNewChat = {
523
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
524
+ };
525
+ client['startChat'] = vi
526
+ .fn()
527
+ .mockResolvedValue(mockNewChat);
528
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
529
+ const newTokenCount = Math.floor(totalChars / 4);
411
530
  // Mock the summary response from the chat
412
531
  mockGenerateContentFn.mockResolvedValue({
413
532
  candidates: [
414
533
  {
415
534
  content: {
416
535
  role: 'model',
417
- parts: [{ text: 'This is a summary.' }],
536
+ parts: [{ text: summaryText }],
418
537
  },
419
538
  },
420
539
  ],
@@ -424,6 +543,8 @@ describe('Gemini Client (client.ts)', () => {
424
543
  tokens_before: originalTokenCount,
425
544
  tokens_after: newTokenCount,
426
545
  }));
546
+ expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledWith(newTokenCount);
547
+ expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledTimes(1);
427
548
  });
428
549
  it('should trigger summarization if token count is at threshold with contextPercentageThreshold setting', async () => {
429
550
  const MOCKED_TOKEN_LIMIT = 1000;
@@ -432,21 +553,46 @@ describe('Gemini Client (client.ts)', () => {
432
553
  vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
433
554
  contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
434
555
  });
435
- mockGetHistory.mockReturnValue([
556
+ const history = [
436
557
  { role: 'user', parts: [{ text: '...history...' }] },
437
- ]);
558
+ { role: 'model', parts: [{ text: '...history...' }] },
559
+ { role: 'user', parts: [{ text: '...history...' }] },
560
+ { role: 'model', parts: [{ text: '...history...' }] },
561
+ { role: 'user', parts: [{ text: '...history...' }] },
562
+ { role: 'model', parts: [{ text: '...history...' }] },
563
+ ];
564
+ mockGetHistory.mockReturnValue(history);
438
565
  const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
439
- const newTokenCount = 100;
440
- vi.mocked(mockContentGenerator.countTokens)
441
- .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
442
- .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
566
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
567
+ // Mock summary and new chat
568
+ const summaryText = 'This is a summary.';
569
+ const splitPoint = findCompressSplitPoint(history, 0.7);
570
+ const historyToKeep = history.slice(splitPoint);
571
+ const newCompressedHistory = [
572
+ { role: 'user', parts: [{ text: 'Mocked env context' }] },
573
+ { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
574
+ { role: 'user', parts: [{ text: summaryText }] },
575
+ {
576
+ role: 'model',
577
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
578
+ },
579
+ ...historyToKeep,
580
+ ];
581
+ const mockNewChat = {
582
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
583
+ };
584
+ client['startChat'] = vi
585
+ .fn()
586
+ .mockResolvedValue(mockNewChat);
587
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
588
+ const newTokenCount = Math.floor(totalChars / 4);
443
589
  // Mock the summary response from the chat
444
590
  mockGenerateContentFn.mockResolvedValue({
445
591
  candidates: [
446
592
  {
447
593
  content: {
448
594
  role: 'model',
449
- parts: [{ text: 'This is a summary.' }],
595
+ parts: [{ text: summaryText }],
450
596
  },
451
597
  },
452
598
  ],
@@ -468,7 +614,7 @@ describe('Gemini Client (client.ts)', () => {
468
614
  it('should not compress across a function call response', async () => {
469
615
  const MOCKED_TOKEN_LIMIT = 1000;
470
616
  vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
471
- mockGetHistory.mockReturnValue([
617
+ const history = [
472
618
  { role: 'user', parts: [{ text: '...history 1...' }] },
473
619
  { role: 'model', parts: [{ text: '...history 2...' }] },
474
620
  { role: 'user', parts: [{ text: '...history 3...' }] },
@@ -485,19 +631,43 @@ describe('Gemini Client (client.ts)', () => {
485
631
  { role: 'model', parts: [{ text: '...history 10...' }] },
486
632
  // Instead we will break here.
487
633
  { role: 'user', parts: [{ text: '...history 10...' }] },
488
- ]);
634
+ ];
635
+ mockGetHistory.mockReturnValue(history);
489
636
  const originalTokenCount = 1000 * 0.7;
490
- const newTokenCount = 100;
491
- vi.mocked(mockContentGenerator.countTokens)
492
- .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
493
- .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
637
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
638
+ // Mock summary and new chat
639
+ const summaryText = 'This is a summary.';
640
+ const splitPoint = findCompressSplitPoint(history, 0.7); // This should be 10
641
+ expect(splitPoint).toBe(10); // Verify split point logic
642
+ const historyToKeep = history.slice(splitPoint); // Should keep last user message
643
+ expect(historyToKeep).toEqual([
644
+ { role: 'user', parts: [{ text: '...history 10...' }] },
645
+ ]);
646
+ const newCompressedHistory = [
647
+ { role: 'user', parts: [{ text: 'Mocked env context' }] },
648
+ { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
649
+ { role: 'user', parts: [{ text: summaryText }] },
650
+ {
651
+ role: 'model',
652
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
653
+ },
654
+ ...historyToKeep,
655
+ ];
656
+ const mockNewChat = {
657
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
658
+ };
659
+ client['startChat'] = vi
660
+ .fn()
661
+ .mockResolvedValue(mockNewChat);
662
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
663
+ const newTokenCount = Math.floor(totalChars / 4);
494
664
  // Mock the summary response from the chat
495
665
  mockGenerateContentFn.mockResolvedValue({
496
666
  candidates: [
497
667
  {
498
668
  content: {
499
669
  role: 'model',
500
- parts: [{ text: 'This is a summary.' }],
670
+ parts: [{ text: summaryText }],
501
671
  },
502
672
  },
503
673
  ],
@@ -515,35 +685,60 @@ describe('Gemini Client (client.ts)', () => {
515
685
  });
516
686
  // Assert that the chat was reset
517
687
  expect(newChat).not.toBe(initialChat);
518
- // 1. standard start context message
519
- // 2. standard canned user start message
520
- // 3. compressed summary message
521
- // 4. standard canned user summary message
522
- // 5. The last user message (not the last 3 because that would start with a function response)
688
+ // 1. standard start context message (env)
689
+ // 2. standard canned model response
690
+ // 3. compressed summary message (user)
691
+ // 4. standard canned model response
692
+ // 5. The last user message (historyToKeep)
523
693
  expect(newChat.getHistory().length).toEqual(5);
524
694
  });
525
695
  it('should always trigger summarization when force is true, regardless of token count', async () => {
526
- mockGetHistory.mockReturnValue([
696
+ const history = [
527
697
  { role: 'user', parts: [{ text: '...history...' }] },
528
- ]);
529
- const originalTokenCount = 10; // Well below threshold
530
- const newTokenCount = 5;
531
- vi.mocked(mockContentGenerator.countTokens)
532
- .mockResolvedValueOnce({ totalTokens: originalTokenCount })
533
- .mockResolvedValueOnce({ totalTokens: newTokenCount });
698
+ { role: 'model', parts: [{ text: '...history...' }] },
699
+ { role: 'user', parts: [{ text: '...history...' }] },
700
+ { role: 'model', parts: [{ text: '...history...' }] },
701
+ { role: 'user', parts: [{ text: '...history...' }] },
702
+ { role: 'model', parts: [{ text: '...history...' }] },
703
+ ];
704
+ mockGetHistory.mockReturnValue(history);
705
+ const originalTokenCount = 100; // Well below threshold, but > estimated new count
706
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
707
+ // Mock summary and new chat
708
+ const summaryText = 'This is a summary.';
709
+ const splitPoint = findCompressSplitPoint(history, 0.7);
710
+ const historyToKeep = history.slice(splitPoint);
711
+ const newCompressedHistory = [
712
+ { role: 'user', parts: [{ text: 'Mocked env context' }] },
713
+ { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
714
+ { role: 'user', parts: [{ text: summaryText }] },
715
+ {
716
+ role: 'model',
717
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
718
+ },
719
+ ...historyToKeep,
720
+ ];
721
+ const mockNewChat = {
722
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
723
+ };
724
+ client['startChat'] = vi
725
+ .fn()
726
+ .mockResolvedValue(mockNewChat);
727
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
728
+ const newTokenCount = Math.floor(totalChars / 4);
534
729
  // Mock the summary response from the chat
535
730
  mockGenerateContentFn.mockResolvedValue({
536
731
  candidates: [
537
732
  {
538
733
  content: {
539
734
  role: 'model',
540
- parts: [{ text: 'This is a summary.' }],
735
+ parts: [{ text: summaryText }],
541
736
  },
542
737
  },
543
738
  ],
544
739
  });
545
740
  const initialChat = client.getChat();
546
- const result = await client.tryCompressChat('prompt-id-1', false); // force = true
741
+ const result = await client.tryCompressChat('prompt-id-1', true); // force = true
547
742
  const newChat = client.getChat();
548
743
  expect(mockGenerateContentFn).toHaveBeenCalled();
549
744
  expect(result).toEqual({
@@ -581,9 +776,6 @@ describe('Gemini Client (client.ts)', () => {
581
776
  compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
582
777
  },
583
778
  { compressionStatus: CompressionStatus.NOOP },
584
- {
585
- compressionStatus: CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR,
586
- },
587
779
  ])('does not emit a compression event when the status is $compressionStatus', async ({ compressionStatus }) => {
588
780
  // Arrange
589
781
  const mockStream = (async function* () {
@@ -841,6 +1033,7 @@ ${JSON.stringify({
841
1033
  expect(finalResult).toBeInstanceOf(Turn);
842
1034
  });
843
1035
  it('should stop infinite loop after MAX_TURNS when nextSpeaker always returns model', async () => {
1036
+ vi.spyOn(client['config'], 'getContinueOnFailedApiCall').mockReturnValue(true);
844
1037
  // Get the mocked checkNextSpeaker function and configure it to trigger infinite loop
845
1038
  const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
846
1039
  const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
@@ -996,6 +1189,82 @@ ${JSON.stringify({
996
1189
  console.log(`Infinite loop protection working: checkNextSpeaker called ${callCount} times, ` +
997
1190
  `${eventCount} events generated (properly bounded by MAX_TURNS)`);
998
1191
  });
1192
+ it('should yield ContextWindowWillOverflow when the context window is about to overflow', async () => {
1193
+ // Arrange
1194
+ const MOCKED_TOKEN_LIMIT = 1000;
1195
+ vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
1196
+ // Set last prompt token count
1197
+ const lastPromptTokenCount = 900;
1198
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(lastPromptTokenCount);
1199
+ // Remaining = 100. Threshold (95%) = 95.
1200
+ // We need a request > 95 tokens.
1201
+ // A string of length 400 is roughly 100 tokens.
1202
+ const longText = 'a'.repeat(400);
1203
+ const request = [{ text: longText }];
1204
+ const estimatedRequestTokenCount = Math.floor(JSON.stringify(request).length / 4);
1205
+ const remainingTokenCount = MOCKED_TOKEN_LIMIT - lastPromptTokenCount;
1206
+ // Mock tryCompressChat to not compress
1207
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
1208
+ originalTokenCount: lastPromptTokenCount,
1209
+ newTokenCount: lastPromptTokenCount,
1210
+ compressionStatus: CompressionStatus.NOOP,
1211
+ });
1212
+ // Act
1213
+ const stream = client.sendMessageStream(request, new AbortController().signal, 'prompt-id-overflow');
1214
+ const events = await fromAsync(stream);
1215
+ // Assert
1216
+ expect(events).toContainEqual({
1217
+ type: GeminiEventType.ContextWindowWillOverflow,
1218
+ value: {
1219
+ estimatedRequestTokenCount,
1220
+ remainingTokenCount,
1221
+ },
1222
+ });
1223
+ // Ensure turn.run is not called
1224
+ expect(mockTurnRunFn).not.toHaveBeenCalled();
1225
+ });
1226
+ it("should use the sticky model's token limit for the overflow check", async () => {
1227
+ // Arrange
1228
+ const STICKY_MODEL = 'gemini-1.5-flash';
1229
+ const STICKY_MODEL_LIMIT = 1000;
1230
+ const CONFIG_MODEL_LIMIT = 2000;
1231
+ // Set up token limits
1232
+ vi.mocked(tokenLimit).mockImplementation((model) => {
1233
+ if (model === STICKY_MODEL)
1234
+ return STICKY_MODEL_LIMIT;
1235
+ return CONFIG_MODEL_LIMIT;
1236
+ });
1237
+ // Set the sticky model
1238
+ client['currentSequenceModel'] = STICKY_MODEL;
1239
+ // Set token count
1240
+ const lastPromptTokenCount = 900;
1241
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(lastPromptTokenCount);
1242
+ // Remaining (sticky) = 100. Threshold (95%) = 95.
1243
+ // We need a request > 95 tokens.
1244
+ const longText = 'a'.repeat(400);
1245
+ const request = [{ text: longText }];
1246
+ const estimatedRequestTokenCount = Math.floor(JSON.stringify(request).length / 4);
1247
+ const remainingTokenCount = STICKY_MODEL_LIMIT - lastPromptTokenCount;
1248
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
1249
+ originalTokenCount: lastPromptTokenCount,
1250
+ newTokenCount: lastPromptTokenCount,
1251
+ compressionStatus: CompressionStatus.NOOP,
1252
+ });
1253
+ // Act
1254
+ const stream = client.sendMessageStream(request, new AbortController().signal, 'test-session-id');
1255
+ const events = await fromAsync(stream);
1256
+ // Assert
1257
+ // Should overflow based on the sticky model's limit
1258
+ expect(events).toContainEqual({
1259
+ type: GeminiEventType.ContextWindowWillOverflow,
1260
+ value: {
1261
+ estimatedRequestTokenCount,
1262
+ remainingTokenCount,
1263
+ },
1264
+ });
1265
+ expect(tokenLimit).toHaveBeenCalledWith(STICKY_MODEL);
1266
+ expect(mockTurnRunFn).not.toHaveBeenCalled();
1267
+ });
999
1268
  describe('Model Routing', () => {
1000
1269
  let mockRouterService;
1001
1270
  beforeEach(() => {
@@ -1081,6 +1350,89 @@ ${JSON.stringify({
1081
1350
  [{ text: 'Continue' }], expect.any(Object));
1082
1351
  });
1083
1352
  });
1353
+ it('should recursively call sendMessageStream with "Please continue." when InvalidStream event is received', async () => {
1354
+ vi.spyOn(client['config'], 'getContinueOnFailedApiCall').mockReturnValue(true);
1355
+ // Arrange
1356
+ const mockStream1 = (async function* () {
1357
+ yield { type: GeminiEventType.InvalidStream };
1358
+ })();
1359
+ const mockStream2 = (async function* () {
1360
+ yield { type: GeminiEventType.Content, value: 'Continued content' };
1361
+ })();
1362
+ mockTurnRunFn
1363
+ .mockReturnValueOnce(mockStream1)
1364
+ .mockReturnValueOnce(mockStream2);
1365
+ const mockChat = {
1366
+ addHistory: vi.fn(),
1367
+ getHistory: vi.fn().mockReturnValue([]),
1368
+ };
1369
+ client['chat'] = mockChat;
1370
+ const initialRequest = [{ text: 'Hi' }];
1371
+ const promptId = 'prompt-id-invalid-stream';
1372
+ const signal = new AbortController().signal;
1373
+ // Act
1374
+ const stream = client.sendMessageStream(initialRequest, signal, promptId);
1375
+ const events = await fromAsync(stream);
1376
+ // Assert
1377
+ expect(events).toEqual([
1378
+ { type: GeminiEventType.InvalidStream },
1379
+ { type: GeminiEventType.Content, value: 'Continued content' },
1380
+ ]);
1381
+ // Verify that turn.run was called twice
1382
+ expect(mockTurnRunFn).toHaveBeenCalledTimes(2);
1383
+ // First call with original request
1384
+ expect(mockTurnRunFn).toHaveBeenNthCalledWith(1, expect.any(String), initialRequest, expect.any(Object));
1385
+ // Second call with "Please continue."
1386
+ expect(mockTurnRunFn).toHaveBeenNthCalledWith(2, expect.any(String), [{ text: 'System: Please continue.' }], expect.any(Object));
1387
+ });
1388
+ it('should not recursively call sendMessageStream with "Please continue." when InvalidStream event is received and flag is false', async () => {
1389
+ vi.spyOn(client['config'], 'getContinueOnFailedApiCall').mockReturnValue(false);
1390
+ // Arrange
1391
+ const mockStream1 = (async function* () {
1392
+ yield { type: GeminiEventType.InvalidStream };
1393
+ })();
1394
+ mockTurnRunFn.mockReturnValueOnce(mockStream1);
1395
+ const mockChat = {
1396
+ addHistory: vi.fn(),
1397
+ getHistory: vi.fn().mockReturnValue([]),
1398
+ };
1399
+ client['chat'] = mockChat;
1400
+ const initialRequest = [{ text: 'Hi' }];
1401
+ const promptId = 'prompt-id-invalid-stream';
1402
+ const signal = new AbortController().signal;
1403
+ // Act
1404
+ const stream = client.sendMessageStream(initialRequest, signal, promptId);
1405
+ const events = await fromAsync(stream);
1406
+ // Assert
1407
+ expect(events).toEqual([{ type: GeminiEventType.InvalidStream }]);
1408
+ // Verify that turn.run was called only once
1409
+ expect(mockTurnRunFn).toHaveBeenCalledTimes(1);
1410
+ });
1411
+ it('should stop recursing after one retry when InvalidStream events are repeatedly received', async () => {
1412
+ vi.spyOn(client['config'], 'getContinueOnFailedApiCall').mockReturnValue(true);
1413
+ // Arrange
1414
+ // Always return a new invalid stream
1415
+ mockTurnRunFn.mockImplementation(() => (async function* () {
1416
+ yield { type: GeminiEventType.InvalidStream };
1417
+ })());
1418
+ const mockChat = {
1419
+ addHistory: vi.fn(),
1420
+ getHistory: vi.fn().mockReturnValue([]),
1421
+ };
1422
+ client['chat'] = mockChat;
1423
+ const initialRequest = [{ text: 'Hi' }];
1424
+ const promptId = 'prompt-id-infinite-invalid-stream';
1425
+ const signal = new AbortController().signal;
1426
+ // Act
1427
+ const stream = client.sendMessageStream(initialRequest, signal, promptId);
1428
+ const events = await fromAsync(stream);
1429
+ // Assert
1430
+ // We expect 2 InvalidStream events (original + 1 retry)
1431
+ expect(events.length).toBe(2);
1432
+ expect(events.every((e) => e.type === GeminiEventType.InvalidStream)).toBe(true);
1433
+ // Verify that turn.run was called twice
1434
+ expect(mockTurnRunFn).toHaveBeenCalledTimes(2);
1435
+ });
1084
1436
  describe('Editor context delta', () => {
1085
1437
  const mockStream = (async function* () {
1086
1438
  yield { type: 'content', value: 'Hello' };
@@ -1649,7 +2001,7 @@ ${JSON.stringify({
1649
2001
  model: DEFAULT_GEMINI_FLASH_MODEL,
1650
2002
  config: {
1651
2003
  abortSignal,
1652
- systemInstruction: getCoreSystemPrompt(''),
2004
+ systemInstruction: getCoreSystemPrompt({}, ''),
1653
2005
  temperature: 0.5,
1654
2006
  topP: 1,
1655
2007
  },