fix(acp): resolve agent mode disconnect and improve mode awareness (#26332)

This commit is contained in:
Sri Pasumarthi
2026-05-01 16:00:10 -07:00
committed by GitHub
parent 40b384de2c
commit 4e175527a2
12 changed files with 186 additions and 40 deletions
+4
View File
@@ -33,6 +33,10 @@ export class GeminiAgent {
this.sessionManager = new AcpSessionManager(settings, argv, connection);
}
dispose(): void {
this.sessionManager.dispose();
}
async initialize(
args: acp.InitializeRequest,
): Promise<acp.InitializeResponse> {
+22
View File
@@ -564,4 +564,26 @@ describe('Session', () => {
expect(result.stopReason).toBe('max_turn_requests');
});
it('should send sessionUpdate when approval mode changes', async () => {
const { coreEvents, CoreEvent, ApprovalMode } = await import(
'@google/gemini-cli-core'
);
coreEvents.emit(CoreEvent.ApprovalModeChanged, {
sessionId: 'session-1',
mode: ApprovalMode.PLAN,
});
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith({
sessionId: 'session-1',
update: {
sessionUpdate: 'agent_message_chunk',
content: {
type: 'text',
text: `[MODE_UPDATE] ${ApprovalMode.PLAN}`,
},
},
});
});
});
+28 -1
View File
@@ -8,6 +8,9 @@ import {
type ApprovalMode,
type ConversationRecord,
CoreToolCallStatus,
coreEvents,
CoreEvent,
type ApprovalModeChangedPayload,
logToolCall,
convertToFunctionResponse,
ToolConfirmationOutcome,
@@ -69,7 +72,31 @@ export class Session {
private readonly context: AgentLoopContext,
private readonly connection: acp.AgentSideConnection,
private readonly settings: LoadedSettings,
) {}
) {
coreEvents.on(
CoreEvent.ApprovalModeChanged,
this.handleApprovalModeChanged,
);
}
private handleApprovalModeChanged = (payload: ApprovalModeChangedPayload) => {
if (payload.sessionId === this.id) {
void this.sendUpdate({
sessionUpdate: 'agent_message_chunk',
content: {
type: 'text',
text: `[MODE_UPDATE] ${payload.mode}`,
},
});
}
};
dispose(): void {
coreEvents.off(
CoreEvent.ApprovalModeChanged,
this.handleApprovalModeChanged,
);
}
async cancelPendingPrompt(): Promise<void> {
if (!this.pendingPrompt) {
+13
View File
@@ -48,6 +48,13 @@ export class AcpSessionManager {
return this.sessions.get(sessionId);
}
dispose(): void {
for (const session of this.sessions.values()) {
session.dispose();
}
this.sessions.clear();
}
async newSession(
{ cwd, mcpServers }: acp.NewSessionRequest,
authDetails: AuthDetails,
@@ -183,6 +190,12 @@ export class AcpSessionManager {
this.connection,
this.settings,
);
const existingSession = this.sessions.get(sessionId);
if (existingSession) {
existingSession.dispose();
}
this.sessions.set(sessionId, session);
// Stream history back to client
+1
View File
@@ -203,6 +203,7 @@ const mockCoreEvents = vi.hoisted(() => ({
emitConsoleLog: vi.fn(),
emitQuotaChanged: vi.fn(),
on: vi.fn(),
emit: vi.fn(),
}));
const mockSetGlobalProxy = vi.hoisted(() => vi.fn());
+18 -16
View File
@@ -2697,26 +2697,28 @@ export class Config implements McpContext, AgentLoopContext {
this,
new ApprovalModeSwitchEvent(currentMode, mode),
);
}
this.policyEngine.setApprovalMode(mode);
this.refreshSandboxManager();
this.policyEngine.setApprovalMode(mode);
this.refreshSandboxManager();
coreEvents.emit(CoreEvent.ApprovalModeChanged, {
sessionId: this.getSessionId(),
mode,
});
const isPlanModeTransition =
currentMode !== mode &&
(currentMode === ApprovalMode.PLAN || mode === ApprovalMode.PLAN);
const isYoloModeTransition =
currentMode !== mode &&
(currentMode === ApprovalMode.YOLO || mode === ApprovalMode.YOLO);
const isPlanModeTransition =
currentMode === ApprovalMode.PLAN || mode === ApprovalMode.PLAN;
const isYoloModeTransition =
currentMode === ApprovalMode.YOLO || mode === ApprovalMode.YOLO;
if (isPlanModeTransition || isYoloModeTransition) {
if (this._geminiClient?.isInitialized()) {
this._geminiClient.clearCurrentSequenceModel();
this._geminiClient.setTools().catch((err) => {
debugLogger.error('Failed to update tools', err);
});
if (isPlanModeTransition || isYoloModeTransition) {
if (this._geminiClient?.isInitialized()) {
this._geminiClient.clearCurrentSequenceModel();
this._geminiClient.setTools().catch((err) => {
debugLogger.error('Failed to update tools', err);
});
}
this.updateSystemInstructionIfInitialized();
}
this.updateSystemInstructionIfInitialized();
}
}
@@ -1,7 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Core System Prompt (prompts.ts) > ApprovalMode in System Prompt > Approved Plan in Plan Mode > should NOT include approved plan section if no plan is set in config 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Plan** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -181,7 +181,7 @@ ONLY use the built-in \`exit_plan_mode\` tool to present the plan for formal app
`;
exports[`Core System Prompt (prompts.ts) > ApprovalMode in System Prompt > Approved Plan in Plan Mode > should include approved plan path when set in config 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Plan** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -482,7 +482,7 @@ Your core function is efficient and safe assistance. Balance extreme conciseness
`;
exports[`Core System Prompt (prompts.ts) > ApprovalMode in System Prompt > should include PLAN mode instructions 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Plan** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -662,7 +662,7 @@ ONLY use the built-in \`exit_plan_mode\` tool to present the plan for formal app
`;
exports[`Core System Prompt (prompts.ts) > should append userMemory with separator when provided 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -843,7 +843,7 @@ Be extra polite.
`;
exports[`Core System Prompt (prompts.ts) > should handle CodebaseInvestigator (enabled=false) 1`] = `
"You are Gemini CLI, an autonomous CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an autonomous CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -976,7 +976,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
`;
exports[`Core System Prompt (prompts.ts) > should handle CodebaseInvestigator (enabled=true) 1`] = `
"You are Gemini CLI, an autonomous CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an autonomous CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -1591,7 +1591,7 @@ Your core function is efficient and safe assistance. Balance extreme conciseness
`;
exports[`Core System Prompt (prompts.ts) > should include available_skills with updated verbiage for preview models 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -1768,7 +1768,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
`;
exports[`Core System Prompt (prompts.ts) > should include correct sandbox instructions for SANDBOX=sandbox-exec 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -1936,7 +1936,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
`;
exports[`Core System Prompt (prompts.ts) > should include correct sandbox instructions for SANDBOX=true 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -2104,7 +2104,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
`;
exports[`Core System Prompt (prompts.ts) > should include correct sandbox instructions for SANDBOX=undefined 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -2268,7 +2268,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
`;
exports[`Core System Prompt (prompts.ts) > should include mandate to distinguish between Directives and Inquiries 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -2432,7 +2432,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
`;
exports[`Core System Prompt (prompts.ts) > should include modern approved plan instructions with completion in DEFAULT mode when approvedPlanPath is set 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -2590,7 +2590,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
`;
exports[`Core System Prompt (prompts.ts) > should include planning phase suggestion when enter_plan_mode tool is enabled 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -2722,7 +2722,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
`;
exports[`Core System Prompt (prompts.ts) > should include sub-agents in XML for preview models when invoke_agent tool is enabled 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -3014,7 +3014,7 @@ Your core function is efficient and safe assistance. Balance extreme conciseness
`;
exports[`Core System Prompt (prompts.ts) > should include the TASK MANAGEMENT PROTOCOL when task tracker is enabled 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -3436,7 +3436,7 @@ project context
`;
exports[`Core System Prompt (prompts.ts) > should return the base prompt when userMemory is empty string 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -3600,7 +3600,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
`;
exports[`Core System Prompt (prompts.ts) > should return the base prompt when userMemory is whitespace only 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -3878,7 +3878,7 @@ Your core function is efficient and safe assistance. Balance extreme conciseness
`;
exports[`Core System Prompt (prompts.ts) > should use chatty system prompt for preview flash model 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
@@ -4042,7 +4042,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
`;
exports[`Core System Prompt (prompts.ts) > should use chatty system prompt for preview model 1`] = `
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.
"You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. You are currently operating in **Default** mode. Your primary goal is to help users safely and effectively.
# Core Mandates
+23
View File
@@ -2055,6 +2055,29 @@ ${JSON.stringify(
);
});
it('should update system instruction when ApprovalModeChanged event is emitted', async () => {
const { ApprovalMode } = await import('../policy/types.js');
vi.mocked(mockConfig.getSessionId).mockReturnValue('session-1');
vi.mocked(mockConfig.getSystemInstructionMemory).mockReturnValue(
'Current Memory',
);
const { getCoreSystemPrompt } = await import('./prompts.js');
const mockGetCoreSystemPrompt = vi.mocked(getCoreSystemPrompt);
mockGetCoreSystemPrompt.mockClear();
coreEvents.emit(CoreEvent.ApprovalModeChanged, {
sessionId: 'session-1',
mode: ApprovalMode.YOLO,
});
expect(mockGetCoreSystemPrompt).toHaveBeenCalledWith(
mockConfig,
'Current Memory',
);
});
it('should propagate InvalidStream events without injecting "Please continue." or recursing', async () => {
// Arrange: a single turn that yields an InvalidStream event.
const mockStream = (async function* () {
+19 -1
View File
@@ -67,7 +67,11 @@ import {
} from '../availability/policyHelpers.js';
import { getDisplayString, resolveModel } from '../config/models.js';
import { partToString } from '../utils/partUtils.js';
import { coreEvents, CoreEvent } from '../utils/events.js';
import {
coreEvents,
CoreEvent,
type ApprovalModeChangedPayload,
} from '../utils/events.js';
import { initializeContextManager } from '../context/initializer.js';
const MAX_TURNS = 100;
@@ -116,6 +120,10 @@ export class GeminiClient {
coreEvents.on(CoreEvent.ModelChanged, this.handleModelChanged);
coreEvents.on(CoreEvent.MemoryChanged, this.handleMemoryChanged);
coreEvents.on(
CoreEvent.ApprovalModeChanged,
this.handleApprovalModeChanged,
);
}
private get config(): Config {
@@ -130,6 +138,12 @@ export class GeminiClient {
this.updateSystemInstruction();
};
private handleApprovalModeChanged = (payload: ApprovalModeChangedPayload) => {
if (payload.sessionId === this.config.getSessionId()) {
this.updateSystemInstruction();
}
};
clearCurrentSequenceModel(): void {
this.currentSequenceModel = null;
}
@@ -314,6 +328,10 @@ export class GeminiClient {
dispose() {
coreEvents.off(CoreEvent.ModelChanged, this.handleModelChanged);
coreEvents.off(CoreEvent.MemoryChanged, this.handleMemoryChanged);
coreEvents.off(
CoreEvent.ApprovalModeChanged,
this.handleApprovalModeChanged,
);
}
async resumeChat(
@@ -142,6 +142,7 @@ export class PromptProvider {
const options: snippets.SystemPromptOptions = {
preamble: this.withSection('preamble', () => ({
interactive: interactiveMode,
approvalMode,
})),
coreMandates: this.withSection('coreMandates', () => ({
interactive: interactiveMode,
+13 -3
View File
@@ -37,6 +37,7 @@ import {
} from '../tools/tool-names.js';
import type { HierarchicalMemory } from '../config/memory.js';
import { DEFAULT_CONTEXT_FILENAME } from '../tools/memoryTool.js';
import type { ApprovalMode } from '../policy/types.js';
// --- Options Structs ---
@@ -57,6 +58,7 @@ export interface SystemPromptOptions {
export interface PreambleOptions {
interactive: boolean;
approvalMode: ApprovalMode;
}
export interface CoreMandatesOptions {
@@ -188,9 +190,17 @@ ${renderUserMemory(userMemory, contextFilenames)}
export function renderPreamble(options?: PreambleOptions): string {
if (!options) return '';
return options.interactive
? 'You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.'
: 'You are Gemini CLI, an autonomous CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and effectively.';
let modeStr = 'Default';
if (options.approvalMode === 'plan') modeStr = 'Plan';
if (options.approvalMode === 'yolo') modeStr = 'YOLO';
if (options.approvalMode === 'autoEdit') modeStr = 'Auto-Edit';
const base = options.interactive
? 'You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks.'
: 'You are Gemini CLI, an autonomous CLI agent specializing in software engineering tasks.';
return `${base} You are currently operating in **${modeStr}** mode. Your primary goal is to help users safely and effectively.`;
}
export function renderCoreMandates(options?: CoreMandatesOptions): string {
+25
View File
@@ -14,6 +14,7 @@ import type {
KeychainAvailabilityEvent,
} from '../telemetry/types.js';
import { debugLogger } from './debugLogger.js';
import type { ApprovalMode } from '../policy/types.js';
/**
* Defines the severity level for user-facing feedback.
@@ -52,6 +53,20 @@ export interface ModelChangedPayload {
model: string;
}
/**
* Payload for the 'approval-mode-changed' event.
*/
export interface ApprovalModeChangedPayload {
/**
* The session ID associated with the mode change.
*/
sessionId: string;
/**
* The new approval mode.
*/
mode: ApprovalMode;
}
/**
* Payload for the 'console-log' event.
*/
@@ -181,6 +196,7 @@ export interface QuotaChangedPayload {
export enum CoreEvent {
UserFeedback = 'user-feedback',
ModelChanged = 'model-changed',
ApprovalModeChanged = 'approval-mode-changed',
ConsoleLog = 'console-log',
Output = 'output',
MemoryChanged = 'memory-changed',
@@ -215,6 +231,7 @@ export interface EditorSelectedPayload {
export interface CoreEvents extends ExtensionEvents {
[CoreEvent.UserFeedback]: [UserFeedbackPayload];
[CoreEvent.ModelChanged]: [ModelChangedPayload];
[CoreEvent.ApprovalModeChanged]: [ApprovalModeChangedPayload];
[CoreEvent.ConsoleLog]: [ConsoleLogPayload];
[CoreEvent.Output]: [OutputPayload];
[CoreEvent.MemoryChanged]: [MemoryChangedPayload];
@@ -327,6 +344,14 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
this.emit(CoreEvent.ModelChanged, payload);
}
/**
* Notifies subscribers that the approval mode has changed.
*/
emitApprovalModeChanged(sessionId: string, mode: ApprovalMode): void {
const payload: ApprovalModeChangedPayload = { sessionId, mode };
this.emit(CoreEvent.ApprovalModeChanged, payload);
}
/**
* Notifies subscribers that settings have been modified.
*/