mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-25 18:56:48 -07:00
feat: foundation for subagent trajectories (Stage 1)
This commit is contained in:
@@ -173,6 +173,8 @@ describe('bugCommand', () => {
|
||||
geminiClient: {
|
||||
getChat: () => ({
|
||||
getHistory: () => history,
|
||||
getSubagentTrajectories: vi.fn().mockResolvedValue({}),
|
||||
getConversation: vi.fn().mockReturnValue({ messages: [] }),
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -187,8 +189,10 @@ describe('bugCommand', () => {
|
||||
'bug-report-history-1704067200000.json',
|
||||
);
|
||||
expect(exportHistoryToFile).toHaveBeenCalledWith({
|
||||
history,
|
||||
messages: [],
|
||||
filePath: expectedPath,
|
||||
trajectories: {},
|
||||
history,
|
||||
});
|
||||
|
||||
const addItemCall = vi.mocked(mockContext.ui.addItem).mock.calls[0];
|
||||
|
||||
@@ -88,7 +88,14 @@ export const bugCommand: SlashCommand = {
|
||||
const historyFileName = `bug-report-history-${Date.now()}.json`;
|
||||
const historyFilePath = path.join(tempDir, historyFileName);
|
||||
try {
|
||||
await exportHistoryToFile({ history, filePath: historyFilePath });
|
||||
const trajectories = await chat?.getSubagentTrajectories();
|
||||
const messages = chat?.getConversation()?.messages ?? [];
|
||||
await exportHistoryToFile({
|
||||
messages,
|
||||
filePath: historyFilePath,
|
||||
trajectories,
|
||||
history,
|
||||
});
|
||||
historyFileMessage = `\n\n--------------------------------------------------------------------------------\n\n📄 **Chat History Exported**\nTo help us debug, we've exported your current chat history to:\n${historyFilePath}\n\nPlease consider attaching this file to your GitHub issue if you feel comfortable doing so.\n\n**Privacy Disclaimer:** Please do not upload any logs containing sensitive or private information that you are not comfortable sharing publicly.`;
|
||||
problemValue += `\n\n[ACTION REQUIRED] 📎 PLEASE ATTACH THE EXPORTED CHAT HISTORY JSON FILE TO THIS ISSUE IF YOU FEEL COMFORTABLE SHARING IT.`;
|
||||
} catch (err) {
|
||||
|
||||
@@ -63,6 +63,8 @@ describe('chatCommand', () => {
|
||||
mockGetHistory = vi.fn().mockReturnValue([]);
|
||||
mockGetChat = vi.fn().mockReturnValue({
|
||||
getHistory: mockGetHistory,
|
||||
getSubagentTrajectories: vi.fn().mockResolvedValue({}),
|
||||
getConversation: vi.fn().mockReturnValue({ messages: [] }),
|
||||
});
|
||||
mockSaveCheckpoint = vi.fn().mockResolvedValue(undefined);
|
||||
mockLoadCheckpoint = vi.fn().mockResolvedValue({ history: [] });
|
||||
@@ -191,6 +193,15 @@ describe('chatCommand', () => {
|
||||
{ role: 'user', parts: [{ text: 'Hello, how are you?' }] },
|
||||
]);
|
||||
result = await saveCommand?.action?.(mockContext, tag);
|
||||
expect(mockSaveCheckpoint).toHaveBeenCalledWith(
|
||||
{
|
||||
history: expect.any(Array),
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
trajectories: {},
|
||||
messages: [],
|
||||
},
|
||||
tag,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
@@ -230,7 +241,12 @@ describe('chatCommand', () => {
|
||||
|
||||
expect(mockCheckpointExists).not.toHaveBeenCalled(); // Should skip existence check
|
||||
expect(mockSaveCheckpoint).toHaveBeenCalledWith(
|
||||
{ history, authType: AuthType.LOGIN_WITH_GOOGLE },
|
||||
{
|
||||
history,
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
trajectories: {},
|
||||
messages: [],
|
||||
},
|
||||
tag,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
@@ -292,6 +308,8 @@ describe('chatCommand', () => {
|
||||
{ type: 'gemini', text: 'hello world' },
|
||||
] as HistoryItemWithoutId[],
|
||||
clientHistory: conversation,
|
||||
messages: undefined,
|
||||
version: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -332,6 +350,8 @@ describe('chatCommand', () => {
|
||||
{ type: 'gemini', text: 'hello world' },
|
||||
] as HistoryItemWithoutId[],
|
||||
clientHistory: conversation,
|
||||
messages: undefined,
|
||||
version: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -463,8 +483,10 @@ describe('chatCommand', () => {
|
||||
'gemini-conversation-1234567890.json',
|
||||
);
|
||||
expect(mockExport).toHaveBeenCalledWith({
|
||||
history: mockHistory,
|
||||
messages: [],
|
||||
filePath: expectedPath,
|
||||
trajectories: {},
|
||||
history: mockHistory,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
@@ -478,8 +500,10 @@ describe('chatCommand', () => {
|
||||
const result = await shareCommand?.action?.(mockContext, filePath);
|
||||
const expectedPath = path.join(process.cwd(), 'my-chat.json');
|
||||
expect(mockExport).toHaveBeenCalledWith({
|
||||
history: mockHistory,
|
||||
messages: [],
|
||||
filePath: expectedPath,
|
||||
trajectories: {},
|
||||
history: mockHistory,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
@@ -493,8 +517,10 @@ describe('chatCommand', () => {
|
||||
const result = await shareCommand?.action?.(mockContext, filePath);
|
||||
const expectedPath = path.join(process.cwd(), 'my-chat.md');
|
||||
expect(mockExport).toHaveBeenCalledWith({
|
||||
history: mockHistory,
|
||||
messages: [],
|
||||
filePath: expectedPath,
|
||||
trajectories: {},
|
||||
history: mockHistory,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
@@ -543,8 +569,10 @@ describe('chatCommand', () => {
|
||||
await shareCommand?.action?.(mockContext, filePath);
|
||||
const expectedPath = path.join(process.cwd(), 'my-chat.json');
|
||||
expect(mockExport).toHaveBeenCalledWith({
|
||||
history: mockHistory,
|
||||
messages: [],
|
||||
filePath: expectedPath,
|
||||
trajectories: {},
|
||||
history: mockHistory,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -553,8 +581,10 @@ describe('chatCommand', () => {
|
||||
await shareCommand?.action?.(mockContext, filePath);
|
||||
const expectedPath = path.join(process.cwd(), 'my-chat.md');
|
||||
expect(mockExport).toHaveBeenCalledWith({
|
||||
history: mockHistory,
|
||||
messages: [],
|
||||
filePath: expectedPath,
|
||||
trajectories: {},
|
||||
history: mockHistory,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,7 +139,12 @@ const saveCommand: SlashCommand = {
|
||||
const history = chat.getHistory();
|
||||
if (history.length > INITIAL_HISTORY_LENGTH) {
|
||||
const authType = config?.getContentGeneratorConfig()?.authType;
|
||||
await logger.saveCheckpoint({ history, authType }, tag);
|
||||
const trajectories = await chat.getSubagentTrajectories();
|
||||
const messages = chat.getConversation()?.messages ?? [];
|
||||
await logger.saveCheckpoint(
|
||||
{ history, authType, trajectories, messages },
|
||||
tag,
|
||||
);
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
@@ -324,7 +329,9 @@ const shareCommand: SlashCommand = {
|
||||
}
|
||||
|
||||
try {
|
||||
await exportHistoryToFile({ history, filePath });
|
||||
const trajectories = await chat.getSubagentTrajectories();
|
||||
const messages = chat.getConversation()?.messages ?? [];
|
||||
await exportHistoryToFile({ messages, filePath, trajectories, history });
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
import * as fsPromises from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { Content } from '@google/genai';
|
||||
import {
|
||||
type ConversationRecord,
|
||||
type MessageRecord,
|
||||
reconstructHistory,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
/**
|
||||
* Serializes chat history to a Markdown string.
|
||||
@@ -51,8 +56,20 @@ export function serializeHistoryToMarkdown(
|
||||
* Options for exporting chat history.
|
||||
*/
|
||||
export interface ExportHistoryOptions {
|
||||
history: readonly Content[];
|
||||
/**
|
||||
* Optional full message records which contain metadata like agentId for tool calls,
|
||||
* providing the link between history and trajectories.
|
||||
*/
|
||||
messages?: MessageRecord[];
|
||||
/** The file path to export to. */
|
||||
filePath: string;
|
||||
/** Optional subagent trajectories to include. */
|
||||
trajectories?: Record<string, ConversationRecord>;
|
||||
/**
|
||||
* Optional standard history array used for model requests.
|
||||
* If provided, it is used for Markdown export to avoid reconstruction.
|
||||
*/
|
||||
history?: readonly Content[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,13 +78,21 @@ export interface ExportHistoryOptions {
|
||||
export async function exportHistoryToFile(
|
||||
options: ExportHistoryOptions,
|
||||
): Promise<void> {
|
||||
const { history, filePath } = options;
|
||||
const {
|
||||
messages,
|
||||
filePath,
|
||||
trajectories: _trajectories, // Collected but not yet included in Stage 1 JSON output
|
||||
history: providedHistory,
|
||||
} = options;
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
|
||||
let content: string;
|
||||
if (extension === '.json') {
|
||||
content = JSON.stringify(history, null, 2);
|
||||
// Stage 1 & 2: Maintain legacy behavior - only export the raw history array.
|
||||
// Trajectories and messages are collected but not yet included in Stage 2 JSON output.
|
||||
content = JSON.stringify(providedHistory ?? [], null, 2);
|
||||
} else if (extension === '.md') {
|
||||
const history = providedHistory ?? reconstructHistory(messages ?? []);
|
||||
content = serializeHistoryToMarkdown(history);
|
||||
} else {
|
||||
throw new Error(
|
||||
|
||||
Reference in New Issue
Block a user