feat: handle multiple dynamic context filenames in system prompt (#18598)

This commit is contained in:
N. Taylor Mullen
2026-02-09 16:37:08 -08:00
committed by GitHub
parent c9f9a7f67a
commit cc2798018b
3 changed files with 123 additions and 7 deletions

View File

@@ -0,0 +1,92 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { PromptProvider } from './promptProvider.js';
import type { Config } from '../config/config.js';
import {
getAllGeminiMdFilenames,
DEFAULT_CONTEXT_FILENAME,
} from '../tools/memoryTool.js';
import { PREVIEW_GEMINI_MODEL } from '../config/models.js';
vi.mock('../tools/memoryTool.js', async (importOriginal) => {
const actual = await importOriginal();
return {
...(actual as object),
getAllGeminiMdFilenames: vi.fn(),
};
});
vi.mock('../utils/gitUtils', () => ({
isGitRepository: vi.fn().mockReturnValue(false),
}));
describe('PromptProvider', () => {
let mockConfig: Config;
beforeEach(() => {
vi.resetAllMocks();
mockConfig = {
getToolRegistry: vi.fn().mockReturnValue({
getAllToolNames: vi.fn().mockReturnValue([]),
getAllTools: vi.fn().mockReturnValue([]),
}),
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project-temp'),
getProjectTempPlansDir: vi
.fn()
.mockReturnValue('/tmp/project-temp/plans'),
},
isInteractive: vi.fn().mockReturnValue(true),
isInteractiveShellEnabled: vi.fn().mockReturnValue(true),
getSkillManager: vi.fn().mockReturnValue({
getSkills: vi.fn().mockReturnValue([]),
}),
getActiveModel: vi.fn().mockReturnValue(PREVIEW_GEMINI_MODEL),
getAgentRegistry: vi.fn().mockReturnValue({
getAllDefinitions: vi.fn().mockReturnValue([]),
}),
getApprovedPlanPath: vi.fn().mockReturnValue(undefined),
getApprovalMode: vi.fn(),
} as unknown as Config;
});
it('should handle multiple context filenames in the system prompt', () => {
vi.mocked(getAllGeminiMdFilenames).mockReturnValue([
DEFAULT_CONTEXT_FILENAME,
'CUSTOM.md',
'ANOTHER.md',
]);
const provider = new PromptProvider();
const prompt = provider.getCoreSystemPrompt(mockConfig);
// Verify renderCoreMandates usage
expect(prompt).toContain(
`Instructions found in \`${DEFAULT_CONTEXT_FILENAME}\`, \`CUSTOM.md\` or \`ANOTHER.md\` files are foundational mandates.`,
);
});
it('should handle multiple context filenames in user memory section', () => {
vi.mocked(getAllGeminiMdFilenames).mockReturnValue([
DEFAULT_CONTEXT_FILENAME,
'CUSTOM.md',
]);
const provider = new PromptProvider();
const prompt = provider.getCoreSystemPrompt(
mockConfig,
'Some memory content',
);
// Verify renderUserMemory usage
expect(prompt).toContain(
`# Contextual Instructions (${DEFAULT_CONTEXT_FILENAME}, CUSTOM.md)`,
);
});
});

View File

@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -28,6 +28,7 @@ import {
} from '../tools/tool-names.js';
import { resolveModel, isPreviewModel } from '../config/models.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
import { getAllGeminiMdFilenames } from '../tools/memoryTool.js';
/**
* Orchestrates prompt generation by gathering context and building options.
@@ -56,6 +57,7 @@ export class PromptProvider {
const desiredModel = resolveModel(config.getActiveModel());
const isGemini3 = isPreviewModel(desiredModel);
const activeSnippets = isGemini3 ? snippets : legacySnippets;
const contextFilenames = getAllGeminiMdFilenames();
// --- Context Gathering ---
let planModeToolsList = PLAN_MODE_TOOLS.filter((t) =>
@@ -114,6 +116,7 @@ export class PromptProvider {
interactive: interactiveMode,
isGemini3,
hasSkills: skills.length > 0,
contextFilenames,
})),
subAgents: this.withSection('agentContexts', () =>
config
@@ -191,7 +194,11 @@ export class PromptProvider {
}
// --- Finalization (Shell) ---
const finalPrompt = activeSnippets.renderFinalShell(basePrompt, userMemory);
const finalPrompt = activeSnippets.renderFinalShell(
basePrompt,
userMemory,
contextFilenames,
);
// Sanitize erratic newlines from composition
const sanitizedPrompt = finalPrompt.replace(/\n{3,}/g, '\n\n');

View File

@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -18,6 +18,7 @@ import {
WRITE_FILE_TOOL_NAME,
WRITE_TODOS_TOOL_NAME,
} from '../tools/tool-names.js';
import { DEFAULT_CONTEXT_FILENAME } from '../tools/memoryTool.js';
// --- Options Structs ---
@@ -42,6 +43,7 @@ export interface CoreMandatesOptions {
interactive: boolean;
isGemini3: boolean;
hasSkills: boolean;
contextFilenames?: string[];
}
export interface PrimaryWorkflowsOptions {
@@ -119,11 +121,12 @@ ${renderGitRepo(options.gitRepo)}
export function renderFinalShell(
basePrompt: string,
userMemory?: string,
contextFilenames?: string[],
): string {
return `
${basePrompt.trim()}
${renderUserMemory(userMemory)}
${renderUserMemory(userMemory, contextFilenames)}
`.trim();
}
@@ -138,6 +141,15 @@ export function renderPreamble(options?: PreambleOptions): string {
export function renderCoreMandates(options?: CoreMandatesOptions): string {
if (!options) return '';
const filenames = options.contextFilenames ?? [DEFAULT_CONTEXT_FILENAME];
const formattedFilenames =
filenames.length > 1
? filenames
.slice(0, -1)
.map((f) => `\`${f}\``)
.join(', ') + ` or \`${filenames[filenames.length - 1]}\``
: `\`${filenames[0]}\``;
return `
# Core Mandates
@@ -147,7 +159,7 @@ export function renderCoreMandates(options?: CoreMandatesOptions): string {
- **Protocol:** Do not ask for permission to use tools; the system handles confirmation. Your responsibility is to justify the action, not to seek authorization.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Contextual Precedence:** Instructions found in ${formattedFilenames} files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
@@ -325,10 +337,15 @@ export function renderGitRepo(options?: GitRepoOptions): string {
- Never push changes to a remote repository without being asked explicitly by the user.`.trim();
}
export function renderUserMemory(memory?: string): string {
export function renderUserMemory(
memory?: string,
contextFilenames?: string[],
): string {
if (!memory || memory.trim().length === 0) return '';
const filenames = contextFilenames ?? [DEFAULT_CONTEXT_FILENAME];
const formattedHeader = filenames.join(', ');
return `
# Contextual Instructions (GEMINI.md)
# Contextual Instructions (${formattedHeader})
The following content is loaded from local and global configuration files.
**Context Precedence:**
- **Global (~/.gemini/):** foundational user preferences. Apply these broadly.