mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
fix(core): resolve subagent chat recording gaps and directory inheritance (#24368)
This commit is contained in:
@@ -18,6 +18,8 @@ const {
|
||||
mockSendMessageStream,
|
||||
mockScheduleAgentTools,
|
||||
mockSetSystemInstruction,
|
||||
mockRecordCompletedToolCalls,
|
||||
mockSaveSummary,
|
||||
mockCompress,
|
||||
mockMaybeDiscoverMcpServer,
|
||||
mockStopMcp,
|
||||
@@ -32,6 +34,8 @@ const {
|
||||
}),
|
||||
mockScheduleAgentTools: vi.fn(),
|
||||
mockSetSystemInstruction: vi.fn(),
|
||||
mockRecordCompletedToolCalls: vi.fn(),
|
||||
mockSaveSummary: vi.fn(),
|
||||
mockCompress: vi.fn(),
|
||||
mockMaybeDiscoverMcpServer: vi.fn().mockResolvedValue(undefined),
|
||||
mockStopMcp: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -127,18 +131,21 @@ vi.mock('../context/chatCompressionService.js', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../core/geminiChat.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../core/geminiChat.js')>();
|
||||
return {
|
||||
...actual,
|
||||
GeminiChat: vi.fn().mockImplementation(() => ({
|
||||
sendMessageStream: mockSendMessageStream,
|
||||
getHistory: vi.fn((_curated?: boolean) => [...mockChatHistory]),
|
||||
setHistory: mockSetHistory,
|
||||
setSystemInstruction: mockSetSystemInstruction,
|
||||
})),
|
||||
};
|
||||
});
|
||||
vi.mock('../core/geminiChat.js', () => ({
|
||||
StreamEventType: {
|
||||
CHUNK: 'chunk',
|
||||
},
|
||||
GeminiChat: vi.fn().mockImplementation(() => ({
|
||||
sendMessageStream: mockSendMessageStream,
|
||||
getHistory: vi.fn((_curated?: boolean) => [...mockChatHistory]),
|
||||
setHistory: mockSetHistory,
|
||||
setSystemInstruction: mockSetSystemInstruction,
|
||||
recordCompletedToolCalls: mockRecordCompletedToolCalls,
|
||||
getChatRecordingService: vi.fn().mockReturnValue({
|
||||
saveSummary: mockSaveSummary,
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('./agent-scheduler.js', () => ({
|
||||
scheduleAgentTools: mockScheduleAgentTools,
|
||||
@@ -337,6 +344,10 @@ describe('LocalAgentExecutor', () => {
|
||||
getHistory: vi.fn((_curated?: boolean) => [...mockChatHistory]),
|
||||
getLastPromptTokenCount: vi.fn(() => 100),
|
||||
setHistory: mockSetHistory,
|
||||
recordCompletedToolCalls: mockRecordCompletedToolCalls,
|
||||
getChatRecordingService: vi.fn().mockReturnValue({
|
||||
saveSummary: mockSaveSummary,
|
||||
}),
|
||||
}) as unknown as GeminiChat,
|
||||
);
|
||||
|
||||
@@ -942,6 +953,20 @@ describe('LocalAgentExecutor', () => {
|
||||
|
||||
// Context checks
|
||||
expect(mockedPromptIdContext.run).toHaveBeenCalledTimes(2); // Two turns
|
||||
|
||||
// Recording checks
|
||||
expect(mockRecordCompletedToolCalls).toHaveBeenCalledTimes(1);
|
||||
expect(mockRecordCompletedToolCalls).toHaveBeenCalledWith(
|
||||
expect.any(String), // model
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
status: 'success',
|
||||
request: expect.objectContaining({ name: LS_TOOL_NAME }),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(mockSaveSummary).toHaveBeenCalledTimes(1);
|
||||
expect(mockSaveSummary).toHaveBeenCalledWith('Found file1.txt');
|
||||
const agentId = executor['agentId'];
|
||||
expect(mockedPromptIdContext.run).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
@@ -2450,6 +2475,10 @@ describe('LocalAgentExecutor', () => {
|
||||
expect(recoveryEvent).toBeInstanceOf(RecoveryAttemptEvent);
|
||||
expect(recoveryEvent.success).toBe(true);
|
||||
expect(recoveryEvent.reason).toBe(AgentTerminateMode.MAX_TURNS);
|
||||
|
||||
// Verify that the summary is saved upon successful recovery
|
||||
expect(mockSaveSummary).toHaveBeenCalledTimes(1);
|
||||
expect(mockSaveSummary).toHaveBeenCalledWith('Recovered!');
|
||||
});
|
||||
|
||||
describe('Model Steering', () => {
|
||||
|
||||
@@ -317,8 +317,10 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
|
||||
await this.tryCompressChat(chat, promptId, combinedSignal);
|
||||
|
||||
const { functionCalls } = await promptIdContext.run(promptId, async () =>
|
||||
this.callModel(chat, currentMessage, combinedSignal, promptId),
|
||||
const { functionCalls, modelToUse } = await promptIdContext.run(
|
||||
promptId,
|
||||
async () =>
|
||||
this.callModel(chat, currentMessage, combinedSignal, promptId),
|
||||
);
|
||||
|
||||
if (combinedSignal.aborted) {
|
||||
@@ -348,6 +350,8 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
|
||||
const { nextMessage, submittedOutput, taskCompleted, aborted } =
|
||||
await this.processFunctionCalls(
|
||||
chat,
|
||||
modelToUse,
|
||||
functionCalls,
|
||||
combinedSignal,
|
||||
promptId,
|
||||
@@ -722,8 +726,17 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
}
|
||||
}
|
||||
|
||||
// === FINAL RETURN LOGIC ===
|
||||
if (terminateReason === AgentTerminateMode.GOAL) {
|
||||
// Save the session summary upon completion
|
||||
if (finalResult && chat) {
|
||||
try {
|
||||
const summary = this.getTruncatedSummary(finalResult);
|
||||
chat.getChatRecordingService()?.saveSummary(summary);
|
||||
} catch (error) {
|
||||
debugLogger.warn('Failed to save subagent session summary.', error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
result: finalResult || 'Task completed.',
|
||||
terminate_reason: terminateReason,
|
||||
@@ -759,6 +772,18 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
// Recovery Succeeded
|
||||
terminateReason = AgentTerminateMode.GOAL;
|
||||
finalResult = recoveryResult;
|
||||
|
||||
// Save the session summary upon successful recovery
|
||||
try {
|
||||
const summary = this.getTruncatedSummary(finalResult);
|
||||
chat.getChatRecordingService()?.saveSummary(summary);
|
||||
} catch (summaryError) {
|
||||
debugLogger.warn(
|
||||
'Failed to save subagent session summary during recovery.',
|
||||
summaryError,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
result: finalResult,
|
||||
terminate_reason: terminateReason,
|
||||
@@ -846,7 +871,11 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
message: Content,
|
||||
signal: AbortSignal,
|
||||
promptId: string,
|
||||
): Promise<{ functionCalls: FunctionCall[]; textResponse: string }> {
|
||||
): Promise<{
|
||||
functionCalls: FunctionCall[];
|
||||
textResponse: string;
|
||||
modelToUse: string;
|
||||
}> {
|
||||
const modelConfigAlias = getModelConfigAlias(this.definition);
|
||||
|
||||
// Resolve the model config early to get the concrete model string (which may be `auto`).
|
||||
@@ -931,7 +960,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
}
|
||||
}
|
||||
|
||||
return { functionCalls, textResponse };
|
||||
return { functionCalls, textResponse, modelToUse };
|
||||
}
|
||||
|
||||
/** Initializes a `GeminiChat` instance for the agent run. */
|
||||
@@ -985,6 +1014,8 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
* @returns A new `Content` object for history, any submitted output, and completion status.
|
||||
*/
|
||||
private async processFunctionCalls(
|
||||
chat: GeminiChat,
|
||||
model: string,
|
||||
functionCalls: FunctionCall[],
|
||||
signal: AbortSignal,
|
||||
promptId: string,
|
||||
@@ -1226,6 +1257,9 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
},
|
||||
);
|
||||
|
||||
// Record completed tool calls for persistent chat history
|
||||
chat.recordCompletedToolCalls(model, completedCalls);
|
||||
|
||||
for (const call of completedCalls) {
|
||||
const toolName =
|
||||
toolNameMap.get(call.request.callId) || call.request.name;
|
||||
@@ -1475,4 +1509,15 @@ Important Rules:
|
||||
this.onActivity(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates a string to 200 characters in a Unicode-safe way for session summaries.
|
||||
*/
|
||||
private getTruncatedSummary(text: string): string {
|
||||
const chars = Array.from(text);
|
||||
if (chars.length <= 200) {
|
||||
return text;
|
||||
}
|
||||
return chars.slice(0, 197).join('') + '...';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user