Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
702 changes: 420 additions & 282 deletions packages/core/src/core/__snapshots__/prompts.test.ts.snap

Large diffs are not rendered by default.

38 changes: 28 additions & 10 deletions packages/core/src/core/prompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
PREVIEW_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_MODEL_AUTO,
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
} from '../config/models.js';
import { ApprovalMode } from '../policy/types.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
Expand Down Expand Up @@ -94,6 +95,7 @@ describe('Core System Prompt (prompts.ts)', () => {
isInteractive: vi.fn().mockReturnValue(true),
isInteractiveShellEnabled: vi.fn().mockReturnValue(true),
isAgentsEnabled: vi.fn().mockReturnValue(false),
getPreviewFeatures: vi.fn().mockReturnValue(true),
getModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO),
getActiveModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL),
getMessageBus: vi.fn(),
Expand Down Expand Up @@ -152,10 +154,23 @@ describe('Core System Prompt (prompts.ts)', () => {
expect(prompt).not.toContain('activate_skill');
});

it('should use legacy system prompt for non-preview model', () => {
vi.mocked(mockConfig.getActiveModel).mockReturnValue(
DEFAULT_GEMINI_FLASH_LITE_MODEL,
);
const prompt = getCoreSystemPrompt(mockConfig);
expect(prompt).toContain(
'You are an interactive CLI agent specializing in software engineering tasks.',
);
expect(prompt).toContain('# Core Mandates');
expect(prompt).toContain('- **Conventions:**');
expect(prompt).toMatchSnapshot();
});

it('should use chatty system prompt for preview model', () => {
vi.mocked(mockConfig.getActiveModel).mockReturnValue(PREVIEW_GEMINI_MODEL);
const prompt = getCoreSystemPrompt(mockConfig);
expect(prompt).toContain('You are an interactive CLI agent'); // Check for core content
expect(prompt).toContain('You are Gemini CLI, an interactive CLI agent'); // Check for core content
expect(prompt).toContain('No Chitchat:');
expect(prompt).toMatchSnapshot();
});
Expand All @@ -165,7 +180,7 @@ describe('Core System Prompt (prompts.ts)', () => {
PREVIEW_GEMINI_FLASH_MODEL,
);
const prompt = getCoreSystemPrompt(mockConfig);
expect(prompt).toContain('You are an interactive CLI agent'); // Check for core content
expect(prompt).toContain('You are Gemini CLI, an interactive CLI agent'); // Check for core content
expect(prompt).toContain('No Chitchat:');
expect(prompt).toMatchSnapshot();
});
Expand All @@ -175,21 +190,24 @@ describe('Core System Prompt (prompts.ts)', () => {
['whitespace only', ' \n \t '],
])('should return the base prompt when userMemory is %s', (_, userMemory) => {
vi.stubEnv('SANDBOX', undefined);
vi.mocked(mockConfig.getActiveModel).mockReturnValue(PREVIEW_GEMINI_MODEL);
const prompt = getCoreSystemPrompt(mockConfig, userMemory);
expect(prompt).not.toContain('---\n\n'); // Separator should not be present
expect(prompt).toContain('You are an interactive CLI agent'); // Check for core content
expect(prompt).toContain('You are Gemini CLI, an interactive CLI agent'); // Check for core content
expect(prompt).toContain('No Chitchat:');
expect(prompt).toMatchSnapshot(); // Use snapshot for base prompt structure
});

it('should append userMemory with separator when provided', () => {
vi.stubEnv('SANDBOX', undefined);
vi.mocked(mockConfig.getActiveModel).mockReturnValue(PREVIEW_GEMINI_MODEL);
const memory = 'This is custom user memory.\nBe extra polite.';
const expectedSuffix = `\n\n---\n\n${memory}`;
const prompt = getCoreSystemPrompt(mockConfig, memory);

expect(prompt.endsWith(expectedSuffix)).toBe(true);
expect(prompt).toContain('You are an interactive CLI agent'); // Ensure base prompt follows
expect(prompt).toContain('# Contextual Instructions (GEMINI.md)');
expect(prompt).toContain('<loaded_context>');
expect(prompt).toContain(memory);
expect(prompt).toContain('You are Gemini CLI, an interactive CLI agent'); // Ensure base prompt follows
expect(prompt).toMatchSnapshot(); // Snapshot the combined prompt
});

Expand Down Expand Up @@ -257,7 +275,8 @@ describe('Core System Prompt (prompts.ts)', () => {
isInteractiveShellEnabled: vi.fn().mockReturnValue(false),
isAgentsEnabled: vi.fn().mockReturnValue(false),
getModel: vi.fn().mockReturnValue('auto'),
getActiveModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL),
getActiveModel: vi.fn().mockReturnValue(PREVIEW_GEMINI_MODEL),
getPreviewFeatures: vi.fn().mockReturnValue(true),
getAgentRegistry: vi.fn().mockReturnValue({
getDirectoryContext: vi.fn().mockReturnValue('Mock Agent Directory'),
}),
Expand All @@ -270,15 +289,14 @@ describe('Core System Prompt (prompts.ts)', () => {
const prompt = getCoreSystemPrompt(testConfig);
if (expectCodebaseInvestigator) {
expect(prompt).toContain(
`your **first and primary action** must be to delegate to the '${CodebaseInvestigatorAgent.name}' agent`,
`Utilize specialized sub-agents (e.g., \`codebase_investigator\`) as the primary mechanism for initial discovery`,
);
expect(prompt).toContain(`do not ignore the output of the agent`);
expect(prompt).not.toContain(
"Use 'grep_search' and 'glob' search tools extensively",
);
} else {
expect(prompt).not.toContain(
`your **first and primary action** must be to delegate to the '${CodebaseInvestigatorAgent.name}' agent`,
`Utilize specialized sub-agents (e.g., \`codebase_investigator\`) as the primary mechanism for initial discovery`,
);
expect(prompt).toContain(
"Use 'grep_search' and 'glob' search tools extensively",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/core/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ export function getCoreSystemPrompt(
/**
* Provides the system prompt for the history compression process.
*/
export function getCompressionPrompt(): string {
return new PromptProvider().getCompressionPrompt();
export function getCompressionPrompt(config: Config): string {
return new PromptProvider().getCompressionPrompt(config);
}
31 changes: 26 additions & 5 deletions packages/core/src/prompts/promptProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { Config } from '../config/config.js';
import { GEMINI_DIR } from '../utils/paths.js';
import { ApprovalMode } from '../policy/types.js';
import * as snippets from './snippets.js';
import * as legacySnippets from './snippets.legacy.js';
import {
resolvePathFromEnv,
applySubstitutions,
Expand Down Expand Up @@ -54,6 +55,19 @@ export class PromptProvider {

const desiredModel = resolveModel(config.getActiveModel());
const isGemini3 = isPreviewModel(desiredModel);
const activeSnippets = isGemini3 ? snippets : legacySnippets;

// --- Context Gathering ---
const planOptions: snippets.ApprovalModePlanOptions | undefined = isPlanMode
? {
planModeToolsList: PLAN_MODE_TOOLS.filter((t) =>
new Set(toolNames).has(t),
)
.map((t) => `- \`${t}\``)
.join('\n'),
plansDir: config.storage.getProjectTempPlansDir(),
}
: undefined;

// --- Context Gathering ---
let planModeToolsList = PLAN_MODE_TOOLS.filter((t) =>
Expand Down Expand Up @@ -89,7 +103,7 @@ export class PromptProvider {
throw new Error(`missing system prompt file '${systemMdPath}'`);
}
basePrompt = fs.readFileSync(systemMdPath, 'utf8');
const skillsPrompt = snippets.renderAgentSkills(
const skillsPrompt = activeSnippets.renderAgentSkills(
skills.map((s) => ({
name: s.name,
description: s.description,
Expand Down Expand Up @@ -167,11 +181,15 @@ export class PromptProvider {
})),
};

basePrompt = snippets.getCoreSystemPrompt(options);
basePrompt = activeSnippets.getCoreSystemPrompt(options);
}

// --- Finalization (Shell) ---
const finalPrompt = snippets.renderFinalShell(basePrompt, userMemory);
const finalPrompt = activeSnippets.renderFinalShell(
basePrompt,
userMemory,
planOptions,
);

// Sanitize erratic newlines from composition
const sanitizedPrompt = finalPrompt.replace(/\n{3,}/g, '\n\n');
Expand All @@ -186,8 +204,11 @@ export class PromptProvider {
return sanitizedPrompt;
}

getCompressionPrompt(): string {
return snippets.getCompressionPrompt();
getCompressionPrompt(config: Config): string {
const desiredModel = resolveModel(config.getActiveModel());
const isGemini3 = isPreviewModel(desiredModel);
const activeSnippets = isGemini3 ? snippets : legacySnippets;
return activeSnippets.getCompressionPrompt();
}

private withSection<T>(
Expand Down
Loading
Loading