From 1891b9ac013e4b62742385aec355bebbc01efa44 Mon Sep 17 00:00:00 2001 From: Douglas Stephen Date: Mon, 20 Apr 2026 16:06:47 -0500 Subject: [PATCH] feat: Expose `BuildSystemPromptOptions` on `before-agent-start` event. (#3473) Resolves: #3463 --- packages/coding-agent/docs/extensions.md | 12 +++ .../examples/extensions/prompt-customizer.ts | 97 +++++++++++++++++++ .../coding-agent/src/core/agent-session.ts | 9 +- .../coding-agent/src/core/extensions/index.ts | 1 + .../src/core/extensions/runner.ts | 3 + .../coding-agent/src/core/extensions/types.ts | 7 ++ packages/coding-agent/src/core/index.ts | 2 + packages/coding-agent/src/index.ts | 2 + .../test/agent-session-concurrent.test.ts | 15 ++- 9 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 packages/coding-agent/examples/extensions/prompt-customizer.ts diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 356561c6..eb546f87 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -465,6 +465,15 @@ pi.on("before_agent_start", async (event, ctx) => { // event.prompt - user's prompt text // event.images - attached images (if any) // event.systemPrompt - current system prompt + // event.systemPromptOptions - structured options used to build the system prompt + // .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates) + // .selectedTools - tools currently active in the prompt + // .toolSnippets - one-line descriptions for each tool + // .promptGuidelines - custom guideline bullets + // .appendSystemPrompt - text from --append-system-prompt flags + // .cwd - working directory + // .contextFiles - AGENTS.md files and other loaded context files + // .skills - loaded skills return { // Inject a persistent message (stored in session, sent to LLM) @@ -479,6 +488,8 @@ pi.on("before_agent_start", async (event, ctx) => { }); ``` +The `systemPromptOptions` field gives extensions access to the same structured data Pi uses to build the system prompt. This lets you inspect what Pi has loaded — custom prompts, guidelines, tool snippets, context files, skills — without re-discovering resources or re-parsing flags. Use it when your extension needs to make deep, informed changes to the system prompt while respecting user-provided configuration. + #### agent_start / agent_end Fired once per user prompt. @@ -2320,6 +2331,7 @@ All examples in [examples/extensions/](../examples/extensions/). | `provider-payload.ts` | Inspect payloads and provider response headers | `on("before_provider_request")`, `on("after_provider_response")` | | `system-prompt-header.ts` | Display system prompt info | `on("agent_start")`, `getSystemPrompt` | | `claude-rules.ts` | Load rules from files | `on("session_start")`, `on("before_agent_start")` | +| `prompt-customizer.ts` | Add context-aware tool guidance using `systemPromptOptions` | `on("before_agent_start")`, `BuildSystemPromptOptions` | | `file-trigger.ts` | File watcher triggers messages | `sendMessage` | | **Compaction & Sessions** ||| | `custom-compaction.ts` | Custom compaction summary | `on("session_before_compact")` | diff --git a/packages/coding-agent/examples/extensions/prompt-customizer.ts b/packages/coding-agent/examples/extensions/prompt-customizer.ts new file mode 100644 index 00000000..92b70feb --- /dev/null +++ b/packages/coding-agent/examples/extensions/prompt-customizer.ts @@ -0,0 +1,97 @@ +/** + * Prompt Customizer Extension + * + * Demonstrates using systemPromptOptions to make informed, context-aware + * modifications to the system prompt without re-discovering resources. + * + * This extension adds tool-specific guidance based on what tools and skills + * are currently active, respecting whatever the user has configured. + * + * Usage: + * 1. Copy this file to ~/.pi/agent/extensions/ or your project's .pi/extensions/ + * 2. Use the extension — it automatically adapts to your active tools and skills + */ + +import type { BuildSystemPromptOptions, ExtensionAPI } from "@mariozechner/pi-coding-agent"; + +/** + * Adds tool-specific guidance that adapts to the active tool set. + * Instead of appending one-size-fits-all instructions, this reads what's + * actually loaded and tailors the guidance accordingly. + */ +function addToolGuidance(options: BuildSystemPromptOptions, basePrompt: string): string { + const hasTool = (name: string) => options.selectedTools?.includes(name) ?? false; + + const parts: string[] = []; + + if (hasTool("read")) { + parts.push( + "• Use the `read` tool for file contents (supports text and images).", + " - For large files, use `offset` and `limit` to read in chunks.", + ); + } + + if (hasTool("bash")) { + parts.push("• Execute commands with the `bash` tool. Use it for file operations like `ls`, `find`, `grep`."); + } + + if (hasTool("edit")) { + parts.push( + "• Use the `edit` tool for precise text replacements in files. Match exact content including whitespace.", + ); + } + + if (hasTool("write")) { + parts.push("• Use the `write` tool to create new files or overwrite existing ones completely."); + } + + if (options.skills && options.skills.length > 0) { + const skillNames = options.skills.map((s) => s.name).join(", "); + parts.push(`\nAvailable skills: ${skillNames}`, "Use skill documentation for best practices on specific tools."); + } + + if (parts.length === 0) { + return basePrompt; + } + + return `${basePrompt} + +## Tool Guidance + +${parts.join("\n")} +`; +} + +/** + * Merges extension instructions with user-provided append prompts. + * This respects whatever the user configured via --append-system-prompt + * flags or files, rather than duplicating that work. + */ +function mergeWithUserAppend(options: BuildSystemPromptOptions): string { + const userAppend = options.appendSystemPrompt; + const extensionSpecific = ` +## Extension-Added Context + +This prompt includes tool guidance and skill information loaded dynamically. +If you have additional requirements, configure them via --append-system-prompt or project context files. +`; + + if (userAppend) { + return `${userAppend}\n\n${extensionSpecific}`; + } + + return extensionSpecific; +} + +export default function promptCustomizer(pi: ExtensionAPI) { + pi.on("before_agent_start", async (event) => { + const { systemPrompt, systemPromptOptions } = event; + + const customPrompt = addToolGuidance(systemPromptOptions, systemPrompt); + const appendSection = mergeWithUserAppend(systemPromptOptions); + + return { + systemPrompt: `${customPrompt}${appendSection}`, + }; + }); +} diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index f90ee4c7..ae47371d 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -76,7 +76,7 @@ import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } import type { SettingsManager } from "./settings-manager.js"; import type { SlashCommandInfo } from "./slash-commands.js"; import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js"; -import { buildSystemPrompt } from "./system-prompt.js"; +import { type BuildSystemPromptOptions, buildSystemPrompt } from "./system-prompt.js"; import { type BashOperations, createLocalBashOperations } from "./tools/bash.js"; import { createAllToolDefinitions } from "./tools/index.js"; import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js"; @@ -300,6 +300,7 @@ export class AgentSession { // Base system prompt (without extension appends) - used to apply fresh appends each turn private _baseSystemPrompt = ""; + private _baseSystemPromptOptions: BuildSystemPromptOptions = {}; constructor(config: AgentSessionConfig) { this.agent = config.agent; @@ -906,7 +907,7 @@ export class AgentSession { const loadedSkills = this._resourceLoader.getSkills().skills; const loadedContextFiles = this._resourceLoader.getAgentsFiles().agentsFiles; - return buildSystemPrompt({ + this._baseSystemPromptOptions = { cwd: this._cwd, skills: loadedSkills, contextFiles: loadedContextFiles, @@ -915,7 +916,8 @@ export class AgentSession { selectedTools: validToolNames, toolSnippets, promptGuidelines, - }); + }; + return buildSystemPrompt(this._baseSystemPromptOptions); } // ========================================================================= @@ -1048,6 +1050,7 @@ export class AgentSession { expandedText, currentImages, this._baseSystemPrompt, + this._baseSystemPromptOptions, ); // Add all custom messages from extensions if (result?.messages) { diff --git a/packages/coding-agent/src/core/extensions/index.ts b/packages/coding-agent/src/core/extensions/index.ts index 5a90c964..339cbe9c 100644 --- a/packages/coding-agent/src/core/extensions/index.ts +++ b/packages/coding-agent/src/core/extensions/index.ts @@ -36,6 +36,7 @@ export type { BeforeAgentStartEventResult, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, + BuildSystemPromptOptions, // Context CompactOptions, // Events - Agent diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index 292d569b..2022bbea 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -10,6 +10,7 @@ import type { ResourceDiagnostic } from "../diagnostics.js"; import type { KeybindingsConfig } from "../keybindings.js"; import type { ModelRegistry } from "../model-registry.js"; import type { SessionManager } from "../session-manager.js"; +import type { BuildSystemPromptOptions } from "../system-prompt.js"; import type { BeforeAgentStartEvent, BeforeAgentStartEventResult, @@ -789,6 +790,7 @@ export class ExtensionRunner { prompt: string, images: ImageContent[] | undefined, systemPrompt: string, + systemPromptOptions: BuildSystemPromptOptions, ): Promise { const ctx = this.createContext(); const messages: NonNullable[] = []; @@ -806,6 +808,7 @@ export class ExtensionRunner { prompt, images, systemPrompt: currentSystemPrompt, + systemPromptOptions, }; const handlerResult = await handler(event, ctx); diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index f7852fa4..31e10ee9 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -57,6 +57,7 @@ import type { } from "../session-manager.js"; import type { SlashCommandInfo } from "../slash-commands.js"; import type { SourceInfo } from "../source-info.js"; +import type { BuildSystemPromptOptions } from "../system-prompt.js"; import type { BashOperations } from "../tools/bash.js"; import type { EditToolDetails } from "../tools/edit.js"; import type { @@ -75,6 +76,7 @@ import type { } from "../tools/index.js"; export type { ExecOptions, ExecResult } from "../exec.js"; +export type { BuildSystemPromptOptions } from "../system-prompt.js"; export type { AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode }; export type { AppKeybinding, KeybindingsManager } from "../keybindings.js"; @@ -582,9 +584,14 @@ export interface AfterProviderResponseEvent { /** Fired after user submits prompt but before agent loop. */ export interface BeforeAgentStartEvent { type: "before_agent_start"; + /** The raw user prompt text (after expansion). */ prompt: string; + /** Images attached to the user prompt, if any. */ images?: ImageContent[]; + /** The fully assembled system prompt string. */ systemPrompt: string; + /** Structured options used to build the system prompt. Extensions can inspect this to understand what Pi loaded without re-discovering resources. */ + systemPromptOptions: BuildSystemPromptOptions; } /** Fired when an agent loop starts */ diff --git a/packages/coding-agent/src/core/index.ts b/packages/coding-agent/src/core/index.ts index d4faf8e2..003e1394 100644 --- a/packages/coding-agent/src/core/index.ts +++ b/packages/coding-agent/src/core/index.ts @@ -35,6 +35,8 @@ export { type AgentToolResult, type AgentToolUpdateCallback, type BeforeAgentStartEvent, + type BeforeAgentStartEventResult, + type BuildSystemPromptOptions, type ContextEvent, defineTool, discoverAndLoadExtensions, diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 1240f7c1..809870ed 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -56,8 +56,10 @@ export type { AppKeybinding, BashToolCallEvent, BeforeAgentStartEvent, + BeforeAgentStartEventResult, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, + BuildSystemPromptOptions, CompactOptions, ContextEvent, ContextUsage, diff --git a/packages/coding-agent/test/agent-session-concurrent.test.ts b/packages/coding-agent/test/agent-session-concurrent.test.ts index 18ff57a9..28001fbd 100644 --- a/packages/coding-agent/test/agent-session-concurrent.test.ts +++ b/packages/coding-agent/test/agent-session-concurrent.test.ts @@ -21,6 +21,7 @@ import { AuthStorage } from "../src/core/auth-storage.js"; import { ModelRegistry } from "../src/core/model-registry.js"; import { SessionManager } from "../src/core/session-manager.js"; import { SettingsManager } from "../src/core/settings-manager.js"; +import type { BuildSystemPromptOptions } from "../src/core/system-prompt.js"; import { createTestExtensionsResult, createTestResourceLoader } from "./utilities.js"; // Mock stream that mimics AssistantMessageEventStream @@ -443,7 +444,12 @@ describe("AgentSession concurrent prompt guard", () => { images: unknown, source: "interactive" | "rpc" | "extension", ) => Promise<{ action: "continue" }>; - emitBeforeAgentStart: (prompt: string, images: unknown, systemPrompt: string) => Promise; + emitBeforeAgentStart: ( + prompt: string, + images: unknown, + systemPrompt: string, + systemPromptOptions: BuildSystemPromptOptions, + ) => Promise; }; }; sessionWithRunner._extensionRunner = { @@ -578,7 +584,12 @@ describe("AgentSession concurrent prompt guard", () => { images: unknown, source: "interactive" | "rpc" | "extension", ) => Promise<{ action: "continue" }>; - emitBeforeAgentStart: (prompt: string, images: unknown, systemPrompt: string) => Promise; + emitBeforeAgentStart: ( + prompt: string, + images: unknown, + systemPrompt: string, + systemPromptOptions: BuildSystemPromptOptions, + ) => Promise; }; }; sessionWithRunner._extensionRunner = {