返回源码地图

packages/coding-agent/src/core/system-prompt.ts

v1.0.0 · a13d35a742c6 · 08:prompt sections 和 diff

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/**
2 * System prompt construction and project context loading
3 */
4
5import { getSystemMessageText } from "@earendil-works/pi-ai";
6import { getDocsPath, getExamplesPath, getReadmePath } from "../config.ts";
7import { formatSkillsForPrompt, type Skill } from "./skills.ts";
8
9export interface BuildSystemPromptOptions {
10 /** Custom system prompt (replaces the default prefix). */
11 customPrompt?: string;
12 /** Exact full prompt replacement set by a before_agent_start handler. */
13 forceSystemPrompt?: string;
14 /** Tools to include in prompt. Default: [read, bash, edit, write]. */
15 selectedTools?: string[];
16 /** Optional one-line tool snippets keyed by tool name. */
17 toolSnippets?: Record<string, string>;
18 /** Guideline bullets contributed by each tool, keyed by tool name. */
19 toolGuidelines?: Record<string, string[]>;
20 /** Additional guideline bullets appended to the default system prompt rules. */
21 promptGuidelines?: string[];
22 /** Text appended from user configuration before project context, skills, and cwd. */
23 appendSystemPrompt?: string;
24 /** Additional XML-wrapped prompt sections keyed by tag name. */
25 sections?: Record<string, string>;
26 /** Working directory. */
27 cwd: string;
28 /** Pre-loaded context files. */
29 contextFiles?: Array<{ path: string; content: string }>;
30 /** Pre-loaded skills. */
31 skills?: Skill[];
32}
33
34export type NormalizedBuildSystemPromptOptions = BuildSystemPromptOptions & {
35 selectedTools: string[];
36 toolSnippets: Record<string, string>;
37 toolGuidelines: Record<string, string[]>;
38 promptGuidelines: string[];
39 appendSystemPrompt: string;
40 sections: Record<string, string>;
41 contextFiles: Array<{ path: string; content: string }>;
42 skills: Skill[];
43};
44
45/**
46 * Ordered system prompt sections, keyed by name. `preamble` is untagged text; every other
47 * section is wrapped in a tag of the same name so the model can match later updates to it.
48 * These become `SystemMessage.sections` in the transcript.
49 */
50export type SystemPromptSections = Record<string, string>;
51
52const SYSTEM_PROMPT_SECTION_NAME = /^[a-z][a-z0-9_-]*$/;
53/** Normalize prompt input into the mutable, collection-complete shape exposed to extensions. */
54export function normalizeBuildSystemPromptOptions(input: BuildSystemPromptOptions): NormalizedBuildSystemPromptOptions {
55 return {
56 customPrompt: input.customPrompt,
57 forceSystemPrompt: input.forceSystemPrompt,
58 selectedTools: [...(input.selectedTools ?? ["read", "bash", "edit", "write"])],
59 toolSnippets: { ...(input.toolSnippets ?? {}) },
60 toolGuidelines: Object.fromEntries(
61 Object.entries(input.toolGuidelines ?? {}).map(([name, guidelines]) => [name, [...guidelines]]),
62 ),
63 promptGuidelines: [...(input.promptGuidelines ?? [])],
64 appendSystemPrompt: input.appendSystemPrompt ?? "",
65 sections: { ...(input.sections ?? {}) },
66 cwd: input.cwd,
67 contextFiles: (input.contextFiles ?? []).map((file) => ({ ...file })),
68 skills: (input.skills ?? []).map((skill) => ({ ...skill })),
69 };
70}
71
72function renderProjectContext(contextFiles: Array<{ path: string; content: string }>): string {
73 return [
74 "Project-specific instructions and guidelines:",
75 ...contextFiles.map(
76 ({ path, content }) => `<project_instructions path="${path}">\n${content}\n</project_instructions>`,
77 ),
78 ].join("\n\n");
79}
80
81function buildRules(
82 selectedTools: string[],
83 toolGuidelines: Record<string, string[]>,
84 promptGuidelines: string[],
85): string {
86 const rules: string[] = [];
87 const seen = new Set<string>();
88 const addRule = (rule: string): void => {
89 const normalized = rule.trim();
90 if (!normalized || seen.has(normalized)) return;
91 seen.add(normalized);
92 rules.push(normalized);
93 };
94
95 const hasBash = selectedTools.includes("bash");
96 const hasPowerShell = selectedTools.includes("powershell");
97 const hasGrep = selectedTools.includes("grep");
98 const hasFind = selectedTools.includes("find");
99 const hasLs = selectedTools.includes("ls");
100
101 if ((hasBash || hasPowerShell) && !hasGrep && !hasFind && !hasLs) {
102 if (hasBash && hasPowerShell) {
103 addRule("Use bash or PowerShell for file operations like listing, searching, and finding files");
104 } else if (hasPowerShell) {
105 addRule("Use PowerShell for file operations like listing, searching, and finding files");
106 } else {
107 addRule("Use bash for file operations like ls, rg, find");
108 }
109 }
110
111 for (const name of selectedTools) {
112 for (const rule of toolGuidelines[name] ?? []) addRule(rule);
113 }
114 for (const rule of promptGuidelines) addRule(rule);
115 addRule("Be concise in your responses");
116 addRule("Show file paths clearly when working with files");
117 return rules.map((rule) => `- ${rule}`).join("\n");
118}
119
120/** Build the ordered, independently replaceable sections of the structured system prompt. */
121export function buildSystemPromptSections(input: BuildSystemPromptOptions): SystemPromptSections {
122 const options = normalizeBuildSystemPromptOptions(input);
123 const {
124 customPrompt,
125 selectedTools,
126 toolSnippets,
127 toolGuidelines,
128 promptGuidelines,
129 appendSystemPrompt,
130 sections: customSections,
131 cwd,
132 contextFiles,
133 skills,
134 } = options;
135
136 for (const name of Object.keys(customSections)) {
137 if (!SYSTEM_PROMPT_SECTION_NAME.test(name) || name === "preamble") {
138 throw new Error(`Invalid system prompt section name: ${name}`);
139 }
140 }
141
142 const promptSections: Record<string, string> = {};
143 if (customPrompt) {
144 promptSections.preamble = customPrompt;
145 } else {
146 promptSections.preamble =
147 "You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
148 const visibleTools = selectedTools.filter((name) => !!toolSnippets[name]);
149 const tools =
150 visibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets[name]}`).join("\n") : "(none)";
151 promptSections.tools = `${tools}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.`;
152 promptSections.rules = buildRules(selectedTools, toolGuidelines, promptGuidelines);
153 promptSections.docs = `Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):
154- Main documentation: ${getReadmePath()}
155- Additional docs: ${getDocsPath()}
156- Examples: ${getExamplesPath()} (extensions, custom tools, SDK)
157- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
158- 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), pi 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)
159- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
160- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
161 }
162
163 if (appendSystemPrompt) promptSections.addendum = appendSystemPrompt;
164 if (contextFiles.length > 0) promptSections.project_context = renderProjectContext(contextFiles);
165 const skillFileReadTool = (["read", "bash"] as const).find((tool) => selectedTools.includes(tool));
166 if (skillFileReadTool && skills.length > 0) {
167 const skillsPrompt = formatSkillsForPrompt(skills, skillFileReadTool).trim();
168 if (skillsPrompt) promptSections.skills = skillsPrompt;
169 }
170 promptSections.cwd = cwd.replace(/\\/g, "/");
171 for (const [name, content] of Object.entries(customSections)) {
172 if (content) promptSections[name] = content;
173 }
174
175 const sections: SystemPromptSections = { preamble: promptSections.preamble };
176 for (const [name, content] of Object.entries(promptSections)) {
177 if (name !== "preamble") sections[name] = `<${name}>\n${content}\n</${name}>`;
178 }
179 return sections;
180}
181
182/**
183 * The complete prompt state for `input`. A forced prompt is opaque and lives in `content`
184 * with no sections; otherwise `content` is empty and the structured sections carry the prompt.
185 */
186export function buildSystemPromptState(input: BuildSystemPromptOptions): {
187 content: string;
188 sections?: SystemPromptSections;
189} {
190 if (input.forceSystemPrompt !== undefined) return { content: input.forceSystemPrompt };
191 return { content: "", sections: buildSystemPromptSections(input) };
192}
193
194/** Build the system prompt text, rendered exactly as the transcript's system message replays it. */
195export function buildSystemPrompt(input: BuildSystemPromptOptions): string {
196 return getSystemMessageText({ role: "system", ...buildSystemPromptState(input), timestamp: 0 });
197}
198
199/**
200 * Diff the sections the model currently has (replayed from the transcript, so never null)
201 * against the desired ones. Returns a `SystemMessage.sections` patch, or undefined when
202 * nothing changed.
203 */
204export function diffSystemPromptSections(
205 previous: Record<string, string | null>,
206 current: SystemPromptSections,
207): Record<string, string | null> | undefined {
208 const patch: Record<string, string | null> = {};
209 for (const [name, text] of Object.entries(current)) {
210 if (previous[name] !== text) patch[name] = text;
211 }
212 for (const name of Object.keys(previous)) {
213 if (current[name] === undefined) patch[name] = null;
214 }
215 return Object.keys(patch).length > 0 ? patch : undefined;
216}