@office-ai/aioncli-core 0.2.3 → 0.8.1

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 (756) hide show
  1. package/dist/index.d.ts +10 -3
  2. package/dist/index.js +10 -3
  3. package/dist/index.js.map +1 -1
  4. package/dist/src/agents/codebase-investigator.d.ts +11 -0
  5. package/dist/src/agents/codebase-investigator.js +73 -0
  6. package/dist/src/agents/codebase-investigator.js.map +1 -0
  7. package/dist/src/agents/executor.d.ts +88 -0
  8. package/dist/src/agents/executor.js +417 -0
  9. package/dist/src/agents/executor.js.map +1 -0
  10. package/dist/src/agents/executor.test.js +419 -0
  11. package/dist/src/agents/executor.test.js.map +1 -0
  12. package/dist/src/agents/invocation.d.ts +43 -0
  13. package/dist/src/agents/invocation.js +100 -0
  14. package/dist/src/agents/invocation.js.map +1 -0
  15. package/dist/src/agents/invocation.test.js +206 -0
  16. package/dist/src/agents/invocation.test.js.map +1 -0
  17. package/dist/src/agents/registry.d.ts +35 -0
  18. package/dist/src/agents/registry.js +58 -0
  19. package/dist/src/agents/registry.js.map +1 -0
  20. package/dist/src/agents/registry.test.js +146 -0
  21. package/dist/src/agents/registry.test.js.map +1 -0
  22. package/dist/src/agents/schema-utils.d.ts +39 -0
  23. package/dist/src/agents/schema-utils.js +57 -0
  24. package/dist/src/agents/schema-utils.js.map +1 -0
  25. package/dist/src/agents/schema-utils.test.d.ts +6 -0
  26. package/dist/src/agents/schema-utils.test.js +144 -0
  27. package/dist/src/agents/schema-utils.test.js.map +1 -0
  28. package/dist/src/agents/subagent-tool-wrapper.d.ts +36 -0
  29. package/dist/src/agents/subagent-tool-wrapper.js +47 -0
  30. package/dist/src/agents/subagent-tool-wrapper.js.map +1 -0
  31. package/dist/src/agents/subagent-tool-wrapper.test.d.ts +6 -0
  32. package/dist/src/agents/subagent-tool-wrapper.test.js +105 -0
  33. package/dist/src/agents/subagent-tool-wrapper.test.js.map +1 -0
  34. package/dist/src/agents/types.d.ts +116 -0
  35. package/dist/src/agents/types.js +17 -0
  36. package/dist/src/agents/types.js.map +1 -0
  37. package/dist/src/agents/utils.d.ts +15 -0
  38. package/dist/src/agents/utils.js +29 -0
  39. package/dist/src/agents/utils.js.map +1 -0
  40. package/dist/src/agents/utils.test.d.ts +6 -0
  41. package/dist/src/agents/utils.test.js +87 -0
  42. package/dist/src/agents/utils.test.js.map +1 -0
  43. package/dist/src/code_assist/codeAssist.d.ts +6 -3
  44. package/dist/src/code_assist/codeAssist.js +12 -0
  45. package/dist/src/code_assist/codeAssist.js.map +1 -1
  46. package/dist/src/code_assist/converter.d.ts +4 -1
  47. package/dist/src/code_assist/converter.js +38 -5
  48. package/dist/src/code_assist/converter.js.map +1 -1
  49. package/dist/src/code_assist/converter.test.js +93 -0
  50. package/dist/src/code_assist/converter.test.js.map +1 -1
  51. package/dist/src/code_assist/oauth-credential-storage.d.ts +25 -0
  52. package/dist/src/code_assist/oauth-credential-storage.js +109 -0
  53. package/dist/src/code_assist/oauth-credential-storage.js.map +1 -0
  54. package/dist/src/code_assist/oauth-credential-storage.test.d.ts +6 -0
  55. package/dist/src/code_assist/oauth-credential-storage.test.js +136 -0
  56. package/dist/src/code_assist/oauth-credential-storage.test.js.map +1 -0
  57. package/dist/src/code_assist/oauth2.d.ts +1 -1
  58. package/dist/src/code_assist/oauth2.js +107 -48
  59. package/dist/src/code_assist/oauth2.js.map +1 -1
  60. package/dist/src/code_assist/oauth2.test.js +735 -343
  61. package/dist/src/code_assist/oauth2.test.js.map +1 -1
  62. package/dist/src/code_assist/server.d.ts +4 -4
  63. package/dist/src/code_assist/server.js +25 -2
  64. package/dist/src/code_assist/server.js.map +1 -1
  65. package/dist/src/code_assist/server.test.js +25 -0
  66. package/dist/src/code_assist/server.test.js.map +1 -1
  67. package/dist/src/code_assist/setup.d.ts +1 -1
  68. package/dist/src/code_assist/setup.js +1 -1
  69. package/dist/src/code_assist/setup.js.map +1 -1
  70. package/dist/src/code_assist/setup.test.js.map +1 -1
  71. package/dist/src/code_assist/types.d.ts +17 -2
  72. package/dist/src/config/config.d.ts +121 -25
  73. package/dist/src/config/config.js +298 -89
  74. package/dist/src/config/config.js.map +1 -1
  75. package/dist/src/config/config.test.js +370 -131
  76. package/dist/src/config/config.test.js.map +1 -1
  77. package/dist/src/config/constants.d.ts +11 -0
  78. package/dist/src/config/constants.js +16 -0
  79. package/dist/src/config/constants.js.map +1 -0
  80. package/dist/src/config/models.d.ts +16 -0
  81. package/dist/src/config/models.js +29 -0
  82. package/dist/src/config/models.js.map +1 -1
  83. package/dist/src/config/models.test.d.ts +6 -0
  84. package/dist/src/config/models.test.js +55 -0
  85. package/dist/src/config/models.test.js.map +1 -0
  86. package/dist/src/config/storage.d.ts +34 -0
  87. package/dist/src/config/storage.js +95 -0
  88. package/dist/src/config/storage.js.map +1 -0
  89. package/dist/src/config/storage.test.d.ts +6 -0
  90. package/dist/src/config/storage.test.js +47 -0
  91. package/dist/src/config/storage.test.js.map +1 -0
  92. package/dist/src/confirmation-bus/index.d.ts +7 -0
  93. package/dist/src/confirmation-bus/index.js +8 -0
  94. package/dist/src/confirmation-bus/index.js.map +1 -0
  95. package/dist/src/confirmation-bus/message-bus.d.ts +17 -0
  96. package/dist/src/confirmation-bus/message-bus.js +81 -0
  97. package/dist/src/confirmation-bus/message-bus.js.map +1 -0
  98. package/dist/src/confirmation-bus/message-bus.test.d.ts +6 -0
  99. package/dist/src/confirmation-bus/message-bus.test.js +164 -0
  100. package/dist/src/confirmation-bus/message-bus.test.js.map +1 -0
  101. package/dist/src/confirmation-bus/types.d.ts +38 -0
  102. package/dist/src/confirmation-bus/types.js +15 -0
  103. package/dist/src/confirmation-bus/types.js.map +1 -0
  104. package/dist/src/core/baseLlmClient.d.ts +54 -0
  105. package/dist/src/core/baseLlmClient.js +190 -0
  106. package/dist/src/core/baseLlmClient.js.map +1 -0
  107. package/dist/src/core/baseLlmClient.test.d.ts +6 -0
  108. package/dist/src/core/baseLlmClient.test.js +316 -0
  109. package/dist/src/core/baseLlmClient.test.js.map +1 -0
  110. package/dist/src/core/client.d.ts +28 -33
  111. package/dist/src/core/client.js +187 -384
  112. package/dist/src/core/client.js.map +1 -1
  113. package/dist/src/core/client.test.js +745 -500
  114. package/dist/src/core/client.test.js.map +1 -1
  115. package/dist/src/core/contentGenerator.d.ts +4 -4
  116. package/dist/src/core/contentGenerator.js +6 -7
  117. package/dist/src/core/contentGenerator.js.map +1 -1
  118. package/dist/src/core/contentGenerator.test.js +1 -3
  119. package/dist/src/core/contentGenerator.test.js.map +1 -1
  120. package/dist/src/core/coreToolScheduler.d.ts +20 -7
  121. package/dist/src/core/coreToolScheduler.js +216 -53
  122. package/dist/src/core/coreToolScheduler.js.map +1 -1
  123. package/dist/src/core/coreToolScheduler.test.js +564 -88
  124. package/dist/src/core/coreToolScheduler.test.js.map +1 -1
  125. package/dist/src/core/geminiChat.d.ts +54 -53
  126. package/dist/src/core/geminiChat.js +300 -356
  127. package/dist/src/core/geminiChat.js.map +1 -1
  128. package/dist/src/core/geminiChat.test.js +1255 -321
  129. package/dist/src/core/geminiChat.test.js.map +1 -1
  130. package/dist/src/core/geminiRequest.js +1 -0
  131. package/dist/src/core/geminiRequest.js.map +1 -1
  132. package/dist/src/core/logger.d.ts +4 -2
  133. package/dist/src/core/logger.js +4 -3
  134. package/dist/src/core/logger.js.map +1 -1
  135. package/dist/src/core/logger.test.js +17 -16
  136. package/dist/src/core/logger.test.js.map +1 -1
  137. package/dist/src/core/loggingContentGenerator.d.ts +3 -3
  138. package/dist/src/core/loggingContentGenerator.js +15 -16
  139. package/dist/src/core/loggingContentGenerator.js.map +1 -1
  140. package/dist/src/core/nonInteractiveToolExecutor.d.ts +3 -5
  141. package/dist/src/core/nonInteractiveToolExecutor.js +14 -122
  142. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  143. package/dist/src/core/nonInteractiveToolExecutor.test.js +158 -78
  144. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
  145. package/dist/src/core/openaiContentGenerator.d.ts +4 -3
  146. package/dist/src/core/openaiContentGenerator.js +21 -14
  147. package/dist/src/core/openaiContentGenerator.js.map +1 -1
  148. package/dist/src/core/openaiContentGenerator.test.js +1 -0
  149. package/dist/src/core/openaiContentGenerator.test.js.map +1 -1
  150. package/dist/src/core/prompts.d.ts +5 -0
  151. package/dist/src/core/prompts.js +66 -44
  152. package/dist/src/core/prompts.js.map +1 -1
  153. package/dist/src/core/prompts.test.js +130 -1
  154. package/dist/src/core/prompts.test.js.map +1 -1
  155. package/dist/src/core/subagent.d.ts +24 -18
  156. package/dist/src/core/subagent.js +125 -90
  157. package/dist/src/core/subagent.js.map +1 -1
  158. package/dist/src/core/subagent.test.js +59 -44
  159. package/dist/src/core/subagent.test.js.map +1 -1
  160. package/dist/src/core/turn.d.ts +37 -13
  161. package/dist/src/core/turn.js +63 -28
  162. package/dist/src/core/turn.js.map +1 -1
  163. package/dist/src/core/turn.test.js +359 -100
  164. package/dist/src/core/turn.test.js.map +1 -1
  165. package/dist/src/fallback/handler.d.ts +7 -0
  166. package/dist/src/fallback/handler.js +129 -0
  167. package/dist/src/fallback/handler.js.map +1 -0
  168. package/dist/src/fallback/handler.test.d.ts +6 -0
  169. package/dist/src/fallback/handler.test.js +130 -0
  170. package/dist/src/fallback/handler.test.js.map +1 -0
  171. package/dist/src/fallback/types.d.ts +14 -0
  172. package/dist/src/fallback/types.js +7 -0
  173. package/dist/src/fallback/types.js.map +1 -0
  174. package/dist/src/ide/constants.d.ts +3 -0
  175. package/dist/src/ide/constants.js +3 -0
  176. package/dist/src/ide/constants.js.map +1 -1
  177. package/dist/src/ide/detect-ide.d.ts +48 -12
  178. package/dist/src/ide/detect-ide.js +47 -66
  179. package/dist/src/ide/detect-ide.js.map +1 -1
  180. package/dist/src/ide/detect-ide.test.js +79 -52
  181. package/dist/src/ide/detect-ide.test.js.map +1 -1
  182. package/dist/src/ide/ide-client.d.ts +69 -23
  183. package/dist/src/ide/ide-client.js +372 -78
  184. package/dist/src/ide/ide-client.js.map +1 -1
  185. package/dist/src/ide/ide-client.test.js +375 -30
  186. package/dist/src/ide/ide-client.test.js.map +1 -1
  187. package/dist/src/ide/ide-installer.d.ts +2 -2
  188. package/dist/src/ide/ide-installer.js +37 -24
  189. package/dist/src/ide/ide-installer.js.map +1 -1
  190. package/dist/src/ide/ide-installer.test.js +104 -26
  191. package/dist/src/ide/ide-installer.test.js.map +1 -1
  192. package/dist/src/ide/ideContext.d.ts +35 -365
  193. package/dist/src/ide/ideContext.js +60 -106
  194. package/dist/src/ide/ideContext.js.map +1 -1
  195. package/dist/src/ide/ideContext.test.js +152 -24
  196. package/dist/src/ide/ideContext.test.js.map +1 -1
  197. package/dist/src/ide/process-utils.d.ts +7 -5
  198. package/dist/src/ide/process-utils.js +81 -50
  199. package/dist/src/ide/process-utils.js.map +1 -1
  200. package/dist/src/ide/process-utils.test.d.ts +6 -0
  201. package/dist/src/ide/process-utils.test.js +158 -0
  202. package/dist/src/ide/process-utils.test.js.map +1 -0
  203. package/dist/src/ide/types.d.ts +486 -0
  204. package/dist/src/ide/types.js +138 -0
  205. package/dist/src/ide/types.js.map +1 -0
  206. package/dist/src/index.d.ts +20 -2
  207. package/dist/src/index.js +20 -2
  208. package/dist/src/index.js.map +1 -1
  209. package/dist/src/mcp/google-auth-provider.d.ts +3 -3
  210. package/dist/src/mcp/google-auth-provider.test.js.map +1 -1
  211. package/dist/src/mcp/oauth-provider.d.ts +17 -13
  212. package/dist/src/mcp/oauth-provider.js +81 -69
  213. package/dist/src/mcp/oauth-provider.js.map +1 -1
  214. package/dist/src/mcp/oauth-provider.test.js +212 -37
  215. package/dist/src/mcp/oauth-provider.test.js.map +1 -1
  216. package/dist/src/mcp/oauth-token-storage.d.ts +14 -32
  217. package/dist/src/mcp/oauth-token-storage.js +54 -25
  218. package/dist/src/mcp/oauth-token-storage.js.map +1 -1
  219. package/dist/src/mcp/oauth-token-storage.test.js +256 -162
  220. package/dist/src/mcp/oauth-token-storage.test.js.map +1 -1
  221. package/dist/src/mcp/oauth-utils.d.ts +9 -1
  222. package/dist/src/mcp/oauth-utils.js +42 -27
  223. package/dist/src/mcp/oauth-utils.js.map +1 -1
  224. package/dist/src/mcp/oauth-utils.test.js +41 -1
  225. package/dist/src/mcp/oauth-utils.test.js.map +1 -1
  226. package/dist/src/mcp/sa-impersonation-provider.d.ts +33 -0
  227. package/dist/src/mcp/sa-impersonation-provider.js +130 -0
  228. package/dist/src/mcp/sa-impersonation-provider.js.map +1 -0
  229. package/dist/src/mcp/sa-impersonation-provider.test.d.ts +6 -0
  230. package/dist/src/mcp/sa-impersonation-provider.test.js +117 -0
  231. package/dist/src/mcp/sa-impersonation-provider.test.js.map +1 -0
  232. package/dist/src/mcp/token-storage/base-token-storage.d.ts +19 -0
  233. package/dist/src/mcp/token-storage/base-token-storage.js +36 -0
  234. package/dist/src/mcp/token-storage/base-token-storage.js.map +1 -0
  235. package/dist/src/mcp/token-storage/base-token-storage.test.d.ts +6 -0
  236. package/dist/src/mcp/token-storage/base-token-storage.test.js +160 -0
  237. package/dist/src/mcp/token-storage/base-token-storage.test.js.map +1 -0
  238. package/dist/src/mcp/token-storage/file-token-storage.d.ts +24 -0
  239. package/dist/src/mcp/token-storage/file-token-storage.js +144 -0
  240. package/dist/src/mcp/token-storage/file-token-storage.js.map +1 -0
  241. package/dist/src/mcp/token-storage/file-token-storage.test.d.ts +6 -0
  242. package/dist/src/mcp/token-storage/file-token-storage.test.js +235 -0
  243. package/dist/src/mcp/token-storage/file-token-storage.test.js.map +1 -0
  244. package/dist/src/mcp/token-storage/hybrid-token-storage.d.ts +23 -0
  245. package/dist/src/mcp/token-storage/hybrid-token-storage.js +78 -0
  246. package/dist/src/mcp/token-storage/hybrid-token-storage.js.map +1 -0
  247. package/dist/src/mcp/token-storage/hybrid-token-storage.test.d.ts +6 -0
  248. package/dist/src/mcp/token-storage/hybrid-token-storage.test.js +193 -0
  249. package/dist/src/mcp/token-storage/hybrid-token-storage.test.js.map +1 -0
  250. package/dist/src/mcp/token-storage/index.d.ts +11 -0
  251. package/dist/src/mcp/token-storage/index.js +12 -0
  252. package/dist/src/mcp/token-storage/index.js.map +1 -0
  253. package/dist/src/mcp/token-storage/keychain-token-storage.d.ts +31 -0
  254. package/dist/src/mcp/token-storage/keychain-token-storage.js +190 -0
  255. package/dist/src/mcp/token-storage/keychain-token-storage.js.map +1 -0
  256. package/dist/src/mcp/token-storage/keychain-token-storage.test.d.ts +6 -0
  257. package/dist/src/mcp/token-storage/keychain-token-storage.test.js +254 -0
  258. package/dist/src/mcp/token-storage/keychain-token-storage.test.js.map +1 -0
  259. package/dist/src/mcp/token-storage/types.d.ts +38 -0
  260. package/dist/src/mcp/token-storage/types.js +11 -0
  261. package/dist/src/mcp/token-storage/types.js.map +1 -0
  262. package/dist/src/output/json-formatter.d.ts +11 -0
  263. package/dist/src/output/json-formatter.js +30 -0
  264. package/dist/src/output/json-formatter.js.map +1 -0
  265. package/dist/src/output/json-formatter.test.d.ts +6 -0
  266. package/dist/src/output/json-formatter.test.js +266 -0
  267. package/dist/src/output/json-formatter.test.js.map +1 -0
  268. package/dist/src/output/types.d.ts +20 -0
  269. package/dist/src/output/types.js +11 -0
  270. package/dist/src/output/types.js.map +1 -0
  271. package/dist/src/policy/index.d.ts +7 -0
  272. package/dist/src/policy/index.js +8 -0
  273. package/dist/src/policy/index.js.map +1 -0
  274. package/dist/src/policy/policy-engine.d.ts +30 -0
  275. package/dist/src/policy/policy-engine.js +92 -0
  276. package/dist/src/policy/policy-engine.js.map +1 -0
  277. package/dist/src/policy/policy-engine.test.d.ts +6 -0
  278. package/dist/src/policy/policy-engine.test.js +515 -0
  279. package/dist/src/policy/policy-engine.test.js.map +1 -0
  280. package/dist/src/policy/stable-stringify.d.ts +58 -0
  281. package/dist/src/policy/stable-stringify.js +122 -0
  282. package/dist/src/policy/stable-stringify.js.map +1 -0
  283. package/dist/src/policy/types.d.ts +47 -0
  284. package/dist/src/policy/types.js +12 -0
  285. package/dist/src/policy/types.js.map +1 -0
  286. package/dist/src/prompts/mcp-prompts.d.ts +2 -2
  287. package/dist/src/prompts/prompt-registry.d.ts +1 -1
  288. package/dist/src/routing/modelRouterService.d.ts +23 -0
  289. package/dist/src/routing/modelRouterService.js +70 -0
  290. package/dist/src/routing/modelRouterService.js.map +1 -0
  291. package/dist/src/routing/modelRouterService.test.d.ts +6 -0
  292. package/dist/src/routing/modelRouterService.test.js +98 -0
  293. package/dist/src/routing/modelRouterService.test.js.map +1 -0
  294. package/dist/src/routing/routingStrategy.d.ts +62 -0
  295. package/dist/src/routing/routingStrategy.js +7 -0
  296. package/dist/src/routing/routingStrategy.js.map +1 -0
  297. package/dist/src/routing/strategies/classifierStrategy.d.ts +12 -0
  298. package/dist/src/routing/strategies/classifierStrategy.js +173 -0
  299. package/dist/src/routing/strategies/classifierStrategy.js.map +1 -0
  300. package/dist/src/routing/strategies/classifierStrategy.test.d.ts +6 -0
  301. package/dist/src/routing/strategies/classifierStrategy.test.js +192 -0
  302. package/dist/src/routing/strategies/classifierStrategy.test.js.map +1 -0
  303. package/dist/src/routing/strategies/compositeStrategy.d.ts +26 -0
  304. package/dist/src/routing/strategies/compositeStrategy.js +68 -0
  305. package/dist/src/routing/strategies/compositeStrategy.js.map +1 -0
  306. package/dist/src/routing/strategies/compositeStrategy.test.d.ts +6 -0
  307. package/dist/src/routing/strategies/compositeStrategy.test.js +123 -0
  308. package/dist/src/routing/strategies/compositeStrategy.test.js.map +1 -0
  309. package/dist/src/routing/strategies/defaultStrategy.d.ts +12 -0
  310. package/dist/src/routing/strategies/defaultStrategy.js +20 -0
  311. package/dist/src/routing/strategies/defaultStrategy.js.map +1 -0
  312. package/dist/src/routing/strategies/defaultStrategy.test.d.ts +6 -0
  313. package/dist/src/routing/strategies/defaultStrategy.test.js +26 -0
  314. package/dist/src/routing/strategies/defaultStrategy.test.js.map +1 -0
  315. package/dist/src/routing/strategies/fallbackStrategy.d.ts +12 -0
  316. package/dist/src/routing/strategies/fallbackStrategy.js +25 -0
  317. package/dist/src/routing/strategies/fallbackStrategy.js.map +1 -0
  318. package/dist/src/routing/strategies/fallbackStrategy.test.d.ts +6 -0
  319. package/dist/src/routing/strategies/fallbackStrategy.test.js +55 -0
  320. package/dist/src/routing/strategies/fallbackStrategy.test.js.map +1 -0
  321. package/dist/src/routing/strategies/overrideStrategy.d.ts +15 -0
  322. package/dist/src/routing/strategies/overrideStrategy.js +28 -0
  323. package/dist/src/routing/strategies/overrideStrategy.js.map +1 -0
  324. package/dist/src/routing/strategies/overrideStrategy.test.d.ts +6 -0
  325. package/dist/src/routing/strategies/overrideStrategy.test.js +42 -0
  326. package/dist/src/routing/strategies/overrideStrategy.test.js.map +1 -0
  327. package/dist/src/services/chatRecordingService.d.ts +8 -14
  328. package/dist/src/services/chatRecordingService.js +33 -21
  329. package/dist/src/services/chatRecordingService.js.map +1 -1
  330. package/dist/src/services/chatRecordingService.test.js +69 -25
  331. package/dist/src/services/chatRecordingService.test.js.map +1 -1
  332. package/dist/src/services/fileDiscoveryService.d.ts +10 -0
  333. package/dist/src/services/fileDiscoveryService.js +32 -18
  334. package/dist/src/services/fileDiscoveryService.js.map +1 -1
  335. package/dist/src/services/fileDiscoveryService.test.js +3 -3
  336. package/dist/src/services/fileDiscoveryService.test.js.map +1 -1
  337. package/dist/src/services/fileSystemService.d.ts +9 -0
  338. package/dist/src/services/fileSystemService.js +12 -1
  339. package/dist/src/services/fileSystemService.js.map +1 -1
  340. package/dist/src/services/fileSystemService.test.js +1 -1
  341. package/dist/src/services/fileSystemService.test.js.map +1 -1
  342. package/dist/src/services/gitService.d.ts +3 -1
  343. package/dist/src/services/gitService.js +30 -24
  344. package/dist/src/services/gitService.js.map +1 -1
  345. package/dist/src/services/gitService.test.js +30 -37
  346. package/dist/src/services/gitService.test.js.map +1 -1
  347. package/dist/src/services/loopDetectionService.d.ts +8 -2
  348. package/dist/src/services/loopDetectionService.js +64 -24
  349. package/dist/src/services/loopDetectionService.js.map +1 -1
  350. package/dist/src/services/loopDetectionService.test.js +64 -13
  351. package/dist/src/services/loopDetectionService.test.js.map +1 -1
  352. package/dist/src/services/shellExecutionService.d.ts +36 -2
  353. package/dist/src/services/shellExecutionService.js +238 -47
  354. package/dist/src/services/shellExecutionService.js.map +1 -1
  355. package/dist/src/services/shellExecutionService.test.js +197 -58
  356. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  357. package/dist/src/telemetry/activity-detector.d.ts +41 -0
  358. package/dist/src/telemetry/activity-detector.js +61 -0
  359. package/dist/src/telemetry/activity-detector.js.map +1 -0
  360. package/dist/src/telemetry/activity-detector.test.d.ts +6 -0
  361. package/dist/src/telemetry/activity-detector.test.js +136 -0
  362. package/dist/src/telemetry/activity-detector.test.js.map +1 -0
  363. package/dist/src/telemetry/activity-types.d.ts +19 -0
  364. package/dist/src/telemetry/activity-types.js +21 -0
  365. package/dist/src/telemetry/activity-types.js.map +1 -0
  366. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +34 -4
  367. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +322 -15
  368. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  369. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.d.ts +1 -0
  370. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +321 -11
  371. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  372. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +51 -2
  373. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +124 -2
  374. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  375. package/dist/src/telemetry/config.d.ts +31 -0
  376. package/dist/src/telemetry/config.js +76 -0
  377. package/dist/src/telemetry/config.js.map +1 -0
  378. package/dist/src/telemetry/config.test.d.ts +6 -0
  379. package/dist/src/telemetry/config.test.js +124 -0
  380. package/dist/src/telemetry/config.test.js.map +1 -0
  381. package/dist/src/telemetry/constants.d.ts +17 -7
  382. package/dist/src/telemetry/constants.js +18 -7
  383. package/dist/src/telemetry/constants.js.map +1 -1
  384. package/dist/src/telemetry/file-exporters.d.ts +5 -4
  385. package/dist/src/telemetry/file-exporters.js +1 -1
  386. package/dist/src/telemetry/file-exporters.js.map +1 -1
  387. package/dist/src/telemetry/gcp-exporters.d.ts +34 -0
  388. package/dist/src/telemetry/gcp-exporters.js +117 -0
  389. package/dist/src/telemetry/gcp-exporters.js.map +1 -0
  390. package/dist/src/telemetry/gcp-exporters.test.d.ts +6 -0
  391. package/dist/src/telemetry/gcp-exporters.test.js +318 -0
  392. package/dist/src/telemetry/gcp-exporters.test.js.map +1 -0
  393. package/dist/src/telemetry/high-water-mark-tracker.d.ts +43 -0
  394. package/dist/src/telemetry/high-water-mark-tracker.js +88 -0
  395. package/dist/src/telemetry/high-water-mark-tracker.js.map +1 -0
  396. package/dist/src/telemetry/high-water-mark-tracker.test.d.ts +6 -0
  397. package/dist/src/telemetry/high-water-mark-tracker.test.js +152 -0
  398. package/dist/src/telemetry/high-water-mark-tracker.test.js.map +1 -0
  399. package/dist/src/telemetry/index.d.ts +12 -2
  400. package/dist/src/telemetry/index.js +16 -2
  401. package/dist/src/telemetry/index.js.map +1 -1
  402. package/dist/src/telemetry/loggers.d.ts +17 -2
  403. package/dist/src/telemetry/loggers.js +316 -14
  404. package/dist/src/telemetry/loggers.js.map +1 -1
  405. package/dist/src/telemetry/loggers.test.circular.js +3 -3
  406. package/dist/src/telemetry/loggers.test.circular.js.map +1 -1
  407. package/dist/src/telemetry/loggers.test.js +452 -48
  408. package/dist/src/telemetry/loggers.test.js.map +1 -1
  409. package/dist/src/telemetry/metrics.d.ts +323 -12
  410. package/dist/src/telemetry/metrics.js +464 -83
  411. package/dist/src/telemetry/metrics.js.map +1 -1
  412. package/dist/src/telemetry/metrics.test.js +583 -38
  413. package/dist/src/telemetry/metrics.test.js.map +1 -1
  414. package/dist/src/telemetry/rate-limiter.d.ts +48 -0
  415. package/dist/src/telemetry/rate-limiter.js +100 -0
  416. package/dist/src/telemetry/rate-limiter.js.map +1 -0
  417. package/dist/src/telemetry/rate-limiter.test.d.ts +6 -0
  418. package/dist/src/telemetry/rate-limiter.test.js +207 -0
  419. package/dist/src/telemetry/rate-limiter.test.js.map +1 -0
  420. package/dist/src/telemetry/sdk.d.ts +1 -1
  421. package/dist/src/telemetry/sdk.js +20 -2
  422. package/dist/src/telemetry/sdk.js.map +1 -1
  423. package/dist/src/telemetry/sdk.test.js +108 -0
  424. package/dist/src/telemetry/sdk.test.js.map +1 -1
  425. package/dist/src/telemetry/telemetry-utils.d.ts +6 -0
  426. package/dist/src/telemetry/telemetry-utils.js +14 -0
  427. package/dist/src/telemetry/telemetry-utils.js.map +1 -0
  428. package/dist/src/telemetry/telemetry-utils.test.d.ts +6 -0
  429. package/dist/src/telemetry/telemetry-utils.test.js +40 -0
  430. package/dist/src/telemetry/telemetry-utils.test.js.map +1 -0
  431. package/dist/src/telemetry/types.d.ts +136 -8
  432. package/dist/src/telemetry/types.js +233 -11
  433. package/dist/src/telemetry/types.js.map +1 -1
  434. package/dist/src/telemetry/uiTelemetry.d.ts +3 -3
  435. package/dist/src/telemetry/uiTelemetry.js +7 -8
  436. package/dist/src/telemetry/uiTelemetry.js.map +1 -1
  437. package/dist/src/telemetry/uiTelemetry.test.js +33 -29
  438. package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
  439. package/dist/src/test-utils/config.d.ts +2 -1
  440. package/dist/src/test-utils/config.js.map +1 -1
  441. package/dist/src/test-utils/index.d.ts +6 -0
  442. package/dist/src/test-utils/index.js +7 -0
  443. package/dist/src/test-utils/index.js.map +1 -0
  444. package/dist/src/test-utils/mock-tool.d.ts +66 -0
  445. package/dist/src/test-utils/{tools.js → mock-tool.js} +45 -29
  446. package/dist/src/test-utils/mock-tool.js.map +1 -0
  447. package/dist/src/test-utils/mockWorkspaceContext.d.ts +1 -1
  448. package/dist/src/tools/diffOptions.d.ts +1 -1
  449. package/dist/src/tools/diffOptions.js +21 -13
  450. package/dist/src/tools/diffOptions.js.map +1 -1
  451. package/dist/src/tools/diffOptions.test.js +58 -22
  452. package/dist/src/tools/diffOptions.test.js.map +1 -1
  453. package/dist/src/tools/edit.d.ts +6 -5
  454. package/dist/src/tools/edit.js +58 -40
  455. package/dist/src/tools/edit.js.map +1 -1
  456. package/dist/src/tools/edit.test.js +192 -16
  457. package/dist/src/tools/edit.test.js.map +1 -1
  458. package/dist/src/tools/glob.d.ts +7 -2
  459. package/dist/src/tools/glob.js +42 -23
  460. package/dist/src/tools/glob.js.map +1 -1
  461. package/dist/src/tools/glob.test.js +80 -4
  462. package/dist/src/tools/glob.test.js.map +1 -1
  463. package/dist/src/tools/grep.d.ts +3 -2
  464. package/dist/src/tools/grep.js +35 -15
  465. package/dist/src/tools/grep.js.map +1 -1
  466. package/dist/src/tools/grep.test.js +26 -3
  467. package/dist/src/tools/grep.test.js.map +1 -1
  468. package/dist/src/tools/ls.d.ts +3 -2
  469. package/dist/src/tools/ls.js +31 -39
  470. package/dist/src/tools/ls.js.map +1 -1
  471. package/dist/src/tools/ls.test.js +145 -280
  472. package/dist/src/tools/ls.test.js.map +1 -1
  473. package/dist/src/tools/mcp-client-manager.d.ts +8 -6
  474. package/dist/src/tools/mcp-client-manager.js +13 -4
  475. package/dist/src/tools/mcp-client-manager.js.map +1 -1
  476. package/dist/src/tools/mcp-client-manager.test.js +20 -1
  477. package/dist/src/tools/mcp-client-manager.test.js.map +1 -1
  478. package/dist/src/tools/mcp-client.d.ts +18 -21
  479. package/dist/src/tools/mcp-client.js +87 -120
  480. package/dist/src/tools/mcp-client.js.map +1 -1
  481. package/dist/src/tools/mcp-client.test.js +32 -152
  482. package/dist/src/tools/mcp-client.test.js.map +1 -1
  483. package/dist/src/tools/mcp-tool.d.ts +6 -4
  484. package/dist/src/tools/mcp-tool.js +51 -13
  485. package/dist/src/tools/mcp-tool.js.map +1 -1
  486. package/dist/src/tools/mcp-tool.test.js +166 -12
  487. package/dist/src/tools/mcp-tool.test.js.map +1 -1
  488. package/dist/src/tools/memoryTool.d.ts +3 -2
  489. package/dist/src/tools/memoryTool.js +14 -37
  490. package/dist/src/tools/memoryTool.js.map +1 -1
  491. package/dist/src/tools/memoryTool.test.js +16 -4
  492. package/dist/src/tools/memoryTool.test.js.map +1 -1
  493. package/dist/src/tools/message-bus-integration.test.d.ts +6 -0
  494. package/dist/src/tools/message-bus-integration.test.js +183 -0
  495. package/dist/src/tools/message-bus-integration.test.js.map +1 -0
  496. package/dist/src/tools/modifiable-tool.d.ts +2 -2
  497. package/dist/src/tools/modifiable-tool.js +3 -3
  498. package/dist/src/tools/modifiable-tool.js.map +1 -1
  499. package/dist/src/tools/modifiable-tool.test.js +4 -4
  500. package/dist/src/tools/modifiable-tool.test.js.map +1 -1
  501. package/dist/src/tools/read-file.d.ts +3 -2
  502. package/dist/src/tools/read-file.js +23 -38
  503. package/dist/src/tools/read-file.js.map +1 -1
  504. package/dist/src/tools/read-file.test.js +38 -6
  505. package/dist/src/tools/read-file.test.js.map +1 -1
  506. package/dist/src/tools/read-many-files.d.ts +3 -2
  507. package/dist/src/tools/read-many-files.js +52 -107
  508. package/dist/src/tools/read-many-files.js.map +1 -1
  509. package/dist/src/tools/read-many-files.test.js +64 -11
  510. package/dist/src/tools/read-many-files.test.js.map +1 -1
  511. package/dist/src/tools/ripGrep.d.ts +55 -0
  512. package/dist/src/tools/ripGrep.js +393 -0
  513. package/dist/src/tools/ripGrep.js.map +1 -0
  514. package/dist/src/tools/ripGrep.test.d.ts +6 -0
  515. package/dist/src/tools/ripGrep.test.js +976 -0
  516. package/dist/src/tools/ripGrep.test.js.map +1 -0
  517. package/dist/src/tools/shell.d.ts +13 -2
  518. package/dist/src/tools/shell.js +42 -32
  519. package/dist/src/tools/shell.js.map +1 -1
  520. package/dist/src/tools/shell.test.js +57 -75
  521. package/dist/src/tools/shell.test.js.map +1 -1
  522. package/dist/src/tools/smart-edit.d.ts +91 -0
  523. package/dist/src/tools/smart-edit.js +702 -0
  524. package/dist/src/tools/smart-edit.js.map +1 -0
  525. package/dist/src/tools/smart-edit.test.d.ts +6 -0
  526. package/dist/src/tools/smart-edit.test.js +542 -0
  527. package/dist/src/tools/smart-edit.test.js.map +1 -0
  528. package/dist/src/tools/tool-error.d.ts +18 -1
  529. package/dist/src/tools/tool-error.js +27 -0
  530. package/dist/src/tools/tool-error.js.map +1 -1
  531. package/dist/src/tools/tool-registry.d.ts +10 -4
  532. package/dist/src/tools/tool-registry.js +20 -7
  533. package/dist/src/tools/tool-registry.js.map +1 -1
  534. package/dist/src/tools/tool-registry.test.js +93 -10
  535. package/dist/src/tools/tool-registry.test.js.map +1 -1
  536. package/dist/src/tools/tools.d.ts +33 -16
  537. package/dist/src/tools/tools.js +115 -5
  538. package/dist/src/tools/tools.js.map +1 -1
  539. package/dist/src/tools/tools.test.js +1 -2
  540. package/dist/src/tools/tools.test.js.map +1 -1
  541. package/dist/src/tools/web-fetch.d.ts +3 -2
  542. package/dist/src/tools/web-fetch.js +14 -10
  543. package/dist/src/tools/web-fetch.js.map +1 -1
  544. package/dist/src/tools/web-fetch.test.js +55 -16
  545. package/dist/src/tools/web-fetch.test.js.map +1 -1
  546. package/dist/src/tools/web-search.d.ts +4 -3
  547. package/dist/src/tools/web-search.js +31 -8
  548. package/dist/src/tools/web-search.js.map +1 -1
  549. package/dist/src/tools/web-search.test.js +69 -1
  550. package/dist/src/tools/web-search.test.js.map +1 -1
  551. package/dist/src/tools/write-file.d.ts +4 -3
  552. package/dist/src/tools/write-file.js +17 -18
  553. package/dist/src/tools/write-file.js.map +1 -1
  554. package/dist/src/tools/write-file.test.js +108 -24
  555. package/dist/src/tools/write-file.test.js.map +1 -1
  556. package/dist/src/tools/write-todos.d.ts +25 -0
  557. package/dist/src/tools/write-todos.js +150 -0
  558. package/dist/src/tools/write-todos.js.map +1 -0
  559. package/dist/src/tools/write-todos.test.d.ts +6 -0
  560. package/dist/src/tools/write-todos.test.js +89 -0
  561. package/dist/src/tools/write-todos.test.js.map +1 -0
  562. package/dist/src/utils/bfsFileSearch.d.ts +2 -2
  563. package/dist/src/utils/bfsFileSearch.js +13 -7
  564. package/dist/src/utils/bfsFileSearch.js.map +1 -1
  565. package/dist/src/utils/bfsFileSearch.test.js +3 -3
  566. package/dist/src/utils/bfsFileSearch.test.js.map +1 -1
  567. package/dist/src/utils/editCorrector.d.ts +9 -8
  568. package/dist/src/utils/editCorrector.js +62 -19
  569. package/dist/src/utils/editCorrector.js.map +1 -1
  570. package/dist/src/utils/editCorrector.test.js +33 -82
  571. package/dist/src/utils/editCorrector.test.js.map +1 -1
  572. package/dist/src/utils/editor.js +32 -45
  573. package/dist/src/utils/editor.js.map +1 -1
  574. package/dist/src/utils/editor.test.js +62 -76
  575. package/dist/src/utils/editor.test.js.map +1 -1
  576. package/dist/src/utils/environmentContext.d.ts +2 -2
  577. package/dist/src/utils/errorParsing.js +2 -2
  578. package/dist/src/utils/errorParsing.js.map +1 -1
  579. package/dist/src/utils/errorParsing.test.js +7 -7
  580. package/dist/src/utils/errorParsing.test.js.map +1 -1
  581. package/dist/src/utils/errorReporting.d.ts +1 -1
  582. package/dist/src/utils/errors.d.ts +25 -0
  583. package/dist/src/utils/errors.js +42 -0
  584. package/dist/src/utils/errors.js.map +1 -1
  585. package/dist/src/utils/fetch.js +1 -1
  586. package/dist/src/utils/fetch.js.map +1 -1
  587. package/dist/src/utils/fileUtils.d.ts +24 -12
  588. package/dist/src/utils/fileUtils.js +170 -79
  589. package/dist/src/utils/fileUtils.js.map +1 -1
  590. package/dist/src/utils/fileUtils.test.js +347 -29
  591. package/dist/src/utils/fileUtils.test.js.map +1 -1
  592. package/dist/src/utils/filesearch/crawler.d.ts +1 -1
  593. package/dist/src/utils/filesearch/crawler.test.js +2 -2
  594. package/dist/src/utils/filesearch/crawler.test.js.map +1 -1
  595. package/dist/src/utils/filesearch/fileSearch.d.ts +1 -0
  596. package/dist/src/utils/filesearch/fileSearch.js +14 -9
  597. package/dist/src/utils/filesearch/fileSearch.js.map +1 -1
  598. package/dist/src/utils/filesearch/fileSearch.test.js +90 -0
  599. package/dist/src/utils/filesearch/fileSearch.test.js.map +1 -1
  600. package/dist/src/utils/flashFallback.test.d.ts +6 -0
  601. package/dist/src/utils/{flashFallback.integration.test.js → flashFallback.test.js} +33 -29
  602. package/dist/src/utils/flashFallback.test.js.map +1 -0
  603. package/dist/src/utils/geminiIgnoreParser.d.ts +18 -0
  604. package/dist/src/utils/geminiIgnoreParser.js +61 -0
  605. package/dist/src/utils/geminiIgnoreParser.js.map +1 -0
  606. package/dist/src/utils/geminiIgnoreParser.test.d.ts +6 -0
  607. package/dist/src/utils/geminiIgnoreParser.test.js +50 -0
  608. package/dist/src/utils/geminiIgnoreParser.test.js.map +1 -0
  609. package/dist/src/utils/generateContentResponseUtilities.d.ts +1 -2
  610. package/dist/src/utils/generateContentResponseUtilities.js +1 -13
  611. package/dist/src/utils/generateContentResponseUtilities.js.map +1 -1
  612. package/dist/src/utils/generateContentResponseUtilities.test.js +2 -40
  613. package/dist/src/utils/generateContentResponseUtilities.test.js.map +1 -1
  614. package/dist/src/utils/getFolderStructure.d.ts +2 -2
  615. package/dist/src/utils/getFolderStructure.js +3 -3
  616. package/dist/src/utils/getFolderStructure.js.map +1 -1
  617. package/dist/src/utils/getFolderStructure.test.js +4 -4
  618. package/dist/src/utils/getFolderStructure.test.js.map +1 -1
  619. package/dist/src/utils/gitIgnoreParser.d.ts +3 -7
  620. package/dist/src/utils/gitIgnoreParser.js +126 -35
  621. package/dist/src/utils/gitIgnoreParser.js.map +1 -1
  622. package/dist/src/utils/gitIgnoreParser.test.js +69 -38
  623. package/dist/src/utils/gitIgnoreParser.test.js.map +1 -1
  624. package/dist/src/utils/gitUtils.js +2 -2
  625. package/dist/src/utils/gitUtils.js.map +1 -1
  626. package/dist/src/utils/ignorePatterns.d.ts +103 -0
  627. package/dist/src/utils/ignorePatterns.js +220 -0
  628. package/dist/src/utils/ignorePatterns.js.map +1 -0
  629. package/dist/src/utils/ignorePatterns.test.d.ts +6 -0
  630. package/dist/src/utils/ignorePatterns.test.js +250 -0
  631. package/dist/src/utils/ignorePatterns.test.js.map +1 -0
  632. package/dist/src/utils/installationManager.d.ts +16 -0
  633. package/dist/src/utils/installationManager.js +50 -0
  634. package/dist/src/utils/installationManager.js.map +1 -0
  635. package/dist/src/utils/installationManager.test.d.ts +6 -0
  636. package/dist/src/utils/installationManager.test.js +83 -0
  637. package/dist/src/utils/installationManager.test.js.map +1 -0
  638. package/dist/src/utils/language-detection.d.ts +6 -0
  639. package/dist/src/utils/language-detection.js +101 -0
  640. package/dist/src/utils/language-detection.js.map +1 -0
  641. package/dist/src/utils/llm-edit-fixer.d.ts +26 -0
  642. package/dist/src/utils/llm-edit-fixer.js +131 -0
  643. package/dist/src/utils/llm-edit-fixer.js.map +1 -0
  644. package/dist/src/utils/llm-edit-fixer.test.d.ts +6 -0
  645. package/dist/src/utils/llm-edit-fixer.test.js +186 -0
  646. package/dist/src/utils/llm-edit-fixer.test.js.map +1 -0
  647. package/dist/src/utils/memoryDiscovery.d.ts +7 -6
  648. package/dist/src/utils/memoryDiscovery.js +68 -33
  649. package/dist/src/utils/memoryDiscovery.js.map +1 -1
  650. package/dist/src/utils/memoryDiscovery.test.js +88 -26
  651. package/dist/src/utils/memoryDiscovery.test.js.map +1 -1
  652. package/dist/src/utils/memoryImportProcessor.js +15 -22
  653. package/dist/src/utils/memoryImportProcessor.js.map +1 -1
  654. package/dist/src/utils/memoryImportProcessor.test.js +16 -141
  655. package/dist/src/utils/memoryImportProcessor.test.js.map +1 -1
  656. package/dist/src/utils/messageInspectors.d.ts +1 -1
  657. package/dist/src/utils/nextSpeakerChecker.d.ts +3 -3
  658. package/dist/src/utils/nextSpeakerChecker.js +8 -2
  659. package/dist/src/utils/nextSpeakerChecker.js.map +1 -1
  660. package/dist/src/utils/nextSpeakerChecker.test.js +75 -64
  661. package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -1
  662. package/dist/src/utils/partUtils.d.ts +22 -1
  663. package/dist/src/utils/partUtils.js +68 -0
  664. package/dist/src/utils/partUtils.js.map +1 -1
  665. package/dist/src/utils/partUtils.test.js +112 -1
  666. package/dist/src/utils/partUtils.test.js.map +1 -1
  667. package/dist/src/utils/pathReader.d.ts +17 -0
  668. package/dist/src/utils/pathReader.js +92 -0
  669. package/dist/src/utils/pathReader.js.map +1 -0
  670. package/dist/src/utils/pathReader.test.d.ts +6 -0
  671. package/dist/src/utils/pathReader.test.js +363 -0
  672. package/dist/src/utils/pathReader.test.js.map +1 -0
  673. package/dist/src/utils/paths.d.ts +0 -17
  674. package/dist/src/utils/paths.js +2 -28
  675. package/dist/src/utils/paths.js.map +1 -1
  676. package/dist/src/utils/promptIdContext.d.ts +7 -0
  677. package/dist/src/utils/promptIdContext.js +8 -0
  678. package/dist/src/utils/promptIdContext.js.map +1 -0
  679. package/dist/src/utils/quotaErrorDetection.d.ts +1 -1
  680. package/dist/src/utils/retry.d.ts +3 -1
  681. package/dist/src/utils/retry.js +20 -5
  682. package/dist/src/utils/retry.js.map +1 -1
  683. package/dist/src/utils/retry.test.js +35 -3
  684. package/dist/src/utils/retry.test.js.map +1 -1
  685. package/dist/src/utils/schemaValidator.js +15 -1
  686. package/dist/src/utils/schemaValidator.js.map +1 -1
  687. package/dist/src/utils/schemaValidator.test.d.ts +6 -0
  688. package/dist/src/utils/schemaValidator.test.js +113 -0
  689. package/dist/src/utils/schemaValidator.test.js.map +1 -0
  690. package/dist/src/utils/session.js +1 -1
  691. package/dist/src/utils/session.js.map +1 -1
  692. package/dist/src/utils/shell-utils.d.ts +6 -1
  693. package/dist/src/utils/shell-utils.js +51 -30
  694. package/dist/src/utils/shell-utils.js.map +1 -1
  695. package/dist/src/utils/shell-utils.test.js +9 -0
  696. package/dist/src/utils/shell-utils.test.js.map +1 -1
  697. package/dist/src/utils/summarizer.d.ts +2 -2
  698. package/dist/src/utils/summarizer.test.js.map +1 -1
  699. package/dist/src/utils/systemEncoding.js +2 -2
  700. package/dist/src/utils/systemEncoding.js.map +1 -1
  701. package/dist/src/utils/systemEncoding.test.js +2 -2
  702. package/dist/src/utils/systemEncoding.test.js.map +1 -1
  703. package/dist/src/utils/terminalSerializer.d.ts +25 -0
  704. package/dist/src/utils/terminalSerializer.js +432 -0
  705. package/dist/src/utils/terminalSerializer.js.map +1 -0
  706. package/dist/src/utils/terminalSerializer.test.d.ts +6 -0
  707. package/dist/src/utils/terminalSerializer.test.js +176 -0
  708. package/dist/src/utils/terminalSerializer.test.js.map +1 -0
  709. package/dist/src/utils/textUtils.d.ts +5 -0
  710. package/dist/src/utils/textUtils.js +14 -0
  711. package/dist/src/utils/textUtils.js.map +1 -1
  712. package/dist/src/utils/textUtils.test.d.ts +6 -0
  713. package/dist/src/utils/textUtils.test.js +59 -0
  714. package/dist/src/utils/textUtils.test.js.map +1 -0
  715. package/dist/src/utils/thoughtUtils.d.ts +21 -0
  716. package/dist/src/utils/thoughtUtils.js +39 -0
  717. package/dist/src/utils/thoughtUtils.js.map +1 -0
  718. package/dist/src/utils/thoughtUtils.test.d.ts +6 -0
  719. package/dist/src/utils/thoughtUtils.test.js +78 -0
  720. package/dist/src/utils/thoughtUtils.test.js.map +1 -0
  721. package/dist/src/utils/tool-utils.d.ts +19 -0
  722. package/dist/src/utils/tool-utils.js +58 -0
  723. package/dist/src/utils/tool-utils.js.map +1 -0
  724. package/dist/src/utils/tool-utils.test.d.ts +6 -0
  725. package/dist/src/utils/tool-utils.test.js +61 -0
  726. package/dist/src/utils/tool-utils.test.js.map +1 -0
  727. package/dist/src/utils/userAccountManager.d.ts +20 -0
  728. package/dist/src/utils/userAccountManager.js +114 -0
  729. package/dist/src/utils/userAccountManager.js.map +1 -0
  730. package/dist/src/utils/userAccountManager.test.d.ts +6 -0
  731. package/dist/src/utils/{user_account.test.js → userAccountManager.test.js} +33 -30
  732. package/dist/src/utils/userAccountManager.test.js.map +1 -0
  733. package/dist/src/utils/workspaceContext.js +13 -7
  734. package/dist/src/utils/workspaceContext.js.map +1 -1
  735. package/dist/src/utils/workspaceContext.test.js +41 -16
  736. package/dist/src/utils/workspaceContext.test.js.map +1 -1
  737. package/dist/tsconfig.tsbuildinfo +1 -1
  738. package/package.json +18 -9
  739. package/dist/src/core/modelCheck.d.ts +0 -14
  740. package/dist/src/core/modelCheck.js +0 -62
  741. package/dist/src/core/modelCheck.js.map +0 -1
  742. package/dist/src/test-utils/tools.d.ts +0 -44
  743. package/dist/src/test-utils/tools.js.map +0 -1
  744. package/dist/src/utils/flashFallback.integration.test.js.map +0 -1
  745. package/dist/src/utils/user_account.d.ts +0 -9
  746. package/dist/src/utils/user_account.js +0 -109
  747. package/dist/src/utils/user_account.js.map +0 -1
  748. package/dist/src/utils/user_account.test.js.map +0 -1
  749. package/dist/src/utils/user_id.d.ts +0 -11
  750. package/dist/src/utils/user_id.js +0 -49
  751. package/dist/src/utils/user_id.js.map +0 -1
  752. package/dist/src/utils/user_id.test.js +0 -21
  753. package/dist/src/utils/user_id.test.js.map +0 -1
  754. /package/dist/src/{utils/flashFallback.integration.test.d.ts → agents/executor.test.d.ts} +0 -0
  755. /package/dist/src/{utils/user_account.test.d.ts → agents/invocation.test.d.ts} +0 -0
  756. /package/dist/src/{utils/user_id.test.d.ts → agents/registry.test.d.ts} +0 -0
@@ -3,26 +3,46 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
7
- import { GoogleGenAI, } from '@google/genai';
8
- import { findIndexAfterFraction, GeminiClient } from './client.js';
9
- import { AuthType } from './contentGenerator.js';
10
- import { Config } from '../config/config.js';
11
- import { GeminiEventType, Turn } from './turn.js';
6
+ import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
7
+ import { findCompressSplitPoint, isThinkingDefault, isThinkingSupported, GeminiClient, } from './client.js';
8
+ import { AuthType, } from './contentGenerator.js';
9
+ import {} from './geminiChat.js';
10
+ import { CompressionStatus, GeminiEventType, Turn, } from './turn.js';
12
11
  import { getCoreSystemPrompt } from './prompts.js';
13
12
  import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
14
13
  import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
15
14
  import { setSimulate429 } from '../utils/testUtils.js';
16
15
  import { tokenLimit } from './tokenLimits.js';
17
- import { ideContext } from '../ide/ideContext.js';
16
+ import { ideContextStore } from '../ide/ideContext.js';
18
17
  import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
18
+ import { uiTelemetryService } from '../telemetry/uiTelemetry.js';
19
+ // Mock fs module to prevent actual file system operations during tests
20
+ const mockFileSystem = new Map();
21
+ vi.mock('node:fs', () => {
22
+ const fsModule = {
23
+ mkdirSync: vi.fn(),
24
+ writeFileSync: vi.fn((path, data) => {
25
+ mockFileSystem.set(path, data);
26
+ }),
27
+ readFileSync: vi.fn((path) => {
28
+ if (mockFileSystem.has(path)) {
29
+ return mockFileSystem.get(path);
30
+ }
31
+ throw Object.assign(new Error('ENOENT: no such file or directory'), {
32
+ code: 'ENOENT',
33
+ });
34
+ }),
35
+ existsSync: vi.fn((path) => mockFileSystem.has(path)),
36
+ };
37
+ return {
38
+ default: fsModule,
39
+ ...fsModule,
40
+ };
41
+ });
19
42
  // --- Mocks ---
20
- const mockChatCreateFn = vi.fn();
21
- const mockGenerateContentFn = vi.fn();
22
- const mockEmbedContentFn = vi.fn();
23
43
  const mockTurnRunFn = vi.fn();
24
- vi.mock('@google/genai');
25
- vi.mock('./turn', () => {
44
+ vi.mock('./turn', async (importOriginal) => {
45
+ const actual = await importOriginal();
26
46
  // Define a mock class that has the same shape as the real Turn
27
47
  class MockTurn {
28
48
  pendingToolCalls = [];
@@ -34,11 +54,8 @@ vi.mock('./turn', () => {
34
54
  }
35
55
  // Export the mock class as 'Turn'
36
56
  return {
57
+ ...actual,
37
58
  Turn: MockTurn,
38
- GeminiEventType: {
39
- MaxSessionTurns: 'MaxSessionTurns',
40
- ChatCompressed: 'ChatCompressed',
41
- },
42
59
  };
43
60
  });
44
61
  vi.mock('../config/config.js');
@@ -60,42 +77,78 @@ vi.mock('../telemetry/index.js', () => ({
60
77
  logApiError: vi.fn(),
61
78
  }));
62
79
  vi.mock('../ide/ideContext.js');
63
- describe('findIndexAfterFraction', () => {
64
- const history = [
65
- { role: 'user', parts: [{ text: 'This is the first message.' }] }, // JSON length: 66
66
- { role: 'model', parts: [{ text: 'This is the second message.' }] }, // JSON length: 68
67
- { role: 'user', parts: [{ text: 'This is the third message.' }] }, // JSON length: 66
68
- { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68
69
- { role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65
70
- ];
71
- // Total length: 333
80
+ vi.mock('../telemetry/uiTelemetry.js', () => ({
81
+ uiTelemetryService: {
82
+ setLastPromptTokenCount: vi.fn(),
83
+ getLastPromptTokenCount: vi.fn(),
84
+ },
85
+ }));
86
+ /**
87
+ * Array.fromAsync ponyfill, which will be available in es 2024.
88
+ *
89
+ * Buffers an async generator into an array and returns the result.
90
+ */
91
+ async function fromAsync(promise) {
92
+ const results = [];
93
+ for await (const result of promise) {
94
+ results.push(result);
95
+ }
96
+ return results;
97
+ }
98
+ describe('findCompressSplitPoint', () => {
72
99
  it('should throw an error for non-positive numbers', () => {
73
- expect(() => findIndexAfterFraction(history, 0)).toThrow('Fraction must be between 0 and 1');
100
+ expect(() => findCompressSplitPoint([], 0)).toThrow('Fraction must be between 0 and 1');
74
101
  });
75
102
  it('should throw an error for a fraction greater than or equal to 1', () => {
76
- 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);
77
107
  });
78
108
  it('should handle a fraction in the middle', () => {
79
- // 333 * 0.5 = 166.5
80
- // 0: 66
81
- // 1: 66 + 68 = 134
82
- // 2: 134 + 66 = 200
83
- // 200 >= 166.5, so index is 2
84
- expect(findIndexAfterFraction(history, 0.5)).toBe(2);
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);
85
117
  });
86
- it('should handle a fraction that results in the last index', () => {
87
- // 333 * 0.9 = 299.7
88
- // ...
89
- // 3: 200 + 68 = 268
90
- // 4: 268 + 65 = 333
91
- // 333 >= 299.7, so index is 4
92
- expect(findIndexAfterFraction(history, 0.9)).toBe(4);
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);
93
127
  });
94
- it('should handle an empty history', () => {
95
- expect(findIndexAfterFraction([], 0.5)).toBe(0);
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);
136
+ });
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);
96
146
  });
97
147
  it('should handle a history with only one item', () => {
98
- expect(findIndexAfterFraction(history.slice(0, 1), 0.5)).toBe(0);
148
+ const historyWithEmptyParts = [
149
+ { role: 'user', parts: [{ text: 'Message 1' }] },
150
+ ];
151
+ expect(findCompressSplitPoint(historyWithEmptyParts, 0.5)).toBe(0);
99
152
  });
100
153
  it('should handle history with weird parts', () => {
101
154
  const historyWithEmptyParts = [
@@ -103,37 +156,54 @@ describe('findIndexAfterFraction', () => {
103
156
  { role: 'model', parts: [{ fileData: { fileUri: 'derp' } }] },
104
157
  { role: 'user', parts: [{ text: 'Message 2' }] },
105
158
  ];
106
- expect(findIndexAfterFraction(historyWithEmptyParts, 0.5)).toBe(1);
159
+ expect(findCompressSplitPoint(historyWithEmptyParts, 0.5)).toBe(2);
160
+ });
161
+ });
162
+ describe('isThinkingSupported', () => {
163
+ it('should return true for gemini-2.5', () => {
164
+ expect(isThinkingSupported('gemini-2.5')).toBe(true);
165
+ });
166
+ it('should return true for gemini-2.5-pro', () => {
167
+ expect(isThinkingSupported('gemini-2.5-pro')).toBe(true);
168
+ });
169
+ it('should return false for other models', () => {
170
+ expect(isThinkingSupported('gemini-1.5-flash')).toBe(false);
171
+ expect(isThinkingSupported('some-other-model')).toBe(false);
172
+ });
173
+ });
174
+ describe('isThinkingDefault', () => {
175
+ it('should return false for gemini-2.5-flash-lite', () => {
176
+ expect(isThinkingDefault('gemini-2.5-flash-lite')).toBe(false);
177
+ });
178
+ it('should return true for gemini-2.5', () => {
179
+ expect(isThinkingDefault('gemini-2.5')).toBe(true);
180
+ });
181
+ it('should return true for gemini-2.5-pro', () => {
182
+ expect(isThinkingDefault('gemini-2.5-pro')).toBe(true);
183
+ });
184
+ it('should return false for other models', () => {
185
+ expect(isThinkingDefault('gemini-1.5-flash')).toBe(false);
186
+ expect(isThinkingDefault('some-other-model')).toBe(false);
107
187
  });
108
188
  });
109
189
  describe('Gemini Client (client.ts)', () => {
190
+ let mockContentGenerator;
191
+ let mockConfig;
110
192
  let client;
193
+ let mockGenerateContentFn;
111
194
  beforeEach(async () => {
112
195
  vi.resetAllMocks();
196
+ vi.mocked(uiTelemetryService.setLastPromptTokenCount).mockClear();
197
+ mockGenerateContentFn = vi.fn().mockResolvedValue({
198
+ candidates: [{ content: { parts: [{ text: '{"key": "value"}' }] } }],
199
+ });
113
200
  // Disable 429 simulation for tests
114
201
  setSimulate429(false);
115
- // Set up the mock for GoogleGenAI constructor and its methods
116
- const MockedGoogleGenAI = vi.mocked(GoogleGenAI);
117
- MockedGoogleGenAI.mockImplementation(() => {
118
- const mock = {
119
- chats: { create: mockChatCreateFn },
120
- models: {
121
- generateContent: mockGenerateContentFn,
122
- embedContent: mockEmbedContentFn,
123
- },
124
- };
125
- return mock;
126
- });
127
- mockChatCreateFn.mockResolvedValue({});
128
- mockGenerateContentFn.mockResolvedValue({
129
- candidates: [
130
- {
131
- content: {
132
- parts: [{ text: '{"key": "value"}' }],
133
- },
134
- },
135
- ],
136
- });
202
+ mockContentGenerator = {
203
+ generateContent: mockGenerateContentFn,
204
+ generateContentStream: vi.fn(),
205
+ batchEmbedContents: vi.fn(),
206
+ };
137
207
  // Because the GeminiClient constructor kicks off an async process (startChat)
138
208
  // that depends on a fully-formed Config object, we need to mock the
139
209
  // entire implementation of Config for these tests.
@@ -143,12 +213,11 @@ describe('Gemini Client (client.ts)', () => {
143
213
  };
144
214
  const fileService = new FileDiscoveryService('/test/dir');
145
215
  const contentGeneratorConfig = {
146
- model: 'test-model',
147
216
  apiKey: 'test-key',
148
217
  vertexai: false,
149
218
  authType: AuthType.USE_GEMINI,
150
219
  };
151
- const mockConfigObject = {
220
+ mockConfig = {
152
221
  getContentGeneratorConfig: vi
153
222
  .fn()
154
223
  .mockReturnValue(contentGeneratorConfig),
@@ -176,181 +245,34 @@ describe('Gemini Client (client.ts)', () => {
176
245
  getDirectories: vi.fn().mockReturnValue(['/test/dir']),
177
246
  }),
178
247
  getGeminiClient: vi.fn(),
248
+ getModelRouterService: vi.fn().mockReturnValue({
249
+ route: vi.fn().mockResolvedValue({ model: 'default-routed-model' }),
250
+ }),
251
+ isInFallbackMode: vi.fn().mockReturnValue(false),
179
252
  setFallbackMode: vi.fn(),
180
253
  getChatCompression: vi.fn().mockReturnValue(undefined),
181
254
  getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
255
+ getUseSmartEdit: vi.fn().mockReturnValue(false),
256
+ getUseModelRouter: vi.fn().mockReturnValue(false),
257
+ getProjectRoot: vi.fn().mockReturnValue('/test/project/root'),
258
+ storage: {
259
+ getProjectTempDir: vi.fn().mockReturnValue('/test/temp'),
260
+ },
261
+ getContentGenerator: vi.fn().mockReturnValue(mockContentGenerator),
262
+ getBaseLlmClient: vi.fn().mockReturnValue({
263
+ generateJson: vi.fn().mockResolvedValue({
264
+ next_speaker: 'user',
265
+ reasoning: 'test',
266
+ }),
267
+ }),
182
268
  };
183
- const MockedConfig = vi.mocked(Config, true);
184
- MockedConfig.mockImplementation(() => mockConfigObject);
185
- // We can instantiate the client here since Config is mocked
186
- // and the constructor will use the mocked GoogleGenAI
187
- client = new GeminiClient(new Config({ sessionId: 'test-session-id' }));
188
- mockConfigObject.getGeminiClient.mockReturnValue(client);
189
- await client.initialize(contentGeneratorConfig);
269
+ client = new GeminiClient(mockConfig);
270
+ await client.initialize();
271
+ vi.mocked(mockConfig.getGeminiClient).mockReturnValue(client);
190
272
  });
191
273
  afterEach(() => {
192
274
  vi.restoreAllMocks();
193
275
  });
194
- // NOTE: The following tests for startChat were removed due to persistent issues with
195
- // the @google/genai mock. Specifically, the mockChatCreateFn (representing instance.chats.create)
196
- // was not being detected as called by the GeminiClient instance.
197
- // This likely points to a subtle issue in how the GoogleGenerativeAI class constructor
198
- // and its instance methods are mocked and then used by the class under test.
199
- // For future debugging, ensure that the `this.client` in `GeminiClient` (which is an
200
- // instance of the mocked GoogleGenerativeAI) correctly has its `chats.create` method
201
- // pointing to `mockChatCreateFn`.
202
- // it('startChat should call getCoreSystemPrompt with userMemory and pass to chats.create', async () => { ... });
203
- // it('startChat should call getCoreSystemPrompt with empty string if userMemory is empty', async () => { ... });
204
- // NOTE: The following tests for generateJson were removed due to persistent issues with
205
- // the @google/genai mock, similar to the startChat tests. The mockGenerateContentFn
206
- // (representing instance.models.generateContent) was not being detected as called, or the mock
207
- // was not preventing an actual API call (leading to API key errors).
208
- // For future debugging, ensure `this.client.models.generateContent` in `GeminiClient` correctly
209
- // uses the `mockGenerateContentFn`.
210
- // it('generateJson should call getCoreSystemPrompt with userMemory and pass to generateContent', async () => { ... });
211
- // it('generateJson should call getCoreSystemPrompt with empty string if userMemory is empty', async () => { ... });
212
- describe('generateEmbedding', () => {
213
- const texts = ['hello world', 'goodbye world'];
214
- const testEmbeddingModel = 'test-embedding-model';
215
- it('should call embedContent with correct parameters and return embeddings', async () => {
216
- const mockEmbeddings = [
217
- [0.1, 0.2, 0.3],
218
- [0.4, 0.5, 0.6],
219
- ];
220
- const mockResponse = {
221
- embeddings: [
222
- { values: mockEmbeddings[0] },
223
- { values: mockEmbeddings[1] },
224
- ],
225
- };
226
- mockEmbedContentFn.mockResolvedValue(mockResponse);
227
- const result = await client.generateEmbedding(texts);
228
- expect(mockEmbedContentFn).toHaveBeenCalledTimes(1);
229
- expect(mockEmbedContentFn).toHaveBeenCalledWith({
230
- model: testEmbeddingModel,
231
- contents: texts,
232
- });
233
- expect(result).toEqual(mockEmbeddings);
234
- });
235
- it('should return an empty array if an empty array is passed', async () => {
236
- const result = await client.generateEmbedding([]);
237
- expect(result).toEqual([]);
238
- expect(mockEmbedContentFn).not.toHaveBeenCalled();
239
- });
240
- it('should throw an error if API response has no embeddings array', async () => {
241
- mockEmbedContentFn.mockResolvedValue({}); // No `embeddings` key
242
- await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
243
- });
244
- it('should throw an error if API response has an empty embeddings array', async () => {
245
- const mockResponse = {
246
- embeddings: [],
247
- };
248
- mockEmbedContentFn.mockResolvedValue(mockResponse);
249
- await expect(client.generateEmbedding(texts)).rejects.toThrow('No embeddings found in API response.');
250
- });
251
- it('should throw an error if API returns a mismatched number of embeddings', async () => {
252
- const mockResponse = {
253
- embeddings: [{ values: [1, 2, 3] }], // Only one for two texts
254
- };
255
- mockEmbedContentFn.mockResolvedValue(mockResponse);
256
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned a mismatched number of embeddings. Expected 2, got 1.');
257
- });
258
- it('should throw an error if any embedding has nullish values', async () => {
259
- const mockResponse = {
260
- embeddings: [{ values: [1, 2, 3] }, { values: undefined }], // Second one is bad
261
- };
262
- mockEmbedContentFn.mockResolvedValue(mockResponse);
263
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 1: "goodbye world"');
264
- });
265
- it('should throw an error if any embedding has an empty values array', async () => {
266
- const mockResponse = {
267
- embeddings: [{ values: [] }, { values: [1, 2, 3] }], // First one is bad
268
- };
269
- mockEmbedContentFn.mockResolvedValue(mockResponse);
270
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API returned an empty embedding for input text at index 0: "hello world"');
271
- });
272
- it('should propagate errors from the API call', async () => {
273
- const apiError = new Error('API Failure');
274
- mockEmbedContentFn.mockRejectedValue(apiError);
275
- await expect(client.generateEmbedding(texts)).rejects.toThrow('API Failure');
276
- });
277
- });
278
- describe('generateContent', () => {
279
- it('should call generateContent with the correct parameters', async () => {
280
- const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
281
- const generationConfig = { temperature: 0.5 };
282
- const abortSignal = new AbortController().signal;
283
- // Mock countTokens
284
- const mockGenerator = {
285
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
286
- generateContent: mockGenerateContentFn,
287
- };
288
- client['contentGenerator'] = mockGenerator;
289
- await client.generateContent(contents, generationConfig, abortSignal);
290
- expect(mockGenerateContentFn).toHaveBeenCalledWith({
291
- model: 'test-model',
292
- config: {
293
- abortSignal,
294
- systemInstruction: getCoreSystemPrompt(''),
295
- temperature: 0.5,
296
- topP: 1,
297
- },
298
- contents,
299
- }, 'test-session-id');
300
- });
301
- });
302
- describe('generateJson', () => {
303
- it('should call generateContent with the correct parameters', async () => {
304
- const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
305
- const schema = { type: 'string' };
306
- const abortSignal = new AbortController().signal;
307
- // Mock countTokens
308
- const mockGenerator = {
309
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
310
- generateContent: mockGenerateContentFn,
311
- };
312
- client['contentGenerator'] = mockGenerator;
313
- await client.generateJson(contents, schema, abortSignal);
314
- expect(mockGenerateContentFn).toHaveBeenCalledWith({
315
- model: 'test-model', // Should use current model from config
316
- config: {
317
- abortSignal,
318
- systemInstruction: getCoreSystemPrompt(''),
319
- temperature: 0,
320
- topP: 1,
321
- responseJsonSchema: schema,
322
- responseMimeType: 'application/json',
323
- },
324
- contents,
325
- }, 'test-session-id');
326
- });
327
- it('should allow overriding model and config', async () => {
328
- const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
329
- const schema = { type: 'string' };
330
- const abortSignal = new AbortController().signal;
331
- const customModel = 'custom-json-model';
332
- const customConfig = { temperature: 0.9, topK: 20 };
333
- const mockGenerator = {
334
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
335
- generateContent: mockGenerateContentFn,
336
- };
337
- client['contentGenerator'] = mockGenerator;
338
- await client.generateJson(contents, schema, abortSignal, customModel, customConfig);
339
- expect(mockGenerateContentFn).toHaveBeenCalledWith({
340
- model: customModel,
341
- config: {
342
- abortSignal,
343
- systemInstruction: getCoreSystemPrompt(''),
344
- temperature: 0.9,
345
- topP: 1, // from default
346
- topK: 20,
347
- responseJsonSchema: schema,
348
- responseMimeType: 'application/json',
349
- },
350
- contents,
351
- }, 'test-session-id');
352
- });
353
- });
354
276
  describe('addHistory', () => {
355
277
  it('should call chat.addHistory with the provided content', async () => {
356
278
  const mockChat = {
@@ -388,37 +310,166 @@ describe('Gemini Client (client.ts)', () => {
388
310
  });
389
311
  });
390
312
  describe('tryCompressChat', () => {
391
- const mockCountTokens = vi.fn();
392
- const mockSendMessage = vi.fn();
393
313
  const mockGetHistory = vi.fn();
394
314
  beforeEach(() => {
395
315
  vi.mock('./tokenLimits', () => ({
396
316
  tokenLimit: vi.fn(),
397
317
  }));
398
- client['contentGenerator'] = {
399
- countTokens: mockCountTokens,
400
- };
401
318
  client['chat'] = {
402
319
  getHistory: mockGetHistory,
403
320
  addHistory: vi.fn(),
404
321
  setHistory: vi.fn(),
405
- sendMessage: mockSendMessage,
406
322
  };
407
323
  });
324
+ function setup({ chatHistory = [
325
+ { role: 'user', parts: [{ text: 'Long conversation' }] },
326
+ { role: 'model', parts: [{ text: 'Long response' }] },
327
+ ], originalTokenCount = 1000, summaryText = 'This is a summary.', } = {}) {
328
+ const mockOriginalChat = {
329
+ getHistory: vi.fn((_curated) => chatHistory),
330
+ setHistory: vi.fn(),
331
+ };
332
+ client['chat'] = mockOriginalChat;
333
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
334
+ mockGenerateContentFn.mockResolvedValue({
335
+ candidates: [
336
+ {
337
+ content: {
338
+ role: 'model',
339
+ parts: [{ text: summaryText }],
340
+ },
341
+ },
342
+ ],
343
+ });
344
+ // Calculate what the new history will be
345
+ const splitPoint = findCompressSplitPoint(chatHistory, 0.7); // 1 - 0.3
346
+ const historyToKeep = chatHistory.slice(splitPoint);
347
+ // This is the history that the new chat will have.
348
+ // It includes the default startChat history + the extra history from tryCompressChat
349
+ const newCompressedHistory = [
350
+ // Mocked envParts + canned response from startChat
351
+ {
352
+ role: 'user',
353
+ parts: [{ text: 'Mocked env context' }],
354
+ },
355
+ {
356
+ role: 'model',
357
+ parts: [{ text: 'Got it. Thanks for the context!' }],
358
+ },
359
+ // extraHistory from tryCompressChat
360
+ {
361
+ role: 'user',
362
+ parts: [{ text: summaryText }],
363
+ },
364
+ {
365
+ role: 'model',
366
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
367
+ },
368
+ ...historyToKeep,
369
+ ];
370
+ const mockNewChat = {
371
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
372
+ setHistory: vi.fn(),
373
+ };
374
+ client['startChat'] = vi
375
+ .fn()
376
+ .mockResolvedValue(mockNewChat);
377
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
378
+ const estimatedNewTokenCount = Math.floor(totalChars / 4);
379
+ return {
380
+ client,
381
+ mockOriginalChat,
382
+ mockNewChat,
383
+ estimatedNewTokenCount,
384
+ };
385
+ }
386
+ describe('when compression inflates the token count', () => {
387
+ it('allows compression to be forced/manual after a failure', async () => {
388
+ // Call 1 (Fails): Setup with a long summary to inflate tokens
389
+ const longSummary = 'long summary '.repeat(100);
390
+ const { client, estimatedNewTokenCount: inflatedTokenCount } = setup({
391
+ originalTokenCount: 100,
392
+ summaryText: longSummary,
393
+ });
394
+ expect(inflatedTokenCount).toBeGreaterThan(100); // Ensure setup is correct
395
+ await client.tryCompressChat('prompt-id-4', false); // Fails
396
+ // Call 2 (Forced): Re-setup with a short summary
397
+ const shortSummary = 'short';
398
+ const { estimatedNewTokenCount: compressedTokenCount } = setup({
399
+ originalTokenCount: 100,
400
+ summaryText: shortSummary,
401
+ });
402
+ expect(compressedTokenCount).toBeLessThanOrEqual(100); // Ensure setup is correct
403
+ const result = await client.tryCompressChat('prompt-id-4', true); // Forced
404
+ expect(result).toEqual({
405
+ compressionStatus: CompressionStatus.COMPRESSED,
406
+ newTokenCount: compressedTokenCount,
407
+ originalTokenCount: 100,
408
+ });
409
+ });
410
+ it('yields the result even if the compression inflated the tokens', async () => {
411
+ const longSummary = 'long summary '.repeat(100);
412
+ const { client, estimatedNewTokenCount } = setup({
413
+ originalTokenCount: 100,
414
+ summaryText: longSummary,
415
+ });
416
+ expect(estimatedNewTokenCount).toBeGreaterThan(100); // Ensure setup is correct
417
+ const result = await client.tryCompressChat('prompt-id-4', false);
418
+ expect(result).toEqual({
419
+ compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
420
+ newTokenCount: estimatedNewTokenCount,
421
+ originalTokenCount: 100,
422
+ });
423
+ // IMPORTANT: The change in client.ts means setLastPromptTokenCount is NOT called on failure
424
+ expect(uiTelemetryService.setLastPromptTokenCount).not.toHaveBeenCalled();
425
+ });
426
+ it('does not manipulate the source chat', async () => {
427
+ const longSummary = 'long summary '.repeat(100);
428
+ const { client, mockOriginalChat, estimatedNewTokenCount } = setup({
429
+ originalTokenCount: 100,
430
+ summaryText: longSummary,
431
+ });
432
+ expect(estimatedNewTokenCount).toBeGreaterThan(100); // Ensure setup is correct
433
+ await client.tryCompressChat('prompt-id-4', false);
434
+ // On failure, the chat should NOT be replaced
435
+ expect(client['chat']).toBe(mockOriginalChat);
436
+ });
437
+ it('will not attempt to compress context after a failure', async () => {
438
+ const longSummary = 'long summary '.repeat(100);
439
+ const { client, estimatedNewTokenCount } = setup({
440
+ originalTokenCount: 100,
441
+ summaryText: longSummary,
442
+ });
443
+ expect(estimatedNewTokenCount).toBeGreaterThan(100); // Ensure setup is correct
444
+ await client.tryCompressChat('prompt-id-4', false); // This fails and sets hasFailedCompressionAttempt = true
445
+ // This call should now be a NOOP
446
+ const result = await client.tryCompressChat('prompt-id-5', false);
447
+ // generateContent (for summary) should only have been called once
448
+ expect(mockGenerateContentFn).toHaveBeenCalledTimes(1);
449
+ expect(result).toEqual({
450
+ compressionStatus: CompressionStatus.NOOP,
451
+ newTokenCount: 0,
452
+ originalTokenCount: 0,
453
+ });
454
+ });
455
+ });
408
456
  it('should not trigger summarization if token count is below threshold', async () => {
409
457
  const MOCKED_TOKEN_LIMIT = 1000;
410
458
  vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
411
459
  mockGetHistory.mockReturnValue([
412
460
  { role: 'user', parts: [{ text: '...history...' }] },
413
461
  ]);
414
- mockCountTokens.mockResolvedValue({
415
- totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
416
- });
462
+ const originalTokenCount = MOCKED_TOKEN_LIMIT * 0.699;
463
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
417
464
  const initialChat = client.getChat();
418
- const result = await client.tryCompressChat('prompt-id-2');
465
+ const result = await client.tryCompressChat('prompt-id-2', false);
419
466
  const newChat = client.getChat();
420
467
  expect(tokenLimit).toHaveBeenCalled();
421
- expect(result).toBeNull();
468
+ expect(result).toEqual({
469
+ compressionStatus: CompressionStatus.NOOP,
470
+ newTokenCount: originalTokenCount,
471
+ originalTokenCount,
472
+ });
422
473
  expect(newChat).toBe(initialChat);
423
474
  });
424
475
  it('logs a telemetry event when compressing', async () => {
@@ -429,24 +480,51 @@ describe('Gemini Client (client.ts)', () => {
429
480
  vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
430
481
  contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
431
482
  });
432
- mockGetHistory.mockReturnValue([
433
- { role: 'user', parts: [{ text: '...history...' }] },
434
- ]);
483
+ const history = [{ role: 'user', parts: [{ text: '...history...' }] }];
484
+ mockGetHistory.mockReturnValue(history);
435
485
  const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
436
- const newTokenCount = 100;
437
- mockCountTokens
438
- .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
439
- .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
486
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
487
+ // We need to control the estimated new token count.
488
+ // We mock startChat to return a chat with a known history.
489
+ const summaryText = 'This is a summary.';
490
+ const splitPoint = findCompressSplitPoint(history, 0.7);
491
+ const historyToKeep = history.slice(splitPoint);
492
+ const newCompressedHistory = [
493
+ { role: 'user', parts: [{ text: 'Mocked env context' }] },
494
+ { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
495
+ { role: 'user', parts: [{ text: summaryText }] },
496
+ {
497
+ role: 'model',
498
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
499
+ },
500
+ ...historyToKeep,
501
+ ];
502
+ const mockNewChat = {
503
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
504
+ };
505
+ client['startChat'] = vi
506
+ .fn()
507
+ .mockResolvedValue(mockNewChat);
508
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
509
+ const newTokenCount = Math.floor(totalChars / 4);
440
510
  // Mock the summary response from the chat
441
- mockSendMessage.mockResolvedValue({
442
- role: 'model',
443
- parts: [{ text: 'This is a summary.' }],
511
+ mockGenerateContentFn.mockResolvedValue({
512
+ candidates: [
513
+ {
514
+ content: {
515
+ role: 'model',
516
+ parts: [{ text: summaryText }],
517
+ },
518
+ },
519
+ ],
444
520
  });
445
- await client.tryCompressChat('prompt-id-3');
521
+ await client.tryCompressChat('prompt-id-3', false);
446
522
  expect(ClearcutLogger.prototype.logChatCompressionEvent).toHaveBeenCalledWith(expect.objectContaining({
447
523
  tokens_before: originalTokenCount,
448
524
  tokens_after: newTokenCount,
449
525
  }));
526
+ expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledWith(newTokenCount);
527
+ expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledTimes(1);
450
528
  });
451
529
  it('should trigger summarization if token count is at threshold with contextPercentageThreshold setting', async () => {
452
530
  const MOCKED_TOKEN_LIMIT = 1000;
@@ -455,26 +533,51 @@ describe('Gemini Client (client.ts)', () => {
455
533
  vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
456
534
  contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
457
535
  });
458
- mockGetHistory.mockReturnValue([
459
- { role: 'user', parts: [{ text: '...history...' }] },
460
- ]);
536
+ const history = [{ role: 'user', parts: [{ text: '...history...' }] }];
537
+ mockGetHistory.mockReturnValue(history);
461
538
  const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
462
- const newTokenCount = 100;
463
- mockCountTokens
464
- .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
465
- .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
539
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
540
+ // Mock summary and new chat
541
+ const summaryText = 'This is a summary.';
542
+ const splitPoint = findCompressSplitPoint(history, 0.7);
543
+ const historyToKeep = history.slice(splitPoint);
544
+ const newCompressedHistory = [
545
+ { role: 'user', parts: [{ text: 'Mocked env context' }] },
546
+ { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
547
+ { role: 'user', parts: [{ text: summaryText }] },
548
+ {
549
+ role: 'model',
550
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
551
+ },
552
+ ...historyToKeep,
553
+ ];
554
+ const mockNewChat = {
555
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
556
+ };
557
+ client['startChat'] = vi
558
+ .fn()
559
+ .mockResolvedValue(mockNewChat);
560
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
561
+ const newTokenCount = Math.floor(totalChars / 4);
466
562
  // Mock the summary response from the chat
467
- mockSendMessage.mockResolvedValue({
468
- role: 'model',
469
- parts: [{ text: 'This is a summary.' }],
563
+ mockGenerateContentFn.mockResolvedValue({
564
+ candidates: [
565
+ {
566
+ content: {
567
+ role: 'model',
568
+ parts: [{ text: summaryText }],
569
+ },
570
+ },
571
+ ],
470
572
  });
471
573
  const initialChat = client.getChat();
472
- const result = await client.tryCompressChat('prompt-id-3');
574
+ const result = await client.tryCompressChat('prompt-id-3', false);
473
575
  const newChat = client.getChat();
474
576
  expect(tokenLimit).toHaveBeenCalled();
475
- expect(mockSendMessage).toHaveBeenCalled();
577
+ expect(mockGenerateContentFn).toHaveBeenCalled();
476
578
  // Assert that summarization happened and returned the correct stats
477
579
  expect(result).toEqual({
580
+ compressionStatus: CompressionStatus.COMPRESSED,
478
581
  originalTokenCount,
479
582
  newTokenCount,
480
583
  });
@@ -484,7 +587,7 @@ describe('Gemini Client (client.ts)', () => {
484
587
  it('should not compress across a function call response', async () => {
485
588
  const MOCKED_TOKEN_LIMIT = 1000;
486
589
  vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
487
- mockGetHistory.mockReturnValue([
590
+ const history = [
488
591
  { role: 'user', parts: [{ text: '...history 1...' }] },
489
592
  { role: 'model', parts: [{ text: '...history 2...' }] },
490
593
  { role: 'user', parts: [{ text: '...history 3...' }] },
@@ -501,55 +604,111 @@ describe('Gemini Client (client.ts)', () => {
501
604
  { role: 'model', parts: [{ text: '...history 10...' }] },
502
605
  // Instead we will break here.
503
606
  { role: 'user', parts: [{ text: '...history 10...' }] },
504
- ]);
607
+ ];
608
+ mockGetHistory.mockReturnValue(history);
505
609
  const originalTokenCount = 1000 * 0.7;
506
- const newTokenCount = 100;
507
- mockCountTokens
508
- .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
509
- .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
610
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
611
+ // Mock summary and new chat
612
+ const summaryText = 'This is a summary.';
613
+ const splitPoint = findCompressSplitPoint(history, 0.7); // This should be 10
614
+ expect(splitPoint).toBe(10); // Verify split point logic
615
+ const historyToKeep = history.slice(splitPoint); // Should keep last user message
616
+ expect(historyToKeep).toEqual([
617
+ { role: 'user', parts: [{ text: '...history 10...' }] },
618
+ ]);
619
+ const newCompressedHistory = [
620
+ { role: 'user', parts: [{ text: 'Mocked env context' }] },
621
+ { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
622
+ { role: 'user', parts: [{ text: summaryText }] },
623
+ {
624
+ role: 'model',
625
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
626
+ },
627
+ ...historyToKeep,
628
+ ];
629
+ const mockNewChat = {
630
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
631
+ };
632
+ client['startChat'] = vi
633
+ .fn()
634
+ .mockResolvedValue(mockNewChat);
635
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
636
+ const newTokenCount = Math.floor(totalChars / 4);
510
637
  // Mock the summary response from the chat
511
- mockSendMessage.mockResolvedValue({
512
- role: 'model',
513
- parts: [{ text: 'This is a summary.' }],
638
+ mockGenerateContentFn.mockResolvedValue({
639
+ candidates: [
640
+ {
641
+ content: {
642
+ role: 'model',
643
+ parts: [{ text: summaryText }],
644
+ },
645
+ },
646
+ ],
514
647
  });
515
648
  const initialChat = client.getChat();
516
- const result = await client.tryCompressChat('prompt-id-3');
649
+ const result = await client.tryCompressChat('prompt-id-3', false);
517
650
  const newChat = client.getChat();
518
651
  expect(tokenLimit).toHaveBeenCalled();
519
- expect(mockSendMessage).toHaveBeenCalled();
652
+ expect(mockGenerateContentFn).toHaveBeenCalled();
520
653
  // Assert that summarization happened and returned the correct stats
521
654
  expect(result).toEqual({
655
+ compressionStatus: CompressionStatus.COMPRESSED,
522
656
  originalTokenCount,
523
657
  newTokenCount,
524
658
  });
525
659
  // Assert that the chat was reset
526
660
  expect(newChat).not.toBe(initialChat);
527
- // 1. standard start context message
528
- // 2. standard canned user start message
529
- // 3. compressed summary message
530
- // 4. standard canned user summary message
531
- // 5. The last user message (not the last 3 because that would start with a function response)
661
+ // 1. standard start context message (env)
662
+ // 2. standard canned model response
663
+ // 3. compressed summary message (user)
664
+ // 4. standard canned model response
665
+ // 5. The last user message (historyToKeep)
532
666
  expect(newChat.getHistory().length).toEqual(5);
533
667
  });
534
668
  it('should always trigger summarization when force is true, regardless of token count', async () => {
535
- mockGetHistory.mockReturnValue([
536
- { role: 'user', parts: [{ text: '...history...' }] },
537
- ]);
538
- const originalTokenCount = 10; // Well below threshold
539
- const newTokenCount = 5;
540
- mockCountTokens
541
- .mockResolvedValueOnce({ totalTokens: originalTokenCount })
542
- .mockResolvedValueOnce({ totalTokens: newTokenCount });
669
+ const history = [{ role: 'user', parts: [{ text: '...history...' }] }];
670
+ mockGetHistory.mockReturnValue(history);
671
+ const originalTokenCount = 100; // Well below threshold, but > estimated new count
672
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
673
+ // Mock summary and new chat
674
+ const summaryText = 'This is a summary.';
675
+ const splitPoint = findCompressSplitPoint(history, 0.7);
676
+ const historyToKeep = history.slice(splitPoint);
677
+ const newCompressedHistory = [
678
+ { role: 'user', parts: [{ text: 'Mocked env context' }] },
679
+ { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
680
+ { role: 'user', parts: [{ text: summaryText }] },
681
+ {
682
+ role: 'model',
683
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
684
+ },
685
+ ...historyToKeep,
686
+ ];
687
+ const mockNewChat = {
688
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
689
+ };
690
+ client['startChat'] = vi
691
+ .fn()
692
+ .mockResolvedValue(mockNewChat);
693
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
694
+ const newTokenCount = Math.floor(totalChars / 4);
543
695
  // Mock the summary response from the chat
544
- mockSendMessage.mockResolvedValue({
545
- role: 'model',
546
- parts: [{ text: 'This is a summary.' }],
696
+ mockGenerateContentFn.mockResolvedValue({
697
+ candidates: [
698
+ {
699
+ content: {
700
+ role: 'model',
701
+ parts: [{ text: summaryText }],
702
+ },
703
+ },
704
+ ],
547
705
  });
548
706
  const initialChat = client.getChat();
549
707
  const result = await client.tryCompressChat('prompt-id-1', true); // force = true
550
708
  const newChat = client.getChat();
551
- expect(mockSendMessage).toHaveBeenCalled();
709
+ expect(mockGenerateContentFn).toHaveBeenCalled();
552
710
  expect(result).toEqual({
711
+ compressionStatus: CompressionStatus.COMPRESSED,
553
712
  originalTokenCount,
554
713
  newTokenCount,
555
714
  });
@@ -558,9 +717,55 @@ describe('Gemini Client (client.ts)', () => {
558
717
  });
559
718
  });
560
719
  describe('sendMessageStream', () => {
720
+ it('emits a compression event when the context was automatically compressed', async () => {
721
+ // Arrange
722
+ mockTurnRunFn.mockReturnValue((async function* () {
723
+ yield { type: 'content', value: 'Hello' };
724
+ })());
725
+ const compressionInfo = {
726
+ compressionStatus: CompressionStatus.COMPRESSED,
727
+ originalTokenCount: 1000,
728
+ newTokenCount: 500,
729
+ };
730
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValueOnce(compressionInfo);
731
+ // Act
732
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-1');
733
+ const events = await fromAsync(stream);
734
+ // Assert
735
+ expect(events).toContainEqual({
736
+ type: GeminiEventType.ChatCompressed,
737
+ value: compressionInfo,
738
+ });
739
+ });
740
+ it.each([
741
+ {
742
+ compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
743
+ },
744
+ { compressionStatus: CompressionStatus.NOOP },
745
+ ])('does not emit a compression event when the status is $compressionStatus', async ({ compressionStatus }) => {
746
+ // Arrange
747
+ const mockStream = (async function* () {
748
+ yield { type: 'content', value: 'Hello' };
749
+ })();
750
+ mockTurnRunFn.mockReturnValue(mockStream);
751
+ const compressionInfo = {
752
+ compressionStatus,
753
+ originalTokenCount: 1000,
754
+ newTokenCount: 500,
755
+ };
756
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValueOnce(compressionInfo);
757
+ // Act
758
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-1');
759
+ const events = await fromAsync(stream);
760
+ // Assert
761
+ expect(events).not.toContainEqual({
762
+ type: GeminiEventType.ChatCompressed,
763
+ value: expect.anything(),
764
+ });
765
+ });
561
766
  it('should include editor context when ideMode is enabled', async () => {
562
767
  // Arrange
563
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
768
+ vi.mocked(ideContextStore.get).mockReturnValue({
564
769
  workspaceState: {
565
770
  openFiles: [
566
771
  {
@@ -581,21 +786,20 @@ describe('Gemini Client (client.ts)', () => {
581
786
  ],
582
787
  },
583
788
  });
584
- vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
585
- const mockStream = (async function* () {
789
+ vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
790
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
791
+ originalTokenCount: 0,
792
+ newTokenCount: 0,
793
+ compressionStatus: CompressionStatus.COMPRESSED,
794
+ });
795
+ mockTurnRunFn.mockReturnValue((async function* () {
586
796
  yield { type: 'content', value: 'Hello' };
587
- })();
588
- mockTurnRunFn.mockReturnValue(mockStream);
797
+ })());
589
798
  const mockChat = {
590
799
  addHistory: vi.fn(),
591
800
  getHistory: vi.fn().mockReturnValue([]),
592
801
  };
593
802
  client['chat'] = mockChat;
594
- const mockGenerator = {
595
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
596
- generateContent: mockGenerateContentFn,
597
- };
598
- client['contentGenerator'] = mockGenerator;
599
803
  const initialRequest = [{ text: 'Hi' }];
600
804
  // Act
601
805
  const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
@@ -603,7 +807,7 @@ describe('Gemini Client (client.ts)', () => {
603
807
  // consume stream
604
808
  }
605
809
  // Assert
606
- expect(ideContext.getIdeContext).toHaveBeenCalled();
810
+ expect(ideContextStore.get).toHaveBeenCalled();
607
811
  const expectedContext = `
608
812
  Here is the user's editor context as a JSON object. This is for your information only.
609
813
  \`\`\`json
@@ -628,7 +832,7 @@ ${JSON.stringify({
628
832
  });
629
833
  it('should not add context if ideMode is enabled but no open files', async () => {
630
834
  // Arrange
631
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
835
+ vi.mocked(ideContextStore.get).mockReturnValue({
632
836
  workspaceState: {
633
837
  openFiles: [],
634
838
  },
@@ -643,11 +847,6 @@ ${JSON.stringify({
643
847
  getHistory: vi.fn().mockReturnValue([]),
644
848
  };
645
849
  client['chat'] = mockChat;
646
- const mockGenerator = {
647
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
648
- generateContent: mockGenerateContentFn,
649
- };
650
- client['contentGenerator'] = mockGenerator;
651
850
  const initialRequest = [{ text: 'Hi' }];
652
851
  // Act
653
852
  const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
@@ -655,12 +854,16 @@ ${JSON.stringify({
655
854
  // consume stream
656
855
  }
657
856
  // Assert
658
- expect(ideContext.getIdeContext).toHaveBeenCalled();
659
- expect(mockTurnRunFn).toHaveBeenCalledWith(initialRequest, expect.any(Object));
857
+ expect(ideContextStore.get).toHaveBeenCalled();
858
+ // The `turn.run` method is now called with the model name as the first
859
+ // argument. We use `expect.any(String)` because this test is
860
+ // concerned with the IDE context logic, not the model routing,
861
+ // which is tested in its own dedicated suite.
862
+ expect(mockTurnRunFn).toHaveBeenCalledWith(expect.any(String), initialRequest, expect.any(Object));
660
863
  });
661
864
  it('should add context if ideMode is enabled and there is one active file', async () => {
662
865
  // Arrange
663
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
866
+ vi.mocked(ideContextStore.get).mockReturnValue({
664
867
  workspaceState: {
665
868
  openFiles: [
666
869
  {
@@ -674,6 +877,11 @@ ${JSON.stringify({
674
877
  },
675
878
  });
676
879
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
880
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
881
+ originalTokenCount: 0,
882
+ newTokenCount: 0,
883
+ compressionStatus: CompressionStatus.COMPRESSED,
884
+ });
677
885
  const mockStream = (async function* () {
678
886
  yield { type: 'content', value: 'Hello' };
679
887
  })();
@@ -683,11 +891,6 @@ ${JSON.stringify({
683
891
  getHistory: vi.fn().mockReturnValue([]),
684
892
  };
685
893
  client['chat'] = mockChat;
686
- const mockGenerator = {
687
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
688
- generateContent: mockGenerateContentFn,
689
- };
690
- client['contentGenerator'] = mockGenerator;
691
894
  const initialRequest = [{ text: 'Hi' }];
692
895
  // Act
693
896
  const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
@@ -695,7 +898,7 @@ ${JSON.stringify({
695
898
  // consume stream
696
899
  }
697
900
  // Assert
698
- expect(ideContext.getIdeContext).toHaveBeenCalled();
901
+ expect(ideContextStore.get).toHaveBeenCalled();
699
902
  const expectedContext = `
700
903
  Here is the user's editor context as a JSON object. This is for your information only.
701
904
  \`\`\`json
@@ -719,7 +922,7 @@ ${JSON.stringify({
719
922
  });
720
923
  it('should add context if ideMode is enabled and there are open files but no active file', async () => {
721
924
  // Arrange
722
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
925
+ vi.mocked(ideContextStore.get).mockReturnValue({
723
926
  workspaceState: {
724
927
  openFiles: [
725
928
  {
@@ -734,6 +937,11 @@ ${JSON.stringify({
734
937
  },
735
938
  });
736
939
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
940
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
941
+ originalTokenCount: 0,
942
+ newTokenCount: 0,
943
+ compressionStatus: CompressionStatus.COMPRESSED,
944
+ });
737
945
  const mockStream = (async function* () {
738
946
  yield { type: 'content', value: 'Hello' };
739
947
  })();
@@ -743,11 +951,6 @@ ${JSON.stringify({
743
951
  getHistory: vi.fn().mockReturnValue([]),
744
952
  };
745
953
  client['chat'] = mockChat;
746
- const mockGenerator = {
747
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
748
- generateContent: mockGenerateContentFn,
749
- };
750
- client['contentGenerator'] = mockGenerator;
751
954
  const initialRequest = [{ text: 'Hi' }];
752
955
  // Act
753
956
  const stream = client.sendMessageStream(initialRequest, new AbortController().signal, 'prompt-id-ide');
@@ -755,7 +958,7 @@ ${JSON.stringify({
755
958
  // consume stream
756
959
  }
757
960
  // Assert
758
- expect(ideContext.getIdeContext).toHaveBeenCalled();
961
+ expect(ideContextStore.get).toHaveBeenCalled();
759
962
  const expectedContext = `
760
963
  Here is the user's editor context as a JSON object. This is for your information only.
761
964
  \`\`\`json
@@ -781,11 +984,6 @@ ${JSON.stringify({
781
984
  getHistory: vi.fn().mockReturnValue([]),
782
985
  };
783
986
  client['chat'] = mockChat;
784
- const mockGenerator = {
785
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
786
- generateContent: mockGenerateContentFn,
787
- };
788
- client['contentGenerator'] = mockGenerator;
789
987
  // Act
790
988
  const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-1');
791
989
  // Consume the stream manually to get the final return value.
@@ -818,11 +1016,6 @@ ${JSON.stringify({
818
1016
  getHistory: vi.fn().mockReturnValue([]),
819
1017
  };
820
1018
  client['chat'] = mockChat;
821
- const mockGenerator = {
822
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
823
- generateContent: mockGenerateContentFn,
824
- };
825
- client['contentGenerator'] = mockGenerator;
826
1019
  // Use a signal that never gets aborted
827
1020
  const abortController = new AbortController();
828
1021
  const signal = abortController.signal;
@@ -885,11 +1078,6 @@ ${JSON.stringify({
885
1078
  getHistory: vi.fn().mockReturnValue([]),
886
1079
  };
887
1080
  client['chat'] = mockChat;
888
- const mockGenerator = {
889
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
890
- generateContent: mockGenerateContentFn,
891
- };
892
- client['contentGenerator'] = mockGenerator;
893
1081
  // Act & Assert
894
1082
  // Run up to the limit
895
1083
  for (let i = 0; i < MAX_SESSION_TURNS; i++) {
@@ -928,11 +1116,6 @@ ${JSON.stringify({
928
1116
  getHistory: vi.fn().mockReturnValue([]),
929
1117
  };
930
1118
  client['chat'] = mockChat;
931
- const mockGenerator = {
932
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
933
- generateContent: mockGenerateContentFn,
934
- };
935
- client['contentGenerator'] = mockGenerator;
936
1119
  // Use a signal that never gets aborted
937
1120
  const abortController = new AbortController();
938
1121
  const signal = abortController.signal;
@@ -971,19 +1154,107 @@ ${JSON.stringify({
971
1154
  console.log(`Infinite loop protection working: checkNextSpeaker called ${callCount} times, ` +
972
1155
  `${eventCount} events generated (properly bounded by MAX_TURNS)`);
973
1156
  });
1157
+ describe('Model Routing', () => {
1158
+ let mockRouterService;
1159
+ beforeEach(() => {
1160
+ mockRouterService = {
1161
+ route: vi
1162
+ .fn()
1163
+ .mockResolvedValue({ model: 'routed-model', reason: 'test' }),
1164
+ };
1165
+ vi.mocked(mockConfig.getModelRouterService).mockReturnValue(mockRouterService);
1166
+ mockTurnRunFn.mockReturnValue((async function* () {
1167
+ yield { type: 'content', value: 'Hello' };
1168
+ })());
1169
+ });
1170
+ it('should use the model router service to select a model on the first turn', async () => {
1171
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1172
+ await fromAsync(stream); // consume stream
1173
+ expect(mockConfig.getModelRouterService).toHaveBeenCalled();
1174
+ expect(mockRouterService.route).toHaveBeenCalled();
1175
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', // The model from the router
1176
+ [{ text: 'Hi' }], expect.any(Object));
1177
+ });
1178
+ it('should use the same model for subsequent turns in the same prompt (stickiness)', async () => {
1179
+ // First turn
1180
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1181
+ await fromAsync(stream);
1182
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1183
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
1184
+ // Second turn
1185
+ stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-1');
1186
+ await fromAsync(stream);
1187
+ // Router should not be called again
1188
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1189
+ // Should stick to the first model
1190
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Continue' }], expect.any(Object));
1191
+ });
1192
+ it('should reset the sticky model and re-route when the prompt_id changes', async () => {
1193
+ // First prompt
1194
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1195
+ await fromAsync(stream);
1196
+ expect(mockRouterService.route).toHaveBeenCalledTimes(1);
1197
+ expect(mockTurnRunFn).toHaveBeenCalledWith('routed-model', [{ text: 'Hi' }], expect.any(Object));
1198
+ // New prompt
1199
+ mockRouterService.route.mockResolvedValue({
1200
+ model: 'new-routed-model',
1201
+ reason: 'test',
1202
+ });
1203
+ stream = client.sendMessageStream([{ text: 'A new topic' }], new AbortController().signal, 'prompt-2');
1204
+ await fromAsync(stream);
1205
+ // Router should be called again for the new prompt
1206
+ expect(mockRouterService.route).toHaveBeenCalledTimes(2);
1207
+ // Should use the newly routed model
1208
+ expect(mockTurnRunFn).toHaveBeenCalledWith('new-routed-model', [{ text: 'A new topic' }], expect.any(Object));
1209
+ });
1210
+ it('should use the fallback model and bypass routing when in fallback mode', async () => {
1211
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
1212
+ mockRouterService.route.mockResolvedValue({
1213
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1214
+ reason: 'fallback',
1215
+ });
1216
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-1');
1217
+ await fromAsync(stream);
1218
+ expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
1219
+ });
1220
+ it('should stick to the fallback model for the entire sequence even if fallback mode ends', async () => {
1221
+ // Start the sequence in fallback mode
1222
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(true);
1223
+ mockRouterService.route.mockResolvedValue({
1224
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1225
+ reason: 'fallback',
1226
+ });
1227
+ let stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-fallback-stickiness');
1228
+ await fromAsync(stream);
1229
+ // First call should use fallback model
1230
+ expect(mockTurnRunFn).toHaveBeenCalledWith(DEFAULT_GEMINI_FLASH_MODEL, [{ text: 'Hi' }], expect.any(Object));
1231
+ // End fallback mode
1232
+ vi.mocked(mockConfig.isInFallbackMode).mockReturnValue(false);
1233
+ // Second call in the same sequence
1234
+ stream = client.sendMessageStream([{ text: 'Continue' }], new AbortController().signal, 'prompt-fallback-stickiness');
1235
+ await fromAsync(stream);
1236
+ // Router should still not be called, and it should stick to the fallback model
1237
+ expect(mockTurnRunFn).toHaveBeenCalledTimes(2); // Ensure it was called again
1238
+ expect(mockTurnRunFn).toHaveBeenLastCalledWith(DEFAULT_GEMINI_FLASH_MODEL, // Still the fallback model
1239
+ [{ text: 'Continue' }], expect.any(Object));
1240
+ });
1241
+ });
974
1242
  describe('Editor context delta', () => {
975
1243
  const mockStream = (async function* () {
976
1244
  yield { type: 'content', value: 'Hello' };
977
1245
  })();
978
1246
  beforeEach(() => {
979
1247
  client['forceFullIdeContext'] = false; // Reset before each delta test
980
- vi.spyOn(client, 'tryCompressChat').mockResolvedValue(null);
1248
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
1249
+ originalTokenCount: 0,
1250
+ newTokenCount: 0,
1251
+ compressionStatus: CompressionStatus.COMPRESSED,
1252
+ });
981
1253
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
982
1254
  mockTurnRunFn.mockReturnValue(mockStream);
983
1255
  const mockChat = {
984
1256
  addHistory: vi.fn(),
985
1257
  setHistory: vi.fn(),
986
- sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
987
1258
  // Assume history is not empty for delta checks
988
1259
  getHistory: vi
989
1260
  .fn()
@@ -992,11 +1263,6 @@ ${JSON.stringify({
992
1263
  ]),
993
1264
  };
994
1265
  client['chat'] = mockChat;
995
- const mockGenerator = {
996
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
997
- generateContent: mockGenerateContentFn,
998
- };
999
- client['contentGenerator'] = mockGenerator;
1000
1266
  });
1001
1267
  const testCases = [
1002
1268
  {
@@ -1112,7 +1378,7 @@ ${JSON.stringify({
1112
1378
  },
1113
1379
  };
1114
1380
  // Setup current context
1115
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1381
+ vi.mocked(ideContextStore.get).mockReturnValue({
1116
1382
  workspaceState: {
1117
1383
  openFiles: [
1118
1384
  { ...currentActiveFile, isActive: true, timestamp: Date.now() },
@@ -1158,7 +1424,7 @@ ${JSON.stringify({
1158
1424
  },
1159
1425
  };
1160
1426
  // Setup current context (same as previous)
1161
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1427
+ vi.mocked(ideContextStore.get).mockReturnValue({
1162
1428
  workspaceState: {
1163
1429
  openFiles: [
1164
1430
  { ...activeFile, isActive: true, timestamp: Date.now() },
@@ -1190,7 +1456,11 @@ ${JSON.stringify({
1190
1456
  describe('IDE context with pending tool calls', () => {
1191
1457
  let mockChat;
1192
1458
  beforeEach(() => {
1193
- vi.spyOn(client, 'tryCompressChat').mockResolvedValue(null);
1459
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
1460
+ originalTokenCount: 0,
1461
+ newTokenCount: 0,
1462
+ compressionStatus: CompressionStatus.COMPRESSED,
1463
+ });
1194
1464
  const mockStream = (async function* () {
1195
1465
  yield { type: 'content', value: 'response' };
1196
1466
  })();
@@ -1199,15 +1469,10 @@ ${JSON.stringify({
1199
1469
  addHistory: vi.fn(),
1200
1470
  getHistory: vi.fn().mockReturnValue([]), // Default empty history
1201
1471
  setHistory: vi.fn(),
1202
- sendMessage: vi.fn().mockResolvedValue({ text: 'summary' }),
1203
1472
  };
1204
1473
  client['chat'] = mockChat;
1205
- const mockGenerator = {
1206
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 0 }),
1207
- };
1208
- client['contentGenerator'] = mockGenerator;
1209
1474
  vi.spyOn(client['config'], 'getIdeMode').mockReturnValue(true);
1210
- vi.mocked(ideContext.getIdeContext).mockReturnValue({
1475
+ vi.mocked(ideContextStore.get).mockReturnValue({
1211
1476
  workspaceState: {
1212
1477
  openFiles: [{ path: '/path/to/file.ts', timestamp: Date.now() }],
1213
1478
  },
@@ -1283,7 +1548,7 @@ ${JSON.stringify({
1283
1548
  openFiles: [{ path: '/path/to/fileA.ts', timestamp: Date.now() }],
1284
1549
  },
1285
1550
  };
1286
- vi.mocked(ideContext.getIdeContext).mockReturnValue(initialIdeContext);
1551
+ vi.mocked(ideContextStore.get).mockReturnValue(initialIdeContext);
1287
1552
  // Act: Send the tool response
1288
1553
  let stream = client.sendMessageStream([
1289
1554
  {
@@ -1329,7 +1594,7 @@ ${JSON.stringify({
1329
1594
  openFiles: [{ path: '/path/to/fileB.ts', timestamp: Date.now() }],
1330
1595
  },
1331
1596
  };
1332
- vi.mocked(ideContext.getIdeContext).mockReturnValue(newIdeContext);
1597
+ vi.mocked(ideContextStore.get).mockReturnValue(newIdeContext);
1333
1598
  // Act: Send a new, regular user message
1334
1599
  stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1335
1600
  for await (const _ of stream) {
@@ -1359,7 +1624,7 @@ ${JSON.stringify({
1359
1624
  ],
1360
1625
  },
1361
1626
  };
1362
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextA);
1627
+ vi.mocked(ideContextStore.get).mockReturnValue(contextA);
1363
1628
  // Act: Send a regular message to establish the initial context
1364
1629
  let stream = client.sendMessageStream([{ text: 'Initial message' }], new AbortController().signal, 'prompt-id-initial');
1365
1630
  for await (const _ of stream) {
@@ -1392,7 +1657,7 @@ ${JSON.stringify({
1392
1657
  ],
1393
1658
  },
1394
1659
  };
1395
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextB);
1660
+ vi.mocked(ideContextStore.get).mockReturnValue(contextB);
1396
1661
  // Act: Send the tool response
1397
1662
  stream = client.sendMessageStream([
1398
1663
  {
@@ -1437,7 +1702,7 @@ ${JSON.stringify({
1437
1702
  ],
1438
1703
  },
1439
1704
  };
1440
- vi.mocked(ideContext.getIdeContext).mockReturnValue(contextC);
1705
+ vi.mocked(ideContextStore.get).mockReturnValue(contextC);
1441
1706
  // Act: Send a new, regular user message
1442
1707
  stream = client.sendMessageStream([{ text: 'Thanks!' }], new AbortController().signal, 'prompt-id-final');
1443
1708
  for await (const _ of stream) {
@@ -1453,150 +1718,130 @@ ${JSON.stringify({
1453
1718
  expect(JSON.stringify(finalCall)).toContain('fileC.ts');
1454
1719
  });
1455
1720
  });
1721
+ it('should not call checkNextSpeaker when turn.run() yields an error', async () => {
1722
+ // Arrange
1723
+ const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
1724
+ const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
1725
+ const mockStream = (async function* () {
1726
+ yield {
1727
+ type: GeminiEventType.Error,
1728
+ value: { error: { message: 'test error' } },
1729
+ };
1730
+ })();
1731
+ mockTurnRunFn.mockReturnValue(mockStream);
1732
+ const mockChat = {
1733
+ addHistory: vi.fn(),
1734
+ getHistory: vi.fn().mockReturnValue([]),
1735
+ };
1736
+ client['chat'] = mockChat;
1737
+ // Act
1738
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-error');
1739
+ for await (const _ of stream) {
1740
+ // consume stream
1741
+ }
1742
+ // Assert
1743
+ expect(mockCheckNextSpeaker).not.toHaveBeenCalled();
1744
+ });
1745
+ it('should not call checkNextSpeaker when turn.run() yields a value then an error', async () => {
1746
+ // Arrange
1747
+ const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
1748
+ const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
1749
+ const mockStream = (async function* () {
1750
+ yield { type: GeminiEventType.Content, value: 'some content' };
1751
+ yield {
1752
+ type: GeminiEventType.Error,
1753
+ value: { error: { message: 'test error' } },
1754
+ };
1755
+ })();
1756
+ mockTurnRunFn.mockReturnValue(mockStream);
1757
+ const mockChat = {
1758
+ addHistory: vi.fn(),
1759
+ getHistory: vi.fn().mockReturnValue([]),
1760
+ };
1761
+ client['chat'] = mockChat;
1762
+ // Act
1763
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-error');
1764
+ for await (const _ of stream) {
1765
+ // consume stream
1766
+ }
1767
+ // Assert
1768
+ expect(mockCheckNextSpeaker).not.toHaveBeenCalled();
1769
+ });
1770
+ it('should abort linked signal when loop is detected', async () => {
1771
+ // Arrange
1772
+ vi.spyOn(client['loopDetector'], 'turnStarted').mockResolvedValue(false);
1773
+ vi.spyOn(client['loopDetector'], 'addAndCheck')
1774
+ .mockReturnValueOnce(false)
1775
+ .mockReturnValueOnce(true);
1776
+ let capturedSignal;
1777
+ mockTurnRunFn.mockImplementation((model, request, signal) => {
1778
+ capturedSignal = signal;
1779
+ return (async function* () {
1780
+ yield { type: 'content', value: 'First event' };
1781
+ yield { type: 'content', value: 'Second event' };
1782
+ })();
1783
+ });
1784
+ const mockChat = {
1785
+ addHistory: vi.fn(),
1786
+ getHistory: vi.fn().mockReturnValue([]),
1787
+ };
1788
+ client['chat'] = mockChat;
1789
+ // Act
1790
+ const stream = client.sendMessageStream([{ text: 'Hi' }], new AbortController().signal, 'prompt-id-loop');
1791
+ const events = [];
1792
+ for await (const event of stream) {
1793
+ events.push(event);
1794
+ }
1795
+ // Assert
1796
+ expect(events).toContainEqual({ type: GeminiEventType.LoopDetected });
1797
+ expect(capturedSignal.aborted).toBe(true);
1798
+ });
1456
1799
  });
1457
1800
  describe('generateContent', () => {
1801
+ it('should call generateContent with the correct parameters', async () => {
1802
+ const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
1803
+ const generationConfig = { temperature: 0.5 };
1804
+ const abortSignal = new AbortController().signal;
1805
+ await client.generateContent(contents, generationConfig, abortSignal, DEFAULT_GEMINI_FLASH_MODEL);
1806
+ expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
1807
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1808
+ config: {
1809
+ abortSignal,
1810
+ systemInstruction: getCoreSystemPrompt(''),
1811
+ temperature: 0.5,
1812
+ topP: 1,
1813
+ },
1814
+ contents,
1815
+ }, 'test-session-id');
1816
+ });
1458
1817
  it('should use current model from config for content generation', async () => {
1459
1818
  const initialModel = client['config'].getModel();
1460
1819
  const contents = [{ role: 'user', parts: [{ text: 'test' }] }];
1461
1820
  const currentModel = initialModel + '-changed';
1462
1821
  vi.spyOn(client['config'], 'getModel').mockReturnValueOnce(currentModel);
1463
- const mockGenerator = {
1464
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
1465
- generateContent: mockGenerateContentFn,
1466
- };
1467
- client['contentGenerator'] = mockGenerator;
1468
- await client.generateContent(contents, {}, new AbortController().signal);
1469
- expect(mockGenerateContentFn).not.toHaveBeenCalledWith({
1822
+ await client.generateContent(contents, {}, new AbortController().signal, DEFAULT_GEMINI_FLASH_MODEL);
1823
+ expect(mockContentGenerator.generateContent).not.toHaveBeenCalledWith({
1470
1824
  model: initialModel,
1471
1825
  config: expect.any(Object),
1472
1826
  contents,
1473
1827
  });
1474
- expect(mockGenerateContentFn).toHaveBeenCalledWith({
1475
- model: currentModel,
1828
+ expect(mockContentGenerator.generateContent).toHaveBeenCalledWith({
1829
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1476
1830
  config: expect.any(Object),
1477
1831
  contents,
1478
1832
  }, 'test-session-id');
1479
1833
  });
1480
- });
1481
- describe('tryCompressChat', () => {
1482
- it('should use current model from config for token counting after sendMessage', async () => {
1483
- const initialModel = client['config'].getModel();
1484
- const mockCountTokens = vi
1485
- .fn()
1486
- .mockResolvedValueOnce({ totalTokens: 100000 })
1487
- .mockResolvedValueOnce({ totalTokens: 5000 });
1488
- const mockSendMessage = vi.fn().mockResolvedValue({ text: 'Summary' });
1489
- const mockChatHistory = [
1490
- { role: 'user', parts: [{ text: 'Long conversation' }] },
1491
- { role: 'model', parts: [{ text: 'Long response' }] },
1492
- ];
1493
- const mockChat = {
1494
- getHistory: vi.fn().mockReturnValue(mockChatHistory),
1495
- setHistory: vi.fn(),
1496
- sendMessage: mockSendMessage,
1497
- };
1498
- const mockGenerator = {
1499
- countTokens: mockCountTokens,
1500
- };
1501
- // mock the model has been changed between calls of `countTokens`
1502
- const firstCurrentModel = initialModel + '-changed-1';
1503
- const secondCurrentModel = initialModel + '-changed-2';
1504
- vi.spyOn(client['config'], 'getModel')
1505
- .mockReturnValueOnce(firstCurrentModel)
1506
- .mockReturnValueOnce(secondCurrentModel);
1507
- client['chat'] = mockChat;
1508
- client['contentGenerator'] = mockGenerator;
1509
- client['startChat'] = vi.fn().mockResolvedValue(mockChat);
1510
- const result = await client.tryCompressChat('prompt-id-4', true);
1511
- expect(mockCountTokens).toHaveBeenCalledTimes(2);
1512
- expect(mockCountTokens).toHaveBeenNthCalledWith(1, {
1513
- model: firstCurrentModel,
1514
- contents: mockChatHistory,
1515
- });
1516
- expect(mockCountTokens).toHaveBeenNthCalledWith(2, {
1517
- model: secondCurrentModel,
1518
- contents: expect.any(Array),
1519
- });
1520
- expect(result).toEqual({
1521
- originalTokenCount: 100000,
1522
- newTokenCount: 5000,
1523
- });
1524
- });
1525
- });
1526
- describe('handleFlashFallback', () => {
1527
- it('should use current model from config when checking for fallback', async () => {
1528
- const initialModel = client['config'].getModel();
1529
- const fallbackModel = DEFAULT_GEMINI_FLASH_MODEL;
1530
- // mock config been changed
1531
- const currentModel = initialModel + '-changed';
1532
- const getModelSpy = vi.spyOn(client['config'], 'getModel');
1533
- getModelSpy.mockReturnValue(currentModel);
1534
- const mockFallbackHandler = vi.fn().mockResolvedValue(true);
1535
- client['config'].flashFallbackHandler = mockFallbackHandler;
1536
- client['config'].setModel = vi.fn();
1537
- const result = await client['handleFlashFallback'](AuthType.LOGIN_WITH_GOOGLE);
1538
- expect(result).toBe(fallbackModel);
1539
- expect(mockFallbackHandler).toHaveBeenCalledWith(currentModel, fallbackModel, undefined);
1540
- });
1541
- });
1542
- describe('setHistory', () => {
1543
- it('should strip thought signatures when stripThoughts is true', () => {
1544
- const mockChat = {
1545
- setHistory: vi.fn(),
1546
- };
1547
- client['chat'] = mockChat;
1548
- const historyWithThoughts = [
1549
- {
1550
- role: 'user',
1551
- parts: [{ text: 'hello' }],
1552
- },
1553
- {
1554
- role: 'model',
1555
- parts: [
1556
- { text: 'thinking...', thoughtSignature: 'thought-123' },
1557
- {
1558
- functionCall: { name: 'test', args: {} },
1559
- thoughtSignature: 'thought-456',
1560
- },
1561
- ],
1562
- },
1563
- ];
1564
- client.setHistory(historyWithThoughts, { stripThoughts: true });
1565
- const expectedHistory = [
1566
- {
1567
- role: 'user',
1568
- parts: [{ text: 'hello' }],
1569
- },
1570
- {
1571
- role: 'model',
1572
- parts: [
1573
- { text: 'thinking...' },
1574
- { functionCall: { name: 'test', args: {} } },
1575
- ],
1576
- },
1577
- ];
1578
- expect(mockChat.setHistory).toHaveBeenCalledWith(expectedHistory);
1579
- });
1580
- it('should not strip thought signatures when stripThoughts is false', () => {
1581
- const mockChat = {
1582
- setHistory: vi.fn(),
1583
- };
1584
- client['chat'] = mockChat;
1585
- const historyWithThoughts = [
1586
- {
1587
- role: 'user',
1588
- parts: [{ text: 'hello' }],
1589
- },
1590
- {
1591
- role: 'model',
1592
- parts: [
1593
- { text: 'thinking...', thoughtSignature: 'thought-123' },
1594
- { text: 'ok', thoughtSignature: 'thought-456' },
1595
- ],
1596
- },
1597
- ];
1598
- client.setHistory(historyWithThoughts, { stripThoughts: false });
1599
- expect(mockChat.setHistory).toHaveBeenCalledWith(historyWithThoughts);
1834
+ it('should use the Flash model when fallback mode is active', async () => {
1835
+ const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
1836
+ const generationConfig = { temperature: 0.5 };
1837
+ const abortSignal = new AbortController().signal;
1838
+ const requestedModel = 'gemini-2.5-pro'; // A non-flash model
1839
+ // Mock config to be in fallback mode
1840
+ vi.spyOn(client['config'], 'isInFallbackMode').mockReturnValue(true);
1841
+ await client.generateContent(contents, generationConfig, abortSignal, requestedModel);
1842
+ expect(mockGenerateContentFn).toHaveBeenCalledWith(expect.objectContaining({
1843
+ model: DEFAULT_GEMINI_FLASH_MODEL,
1844
+ }), 'test-session-id');
1600
1845
  });
1601
1846
  });
1602
1847
  });