@google/gemini-cli-core 0.9.0-nightly.20250926.1487841d → 0.9.0-nightly.20251001.33269bdb

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 (164) hide show
  1. package/dist/src/agents/codebase-investigator.d.ts +11 -0
  2. package/dist/src/agents/codebase-investigator.js +164 -0
  3. package/dist/src/agents/codebase-investigator.js.map +1 -0
  4. package/dist/src/agents/executor.d.ts +96 -0
  5. package/dist/src/agents/executor.js +438 -0
  6. package/dist/src/agents/executor.js.map +1 -0
  7. package/dist/src/agents/executor.test.d.ts +6 -0
  8. package/dist/src/agents/executor.test.js +492 -0
  9. package/dist/src/agents/executor.test.js.map +1 -0
  10. package/dist/src/agents/invocation.d.ts +45 -0
  11. package/dist/src/agents/invocation.js +101 -0
  12. package/dist/src/agents/invocation.js.map +1 -0
  13. package/dist/src/agents/invocation.test.d.ts +6 -0
  14. package/dist/src/agents/invocation.test.js +214 -0
  15. package/dist/src/agents/invocation.test.js.map +1 -0
  16. package/dist/src/agents/registry.d.ts +35 -0
  17. package/dist/src/agents/registry.js +58 -0
  18. package/dist/src/agents/registry.js.map +1 -0
  19. package/dist/src/agents/registry.test.d.ts +6 -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 +38 -0
  29. package/dist/src/agents/subagent-tool-wrapper.js +48 -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 +112 -0
  33. package/dist/src/agents/subagent-tool-wrapper.test.js.map +1 -0
  34. package/dist/src/agents/types.d.ts +123 -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/config/config.d.ts +11 -2
  44. package/dist/src/config/config.js +49 -3
  45. package/dist/src/config/config.js.map +1 -1
  46. package/dist/src/config/config.test.js +63 -0
  47. package/dist/src/config/config.test.js.map +1 -1
  48. package/dist/src/core/baseLlmClient.js +19 -21
  49. package/dist/src/core/baseLlmClient.js.map +1 -1
  50. package/dist/src/core/baseLlmClient.test.js +57 -17
  51. package/dist/src/core/baseLlmClient.test.js.map +1 -1
  52. package/dist/src/core/client.js +8 -29
  53. package/dist/src/core/client.js.map +1 -1
  54. package/dist/src/core/client.test.js +223 -94
  55. package/dist/src/core/client.test.js.map +1 -1
  56. package/dist/src/core/coreToolScheduler.test.js +33 -23
  57. package/dist/src/core/coreToolScheduler.test.js.map +1 -1
  58. package/dist/src/core/geminiChat.js +1 -1
  59. package/dist/src/core/geminiChat.js.map +1 -1
  60. package/dist/src/core/geminiChat.test.js +2 -1
  61. package/dist/src/core/geminiChat.test.js.map +1 -1
  62. package/dist/src/core/nonInteractiveToolExecutor.test.js +11 -11
  63. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
  64. package/dist/src/generated/git-commit.d.ts +2 -2
  65. package/dist/src/generated/git-commit.js +2 -2
  66. package/dist/src/ide/detect-ide.d.ts +1 -0
  67. package/dist/src/ide/detect-ide.js +4 -1
  68. package/dist/src/ide/detect-ide.js.map +1 -1
  69. package/dist/src/mcp/sa-impersonation-provider.d.ts +33 -0
  70. package/dist/src/mcp/sa-impersonation-provider.js +130 -0
  71. package/dist/src/mcp/sa-impersonation-provider.js.map +1 -0
  72. package/dist/src/mcp/sa-impersonation-provider.test.d.ts +6 -0
  73. package/dist/src/mcp/sa-impersonation-provider.test.js +117 -0
  74. package/dist/src/mcp/sa-impersonation-provider.test.js.map +1 -0
  75. package/dist/src/services/fileSystemService.d.ts +9 -0
  76. package/dist/src/services/fileSystemService.js +11 -0
  77. package/dist/src/services/fileSystemService.js.map +1 -1
  78. package/dist/src/services/shellExecutionService.d.ts +2 -0
  79. package/dist/src/services/shellExecutionService.js +48 -7
  80. package/dist/src/services/shellExecutionService.js.map +1 -1
  81. package/dist/src/services/shellExecutionService.test.js +13 -4
  82. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  83. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +4 -2
  84. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +15 -1
  85. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  86. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +18 -0
  87. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  88. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +2 -1
  89. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +2 -0
  90. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  91. package/dist/src/telemetry/constants.d.ts +1 -24
  92. package/dist/src/telemetry/constants.js +1 -25
  93. package/dist/src/telemetry/constants.js.map +1 -1
  94. package/dist/src/telemetry/index.d.ts +1 -1
  95. package/dist/src/telemetry/index.js +7 -1
  96. package/dist/src/telemetry/index.js.map +1 -1
  97. package/dist/src/telemetry/loggers.d.ts +2 -1
  98. package/dist/src/telemetry/loggers.js +66 -11
  99. package/dist/src/telemetry/loggers.js.map +1 -1
  100. package/dist/src/telemetry/loggers.test.circular.js +3 -3
  101. package/dist/src/telemetry/loggers.test.circular.js.map +1 -1
  102. package/dist/src/telemetry/loggers.test.js +76 -9
  103. package/dist/src/telemetry/loggers.test.js.map +1 -1
  104. package/dist/src/telemetry/metrics.d.ts +363 -19
  105. package/dist/src/telemetry/metrics.js +415 -235
  106. package/dist/src/telemetry/metrics.js.map +1 -1
  107. package/dist/src/telemetry/metrics.test.js +352 -54
  108. package/dist/src/telemetry/metrics.test.js.map +1 -1
  109. package/dist/src/telemetry/types.d.ts +6 -0
  110. package/dist/src/telemetry/types.js +10 -0
  111. package/dist/src/telemetry/types.js.map +1 -1
  112. package/dist/src/telemetry/uiTelemetry.test.js +2 -2
  113. package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
  114. package/dist/src/test-utils/mock-tool.d.ts +28 -3
  115. package/dist/src/test-utils/mock-tool.js +71 -1
  116. package/dist/src/test-utils/mock-tool.js.map +1 -1
  117. package/dist/src/tools/glob.js +2 -1
  118. package/dist/src/tools/glob.js.map +1 -1
  119. package/dist/src/tools/mcp-client.js +16 -0
  120. package/dist/src/tools/mcp-client.js.map +1 -1
  121. package/dist/src/tools/shell.js +1 -54
  122. package/dist/src/tools/shell.js.map +1 -1
  123. package/dist/src/tools/shell.test.js +0 -1
  124. package/dist/src/tools/shell.test.js.map +1 -1
  125. package/dist/src/tools/smart-edit.d.ts +20 -1
  126. package/dist/src/tools/smart-edit.js +114 -4
  127. package/dist/src/tools/smart-edit.js.map +1 -1
  128. package/dist/src/tools/smart-edit.test.js +90 -6
  129. package/dist/src/tools/smart-edit.test.js.map +1 -1
  130. package/dist/src/tools/tool-names.d.ts +6 -0
  131. package/dist/src/tools/tool-names.js +15 -0
  132. package/dist/src/tools/tool-names.js.map +1 -0
  133. package/dist/src/tools/tool-registry.test.js +10 -10
  134. package/dist/src/tools/tool-registry.test.js.map +1 -1
  135. package/dist/src/utils/flashFallback.test.js +2 -2
  136. package/dist/src/utils/flashFallback.test.js.map +1 -1
  137. package/dist/src/utils/llm-edit-fixer.js +10 -1
  138. package/dist/src/utils/llm-edit-fixer.js.map +1 -1
  139. package/dist/src/utils/llm-edit-fixer.test.js +81 -0
  140. package/dist/src/utils/llm-edit-fixer.test.js.map +1 -1
  141. package/dist/src/utils/memoryImportProcessor.js +13 -20
  142. package/dist/src/utils/memoryImportProcessor.js.map +1 -1
  143. package/dist/src/utils/memoryImportProcessor.test.js +14 -0
  144. package/dist/src/utils/memoryImportProcessor.test.js.map +1 -1
  145. package/dist/src/utils/retry.d.ts +3 -1
  146. package/dist/src/utils/retry.js +13 -4
  147. package/dist/src/utils/retry.js.map +1 -1
  148. package/dist/src/utils/retry.test.js +2 -2
  149. package/dist/src/utils/retry.test.js.map +1 -1
  150. package/dist/src/utils/shell-utils.d.ts +0 -1
  151. package/dist/src/utils/shell-utils.js +1 -1
  152. package/dist/src/utils/shell-utils.js.map +1 -1
  153. package/dist/src/utils/terminalSerializer.d.ts +1 -4
  154. package/dist/src/utils/terminalSerializer.js +3 -3
  155. package/dist/src/utils/terminalSerializer.js.map +1 -1
  156. package/dist/src/utils/tool-utils.js +2 -2
  157. package/dist/src/utils/tool-utils.js.map +1 -1
  158. package/dist/src/utils/tool-utils.test.js +0 -8
  159. package/dist/src/utils/tool-utils.test.js.map +1 -1
  160. package/dist/tsconfig.tsbuildinfo +1 -1
  161. package/package.json +1 -1
  162. package/dist/src/test-utils/tools.d.ts +0 -45
  163. package/dist/src/test-utils/tools.js +0 -105
  164. 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)
@@ -324,74 +324,128 @@ describe('Gemini Client (client.ts)', () => {
324
324
  function setup({ chatHistory = [
325
325
  { role: 'user', parts: [{ text: 'Long conversation' }] },
326
326
  { role: 'model', parts: [{ text: 'Long response' }] },
327
- ], } = {}) {
328
- const mockChat = {
329
- getHistory: vi.fn().mockReturnValue(chatHistory),
327
+ ], originalTokenCount = 1000, summaryText = 'This is a summary.', } = {}) {
328
+ const mockOriginalChat = {
329
+ getHistory: vi.fn((_curated) => chatHistory),
330
330
  setHistory: vi.fn(),
331
331
  };
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 };
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
+ };
338
385
  }
339
386
  describe('when compression inflates the token count', () => {
340
387
  it('allows compression to be forced/manual after a failure', async () => {
341
- const { client } = setup();
342
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
343
- totalTokens: 1000,
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,
344
393
  });
394
+ expect(inflatedTokenCount).toBeGreaterThan(100); // Ensure setup is correct
345
395
  await client.tryCompressChat('prompt-id-4', false); // Fails
346
- const result = await client.tryCompressChat('prompt-id-4', true);
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
347
404
  expect(result).toEqual({
348
405
  compressionStatus: CompressionStatus.COMPRESSED,
349
- newTokenCount: 1000,
350
- originalTokenCount: 1000,
406
+ newTokenCount: compressedTokenCount,
407
+ originalTokenCount: 100,
351
408
  });
352
409
  });
353
410
  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,
411
+ const longSummary = 'long summary '.repeat(100);
412
+ const { client, estimatedNewTokenCount } = setup({
413
+ originalTokenCount: 100,
414
+ summaryText: longSummary,
357
415
  });
416
+ expect(estimatedNewTokenCount).toBeGreaterThan(100); // Ensure setup is correct
358
417
  const result = await client.tryCompressChat('prompt-id-4', false);
359
418
  expect(result).toEqual({
360
419
  compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
361
- newTokenCount: 5000,
362
- originalTokenCount: 1000,
420
+ newTokenCount: estimatedNewTokenCount,
421
+ originalTokenCount: 100,
363
422
  });
364
- expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledWith(5000);
365
- expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledTimes(1);
423
+ // IMPORTANT: The change in client.ts means setLastPromptTokenCount is NOT called on failure
424
+ expect(uiTelemetryService.setLastPromptTokenCount).not.toHaveBeenCalled();
366
425
  });
367
426
  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,
427
+ const longSummary = 'long summary '.repeat(100);
428
+ const { client, mockOriginalChat, estimatedNewTokenCount } = setup({
429
+ originalTokenCount: 100,
430
+ summaryText: longSummary,
376
431
  });
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,
384
- });
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);
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);
388
436
  });
389
437
  it('will not attempt to compress context after a failure', async () => {
390
- const { client } = setup();
391
- await client.tryCompressChat('prompt-id-4', false);
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
392
446
  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);
447
+ // generateContent (for summary) should only have been called once
448
+ expect(mockGenerateContentFn).toHaveBeenCalledTimes(1);
395
449
  expect(result).toEqual({
396
450
  compressionStatus: CompressionStatus.NOOP,
397
451
  newTokenCount: 0,
@@ -405,17 +459,16 @@ describe('Gemini Client (client.ts)', () => {
405
459
  mockGetHistory.mockReturnValue([
406
460
  { role: 'user', parts: [{ text: '...history...' }] },
407
461
  ]);
408
- vi.mocked(mockContentGenerator.countTokens).mockResolvedValue({
409
- totalTokens: MOCKED_TOKEN_LIMIT * 0.699, // TOKEN_THRESHOLD_FOR_SUMMARIZATION = 0.7
410
- });
462
+ const originalTokenCount = MOCKED_TOKEN_LIMIT * 0.699;
463
+ vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(originalTokenCount);
411
464
  const initialChat = client.getChat();
412
465
  const result = await client.tryCompressChat('prompt-id-2', false);
413
466
  const newChat = client.getChat();
414
467
  expect(tokenLimit).toHaveBeenCalled();
415
468
  expect(result).toEqual({
416
469
  compressionStatus: CompressionStatus.NOOP,
417
- newTokenCount: 699,
418
- originalTokenCount: 699,
470
+ newTokenCount: originalTokenCount,
471
+ originalTokenCount,
419
472
  });
420
473
  expect(newChat).toBe(initialChat);
421
474
  });
@@ -427,21 +480,40 @@ describe('Gemini Client (client.ts)', () => {
427
480
  vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
428
481
  contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
429
482
  });
430
- mockGetHistory.mockReturnValue([
431
- { role: 'user', parts: [{ text: '...history...' }] },
432
- ]);
483
+ const history = [{ role: 'user', parts: [{ text: '...history...' }] }];
484
+ mockGetHistory.mockReturnValue(history);
433
485
  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
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);
438
510
  // Mock the summary response from the chat
439
511
  mockGenerateContentFn.mockResolvedValue({
440
512
  candidates: [
441
513
  {
442
514
  content: {
443
515
  role: 'model',
444
- parts: [{ text: 'This is a summary.' }],
516
+ parts: [{ text: summaryText }],
445
517
  },
446
518
  },
447
519
  ],
@@ -461,21 +533,39 @@ describe('Gemini Client (client.ts)', () => {
461
533
  vi.spyOn(client['config'], 'getChatCompression').mockReturnValue({
462
534
  contextPercentageThreshold: MOCKED_CONTEXT_PERCENTAGE_THRESHOLD,
463
535
  });
464
- mockGetHistory.mockReturnValue([
465
- { role: 'user', parts: [{ text: '...history...' }] },
466
- ]);
536
+ const history = [{ role: 'user', parts: [{ text: '...history...' }] }];
537
+ mockGetHistory.mockReturnValue(history);
467
538
  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
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);
472
562
  // Mock the summary response from the chat
473
563
  mockGenerateContentFn.mockResolvedValue({
474
564
  candidates: [
475
565
  {
476
566
  content: {
477
567
  role: 'model',
478
- parts: [{ text: 'This is a summary.' }],
568
+ parts: [{ text: summaryText }],
479
569
  },
480
570
  },
481
571
  ],
@@ -497,7 +587,7 @@ describe('Gemini Client (client.ts)', () => {
497
587
  it('should not compress across a function call response', async () => {
498
588
  const MOCKED_TOKEN_LIMIT = 1000;
499
589
  vi.mocked(tokenLimit).mockReturnValue(MOCKED_TOKEN_LIMIT);
500
- mockGetHistory.mockReturnValue([
590
+ const history = [
501
591
  { role: 'user', parts: [{ text: '...history 1...' }] },
502
592
  { role: 'model', parts: [{ text: '...history 2...' }] },
503
593
  { role: 'user', parts: [{ text: '...history 3...' }] },
@@ -514,19 +604,43 @@ describe('Gemini Client (client.ts)', () => {
514
604
  { role: 'model', parts: [{ text: '...history 10...' }] },
515
605
  // Instead we will break here.
516
606
  { role: 'user', parts: [{ text: '...history 10...' }] },
517
- ]);
607
+ ];
608
+ mockGetHistory.mockReturnValue(history);
518
609
  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
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);
523
637
  // Mock the summary response from the chat
524
638
  mockGenerateContentFn.mockResolvedValue({
525
639
  candidates: [
526
640
  {
527
641
  content: {
528
642
  role: 'model',
529
- parts: [{ text: 'This is a summary.' }],
643
+ parts: [{ text: summaryText }],
530
644
  },
531
645
  },
532
646
  ],
@@ -544,35 +658,53 @@ describe('Gemini Client (client.ts)', () => {
544
658
  });
545
659
  // Assert that the chat was reset
546
660
  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)
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)
552
666
  expect(newChat.getHistory().length).toEqual(5);
553
667
  });
554
668
  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 });
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);
563
695
  // Mock the summary response from the chat
564
696
  mockGenerateContentFn.mockResolvedValue({
565
697
  candidates: [
566
698
  {
567
699
  content: {
568
700
  role: 'model',
569
- parts: [{ text: 'This is a summary.' }],
701
+ parts: [{ text: summaryText }],
570
702
  },
571
703
  },
572
704
  ],
573
705
  });
574
706
  const initialChat = client.getChat();
575
- const result = await client.tryCompressChat('prompt-id-1', false); // force = true
707
+ const result = await client.tryCompressChat('prompt-id-1', true); // force = true
576
708
  const newChat = client.getChat();
577
709
  expect(mockGenerateContentFn).toHaveBeenCalled();
578
710
  expect(result).toEqual({
@@ -610,9 +742,6 @@ describe('Gemini Client (client.ts)', () => {
610
742
  compressionStatus: CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
611
743
  },
612
744
  { compressionStatus: CompressionStatus.NOOP },
613
- {
614
- compressionStatus: CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR,
615
- },
616
745
  ])('does not emit a compression event when the status is $compressionStatus', async ({ compressionStatus }) => {
617
746
  // Arrange
618
747
  const mockStream = (async function* () {