diff --git a/packages/core/src/code_assist/converter.test.ts b/packages/core/src/code_assist/converter.test.ts index 83309412038..67e6123a931 100644 --- a/packages/core/src/code_assist/converter.test.ts +++ b/packages/core/src/code_assist/converter.test.ts @@ -17,7 +17,6 @@ import { BlockedReason, type ContentListUnion, type GenerateContentParameters, - type Part, } from '@google/genai'; describe('converter', () => { @@ -392,94 +391,5 @@ describe('converter', () => { { role: 'user', parts: [{ text: 'string 2' }] }, ]); }); - - it('should convert thought parts to text parts for API compatibility', () => { - const contentWithThought: ContentListUnion = { - role: 'model', - parts: [ - { text: 'regular text' }, - { thought: 'thinking about the problem' } as Part & { - thought: string; - }, - { text: 'more text' }, - ], - }; - expect(toContents(contentWithThought)).toEqual([ - { - role: 'model', - parts: [ - { text: 'regular text' }, - { text: '[Thought: thinking about the problem]' }, - { text: 'more text' }, - ], - }, - ]); - }); - - it('should combine text and thought for text parts with thoughts', () => { - const contentWithTextAndThought: ContentListUnion = { - role: 'model', - parts: [ - { - text: 'Here is my response', - thought: 'I need to be careful here', - } as Part & { thought: string }, - ], - }; - expect(toContents(contentWithTextAndThought)).toEqual([ - { - role: 'model', - parts: [ - { - text: 'Here is my response\n[Thought: I need to be careful here]', - }, - ], - }, - ]); - }); - - it('should preserve non-thought properties while removing thought', () => { - const contentWithComplexPart: ContentListUnion = { - role: 'model', - parts: [ - { - functionCall: { name: 'calculate', args: { x: 5, y: 10 } }, - thought: 'Performing calculation', - } as Part & { thought: string }, - ], - }; - expect(toContents(contentWithComplexPart)).toEqual([ - { - role: 'model', - parts: [ - { - functionCall: { name: 'calculate', args: { x: 5, y: 10 } }, - }, - ], - }, - ]); - }); - - it('should convert invalid text content to valid text part with thought', () => { - const contentWithInvalidText: ContentListUnion = { - role: 'model', - parts: [ - { - text: 123, // Invalid - should be string - thought: 'Processing number', - } as Part & { thought: string; text: number }, - ], - }; - expect(toContents(contentWithInvalidText)).toEqual([ - { - role: 'model', - parts: [ - { - text: '123\n[Thought: Processing number]', - }, - ], - }, - ]); - }); }); }); diff --git a/packages/core/src/code_assist/converter.ts b/packages/core/src/code_assist/converter.ts index 005a8cf85d6..fe52afc8a90 100644 --- a/packages/core/src/code_assist/converter.ts +++ b/packages/core/src/code_assist/converter.ts @@ -244,40 +244,6 @@ function toPart(part: PartUnion): Part { return { text: part }; } - // Handle thought parts for CountToken API compatibility - // The CountToken API expects parts to have certain required "oneof" fields initialized, - // but thought parts don't conform to this schema and cause API failures - if ('thought' in part && part.thought) { - const thoughtText = `[Thought: ${part.thought}]`; - - const newPart = { ...part }; - delete (newPart as Record)['thought']; - - const hasApiContent = - 'functionCall' in newPart || - 'functionResponse' in newPart || - 'inlineData' in newPart || - 'fileData' in newPart; - - if (hasApiContent) { - // It's a functionCall or other non-text part. Just strip the thought. - return newPart; - } - - // If no other valid API content, this must be a text part. - // Combine existing text (if any) with the thought, preserving other properties. - const text = (newPart as { text?: unknown }).text; - const existingText = text ? String(text) : ''; - const combinedText = existingText - ? `${existingText}\n${thoughtText}` - : thoughtText; - - return { - ...newPart, - text: combinedText, - }; - } - return part; } diff --git a/packages/core/src/utils/sessionUtils.test.ts b/packages/core/src/utils/sessionUtils.test.ts index d132087ee82..1341cf37234 100644 --- a/packages/core/src/utils/sessionUtils.test.ts +++ b/packages/core/src/utils/sessionUtils.test.ts @@ -7,6 +7,7 @@ import { describe, it, expect } from 'vitest'; import { convertSessionToClientHistory } from './sessionUtils.js'; import { type ConversationRecord } from '../services/chatRecordingService.js'; import { CoreToolCallStatus } from '../scheduler/types.js'; +import { toContents } from '../code_assist/converter.js'; describe('convertSessionToClientHistory', () => { it('should convert a simple conversation without tool calls', () => { @@ -182,4 +183,60 @@ describe('convertSessionToClientHistory', () => { }, ]); }); + + it('integration: should ensure thoughts survive conversion back to API format without string mutation on session resume', () => { + // 1. A simulated conversation pulled from storage (like session resume) + const messages: ConversationRecord['messages'] = [ + { + id: '1', + type: 'user', + timestamp: '2024-01-01T10:00:00Z', + content: 'Hello, can you help me?', + }, + { + id: '2', + type: 'gemini', + timestamp: '2024-01-01T10:01:00Z', + content: 'AI Response', + thoughts: [ + { + subject: 'Thinking', + description: 'I need to process this', + timestamp: '2024-01-01T10:00:50Z', + }, + ], + }, + { + id: '3', + type: 'user', + timestamp: '2024-01-01T10:02:00Z', + content: 'This is the first message after session resume.', + }, + ]; + + // 2. convertSessionToClientHistory (what GeminiChat uses to load history internally) + const history = convertSessionToClientHistory(messages); + + // 3. toContents (what the CodeAssist API / external AI provider uses internally to format network requests) + const generatedContents = toContents(history); + + // 4. Assert that we don't accidentally mutate or inject '[Thought: true]' as string text. + expect(generatedContents).toEqual([ + { + role: 'user', + parts: [{ text: 'Hello, can you help me?' }], + }, + { + role: 'model', + parts: [ + { text: '**Thinking** I need to process this', thought: true }, + { text: 'AI Response' }, + ], + }, + { + role: 'user', + parts: [{ text: 'This is the first message after session resume.' }], + }, + ]); + }); });