-
Notifications
You must be signed in to change notification settings - Fork 14k
Expand file tree
/
Copy pathsystem-prompt.ts
More file actions
216 lines (198 loc) · 9.21 KB
/
Copy pathsystem-prompt.ts
File metadata and controls
216 lines (198 loc) · 9.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
/**
* System prompt construction and project context loading
*/
import { getSystemMessageText } from "@earendil-works/pi-ai";
import { getDocsPath, getExamplesPath, getReadmePath } from "../config.ts";
import { formatSkillsForPrompt, type Skill } from "./skills.ts";
export interface BuildSystemPromptOptions {
/** Custom system prompt (replaces the default prefix). */
customPrompt?: string;
/** Exact full prompt replacement set by a before_agent_start handler. */
forceSystemPrompt?: string;
/** Tools to include in prompt. Default: [read, bash, edit, write]. */
selectedTools?: string[];
/** Optional one-line tool snippets keyed by tool name. */
toolSnippets?: Record<string, string>;
/** Guideline bullets contributed by each tool, keyed by tool name. */
toolGuidelines?: Record<string, string[]>;
/** Additional guideline bullets appended to the default system prompt rules. */
promptGuidelines?: string[];
/** Text appended from user configuration before project context, skills, and cwd. */
appendSystemPrompt?: string;
/** Additional XML-wrapped prompt sections keyed by tag name. */
sections?: Record<string, string>;
/** Working directory. */
cwd: string;
/** Pre-loaded context files. */
contextFiles?: Array<{ path: string; content: string }>;
/** Pre-loaded skills. */
skills?: Skill[];
}
export type NormalizedBuildSystemPromptOptions = BuildSystemPromptOptions & {
selectedTools: string[];
toolSnippets: Record<string, string>;
toolGuidelines: Record<string, string[]>;
promptGuidelines: string[];
appendSystemPrompt: string;
sections: Record<string, string>;
contextFiles: Array<{ path: string; content: string }>;
skills: Skill[];
};
/**
* Ordered system prompt sections, keyed by name. `preamble` is untagged text; every other
* section is wrapped in a tag of the same name so the model can match later updates to it.
* These become `SystemMessage.sections` in the transcript.
*/
export type SystemPromptSections = Record<string, string>;
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: BuildSystemPromptOptions): NormalizedBuildSystemPromptOptions {
return {
customPrompt: input.customPrompt,
forceSystemPrompt: input.forceSystemPrompt,
selectedTools: [...(input.selectedTools ?? ["read", "bash", "edit", "write"])],
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: Array<{ path: string; content: string }>): string {
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: string[],
toolGuidelines: Record<string, string[]>,
promptGuidelines: string[],
): string {
const rules: string[] = [];
const seen = new Set<string>();
const addRule = (rule: string): void => {
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: BuildSystemPromptOptions): SystemPromptSections {
const options = normalizeBuildSystemPromptOptions(input);
const {
customPrompt,
selectedTools,
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 promptSections: Record<string, string> = {};
if (customPrompt) {
promptSections.preamble = customPrompt;
} else {
promptSections.preamble =
"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.";
const visibleTools = selectedTools.filter((name) => !!toolSnippets[name]);
const tools =
visibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets[name]}`).join("\n") : "(none)";
promptSections.tools = `${tools}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.`;
promptSections.rules = buildRules(selectedTools, toolGuidelines, promptGuidelines);
promptSections.docs = `Pi documentation (read only when the user asks about pi 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), pi packages (docs/packages.md), environment variables (docs/environment-variables.md)
- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
}
if (appendSystemPrompt) promptSections.addendum = appendSystemPrompt;
if (contextFiles.length > 0) promptSections.project_context = renderProjectContext(contextFiles);
const skillFileReadTool = (["read", "bash"] as const).find((tool) => selectedTools.includes(tool));
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: SystemPromptSections = { 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: BuildSystemPromptOptions): {
content: string;
sections?: SystemPromptSections;
} {
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: BuildSystemPromptOptions): string {
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: Record<string, string | null>,
current: SystemPromptSections,
): Record<string, string | null> | undefined {
const patch: Record<string, string | null> = {};
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;
}