Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 0 additions & 90 deletions packages/core/src/code_assist/converter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
BlockedReason,
type ContentListUnion,
type GenerateContentParameters,
type Part,
} from '@google/genai';

describe('converter', () => {
Expand Down Expand Up @@ -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]',
},
],
},
]);
});
});
});
34 changes: 0 additions & 34 deletions packages/core/src/code_assist/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>)['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;
}

Expand Down
57 changes: 57 additions & 0 deletions packages/core/src/utils/sessionUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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.' }],
},
]);
});
});