PromptBuilder: la cadena de montaje del system prompt
Responsabilidades
PromptBuilder es la clase con la que Cline, «una vez obtiene un provider/model», materializa el system prompt correspondiente. Lo instancia PromptRegistry.get cada vez que hace falta (new PromptBuilder:92) y se alimenta de tres cosas: un PromptVariant (que decide plantilla y orden de componentes), un SystemPromptContext (contexto de runtime con cwd / mcp / rules / skills, etc.) y un ComponentRegistry (tabla de funciones de componentes). Devuelve una cadena que es justo el systemPrompt que Task pasa a ApiHandler.createMessage dentro de attemptApiRequest (getSystemPrompt call:2350).
La división de responsabilidades con PromptRegistry es: Registry se encarga de, según providerInfo, emparejar qué variant usar y de mantener la tabla de componentes; Builder se encarga de recorrer el baseTemplate y el componentOrder del variant, rellenar los placeholders con el resultado de cada función componente y hacer el postprocesado que limpia los segmentos vacíos (build:23). En pocas palabras: Registry elige la plantilla, Builder la rellena.
Motivación de diseño
- Componentización en vez de cadenas gigantes: el system prompt se descompone en componentes
AGENT_ROLE / TOOL_USE / TASK_PROGRESS / MCP / EDITING_FILES / ACT_VS_PLAN / CAPABILITIES / RULES / SYSTEM_INFO / OBJECTIVE / USER_INSTRUCTIONS / SKILLS(components:44); cada uno es una función independiente que devuelve una cadena o undefined, y el orden lo fija el variant. Así, distintas familias de modelos pueden reemplazar solo algunos componentes y reutilizar el resto. - placeholder + TemplateEngine: en
baseTemplatese escriben placeholders como/ /; Builder primero vuelca los resultados de componentes encomponentSections, luego inyecta los placeholders estándar (cwd / currentDate / modelFamily) (preparePlaceholders:55) y finalmente delega aTemplateEngine.resolvela sustitución. - variant matcher elige la plantilla:
PromptRegistry.getModelFamilyrecorre todas las variantes registradas, llama amatcher(context)de cada una y gana la primera que devuelva true (getModelFamily:42); el variant generic actúa como retroceso. - runtimePlaceholders tiene la máxima prioridad: el
runtimePlaceholdersdel context se aplica al final con unObject.assignque sobrescribe todos los valores previos (runtime placeholders:79), de modo que los campos calculados dinámicamente en runtime pueden forzar la sobreescritura de los valores por defecto del variant. - Postprocesado de limpieza:
postProcessaplica una serie de expresiones regulares que colapsan varias líneas vacías consecutivas en dos, eliminan cabeceras de sección vacías##y separadores=====vacíos (postProcess:86), ya que el ensamblaje dinámico de componentes deja con facilidad segmentos vacíos y el prompt se vería desordenado sin esta limpieza. - Entrada independiente para los prompts de herramientas:
getToolsPromptses un método static destinado a que el variant lo llame para ensamblar el segmento «XML schema de herramientas» (getToolsPrompts:139), convirtiendo el resultado deClineToolSet.getEnabledToolSpecsen bloques de texto con forma<tool_name>...</tool_name>.
Archivos clave
PromptBuilder class:12— mantiene cuatro dependencias: variant, context, components y templateEngine.build method:23— entrada principal; cuatro pasos:buildComponents → preparePlaceholders → templateEngine.resolve → postProcess.buildComponents:30— recorre las funciones de componente segúnvariant.componentOrdery vuelca los resultados en sections.preparePlaceholders:55— cuatro capas de sobreescritura: placeholders del variant + campos estándar + sections de componentes + runtime placeholders.postProcess:86— serie de regex que limpia líneas vacías, secciones vacías y separadores vacíos, preservando intactos los=====de estilo diff.getToolsPrompts:139— método static que convierte las herramientas habilitadas en texto de prompt, una sección## tool_namepor herramienta.tool method:146— constructor del prompt de una sola herramienta; primero filtra dependencies y contextRequirements, luego ensambla las secciones Parameters / Usage.buildUsageSection:210— genera el ejemplo XML<tool><param>...</param></tool>.PromptRegistry.get:86— llama agetVariantpara elegir variant, calcula nativeTools y por último ejecutanew PromptBuilder(...).build().getModelFamily:42— recorre las variantes llamando a matcher; gana la primera que coincida; si ninguna, retrocede a GENERIC.generic components:44— orden de componentes del variant de retroceso; prácticamente todos los modelos van por aquí por defecto.SystemPromptContext:94— definición del tipo de contexto, con providerInfo, cwd, mcpHub, skills, rules, browserSettings y decenas de campos más.
Flujo de datos
El núcleo de Builder es «llamar a las funciones de componente en orden, recoger los resultados en sections y pasarlos al motor de plantillas»:
// 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
}Observa que buildComponents se recorre con un for await secuencial, no con Promise.all, porque los componentes pueden tener dependencias de orden (por ejemplo, OBJECTIVE necesita referenciar contenido producido por componentes previos) y el recorrido secuencial lo garantiza (sequential for:35). Si un componente lanza una excepción, solo se hace warn y no se interrumpe la construcción; los componentes faltantes dejan un placeholder vacío que luego se sustituye por una cadena vacía.
El orden de sobreescritura en preparePlaceholders es crítico: lo que se escribe después pisa a lo previo:
// apps/vscode/src/core/prompts/system-prompt/registry/PromptBuilder.ts
const placeholders: Record<string, unknown> = {}
Object.assign(placeholders, this.variant.placeholders) // 1. valores por defecto del variant
placeholders[STANDARD_PLACEHOLDERS.CWD] = this.context.cwd || process.cwd() // 2. campos estándar
placeholders[STANDARD_PLACEHOLDERS.MODEL_FAMILY] = this.variant.family
placeholders[STANDARD_PLACEHOLDERS.CURRENT_DATE] = new Date().toISOString().split("T")[0]
Object.assign(placeholders, componentSections) // 3. salida de componentes
for (const key of STANDARD_PLACEHOLDER_KEYS) {
if (!placeholders[key]) {
placeholders[key] = componentSections[key] || "" // 4. rellenar con vacío los campos estándar ausentes
}
}
const runtimePlaceholders = (this.context as any).runtimePlaceholders
if (runtimePlaceholders) {
Object.assign(placeholders, runtimePlaceholders) // 5. sobreescritura de runtime, máxima prioridad
}
return placeholdersEste diseño en capas deja clara la precedencia «valores por defecto del variant → campos estándar → salida de componentes → sobreescritura forzada de runtime»; modificar una capa no rompe accidentalmente otra.
Límites y fallos
- Componente devuelve undefined: el filtro
result?.trim()deja entrar en sections solo las cadenas no vacías (trim check:44), de modo que un componente puede devolver contenido de forma selectiva (por ejemplo, el segmentoBROWSERdevuelve vacío cuandosupportsBrowserUse=false). - Excepción en un componente: tras el catch solo se hace
Logger.warn, no se relanza; los demás componentes siguen ejecutándose (catch warn:47), de modo que un bug en un componente único no tira todo el system prompt. - Componente no registrado: si
components[componentId]no existe, se hace warn y se continúa (not found warn:37); el placeholder correspondiente se rellena con una cadena vacía enpreparePlaceholders. - variant no encontrado: si
PromptRegistry.getVariantno empareja ninguna variante y no hay retroceso GENERIC, lanza un error directamente (no variant throw:71), aunque el variant GENERIC está registrado por defecto y normalmente no se llega a este punto. - Excepción en matcher:
getModelFamilyenvuelvev.matcher(context)en try/catch; si falla, continúa con la siguiente variante (matcher catch:51), evitando que el bug de una variante arrastre toda la selección. - Limpieza indebida de
=====de estilo diff:postProcessmira 50 caracteres de contexto antes y después al sustituir=====; si es un marcador diff del tipoSEARCH / REPLACE / ++++ / ----, lo deja intacto (diff guard:106), evitando borrar por error los separadores diff del prompt de la herramientareplace_in_file. - Recálculo repetido de nativeTools: en
PromptRegistry.get,this.nativeTools = ClineToolSet.getNativeTools(variant, context)es un side effect (nativeTools side effect:90) marcado con el comentario «Hacky way»; cada vez que se obtiene el systemPrompt se recalcula una vez.
Resumen
PromptBuilder descompone con claridad la ecuación «system prompt = plantilla + componentes + contexto»: el variant fija la plantilla y el orden de componentes; las funciones de componente leen el context y devuelven una cadena; placeholder + TemplateEngine los ensamblan; postProcess cierra la limpieza. Esta estructura permite a Cline personalizar rápidamente el prompt para distintas familias de modelos (next-gen / glm / gemini-3 / gpt-5 tienen cada una su variant) compartiendo el mismo conjunto de funciones de componente. A continuación puedes ver cómo se definen las herramientas o cómo los modos plan/act influyen en el prompt.
- Toolset y XML schema:
/prompts/toolset - Diferencias entre modos plan / act:
/prompts/mode - Cómo se selecciona el provider:
/providers/api-handler - Cómo Anthropic handler consume el systemPrompt:
/providers/anthropic