@google/gemini-cli-core 0.0.77777772 → 0.0.77777774

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 (291) hide show
  1. package/README.md +108 -71
  2. package/dist/index.d.ts +3 -1
  3. package/dist/index.js +2 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/agents/codebase-investigator.d.ts +46 -0
  6. package/dist/src/agents/codebase-investigator.js +136 -0
  7. package/dist/src/agents/codebase-investigator.js.map +1 -0
  8. package/dist/src/agents/executor.d.ts +92 -0
  9. package/dist/src/agents/executor.js +579 -0
  10. package/dist/src/agents/executor.js.map +1 -0
  11. package/dist/src/agents/executor.test.d.ts +6 -0
  12. package/dist/src/agents/executor.test.js +595 -0
  13. package/dist/src/agents/executor.test.js.map +1 -0
  14. package/dist/src/agents/invocation.d.ts +46 -0
  15. package/dist/src/agents/invocation.js +102 -0
  16. package/dist/src/agents/invocation.js.map +1 -0
  17. package/dist/src/agents/invocation.test.d.ts +6 -0
  18. package/dist/src/agents/invocation.test.js +215 -0
  19. package/dist/src/agents/invocation.test.js.map +1 -0
  20. package/dist/src/agents/registry.d.ts +36 -0
  21. package/dist/src/agents/registry.js +60 -0
  22. package/dist/src/agents/registry.js.map +1 -0
  23. package/dist/src/agents/registry.test.d.ts +6 -0
  24. package/dist/src/agents/registry.test.js +146 -0
  25. package/dist/src/agents/registry.test.js.map +1 -0
  26. package/dist/src/agents/schema-utils.d.ts +39 -0
  27. package/dist/src/agents/schema-utils.js +57 -0
  28. package/dist/src/agents/schema-utils.js.map +1 -0
  29. package/dist/src/agents/schema-utils.test.d.ts +6 -0
  30. package/dist/src/agents/schema-utils.test.js +144 -0
  31. package/dist/src/agents/schema-utils.test.js.map +1 -0
  32. package/dist/src/agents/subagent-tool-wrapper.d.ts +38 -0
  33. package/dist/src/agents/subagent-tool-wrapper.js +48 -0
  34. package/dist/src/agents/subagent-tool-wrapper.js.map +1 -0
  35. package/dist/src/agents/subagent-tool-wrapper.test.d.ts +6 -0
  36. package/dist/src/agents/subagent-tool-wrapper.test.js +112 -0
  37. package/dist/src/agents/subagent-tool-wrapper.test.js.map +1 -0
  38. package/dist/src/agents/types.d.ts +145 -0
  39. package/dist/src/agents/types.js +18 -0
  40. package/dist/src/agents/types.js.map +1 -0
  41. package/dist/src/agents/utils.d.ts +15 -0
  42. package/dist/src/agents/utils.js +29 -0
  43. package/dist/src/agents/utils.js.map +1 -0
  44. package/dist/src/agents/utils.test.d.ts +6 -0
  45. package/dist/src/agents/utils.test.js +87 -0
  46. package/dist/src/agents/utils.test.js.map +1 -0
  47. package/dist/src/code_assist/oauth-credential-storage.js +1 -1
  48. package/dist/src/code_assist/oauth-credential-storage.js.map +1 -1
  49. package/dist/src/code_assist/oauth2.test.js +14 -13
  50. package/dist/src/code_assist/oauth2.test.js.map +1 -1
  51. package/dist/src/code_assist/setup.js +4 -2
  52. package/dist/src/code_assist/setup.js.map +1 -1
  53. package/dist/src/config/config.d.ts +37 -11
  54. package/dist/src/config/config.js +77 -18
  55. package/dist/src/config/config.js.map +1 -1
  56. package/dist/src/config/config.test.js +88 -3
  57. package/dist/src/config/config.test.js.map +1 -1
  58. package/dist/src/config/storage.d.ts +0 -1
  59. package/dist/src/config/storage.js +2 -2
  60. package/dist/src/config/storage.js.map +1 -1
  61. package/dist/src/config/storage.test.js +7 -6
  62. package/dist/src/config/storage.test.js.map +1 -1
  63. package/dist/src/core/baseLlmClient.js +19 -21
  64. package/dist/src/core/baseLlmClient.js.map +1 -1
  65. package/dist/src/core/baseLlmClient.test.js +57 -17
  66. package/dist/src/core/baseLlmClient.test.js.map +1 -1
  67. package/dist/src/core/client.d.ts +3 -1
  68. package/dist/src/core/client.js +68 -46
  69. package/dist/src/core/client.js.map +1 -1
  70. package/dist/src/core/client.test.js +385 -95
  71. package/dist/src/core/client.test.js.map +1 -1
  72. package/dist/src/core/contentGenerator.js +3 -1
  73. package/dist/src/core/contentGenerator.js.map +1 -1
  74. package/dist/src/core/coreToolScheduler.js +12 -12
  75. package/dist/src/core/coreToolScheduler.js.map +1 -1
  76. package/dist/src/core/coreToolScheduler.test.js +260 -23
  77. package/dist/src/core/coreToolScheduler.test.js.map +1 -1
  78. package/dist/src/core/geminiChat.d.ts +7 -11
  79. package/dist/src/core/geminiChat.js +31 -74
  80. package/dist/src/core/geminiChat.js.map +1 -1
  81. package/dist/src/core/geminiChat.test.js +60 -229
  82. package/dist/src/core/geminiChat.test.js.map +1 -1
  83. package/dist/src/core/logger.test.js +2 -2
  84. package/dist/src/core/logger.test.js.map +1 -1
  85. package/dist/src/core/nonInteractiveToolExecutor.test.js +11 -11
  86. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
  87. package/dist/src/core/prompts.d.ts +2 -1
  88. package/dist/src/core/prompts.js +91 -17
  89. package/dist/src/core/prompts.js.map +1 -1
  90. package/dist/src/core/prompts.test.js +78 -29
  91. package/dist/src/core/prompts.test.js.map +1 -1
  92. package/dist/src/core/turn.d.ts +14 -2
  93. package/dist/src/core/turn.js +12 -1
  94. package/dist/src/core/turn.js.map +1 -1
  95. package/dist/src/core/turn.test.js +14 -2
  96. package/dist/src/core/turn.test.js.map +1 -1
  97. package/dist/src/generated/git-commit.d.ts +2 -2
  98. package/dist/src/generated/git-commit.js +2 -2
  99. package/dist/src/ide/detect-ide.d.ts +1 -0
  100. package/dist/src/ide/detect-ide.js +4 -1
  101. package/dist/src/ide/detect-ide.js.map +1 -1
  102. package/dist/src/ide/detect-ide.test.js +11 -0
  103. package/dist/src/ide/detect-ide.test.js.map +1 -1
  104. package/dist/src/ide/ide-client.js +3 -3
  105. package/dist/src/ide/ide-client.test.js +4 -4
  106. package/dist/src/ide/ide-installer.js +1 -1
  107. package/dist/src/ide/ide-installer.js.map +1 -1
  108. package/dist/src/ide/ide-installer.test.js +13 -1
  109. package/dist/src/ide/ide-installer.test.js.map +1 -1
  110. package/dist/src/index.d.ts +1 -0
  111. package/dist/src/index.js +1 -0
  112. package/dist/src/index.js.map +1 -1
  113. package/dist/src/mcp/sa-impersonation-provider.d.ts +33 -0
  114. package/dist/src/mcp/sa-impersonation-provider.js +130 -0
  115. package/dist/src/mcp/sa-impersonation-provider.js.map +1 -0
  116. package/dist/src/mcp/sa-impersonation-provider.test.d.ts +6 -0
  117. package/dist/src/mcp/sa-impersonation-provider.test.js +117 -0
  118. package/dist/src/mcp/sa-impersonation-provider.test.js.map +1 -0
  119. package/dist/src/mcp/token-storage/file-token-storage.js +2 -1
  120. package/dist/src/mcp/token-storage/file-token-storage.js.map +1 -1
  121. package/dist/src/mcp/token-storage/file-token-storage.test.js +4 -3
  122. package/dist/src/mcp/token-storage/file-token-storage.test.js.map +1 -1
  123. package/dist/src/services/chatRecordingService.d.ts +2 -1
  124. package/dist/src/services/chatRecordingService.js +2 -1
  125. package/dist/src/services/chatRecordingService.js.map +1 -1
  126. package/dist/src/services/fileSystemService.d.ts +9 -0
  127. package/dist/src/services/fileSystemService.js +11 -0
  128. package/dist/src/services/fileSystemService.js.map +1 -1
  129. package/dist/src/services/shellExecutionService.d.ts +3 -0
  130. package/dist/src/services/shellExecutionService.js +165 -49
  131. package/dist/src/services/shellExecutionService.js.map +1 -1
  132. package/dist/src/services/shellExecutionService.test.js +74 -5
  133. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  134. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +14 -2
  135. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +119 -12
  136. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  137. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +99 -1
  138. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  139. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +13 -3
  140. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +32 -5
  141. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  142. package/dist/src/telemetry/constants.d.ts +0 -52
  143. package/dist/src/telemetry/constants.js +0 -54
  144. package/dist/src/telemetry/constants.js.map +1 -1
  145. package/dist/src/telemetry/index.d.ts +5 -3
  146. package/dist/src/telemetry/index.js +11 -4
  147. package/dist/src/telemetry/index.js.map +1 -1
  148. package/dist/src/telemetry/loggers.d.ts +8 -2
  149. package/dist/src/telemetry/loggers.js +187 -288
  150. package/dist/src/telemetry/loggers.js.map +1 -1
  151. package/dist/src/telemetry/loggers.test.circular.js +3 -3
  152. package/dist/src/telemetry/loggers.test.circular.js.map +1 -1
  153. package/dist/src/telemetry/loggers.test.js +232 -13
  154. package/dist/src/telemetry/loggers.test.js.map +1 -1
  155. package/dist/src/telemetry/memory-monitor.d.ts +149 -0
  156. package/dist/src/telemetry/memory-monitor.js +335 -0
  157. package/dist/src/telemetry/memory-monitor.js.map +1 -0
  158. package/dist/src/telemetry/memory-monitor.test.d.ts +6 -0
  159. package/dist/src/telemetry/memory-monitor.test.js +472 -0
  160. package/dist/src/telemetry/memory-monitor.test.js.map +1 -0
  161. package/dist/src/telemetry/metrics.d.ts +406 -20
  162. package/dist/src/telemetry/metrics.js +479 -235
  163. package/dist/src/telemetry/metrics.js.map +1 -1
  164. package/dist/src/telemetry/metrics.test.js +551 -55
  165. package/dist/src/telemetry/metrics.test.js.map +1 -1
  166. package/dist/src/telemetry/telemetryAttributes.d.ts +8 -0
  167. package/dist/src/telemetry/telemetryAttributes.js +18 -0
  168. package/dist/src/telemetry/telemetryAttributes.js.map +1 -0
  169. package/dist/src/telemetry/types.d.ts +150 -3
  170. package/dist/src/telemetry/types.js +664 -33
  171. package/dist/src/telemetry/types.js.map +1 -1
  172. package/dist/src/telemetry/uiTelemetry.d.ts +1 -1
  173. package/dist/src/telemetry/uiTelemetry.js +1 -1
  174. package/dist/src/telemetry/uiTelemetry.js.map +1 -1
  175. package/dist/src/telemetry/uiTelemetry.test.js +3 -3
  176. package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
  177. package/dist/src/test-utils/mock-tool.d.ts +28 -3
  178. package/dist/src/test-utils/mock-tool.js +71 -1
  179. package/dist/src/test-utils/mock-tool.js.map +1 -1
  180. package/dist/src/tools/glob.js +2 -1
  181. package/dist/src/tools/glob.js.map +1 -1
  182. package/dist/src/tools/mcp-client.d.ts +3 -2
  183. package/dist/src/tools/mcp-client.js +45 -33
  184. package/dist/src/tools/mcp-client.js.map +1 -1
  185. package/dist/src/tools/mcp-client.test.js +168 -5
  186. package/dist/src/tools/mcp-client.test.js.map +1 -1
  187. package/dist/src/tools/memoryTool.d.ts +1 -1
  188. package/dist/src/tools/memoryTool.js +1 -2
  189. package/dist/src/tools/memoryTool.js.map +1 -1
  190. package/dist/src/tools/memoryTool.test.js +9 -8
  191. package/dist/src/tools/memoryTool.test.js.map +1 -1
  192. package/dist/src/tools/shell.js +2 -2
  193. package/dist/src/tools/shell.js.map +1 -1
  194. package/dist/src/tools/shell.test.js +1 -1
  195. package/dist/src/tools/shell.test.js.map +1 -1
  196. package/dist/src/tools/smart-edit.d.ts +1 -1
  197. package/dist/src/tools/smart-edit.js +114 -10
  198. package/dist/src/tools/smart-edit.js.map +1 -1
  199. package/dist/src/tools/smart-edit.test.js +91 -29
  200. package/dist/src/tools/smart-edit.test.js.map +1 -1
  201. package/dist/src/tools/tool-error.d.ts +21 -0
  202. package/dist/src/tools/tool-error.js +27 -0
  203. package/dist/src/tools/tool-error.js.map +1 -1
  204. package/dist/src/tools/tool-names.d.ts +9 -0
  205. package/dist/src/tools/tool-names.js +18 -0
  206. package/dist/src/tools/tool-names.js.map +1 -0
  207. package/dist/src/tools/tool-registry.test.js +10 -10
  208. package/dist/src/tools/tool-registry.test.js.map +1 -1
  209. package/dist/src/tools/web-fetch.js +3 -0
  210. package/dist/src/tools/web-fetch.js.map +1 -1
  211. package/dist/src/tools/web-fetch.test.js +44 -0
  212. package/dist/src/tools/web-fetch.test.js.map +1 -1
  213. package/dist/src/tools/web-search.js +2 -1
  214. package/dist/src/tools/web-search.js.map +1 -1
  215. package/dist/src/tools/write-file.js +2 -1
  216. package/dist/src/tools/write-file.js.map +1 -1
  217. package/dist/src/tools/write-todos.d.ts +1 -1
  218. package/dist/src/tools/write-todos.js +4 -3
  219. package/dist/src/tools/write-todos.js.map +1 -1
  220. package/dist/src/utils/editCorrector.js +2 -2
  221. package/dist/src/utils/editCorrector.js.map +1 -1
  222. package/dist/src/utils/editor.js +1 -0
  223. package/dist/src/utils/editor.js.map +1 -1
  224. package/dist/src/utils/editor.test.js +1 -0
  225. package/dist/src/utils/editor.test.js.map +1 -1
  226. package/dist/src/utils/errorParsing.d.ts +1 -1
  227. package/dist/src/utils/errorParsing.js +5 -33
  228. package/dist/src/utils/errorParsing.js.map +1 -1
  229. package/dist/src/utils/errorParsing.test.js +0 -88
  230. package/dist/src/utils/errorParsing.test.js.map +1 -1
  231. package/dist/src/utils/flashFallback.test.js +26 -45
  232. package/dist/src/utils/flashFallback.test.js.map +1 -1
  233. package/dist/src/utils/formatters.d.ts +1 -0
  234. package/dist/src/utils/formatters.js +2 -1
  235. package/dist/src/utils/formatters.js.map +1 -1
  236. package/dist/src/utils/formatters.test.d.ts +6 -0
  237. package/dist/src/utils/formatters.test.js +26 -0
  238. package/dist/src/utils/formatters.test.js.map +1 -0
  239. package/dist/src/utils/getFolderStructure.test.js +7 -6
  240. package/dist/src/utils/getFolderStructure.test.js.map +1 -1
  241. package/dist/src/utils/googleErrors.d.ts +104 -0
  242. package/dist/src/utils/googleErrors.js +108 -0
  243. package/dist/src/utils/googleErrors.js.map +1 -0
  244. package/dist/src/utils/googleErrors.test.d.ts +6 -0
  245. package/dist/src/utils/googleErrors.test.js +212 -0
  246. package/dist/src/utils/googleErrors.test.js.map +1 -0
  247. package/dist/src/utils/googleQuotaErrors.d.ts +35 -0
  248. package/dist/src/utils/googleQuotaErrors.js +108 -0
  249. package/dist/src/utils/googleQuotaErrors.js.map +1 -0
  250. package/dist/src/utils/googleQuotaErrors.test.d.ts +6 -0
  251. package/dist/src/utils/googleQuotaErrors.test.js +189 -0
  252. package/dist/src/utils/googleQuotaErrors.test.js.map +1 -0
  253. package/dist/src/utils/installationManager.test.js +2 -1
  254. package/dist/src/utils/installationManager.test.js.map +1 -1
  255. package/dist/src/utils/llm-edit-fixer.js +13 -4
  256. package/dist/src/utils/llm-edit-fixer.js.map +1 -1
  257. package/dist/src/utils/llm-edit-fixer.test.js +81 -0
  258. package/dist/src/utils/llm-edit-fixer.test.js.map +1 -1
  259. package/dist/src/utils/memoryDiscovery.d.ts +1 -0
  260. package/dist/src/utils/memoryDiscovery.js +2 -1
  261. package/dist/src/utils/memoryDiscovery.js.map +1 -1
  262. package/dist/src/utils/memoryDiscovery.test.js +99 -21
  263. package/dist/src/utils/memoryDiscovery.test.js.map +1 -1
  264. package/dist/src/utils/memoryImportProcessor.js +13 -20
  265. package/dist/src/utils/memoryImportProcessor.js.map +1 -1
  266. package/dist/src/utils/memoryImportProcessor.test.js +14 -0
  267. package/dist/src/utils/memoryImportProcessor.test.js.map +1 -1
  268. package/dist/src/utils/pathCorrector.d.ts +25 -0
  269. package/dist/src/utils/pathCorrector.js +33 -0
  270. package/dist/src/utils/pathCorrector.js.map +1 -0
  271. package/dist/src/utils/pathCorrector.test.d.ts +6 -0
  272. package/dist/src/utils/pathCorrector.test.js +83 -0
  273. package/dist/src/utils/pathCorrector.test.js.map +1 -0
  274. package/dist/src/utils/quotaErrorDetection.d.ts +0 -2
  275. package/dist/src/utils/quotaErrorDetection.js +0 -46
  276. package/dist/src/utils/quotaErrorDetection.js.map +1 -1
  277. package/dist/src/utils/retry.d.ts +3 -1
  278. package/dist/src/utils/retry.js +53 -161
  279. package/dist/src/utils/retry.js.map +1 -1
  280. package/dist/src/utils/retry.test.js +90 -149
  281. package/dist/src/utils/retry.test.js.map +1 -1
  282. package/dist/src/utils/terminalSerializer.d.ts +1 -4
  283. package/dist/src/utils/terminalSerializer.js +3 -3
  284. package/dist/src/utils/terminalSerializer.js.map +1 -1
  285. package/dist/src/utils/userAccountManager.test.js +2 -1
  286. package/dist/src/utils/userAccountManager.test.js.map +1 -1
  287. package/dist/tsconfig.tsbuildinfo +1 -1
  288. package/package.json +1 -1
  289. package/dist/src/test-utils/tools.d.ts +0 -45
  290. package/dist/src/test-utils/tools.js +0 -105
  291. package/dist/src/test-utils/tools.js.map +0 -1
@@ -80,6 +80,7 @@ vi.mock('../ide/ideContext.js');
80
80
  vi.mock('../telemetry/uiTelemetry.js', () => ({
81
81
  uiTelemetryService: {
82
82
  setLastPromptTokenCount: vi.fn(),
83
+ getLastPromptTokenCount: vi.fn(),
83
84
  },
84
85
  }));
85
86
  /**
@@ -94,7 +95,7 @@ async function fromAsync(promise) {
94
95
  }
95
96
  return results;
96
97
  }
97
- describe('findIndexAfterFraction', () => {
98
+ describe('findCompressSplitPoint', () => {
98
99
  it('should throw an error for non-positive numbers', () => {
99
100
  expect(() => findCompressSplitPoint([], 0)).toThrow('Fraction must be between 0 and 1');
100
101
  });
@@ -112,7 +113,7 @@ describe('findIndexAfterFraction', () => {
112
113
  { role: 'model', parts: [{ text: 'This is the fourth message.' }] }, // JSON length: 68 (80%)
113
114
  { role: 'user', parts: [{ text: 'This is the fifth message.' }] }, // JSON length: 65 (100%)
114
115
  ];
115
- expect(findCompressSplitPoint(history, 0.5)).toBe(2);
116
+ expect(findCompressSplitPoint(history, 0.5)).toBe(4);
116
117
  });
117
118
  it('should handle a fraction of last index', () => {
118
119
  const history = [
@@ -201,7 +202,6 @@ describe('Gemini Client (client.ts)', () => {
201
202
  mockContentGenerator = {
202
203
  generateContent: mockGenerateContentFn,
203
204
  generateContentStream: vi.fn(),
204
- countTokens: vi.fn().mockResolvedValue({ totalTokens: 100 }),
205
205
  batchEmbedContents: vi.fn(),
206
206
  };
207
207
  // Because the GeminiClient constructor kicks off an async process (startChat)
@@ -254,6 +254,7 @@ describe('Gemini Client (client.ts)', () => {
254
254
  getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
255
255
  getUseSmartEdit: vi.fn().mockReturnValue(false),
256
256
  getUseModelRouter: vi.fn().mockReturnValue(false),
257
+ getContinueOnFailedApiCall: vi.fn(),
257
258
  getProjectRoot: vi.fn().mockReturnValue('/test/project/root'),
258
259
  storage: {
259
260
  getProjectTempDir: vi.fn().mockReturnValue('/test/temp'),
@@ -324,74 +325,128 @@ describe('Gemini Client (client.ts)', () => {
324
325
  function setup({ chatHistory = [
325
326
  { role: 'user', parts: [{ text: 'Long conversation' }] },
326
327
  { role: 'model', parts: [{ text: 'Long response' }] },
327
- ], } = {}) {
328
- const mockChat = {
329
- getHistory: vi.fn().mockReturnValue(chatHistory),
328
+ ], originalTokenCount = 1000, summaryText = 'This is a summary.', } = {}) {
329
+ const mockOriginalChat = {
330
+ getHistory: vi.fn((_curated) => chatHistory),
330
331
  setHistory: vi.fn(),
331
332
  };
332
- vi.mocked(mockContentGenerator.countTokens)
333
- .mockResolvedValueOnce({ totalTokens: 1000 })
334
- .mockResolvedValueOnce({ totalTokens: 5000 });
335
- client['chat'] = mockChat;
336
- client['startChat'] = vi.fn().mockResolvedValue({ ...mockChat });
337
- return { client, mockChat };
333
+ client['chat'] = mockOriginalChat;
334
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
335
+ mockGenerateContentFn.mockResolvedValue({
336
+ candidates: [
337
+ {
338
+ content: {
339
+ role: 'model',
340
+ parts: [{ text: summaryText }],
341
+ },
342
+ },
343
+ ],
344
+ });
345
+ // Calculate what the new history will be
346
+ const splitPoint = findCompressSplitPoint(chatHistory, 0.7); // 1 - 0.3
347
+ const historyToKeep = chatHistory.slice(splitPoint);
348
+ // This is the history that the new chat will have.
349
+ // It includes the default startChat history + the extra history from tryCompressChat
350
+ const newCompressedHistory = [
351
+ // Mocked envParts + canned response from startChat
352
+ {
353
+ role: 'user',
354
+ parts: [{ text: 'Mocked env context' }],
355
+ },
356
+ {
357
+ role: 'model',
358
+ parts: [{ text: 'Got it. Thanks for the context!' }],
359
+ },
360
+ // extraHistory from tryCompressChat
361
+ {
362
+ role: 'user',
363
+ parts: [{ text: summaryText }],
364
+ },
365
+ {
366
+ role: 'model',
367
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
368
+ },
369
+ ...historyToKeep,
370
+ ];
371
+ const mockNewChat = {
372
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
373
+ setHistory: vi.fn(),
374
+ };
375
+ client['startChat'] = vi
376
+ .fn()
377
+ .mockResolvedValue(mockNewChat);
378
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
379
+ const estimatedNewTokenCount = Math.floor(totalChars / 4);
380
+ return {
381
+ client,
382
+ mockOriginalChat,
383
+ mockNewChat,
384
+ estimatedNewTokenCount,
385
+ };
338
386
  }
339
387
  describe('when compression inflates the token count', () => {
340
388
  it('allows compression to be forced/manual after a failure', async () => {
341
- const { client } = setup();
342
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
343
- totalTokens: 1000,
389
+ // Call 1 (Fails): Setup with a long summary to inflate tokens
390
+ const longSummary = 'long summary '.repeat(100);
391
+ const { client, estimatedNewTokenCount: inflatedTokenCount } = setup({
392
+ originalTokenCount: 100,
393
+ summaryText: longSummary,
344
394
  });
395
+ expect(inflatedTokenCount).toBeGreaterThan(100); // Ensure setup is correct
345
396
  await client.tryCompressChat('prompt-id-4', false); // Fails
346
- const result = await client.tryCompressChat('prompt-id-4', true);
397
+ // Call 2 (Forced): Re-setup with a short summary
398
+ const shortSummary = 'short';
399
+ const { estimatedNewTokenCount: compressedTokenCount } = setup({
400
+ originalTokenCount: 100,
401
+ summaryText: shortSummary,
402
+ });
403
+ expect(compressedTokenCount).toBeLessThanOrEqual(100); // Ensure setup is correct
404
+ const result = await client.tryCompressChat('prompt-id-4', true); // Forced
347
405
  expect(result).toEqual({
348
406
  compressionStatus: CompressionStatus.COMPRESSED,
349
- newTokenCount: 1000,
350
- originalTokenCount: 1000,
407
+ newTokenCount: compressedTokenCount,
408
+ originalTokenCount: 100,
351
409
  });
352
410
  });
353
411
  it('yields the result even if the compression inflated the tokens', async () => {
354
- const { client } = setup();
355
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
356
- totalTokens: 1000,
412
+ const longSummary = 'long summary '.repeat(100);
413
+ const { client, estimatedNewTokenCount } = setup({
414
+ originalTokenCount: 100,
415
+ summaryText: longSummary,
357
416
  });
417
+ expect(estimatedNewTokenCount).toBeGreaterThan(100); // Ensure setup is correct
358
418
  const result = await client.tryCompressChat('prompt-id-4', false);
359
419
  expect(result).toEqual({
360
420
  compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
361
- newTokenCount: 5000,
362
- originalTokenCount: 1000,
421
+ newTokenCount: estimatedNewTokenCount,
422
+ originalTokenCount: 100,
363
423
  });
364
- expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledWith(5000);
365
- expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledTimes(1);
424
+ // IMPORTANT: The change in client.ts means setLastPromptTokenCount is NOT called on failure
425
+ expect(uiTelemetryService.setLastPromptTokenCount).not.toHaveBeenCalled();
366
426
  });
367
427
  it('does not manipulate the source chat', async () => {
368
- const { client, mockChat } = setup();
369
- await client.tryCompressChat('prompt-id-4', false);
370
- expect(client['chat']).toBe(mockChat); // a new chat session was not created
371
- });
372
- it('restores the history back to the original', async () => {
373
- vi.mocked(tokenLimit).mockReturnValue(1000);
374
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
375
- totalTokens: 999,
376
- });
377
- const originalHistory = [
378
- { role: 'user', parts: [{ text: 'what is your wisdom?' }] },
379
- { role: 'model', parts: [{ text: 'some wisdom' }] },
380
- { role: 'user', parts: [{ text: 'ahh that is a good a wisdom' }] },
381
- ];
382
- const { client } = setup({
383
- chatHistory: originalHistory,
428
+ const longSummary = 'long summary '.repeat(100);
429
+ const { client, mockOriginalChat, estimatedNewTokenCount } = setup({
430
+ originalTokenCount: 100,
431
+ summaryText: longSummary,
384
432
  });
385
- const { compressionStatus } = await client.tryCompressChat('prompt-id-4', false);
386
- expect(compressionStatus).toBe(CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT);
387
- expect(client['chat']?.setHistory).toHaveBeenCalledWith(originalHistory);
433
+ expect(estimatedNewTokenCount).toBeGreaterThan(100); // Ensure setup is correct
434
+ await client.tryCompressChat('prompt-id-4', false);
435
+ // On failure, the chat should NOT be replaced
436
+ expect(client['chat']).toBe(mockOriginalChat);
388
437
  });
389
438
  it('will not attempt to compress context after a failure', async () => {
390
- const { client } = setup();
391
- await client.tryCompressChat('prompt-id-4', false);
439
+ const longSummary = 'long summary '.repeat(100);
440
+ const { client, estimatedNewTokenCount } = setup({
441
+ originalTokenCount: 100,
442
+ summaryText: longSummary,
443
+ });
444
+ expect(estimatedNewTokenCount).toBeGreaterThan(100); // Ensure setup is correct
445
+ await client.tryCompressChat('prompt-id-4', false); // This fails and sets hasFailedCompressionAttempt = true
446
+ // This call should now be a NOOP
392
447
  const result = await client.tryCompressChat('prompt-id-5', false);
393
- // it counts tokens for {original, compressed} and then never again
394
- expect(mockContentGenerator.countTokens).toHaveBeenCalledTimes(2);
448
+ // generateContent (for summary) should only have been called once
449
+ expect(mockGenerateContentFn).toHaveBeenCalledTimes(1);
395
450
  expect(result).toEqual({
396
451
  compressionStatus: CompressionStatus.NOOP,
397
452
  newTokenCount: 0,
@@ -405,17 +460,16 @@ describe('Gemini Client (client.ts)', () => {
405
460
  mockGetHistory.mockReturnValue([
406
461
  { role: 'user', parts: [{ text: '...history...' }] },
407
462
  ]);
408
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
409
- totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
410
- });
463
+ const originalTokenCount = MOCKED_TOKEN_LIMIT * 0.699;
464
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
411
465
  const initialChat = client.getChat();
412
466
  const result = await client.tryCompressChat('prompt-id-2', false);
413
467
  const newChat = client.getChat();
414
468
  expect(tokenLimit).toHaveBeenCalled();
415
469
  expect(result).toEqual({
416
470
  compressionStatus: CompressionStatus.NOOP,
417
- newTokenCount: 699,
418
- originalTokenCount: 699,
471
+ newTokenCount: originalTokenCount,
472
+ originalTokenCount,
419
473
  });
420
474
  expect(newChat).toBe(initialChat);
421
475
  });
@@ -427,21 +481,40 @@ describe('Gemini Client (client.ts)', () => {
427
481
  vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
428
482
  contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
429
483
  });
430
- mockGetHistory.mockReturnValue([
431
- { role: 'user', parts: [{ text: '...history...' }] },
432
- ]);
484
+ const history = [{ role: 'user', parts: [{ text: '...history...' }] }];
485
+ mockGetHistory.mockReturnValue(history);
433
486
  const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
434
- const newTokenCount = 100;
435
- vi.mocked(mockContentGenerator.countTokens)
436
- .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
437
- .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
487
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
488
+ // We need to control the estimated new token count.
489
+ // We mock startChat to return a chat with a known history.
490
+ const summaryText = 'This is a summary.';
491
+ const splitPoint = findCompressSplitPoint(history, 0.7);
492
+ const historyToKeep = history.slice(splitPoint);
493
+ const newCompressedHistory = [
494
+ { role: 'user', parts: [{ text: 'Mocked env context' }] },
495
+ { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
496
+ { role: 'user', parts: [{ text: summaryText }] },
497
+ {
498
+ role: 'model',
499
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
500
+ },
501
+ ...historyToKeep,
502
+ ];
503
+ const mockNewChat = {
504
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
505
+ };
506
+ client['startChat'] = vi
507
+ .fn()
508
+ .mockResolvedValue(mockNewChat);
509
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
510
+ const newTokenCount = Math.floor(totalChars / 4);
438
511
  // Mock the summary response from the chat
439
512
  mockGenerateContentFn.mockResolvedValue({
440
513
  candidates: [
441
514
  {
442
515
  content: {
443
516
  role: 'model',
444
- parts: [{ text: 'This is a summary.' }],
517
+ parts: [{ text: summaryText }],
445
518
  },
446
519
  },
447
520
  ],
@@ -461,21 +534,39 @@ describe('Gemini Client (client.ts)', () => {
461
534
  vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
462
535
  contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
463
536
  });
464
- mockGetHistory.mockReturnValue([
465
- { role: 'user', parts: [{ text: '...history...' }] },
466
- ]);
537
+ const history = [{ role: 'user', parts: [{ text: '...history...' }] }];
538
+ mockGetHistory.mockReturnValue(history);
467
539
  const originalTokenCount = MOCKED_TOKEN_LIMIT * MOCKED_CONTEXT_PERCENTAGE_THRESHOLD;
468
- const newTokenCount = 100;
469
- vi.mocked(mockContentGenerator.countTokens)
470
- .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
471
- .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
540
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
541
+ // Mock summary and new chat
542
+ const summaryText = 'This is a summary.';
543
+ const splitPoint = findCompressSplitPoint(history, 0.7);
544
+ const historyToKeep = history.slice(splitPoint);
545
+ const newCompressedHistory = [
546
+ { role: 'user', parts: [{ text: 'Mocked env context' }] },
547
+ { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
548
+ { role: 'user', parts: [{ text: summaryText }] },
549
+ {
550
+ role: 'model',
551
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
552
+ },
553
+ ...historyToKeep,
554
+ ];
555
+ const mockNewChat = {
556
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
557
+ };
558
+ client['startChat'] = vi
559
+ .fn()
560
+ .mockResolvedValue(mockNewChat);
561
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
562
+ const newTokenCount = Math.floor(totalChars / 4);
472
563
  // Mock the summary response from the chat
473
564
  mockGenerateContentFn.mockResolvedValue({
474
565
  candidates: [
475
566
  {
476
567
  content: {
477
568
  role: 'model',
478
- parts: [{ text: 'This is a summary.' }],
569
+ parts: [{ text: summaryText }],
479
570
  },
480
571
  },
481
572
  ],
@@ -497,7 +588,7 @@ describe('Gemini Client (client.ts)', () => {
497
588
  it('should not compress across a function call response', async () => {
498
589
  const MOCKED_TOKEN_LIMIT = 1000;
499
590
  vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
500
- mockGetHistory.mockReturnValue([
591
+ const history = [
501
592
  { role: 'user', parts: [{ text: '...history 1...' }] },
502
593
  { role: 'model', parts: [{ text: '...history 2...' }] },
503
594
  { role: 'user', parts: [{ text: '...history 3...' }] },
@@ -514,19 +605,43 @@ describe('Gemini Client (client.ts)', () => {
514
605
  { role: 'model', parts: [{ text: '...history 10...' }] },
515
606
  // Instead we will break here.
516
607
  { role: 'user', parts: [{ text: '...history 10...' }] },
517
- ]);
608
+ ];
609
+ mockGetHistory.mockReturnValue(history);
518
610
  const originalTokenCount = 1000 * 0.7;
519
- const newTokenCount = 100;
520
- vi.mocked(mockContentGenerator.countTokens)
521
- .mockResolvedValueOnce({ totalTokens: originalTokenCount }) // First call for the check
522
- .mockResolvedValueOnce({ totalTokens: newTokenCount }); // Second call for the new history
611
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
612
+ // Mock summary and new chat
613
+ const summaryText = 'This is a summary.';
614
+ const splitPoint = findCompressSplitPoint(history, 0.7); // This should be 10
615
+ expect(splitPoint).toBe(10); // Verify split point logic
616
+ const historyToKeep = history.slice(splitPoint); // Should keep last user message
617
+ expect(historyToKeep).toEqual([
618
+ { role: 'user', parts: [{ text: '...history 10...' }] },
619
+ ]);
620
+ const newCompressedHistory = [
621
+ { role: 'user', parts: [{ text: 'Mocked env context' }] },
622
+ { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
623
+ { role: 'user', parts: [{ text: summaryText }] },
624
+ {
625
+ role: 'model',
626
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
627
+ },
628
+ ...historyToKeep,
629
+ ];
630
+ const mockNewChat = {
631
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
632
+ };
633
+ client['startChat'] = vi
634
+ .fn()
635
+ .mockResolvedValue(mockNewChat);
636
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
637
+ const newTokenCount = Math.floor(totalChars / 4);
523
638
  // Mock the summary response from the chat
524
639
  mockGenerateContentFn.mockResolvedValue({
525
640
  candidates: [
526
641
  {
527
642
  content: {
528
643
  role: 'model',
529
- parts: [{ text: 'This is a summary.' }],
644
+ parts: [{ text: summaryText }],
530
645
  },
531
646
  },
532
647
  ],
@@ -544,35 +659,53 @@ describe('Gemini Client (client.ts)', () => {
544
659
  });
545
660
  // Assert that the chat was reset
546
661
  expect(newChat).not.toBe(initialChat);
547
- // 1. standard start context message
548
- // 2. standard canned user start message
549
- // 3. compressed summary message
550
- // 4. standard canned user summary message
551
- // 5. The last user message (not the last 3 because that would start with a function response)
662
+ // 1. standard start context message (env)
663
+ // 2. standard canned model response
664
+ // 3. compressed summary message (user)
665
+ // 4. standard canned model response
666
+ // 5. The last user message (historyToKeep)
552
667
  expect(newChat.getHistory().length).toEqual(5);
553
668
  });
554
669
  it('should always trigger summarization when force is true, regardless of token count', async () => {
555
- mockGetHistory.mockReturnValue([
556
- { role: 'user', parts: [{ text: '...history...' }] },
557
- ]);
558
- const originalTokenCount = 10; // Well below threshold
559
- const newTokenCount = 5;
560
- vi.mocked(mockContentGenerator.countTokens)
561
- .mockResolvedValueOnce({ totalTokens: originalTokenCount })
562
- .mockResolvedValueOnce({ totalTokens: newTokenCount });
670
+ const history = [{ role: 'user', parts: [{ text: '...history...' }] }];
671
+ mockGetHistory.mockReturnValue(history);
672
+ const originalTokenCount = 100; // Well below threshold, but > estimated new count
673
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
674
+ // Mock summary and new chat
675
+ const summaryText = 'This is a summary.';
676
+ const splitPoint = findCompressSplitPoint(history, 0.7);
677
+ const historyToKeep = history.slice(splitPoint);
678
+ const newCompressedHistory = [
679
+ { role: 'user', parts: [{ text: 'Mocked env context' }] },
680
+ { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
681
+ { role: 'user', parts: [{ text: summaryText }] },
682
+ {
683
+ role: 'model',
684
+ parts: [{ text: 'Got it. Thanks for the additional context!' }],
685
+ },
686
+ ...historyToKeep,
687
+ ];
688
+ const mockNewChat = {
689
+ getHistory: vi.fn().mockReturnValue(newCompressedHistory),
690
+ };
691
+ client['startChat'] = vi
692
+ .fn()
693
+ .mockResolvedValue(mockNewChat);
694
+ const totalChars = newCompressedHistory.reduce((total, content) => total + JSON.stringify(content).length, 0);
695
+ const newTokenCount = Math.floor(totalChars / 4);
563
696
  // Mock the summary response from the chat
564
697
  mockGenerateContentFn.mockResolvedValue({
565
698
  candidates: [
566
699
  {
567
700
  content: {
568
701
  role: 'model',
569
- parts: [{ text: 'This is a summary.' }],
702
+ parts: [{ text: summaryText }],
570
703
  },
571
704
  },
572
705
  ],
573
706
  });
574
707
  const initialChat = client.getChat();
575
- const result = await client.tryCompressChat('prompt-id-1', false); // force = true
708
+ const result = await client.tryCompressChat('prompt-id-1', true); // force = true
576
709
  const newChat = client.getChat();
577
710
  expect(mockGenerateContentFn).toHaveBeenCalled();
578
711
  expect(result).toEqual({
@@ -610,9 +743,6 @@ describe('Gemini Client (client.ts)', () => {
610
743
  compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
611
744
  },
612
745
  { compressionStatus: CompressionStatus.NOOP },
613
- {
614
- compressionStatus: CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR,
615
- },
616
746
  ])('does not emit a compression event when the status is $compressionStatus', async ({ compressionStatus }) => {
617
747
  // Arrange
618
748
  const mockStream = (async function* () {
@@ -870,6 +1000,7 @@ ${JSON.stringify({
870
1000
  expect(finalResult).toBeInstanceOf(Turn);
871
1001
  });
872
1002
  it('should stop infinite loop after MAX_TURNS when nextSpeaker always returns model', async () => {
1003
+ vi.spyOn(client['config'], 'getContinueOnFailedApiCall').mockReturnValue(true);
873
1004
  // Get the mocked checkNextSpeaker function and configure it to trigger infinite loop
874
1005
  const { checkNextSpeaker } = await import('../utils/nextSpeakerChecker.js');
875
1006
  const mockCheckNextSpeaker = vi.mocked(checkNextSpeaker);
@@ -1025,6 +1156,82 @@ ${JSON.stringify({
1025
1156
  console.log(`Infinite loop protection working: checkNextSpeaker called ${callCount} times, ` +
1026
1157
  `${eventCount} events generated (properly bounded by MAX_TURNS)`);
1027
1158
  });
1159
+ it('should yield ContextWindowWillOverflow when the context window is about to overflow', async () => {
1160
+ // Arrange
1161
+ const MOCKED_TOKEN_LIMIT = 1000;
1162
+ vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
1163
+ // Set last prompt token count
1164
+ const lastPromptTokenCount = 900;
1165
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(lastPromptTokenCount);
1166
+ // Remaining = 100. Threshold (95%) = 95.
1167
+ // We need a request > 95 tokens.
1168
+ // A string of length 400 is roughly 100 tokens.
1169
+ const longText = 'a'.repeat(400);
1170
+ const request = [{ text: longText }];
1171
+ const estimatedRequestTokenCount = Math.floor(JSON.stringify(request).length / 4);
1172
+ const remainingTokenCount = MOCKED_TOKEN_LIMIT - lastPromptTokenCount;
1173
+ // Mock tryCompressChat to not compress
1174
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
1175
+ originalTokenCount: lastPromptTokenCount,
1176
+ newTokenCount: lastPromptTokenCount,
1177
+ compressionStatus: CompressionStatus.NOOP,
1178
+ });
1179
+ // Act
1180
+ const stream = client.sendMessageStream(request, new AbortController().signal, 'prompt-id-overflow');
1181
+ const events = await fromAsync(stream);
1182
+ // Assert
1183
+ expect(events).toContainEqual({
1184
+ type: GeminiEventType.ContextWindowWillOverflow,
1185
+ value: {
1186
+ estimatedRequestTokenCount,
1187
+ remainingTokenCount,
1188
+ },
1189
+ });
1190
+ // Ensure turn.run is not called
1191
+ expect(mockTurnRunFn).not.toHaveBeenCalled();
1192
+ });
1193
+ it("should use the sticky model's token limit for the overflow check", async () => {
1194
+ // Arrange
1195
+ const STICKY_MODEL = 'gemini-1.5-flash';
1196
+ const STICKY_MODEL_LIMIT = 1000;
1197
+ const CONFIG_MODEL_LIMIT = 2000;
1198
+ // Set up token limits
1199
+ vi.mocked(tokenLimit).mockImplementation((model) => {
1200
+ if (model === STICKY_MODEL)
1201
+ return STICKY_MODEL_LIMIT;
1202
+ return CONFIG_MODEL_LIMIT;
1203
+ });
1204
+ // Set the sticky model
1205
+ client['currentSequenceModel'] = STICKY_MODEL;
1206
+ // Set token count
1207
+ const lastPromptTokenCount = 900;
1208
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(lastPromptTokenCount);
1209
+ // Remaining (sticky) = 100. Threshold (95%) = 95.
1210
+ // We need a request > 95 tokens.
1211
+ const longText = 'a'.repeat(400);
1212
+ const request = [{ text: longText }];
1213
+ const estimatedRequestTokenCount = Math.floor(JSON.stringify(request).length / 4);
1214
+ const remainingTokenCount = STICKY_MODEL_LIMIT - lastPromptTokenCount;
1215
+ vi.spyOn(client, 'tryCompressChat').mockResolvedValue({
1216
+ originalTokenCount: lastPromptTokenCount,
1217
+ newTokenCount: lastPromptTokenCount,
1218
+ compressionStatus: CompressionStatus.NOOP,
1219
+ });
1220
+ // Act
1221
+ const stream = client.sendMessageStream(request, new AbortController().signal, 'test-session-id');
1222
+ const events = await fromAsync(stream);
1223
+ // Assert
1224
+ // Should overflow based on the sticky model's limit
1225
+ expect(events).toContainEqual({
1226
+ type: GeminiEventType.ContextWindowWillOverflow,
1227
+ value: {
1228
+ estimatedRequestTokenCount,
1229
+ remainingTokenCount,
1230
+ },
1231
+ });
1232
+ expect(tokenLimit).toHaveBeenCalledWith(STICKY_MODEL);
1233
+ expect(mockTurnRunFn).not.toHaveBeenCalled();
1234
+ });
1028
1235
  describe('Model Routing', () => {
1029
1236
  let mockRouterService;
1030
1237
  beforeEach(() => {
@@ -1110,6 +1317,89 @@ ${JSON.stringify({
1110
1317
  [{ text: 'Continue' }], expect.any(Object));
1111
1318
  });
1112
1319
  });
1320
+ it('should recursively call sendMessageStream with "Please continue." when InvalidStream event is received', async () => {
1321
+ vi.spyOn(client['config'], 'getContinueOnFailedApiCall').mockReturnValue(true);
1322
+ // Arrange
1323
+ const mockStream1 = (async function* () {
1324
+ yield { type: GeminiEventType.InvalidStream };
1325
+ })();
1326
+ const mockStream2 = (async function* () {
1327
+ yield { type: GeminiEventType.Content, value: 'Continued content' };
1328
+ })();
1329
+ mockTurnRunFn
1330
+ .mockReturnValueOnce(mockStream1)
1331
+ .mockReturnValueOnce(mockStream2);
1332
+ const mockChat = {
1333
+ addHistory: vi.fn(),
1334
+ getHistory: vi.fn().mockReturnValue([]),
1335
+ };
1336
+ client['chat'] = mockChat;
1337
+ const initialRequest = [{ text: 'Hi' }];
1338
+ const promptId = 'prompt-id-invalid-stream';
1339
+ const signal = new AbortController().signal;
1340
+ // Act
1341
+ const stream = client.sendMessageStream(initialRequest, signal, promptId);
1342
+ const events = await fromAsync(stream);
1343
+ // Assert
1344
+ expect(events).toEqual([
1345
+ { type: GeminiEventType.InvalidStream },
1346
+ { type: GeminiEventType.Content, value: 'Continued content' },
1347
+ ]);
1348
+ // Verify that turn.run was called twice
1349
+ expect(mockTurnRunFn).toHaveBeenCalledTimes(2);
1350
+ // First call with original request
1351
+ expect(mockTurnRunFn).toHaveBeenNthCalledWith(1, expect.any(String), initialRequest, expect.any(Object));
1352
+ // Second call with "Please continue."
1353
+ expect(mockTurnRunFn).toHaveBeenNthCalledWith(2, expect.any(String), [{ text: 'System: Please continue.' }], expect.any(Object));
1354
+ });
1355
+ it('should not recursively call sendMessageStream with "Please continue." when InvalidStream event is received and flag is false', async () => {
1356
+ vi.spyOn(client['config'], 'getContinueOnFailedApiCall').mockReturnValue(false);
1357
+ // Arrange
1358
+ const mockStream1 = (async function* () {
1359
+ yield { type: GeminiEventType.InvalidStream };
1360
+ })();
1361
+ mockTurnRunFn.mockReturnValueOnce(mockStream1);
1362
+ const mockChat = {
1363
+ addHistory: vi.fn(),
1364
+ getHistory: vi.fn().mockReturnValue([]),
1365
+ };
1366
+ client['chat'] = mockChat;
1367
+ const initialRequest = [{ text: 'Hi' }];
1368
+ const promptId = 'prompt-id-invalid-stream';
1369
+ const signal = new AbortController().signal;
1370
+ // Act
1371
+ const stream = client.sendMessageStream(initialRequest, signal, promptId);
1372
+ const events = await fromAsync(stream);
1373
+ // Assert
1374
+ expect(events).toEqual([{ type: GeminiEventType.InvalidStream }]);
1375
+ // Verify that turn.run was called only once
1376
+ expect(mockTurnRunFn).toHaveBeenCalledTimes(1);
1377
+ });
1378
+ it('should stop recursing after one retry when InvalidStream events are repeatedly received', async () => {
1379
+ vi.spyOn(client['config'], 'getContinueOnFailedApiCall').mockReturnValue(true);
1380
+ // Arrange
1381
+ // Always return a new invalid stream
1382
+ mockTurnRunFn.mockImplementation(() => (async function* () {
1383
+ yield { type: GeminiEventType.InvalidStream };
1384
+ })());
1385
+ const mockChat = {
1386
+ addHistory: vi.fn(),
1387
+ getHistory: vi.fn().mockReturnValue([]),
1388
+ };
1389
+ client['chat'] = mockChat;
1390
+ const initialRequest = [{ text: 'Hi' }];
1391
+ const promptId = 'prompt-id-infinite-invalid-stream';
1392
+ const signal = new AbortController().signal;
1393
+ // Act
1394
+ const stream = client.sendMessageStream(initialRequest, signal, promptId);
1395
+ const events = await fromAsync(stream);
1396
+ // Assert
1397
+ // We expect 2 InvalidStream events (original + 1 retry)
1398
+ expect(events.length).toBe(2);
1399
+ expect(events.every((e) => e.type === GeminiEventType.InvalidStream)).toBe(true);
1400
+ // Verify that turn.run was called twice
1401
+ expect(mockTurnRunFn).toHaveBeenCalledTimes(2);
1402
+ });
1113
1403
  describe('Editor context delta', () => {
1114
1404
  const mockStream = (async function* () {
1115
1405
  yield { type: 'content', value: 'Hello' };
@@ -1678,7 +1968,7 @@ ${JSON.stringify({
1678
1968
  model: DEFAULT_GEMINI_FLASH_MODEL,
1679
1969
  config: {
1680
1970
  abortSignal,
1681
- systemInstruction: getCoreSystemPrompt(''),
1971
+ systemInstruction: getCoreSystemPrompt({}, ''),
1682
1972
  temperature: 0.5,
1683
1973
  topP: 1,
1684
1974
  },