Skip to content

PromptBuilder: the system prompt assembly line

源码版本v4.0.10

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: baseTemplate carries placeholders like / / . Builder first fills component results into componentSections, then injects standard placeholders (cwd / currentDate / modelFamily) (preparePlaceholders:55), and finally hands the result to TemplateEngine.resolve for substitution.
  • Variant matcher picks the template: PromptRegistry.getModelFamily iterates over all registered variants, calling each variant's matcher(context); the first one to return true wins (getModelFamily:42), with the generic variant as the fallback.
  • runtimePlaceholders take top priority: runtimePlaceholders on the context is the final Object.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 postProcess collapses 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: getToolsPrompts is a static method specifically for variants to call when assembling the "tool XML schema" section (getToolsPrompts:139). It turns the output of ClineToolSet.getEnabledToolSpecs into 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 in variant.componentOrder order, 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_name section 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 — Calls getVariant to pick the variant, computes nativeTools as a side effect, and finally runs new 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":

typescript
// 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:

typescript
// 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 placeholders

This 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. the BROWSER section returns empty when supportsBrowserUse=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 in preparePlaceholders.
  • No variant found: PromptRegistry.getVariant throws 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: getModelFamily wraps v.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: postProcess looks 50 characters ahead and behind before replacing =====; if the context contains diff markers like SEARCH / REPLACE / ++++ / ---- it is left untouched (diff guard:106), so the diff separators in the replace_in_file tool prompt are not accidentally stripped.
  • nativeTools recomputed: this.nativeTools = ClineToolSet.getNativeTools(variant, context) inside PromptRegistry.get is 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

See official docs: Cline 文档 · README