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