/** * prompts.ts — System prompt builder for agents. */ import type { EnvInfo } from "#src/session/env"; import type { AgentPromptConfig } from "#src/types"; /** * Build the system prompt for an agent from its config. * * Both modes place the shared/stable parent prompt (or `genericBase` when no * parent is available) first so the LLM's KV cache can reuse the inherited * prefix across all subagent invocations. * * - "replace" mode: parent/genericBase + active_agent tag + env header + * config.systemPrompt. No `` bridge and no * `` wrapper — the custom prompt has full control and * the final say. * - "append" mode: parent/genericBase + sub-agent context bridge + * active_agent tag + env header + config.systemPrompt (wrapped in * `` when non-empty). * - "append" with empty systemPrompt: pure parent clone. * * Both modes include an `` tag so * downstream extensions (e.g. `@gotgenes/pi-permission-system`) can resolve * per-agent policy inside the child session by parsing the system prompt. * The tag follows the cacheable parent prefix in both modes. * * @param parentSystemPrompt The parent agent's effective system prompt. * @param parentCwd Parent cwd used to remove a contradictory Pi footer. */ export function buildAgentPrompt( config: AgentPromptConfig, cwd: string, env: EnvInfo, parentSystemPrompt?: string, parentCwd?: string, ): string { const activeAgentTag = `\n\n`; const envBlock = `# Environment Working directory: ${cwd} ${env.isGitRepo ? `Git repository: yes\nBranch: ${env.branch}` : "Not a git repository"} Platform: ${env.platform}`; const identity = parentSystemPrompt ? withoutContradictoryCwdFooter(parentSystemPrompt, parentCwd, cwd) : genericBase; if (config.promptMode === "append") { const bridge = ` You are operating as a sub-agent invoked to handle a specific task. - Use the read tool instead of cat/head/tail - Use the edit tool instead of sed/awk - Use the write tool instead of echo/heredoc - Use the find tool instead of bash find/ls for file search - Use the grep tool instead of bash grep/rg for content search - Make independent tool calls in parallel - Use absolute file paths - Do not use emojis - Be concise but complete `; const customSection = config.systemPrompt.trim() ? `\n\n\n${config.systemPrompt}\n` : ""; // Place shared/stable content first so the LLM's KV cache can reuse the // inherited prefix across all subagent invocations. The parent prompt is // placed verbatim (no wrapper tag) so it forms an identical byte prefix // with the parent session, maximising KV cache hits. The // tag and env block vary per call and are placed after the cached prefix. return ( identity + "\n\n" + bridge + "\n\n" + activeAgentTag + envBlock + customSection ); } // "replace" mode — parent/genericBase prefix first for KV cache reuse, then // the active_agent tag, env block, and the config's full system prompt. // Unlike append mode, no bridge or // wrapper is injected — the custom prompt retains full control. return identity + "\n\n" + activeAgentTag + envBlock + "\n\n" + config.systemPrompt; } /** * Pi appends an authoritative cwd line to the parent prompt. A workspace-backed * child appends its own environment block, so retain that footer only when both * sessions actually share a directory. */ function withoutContradictoryCwdFooter( prompt: string, parentCwd: string | undefined, childCwd: string, ): string { if (!parentCwd || toPromptPath(parentCwd) === toPromptPath(childCwd)) return prompt; const footerLine = `Current working directory: ${toPromptPath(parentCwd)}`; return prompt.split("\n").filter((line) => line !== footerLine).join("\n"); } function toPromptPath(cwd: string): string { return cwd.replaceAll("\\", "/"); } /** Fallback base prompt when parent system prompt is unavailable (both modes). */ const genericBase = `# Role You are a general-purpose coding agent for complex, multi-step tasks. You have full access to read, write, edit files, and execute commands. Do what has been asked; nothing more, nothing less.`;