feat(prompts): redesign SI for modularity, skill activation, and essential workflows

- Redesigned CORE_SI_SKELETON for maximum reasoning fidelity and minimum token usage.
- Extracted Software Engineering and New Application workflows to dynamic skills.
- Added 'Essential Workflows' section to maintain visibility for core user journeys.
- Implemented precedence-based sorting for available skills (Workspace > User > Built-in).
- Added behavioral tests in evals/skill_activation.eval.ts to verify proactive skill activation.
- Fixed pre-existing build error in useGeminiStream.ts related to missing FinishReason values.
This commit is contained in:
Aishanee Shah
2026-02-23 20:55:27 +00:00
parent 25797bed8c
commit 88985511c6
7 changed files with 228 additions and 10 deletions
+110
View File
@@ -0,0 +1,110 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe } from 'vitest';
import { evalTest } from './test-helper.js';
describe('Skill Activation Behavioral Evals', () => {
/**
* Tests that the model proactively activates the software-engineering skill
* when faced with a typical engineering task like bug fixing.
*/
evalTest('ALWAYS_PASSES', {
name: 'should activate software-engineering skill for bug fixes',
prompt:
'There is a bug in the greeting logic in src/index.ts. Please fix it.',
files: {
'src/index.ts':
'export const greet = (name: string) => `Hello, ${name}!`;',
'src/index.test.ts': `
import { greet } from './index';
import { expect, test } from 'vitest';
test('greet', () => { expect(greet('World')).toBe('Hello, World!'); });
`,
},
assert: async (rig) => {
await rig.expectToolCallSuccess(['activate_skill'], undefined, (args) => {
try {
const parsed = JSON.parse(args);
return parsed.name === 'software-engineering';
} catch {
return false;
}
});
},
});
/**
* Tests that the model proactively activates the new-application skill
* when asked to scaffold a new project.
*/
evalTest('ALWAYS_PASSES', {
name: 'should activate new-application skill for prototyping',
prompt: 'Build me a new Todo app using React and Vanilla CSS.',
assert: async (rig) => {
await rig.expectToolCallSuccess(['activate_skill'], undefined, (args) => {
try {
const parsed = JSON.parse(args);
return parsed.name === 'new-application';
} catch {
return false;
}
});
},
});
/**
* Tests that the model proactively activates the docs-writer skill
* when asked to update documentation files.
*/
evalTest('ALWAYS_PASSES', {
name: 'should activate docs-writer skill for documentation tasks',
prompt:
'Update the documentation in docs/index.md to include the new features.',
files: {
'docs/index.md': `# Documentation
Existing content.`,
},
assert: async (rig) => {
await rig.expectToolCallSuccess(['activate_skill'], undefined, (args) => {
try {
const parsed = JSON.parse(args);
return parsed.name === 'docs-writer';
} catch {
return false;
}
});
},
});
/**
* Tests that the model can handle multi-step tasks that might require
* activating multiple skills sequentially (though usually it just activates one).
*/
evalTest('USUALLY_PASSES', {
name: 'should activate software-engineering even when the prompt is slightly indirect',
prompt:
'The CI is failing on the main branch. Can you investigate and fix whatever is broken?',
files: {
'package.json': '{ "scripts": { "test": "vitest run" } }',
'src/logic.ts':
'export const compute = () => { throw new Error("Broken"); };',
'src/logic.test.ts':
'import { compute } from "./logic"; import { test } from "vitest"; test("compute", () => { compute(); });',
},
assert: async (rig) => {
await rig.expectToolCallSuccess(['activate_skill'], undefined, (args) => {
try {
const parsed = JSON.parse(args);
return parsed.name === 'software-engineering';
} catch {
return false;
}
});
},
});
});
@@ -961,6 +961,9 @@ export const useGeminiStream = (
[FinishReason.SPII]:
'Response stopped due to sensitive personally identifiable information.',
[FinishReason.OTHER]: 'Response stopped for other reasons.',
[FinishReason.IMAGE_RECITATION]:
'Response stopped due to image recitation policy.',
[FinishReason.IMAGE_OTHER]: 'Response stopped for other image reasons.',
[FinishReason.MALFORMED_FUNCTION_CALL]:
'Response stopped due to malformed function call.',
[FinishReason.IMAGE_SAFETY]:
@@ -118,6 +118,65 @@ describe('PromptProvider', () => {
);
});
it('should include skill activation guidance and placeholders in capability variant', () => {
vi.mocked(mockConfig.getActiveModel).mockReturnValue(
PREVIEW_GEMINI_FLASH_MODEL,
);
vi.mocked(mockConfig.getSkillManager().getSkills).mockReturnValue([
{
name: 'software-engineering',
description: 'Expert guidance.',
location: '/path/to/skill',
body: 'Skill body',
},
]);
const provider = new PromptProvider();
const prompt = provider.getCoreSystemPrompt(mockConfig);
expect(prompt).toContain('## Essential Workflows');
expect(prompt).toContain('software-engineering');
expect(prompt).toContain(
'Use `activate_skill` to enable specialized expert guidance',
);
});
it('should sort available skills (workspace first) and elevate activated skills in capability variant', () => {
vi.mocked(mockConfig.getActiveModel).mockReturnValue(
PREVIEW_GEMINI_FLASH_MODEL,
);
vi.mocked(mockConfig.getSkillManager().getSkills).mockReturnValue([
{
name: 'builtin-skill',
description: 'Builtin description',
location: '/path/to/builtin',
isBuiltin: true,
},
{
name: 'workspace-skill',
description: 'Workspace description',
location: '/path/to/workspace',
isBuiltin: false,
},
]);
vi.mocked(mockConfig.getSkillManager().isSkillActive).mockImplementation(
(name) => name === 'workspace-skill',
);
const provider = new PromptProvider();
const prompt = provider.getCoreSystemPrompt(mockConfig);
// Activated skills should be before available skills in the Capabilities section
const activatedIndex = prompt.indexOf('## Activated Skills');
const availableIndex = prompt.indexOf('## Available Skills');
expect(activatedIndex).toBeLessThan(availableIndex);
// Workspace skill should be before built-in skill in Available Skills
const workspaceIndex = prompt.indexOf('workspace-skill', availableIndex);
const builtinIndex = prompt.indexOf('builtin-skill', availableIndex);
expect(workspaceIndex).toBeLessThan(builtinIndex);
});
it('should handle multiple context filenames in user memory section', () => {
vi.mocked(getAllGeminiMdFilenames).mockReturnValue([
DEFAULT_CONTEXT_FILENAME,
+4 -3
View File
@@ -128,6 +128,7 @@ export class PromptProvider {
name: s.name,
description: s.description,
location: s.location,
isBuiltin: s.isBuiltin,
})),
);
basePrompt = applySubstitutions(
@@ -171,6 +172,7 @@ export class PromptProvider {
name: s.name,
description: s.description,
location: s.location,
isBuiltin: s.isBuiltin,
})),
skills.length > 0,
),
@@ -241,7 +243,7 @@ export class PromptProvider {
const getCoreSystemPrompt = activeSnippets.getCoreSystemPrompt as (
options: snippets.SystemPromptOptions,
) => string;
basePrompt = getCoreSystemPrompt(options);
}
@@ -269,7 +271,6 @@ export class PromptProvider {
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
return sanitizedPrompt;
}
getCompressionPrompt(config: Config): string {
@@ -279,7 +280,7 @@ export class PromptProvider {
);
const isModernModel = supportsModernFeatures(desiredModel);
const activeSnippets = isModernModel ? snippets : legacySnippets;
return activeSnippets.getCompressionPrompt();
}
+4 -1
View File
@@ -24,9 +24,10 @@ You are Gemini CLI, an expert agent. Help users safely and effectively.
- **Self-Correction:** If progress stalls or deviates from the goal, pause and "take a step back." If you realize you are making fixes unrelated to the original objective, stop, revert to a stable state if necessary, and re-approach the problem.
# Capabilities
{{ACTIVATED_SKILLS}}
{{AVAILABLE_SUB_AGENTS}}
{{AVAILABLE_SKILLS}}
{{ACTIVATED_SKILLS}}
- **Activation:** Use \`activate_skill\` to enable specialized expert guidance when a task matches a skill's description.
# Operational Style
- **Tone:** Professional, direct, senior engineer peer.
@@ -37,4 +38,6 @@ You are Gemini CLI, an expert agent. Help users safely and effectively.
{{HOOK_CONTEXT}}
{{PLAN_MODE_OVERRIDE}}
{{GIT_REPO_CONTEXT}}
{{SANDBOX_CONTEXT}}
{{YOLO_MODE_CONTEXT}}
`.trim();
@@ -56,10 +56,28 @@ export function getCoreSystemPrompt(
'{{GIT_REPO_CONTEXT}}',
renderGitRepo(options.gitRepo),
);
prompt = prompt.replace(
'{{SANDBOX_CONTEXT}}',
renderSandbox(options.sandbox),
);
prompt = prompt.replace(
'{{YOLO_MODE_CONTEXT}}',
renderInteractiveYoloMode(options.interactiveYoloMode),
);
return prompt.trim();
}
function renderSandbox(mode?: snippets.SandboxMode): string {
if (!mode || mode === 'outside') return '';
return `## Sandbox\nYou are in a ${mode} sandbox. Access to host resources and files outside the project is restricted. If a command fails with 'Operation not permitted', explain it might be due to sandboxing.`;
}
function renderInteractiveYoloMode(enabled?: boolean): string {
if (!enabled) return '';
return `## Autonomous Mode (YOLO)\nMinimal interruption requested. Use \`ask_user\` ONLY for critical architectural pivots or fundamental ambiguity. Otherwise, make expert decisions autonomously.`;
}
function renderSubAgents(subAgents?: snippets.SubAgentOptions[]): string {
if (!subAgents || subAgents.length === 0) return '';
const agents = subAgents
@@ -70,22 +88,45 @@ function renderSubAgents(subAgents?: snippets.SubAgentOptions[]): string {
function renderAvailableSkills(skills?: snippets.AgentSkillOptions[]): string {
if (!skills || skills.length === 0) return '';
const available = skills
.map((s) => `- **${s.name}**: ${s.description}`)
.join('\n');
return `## Available Skills\nActivate with \`activate_skill\`:\n${available}`;
// Essential Workflows that we want to keep highly visible
const essentialNames = ['software-engineering', 'new-application'];
const essential = skills.filter((s) => essentialNames.includes(s.name));
const others = skills.filter((s) => !essentialNames.includes(s.name));
// Sort others: Workspace/User skills first, Built-ins last
const sortedOthers = [...others].sort((a, b) => {
if (a.isBuiltin && !b.isBuiltin) return 1;
if (!a.isBuiltin && b.isBuiltin) return -1;
return a.name.localeCompare(b.name);
});
const renderList = (list: snippets.AgentSkillOptions[]) =>
list.map((s) => `- **${s.name}**: ${s.description}`).join('\n');
let output = '';
if (essential.length > 0) {
output += `## Essential Workflows\nActivate these for core agent behaviors:\n${renderList(essential)}\n\n`;
}
if (sortedOthers.length > 0) {
output += `## Available Skills\nProactively activate a skill with \`activate_skill\` when a task matches its expertise. This provides specialized protocols and expert guidance.\n${renderList(sortedOthers)}`;
}
return output.trim();
}
function renderActivatedSkills(
skills?: snippets.ActivatedSkillOptions[],
): string {
if (!skills || skills.length === 0) return '';
return skills
const skillsXml = skills
.map(
(s) =>
`### <activated_skill name="${s.name}">\n${s.body}\n### </activated_skill>`,
`<activated_skill name="${s.name}">\n${s.body}\n</activated_skill>`,
)
.join('\n\n');
return `## Activated Skills\nFollow \`<activated_skill>\` instructions as expert guidance. These rules supersede general workflows.\n${skillsXml}`;
}
function renderHookContext(enabled?: boolean): string {
+1
View File
@@ -85,6 +85,7 @@ export interface AgentSkillOptions {
name: string;
description: string;
location: string;
isBuiltin?: boolean;
}
export interface SubAgentOptions {