@earendil-works/pi-coding-agent
Version:
Coding agent CLI with read, bash, edit, write tools and session management
151 lines • 7.97 kB
JavaScript
/**
* 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