mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-13 07:30:52 -07:00
132 lines
3.7 KiB
TypeScript
132 lines
3.7 KiB
TypeScript
/**
|
|
* @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;
|
|
}
|