feat: Expose BuildSystemPromptOptions on before-agent-start event. (#3473)

Resolves: #3463
This commit is contained in:
Douglas Stephen
2026-04-20 16:06:47 -05:00
committed by GitHub
parent 7c69aadcaf
commit 1891b9ac01
9 changed files with 143 additions and 5 deletions

View File

@@ -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")` |

View File

@@ -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}`,
};
});
}

View File

@@ -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) {

View File

@@ -36,6 +36,7 @@ export type {
BeforeAgentStartEventResult,
BeforeProviderRequestEvent,
BeforeProviderRequestEventResult,
BuildSystemPromptOptions,
// Context
CompactOptions,
// Events - Agent

View File

@@ -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<BeforeAgentStartCombinedResult | undefined> {
const ctx = this.createContext();
const messages: NonNullable<BeforeAgentStartEventResult["message"]>[] = [];
@@ -806,6 +808,7 @@ export class ExtensionRunner {
prompt,
images,
systemPrompt: currentSystemPrompt,
systemPromptOptions,
};
const handlerResult = await handler(event, ctx);

View File

@@ -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 */

View File

@@ -35,6 +35,8 @@ export {
type AgentToolResult,
type AgentToolUpdateCallback,
type BeforeAgentStartEvent,
type BeforeAgentStartEventResult,
type BuildSystemPromptOptions,
type ContextEvent,
defineTool,
discoverAndLoadExtensions,

View File

@@ -56,8 +56,10 @@ export type {
AppKeybinding,
BashToolCallEvent,
BeforeAgentStartEvent,
BeforeAgentStartEventResult,
BeforeProviderRequestEvent,
BeforeProviderRequestEventResult,
BuildSystemPromptOptions,
CompactOptions,
ContextEvent,
ContextUsage,

View File

@@ -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<undefined>;
emitBeforeAgentStart: (
prompt: string,
images: unknown,
systemPrompt: string,
systemPromptOptions: BuildSystemPromptOptions,
) => Promise<undefined>;
};
};
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<undefined>;
emitBeforeAgentStart: (
prompt: string,
images: unknown,
systemPrompt: string,
systemPromptOptions: BuildSystemPromptOptions,
) => Promise<undefined>;
};
};
sessionWithRunner._extensionRunner = {