maintain turn metadata and coalesce same roles.

This commit is contained in:
davidapierce
2026-07-23 20:03:19 +00:00
parent 83d4deafbb
commit 63ec1392ea
2 changed files with 51 additions and 2 deletions
+49
View File
@@ -2363,6 +2363,30 @@ describe('GeminiChat', () => {
expect(turns).toHaveLength(1);
expect(turns[0].content.role).toBe('user');
});
it('should coalesce consecutive user turns when an intermediate model turn is stripped', () => {
vi.mocked(mockConfig.isContextManagementEnabled).mockReturnValue(false);
vi.mocked(mockConfig.getModel).mockReturnValue('gemini-2.5-pro');
chat.setHistory([
{ role: 'user', parts: [{ text: 'Question 1' }] },
{
role: 'model',
parts: [{ text: 'thinking...', thought: true } as unknown as Part],
},
{ role: 'user', parts: [{ text: 'Question 2' }] },
]);
const turns = chat.getHistoryTurns(true);
// The model turn contains only a thought part, so it is stripped.
// The two adjacent user turns must be coalesced into one user turn.
expect(turns).toHaveLength(1);
expect(turns[0].content.role).toBe('user');
expect(turns[0].content.parts).toHaveLength(2);
expect(turns[0].content.parts![0].text).toBe('Question 1');
expect(turns[0].content.parts![1].text).toBe('Question 2');
});
});
describe('ensureActiveLoopHasThoughtSignatures', () => {
@@ -3341,5 +3365,30 @@ describe('GeminiChat', () => {
const history: HistoryTurn[] = [{ id: '1', content: { role: 'user' } }];
expect(stripThoughts(history)).toEqual(history);
});
it('should preserve top-level metadata when stripping thoughts', () => {
const history: HistoryTurn[] = [
{
id: '1',
content: {
role: 'model',
parts: [
{ text: 'internal monologue', thought: true } as unknown as Part,
{ text: 'visible response' },
],
},
// top-level turn metadata
timestamp: '2026-07-23T00:00:00.000Z',
metadata: { some: 'value' },
} as unknown as HistoryTurn,
];
const stripped = stripThoughts(history);
expect(stripped).toHaveLength(1);
expect(stripped[0]).toHaveProperty(
'timestamp',
'2026-07-23T00:00:00.000Z',
);
expect(stripped[0]).toHaveProperty('metadata', { some: 'value' });
});
});
});
+2 -2
View File
@@ -968,7 +968,7 @@ export class GeminiChat {
const model = this.context.config.getModel();
if (isGemini2Model(model) || supportsModernFeatures(model)) {
return stripThoughts(history);
return coalesceConsecutiveRoles(stripThoughts(history));
}
return history;
@@ -1526,7 +1526,7 @@ export function stripThoughts(history: HistoryTurn[]): HistoryTurn[] {
const nonThoughtParts = turn.content.parts.filter((p) => p && !p.thought);
return {
id: turn.id,
...turn,
content: {
...turn.content,
parts: nonThoughtParts,