@google/gemini-cli-core 0.56.0 → 0.57.0-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/docs/changelogs/latest.md +224 -3
  2. package/dist/docs/get-started/authentication.mdx +1 -1
  3. package/dist/docs/reference/commands.md +7 -5
  4. package/dist/src/agents/agentLoader.d.ts +18 -18
  5. package/dist/src/agents/cli-help-agent.js +13 -1
  6. package/dist/src/agents/cli-help-agent.js.map +1 -1
  7. package/dist/src/agents/cli-help-agent.test.js +4 -4
  8. package/dist/src/agents/cli-help-agent.test.js.map +1 -1
  9. package/dist/src/agents/registry.js +4 -1
  10. package/dist/src/agents/registry.js.map +1 -1
  11. package/dist/src/agents/registry.test.js +8 -0
  12. package/dist/src/agents/registry.test.js.map +1 -1
  13. package/dist/src/availability/modelAvailabilityService.d.ts +2 -1
  14. package/dist/src/availability/modelAvailabilityService.js +20 -5
  15. package/dist/src/availability/modelAvailabilityService.js.map +1 -1
  16. package/dist/src/availability/modelAvailabilityService.test.js +50 -1
  17. package/dist/src/availability/modelAvailabilityService.test.js.map +1 -1
  18. package/dist/src/config/config.js +5 -2
  19. package/dist/src/config/config.js.map +1 -1
  20. package/dist/src/context/contextManager.js +5 -6
  21. package/dist/src/context/contextManager.js.map +1 -1
  22. package/dist/src/context/contextManager.test.js +17 -0
  23. package/dist/src/context/contextManager.test.js.map +1 -1
  24. package/dist/src/core/geminiChat.d.ts +3 -0
  25. package/dist/src/core/geminiChat.js +65 -12
  26. package/dist/src/core/geminiChat.js.map +1 -1
  27. package/dist/src/core/geminiChat.test.js +233 -22
  28. package/dist/src/core/geminiChat.test.js.map +1 -1
  29. package/dist/src/generated/git-commit.d.ts +2 -2
  30. package/dist/src/generated/git-commit.js +2 -2
  31. package/dist/src/generated/git-commit.js.map +1 -1
  32. package/dist/src/ide/ide-connection-utils.js +47 -10
  33. package/dist/src/ide/ide-connection-utils.js.map +1 -1
  34. package/dist/src/ide/ide-connection-utils.test.js +85 -0
  35. package/dist/src/ide/ide-connection-utils.test.js.map +1 -1
  36. package/dist/src/ide/process-utils.js +5 -2
  37. package/dist/src/ide/process-utils.js.map +1 -1
  38. package/dist/src/ide/process-utils.test.js +13 -1
  39. package/dist/src/ide/process-utils.test.js.map +1 -1
  40. package/dist/src/mcp/oauth-provider.js +2 -2
  41. package/dist/src/mcp/oauth-provider.js.map +1 -1
  42. package/dist/src/mcp/oauth-provider.test.js +70 -0
  43. package/dist/src/mcp/oauth-provider.test.js.map +1 -1
  44. package/dist/src/policy/policy-engine.d.ts +2 -0
  45. package/dist/src/policy/policy-engine.js +56 -0
  46. package/dist/src/policy/policy-engine.js.map +1 -1
  47. package/dist/src/policy/policy-engine.test.js +79 -0
  48. package/dist/src/policy/policy-engine.test.js.map +1 -1
  49. package/dist/src/policy/types.d.ts +4 -0
  50. package/dist/src/policy/types.js.map +1 -1
  51. package/dist/src/services/gitService.js +4 -3
  52. package/dist/src/services/gitService.js.map +1 -1
  53. package/dist/src/services/gitService.test.js +16 -9
  54. package/dist/src/services/gitService.test.js.map +1 -1
  55. package/dist/src/services/shellExecutionService.d.ts +1 -1
  56. package/dist/src/services/shellExecutionService.js +40 -27
  57. package/dist/src/services/shellExecutionService.js.map +1 -1
  58. package/dist/src/services/shellExecutionService.test.js +29 -9
  59. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  60. package/dist/src/services/worktreeService.js +9 -0
  61. package/dist/src/services/worktreeService.js.map +1 -1
  62. package/dist/src/services/worktreeService.test.js +8 -6
  63. package/dist/src/services/worktreeService.test.js.map +1 -1
  64. package/dist/src/skills/builtin/antigravity-support/SKILL.md +55 -18
  65. package/dist/src/skills/builtin/skill-creator/SKILL.md +221 -84
  66. package/dist/src/tools/grep.js +2 -1
  67. package/dist/src/tools/grep.js.map +1 -1
  68. package/dist/src/utils/gitUtils.d.ts +1 -0
  69. package/dist/src/utils/gitUtils.js +34 -0
  70. package/dist/src/utils/gitUtils.js.map +1 -1
  71. package/dist/src/utils/oauth-flow.d.ts +6 -0
  72. package/dist/src/utils/oauth-flow.js +33 -2
  73. package/dist/src/utils/oauth-flow.js.map +1 -1
  74. package/dist/src/utils/oauth-flow.test.js +81 -0
  75. package/dist/src/utils/oauth-flow.test.js.map +1 -1
  76. package/dist/src/utils/retry.js +55 -14
  77. package/dist/src/utils/retry.js.map +1 -1
  78. package/dist/src/utils/retry.test.js +88 -0
  79. package/dist/src/utils/retry.test.js.map +1 -1
  80. package/dist/src/utils/trust.js +3 -0
  81. package/dist/src/utils/trust.js.map +1 -1
  82. package/dist/tsconfig.tsbuildinfo +1 -1
  83. package/package.json +1 -1
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
7
- import { ApiError, ThinkingLevel, } from '@google/genai';
7
+ import { ApiError, ThinkingLevel, Language, } from '@google/genai';
8
8
  import { GeminiChat, InvalidStreamError, StreamEventType, SYNTHETIC_THOUGHT_SIGNATURE, stripToolCallIdPrefixes, coalesceConsecutiveRoles, stripThoughts, } from './geminiChat.js';
9
9
  import { CoreToolCallStatus, } from '../scheduler/types.js';
10
10
  import { MockTool } from '../test-utils/mock-tool.js';
@@ -603,7 +603,7 @@ describe('GeminiChat', () => {
603
603
  expect(modelTurn?.parts?.length).toBe(1);
604
604
  expect(modelTurn?.parts[0].text).toBe('This is the visible text that should not be lost.');
605
605
  });
606
- it('should throw an error when a tool call is followed by an empty stream response', async () => {
606
+ it('should succeed when a tool call is followed by an empty stream response', async () => {
607
607
  // 1. Setup: A history where the model has just made a function call.
608
608
  const initialHistory = [
609
609
  {
@@ -648,12 +648,16 @@ describe('GeminiChat', () => {
648
648
  response: { name: 'Vesuvio' },
649
649
  },
650
650
  }, 'prompt-id-stream-1', new AbortController().signal, LlmRole.MAIN);
651
- // 4. Assert: The stream processing should throw an InvalidStreamError.
651
+ // 4. Assert: The stream processing should succeed.
652
652
  await expect((async () => {
653
653
  for await (const _ of stream) {
654
654
  // This loop consumes the stream to trigger the internal logic.
655
655
  }
656
- })()).rejects.toThrow(InvalidStreamError);
656
+ })()).resolves.not.toThrow();
657
+ // Verify history now ends with a successful model turn (with empty parts)
658
+ const lastTurn = chat.agentHistory.get()[chat.agentHistory.length - 1];
659
+ expect(lastTurn.content.role).toBe('model');
660
+ expect(lastTurn.content.parts).toEqual([]);
657
661
  });
658
662
  it('should succeed when there is a tool call without finish reason', async () => {
659
663
  // Setup: Stream with tool call but no finish reason
@@ -715,7 +719,7 @@ describe('GeminiChat', () => {
715
719
  // Verify history has been rolled back to its initial state
716
720
  expect(chat.agentHistory.length).toBe(initialHistoryLength);
717
721
  });
718
- it('should preserve function responses during rollback when InvalidStreamError is thrown', async () => {
722
+ it('should preserve function responses and matching model turn when empty stream response is received', async () => {
719
723
  // 1. Setup history ending with a model turn containing functionCall
720
724
  chat.agentHistory.push({
721
725
  id: 'model-turn-1',
@@ -732,7 +736,7 @@ describe('GeminiChat', () => {
732
736
  },
733
737
  });
734
738
  const initialHistoryLength = chat.agentHistory.length;
735
- // Setup: Stream that will throw InvalidStreamError
739
+ // Setup: Stream with empty parts
736
740
  const streamWithNoResponseText = (async function* () {
737
741
  yield {
738
742
  candidates: [
@@ -758,11 +762,13 @@ describe('GeminiChat', () => {
758
762
  for await (const _ of stream) {
759
763
  // consume
760
764
  }
761
- })()).rejects.toThrow(InvalidStreamError);
762
- // Verify that history was NOT rolled back, i.e., function response is preserved!
763
- expect(chat.agentHistory.length).toBe(initialHistoryLength + 1);
764
- const lastTurn = chat.agentHistory.get()[chat.agentHistory.length - 1];
765
- expect(lastTurn.content.parts?.[0]?.functionResponse).toBeDefined();
765
+ })()).resolves.not.toThrow();
766
+ // Verify that history contains both the function response and the matched empty model response turn
767
+ expect(chat.agentHistory.length).toBe(initialHistoryLength + 2);
768
+ const turns = chat.agentHistory.get();
769
+ expect(turns[turns.length - 2].content.parts?.[0]?.functionResponse).toBeDefined();
770
+ expect(turns[turns.length - 1].content.role).toBe('model');
771
+ expect(turns[turns.length - 1].content.parts).toEqual([]);
766
772
  });
767
773
  it('should not fuse the next user message into a preserved tool-response turn', async () => {
768
774
  // Regression: when a stream fails mid tool-loop the tool response is
@@ -778,7 +784,7 @@ describe('GeminiChat', () => {
778
784
  parts: [{ functionCall: { name: 'test_tool', args: {} } }],
779
785
  },
780
786
  });
781
- // 1. Tool response goes back, model returns nothing -> InvalidStreamError.
787
+ // 1. Tool response goes back, model returns nothing -> succeeds under Option 2.
782
788
  vi.mocked(mockContentGenerator.generateContentStream).mockResolvedValue((async function* () {
783
789
  yield {
784
790
  candidates: [
@@ -786,7 +792,7 @@ describe('GeminiChat', () => {
786
792
  ],
787
793
  };
788
794
  })());
789
- const failingStream = await chat.sendMessageStream({ model: 'gemini-2.0-flash' }, [
795
+ const successfulStream = await chat.sendMessageStream({ model: 'gemini-2.0-flash' }, [
790
796
  {
791
797
  functionResponse: {
792
798
  name: 'test_tool',
@@ -795,10 +801,10 @@ describe('GeminiChat', () => {
795
801
  },
796
802
  ], 'prompt-id-fusion-setup', new AbortController().signal, LlmRole.MAIN);
797
803
  await expect((async () => {
798
- for await (const _ of failingStream) {
804
+ for await (const _ of successfulStream) {
799
805
  // consume
800
806
  }
801
- })()).rejects.toThrow(InvalidStreamError);
807
+ })()).resolves.not.toThrow();
802
808
  // 2. The user types a brand new instruction.
803
809
  let capturedContents = [];
804
810
  vi.mocked(mockContentGenerator.generateContentStream).mockImplementation(async (req) => {
@@ -937,7 +943,7 @@ describe('GeminiChat', () => {
937
943
  },
938
944
  });
939
945
  const initialHistoryLength = chat.agentHistory.length;
940
- // Setup: Stream that will throw InvalidStreamError
946
+ // Setup: Stream with empty parts
941
947
  const streamWithNoResponseText = (async function* () {
942
948
  yield {
943
949
  candidates: [
@@ -969,12 +975,14 @@ describe('GeminiChat', () => {
969
975
  for await (const _ of stream) {
970
976
  // consume
971
977
  }
972
- })()).rejects.toThrow(InvalidStreamError);
973
- // Verify that history was NOT rolled back, i.e., function response and sibling fileData are preserved!
974
- expect(chat.agentHistory.length).toBe(initialHistoryLength + 1);
975
- const lastTurn = chat.agentHistory.get()[chat.agentHistory.length - 1];
976
- expect(lastTurn.content.parts?.[0]?.functionResponse).toBeDefined();
977
- expect(lastTurn.content.parts?.[1]?.fileData).toBeDefined();
978
+ })()).resolves.not.toThrow();
979
+ // Verify that history contains both the function response and the matched empty model response turn
980
+ expect(chat.agentHistory.length).toBe(initialHistoryLength + 2);
981
+ const turns = chat.agentHistory.get();
982
+ expect(turns[turns.length - 2].content.parts?.[0]?.functionResponse).toBeDefined();
983
+ expect(turns[turns.length - 2].content.parts?.[1]?.fileData).toBeDefined();
984
+ expect(turns[turns.length - 1].content.role).toBe('model');
985
+ expect(turns[turns.length - 1].content.parts).toEqual([]);
978
986
  });
979
987
  it('should restore the lastPromptTokenCount baseline on history rollback when InvalidStreamError is thrown', async () => {
980
988
  // Establish an initial token count baseline
@@ -1201,6 +1209,101 @@ describe('GeminiChat', () => {
1201
1209
  // Verify history has been rolled back to its initial state
1202
1210
  expect(chat.agentHistory.length).toBe(initialHistoryLength);
1203
1211
  });
1212
+ it('should roll back the un-responded user turn from history when stream consumption is broken out of early', async () => {
1213
+ const initialHistoryLength = chat.agentHistory.length;
1214
+ // Setup: A multi-chunk stream that would succeed if fully consumed
1215
+ const activeStream = (async function* () {
1216
+ yield {
1217
+ candidates: [
1218
+ {
1219
+ content: {
1220
+ role: 'model',
1221
+ parts: [{ text: 'chunk 1' }],
1222
+ },
1223
+ },
1224
+ ],
1225
+ };
1226
+ yield {
1227
+ candidates: [
1228
+ {
1229
+ content: {
1230
+ role: 'model',
1231
+ parts: [{ text: 'chunk 2' }],
1232
+ },
1233
+ finishReason: 'STOP',
1234
+ },
1235
+ ],
1236
+ };
1237
+ })();
1238
+ vi.mocked(mockContentGenerator.generateContentStream).mockResolvedValue(activeStream);
1239
+ const stream = await chat.sendMessageStream({ model: 'gemini-2.0-flash' }, 'test early exit message', 'prompt-id-early-exit', new AbortController().signal, LlmRole.MAIN);
1240
+ // Verify the user turn WAS added during sendMessageStream
1241
+ expect(chat.agentHistory.length).toBe(initialHistoryLength + 1);
1242
+ // Consume only the first chunk and break out of the loop early!
1243
+ for await (const chunk of stream) {
1244
+ expect(chunk.type).toBe(StreamEventType.CHUNK);
1245
+ break; // Trigger early exit, calling generator.return() under the hood
1246
+ }
1247
+ // Verify history has been rolled back to its initial state because of the early exit
1248
+ expect(chat.agentHistory.length).toBe(initialHistoryLength);
1249
+ });
1250
+ it('should roll back the entire multi-turn request including function responses when a continuation stream is aborted/cancelled', async () => {
1251
+ const initialHistoryLength = chat.agentHistory.length;
1252
+ const abortController = new AbortController();
1253
+ // 1. Send the first message of the prompt. This will succeed and register prompt-id-multi-turn-abort.
1254
+ const streamFirst = (async function* () {
1255
+ yield {
1256
+ candidates: [
1257
+ {
1258
+ content: {
1259
+ role: 'model',
1260
+ parts: [{ text: 'model first response' }],
1261
+ },
1262
+ finishReason: 'STOP',
1263
+ },
1264
+ ],
1265
+ };
1266
+ })();
1267
+ vi.mocked(mockContentGenerator.generateContentStream).mockResolvedValue(streamFirst);
1268
+ const s1 = await chat.sendMessageStream({ model: 'gemini-2.0-flash' }, 'user original prompt', 'prompt-id-multi-turn-abort', new AbortController().signal, LlmRole.MAIN);
1269
+ for await (const _ of s1) {
1270
+ // consume the stream
1271
+ }
1272
+ // Expect history to contain: user, model
1273
+ expect(chat.agentHistory.length).toBe(initialHistoryLength + 2);
1274
+ // 2. Send a continuation (functionResponse), which is cancelled mid-stream.
1275
+ const streamSecond = (async function* () {
1276
+ yield {
1277
+ candidates: [
1278
+ {
1279
+ content: {
1280
+ role: 'model',
1281
+ parts: [{ text: 'partial model response before abort' }],
1282
+ },
1283
+ },
1284
+ ],
1285
+ };
1286
+ abortController.abort();
1287
+ throw new Error('User aborted a continuation stream.');
1288
+ })();
1289
+ vi.mocked(mockContentGenerator.generateContentStream).mockResolvedValue(streamSecond);
1290
+ const s2 = await chat.sendMessageStream({ model: 'gemini-2.0-flash' }, [
1291
+ {
1292
+ functionResponse: {
1293
+ id: 'call_id_1',
1294
+ name: 'my_tool',
1295
+ response: { result: 'success' },
1296
+ },
1297
+ },
1298
+ ], 'prompt-id-multi-turn-abort', abortController.signal, LlmRole.MAIN);
1299
+ await expect((async () => {
1300
+ for await (const _ of s2) {
1301
+ // consume the stream to trigger abort
1302
+ }
1303
+ })()).rejects.toThrow();
1304
+ // Verify history has been rolled back entirely to initialHistoryLength (before the original prompt started)!
1305
+ expect(chat.agentHistory.length).toBe(initialHistoryLength);
1306
+ });
1204
1307
  it('should roll back the un-responded user turn from history when an ApiError is thrown', async () => {
1205
1308
  const initialHistoryLength = chat.agentHistory.length;
1206
1309
  // Setup: Stream that throws a standard API error
@@ -1646,6 +1749,43 @@ describe('GeminiChat', () => {
1646
1749
  });
1647
1750
  expect(geminiWrite).toBeDefined();
1648
1751
  });
1752
+ it('should accept a completely empty response without throwing NO_RESPONSE_TEXT if the input is a tool response (isOriginalFunctionResponse is true)', async () => {
1753
+ // Mock an empty stream response with no text/thoughts
1754
+ const responseStream = (async function* () {
1755
+ yield {
1756
+ candidates: [
1757
+ {
1758
+ content: {
1759
+ role: 'model',
1760
+ parts: [],
1761
+ },
1762
+ finishReason: 'STOP',
1763
+ },
1764
+ ],
1765
+ };
1766
+ })();
1767
+ vi.mocked(mockContentGenerator.generateContentStream).mockResolvedValue(responseStream);
1768
+ // We send a functionResponse message so that isOriginalFunctionResponse is true
1769
+ const stream = await chat.sendMessageStream({ model: 'test-model' }, [
1770
+ {
1771
+ functionResponse: {
1772
+ name: 'edit_file',
1773
+ response: { success: true },
1774
+ },
1775
+ },
1776
+ ], 'prompt-id-empty-tool-response', new AbortController().signal, LlmRole.MAIN);
1777
+ let error = undefined;
1778
+ try {
1779
+ for await (const _ of stream) {
1780
+ // consume
1781
+ }
1782
+ }
1783
+ catch (err) {
1784
+ error = err;
1785
+ }
1786
+ // It should NOT throw InvalidStreamError NO_RESPONSE_TEXT
1787
+ expect(error).toBeUndefined();
1788
+ });
1649
1789
  });
1650
1790
  describe('addHistory', () => {
1651
1791
  it('should add a new content item to the history', () => {
@@ -3346,5 +3486,76 @@ describe('GeminiChat', () => {
3346
3486
  expect(stripped[0]).toHaveProperty('metadata', { some: 'value' });
3347
3487
  });
3348
3488
  });
3489
+ describe('isValidContent via curated history', () => {
3490
+ it('should strip turns with truly empty text parts and no other keys', () => {
3491
+ const chat = new GeminiChat(mockConfig, '', [], []);
3492
+ chat.setHistory([
3493
+ { role: 'user', parts: [{ text: 'User prompt' }] },
3494
+ { role: 'model', parts: [{ text: '' }] },
3495
+ ]);
3496
+ // Curated history should strip the invalid model turn
3497
+ const history = chat.getHistory(true);
3498
+ expect(history).toHaveLength(1);
3499
+ expect(history[0].role).toBe('user');
3500
+ });
3501
+ it('should preserve turns with empty text parts that contain functionCall', () => {
3502
+ const chat = new GeminiChat(mockConfig, '', [], []);
3503
+ chat.setHistory([
3504
+ { role: 'user', parts: [{ text: 'User prompt' }] },
3505
+ {
3506
+ role: 'model',
3507
+ parts: [
3508
+ {
3509
+ text: '',
3510
+ functionCall: { name: 'test_tool', args: {} },
3511
+ },
3512
+ ],
3513
+ },
3514
+ ]);
3515
+ // Curated history should preserve the valid model turn because of functionCall
3516
+ const history = chat.getHistory(true);
3517
+ expect(history).toHaveLength(2);
3518
+ expect(history[1].role).toBe('model');
3519
+ });
3520
+ it('should preserve turns with empty text parts that contain functionResponse, inlineData, or fileData', () => {
3521
+ const chat = new GeminiChat(mockConfig, '', [], []);
3522
+ chat.setHistory([
3523
+ { role: 'user', parts: [{ text: 'User prompt' }] },
3524
+ {
3525
+ role: 'model',
3526
+ parts: [
3527
+ {
3528
+ text: '',
3529
+ functionResponse: { name: 'test_tool', response: {} },
3530
+ },
3531
+ ],
3532
+ },
3533
+ ]);
3534
+ const history = chat.getHistory(true);
3535
+ expect(history).toHaveLength(2);
3536
+ expect(history[1].role).toBe('model');
3537
+ });
3538
+ it('should preserve turns with empty text parts that contain executableCode or codeExecutionResult', () => {
3539
+ const chat = new GeminiChat(mockConfig, '', [], []);
3540
+ chat.setHistory([
3541
+ { role: 'user', parts: [{ text: 'User prompt' }] },
3542
+ {
3543
+ role: 'model',
3544
+ parts: [
3545
+ {
3546
+ text: '',
3547
+ executableCode: {
3548
+ language: Language.PYTHON,
3549
+ code: 'print("test")',
3550
+ },
3551
+ },
3552
+ ],
3553
+ },
3554
+ ]);
3555
+ const history = chat.getHistory(true);
3556
+ expect(history).toHaveLength(2);
3557
+ expect(history[1].role).toBe('model');
3558
+ });
3559
+ });
3349
3560
  });
3350
3561
  //# sourceMappingURL=geminiChat.test.js.map