UNPKG

@earendil-works/pi-coding-agent

Version:

Coding agent CLI with read, bash, edit, write tools and session management

151 lines • 7.97 kB
/** * System prompt construction and project context loading */ import { getSystemMessageText } from "@earendil-works/pi-ai"; import { getDocsPath, getExamplesPath, getReadmePath } from "../config.js"; import { formatSkillsForPrompt } from "./skills.js"; const SYSTEM_PROMPT_SECTION_NAME = /^[a-z][a-z0-9_-]*$/; /** Normalize prompt input into the mutable, collection-complete shape exposed to extensions. */ export function normalizeBuildSystemPromptOptions(input) { return { customPrompt: input.customPrompt, forceSystemPrompt: input.forceSystemPrompt, selectedTools: [...(input.selectedTools ?? ["read", "bash", "edit", "write"])], hiddenTools: [...(input.hiddenTools ?? [])], toolSnippets: { ...(input.toolSnippets ?? {}) }, toolGuidelines: Object.fromEntries(Object.entries(input.toolGuidelines ?? {}).map(([name, guidelines]) => [name, [...guidelines]])), promptGuidelines: [...(input.promptGuidelines ?? [])], appendSystemPrompt: input.appendSystemPrompt ?? "", sections: { ...(input.sections ?? {}) }, cwd: input.cwd, contextFiles: (input.contextFiles ?? []).map((file) => ({ ...file })), skills: (input.skills ?? []).map((skill) => ({ ...skill })), }; } function renderProjectContext(contextFiles) { return [ "Project-specific instructions and guidelines:", ...contextFiles.map(({ path, content }) => `<project_instructions path="${path}">\n${content}\n</project_instructions>`), ].join("\n\n"); } function buildRules(selectedTools, toolGuidelines, promptGuidelines) { const rules = []; const seen = new Set(); const addRule = (rule) => { const normalized = rule.trim(); if (!normalized || seen.has(normalized)) return; seen.add(normalized); rules.push(normalized); }; const hasBash = selectedTools.includes("bash"); const hasPowerShell = selectedTools.includes("powershell"); const hasGrep = selectedTools.includes("grep"); const hasFind = selectedTools.includes("find"); const hasLs = selectedTools.includes("ls"); if ((hasBash || hasPowerShell) && !hasGrep && !hasFind && !hasLs) { if (hasBash && hasPowerShell) { addRule("Use bash or PowerShell for file operations like listing, searching, and finding files"); } else if (hasPowerShell) { addRule("Use PowerShell for file operations like listing, searching, and finding files"); } else { addRule("Use bash for file operations like ls, rg, find"); } } for (const name of selectedTools) { for (const rule of toolGuidelines[name] ?? []) addRule(rule); } for (const rule of promptGuidelines) addRule(rule); addRule("Be concise in your responses"); addRule("Show file paths clearly when working with files"); return rules.map((rule) => `- ${rule}`).join("\n"); } /** Build the ordered, independently replaceable sections of the structured system prompt. */ export function buildSystemPromptSections(input) { const options = normalizeBuildSystemPromptOptions(input); const { customPrompt, selectedTools, hiddenTools, toolSnippets, toolGuidelines, promptGuidelines, appendSystemPrompt, sections: customSections, cwd, contextFiles, skills, } = options; for (const name of Object.keys(customSections)) { if (!SYSTEM_PROMPT_SECTION_NAME.test(name) || name === "preamble") { throw new Error(`Invalid system prompt section name: ${name}`); } } const declaredTools = selectedTools.filter((name) => !hiddenTools.includes(name)); const promptSections = {}; if (customPrompt) { promptSections.preamble = customPrompt; } else { promptSections.preamble = "You are a coding assistant running inside pi, a coding agent harness. Help users inspect files, run commands, and modify code."; const visibleTools = declaredTools.filter((name) => !!toolSnippets[name]); const tools = visibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets[name]}`).join("\n") : "(none)"; promptSections.tools = `${tools}\n\nAdditional tools may be available.`; promptSections.rules = buildRules(declaredTools, toolGuidelines, promptGuidelines); promptSections.docs = `Pi documentation (read only when the user asks about the harness itself, its SDK, extensions, themes, skills, or TUI): - Main documentation: ${getReadmePath()} - Additional docs: ${getDocsPath()} - Examples: ${getExamplesPath()} (extensions, custom tools, SDK) - When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory - When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), packages (docs/packages.md), environment variables (docs/environment-variables.md), MCP servers (docs/mcp.md), codemode scripts and non-LLM models such as classifiers and image models (docs/codemode.md) - Before implementing harness changes, read the relevant docs and examples in full, following related cross-references.`; } if (appendSystemPrompt) promptSections.addendum = appendSystemPrompt; if (contextFiles.length > 0) promptSections.project_context = renderProjectContext(contextFiles); // A hidden reader is still reachable through another tool, so skills stay but the hint names no tool. const readers = ["read", "bash"]; const skillFileReadTool = readers.find((tool) => declaredTools.includes(tool)) ?? (readers.some((tool) => selectedTools.includes(tool)) ? "indirect" : undefined); if (skillFileReadTool && skills.length > 0) { const skillsPrompt = formatSkillsForPrompt(skills, skillFileReadTool).trim(); if (skillsPrompt) promptSections.skills = skillsPrompt; } promptSections.cwd = cwd.replace(/\\/g, "/"); for (const [name, content] of Object.entries(customSections)) { if (content) promptSections[name] = content; } const sections = { preamble: promptSections.preamble }; for (const [name, content] of Object.entries(promptSections)) { if (name !== "preamble") sections[name] = `<${name}>\n${content}\n</${name}>`; } return sections; } /** * The complete prompt state for `input`. A forced prompt is opaque and lives in `content` * with no sections; otherwise `content` is empty and the structured sections carry the prompt. */ export function buildSystemPromptState(input) { if (input.forceSystemPrompt !== undefined) return { content: input.forceSystemPrompt }; return { content: "", sections: buildSystemPromptSections(input) }; } /** Build the system prompt text, rendered exactly as the transcript's system message replays it. */ export function buildSystemPrompt(input) { return getSystemMessageText({ role: "system", ...buildSystemPromptState(input), timestamp: 0 }); } /** * Diff the sections the model currently has (replayed from the transcript, so never null) * against the desired ones. Returns a `SystemMessage.sections` patch, or undefined when * nothing changed. */ export function diffSystemPromptSections(previous, current) { const patch = {}; for (const [name, text] of Object.entries(current)) { if (previous[name] !== text) patch[name] = text; } for (const name of Object.keys(previous)) { if (current[name] === undefined) patch[name] = null; } return Object.keys(patch).length > 0 ? patch : undefined; } //# sourceMappingURL=system-prompt.js.map