fix(core): make subagents aware of active approval modes (#23608)

This commit is contained in:
AK
2026-05-01 15:21:38 -07:00
committed by GitHub
parent de8fdcfa16
commit 40b384de2c
9 changed files with 124 additions and 8 deletions
@@ -7,6 +7,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { GeneralistAgent } from './generalist-agent.js';
import { makeFakeConfig } from '../test-utils/config.js';
import { ApprovalMode } from '../policy/types.js';
import type { ToolRegistry } from '../tools/tool-registry.js';
import type { AgentRegistry } from './registry.js';
@@ -54,4 +55,35 @@ describe('GeneralistAgent', () => {
// Ensure it's non-interactive
expect(agent.promptConfig.systemPrompt).toContain('non-interactive');
});
it('should adjust its description dynamically based on the approval mode', () => {
const config = makeFakeConfig();
const mockToolRegistry = {
getAllToolNames: () => ['tool1'],
} as unknown as ToolRegistry;
Object.defineProperty(config, 'toolRegistry', {
get: () => mockToolRegistry,
});
Object.defineProperty(config, 'config', {
get() {
return this;
},
});
const agent = GeneralistAgent(config);
// Default description
vi.spyOn(config, 'getApprovalMode').mockReturnValue(ApprovalMode.DEFAULT);
expect(agent.description).toContain('batch refactoring/error fixing');
expect(agent.description).not.toContain(
'large-scale investigation and batch planning',
);
// Plan Mode description
vi.spyOn(config, 'getApprovalMode').mockReturnValue(ApprovalMode.PLAN);
expect(agent.description).not.toContain('batch refactoring/error fixing');
expect(agent.description).toContain(
'large-scale investigation and batch planning',
);
});
});
+10 -2
View File
@@ -9,6 +9,8 @@ import type { AgentLoopContext } from '../config/agent-loop-context.js';
import { getCoreSystemPrompt } from '../core/prompts.js';
import type { LocalAgentDefinition } from './types.js';
import { ApprovalMode } from '../policy/types.js';
const GeneralistAgentSchema = z.object({
response: z.string().describe('The final response from the agent.'),
});
@@ -23,8 +25,14 @@ export const GeneralistAgent = (
kind: 'local',
name: 'generalist',
displayName: 'Generalist Agent',
description:
'A general-purpose AI agent with access to all tools. Highly recommended for tasks that are turn-intensive or involve processing large amounts of data. Use this to keep the main session history lean and efficient. Excellent for: batch refactoring/error fixing across multiple files, running commands with high-volume output, and speculative investigations.',
get description() {
const baseDescription =
'A general-purpose AI agent with access to all tools. Highly recommended for tasks that are turn-intensive or involve processing large amounts of data. Use this to keep the main session history lean and efficient. Excellent for: ';
if (context.config.getApprovalMode() === ApprovalMode.PLAN) {
return `${baseDescription}large-scale investigation and batch planning across multiple files.`;
}
return `${baseDescription}batch refactoring/error fixing across multiple files, running commands with high-volume output, and speculative investigations.`;
},
inputConfig: {
inputSchema: {
type: 'object',
@@ -105,6 +105,7 @@ import {
type OutputConfig,
SubagentActivityErrorType,
} from './types.js';
import { ApprovalMode } from '../policy/types.js';
import {
ToolConfirmationOutcome,
type AnyDeclarativeTool,
@@ -1276,6 +1277,42 @@ describe('LocalAgentExecutor', () => {
expect(mockScheduleAgentTools).toHaveBeenCalledTimes(2);
});
it('should inject Plan Mode context into the system prompt when in Plan Mode', async () => {
const definition = createTestDefinition([LS_TOOL_NAME], {}, 'none');
vi.spyOn(mockConfig, 'getApprovalMode').mockReturnValue(
ApprovalMode.PLAN,
);
vi.spyOn(mockConfig.storage, 'getPlansDir').mockReturnValue(
'/mock/plans',
);
const executor = await LocalAgentExecutor.create(
definition,
mockConfig,
onActivity,
);
// Turn 1: Model calls complete_task immediately
mockModelResponse(
[
{
name: COMPLETE_TASK_TOOL_NAME,
args: { result: 'Plan done' },
id: 'call1',
},
],
'Task finished.',
);
await executor.run({ goal: 'Do plan' }, signal);
const systemInstruction = MockedGeminiChat.mock.calls[0][1];
expect(systemInstruction).toContain('Execution Constraints');
expect(systemInstruction).toContain(
'You are currently operating in Plan Mode. Your write tools are globally restricted to only modifying plan (.md) files in the plans directory: /mock/plans/',
);
});
it('should error immediately if the model stops tools without calling complete_task (Protocol Violation)', async () => {
const definition = createTestDefinition();
const executor = await LocalAgentExecutor.create(
@@ -6,6 +6,7 @@
import { type AgentLoopContext } from '../config/agent-loop-context.js';
import { reportError } from '../utils/errorReporting.js';
import { ApprovalMode } from '../policy/types.js';
import { GeminiChat, StreamEventType } from '../core/geminiChat.js';
import {
type Content,
@@ -1355,6 +1356,12 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
const dirContext = await getDirectoryContextString(this.context.config);
finalPrompt += `\n\n# Environment Context\n${dirContext}`;
const approvalMode = this.context.config.getApprovalMode();
if (approvalMode === ApprovalMode.PLAN) {
const plansDir = this.context.config.storage.getPlansDir();
finalPrompt += `\n\n# Execution Constraints\nYou are currently operating in Plan Mode. Your write tools are globally restricted to only modifying plan (.md) files in the plans directory: ${plansDir}/. Do not attempt to modify source code directly.`;
}
// Append standard rules for non-interactive execution.
finalPrompt += `
Important Rules: