Skip to content

refactor(core): prompt snippets into layered architecture#23307

Open
gundermanc wants to merge 3 commits intomainfrom
prompt-template
Open

refactor(core): prompt snippets into layered architecture#23307
gundermanc wants to merge 3 commits intomainfrom
prompt-template

Conversation

@gundermanc
Copy link
Member

Summary

Refactoring packages/core/src/prompts/snippets.ts and snippets.legacy.ts into a modular, type-safe, and model-specific architecture using the promptTemplating.ts DSL. This change introduces a layered approach to separate core identity, environmental refinements, and specific feature sets.

Details

Proposed Directory Structure

packages/core/src/prompts/
├── templates/               # Schema definitions and base interfaces
│   ├── options.ts           # Shared SystemPromptOptions and sub-interfaces
│   └── system.ts            # PromptTemplate definitions for agents
├── root-harness/            # Layer 1: Core identity and foundational mandates
│   ├── common.ts            # Preferred model (Gemini 3.1 Pro) implementation
│   └── flash.ts             # Optimized snippets for Flash models
├── refinements/             # Layer 2: Environmental and operational adjustments
│   ├── common.ts            # Preferred model implementation (Interactive, Efficiency)
│   └── autonomous.ts        # Specialized refinements for non-interactive modes
├── features/                # Layer 3: Capability-specific snippets (Tools, Workflows)
│   ├── common.ts            # Preferred model implementation (Git, Skills, Tracker)
│   └── light.ts             # Reduced feature set for simpler agents
├── compositions/            # Layer 4: Final assembly of layers for specific agents
│   └── gemini-cli.ts        # Main composition for the Gemini CLI agent
├── promptTemplating.ts      # The Prompt DSL (Existing)
├── snippets.ts              # Entry point (Maintains backward compatibility via exports)
└── snippets.legacy.ts       # Legacy snapshot (Untouched)

Key Components & Snippets

Root Harness (root-harness/common.ts)

import { promptComponent, xmlSection } from '../promptTemplating.js';
import { SystemPromptOptions } from '../templates/options.js';

export const identity = (opt: SystemPromptOptions) => 
  `You are Gemini CLI, an ${opt.preamble?.interactive ? 'interactive' : 'autonomous'} CLI agent...`;

export const securityMandates = xmlSection('security', 
  "Never log or commit secrets. Protect .env and .git folders."
);

Refinements (refinements/common.ts)

import { section, enabledWhen } from '../promptTemplating.js';
import { SystemPromptOptions } from '../templates/options.js';

export const contextEfficiency = section('Context Efficiency', 
  "Be strategic in your use of tools to minimize unnecessary context usage...",
  { headerLevel: 2 }
);

Features (features/common.ts)

Demonstrates complex composition using list interpolation (each), structural helpers (xmlSection), and multi-line snippets.

import { promptComponent, xmlSection, each, section } from '../promptTemplating.js';
import { SystemPromptOptions, SubAgentOptions } from '../templates/options.js';

/**
 * Renders the sub-agents section, demonstrating list interpolation and structural XML.
 */
export const subAgents = section('Available Sub-Agents', 
  promptComponent(
    "Sub-agents are specialized expert agents. Each sub-agent is available as a tool of the same name.",
    "You MUST delegate tasks to the sub-agent with the most relevant expertise.",
    "\n### Strategic Orchestration & Delegation",
    "Operate as a strategic orchestrator. Use sub-agents to 'compress' complex or repetitive work.",
    xmlSection('available_subagents', 
      each<SystemPromptOptions, SubAgentOptions>('subAgents', (agent) => 
        `  <subagent>\n    <name>${agent.name}</name>\n    <description>${agent.description}</description>\n  </subagent>`
      )
    ),
    "\nRemember that the closest relevant sub-agent should still be used even if its expertise is broader."
  ),
  { headerLevel: 1 }
);

Implementation Strategy

  1. Extract Interfaces: Move all options interfaces from snippets.ts to templates/options.ts.
  2. Define Structure: Create templates/system.ts to define the common structure for prompts.
  3. Decompose Snippets: Migrating logic from snippets.ts into the three layers (root-harness, refinements, features).
  4. Create Compositions: Implement the final gemini-cli.ts composition.
  5. Validation: Ensure renderTemplate output matches the existing getCoreSystemPrompt output for equivalent options to prevent regressions.

Related Issues

Related to #123 (hypothetical)

How to Validate

  1. Run unit tests for promptTemplating.ts.
  2. Compare output of renderTemplate with the previous getCoreSystemPrompt for various option sets.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any)
  • Validated on MacOS
  • Validated on Windows
  • Validated on Linux

@gundermanc gundermanc requested a review from a team as a code owner March 20, 2026 21:59
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant refactoring of how LLM prompts are constructed within the core package. By leveraging a new, type-safe Domain Specific Language (DSL) for prompt templating, the existing monolithic snippet management is replaced with a modular, layered architecture. This change aims to enhance maintainability, reusability, and clarity of prompt definitions, allowing for more flexible and model-specific prompt generation.

Highlights

  • New Prompt Templating DSL: Introduced a new promptTemplating.ts module that provides a functional, type-safe Domain Specific Language (DSL) for constructing complex LLM prompts. This DSL supports dynamic content generation, conditional snippets, structural formatting (XML, Markdown), and collection iteration.
  • Layered Architecture for Prompts: The refactoring establishes a layered approach for prompt snippets, separating concerns into core identity (root-harness), environmental refinements (refinements), and specific feature sets (features). This enhances modularity and maintainability.
  • Improved Type Safety and Modularity: The new architecture ensures prompts are tied to an options interface, providing type safety for dynamic data. Small, reusable snippets can be combined, and templates ensure consistent required elements.
  • Comprehensive Unit Testing: Added extensive unit tests for the promptTemplating.ts module, covering various functionalities like variadic arguments, conditional rendering (enabledWhen, switchOn), structural helpers (xmlSection, section), and iteration (each).
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link

Size Change: -4 B (0%)

Total Size: 26.1 MB

Filename Size Change
./bundle/chunk-4U3BL5PG.js 0 B -3.64 MB (removed) 🏆
./bundle/chunk-5E43MTB2.js 0 B -14.5 MB (removed) 🏆
./bundle/core-KC6SNSXQ.js 0 B -42.2 kB (removed) 🏆
./bundle/devtoolsService-IZJAOQDS.js 0 B -27.7 kB (removed) 🏆
./bundle/interactiveCli-WZXPXO6W.js 0 B -1.61 MB (removed) 🏆
./bundle/oauth2-provider-HVDBREX4.js 0 B -9.16 kB (removed) 🏆
./bundle/chunk-DUV42C7P.js 14.5 MB +14.5 MB (new file) 🆕
./bundle/chunk-WVR7HJFN.js 3.64 MB +3.64 MB (new file) 🆕
./bundle/core-OH3HWVUL.js 42.2 kB +42.2 kB (new file) 🆕
./bundle/devtoolsService-OJTFWLEM.js 27.7 kB +27.7 kB (new file) 🆕
./bundle/interactiveCli-EROYDM4G.js 1.61 MB +1.61 MB (new file) 🆕
./bundle/oauth2-provider-4YPP72RA.js 9.16 kB +9.16 kB (new file) 🆕
ℹ️ View Unchanged
Filename Size
./bundle/chunk-34MYV7JD.js 2.45 kB
./bundle/chunk-5725SFQR.js 1.95 MB
./bundle/chunk-5AUYMPVF.js 858 B
./bundle/chunk-664ZODQF.js 124 kB
./bundle/chunk-DAHVX5MI.js 206 kB
./bundle/chunk-IUUIT4SU.js 56.5 kB
./bundle/chunk-RJTRUG2J.js 39.8 kB
./bundle/devtools-36NN55EP.js 696 kB
./bundle/dist-T73EYRDX.js 356 B
./bundle/gemini.js 519 kB
./bundle/getMachineId-bsd-TXG52NKR.js 1.55 kB
./bundle/getMachineId-darwin-7OE4DDZ6.js 1.55 kB
./bundle/getMachineId-linux-SHIFKOOX.js 1.34 kB
./bundle/getMachineId-unsupported-5U5DOEYY.js 1.06 kB
./bundle/getMachineId-win-6KLLGOI4.js 1.72 kB
./bundle/memoryDiscovery-OV4FUTHJ.js 922 B
./bundle/multipart-parser-KPBZEGQU.js 11.7 kB
./bundle/node_modules/@google/gemini-cli-devtools/dist/client/main.js 221 kB
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/_client-assets.js 227 kB
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/index.js 11.5 kB
./bundle/node_modules/@google/gemini-cli-devtools/dist/src/types.js 132 B
./bundle/sandbox-macos-permissive-open.sb 890 B
./bundle/sandbox-macos-permissive-proxied.sb 1.31 kB
./bundle/sandbox-macos-restrictive-open.sb 3.36 kB
./bundle/sandbox-macos-restrictive-proxied.sb 3.56 kB
./bundle/sandbox-macos-strict-open.sb 4.82 kB
./bundle/sandbox-macos-strict-proxied.sb 5.02 kB
./bundle/src-QVCVGIUX.js 47 kB
./bundle/tree-sitter-7U6MW5PS.js 274 kB
./bundle/tree-sitter-bash-34ZGLXVX.js 1.84 MB

compressed-size-action

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a DSL for prompt templating, enhancing maintainability with a type-safe approach and layered architecture. However, it's critical to address high-severity prompt injection vulnerabilities due to the lack of sanitization for dynamic data. Specifically, the xmlSection, section, and renderSnippet functions pose a risk. Additionally, ensure that prompt snippets are joined with newlines instead of commas in renderTemplate and renderSnippet to avoid malformed prompts, adhering to the guideline that variables should ideally not be empty to prevent extra newlines. After fixing these issues, update the unit tests in promptTemplating.test.ts to reflect the corrected output format.

Comment on lines +237 to +239
return Object.values(implementation)
.map((eachSnippet) => renderSnippet<TOption>(options, eachSnippet))
.join();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Using .join() without an argument defaults to a comma separator, which will incorrectly format the final prompt by joining template parts with commas. It's better to use a newline separator.

Suggested change
return Object.values(implementation)
.map((eachSnippet) => renderSnippet<TOption>(options, eachSnippet))
.join();
return Object.values(implementation)
.map((eachSnippet) => renderSnippet<TOption>(options, eachSnippet))
.join('\n');
References
  1. This rule advises against using conditional rendering logic like filter(Boolean) to remove empty variables, instead preferring to ensure variables are never empty to prevent extra newlines.

Comment on lines +252 to +254
return snippet
.map((eachSnippet) => renderSnippet<TOption>(options, eachSnippet))
.join();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

When rendering an array of snippets (from promptComponent), .join() is used without a separator, which defaults to a comma. This will lead to incorrectly formatted prompts (e.g., part1,part2). These snippets should be joined by newlines.

Suggested change
return snippet
.map((eachSnippet) => renderSnippet<TOption>(options, eachSnippet))
.join();
return snippet
.map((eachSnippet) => renderSnippet<TOption>(options, eachSnippet))
.join('\n');
References
  1. This rule advises against using conditional rendering logic like filter(Boolean) to remove empty variables, instead preferring to ensure variables are never empty to prevent extra newlines.

Comment on lines +258 to +260
return Object.values(snippet)
.map((eachSnippet) => renderSnippet<TOption>(options, eachSnippet))
.join();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

When rendering a nested template object, .join() is used without a separator, which defaults to a comma. This will lead to incorrectly formatted prompts. These snippets should be joined by newlines.

Suggested change
return Object.values(snippet)
.map((eachSnippet) => renderSnippet<TOption>(options, eachSnippet))
.join();
return Object.values(snippet)
.map((eachSnippet) => renderSnippet<TOption>(options, eachSnippet))
.join('\n');
References
  1. This rule advises against using conditional rendering logic like filter(Boolean) to remove empty variables, instead preferring to ensure variables are never empty to prevent extra newlines.

): Snippet<TOption> {
return (options: TOption) => {
const content = renderSnippet(options, snippet);
return content ? `<${name}>\n${content}\n</${name}>` : '';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The xmlSection helper wraps the rendered content in XML tags without any sanitization or escaping. If the content (derived from the options object) contains characters that can break out of the XML structure (e.g., </rules>), an attacker can perform prompt injection. This is particularly dangerous if the resulting prompt is used by an agent with sensitive tools (like shell access).

References
  1. This rule emphasizes the need to escape HTML-like tag characters to prevent prompt injection, which is relevant when content is wrapped in XML tags.

const content = renderSnippet(options, snippet);
const level = sectionOptions?.headerLevel ?? 1;
const hashes = '#'.repeat(level);
return content ? `${hashes} ${name}\n\n${content}` : '';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The section helper prepends a Markdown header to the rendered content without any sanitization. If the content contains Markdown control characters (e.g., #, \n#), an attacker can inject new headers or manipulate the prompt structure, leading to prompt injection.

References
  1. This rule highlights the general principle of avoiding user-provided input in LLM content without sanitization to prevent prompt injection, which applies to content used in Markdown sections.

.map((eachSnippet) => renderSnippet<TOption>(options, eachSnippet))
.join();
} else if (typeof snippet === 'function') {
return snippet(options);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The renderSnippet function calls dynamic snippet functions with the options object. These functions typically perform string interpolation (e.g., `Your name is ${opt.name}`) using data from options. Since the DSL does not provide any built-in sanitization, untrusted data in options can lead to prompt injection. Developers using this DSL must be explicitly warned to sanitize all dynamic data before interpolation.

References
  1. This rule directly addresses the risk of prompt injection when user-provided input from the options object is passed to the LLM without sanitization.

@gemini-cli gemini-cli bot added the priority/p1 Important and should be addressed in the near term. label Mar 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority/p1 Important and should be addressed in the near term.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant