mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 22:14:52 -07:00
refactor(core): move session conversion logic to core (#19972)
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { convertSessionToClientHistory } from './sessionUtils.js';
|
||||
import { type ConversationRecord } from '../services/chatRecordingService.js';
|
||||
import { CoreToolCallStatus } from '../scheduler/types.js';
|
||||
|
||||
describe('convertSessionToClientHistory', () => {
|
||||
it('should convert a simple conversation without tool calls', () => {
|
||||
const messages: ConversationRecord['messages'] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'user',
|
||||
timestamp: '2024-01-01T10:00:00Z',
|
||||
content: 'Hello',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'gemini',
|
||||
timestamp: '2024-01-01T10:01:00Z',
|
||||
content: 'Hi there',
|
||||
},
|
||||
];
|
||||
|
||||
const history = convertSessionToClientHistory(messages);
|
||||
|
||||
expect(history).toEqual([
|
||||
{ role: 'user', parts: [{ text: 'Hello' }] },
|
||||
{ role: 'model', parts: [{ text: 'Hi there' }] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore info, error, and slash commands', () => {
|
||||
const messages: ConversationRecord['messages'] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'info',
|
||||
timestamp: '2024-01-01T10:00:00Z',
|
||||
content: 'System info',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'user',
|
||||
timestamp: '2024-01-01T10:01:00Z',
|
||||
content: '/clear',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'user',
|
||||
timestamp: '2024-01-01T10:02:00Z',
|
||||
content: '?help',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'user',
|
||||
timestamp: '2024-01-01T10:03:00Z',
|
||||
content: 'Actual query',
|
||||
},
|
||||
];
|
||||
|
||||
const history = convertSessionToClientHistory(messages);
|
||||
|
||||
expect(history).toEqual([
|
||||
{ role: 'user', parts: [{ text: 'Actual query' }] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should correctly map tool calls and their responses', () => {
|
||||
const messages: ConversationRecord['messages'] = [
|
||||
{
|
||||
id: 'msg1',
|
||||
type: 'user',
|
||||
timestamp: '2024-01-01T10:00:00Z',
|
||||
content: 'List files',
|
||||
},
|
||||
{
|
||||
id: 'msg2',
|
||||
type: 'gemini',
|
||||
timestamp: '2024-01-01T10:01:00Z',
|
||||
content: 'Let me check.',
|
||||
toolCalls: [
|
||||
{
|
||||
id: 'call123',
|
||||
name: 'ls',
|
||||
args: { dir: '.' },
|
||||
status: CoreToolCallStatus.Success,
|
||||
timestamp: '2024-01-01T10:01:05Z',
|
||||
result: 'file.txt',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const history = convertSessionToClientHistory(messages);
|
||||
|
||||
expect(history).toEqual([
|
||||
{ role: 'user', parts: [{ text: 'List files' }] },
|
||||
{
|
||||
role: 'model',
|
||||
parts: [
|
||||
{ text: 'Let me check.' },
|
||||
{ functionCall: { name: 'ls', args: { dir: '.' }, id: 'call123' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{
|
||||
functionResponse: {
|
||||
id: 'call123',
|
||||
name: 'ls',
|
||||
response: { output: 'file.txt' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should preserve multi-modal parts (inlineData)', () => {
|
||||
const messages: ConversationRecord['messages'] = [
|
||||
{
|
||||
id: 'msg1',
|
||||
type: 'user',
|
||||
timestamp: '2024-01-01T10:00:00Z',
|
||||
content: [
|
||||
{ text: 'Look at this image' },
|
||||
{ inlineData: { mimeType: 'image/png', data: 'base64data' } },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const history = convertSessionToClientHistory(messages);
|
||||
|
||||
expect(history).toEqual([
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{ text: 'Look at this image' },
|
||||
{ inlineData: { mimeType: 'image/png', data: 'base64data' } },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { type Part, type PartListUnion } from '@google/genai';
|
||||
import { type ConversationRecord } from '../services/chatRecordingService.js';
|
||||
import { partListUnionToString } from '../core/geminiRequest.js';
|
||||
|
||||
/**
|
||||
* Converts a PartListUnion into a normalized array of Part objects.
|
||||
* This handles converting raw strings into { text: string } parts.
|
||||
*/
|
||||
function ensurePartArray(content: PartListUnion): Part[] {
|
||||
if (Array.isArray(content)) {
|
||||
return content.map((part) =>
|
||||
typeof part === 'string' ? { text: part } : part,
|
||||
);
|
||||
}
|
||||
if (typeof content === 'string') {
|
||||
return [{ text: content }];
|
||||
}
|
||||
return [content];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts session/conversation data into Gemini client history formats.
|
||||
*/
|
||||
export function convertSessionToClientHistory(
|
||||
messages: ConversationRecord['messages'],
|
||||
): Array<{ role: 'user' | 'model'; parts: Part[] }> {
|
||||
const clientHistory: Array<{ role: 'user' | 'model'; parts: Part[] }> = [];
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.type === 'info' || msg.type === 'error' || msg.type === 'warning') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.type === 'user') {
|
||||
const contentString = partListUnionToString(msg.content);
|
||||
if (
|
||||
contentString.trim().startsWith('/') ||
|
||||
contentString.trim().startsWith('?')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
clientHistory.push({
|
||||
role: 'user',
|
||||
parts: ensurePartArray(msg.content),
|
||||
});
|
||||
} else if (msg.type === 'gemini') {
|
||||
const hasToolCalls = msg.toolCalls && msg.toolCalls.length > 0;
|
||||
|
||||
if (hasToolCalls) {
|
||||
const modelParts: Part[] = [];
|
||||
|
||||
// TODO: Revisit if we should preserve more than just Part metadata (e.g. thoughtSignatures)
|
||||
// currently those are only required within an active loop turn which resume clears
|
||||
// by forcing a new user text prompt.
|
||||
|
||||
// Preserve original parts to maintain multimodal integrity
|
||||
if (msg.content) {
|
||||
modelParts.push(...ensurePartArray(msg.content));
|
||||
}
|
||||
|
||||
for (const toolCall of msg.toolCalls!) {
|
||||
modelParts.push({
|
||||
functionCall: {
|
||||
name: toolCall.name,
|
||||
args: toolCall.args,
|
||||
...(toolCall.id && { id: toolCall.id }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
clientHistory.push({
|
||||
role: 'model',
|
||||
parts: modelParts,
|
||||
});
|
||||
|
||||
const functionResponseParts: Part[] = [];
|
||||
for (const toolCall of msg.toolCalls!) {
|
||||
if (toolCall.result) {
|
||||
let responseData: Part;
|
||||
|
||||
if (typeof toolCall.result === 'string') {
|
||||
responseData = {
|
||||
functionResponse: {
|
||||
id: toolCall.id,
|
||||
name: toolCall.name,
|
||||
response: {
|
||||
output: toolCall.result,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (Array.isArray(toolCall.result)) {
|
||||
functionResponseParts.push(...ensurePartArray(toolCall.result));
|
||||
continue;
|
||||
} else {
|
||||
responseData = toolCall.result;
|
||||
}
|
||||
|
||||
functionResponseParts.push(responseData);
|
||||
}
|
||||
}
|
||||
|
||||
if (functionResponseParts.length > 0) {
|
||||
clientHistory.push({
|
||||
role: 'user',
|
||||
parts: functionResponseParts,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (msg.content) {
|
||||
const parts = ensurePartArray(msg.content);
|
||||
|
||||
if (parts.length > 0) {
|
||||
clientHistory.push({
|
||||
role: 'model',
|
||||
parts,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return clientHistory;
|
||||
}
|
||||
Reference in New Issue
Block a user