mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-19 02:43:18 +00:00
feat: implement trajectory discovery and resumption logic (Stage 2)
This commit is contained in:
@@ -183,9 +183,9 @@ const resumeCheckpointCommand: SlashCommand = {
|
||||
const config = context.services.agentContext?.config;
|
||||
await logger.initialize();
|
||||
const checkpoint = await logger.loadCheckpoint(tag);
|
||||
const conversation = checkpoint.history;
|
||||
const conversation = checkpoint.history ?? [];
|
||||
|
||||
if (conversation.length === 0) {
|
||||
if (conversation.length === 0 && !checkpoint.messages) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
@@ -233,6 +233,8 @@ const resumeCheckpointCommand: SlashCommand = {
|
||||
type: 'load_history',
|
||||
history: uiHistory,
|
||||
clientHistory: conversation,
|
||||
messages: checkpoint.messages,
|
||||
version: checkpoint.version,
|
||||
};
|
||||
},
|
||||
completion: async (context, partialArg) => {
|
||||
|
||||
@@ -549,7 +549,16 @@ export const useSlashCommandProcessor = (
|
||||
}
|
||||
}
|
||||
case 'load_history': {
|
||||
config?.getGeminiClient()?.setHistory(result.clientHistory);
|
||||
const client = config?.getGeminiClient();
|
||||
if (result.version === '2.0' && client) {
|
||||
await client.resumeChat(
|
||||
[...result.clientHistory],
|
||||
undefined,
|
||||
result.messages,
|
||||
);
|
||||
} else {
|
||||
client?.setHistory(result.clientHistory);
|
||||
}
|
||||
fullCommandContext.ui.clear();
|
||||
result.history.forEach((item, index) => {
|
||||
fullCommandContext.ui.addItem(item, index);
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
|
||||
import type { Content, PartListUnion } from '@google/genai';
|
||||
import type { MessageRecord } from '../services/chatRecordingTypes.js';
|
||||
|
||||
/**
|
||||
* The return type for a command action that results in scheduling a tool call.
|
||||
*/
|
||||
@@ -37,6 +39,8 @@ export interface LoadHistoryActionReturn<HistoryType = unknown> {
|
||||
type: 'load_history';
|
||||
history: HistoryType;
|
||||
clientHistory: readonly Content[]; // The history for the generative client
|
||||
messages?: MessageRecord[];
|
||||
version?: '2.0';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -546,6 +546,33 @@ describe('Logger', () => {
|
||||
expect(loaded).toEqual({ history: conversation, messages: [] });
|
||||
});
|
||||
|
||||
it('should load a version 2.0 checkpoint and reconstruct history', async () => {
|
||||
const tag = 'v2-tag';
|
||||
const v2Checkpoint = {
|
||||
version: '2.0',
|
||||
history: [{ role: 'user', parts: [{ text: 'preserved' }] }],
|
||||
messages: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'user',
|
||||
content: 'hello',
|
||||
timestamp: '2025-01-01T12:00:00.000Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
const taggedFilePath = path.join(
|
||||
TEST_GEMINI_DIR,
|
||||
'checkpoint-v2-tag.json',
|
||||
);
|
||||
await fs.writeFile(taggedFilePath, JSON.stringify(v2Checkpoint, null, 2));
|
||||
|
||||
const loaded = await logger.loadCheckpoint(tag);
|
||||
expect(loaded.messages).toEqual(v2Checkpoint.messages);
|
||||
expect(loaded.history).toEqual([
|
||||
{ role: 'user', parts: [{ text: 'hello' }] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an empty message list if a tagged checkpoint file does not exist', async () => {
|
||||
const loaded = await logger.loadCheckpoint('nonexistent-tag');
|
||||
expect(loaded).toEqual({ history: [], messages: [] });
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { AuthType } from './contentGenerator.js';
|
||||
import type { Storage } from '../config/storage.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { coreEvents } from '../utils/events.js';
|
||||
import { reconstructHistory } from '../utils/history-reconstruction.js';
|
||||
|
||||
import {
|
||||
type ConversationRecord,
|
||||
@@ -32,7 +33,11 @@ export interface LogEntry {
|
||||
}
|
||||
|
||||
export interface Checkpoint {
|
||||
history: readonly Content[];
|
||||
/**
|
||||
* The standard history array used for model requests.
|
||||
* Only included in legacy checkpoints (pre-2.0).
|
||||
*/
|
||||
history?: readonly Content[];
|
||||
authType?: AuthType;
|
||||
/**
|
||||
* The rich message records which are the source of truth for the session.
|
||||
@@ -372,34 +377,37 @@ export class Logger {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const parsedContent = JSON.parse(fileContent);
|
||||
|
||||
// Handle legacy format (just an array of Content)
|
||||
if (Array.isArray(parsedContent)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return { history: parsedContent as Content[], messages: [] };
|
||||
}
|
||||
|
||||
if (
|
||||
typeof parsedContent === 'object' &&
|
||||
parsedContent !== null &&
|
||||
'history' in parsedContent
|
||||
) {
|
||||
if (typeof parsedContent === 'object' && parsedContent !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const checkpoint = parsedContent as Checkpoint;
|
||||
const raw = parsedContent as Record<string, unknown>;
|
||||
if (raw['version'] === '2.0') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const msgs = (raw['messages'] as MessageRecord[]) ?? [];
|
||||
return {
|
||||
...raw,
|
||||
history: reconstructHistory(msgs),
|
||||
messages: msgs,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...checkpoint,
|
||||
messages: checkpoint.messages ?? [],
|
||||
...raw,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
history: (raw['history'] as Content[]) ?? [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
messages: (raw['messages'] as MessageRecord[]) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
debugLogger.warn(
|
||||
`Checkpoint file at ${path} has an unknown format. Returning empty checkpoint.`,
|
||||
);
|
||||
return { history: [], messages: [] };
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const nodeError = error as NodeJS.ErrnoException;
|
||||
if (nodeError.code === 'ENOENT') {
|
||||
// This is okay, it just means the checkpoint doesn't exist in either format.
|
||||
return { history: [], messages: [] };
|
||||
}
|
||||
debugLogger.error(
|
||||
|
||||
Reference in New Issue
Block a user