chore: simplify codex prompt handling

This commit is contained in:
Mario Zechner 2026-01-17 21:53:01 +01:00
parent 94bd7f69fd
commit 4068bc556a
8 changed files with 33 additions and 87 deletions

View file

@ -2,6 +2,10 @@
## [Unreleased] ## [Unreleased]
### Changed
- OpenAI Codex responses now use the context system prompt directly in the instructions field.
### Fixed ### Fixed
- Fixed orphaned tool results after errored assistant messages causing Codex API errors. When an assistant message has `stopReason: "error"`, its tool calls are now excluded from pending tool tracking, preventing synthetic tool results from being generated for calls that will be dropped by provider-specific converters. ([#812](https://github.com/badlogic/pi-mono/issues/812)) - Fixed orphaned tool results after errored assistant messages causing Codex API errors. When an assistant message has `stopReason: "error"`, its tool calls are now excluded from pending tool tracking, preventing synthetic tool results from being generated for calls that will be dropped by provider-specific converters. ([#812](https://github.com/badlogic/pi-mono/issues/812))

View file

@ -1,13 +0,0 @@
/**
* Static Pi instructions for OpenAI Codex.
* This string is whitelisted by OpenAI and must not change.
*/
export const PI_STATIC_INSTRUCTIONS = `You are pi, an expert coding assistant. You help users with coding tasks by reading files, executing commands, editing code, and writing new files.
Pi specific Documentation:
- Main documentation: pi-internal://README.md
- Additional docs: pi-internal://docs
- Examples: pi-internal://examples (extensions, custom tools, SDK)
- When asked to create: custom models/providers (README.md), extensions (docs/extensions.md, examples/extensions/), themes (docs/theme.md), skills (docs/skills.md), TUI components (docs/tui.md - has copy-paste patterns)
- Always read the doc, examples, AND follow .md cross-references before implementing
`;

View file

@ -1,4 +1,3 @@
export * from "./constants.js";
export * from "./models.js"; export * from "./models.js";
export * from "./providers/anthropic.js"; export * from "./providers/anthropic.js";
export * from "./providers/google.js"; export * from "./providers/google.js";

View file

@ -4,7 +4,6 @@ import type {
ResponseOutputMessage, ResponseOutputMessage,
ResponseReasoningItem, ResponseReasoningItem,
} from "openai/resources/responses/responses.js"; } from "openai/resources/responses/responses.js";
import { PI_STATIC_INSTRUCTIONS } from "../constants.js";
import { calculateCost } from "../models.js"; import { calculateCost } from "../models.js";
import { getEnvApiKey } from "../stream.js"; import { getEnvApiKey } from "../stream.js";
import type { import type {
@ -215,22 +214,14 @@ function buildRequestBody(
context: Context, context: Context,
options?: OpenAICodexResponsesOptions, options?: OpenAICodexResponsesOptions,
): RequestBody { ): RequestBody {
const systemPrompt = buildSystemPrompt(context.systemPrompt);
const messages = convertMessages(model, context); const messages = convertMessages(model, context);
// Prepend developer messages
const developerMessages = systemPrompt.developerMessages.map((text) => ({
type: "message",
role: "developer",
content: [{ type: "input_text", text }],
}));
const body: RequestBody = { const body: RequestBody = {
model: model.id, model: model.id,
store: false, store: false,
stream: true, stream: true,
instructions: systemPrompt.instructions, instructions: context.systemPrompt,
input: [...developerMessages, ...messages], input: messages,
text: { verbosity: options?.textVerbosity || "medium" }, text: { verbosity: options?.textVerbosity || "medium" },
include: ["reasoning.encrypted_content"], include: ["reasoning.encrypted_content"],
prompt_cache_key: options?.sessionId, prompt_cache_key: options?.sessionId,
@ -262,23 +253,6 @@ function buildRequestBody(
return body; return body;
} }
function buildSystemPrompt(userSystemPrompt?: string): { instructions: string; developerMessages: string[] } {
// PI_STATIC_INSTRUCTIONS is whitelisted and must be in the instructions field.
// User's system prompt goes in developer messages, with the static prefix stripped.
const staticPrefix = PI_STATIC_INSTRUCTIONS.trim();
const developerMessages: string[] = [];
if (userSystemPrompt?.trim()) {
let dynamicPart = userSystemPrompt.trim();
if (dynamicPart.startsWith(staticPrefix)) {
dynamicPart = dynamicPart.slice(staticPrefix.length).trim();
}
if (dynamicPart) developerMessages.push(dynamicPart);
}
return { instructions: staticPrefix, developerMessages };
}
function clampReasoningEffort(modelId: string, effort: string): string { function clampReasoningEffort(modelId: string, effort: string): string {
const id = modelId.includes("/") ? modelId.split("/").pop()! : modelId; const id = modelId.includes("/") ? modelId.split("/").pop()! : modelId;
if (id.startsWith("gpt-5.2") && effort === "minimal") return "low"; if (id.startsWith("gpt-5.2") && effort === "minimal") return "low";

View file

@ -10,10 +10,18 @@
- Added `ctx.compact()` and `ctx.getContextUsage()` to extension contexts for programmatic compaction and context usage checks. - Added `ctx.compact()` and `ctx.getContextUsage()` to extension contexts for programmatic compaction and context usage checks.
- Added documentation for delete word forward and kill ring keybindings in interactive mode. ([#810](https://github.com/badlogic/pi-mono/pull/810) by [@Perlence](https://github.com/Perlence)) - Added documentation for delete word forward and kill ring keybindings in interactive mode. ([#810](https://github.com/badlogic/pi-mono/pull/810) by [@Perlence](https://github.com/Perlence))
### Changed
- Updated the default system prompt wording to clarify the pi harness and documentation scope.
### Fixed ### Fixed
- Fixed photon module failing to load in ESM context with "require is not defined" error ([#795](https://github.com/badlogic/pi-mono/pull/795) by [@dannote](https://github.com/dannote)) - Fixed photon module failing to load in ESM context with "require is not defined" error ([#795](https://github.com/badlogic/pi-mono/pull/795) by [@dannote](https://github.com/dannote))
### Removed
- Removed `pi-internal://` path resolution from the read tool.
## [0.48.0] - 2026-01-16 ## [0.48.0] - 2026-01-16
### Added ### Added

View file

@ -2,11 +2,10 @@
* System prompt construction and project context loading * System prompt construction and project context loading
*/ */
import { PI_STATIC_INSTRUCTIONS } from "@mariozechner/pi-ai";
import chalk from "chalk"; import chalk from "chalk";
import { existsSync, readFileSync } from "fs"; import { existsSync, readFileSync } from "fs";
import { join, resolve } from "path"; import { join, resolve } from "path";
import { getAgentDir, getReadmePath } from "../config.js"; import { getAgentDir, getDocsPath, getExamplesPath, getReadmePath } from "../config.js";
import type { SkillsSettings } from "./settings-manager.js"; import type { SkillsSettings } from "./settings-manager.js";
import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills.js"; import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills.js";
import type { ToolName } from "./tools/index.js"; import type { ToolName } from "./tools/index.js";
@ -136,17 +135,6 @@ export interface BuildSystemPromptOptions {
skills?: Skill[]; skills?: Skill[];
} }
/**
* Get the Pi installation path for documentation references.
* This resolves the pi-internal:// scheme used in the static instructions.
*/
function getPiPath(): string {
// getReadmePath returns something like /path/to/pi/README.md
// We want the parent directory
const readmePath = getReadmePath();
return resolve(readmePath, "..");
}
/** Build the system prompt with tools, guidelines, and context */ /** Build the system prompt with tools, guidelines, and context */
export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string { export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
const { const {
@ -185,7 +173,6 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
providedSkills ?? providedSkills ??
(skillsSettings?.enabled !== false ? loadSkills({ ...skillsSettings, cwd: resolvedCwd, agentDir }).skills : []); (skillsSettings?.enabled !== false ? loadSkills({ ...skillsSettings, cwd: resolvedCwd, agentDir }).skills : []);
// Handle custom prompt (full replacement)
if (resolvedCustomPrompt) { if (resolvedCustomPrompt) {
let prompt = resolvedCustomPrompt; let prompt = resolvedCustomPrompt;
@ -196,7 +183,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
// Append project context files // Append project context files
if (contextFiles.length > 0) { if (contextFiles.length > 0) {
prompt += "\n\n# Project Context\n\n"; prompt += "\n\n# Project Context\n\n";
prompt += "The following project context files have been loaded:\n\n"; prompt += "Project-specific instructions and guidelines:\n\n";
for (const { path: filePath, content } of contextFiles) { for (const { path: filePath, content } of contextFiles) {
prompt += `## ${filePath}\n\n${content}\n\n`; prompt += `## ${filePath}\n\n${content}\n\n`;
} }
@ -215,6 +202,11 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
return prompt; return prompt;
} }
// Get absolute paths to documentation and examples
const readmePath = getReadmePath();
const docsPath = getDocsPath();
const examplesPath = getExamplesPath();
// Build tools list based on selected tools // Build tools list based on selected tools
const tools = selectedTools || (["read", "bash", "edit", "write"] as ToolName[]); const tools = selectedTools || (["read", "bash", "edit", "write"] as ToolName[]);
const toolsList = tools.length > 0 ? tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join("\n") : "(none)"; const toolsList = tools.length > 0 ? tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join("\n") : "(none)";
@ -272,12 +264,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n"); const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n");
// Build prompt with static prefix + dynamic parts let prompt = `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 piPath = getPiPath();
let prompt = `${PI_STATIC_INSTRUCTIONS}
Pi path:
pi-internal:// refers to paths in ${piPath}
Available tools: Available tools:
${toolsList} ${toolsList}
@ -285,7 +272,14 @@ ${toolsList}
In addition to the tools above, you may have access to other custom tools depending on the project. In addition to the tools above, you may have access to other custom tools depending on the project.
Guidelines: Guidelines:
${guidelines}`; ${guidelines}
Pi documentation (only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):
- Main documentation: ${readmePath}
- Additional docs: ${docsPath}
- Examples: ${examplesPath} (extensions, custom tools, SDK)
- When asked to create: custom models/providers (README.md), extensions (docs/extensions.md, examples/extensions/), themes (docs/theme.md), skills (docs/skills.md), TUI components (docs/tui.md - has copy-paste patterns)
- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing`;
if (appendSection) { if (appendSection) {
prompt += appendSection; prompt += appendSection;
@ -294,7 +288,7 @@ ${guidelines}`;
// Append project context files // Append project context files
if (contextFiles.length > 0) { if (contextFiles.length > 0) {
prompt += "\n\n# Project Context\n\n"; prompt += "\n\n# Project Context\n\n";
prompt += "The following project context files have been loaded:\n\n"; prompt += "Project-specific instructions and guidelines:\n\n";
for (const { path: filePath, content } of contextFiles) { for (const { path: filePath, content } of contextFiles) {
prompt += `## ${filePath}\n\n${content}\n\n`; prompt += `## ${filePath}\n\n${content}\n\n`;
} }

View file

@ -1,12 +1,9 @@
import { accessSync, constants } from "node:fs"; import { accessSync, constants } from "node:fs";
import * as os from "node:os"; import * as os from "node:os";
import { isAbsolute, resolve as resolvePath } from "node:path"; import { isAbsolute, resolve as resolvePath } from "node:path";
import { getPackageDir } from "../../config.js";
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
const NARROW_NO_BREAK_SPACE = "\u202F"; const NARROW_NO_BREAK_SPACE = "\u202F";
export const PI_INTERNAL_SCHEME = "pi-internal://";
function normalizeUnicodeSpaces(str: string): string { function normalizeUnicodeSpaces(str: string): string {
return str.replace(UNICODE_SPACES, " "); return str.replace(UNICODE_SPACES, " ");
} }
@ -48,12 +45,6 @@ export function resolveToCwd(filePath: string, cwd: string): string {
} }
export function resolveReadPath(filePath: string, cwd: string): string { export function resolveReadPath(filePath: string, cwd: string): string {
// Handle pi-internal:// scheme for Pi package documentation
if (filePath.startsWith(PI_INTERNAL_SCHEME)) {
const relativePath = filePath.slice(PI_INTERNAL_SCHEME.length);
return resolvePath(getPackageDir(), relativePath);
}
const resolved = resolveToCwd(filePath, cwd); const resolved = resolveToCwd(filePath, cwd);
if (fileExists(resolved)) { if (fileExists(resolved)) {

View file

@ -5,7 +5,7 @@ import { constants } from "fs";
import { access as fsAccess, readFile as fsReadFile } from "fs/promises"; import { access as fsAccess, readFile as fsReadFile } from "fs/promises";
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js"; import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js"; import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
import { PI_INTERNAL_SCHEME, resolveReadPath } from "./path-utils.js"; import { resolveReadPath } from "./path-utils.js";
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
const readSchema = Type.Object({ const readSchema = Type.Object({
@ -111,19 +111,13 @@ export function createReadTool(cwd: string, options?: ReadToolOptions): AgentToo
if (dimensionNote) { if (dimensionNote) {
textNote += `\n${dimensionNote}`; textNote += `\n${dimensionNote}`;
} }
if (path.startsWith(PI_INTERNAL_SCHEME)) {
textNote += `\n[${path} -> ${absolutePath}. Use filesystem paths for further reads.]`;
}
content = [ content = [
{ type: "text", text: textNote }, { type: "text", text: textNote },
{ type: "image", data: resized.data, mimeType: resized.mimeType }, { type: "image", data: resized.data, mimeType: resized.mimeType },
]; ];
} else { } else {
let textNote = `Read image file [${mimeType}]`; const textNote = `Read image file [${mimeType}]`;
if (path.startsWith(PI_INTERNAL_SCHEME)) {
textNote += `\n[${path} -> ${absolutePath}. Use filesystem paths for further reads.]`;
}
content = [ content = [
{ type: "text", text: textNote }, { type: "text", text: textNote },
{ type: "image", data: base64, mimeType }, { type: "image", data: base64, mimeType },
@ -191,11 +185,6 @@ export function createReadTool(cwd: string, options?: ReadToolOptions): AgentToo
outputText = truncation.content; outputText = truncation.content;
} }
// Add filesystem path hint for pi-internal:// paths
if (path.startsWith(PI_INTERNAL_SCHEME)) {
outputText += `\n\n[${path} -> ${absolutePath}. Use filesystem paths for further reads.]`;
}
content = [{ type: "text", text: outputText }]; content = [{ type: "text", text: outputText }];
} }