refactor(core): move session conversion logic to core (#19972)

This commit is contained in:
Abhi
2026-02-22 20:18:07 -05:00
committed by GitHub
parent c537fd5aec
commit 621ddbe744
11 changed files with 328 additions and 142 deletions
+3 -3
View File
@@ -13,6 +13,7 @@ import type {
import { isSlashCommand } from './ui/utils/commandUtils.js';
import type { LoadedSettings } from './config/settings.js';
import {
convertSessionToClientHistory,
GeminiEventType,
FatalInputError,
promptIdContext,
@@ -35,7 +36,6 @@ import type { Content, Part } from '@google/genai';
import readline from 'node:readline';
import stripAnsi from 'strip-ansi';
import { convertSessionToHistoryFormats } from './ui/hooks/useSessionBrowser.js';
import { handleSlashCommand } from './nonInteractiveCliCommands.js';
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
import { handleAtCommand } from './ui/hooks/atCommandProcessor.js';
@@ -220,9 +220,9 @@ export async function runNonInteractive({
// Initialize chat. Resume if resume data is passed.
if (resumedSessionData) {
await geminiClient.resumeChat(
convertSessionToHistoryFormats(
convertSessionToClientHistory(
resumedSessionData.conversation.messages,
).clientHistory,
),
resumedSessionData,
);
}
@@ -23,6 +23,7 @@ import {
RewindEvent,
type ChatRecordingService,
type GeminiClient,
convertSessionToClientHistory,
} from '@google/gemini-cli-core';
/**
@@ -54,9 +55,8 @@ async function rewindConversation(
}
// Convert to UI and Client formats
const { uiHistory, clientHistory } = convertSessionToHistoryFormats(
conversation.messages,
);
const { uiHistory } = convertSessionToHistoryFormats(conversation.messages);
const clientHistory = convertSessionToClientHistory(conversation.messages);
client.setHistory(clientHistory as Content[]);
@@ -20,7 +20,10 @@ import {
type MessageRecord,
CoreToolCallStatus,
} from '@google/gemini-cli-core';
import { coreEvents } from '@google/gemini-cli-core';
import {
coreEvents,
convertSessionToClientHistory,
} from '@google/gemini-cli-core';
// Mock modules
vi.mock('fs/promises');
@@ -157,7 +160,7 @@ describe('convertSessionToHistoryFormats', () => {
it('should convert empty messages array', () => {
const result = convertSessionToHistoryFormats([]);
expect(result.uiHistory).toEqual([]);
expect(result.clientHistory).toEqual([]);
expect(convertSessionToClientHistory([])).toEqual([]);
});
it('should convert basic user and model messages', () => {
@@ -175,12 +178,13 @@ describe('convertSessionToHistoryFormats', () => {
text: 'Hi there',
});
expect(result.clientHistory).toHaveLength(2);
expect(result.clientHistory[0]).toEqual({
const clientHistory = convertSessionToClientHistory(messages);
expect(clientHistory).toHaveLength(2);
expect(clientHistory[0]).toEqual({
role: 'user',
parts: [{ text: 'Hello' }],
});
expect(result.clientHistory[1]).toEqual({
expect(clientHistory[1]).toEqual({
role: 'model',
parts: [{ text: 'Hi there' }],
});
@@ -203,8 +207,9 @@ describe('convertSessionToHistoryFormats', () => {
text: 'User input',
});
expect(result.clientHistory).toHaveLength(1);
expect(result.clientHistory[0]).toEqual({
const clientHistory = convertSessionToClientHistory(messages);
expect(clientHistory).toHaveLength(1);
expect(clientHistory[0]).toEqual({
role: 'user',
parts: [{ text: 'Expanded content' }],
});
@@ -225,7 +230,7 @@ describe('convertSessionToHistoryFormats', () => {
text: 'Help text',
});
expect(result.clientHistory).toHaveLength(0);
expect(convertSessionToClientHistory(messages)).toHaveLength(0);
});
it('should handle tool calls and responses', () => {
@@ -264,12 +269,13 @@ describe('convertSessionToHistoryFormats', () => {
],
});
expect(result.clientHistory).toHaveLength(3); // User, Model (call), User (response)
expect(result.clientHistory[0]).toEqual({
const clientHistory = convertSessionToClientHistory(messages);
expect(clientHistory).toHaveLength(3); // User, Model (call), User (response)
expect(clientHistory[0]).toEqual({
role: 'user',
parts: [{ text: 'What time is it?' }],
});
expect(result.clientHistory[1]).toEqual({
expect(clientHistory[1]).toEqual({
role: 'model',
parts: [
{
@@ -281,7 +287,7 @@ describe('convertSessionToHistoryFormats', () => {
},
],
});
expect(result.clientHistory[2]).toEqual({
expect(clientHistory[2]).toEqual({
role: 'user',
parts: [
{
@@ -13,7 +13,10 @@ import type {
ConversationRecord,
ResumedSessionData,
} from '@google/gemini-cli-core';
import { coreEvents } from '@google/gemini-cli-core';
import {
coreEvents,
convertSessionToClientHistory,
} from '@google/gemini-cli-core';
import type { SessionInfo } from '../../utils/sessionUtils.js';
import { convertSessionToHistoryFormats } from '../../utils/sessionUtils.js';
import type { Part } from '@google/genai';
@@ -78,7 +81,7 @@ export const useSessionBrowser = (
);
await onLoadHistory(
historyData.uiHistory,
historyData.clientHistory,
convertSessionToClientHistory(conversation.messages),
resumedSessionData,
);
} catch (error) {
@@ -9,6 +9,7 @@ import {
coreEvents,
type Config,
type ResumedSessionData,
convertSessionToClientHistory,
} from '@google/gemini-cli-core';
import type { Part } from '@google/genai';
import type { HistoryItemWithoutId } from '../types.js';
@@ -113,7 +114,7 @@ export function useSessionResume({
);
void loadHistoryForResume(
historyData.uiHistory,
historyData.clientHistory,
convertSessionToClientHistory(resumedSessionData.conversation.messages),
resumedSessionData,
);
}
+1 -113
View File
@@ -16,7 +16,6 @@ import {
import * as fs from 'node:fs/promises';
import path from 'node:path';
import { stripUnsafeCharacters } from '../ui/utils/textUtils.js';
import type { Part } from '@google/genai';
import { MessageType, type HistoryItemWithoutId } from '../ui/types.js';
/**
@@ -526,13 +525,12 @@ export class SessionSelector {
}
/**
* Converts session/conversation data into UI history and Gemini client history formats.
* Converts session/conversation data into UI history format.
*/
export function convertSessionToHistoryFormats(
messages: ConversationRecord['messages'],
): {
uiHistory: HistoryItemWithoutId[];
clientHistory: Array<{ role: 'user' | 'model'; parts: Part[] }>;
} {
const uiHistory: HistoryItemWithoutId[] = [];
@@ -599,117 +597,7 @@ export function convertSessionToHistoryFormats(
}
}
// Convert to Gemini client history format
const clientHistory: Array<{ role: 'user' | 'model'; parts: Part[] }> = [];
for (const msg of messages) {
// Skip system/error messages and user slash commands
if (msg.type === 'info' || msg.type === 'error' || msg.type === 'warning') {
continue;
}
if (msg.type === 'user') {
// Skip user slash commands
const contentString = partListUnionToString(msg.content);
if (
contentString.trim().startsWith('/') ||
contentString.trim().startsWith('?')
) {
continue;
}
// Add regular user message
clientHistory.push({
role: 'user',
parts: Array.isArray(msg.content)
? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(msg.content as Part[])
: [{ text: contentString }],
});
} else if (msg.type === 'gemini') {
// Handle Gemini messages with potential tool calls
const hasToolCalls = msg.toolCalls && msg.toolCalls.length > 0;
if (hasToolCalls) {
// Create model message with function calls
const modelParts: Part[] = [];
// Add text content if present
const contentString = partListUnionToString(msg.content);
if (msg.content && contentString.trim()) {
modelParts.push({ text: contentString });
}
// Add function calls
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,
});
// Create single function response message with all tool call responses
const functionResponseParts: Part[] = [];
for (const toolCall of msg.toolCalls!) {
if (toolCall.result) {
// Convert PartListUnion result to function response format
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)) {
// toolCall.result is an array containing properly formatted
// function responses
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
functionResponseParts.push(...(toolCall.result as Part[]));
continue;
} else {
// Fallback for non-array results
responseData = toolCall.result;
}
functionResponseParts.push(responseData);
}
}
// Only add user message if we have function responses
if (functionResponseParts.length > 0) {
clientHistory.push({
role: 'user',
parts: functionResponseParts,
});
}
} else {
// Regular Gemini message without tool calls
const contentString = partListUnionToString(msg.content);
if (msg.content && contentString.trim()) {
clientHistory.push({
role: 'model',
parts: [{ text: contentString }],
});
}
}
}
}
return {
uiHistory,
clientHistory,
};
}
@@ -26,6 +26,7 @@ import {
SessionSelector,
convertSessionToHistoryFormats,
} from '../utils/sessionUtils.js';
import { convertSessionToClientHistory } from '@google/gemini-cli-core';
import type { LoadedSettings } from '../config/settings.js';
vi.mock('../config/config.js', () => ({
@@ -42,6 +43,15 @@ vi.mock('../utils/sessionUtils.js', async (importOriginal) => {
};
});
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
convertSessionToClientHistory: vi.fn(),
};
});
describe('GeminiAgent Session Resume', () => {
let mockConfig: Mocked<Config>;
let mockSettings: Mocked<LoadedSettings>;
@@ -142,9 +152,11 @@ describe('GeminiAgent Session Resume', () => {
{ role: 'model', parts: [{ text: 'Hi there' }] },
];
(convertSessionToHistoryFormats as unknown as Mock).mockReturnValue({
clientHistory: mockClientHistory,
uiHistory: [],
});
(convertSessionToClientHistory as unknown as Mock).mockReturnValue(
mockClientHistory,
);
const response = await agent.loadSession({
sessionId,
@@ -37,6 +37,7 @@ import {
partListUnionToString,
LlmRole,
ApprovalMode,
convertSessionToClientHistory,
} from '@google/gemini-cli-core';
import * as acp from '@agentclientprotocol/sdk';
import { AcpFileSystemService } from './fileSystemService.js';
@@ -53,10 +54,7 @@ import { randomUUID } from 'node:crypto';
import type { CliArgs } from '../config/config.js';
import { loadCliConfig } from '../config/config.js';
import { runExitCleanup } from '../utils/cleanup.js';
import {
SessionSelector,
convertSessionToHistoryFormats,
} from '../utils/sessionUtils.js';
import { SessionSelector } from '../utils/sessionUtils.js';
export async function runZedIntegration(
config: Config,
@@ -258,9 +256,7 @@ export class GeminiAgent {
config.setFileSystemService(acpFileSystemService);
}
const { clientHistory } = convertSessionToHistoryFormats(
sessionData.messages,
);
const clientHistory = convertSessionToClientHistory(sessionData.messages);
const geminiClient = config.getGeminiClient();
await geminiClient.initialize();