PromptBuilder: the system prompt assembly line
Responsibilities
PromptBuilder is the class that actually assembles a provider's/model's system prompt once Cline has one in hand. It is instantiated by PromptRegistry.get each time it is needed (new PromptBuilder:92), taking three inputs: a PromptVariant (which determines the template and component order), a SystemPromptContext (runtime context carrying cwd / mcp / rules / skills etc.), and a ComponentRegistry (the component function table). It ultimately returns a string, which is the systemPrompt that Task feeds to ApiHandler.createMessage inside attemptApiRequest (getSystemPrompt call:2350).
The division of labor with PromptRegistry is: the Registry matches providerInfo to decide which variant to use and maintains the component table; the Builder actually walks the variant's baseTemplate and componentOrder, fills each component function's result into the template placeholders, and then runs post-processing to clean up empty sections (build:23). In short: Registry picks the template, Builder fills it.
Design motivation
- Components over a giant string: the system prompt is broken into components like
AGENT_ROLE / TOOL_USE / TASK_PROGRESS / MCP / EDITING_FILES / ACT_VS_PLAN / CAPABILITIES / RULES / SYSTEM_INFO / OBJECTIVE / USER_INSTRUCTIONS / SKILLS(components:44). Each component is an independent function returning a string or undefined, with order determined by the variant. This lets different model families swap in just a few components and reuse the rest. - Placeholders + TemplateEngine:
baseTemplatecarries placeholders like/ /. Builder first fills component results intocomponentSections, then injects standard placeholders (cwd / currentDate / modelFamily) (preparePlaceholders:55), and finally hands the result toTemplateEngine.resolvefor substitution. - Variant matcher picks the template:
PromptRegistry.getModelFamilyiterates over all registered variants, calling each variant'smatcher(context); the first one to return true wins (getModelFamily:42), with the generic variant as the fallback. - runtimePlaceholders take top priority:
runtimePlaceholderson the context is the finalObject.assign, overriding every prior value (runtime placeholders:79), so runtime-computed fields can forcibly override variant defaults. - Post-processing cleanup: a chain of regexes in
postProcesscollapses consecutive blank lines to at most two, removes empty section headers##, and drops empty=====separators (postProcess:86). Dynamic component assembly easily leaves empty sections, and without cleanup the prompt looks messy. - Tool prompt entry point:
getToolsPromptsis a static method specifically for variants to call when assembling the "tool XML schema" section (getToolsPrompts:139). It turns the output ofClineToolSet.getEnabledToolSpecsinto prompt text in the<tool_name>...</tool_name>format.
Key files
PromptBuilder class:12— Holds four dependencies: variant, context, components, templateEngine.build method:23— Main entry; four steps:buildComponents → preparePlaceholders → templateEngine.resolve → postProcess.buildComponents:30— Calls each component function invariant.componentOrderorder, storing results in sections.preparePlaceholders:55— Four layers of override: variant placeholders + standard placeholders + component sections + runtime placeholders.postProcess:86— A chain of regexes cleaning blank lines, empty sections, empty separators; preserves diff-style=====untouched.getToolsPrompts:139— Static method that turns enabled tools into prompt text, one## tool_namesection per tool.tool method:146— Per-tool prompt assembly; first filters dependencies and contextRequirements, then assembles Parameters / Usage sections.buildUsageSection:210— Emits the XML usage example<tool><param>...</param></tool>.PromptRegistry.get:86— CallsgetVariantto pick the variant, computes nativeTools as a side effect, and finally runsnew PromptBuilder(...).build().getModelFamily:42— Iterates variants calling matcher; first match wins, otherwise falls back to GENERIC.generic components:44— The fallback variant's component order; nearly all models use this set by default.SystemPromptContext:94— Context type definition, carrying dozens of fields like providerInfo, cwd, mcpHub, skills, rules, browserSettings.
Data flow
The Builder's core is "call component functions in order, collect the results into sections, then feed them to the template engine":
// apps/vscode/src/core/prompts/system-prompt/registry/PromptBuilder.ts
async build(): Promise<string> {
const componentSections = await this.buildComponents()
const placeholderValues = this.preparePlaceholders(componentSections)
const prompt = this.templateEngine.resolve(this.variant.baseTemplate, this.context, placeholderValues)
return this.postProcess(prompt)
}
private async buildComponents(): Promise<Record<string, string>> {
const sections: Record<string, string> = {}
const { componentOrder } = this.variant
// Process components sequentially to maintain order
for (const componentId of componentOrder) {
const componentFn = this.components[componentId]
if (!componentFn) {
Logger.warn(`Warning: Component '${componentId}' not found`)
continue
}
try {
const result = await componentFn(this.variant, this.context)
if (result?.trim()) {
sections[componentId] = result
}
} catch (error) {
Logger.warn(`Warning: Failed to build component '${componentId}':`, error)
}
}
return sections
}Note that buildComponents runs as a sequential for await loop, not Promise.all, because components may depend on order (for example, OBJECTIVE may reference content assembled by earlier components), and sequential execution guarantees that (sequential for:35). A single component throwing only warns and does not abort the whole build; a missing component leaves its placeholder to be replaced with an empty string later.
The override order in preparePlaceholders matters — later writes win:
// apps/vscode/src/core/prompts/system-prompt/registry/PromptBuilder.ts
const placeholders: Record<string, unknown> = {}
Object.assign(placeholders, this.variant.placeholders) // 1. variant defaults
placeholders[STANDARD_PLACEHOLDERS.CWD] = this.context.cwd || process.cwd() // 2. standard fields
placeholders[STANDARD_PLACEHOLDERS.MODEL_FAMILY] = this.variant.family
placeholders[STANDARD_PLACEHOLDERS.CURRENT_DATE] = new Date().toISOString().split("T")[0]
Object.assign(placeholders, componentSections) // 3. component outputs
for (const key of STANDARD_PLACEHOLDER_KEYS) {
if (!placeholders[key]) {
placeholders[key] = componentSections[key] || "" // 4. missing standard fields filled with empty
}
}
const runtimePlaceholders = (this.context as any).runtimePlaceholders
if (runtimePlaceholders) {
Object.assign(placeholders, runtimePlaceholders) // 5. runtime override has the highest priority
}
return placeholdersThis layered design keeps the "variant defaults → standard fields → component outputs → runtime forced override" priority clear; changing any layer will not accidentally break another.
Boundaries and failure modes
- Component returns undefined:
result?.trim()truthy check — only non-empty strings go into sections (trim check:44). Components can return content selectively (e.g. theBROWSERsection returns empty whensupportsBrowserUse=false). - Component throws: caught and only
Logger.warn'd, not re-thrown; other components keep running (catch warn:47), so a single component bug does not bring down the whole system prompt. - Component not registered: when
components[componentId]is missing, warn and continue (not found warn:37); the corresponding placeholder is filled with an empty string inpreparePlaceholders. - No variant found:
PromptRegistry.getVariantthrows outright if no variant matches and there is no GENERIC fallback (no variant throw:71). The GENERIC variant is registered by default, so this path is normally unreachable. - Matcher throws:
getModelFamilywrapsv.matcher(context)in try/catch; on failure it moves to the next variant (matcher catch:51), so one variant's bug does not sink the whole selection. - Diff-style
=====mistakenly cleaned:postProcesslooks 50 characters ahead and behind before replacing=====; if the context contains diff markers likeSEARCH / REPLACE / ++++ / ----it is left untouched (diff guard:106), so the diff separators in thereplace_in_filetool prompt are not accidentally stripped. - nativeTools recomputed:
this.nativeTools = ClineToolSet.getNativeTools(variant, context)insidePromptRegistry.getis a side effect (nativeTools side effect:90) — the comment even calls it "Hacky way"; every systemPrompt fetch recomputes it once.
Summary
PromptBuilder cleanly separates the three concerns of "system prompt = template + components + context": the variant decides the template and component order, component functions read the context and return strings, placeholders + TemplateEngine glue them together, and postProcess wraps things up. This structure lets Cline quickly tailor prompts for different model families (next-gen / glm / gemini-3 / gpt-5 each have their own variant) while sharing the same set of component functions. Next you might look at how tools are defined, or how plan/act mode shapes the prompt.
- Toolset and XML schema:
/prompts/toolset - plan / act mode differences:
/prompts/mode - How a provider is selected:
/providers/api-handler - How the Anthropic handler consumes the systemPrompt:
/providers/anthropic