mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
feat(core): support dynamic variable substitution in system prompt override (#17042)
This commit is contained in:
@@ -56,6 +56,38 @@ error with: `missing system prompt file '<path>'`.
|
|||||||
When `GEMINI_SYSTEM_MD` is active, the CLI shows a `|⌐■_■|` indicator in the UI
|
When `GEMINI_SYSTEM_MD` is active, the CLI shows a `|⌐■_■|` indicator in the UI
|
||||||
to signal custom system‑prompt mode.
|
to signal custom system‑prompt mode.
|
||||||
|
|
||||||
|
## Variable Substitution
|
||||||
|
|
||||||
|
When using a custom system prompt file, you can use the following variables to
|
||||||
|
dynamically include built-in content:
|
||||||
|
|
||||||
|
- `${AgentSkills}`: Injects a complete section (including header) of all
|
||||||
|
available agent skills.
|
||||||
|
- `${SubAgents}`: Injects a complete section (including header) of available
|
||||||
|
sub-agents.
|
||||||
|
- `${AvailableTools}`: Injects a bulleted list of all currently enabled tool
|
||||||
|
names.
|
||||||
|
- Tool Name Variables: Injects the actual name of a tool using the pattern:
|
||||||
|
`${toolName}_ToolName` (e.g., `${write_file_ToolName}`,
|
||||||
|
`${run_shell_command_ToolName}`).
|
||||||
|
|
||||||
|
This pattern is generated dynamically for all available tools.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Custom System Prompt
|
||||||
|
|
||||||
|
You are a helpful assistant. ${AgentSkills}
|
||||||
|
${SubAgents}
|
||||||
|
|
||||||
|
## Tooling
|
||||||
|
|
||||||
|
The following tools are available to you: ${AvailableTools}
|
||||||
|
|
||||||
|
You can use ${write_file_ToolName} to save logs.
|
||||||
|
```
|
||||||
|
|
||||||
## Export the default prompt (recommended)
|
## Export the default prompt (recommended)
|
||||||
|
|
||||||
Before overriding, export the current default prompt so you can review required
|
Before overriding, export the current default prompt so you can review required
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { getCoreSystemPrompt } from './prompts.js';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import type { Config } from '../config/config.js';
|
||||||
|
import * as toolNames from '../tools/tool-names.js';
|
||||||
|
|
||||||
|
vi.mock('node:fs');
|
||||||
|
vi.mock('../utils/gitUtils', () => ({
|
||||||
|
isGitRepository: vi.fn().mockReturnValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Core System Prompt Substitution', () => {
|
||||||
|
let mockConfig: Config;
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
vi.stubEnv('GEMINI_SYSTEM_MD', 'true');
|
||||||
|
mockConfig = {
|
||||||
|
getToolRegistry: vi.fn().mockReturnValue({
|
||||||
|
getAllToolNames: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue([
|
||||||
|
toolNames.WRITE_FILE_TOOL_NAME,
|
||||||
|
toolNames.READ_FILE_TOOL_NAME,
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
|
||||||
|
storage: {
|
||||||
|
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project-temp'),
|
||||||
|
},
|
||||||
|
isInteractive: vi.fn().mockReturnValue(true),
|
||||||
|
isInteractiveShellEnabled: vi.fn().mockReturnValue(true),
|
||||||
|
isAgentsEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
getModel: vi.fn().mockReturnValue('auto'),
|
||||||
|
getActiveModel: vi.fn().mockReturnValue('gemini-1.5-pro'),
|
||||||
|
getPreviewFeatures: vi.fn().mockReturnValue(false),
|
||||||
|
getAgentRegistry: vi.fn().mockReturnValue({
|
||||||
|
getDirectoryContext: vi.fn().mockReturnValue('Mock Agent Directory'),
|
||||||
|
}),
|
||||||
|
getSkillManager: vi.fn().mockReturnValue({
|
||||||
|
getSkills: vi.fn().mockReturnValue([]),
|
||||||
|
}),
|
||||||
|
} as unknown as Config;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should substitute ${AgentSkills} in custom system prompt', () => {
|
||||||
|
const skills = [
|
||||||
|
{
|
||||||
|
name: 'test-skill',
|
||||||
|
description: 'A test skill description',
|
||||||
|
location: '/path/to/test-skill/SKILL.md',
|
||||||
|
body: 'Skill content',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
vi.mocked(mockConfig.getSkillManager().getSkills).mockReturnValue(skills);
|
||||||
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
|
vi.mocked(fs.readFileSync).mockReturnValue(
|
||||||
|
'Skills go here: ${AgentSkills}',
|
||||||
|
);
|
||||||
|
|
||||||
|
const prompt = getCoreSystemPrompt(mockConfig);
|
||||||
|
|
||||||
|
expect(prompt).toContain('Skills go here:');
|
||||||
|
expect(prompt).toContain('<available_skills>');
|
||||||
|
expect(prompt).toContain('<name>test-skill</name>');
|
||||||
|
expect(prompt).not.toContain('${AgentSkills}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should substitute ${SubAgents} in custom system prompt', () => {
|
||||||
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
|
vi.mocked(fs.readFileSync).mockReturnValue('Agents: ${SubAgents}');
|
||||||
|
vi.mocked(
|
||||||
|
mockConfig.getAgentRegistry().getDirectoryContext,
|
||||||
|
).mockReturnValue('Actual Agent Directory');
|
||||||
|
|
||||||
|
const prompt = getCoreSystemPrompt(mockConfig);
|
||||||
|
|
||||||
|
expect(prompt).toContain('Agents: Actual Agent Directory');
|
||||||
|
expect(prompt).not.toContain('${SubAgents}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should substitute ${AvailableTools} in custom system prompt', () => {
|
||||||
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
|
vi.mocked(fs.readFileSync).mockReturnValue('Tools:\n${AvailableTools}');
|
||||||
|
|
||||||
|
const prompt = getCoreSystemPrompt(mockConfig);
|
||||||
|
|
||||||
|
expect(prompt).toContain(
|
||||||
|
`Tools:\n- ${toolNames.WRITE_FILE_TOOL_NAME}\n- ${toolNames.READ_FILE_TOOL_NAME}`,
|
||||||
|
);
|
||||||
|
expect(prompt).not.toContain('${AvailableTools}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should substitute tool names using the ${toolName}_ToolName pattern', () => {
|
||||||
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
|
vi.mocked(fs.readFileSync).mockReturnValue(
|
||||||
|
'Use ${write_file_ToolName} and ${read_file_ToolName}.',
|
||||||
|
);
|
||||||
|
|
||||||
|
const prompt = getCoreSystemPrompt(mockConfig);
|
||||||
|
|
||||||
|
expect(prompt).toContain(
|
||||||
|
`Use ${toolNames.WRITE_FILE_TOOL_NAME} and ${toolNames.READ_FILE_TOOL_NAME}.`,
|
||||||
|
);
|
||||||
|
expect(prompt).not.toContain('${write_file_ToolName}');
|
||||||
|
expect(prompt).not.toContain('${read_file_ToolName}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not substitute old patterns', () => {
|
||||||
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
|
vi.mocked(fs.readFileSync).mockReturnValue(
|
||||||
|
'${WriteFileToolName} and ${WRITE_FILE_TOOL_NAME}',
|
||||||
|
);
|
||||||
|
|
||||||
|
const prompt = getCoreSystemPrompt(mockConfig);
|
||||||
|
|
||||||
|
expect(prompt).toBe('${WriteFileToolName} and ${WRITE_FILE_TOOL_NAME}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not substitute disabled tool names', () => {
|
||||||
|
vi.mocked(mockConfig.getToolRegistry().getAllToolNames).mockReturnValue([]);
|
||||||
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
|
vi.mocked(fs.readFileSync).mockReturnValue('Use ${write_file_ToolName}.');
|
||||||
|
|
||||||
|
const prompt = getCoreSystemPrompt(mockConfig);
|
||||||
|
|
||||||
|
expect(prompt).toBe('Use ${write_file_ToolName}.');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -26,6 +26,7 @@ import { GEMINI_DIR, homedir } from '../utils/paths.js';
|
|||||||
import { debugLogger } from '../utils/debugLogger.js';
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
import { WriteTodosTool } from '../tools/write-todos.js';
|
import { WriteTodosTool } from '../tools/write-todos.js';
|
||||||
import { resolveModel, isPreviewModel } from '../config/models.js';
|
import { resolveModel, isPreviewModel } from '../config/models.js';
|
||||||
|
import type { SkillDefinition } from '../skills/skillLoader.js';
|
||||||
|
|
||||||
export function resolvePathFromEnv(envVar?: string): {
|
export function resolvePathFromEnv(envVar?: string): {
|
||||||
isSwitch: boolean;
|
isSwitch: boolean;
|
||||||
@@ -132,32 +133,12 @@ export function getCoreSystemPrompt(
|
|||||||
const interactiveMode = interactiveOverride ?? config.isInteractive();
|
const interactiveMode = interactiveOverride ?? config.isInteractive();
|
||||||
|
|
||||||
const skills = config.getSkillManager().getSkills();
|
const skills = config.getSkillManager().getSkills();
|
||||||
let skillsPrompt = '';
|
const skillsPrompt = getSkillsPrompt(skills);
|
||||||
if (skills.length > 0) {
|
|
||||||
const skillsXml = skills
|
|
||||||
.map(
|
|
||||||
(skill) => ` <skill>
|
|
||||||
<name>${skill.name}</name>
|
|
||||||
<description>${skill.description}</description>
|
|
||||||
<location>${skill.location}</location>
|
|
||||||
</skill>`,
|
|
||||||
)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
skillsPrompt = `
|
|
||||||
# Available Agent Skills
|
|
||||||
|
|
||||||
You have access to the following specialized skills. To activate a skill and receive its detailed instructions, you can call the \`${ACTIVATE_SKILL_TOOL_NAME}\` tool with the skill's name.
|
|
||||||
|
|
||||||
<available_skills>
|
|
||||||
${skillsXml}
|
|
||||||
</available_skills>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let basePrompt: string;
|
let basePrompt: string;
|
||||||
if (systemMdEnabled) {
|
if (systemMdEnabled) {
|
||||||
basePrompt = fs.readFileSync(systemMdPath, 'utf8');
|
basePrompt = fs.readFileSync(systemMdPath, 'utf8');
|
||||||
|
basePrompt = applySubstitutions(basePrompt, config, skillsPrompt);
|
||||||
} else {
|
} else {
|
||||||
const promptConfig = {
|
const promptConfig = {
|
||||||
preamble: `You are ${interactiveMode ? 'an interactive ' : 'a non-interactive '}CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and efficiently, adhering strictly to the following instructions and utilizing your available tools.`,
|
preamble: `You are ${interactiveMode ? 'an interactive ' : 'a non-interactive '}CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and efficiently, adhering strictly to the following instructions and utilizing your available tools.`,
|
||||||
@@ -497,3 +478,64 @@ The structure MUST be as follows:
|
|||||||
</state_snapshot>
|
</state_snapshot>
|
||||||
`.trim();
|
`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSkillsPrompt(skills: SkillDefinition[]): string {
|
||||||
|
if (skills.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillsXml = skills
|
||||||
|
.map(
|
||||||
|
(skill) => ` <skill>
|
||||||
|
<name>${skill.name}</name>
|
||||||
|
<description>${skill.description}</description>
|
||||||
|
<location>${skill.location}</location>
|
||||||
|
</skill>`,
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return `
|
||||||
|
# Available Agent Skills
|
||||||
|
|
||||||
|
You have access to the following specialized skills. To activate a skill and receive its detailed instructions, you can call the \`${ACTIVATE_SKILL_TOOL_NAME}\` tool with the skill's name.
|
||||||
|
|
||||||
|
<available_skills>
|
||||||
|
${skillsXml}
|
||||||
|
</available_skills>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySubstitutions(
|
||||||
|
prompt: string,
|
||||||
|
config: Config,
|
||||||
|
skillsPrompt: string,
|
||||||
|
): string {
|
||||||
|
let result = prompt;
|
||||||
|
|
||||||
|
// Substitute skills and agents
|
||||||
|
result = result.replace(/\${AgentSkills}/g, skillsPrompt);
|
||||||
|
result = result.replace(
|
||||||
|
/\${SubAgents}/g,
|
||||||
|
config.getAgentRegistry().getDirectoryContext(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Substitute available tools list
|
||||||
|
const toolRegistry = config.getToolRegistry();
|
||||||
|
const allToolNames = toolRegistry.getAllToolNames();
|
||||||
|
const availableToolsList =
|
||||||
|
allToolNames.length > 0
|
||||||
|
? allToolNames.map((name) => `- ${name}`).join('\n')
|
||||||
|
: 'No tools are currently available.';
|
||||||
|
result = result.replace(/\${AvailableTools}/g, availableToolsList);
|
||||||
|
|
||||||
|
// Substitute tool names
|
||||||
|
for (const toolName of allToolNames) {
|
||||||
|
const varName = `${toolName}_ToolName`;
|
||||||
|
result = result.replace(
|
||||||
|
new RegExp(`\\\${\\b${varName}\\b}`, 'g'),
|
||||||
|
toolName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user