@machina.ai/cell-cli-core 1.2.2-rc1 → 1.4.0-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/dist/index.d.ts +3 -1
  2. package/dist/index.js +3 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/package.json +2 -1
  5. package/dist/src/code_assist/codeAssist.d.ts +2 -0
  6. package/dist/src/code_assist/codeAssist.js +12 -0
  7. package/dist/src/code_assist/codeAssist.js.map +1 -1
  8. package/dist/src/code_assist/converter.d.ts +2 -1
  9. package/dist/src/code_assist/converter.js +1 -1
  10. package/dist/src/code_assist/converter.js.map +1 -1
  11. package/dist/src/code_assist/oauth2.js +58 -27
  12. package/dist/src/code_assist/oauth2.js.map +1 -1
  13. package/dist/src/code_assist/oauth2.test.js +252 -0
  14. package/dist/src/code_assist/oauth2.test.js.map +1 -1
  15. package/dist/src/config/config.d.ts +0 -4
  16. package/dist/src/config/config.js +18 -22
  17. package/dist/src/config/config.js.map +1 -1
  18. package/dist/src/config/config.test.js +48 -1
  19. package/dist/src/config/config.test.js.map +1 -1
  20. package/dist/src/core/client.d.ts +4 -2
  21. package/dist/src/core/client.js +8 -8
  22. package/dist/src/core/client.js.map +1 -1
  23. package/dist/src/core/client.test.js +34 -6
  24. package/dist/src/core/client.test.js.map +1 -1
  25. package/dist/src/core/coreToolScheduler.test.js +18 -0
  26. package/dist/src/core/coreToolScheduler.test.js.map +1 -1
  27. package/dist/src/core/geminiChat.d.ts +30 -1
  28. package/dist/src/core/geminiChat.js +131 -25
  29. package/dist/src/core/geminiChat.js.map +1 -1
  30. package/dist/src/core/geminiChat.test.js +202 -23
  31. package/dist/src/core/geminiChat.test.js.map +1 -1
  32. package/dist/src/core/loggingContentGenerator.js +9 -9
  33. package/dist/src/core/loggingContentGenerator.js.map +1 -1
  34. package/dist/src/core/nonInteractiveToolExecutor.test.js +2 -0
  35. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
  36. package/dist/src/core/subagent.js +5 -6
  37. package/dist/src/core/subagent.js.map +1 -1
  38. package/dist/src/core/subagent.test.js +20 -9
  39. package/dist/src/core/subagent.test.js.map +1 -1
  40. package/dist/src/core/turn.d.ts +12 -4
  41. package/dist/src/core/turn.js +18 -3
  42. package/dist/src/core/turn.js.map +1 -1
  43. package/dist/src/core/turn.test.js +256 -153
  44. package/dist/src/core/turn.test.js.map +1 -1
  45. package/dist/src/generated/git-commit.d.ts +2 -2
  46. package/dist/src/generated/git-commit.js +2 -2
  47. package/dist/src/ide/ide-client.d.ts +7 -2
  48. package/dist/src/ide/ide-client.js +33 -13
  49. package/dist/src/ide/ide-client.js.map +1 -1
  50. package/dist/src/ide/ide-installer.js +1 -1
  51. package/dist/src/ide/ide-installer.js.map +1 -1
  52. package/dist/src/ide/ideContext.d.ts +12 -0
  53. package/dist/src/ide/ideContext.js +1 -0
  54. package/dist/src/ide/ideContext.js.map +1 -1
  55. package/dist/src/ide/process-utils.d.ts +0 -1
  56. package/dist/src/ide/process-utils.js +36 -31
  57. package/dist/src/ide/process-utils.js.map +1 -1
  58. package/dist/src/index.d.ts +1 -0
  59. package/dist/src/index.js +1 -0
  60. package/dist/src/index.js.map +1 -1
  61. package/dist/src/mcp/oauth-provider.d.ts +12 -12
  62. package/dist/src/mcp/oauth-provider.js +32 -31
  63. package/dist/src/mcp/oauth-provider.js.map +1 -1
  64. package/dist/src/mcp/oauth-provider.test.js +74 -35
  65. package/dist/src/mcp/oauth-provider.test.js.map +1 -1
  66. package/dist/src/mcp/oauth-token-storage.d.ts +8 -8
  67. package/dist/src/mcp/oauth-token-storage.js +8 -8
  68. package/dist/src/mcp/oauth-token-storage.js.map +1 -1
  69. package/dist/src/mcp/oauth-token-storage.test.js +23 -21
  70. package/dist/src/mcp/oauth-token-storage.test.js.map +1 -1
  71. package/dist/src/mcp/token-storage/hybrid-token-storage.d.ts +23 -0
  72. package/dist/src/mcp/token-storage/hybrid-token-storage.js +78 -0
  73. package/dist/src/mcp/token-storage/hybrid-token-storage.js.map +1 -0
  74. package/dist/src/mcp/token-storage/hybrid-token-storage.test.d.ts +6 -0
  75. package/dist/src/mcp/token-storage/hybrid-token-storage.test.js +193 -0
  76. package/dist/src/mcp/token-storage/hybrid-token-storage.test.js.map +1 -0
  77. package/dist/src/mcp/token-storage/keychain-token-storage.d.ts +31 -0
  78. package/dist/src/mcp/token-storage/keychain-token-storage.js +190 -0
  79. package/dist/src/mcp/token-storage/keychain-token-storage.js.map +1 -0
  80. package/dist/src/mcp/token-storage/keychain-token-storage.test.d.ts +6 -0
  81. package/dist/src/mcp/token-storage/keychain-token-storage.test.js +254 -0
  82. package/dist/src/mcp/token-storage/keychain-token-storage.test.js.map +1 -0
  83. package/dist/src/mcp/token-storage/types.d.ts +4 -0
  84. package/dist/src/mcp/token-storage/types.js +5 -1
  85. package/dist/src/mcp/token-storage/types.js.map +1 -1
  86. package/dist/src/services/chatRecordingService.d.ts +5 -12
  87. package/dist/src/services/chatRecordingService.js +25 -16
  88. package/dist/src/services/chatRecordingService.js.map +1 -1
  89. package/dist/src/services/chatRecordingService.test.js +54 -17
  90. package/dist/src/services/chatRecordingService.test.js.map +1 -1
  91. package/dist/src/services/loopDetectionService.d.ts +1 -0
  92. package/dist/src/services/loopDetectionService.js +19 -1
  93. package/dist/src/services/loopDetectionService.js.map +1 -1
  94. package/dist/src/services/shellExecutionService.js +5 -5
  95. package/dist/src/services/shellExecutionService.js.map +1 -1
  96. package/dist/src/services/shellExecutionService.test.js +1 -1
  97. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  98. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +4 -2
  99. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +31 -2
  100. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  101. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +19 -0
  102. package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
  103. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +6 -1
  104. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +13 -0
  105. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  106. package/dist/src/telemetry/constants.d.ts +1 -0
  107. package/dist/src/telemetry/constants.js +1 -0
  108. package/dist/src/telemetry/constants.js.map +1 -1
  109. package/dist/src/telemetry/loggers.js +26 -1
  110. package/dist/src/telemetry/loggers.js.map +1 -1
  111. package/dist/src/telemetry/loggers.test.js +39 -3
  112. package/dist/src/telemetry/loggers.test.js.map +1 -1
  113. package/dist/src/telemetry/types.d.ts +10 -1
  114. package/dist/src/telemetry/types.js +18 -2
  115. package/dist/src/telemetry/types.js.map +1 -1
  116. package/dist/src/telemetry/uiTelemetry.js +4 -4
  117. package/dist/src/telemetry/uiTelemetry.js.map +1 -1
  118. package/dist/src/telemetry/uiTelemetry.test.js +4 -4
  119. package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
  120. package/dist/src/tools/diffOptions.js +21 -13
  121. package/dist/src/tools/diffOptions.js.map +1 -1
  122. package/dist/src/tools/diffOptions.test.js +58 -22
  123. package/dist/src/tools/diffOptions.test.js.map +1 -1
  124. package/dist/src/tools/edit.js +11 -19
  125. package/dist/src/tools/edit.js.map +1 -1
  126. package/dist/src/tools/edit.test.js +36 -8
  127. package/dist/src/tools/edit.test.js.map +1 -1
  128. package/dist/src/tools/mcp-client.js +31 -26
  129. package/dist/src/tools/mcp-client.js.map +1 -1
  130. package/dist/src/tools/mcp-tool.d.ts +1 -2
  131. package/dist/src/tools/mcp-tool.js +4 -8
  132. package/dist/src/tools/mcp-tool.js.map +1 -1
  133. package/dist/src/tools/mcp-tool.test.js +4 -10
  134. package/dist/src/tools/mcp-tool.test.js.map +1 -1
  135. package/dist/src/tools/read-many-files.test.js +4 -4
  136. package/dist/src/tools/read-many-files.test.js.map +1 -1
  137. package/dist/src/tools/shell.js +6 -3
  138. package/dist/src/tools/shell.js.map +1 -1
  139. package/dist/src/tools/shell.test.js +3 -3
  140. package/dist/src/tools/shell.test.js.map +1 -1
  141. package/dist/src/tools/smart-edit.d.ts +1 -1
  142. package/dist/src/tools/smart-edit.js +3 -3
  143. package/dist/src/tools/smart-edit.js.map +1 -1
  144. package/dist/src/tools/smart-edit.test.js +11 -3
  145. package/dist/src/tools/smart-edit.test.js.map +1 -1
  146. package/dist/src/tools/tools.d.ts +6 -2
  147. package/dist/src/tools/tools.js.map +1 -1
  148. package/dist/src/tools/web-fetch.js +4 -3
  149. package/dist/src/tools/web-fetch.js.map +1 -1
  150. package/dist/src/tools/web-search.d.ts +1 -1
  151. package/dist/src/tools/web-search.js +3 -1
  152. package/dist/src/tools/web-search.js.map +1 -1
  153. package/dist/src/tools/write-file.js +2 -2
  154. package/dist/src/tools/write-file.js.map +1 -1
  155. package/dist/src/tools/write-file.test.js +5 -9
  156. package/dist/src/tools/write-file.test.js.map +1 -1
  157. package/dist/src/utils/fileUtils.js +5 -4
  158. package/dist/src/utils/fileUtils.js.map +1 -1
  159. package/dist/src/utils/fileUtils.test.js +21 -20
  160. package/dist/src/utils/fileUtils.test.js.map +1 -1
  161. package/dist/src/utils/gitIgnoreParser.d.ts +1 -0
  162. package/dist/src/utils/gitIgnoreParser.js +101 -10
  163. package/dist/src/utils/gitIgnoreParser.js.map +1 -1
  164. package/dist/src/utils/gitIgnoreParser.test.js +66 -0
  165. package/dist/src/utils/gitIgnoreParser.test.js.map +1 -1
  166. package/dist/src/utils/ide-trust.d.ts +10 -0
  167. package/dist/src/utils/ide-trust.js +14 -0
  168. package/dist/src/utils/ide-trust.js.map +1 -0
  169. package/dist/src/utils/nextSpeakerChecker.test.js +33 -0
  170. package/dist/src/utils/nextSpeakerChecker.test.js.map +1 -1
  171. package/dist/src/utils/shell-utils.test.js +1 -1
  172. package/dist/src/utils/shell-utils.test.js.map +1 -1
  173. package/dist/tsconfig.tsbuildinfo +1 -1
  174. package/package.json +3 -2
@@ -6,6 +6,7 @@
6
6
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
7
7
  import { Turn, GeminiEventType } from './turn.js';
8
8
  import { reportError } from '../utils/errorReporting.js';
9
+ import { StreamEventType } from './geminiChat.js';
9
10
  const mockSendMessageStream = vi.fn();
10
11
  const mockGetHistory = vi.fn();
11
12
  const mockMaybeIncludeSchemaDepthContext = vi.fn();
@@ -24,6 +25,7 @@ vi.mock('@google/genai', async (importOriginal) => {
24
25
  vi.mock('../utils/errorReporting', () => ({
25
26
  reportError: vi.fn(),
26
27
  }));
28
+ // Use the actual implementation from partUtils now that it's provided.
27
29
  vi.mock('../utils/generateContentResponseUtilities', () => ({
28
30
  getResponseText: (resp) => resp.candidates?.[0]?.content?.parts?.map((part) => part.text).join('') ||
29
31
  undefined,
@@ -55,10 +57,16 @@ describe('Turn', () => {
55
57
  it('should yield content events for text parts', async () => {
56
58
  const mockResponseStream = (async function* () {
57
59
  yield {
58
- candidates: [{ content: { parts: [{ text: 'Hello' }] } }],
60
+ type: StreamEventType.CHUNK,
61
+ value: {
62
+ candidates: [{ content: { parts: [{ text: 'Hello' }] } }],
63
+ },
59
64
  };
60
65
  yield {
61
- candidates: [{ content: { parts: [{ text: ' world' }] } }],
66
+ type: StreamEventType.CHUNK,
67
+ value: {
68
+ candidates: [{ content: { parts: [{ text: ' world' }] } }],
69
+ },
62
70
  };
63
71
  })();
64
72
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
@@ -80,15 +88,22 @@ describe('Turn', () => {
80
88
  it('should yield tool_call_request events for function calls', async () => {
81
89
  const mockResponseStream = (async function* () {
82
90
  yield {
83
- functionCalls: [
84
- {
85
- id: 'fc1',
86
- name: 'tool1',
87
- args: { arg1: 'val1' },
88
- isClientInitiated: false,
89
- },
90
- { name: 'tool2', args: { arg2: 'val2' }, isClientInitiated: false }, // No ID
91
- ],
91
+ type: StreamEventType.CHUNK,
92
+ value: {
93
+ functionCalls: [
94
+ {
95
+ id: 'fc1',
96
+ name: 'tool1',
97
+ args: { arg1: 'val1' },
98
+ isClientInitiated: false,
99
+ },
100
+ {
101
+ name: 'tool2',
102
+ args: { arg2: 'val2' },
103
+ isClientInitiated: false,
104
+ }, // No ID
105
+ ],
106
+ },
92
107
  };
93
108
  })();
94
109
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
@@ -122,17 +137,23 @@ describe('Turn', () => {
122
137
  const abortController = new AbortController();
123
138
  const mockResponseStream = (async function* () {
124
139
  yield {
125
- candidates: [{ content: { parts: [{ text: 'First part' }] } }],
140
+ type: StreamEventType.CHUNK,
141
+ value: {
142
+ candidates: [{ content: { parts: [{ text: 'First part' }] } }],
143
+ },
126
144
  };
127
145
  abortController.abort();
128
146
  yield {
129
- candidates: [
130
- {
131
- content: {
132
- parts: [{ text: 'Second part - should not be processed' }],
147
+ type: StreamEventType.CHUNK,
148
+ value: {
149
+ candidates: [
150
+ {
151
+ content: {
152
+ parts: [{ text: 'Second part - should not be processed' }],
153
+ },
133
154
  },
134
- },
135
- ],
155
+ ],
156
+ },
136
157
  };
137
158
  })();
138
159
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
@@ -172,84 +193,103 @@ describe('Turn', () => {
172
193
  it('should handle function calls with undefined name or args', async () => {
173
194
  const mockResponseStream = (async function* () {
174
195
  yield {
175
- functionCalls: [
176
- { id: 'fc1', name: undefined, args: { arg1: 'val1' } },
177
- { id: 'fc2', name: 'tool2', args: undefined },
178
- { id: 'fc3', name: undefined, args: undefined },
179
- ],
196
+ type: StreamEventType.CHUNK,
197
+ value: {
198
+ candidates: [],
199
+ functionCalls: [
200
+ // Add `id` back to the mock to match what the code expects
201
+ { id: 'fc1', name: undefined, args: { arg1: 'val1' } },
202
+ { id: 'fc2', name: 'tool2', args: undefined },
203
+ { id: 'fc3', name: undefined, args: undefined },
204
+ ],
205
+ },
180
206
  };
181
207
  })();
182
208
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
183
209
  const events = [];
184
- const reqParts = [{ text: 'Test undefined tool parts' }];
185
- for await (const event of turn.run(reqParts, new AbortController().signal)) {
210
+ for await (const event of turn.run([{ text: 'Test undefined tool parts' }], new AbortController().signal)) {
186
211
  events.push(event);
187
212
  }
188
213
  expect(events.length).toBe(3);
214
+ // Assertions for each specific tool call event
189
215
  const event1 = events[0];
190
- expect(event1.type).toBe(GeminiEventType.ToolCallRequest);
191
- expect(event1.value).toEqual(expect.objectContaining({
216
+ expect(event1.value).toMatchObject({
192
217
  callId: 'fc1',
193
218
  name: 'undefined_tool_name',
194
219
  args: { arg1: 'val1' },
195
- isClientInitiated: false,
196
- }));
197
- expect(turn.pendingToolCalls[0]).toEqual(event1.value);
220
+ });
198
221
  const event2 = events[1];
199
- expect(event2.type).toBe(GeminiEventType.ToolCallRequest);
200
- expect(event2.value).toEqual(expect.objectContaining({
222
+ expect(event2.value).toMatchObject({
201
223
  callId: 'fc2',
202
224
  name: 'tool2',
203
225
  args: {},
204
- isClientInitiated: false,
205
- }));
206
- expect(turn.pendingToolCalls[1]).toEqual(event2.value);
226
+ });
207
227
  const event3 = events[2];
208
- expect(event3.type).toBe(GeminiEventType.ToolCallRequest);
209
- expect(event3.value).toEqual(expect.objectContaining({
228
+ expect(event3.value).toMatchObject({
210
229
  callId: 'fc3',
211
230
  name: 'undefined_tool_name',
212
231
  args: {},
213
- isClientInitiated: false,
214
- }));
215
- expect(turn.pendingToolCalls[2]).toEqual(event3.value);
216
- expect(turn.getDebugResponses().length).toBe(1);
232
+ });
217
233
  });
218
234
  it('should yield finished event when response has finish reason', async () => {
219
235
  const mockResponseStream = (async function* () {
220
236
  yield {
221
- candidates: [
222
- {
223
- content: { parts: [{ text: 'Partial response' }] },
224
- finishReason: 'STOP',
237
+ type: StreamEventType.CHUNK,
238
+ value: {
239
+ candidates: [
240
+ {
241
+ content: { parts: [{ text: 'Partial response' }] },
242
+ finishReason: 'STOP',
243
+ },
244
+ ],
245
+ usageMetadata: {
246
+ promptTokenCount: 17,
247
+ candidatesTokenCount: 50,
248
+ cachedContentTokenCount: 10,
249
+ thoughtsTokenCount: 5,
250
+ toolUsePromptTokenCount: 2,
225
251
  },
226
- ],
252
+ },
227
253
  };
228
254
  })();
229
255
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
230
256
  const events = [];
231
- const reqParts = [{ text: 'Test finish reason' }];
232
- for await (const event of turn.run(reqParts, new AbortController().signal)) {
257
+ for await (const event of turn.run([{ text: 'Test finish reason' }], new AbortController().signal)) {
233
258
  events.push(event);
234
259
  }
235
260
  expect(events).toEqual([
236
261
  { type: GeminiEventType.Content, value: 'Partial response' },
237
- { type: GeminiEventType.Finished, value: 'STOP' },
262
+ {
263
+ type: GeminiEventType.Finished,
264
+ value: {
265
+ reason: 'STOP',
266
+ usageMetadata: {
267
+ promptTokenCount: 17,
268
+ candidatesTokenCount: 50,
269
+ cachedContentTokenCount: 10,
270
+ thoughtsTokenCount: 5,
271
+ toolUsePromptTokenCount: 2,
272
+ },
273
+ },
274
+ },
238
275
  ]);
239
276
  });
240
277
  it('should yield finished event for MAX_TOKENS finish reason', async () => {
241
278
  const mockResponseStream = (async function* () {
242
279
  yield {
243
- candidates: [
244
- {
245
- content: {
246
- parts: [
247
- { text: 'This is a long response that was cut off...' },
248
- ],
280
+ type: StreamEventType.CHUNK,
281
+ value: {
282
+ candidates: [
283
+ {
284
+ content: {
285
+ parts: [
286
+ { text: 'This is a long response that was cut off...' },
287
+ ],
288
+ },
289
+ finishReason: 'MAX_TOKENS',
249
290
  },
250
- finishReason: 'MAX_TOKENS',
251
- },
252
- ],
291
+ ],
292
+ },
253
293
  };
254
294
  })();
255
295
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
@@ -263,18 +303,24 @@ describe('Turn', () => {
263
303
  type: GeminiEventType.Content,
264
304
  value: 'This is a long response that was cut off...',
265
305
  },
266
- { type: GeminiEventType.Finished, value: 'MAX_TOKENS' },
306
+ {
307
+ type: GeminiEventType.Finished,
308
+ value: { reason: 'MAX_TOKENS', usageMetadata: undefined },
309
+ },
267
310
  ]);
268
311
  });
269
312
  it('should yield finished event for SAFETY finish reason', async () => {
270
313
  const mockResponseStream = (async function* () {
271
314
  yield {
272
- candidates: [
273
- {
274
- content: { parts: [{ text: 'Content blocked' }] },
275
- finishReason: 'SAFETY',
276
- },
277
- ],
315
+ type: StreamEventType.CHUNK,
316
+ value: {
317
+ candidates: [
318
+ {
319
+ content: { parts: [{ text: 'Content blocked' }] },
320
+ finishReason: 'SAFETY',
321
+ },
322
+ ],
323
+ },
278
324
  };
279
325
  })();
280
326
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
@@ -285,18 +331,26 @@ describe('Turn', () => {
285
331
  }
286
332
  expect(events).toEqual([
287
333
  { type: GeminiEventType.Content, value: 'Content blocked' },
288
- { type: GeminiEventType.Finished, value: 'SAFETY' },
334
+ {
335
+ type: GeminiEventType.Finished,
336
+ value: { reason: 'SAFETY', usageMetadata: undefined },
337
+ },
289
338
  ]);
290
339
  });
291
- it('should not yield finished event when there is no finish reason', async () => {
340
+ it('should yield finished event with undefined reason when there is no finish reason', async () => {
292
341
  const mockResponseStream = (async function* () {
293
342
  yield {
294
- candidates: [
295
- {
296
- content: { parts: [{ text: 'Response without finish reason' }] },
297
- // No finishReason property
298
- },
299
- ],
343
+ type: StreamEventType.CHUNK,
344
+ value: {
345
+ candidates: [
346
+ {
347
+ content: {
348
+ parts: [{ text: 'Response without finish reason' }],
349
+ },
350
+ // No finishReason property
351
+ },
352
+ ],
353
+ },
300
354
  };
301
355
  })();
302
356
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
@@ -311,25 +365,30 @@ describe('Turn', () => {
311
365
  value: 'Response without finish reason',
312
366
  },
313
367
  ]);
314
- // No Finished event should be emitted
315
368
  });
316
369
  it('should handle multiple responses with different finish reasons', async () => {
317
370
  const mockResponseStream = (async function* () {
318
371
  yield {
319
- candidates: [
320
- {
321
- content: { parts: [{ text: 'First part' }] },
322
- // No finish reason on first response
323
- },
324
- ],
372
+ type: StreamEventType.CHUNK,
373
+ value: {
374
+ candidates: [
375
+ {
376
+ content: { parts: [{ text: 'First part' }] },
377
+ // No finish reason on first response
378
+ },
379
+ ],
380
+ },
325
381
  };
326
382
  yield {
327
- candidates: [
328
- {
329
- content: { parts: [{ text: 'Second part' }] },
330
- finishReason: 'OTHER',
331
- },
332
- ],
383
+ value: {
384
+ type: StreamEventType.CHUNK,
385
+ candidates: [
386
+ {
387
+ content: { parts: [{ text: 'Second part' }] },
388
+ finishReason: 'OTHER',
389
+ },
390
+ ],
391
+ },
333
392
  };
334
393
  })();
335
394
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
@@ -341,26 +400,32 @@ describe('Turn', () => {
341
400
  expect(events).toEqual([
342
401
  { type: GeminiEventType.Content, value: 'First part' },
343
402
  { type: GeminiEventType.Content, value: 'Second part' },
344
- { type: GeminiEventType.Finished, value: 'OTHER' },
403
+ {
404
+ type: GeminiEventType.Finished,
405
+ value: { reason: 'OTHER', usageMetadata: undefined },
406
+ },
345
407
  ]);
346
408
  });
347
409
  it('should yield citation and finished events when response has citationMetadata', async () => {
348
410
  const mockResponseStream = (async function* () {
349
411
  yield {
350
- candidates: [
351
- {
352
- content: { parts: [{ text: 'Some text.' }] },
353
- citationMetadata: {
354
- citations: [
355
- {
356
- uri: 'https://example.com/source1',
357
- title: 'Source 1 Title',
358
- },
359
- ],
412
+ type: StreamEventType.CHUNK,
413
+ value: {
414
+ candidates: [
415
+ {
416
+ content: { parts: [{ text: 'Some text.' }] },
417
+ citationMetadata: {
418
+ citations: [
419
+ {
420
+ uri: 'https://example.com/source1',
421
+ title: 'Source 1 Title',
422
+ },
423
+ ],
424
+ },
425
+ finishReason: 'STOP',
360
426
  },
361
- finishReason: 'STOP',
362
- },
363
- ],
427
+ ],
428
+ },
364
429
  };
365
430
  })();
366
431
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
@@ -374,30 +439,36 @@ describe('Turn', () => {
374
439
  type: GeminiEventType.Citation,
375
440
  value: 'Citations:\n(Source 1 Title) https://example.com/source1',
376
441
  },
377
- { type: GeminiEventType.Finished, value: 'STOP' },
442
+ {
443
+ type: GeminiEventType.Finished,
444
+ value: { reason: 'STOP', usageMetadata: undefined },
445
+ },
378
446
  ]);
379
447
  });
380
448
  it('should yield a single citation event for multiple citations in one response', async () => {
381
449
  const mockResponseStream = (async function* () {
382
450
  yield {
383
- candidates: [
384
- {
385
- content: { parts: [{ text: 'Some text.' }] },
386
- citationMetadata: {
387
- citations: [
388
- {
389
- uri: 'https://example.com/source2',
390
- title: 'Title2',
391
- },
392
- {
393
- uri: 'https://example.com/source1',
394
- title: 'Title1',
395
- },
396
- ],
451
+ type: StreamEventType.CHUNK,
452
+ value: {
453
+ candidates: [
454
+ {
455
+ content: { parts: [{ text: 'Some text.' }] },
456
+ citationMetadata: {
457
+ citations: [
458
+ {
459
+ uri: 'https://example.com/source2',
460
+ title: 'Title2',
461
+ },
462
+ {
463
+ uri: 'https://example.com/source1',
464
+ title: 'Title1',
465
+ },
466
+ ],
467
+ },
468
+ finishReason: 'STOP',
397
469
  },
398
- finishReason: 'STOP',
399
- },
400
- ],
470
+ ],
471
+ },
401
472
  };
402
473
  })();
403
474
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
@@ -411,26 +482,32 @@ describe('Turn', () => {
411
482
  type: GeminiEventType.Citation,
412
483
  value: 'Citations:\n(Title1) https://example.com/source1\n(Title2) https://example.com/source2',
413
484
  },
414
- { type: GeminiEventType.Finished, value: 'STOP' },
485
+ {
486
+ type: GeminiEventType.Finished,
487
+ value: { reason: 'STOP', usageMetadata: undefined },
488
+ },
415
489
  ]);
416
490
  });
417
491
  it('should not yield citation event if there is no finish reason', async () => {
418
492
  const mockResponseStream = (async function* () {
419
493
  yield {
420
- candidates: [
421
- {
422
- content: { parts: [{ text: 'Some text.' }] },
423
- citationMetadata: {
424
- citations: [
425
- {
426
- uri: 'https://example.com/source1',
427
- title: 'Source 1 Title',
428
- },
429
- ],
494
+ type: StreamEventType.CHUNK,
495
+ value: {
496
+ candidates: [
497
+ {
498
+ content: { parts: [{ text: 'Some text.' }] },
499
+ citationMetadata: {
500
+ citations: [
501
+ {
502
+ uri: 'https://example.com/source1',
503
+ title: 'Source 1 Title',
504
+ },
505
+ ],
506
+ },
507
+ // No finishReason
430
508
  },
431
- // No finishReason
432
- },
433
- ],
509
+ ],
510
+ },
434
511
  };
435
512
  })();
436
513
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
@@ -441,30 +518,33 @@ describe('Turn', () => {
441
518
  expect(events).toEqual([
442
519
  { type: GeminiEventType.Content, value: 'Some text.' },
443
520
  ]);
444
- // No Citation or Finished event
521
+ // No Citation event (but we do get a Finished event with undefined reason)
445
522
  expect(events.some((e) => e.type === GeminiEventType.Citation)).toBe(false);
446
523
  });
447
524
  it('should ignore citations without a URI', async () => {
448
525
  const mockResponseStream = (async function* () {
449
526
  yield {
450
- candidates: [
451
- {
452
- content: { parts: [{ text: 'Some text.' }] },
453
- citationMetadata: {
454
- citations: [
455
- {
456
- uri: 'https://example.com/source1',
457
- title: 'Good Source',
458
- },
459
- {
460
- // uri is undefined
461
- title: 'Bad Source',
462
- },
463
- ],
527
+ type: StreamEventType.CHUNK,
528
+ value: {
529
+ candidates: [
530
+ {
531
+ content: { parts: [{ text: 'Some text.' }] },
532
+ citationMetadata: {
533
+ citations: [
534
+ {
535
+ uri: 'https://example.com/source1',
536
+ title: 'Good Source',
537
+ },
538
+ {
539
+ // uri is undefined
540
+ title: 'Bad Source',
541
+ },
542
+ ],
543
+ },
544
+ finishReason: 'STOP',
464
545
  },
465
- finishReason: 'STOP',
466
- },
467
- ],
546
+ ],
547
+ },
468
548
  };
469
549
  })();
470
550
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
@@ -478,7 +558,10 @@ describe('Turn', () => {
478
558
  type: GeminiEventType.Citation,
479
559
  value: 'Citations:\n(Good Source) https://example.com/source1',
480
560
  },
481
- { type: GeminiEventType.Finished, value: 'STOP' },
561
+ {
562
+ type: GeminiEventType.Finished,
563
+ value: { reason: 'STOP', usageMetadata: undefined },
564
+ },
482
565
  ]);
483
566
  });
484
567
  it('should not crash when cancelled request has malformed error', async () => {
@@ -500,6 +583,26 @@ describe('Turn', () => {
500
583
  expect(events).toEqual([{ type: GeminiEventType.UserCancelled }]);
501
584
  expect(reportError).not.toHaveBeenCalled();
502
585
  });
586
+ it('should yield a Retry event when it receives one from the chat stream', async () => {
587
+ const mockResponseStream = (async function* () {
588
+ yield { type: StreamEventType.RETRY };
589
+ yield {
590
+ type: StreamEventType.CHUNK,
591
+ value: {
592
+ candidates: [{ content: { parts: [{ text: 'Success' }] } }],
593
+ },
594
+ };
595
+ })();
596
+ mockSendMessageStream.mockResolvedValue(mockResponseStream);
597
+ const events = [];
598
+ for await (const event of turn.run([], new AbortController().signal)) {
599
+ events.push(event);
600
+ }
601
+ expect(events).toEqual([
602
+ { type: GeminiEventType.Retry },
603
+ { type: GeminiEventType.Content, value: 'Success' },
604
+ ]);
605
+ });
503
606
  });
504
607
  describe('getDebugResponses', () => {
505
608
  it('should return collected debug responses', async () => {
@@ -510,8 +613,8 @@ describe('Turn', () => {
510
613
  functionCalls: [{ name: 'debugTool' }],
511
614
  };
512
615
  const mockResponseStream = (async function* () {
513
- yield resp1;
514
- yield resp2;
616
+ yield { type: StreamEventType.CHUNK, value: resp1 };
617
+ yield { type: StreamEventType.CHUNK, value: resp2 };
515
618
  })();
516
619
  mockSendMessageStream.mockResolvedValue(mockResponseStream);
517
620
  const reqParts = [{ text: 'Hi' }];