mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -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
|
||||
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)
|
||||
|
||||
Before overriding, export the current default prompt so you can review required
|
||||
|
||||
134
packages/core/src/core/prompts-substitution.test.ts
Normal file
134
packages/core/src/core/prompts-substitution.test.ts
Normal file
@@ -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 { WriteTodosTool } from '../tools/write-todos.js';
|
||||
import { resolveModel, isPreviewModel } from '../config/models.js';
|
||||
import type { SkillDefinition } from '../skills/skillLoader.js';
|
||||
|
||||
export function resolvePathFromEnv(envVar?: string): {
|
||||
isSwitch: boolean;
|
||||
@@ -132,32 +133,12 @@ export function getCoreSystemPrompt(
|
||||
const interactiveMode = interactiveOverride ?? config.isInteractive();
|
||||
|
||||
const skills = config.getSkillManager().getSkills();
|
||||
let skillsPrompt = '';
|
||||
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>
|
||||
`;
|
||||
}
|
||||
const skillsPrompt = getSkillsPrompt(skills);
|
||||
|
||||
let basePrompt: string;
|
||||
if (systemMdEnabled) {
|
||||
basePrompt = fs.readFileSync(systemMdPath, 'utf8');
|
||||
basePrompt = applySubstitutions(basePrompt, config, skillsPrompt);
|
||||
} else {
|
||||
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.`,
|
||||
@@ -497,3 +478,64 @@ The structure MUST be as follows:
|
||||
</state_snapshot>
|
||||
`.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