mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-16 04:50:29 -07:00
strong owner
This commit is contained in:
@@ -17,13 +17,12 @@ import {
|
||||
import readline from 'node:readline';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type {
|
||||
Content,
|
||||
Part,
|
||||
PartListUnion,
|
||||
GenerateContentResponseUsageMetadata,
|
||||
} from '@google/genai';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import type { AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
import type { HistoryTurn } from '../core/agentChatHistory.js';
|
||||
import {
|
||||
SESSION_FILE_PREFIX,
|
||||
type TokensSummary,
|
||||
@@ -36,7 +35,6 @@ import {
|
||||
type RewindRecord,
|
||||
type MetadataUpdateRecord,
|
||||
type PartialMetadataRecord,
|
||||
type ContextEngineState,
|
||||
} from './chatRecordingTypes.js';
|
||||
export * from './chatRecordingTypes.js';
|
||||
|
||||
@@ -276,7 +274,6 @@ export async function loadConversationRecord(
|
||||
memoryScratchpad: metadata.memoryScratchpad,
|
||||
directories: metadata.directories,
|
||||
kind: metadata.kind,
|
||||
contextState: metadata.contextState,
|
||||
messages: options?.metadataOnly ? [] : loadedMessages,
|
||||
messageCount: options?.metadataOnly
|
||||
? metadataMessages.length || messageIds.length
|
||||
@@ -499,9 +496,10 @@ export class ChatRecordingService {
|
||||
type: ConversationRecordExtra['type'],
|
||||
content: PartListUnion,
|
||||
displayContent?: PartListUnion,
|
||||
id?: string,
|
||||
): MessageRecord {
|
||||
return {
|
||||
id: randomUUID(),
|
||||
id: id || randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
type,
|
||||
content,
|
||||
@@ -514,14 +512,16 @@ export class ChatRecordingService {
|
||||
type: ConversationRecordExtra['type'];
|
||||
content: PartListUnion;
|
||||
displayContent?: PartListUnion;
|
||||
}): void {
|
||||
if (!this.conversationFile || !this.cachedConversation) return;
|
||||
id?: string;
|
||||
}): string {
|
||||
if (!this.conversationFile || !this.cachedConversation) return message.id || randomUUID();
|
||||
|
||||
try {
|
||||
const msg = this.newMessage(
|
||||
message.type,
|
||||
message.content,
|
||||
message.displayContent,
|
||||
message.id,
|
||||
);
|
||||
if (msg.type === 'gemini') {
|
||||
msg.thoughts = this.queuedThoughts;
|
||||
@@ -532,12 +532,30 @@ export class ChatRecordingService {
|
||||
}
|
||||
this.pushMessage(msg);
|
||||
this.updateMetadata({ lastUpdated: new Date().toISOString() });
|
||||
return msg.id;
|
||||
} catch (error) {
|
||||
debugLogger.error('Error saving message to chat history.', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a synthetic message (e.g. Binary Received, Snapshot/Summary)
|
||||
* and returns its durable ID.
|
||||
*/
|
||||
recordSyntheticMessage(
|
||||
type: ConversationRecordExtra['type'],
|
||||
content: PartListUnion,
|
||||
id?: string,
|
||||
): string {
|
||||
return this.recordMessage({
|
||||
model: undefined,
|
||||
type,
|
||||
content,
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
recordThought(thought: ThoughtSummary): void {
|
||||
if (!this.conversationFile) return;
|
||||
this.queuedThoughts.push({
|
||||
@@ -648,15 +666,6 @@ export class ChatRecordingService {
|
||||
}
|
||||
}
|
||||
|
||||
saveContextState(contextState: ContextEngineState): void {
|
||||
if (!this.conversationFile) return;
|
||||
try {
|
||||
this.updateMetadata({ contextState } as Partial<ConversationRecord>);
|
||||
} catch (e: unknown) {
|
||||
debugLogger.error('Error saving context state to chat history.', e);
|
||||
}
|
||||
}
|
||||
|
||||
saveSummary(summary: string): void {
|
||||
if (!this.conversationFile) return;
|
||||
try {
|
||||
@@ -880,48 +889,77 @@ export class ChatRecordingService {
|
||||
return this.cachedConversation;
|
||||
}
|
||||
|
||||
updateMessagesFromHistory(history: readonly Content[]): void {
|
||||
updateMessagesFromHistory(history: readonly HistoryTurn[]): void {
|
||||
if (!this.conversationFile || !this.cachedConversation) return;
|
||||
|
||||
try {
|
||||
const partsMap = new Map<string, Part[]>();
|
||||
for (const content of history) {
|
||||
if (content.role === 'user' && content.parts) {
|
||||
const callIds = content.parts
|
||||
.map((p) => p.functionResponse?.id)
|
||||
.filter((id): id is string => !!id);
|
||||
let updated = false;
|
||||
|
||||
if (callIds.length === 0) continue;
|
||||
// 1. Sync content and IDs
|
||||
const newMessages: MessageRecord[] = history.map((turn) => {
|
||||
const existing = this.cachedConversation?.messages.find(
|
||||
(m) => m.id === turn.id,
|
||||
);
|
||||
|
||||
let currentCallId = callIds[0];
|
||||
for (const part of content.parts) {
|
||||
if (part.functionResponse?.id) {
|
||||
currentCallId = part.functionResponse.id;
|
||||
if (existing) {
|
||||
// If content parts have changed (e.g. masking), update them
|
||||
if (
|
||||
JSON.stringify(existing.content) !==
|
||||
JSON.stringify(turn.content.parts)
|
||||
) {
|
||||
updated = true;
|
||||
}
|
||||
return {
|
||||
...existing,
|
||||
content: turn.content.parts || [],
|
||||
};
|
||||
}
|
||||
|
||||
// It's a new (possibly synthetic) turn like a summary
|
||||
updated = true;
|
||||
return this.newMessage(
|
||||
turn.content.role === 'user' ? 'user' : 'gemini',
|
||||
turn.content.parts || [],
|
||||
undefined,
|
||||
turn.id,
|
||||
);
|
||||
});
|
||||
|
||||
// 2. Specialized 'Masking Sync' for tool call results
|
||||
// If a user turn in history contains a functionResponse, we update the
|
||||
// corresponding ToolCallRecord in the preceding gemini message.
|
||||
for (const turn of history) {
|
||||
if (turn.content.role !== 'user') continue;
|
||||
for (const part of turn.content.parts || []) {
|
||||
if (part.functionResponse) {
|
||||
const callId = part.functionResponse.id;
|
||||
// Find the gemini message that contains this tool call
|
||||
const geminiMsg = newMessages.find(
|
||||
(m) =>
|
||||
m.type === 'gemini' &&
|
||||
m.toolCalls?.some((tc) => tc.id === callId),
|
||||
) as MessageRecord & { type: 'gemini' };
|
||||
if (geminiMsg) {
|
||||
const tc = geminiMsg.toolCalls!.find((tc) => tc.id === callId);
|
||||
if (tc) {
|
||||
// If the history version is different (e.g. masked), sync it into the record
|
||||
// We sync the entire parts array of the user turn to ensure sibling parts are preserved
|
||||
if (JSON.stringify(tc.result) !== JSON.stringify(turn.content.parts)) {
|
||||
tc.result = turn.content.parts || [];
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!partsMap.has(currentCallId)) {
|
||||
partsMap.set(currentCallId, []);
|
||||
}
|
||||
partsMap.get(currentCallId)!.push(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const message of this.cachedConversation.messages) {
|
||||
let msgChanged = false;
|
||||
if (message.type === 'gemini' && message.toolCalls) {
|
||||
for (const toolCall of message.toolCalls) {
|
||||
const newParts = partsMap.get(toolCall.id);
|
||||
if (newParts !== undefined) {
|
||||
toolCall.result = newParts;
|
||||
msgChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msgChanged) {
|
||||
// Push updated message to log
|
||||
this.pushMessage(message);
|
||||
}
|
||||
if (updated || newMessages.length !== this.cachedConversation.messages.length) {
|
||||
this.cachedConversation.messages = newMessages;
|
||||
this.updateMetadata({
|
||||
messages: newMessages,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
debugLogger.error(
|
||||
|
||||
Reference in New Issue
Block a user