Files
gemini-cli/packages/core/src/prompts/promptProvider.ts

229 lines
7.2 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
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 {
resolvePathFromEnv,
applySubstitutions,
isSectionEnabled,
type ResolvedPath,
} from './utils.js';
import { CodebaseInvestigatorAgent } from '../agents/codebase-investigator.js';
import { isGitRepository } from '../utils/gitUtils.js';
import {
PLAN_MODE_TOOLS,
WRITE_TODOS_TOOL_NAME,
READ_FILE_TOOL_NAME,
ENTER_PLAN_MODE_TOOL_NAME,
} from '../tools/tool-names.js';
import { resolveModel, isPreviewModel } from '../config/models.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
/**
* Orchestrates prompt generation by gathering context and building options.
*/
export class PromptProvider {
/**
* Generates the core system prompt.
*/
getCoreSystemPrompt(
config: Config,
userMemory?: string,
interactiveOverride?: boolean,
): string {
const systemMdResolution = resolvePathFromEnv(
process.env['GEMINI_SYSTEM_MD'],
);
const interactiveMode = interactiveOverride ?? config.isInteractive();
const approvalMode = config.getApprovalMode?.() ?? ApprovalMode.DEFAULT;
const isPlanMode = approvalMode === ApprovalMode.PLAN;
const skills = config.getSkillManager().getSkills();
const toolNames = config.getToolRegistry().getAllToolNames();
const enabledToolNames = new Set(toolNames);
const approvedPlanPath = config.getApprovedPlanPath();
const desiredModel = resolveModel(
config.getActiveModel(),
config.getPreviewFeatures(),
);
const isGemini3 = isPreviewModel(desiredModel);
// --- Context Gathering ---
let planModeToolsList = PLAN_MODE_TOOLS.filter((t) =>
enabledToolNames.has(t),
)
.map((t) => `- \`${t}\``)
.join('\n');
// Add read-only MCP tools to the list
if (isPlanMode) {
const allTools = config.getToolRegistry().getAllTools();
const readOnlyMcpTools = allTools.filter(
(t): t is DiscoveredMCPTool =>
t instanceof DiscoveredMCPTool && !!t.isReadOnly,
);
if (readOnlyMcpTools.length > 0) {
const mcpToolsList = readOnlyMcpTools
.map((t) => `- \`${t.name}\` (${t.serverName})`)
.join('\n');
planModeToolsList += `\n${mcpToolsList}`;
}
}
let basePrompt: string;
// --- Template File Override ---
if (systemMdResolution.value && !systemMdResolution.isDisabled) {
let systemMdPath = path.resolve(path.join(GEMINI_DIR, 'system.md'));
if (!systemMdResolution.isSwitch) {
systemMdPath = systemMdResolution.value;
}
if (!fs.existsSync(systemMdPath)) {
throw new Error(`missing system prompt file '${systemMdPath}'`);
}
basePrompt = fs.readFileSync(systemMdPath, 'utf8');
const skillsPrompt = snippets.renderAgentSkills(
skills.map((s) => ({
name: s.name,
description: s.description,
location: s.location,
})),
);
basePrompt = applySubstitutions(basePrompt, config, skillsPrompt);
} else {
// --- Standard Composition ---
const options: snippets.SystemPromptOptions = {
preamble: this.withSection('preamble', () => ({
interactive: interactiveMode,
})),
coreMandates: this.withSection('coreMandates', () => ({
interactive: interactiveMode,
isGemini3,
hasSkills: skills.length > 0,
})),
agentContexts: this.withSection('agentContexts', () =>
config.getAgentRegistry().getDirectoryContext(),
),
agentSkills: this.withSection(
'agentSkills',
() =>
skills.map((s) => ({
name: s.name,
description: s.description,
location: s.location,
})),
skills.length > 0,
),
hookContext: isSectionEnabled('hookContext') || undefined,
primaryWorkflows: this.withSection(
'primaryWorkflows',
() => ({
interactive: interactiveMode,
enableCodebaseInvestigator: enabledToolNames.has(
CodebaseInvestigatorAgent.name,
),
enableWriteTodosTool: enabledToolNames.has(WRITE_TODOS_TOOL_NAME),
enableEnterPlanModeTool: enabledToolNames.has(
ENTER_PLAN_MODE_TOOL_NAME,
),
approvedPlan: approvedPlanPath
? { path: approvedPlanPath }
: undefined,
}),
!isPlanMode,
),
planningWorkflow: this.withSection(
'planningWorkflow',
() => ({
planModeToolsList,
plansDir: config.storage.getProjectTempPlansDir(),
approvedPlanPath: config.getApprovedPlanPath(),
}),
isPlanMode,
),
operationalGuidelines: this.withSection(
'operationalGuidelines',
() => ({
interactive: interactiveMode,
isGemini3,
enableShellEfficiency: config.getEnableShellOutputEfficiency(),
}),
),
sandbox: this.withSection('sandbox', () => getSandboxMode()),
gitRepo: this.withSection(
'git',
() => ({ interactive: interactiveMode }),
isGitRepository(process.cwd()) ? true : false,
),
finalReminder: this.withSection('finalReminder', () => ({
readFileToolName: READ_FILE_TOOL_NAME,
})),
};
basePrompt = snippets.getCoreSystemPrompt(options);
}
// --- Finalization (Shell) ---
const finalPrompt = snippets.renderFinalShell(basePrompt, userMemory);
// Sanitize erratic newlines from composition
const sanitizedPrompt = finalPrompt.replace(/\n{3,}/g, '\n\n');
// Write back to file if requested
this.maybeWriteSystemMd(
sanitizedPrompt,
systemMdResolution,
path.resolve(path.join(GEMINI_DIR, 'system.md')),
);
return sanitizedPrompt;
}
getCompressionPrompt(): string {
return snippets.getCompressionPrompt();
}
private withSection<T>(
key: string,
factory: () => T,
guard: boolean = true,
): T | undefined {
return guard && isSectionEnabled(key) ? factory() : undefined;
}
private maybeWriteSystemMd(
basePrompt: string,
resolution: ResolvedPath,
defaultPath: string,
): void {
const writeSystemMdResolution = resolvePathFromEnv(
process.env['GEMINI_WRITE_SYSTEM_MD'],
);
if (writeSystemMdResolution.value && !writeSystemMdResolution.isDisabled) {
const writePath = writeSystemMdResolution.isSwitch
? defaultPath
: writeSystemMdResolution.value;
fs.mkdirSync(path.dirname(writePath), { recursive: true });
fs.writeFileSync(writePath, basePrompt);
}
}
}
// --- Internal Context Helpers ---
function getSandboxMode(): snippets.SandboxMode {
if (process.env['SANDBOX'] === 'sandbox-exec') return 'macos-seatbelt';
if (process.env['SANDBOX']) return 'generic';
return 'outside';
}