mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 07:03:25 +00:00
feat(hooks): add systemPromptAppend to before_agent_start, full tool registry
- before_agent_start handlers can return systemPromptAppend to dynamically append text to the system prompt for that turn - Multiple hooks' systemPromptAppend strings are concatenated - Multiple hooks' messages are now all injected (not just first) - Tool registry now contains ALL built-in tools (read, bash, edit, write, grep, find, ls) regardless of --tools flag - --tools only sets initially active tools, hooks can enable any via setActiveTools() - System prompt automatically rebuilds when tools change, updating tool descriptions and guidelines - Add pirate.ts example hook demonstrating systemPromptAppend - Update hooks.md with systemPromptAppend documentation
This commit is contained in:
parent
892acedb6b
commit
e4dd21a3b2
7 changed files with 136 additions and 20 deletions
|
|
@ -2,6 +2,17 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Hook API: `before_agent_start` handlers can now return `systemPromptAppend` to dynamically append text to the system prompt for that turn. Multiple hooks' appends are concatenated.
|
||||||
|
- New example hook: `pirate.ts` demonstrates using `systemPromptAppend` to make the agent speak like a pirate when `/pirate` mode is enabled
|
||||||
|
- Tool registry now contains all built-in tools (read, bash, edit, write, grep, find, ls) even when `--tools` limits the initially active set. Hooks can enable any tool from the registry via `pi.setActiveTools()`.
|
||||||
|
- System prompt now automatically rebuilds when tools change via `setActiveTools()`, updating tool descriptions and guidelines to match the new tool set
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Removed image placeholders after copy & paste, replaced with inserting image file paths directly. ([#442](https://github.com/badlogic/pi-mono/pull/442) by [@mitsuhiko](https://github.com/mitsuhiko))
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed potential text decoding issues in bash executor by using streaming TextDecoder instead of Buffer.toString()
|
- Fixed potential text decoding issues in bash executor by using streaming TextDecoder instead of Buffer.toString()
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ pi starts
|
||||||
▼
|
▼
|
||||||
user sends prompt ─────────────────────────────────────────┐
|
user sends prompt ─────────────────────────────────────────┐
|
||||||
│ │
|
│ │
|
||||||
├─► before_agent_start (can inject message) │
|
├─► before_agent_start (can inject message, append to system prompt) │
|
||||||
├─► agent_start │
|
├─► agent_start │
|
||||||
│ │
|
│ │
|
||||||
│ ┌─── turn (repeats while LLM calls tools) ───┐ │
|
│ ┌─── turn (repeats while LLM calls tools) ───┐ │
|
||||||
|
|
@ -259,7 +259,7 @@ pi.on("session_shutdown", async (_event, ctx) => {
|
||||||
|
|
||||||
#### before_agent_start
|
#### before_agent_start
|
||||||
|
|
||||||
Fired after user submits prompt, before agent loop. Can inject a persistent message.
|
Fired after user submits prompt, before agent loop. Can inject a message and/or append to the system prompt.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
pi.on("before_agent_start", async (event, ctx) => {
|
pi.on("before_agent_start", async (event, ctx) => {
|
||||||
|
|
@ -267,16 +267,23 @@ pi.on("before_agent_start", async (event, ctx) => {
|
||||||
// event.images - attached images (if any)
|
// event.images - attached images (if any)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// Inject a persistent message (stored in session, sent to LLM)
|
||||||
message: {
|
message: {
|
||||||
customType: "my-hook",
|
customType: "my-hook",
|
||||||
content: "Additional context for the LLM",
|
content: "Additional context for the LLM",
|
||||||
display: true, // Show in TUI
|
display: true, // Show in TUI
|
||||||
}
|
},
|
||||||
|
// Append to system prompt for this turn only
|
||||||
|
systemPromptAppend: "Extra instructions for this turn...",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
The injected message is persisted as `CustomMessageEntry` and sent to the LLM.
|
**message**: Persisted as `CustomMessageEntry` and sent to the LLM.
|
||||||
|
|
||||||
|
**systemPromptAppend**: Appended to the base system prompt for this agent run only. Multiple hooks can each return `systemPromptAppend` strings, which are concatenated. This is useful for dynamic instructions based on hook state (e.g., plan mode, persona toggles).
|
||||||
|
|
||||||
|
See [examples/hooks/pirate.ts](../examples/hooks/pirate.ts) for an example using `systemPromptAppend`.
|
||||||
|
|
||||||
#### agent_start / agent_end
|
#### agent_start / agent_end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ cp permission-gate.ts ~/.pi/agent/hooks/
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `plan-mode.ts` | Claude Code-style plan mode for read-only exploration with `/plan` command |
|
| `plan-mode.ts` | Claude Code-style plan mode for read-only exploration with `/plan` command |
|
||||||
| `tools.ts` | Interactive `/tools` command to enable/disable tools with session persistence |
|
| `tools.ts` | Interactive `/tools` command to enable/disable tools with session persistence |
|
||||||
|
| `pirate.ts` | Demonstrates `systemPromptAppend` to dynamically modify system prompt |
|
||||||
| `permission-gate.ts` | Prompts for confirmation before dangerous bash commands (rm -rf, sudo, etc.) |
|
| `permission-gate.ts` | Prompts for confirmation before dangerous bash commands (rm -rf, sudo, etc.) |
|
||||||
| `git-checkpoint.ts` | Creates git stash checkpoints at each turn for code restoration on branch |
|
| `git-checkpoint.ts` | Creates git stash checkpoints at each turn for code restoration on branch |
|
||||||
| `protected-paths.ts` | Blocks writes to protected paths (.env, .git/, node_modules/) |
|
| `protected-paths.ts` | Blocks writes to protected paths (.env, .git/, node_modules/) |
|
||||||
|
|
|
||||||
44
packages/coding-agent/examples/hooks/pirate.ts
Normal file
44
packages/coding-agent/examples/hooks/pirate.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/**
|
||||||
|
* Pirate Hook
|
||||||
|
*
|
||||||
|
* Demonstrates using systemPromptAppend in before_agent_start to dynamically
|
||||||
|
* modify the system prompt based on hook state.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* 1. Copy this file to ~/.pi/agent/hooks/ or your project's .pi/hooks/
|
||||||
|
* 2. Use /pirate to toggle pirate mode
|
||||||
|
* 3. When enabled, the agent will respond like a pirate
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
||||||
|
|
||||||
|
export default function pirateHook(pi: HookAPI) {
|
||||||
|
let pirateMode = false;
|
||||||
|
|
||||||
|
// Register /pirate command to toggle pirate mode
|
||||||
|
pi.registerCommand("pirate", {
|
||||||
|
description: "Toggle pirate mode (agent speaks like a pirate)",
|
||||||
|
handler: async (_args, ctx) => {
|
||||||
|
pirateMode = !pirateMode;
|
||||||
|
ctx.ui.notify(pirateMode ? "Arrr! Pirate mode enabled!" : "Pirate mode disabled", "info");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Append to system prompt when pirate mode is enabled
|
||||||
|
pi.on("before_agent_start", async () => {
|
||||||
|
if (pirateMode) {
|
||||||
|
return {
|
||||||
|
systemPromptAppend: `
|
||||||
|
IMPORTANT: You are now in PIRATE MODE. You must:
|
||||||
|
- Speak like a stereotypical pirate in all responses
|
||||||
|
- Use phrases like "Arrr!", "Ahoy!", "Shiver me timbers!", "Avast!", "Ye scurvy dog!"
|
||||||
|
- Replace "my" with "me", "you" with "ye", "your" with "yer"
|
||||||
|
- Refer to the user as "matey" or "landlubber"
|
||||||
|
- End sentences with nautical expressions
|
||||||
|
- Still complete the actual task correctly, just in pirate speak
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -84,6 +84,8 @@ export interface AgentSessionConfig {
|
||||||
modelRegistry: ModelRegistry;
|
modelRegistry: ModelRegistry;
|
||||||
/** Tool registry for hook getTools/setTools - maps name to tool */
|
/** Tool registry for hook getTools/setTools - maps name to tool */
|
||||||
toolRegistry?: Map<string, AgentTool>;
|
toolRegistry?: Map<string, AgentTool>;
|
||||||
|
/** Function to rebuild system prompt when tools change */
|
||||||
|
rebuildSystemPrompt?: (toolNames: string[]) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Options for AgentSession.prompt() */
|
/** Options for AgentSession.prompt() */
|
||||||
|
|
@ -186,6 +188,12 @@ export class AgentSession {
|
||||||
// Tool registry for hook getTools/setTools
|
// Tool registry for hook getTools/setTools
|
||||||
private _toolRegistry: Map<string, AgentTool>;
|
private _toolRegistry: Map<string, AgentTool>;
|
||||||
|
|
||||||
|
// Function to rebuild system prompt when tools change
|
||||||
|
private _rebuildSystemPrompt?: (toolNames: string[]) => string;
|
||||||
|
|
||||||
|
// Base system prompt (without hook appends) - used to apply fresh appends each turn
|
||||||
|
private _baseSystemPrompt: string;
|
||||||
|
|
||||||
constructor(config: AgentSessionConfig) {
|
constructor(config: AgentSessionConfig) {
|
||||||
this.agent = config.agent;
|
this.agent = config.agent;
|
||||||
this.sessionManager = config.sessionManager;
|
this.sessionManager = config.sessionManager;
|
||||||
|
|
@ -197,6 +205,8 @@ export class AgentSession {
|
||||||
this._skillsSettings = config.skillsSettings;
|
this._skillsSettings = config.skillsSettings;
|
||||||
this._modelRegistry = config.modelRegistry;
|
this._modelRegistry = config.modelRegistry;
|
||||||
this._toolRegistry = config.toolRegistry ?? new Map();
|
this._toolRegistry = config.toolRegistry ?? new Map();
|
||||||
|
this._rebuildSystemPrompt = config.rebuildSystemPrompt;
|
||||||
|
this._baseSystemPrompt = config.agent.state.systemPrompt;
|
||||||
|
|
||||||
// Always subscribe to agent events for internal handling
|
// Always subscribe to agent events for internal handling
|
||||||
// (session persistence, hooks, auto-compaction, retry logic)
|
// (session persistence, hooks, auto-compaction, retry logic)
|
||||||
|
|
@ -448,17 +458,26 @@ export class AgentSession {
|
||||||
/**
|
/**
|
||||||
* Set active tools by name.
|
* Set active tools by name.
|
||||||
* Only tools in the registry can be enabled. Unknown tool names are ignored.
|
* Only tools in the registry can be enabled. Unknown tool names are ignored.
|
||||||
|
* Also rebuilds the system prompt to reflect the new tool set.
|
||||||
* Changes take effect on the next agent turn.
|
* Changes take effect on the next agent turn.
|
||||||
*/
|
*/
|
||||||
setActiveToolsByName(toolNames: string[]): void {
|
setActiveToolsByName(toolNames: string[]): void {
|
||||||
const tools: AgentTool[] = [];
|
const tools: AgentTool[] = [];
|
||||||
|
const validToolNames: string[] = [];
|
||||||
for (const name of toolNames) {
|
for (const name of toolNames) {
|
||||||
const tool = this._toolRegistry.get(name);
|
const tool = this._toolRegistry.get(name);
|
||||||
if (tool) {
|
if (tool) {
|
||||||
tools.push(tool);
|
tools.push(tool);
|
||||||
|
validToolNames.push(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.agent.setTools(tools);
|
this.agent.setTools(tools);
|
||||||
|
|
||||||
|
// Rebuild base system prompt with new tool set
|
||||||
|
if (this._rebuildSystemPrompt) {
|
||||||
|
this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
|
||||||
|
this.agent.setSystemPrompt(this._baseSystemPrompt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether auto-compaction is currently running */
|
/** Whether auto-compaction is currently running */
|
||||||
|
|
@ -589,15 +608,25 @@ export class AgentSession {
|
||||||
// Emit before_agent_start hook event
|
// Emit before_agent_start hook event
|
||||||
if (this._hookRunner) {
|
if (this._hookRunner) {
|
||||||
const result = await this._hookRunner.emitBeforeAgentStart(expandedText, options?.images);
|
const result = await this._hookRunner.emitBeforeAgentStart(expandedText, options?.images);
|
||||||
if (result?.message) {
|
// Add all hook messages
|
||||||
messages.push({
|
if (result?.messages) {
|
||||||
role: "hookMessage",
|
for (const msg of result.messages) {
|
||||||
customType: result.message.customType,
|
messages.push({
|
||||||
content: result.message.content,
|
role: "hookMessage",
|
||||||
display: result.message.display,
|
customType: msg.customType,
|
||||||
details: result.message.details,
|
content: msg.content,
|
||||||
timestamp: Date.now(),
|
display: msg.display,
|
||||||
});
|
details: msg.details,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Apply hook systemPromptAppend on top of base prompt
|
||||||
|
if (result?.systemPromptAppend) {
|
||||||
|
this.agent.setSystemPrompt(`${this._baseSystemPrompt}\n\n${result.systemPromptAppend}`);
|
||||||
|
} else {
|
||||||
|
// Ensure we're using the base prompt (in case previous turn had appends)
|
||||||
|
this.agent.setSystemPrompt(this._baseSystemPrompt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,12 @@ import type {
|
||||||
ToolResultEventResult,
|
ToolResultEventResult,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
|
/** Combined result from all before_agent_start handlers (internal) */
|
||||||
|
interface BeforeAgentStartCombinedResult {
|
||||||
|
messages?: NonNullable<BeforeAgentStartEventResult["message"]>[];
|
||||||
|
systemPromptAppend?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener for hook errors.
|
* Listener for hook errors.
|
||||||
*/
|
*/
|
||||||
|
|
@ -485,14 +491,15 @@ export class HookRunner {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit before_agent_start event to all hooks.
|
* Emit before_agent_start event to all hooks.
|
||||||
* Returns the first message to inject (if any handler returns one).
|
* Returns combined result: all messages and all systemPromptAppend strings concatenated.
|
||||||
*/
|
*/
|
||||||
async emitBeforeAgentStart(
|
async emitBeforeAgentStart(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
images?: ImageContent[],
|
images?: ImageContent[],
|
||||||
): Promise<BeforeAgentStartEventResult | undefined> {
|
): Promise<BeforeAgentStartCombinedResult | undefined> {
|
||||||
const ctx = this.createContext();
|
const ctx = this.createContext();
|
||||||
let result: BeforeAgentStartEventResult | undefined;
|
const messages: NonNullable<BeforeAgentStartEventResult["message"]>[] = [];
|
||||||
|
const systemPromptAppends: string[] = [];
|
||||||
|
|
||||||
for (const hook of this.hooks) {
|
for (const hook of this.hooks) {
|
||||||
const handlers = hook.handlers.get("before_agent_start");
|
const handlers = hook.handlers.get("before_agent_start");
|
||||||
|
|
@ -503,9 +510,16 @@ export class HookRunner {
|
||||||
const event: BeforeAgentStartEvent = { type: "before_agent_start", prompt, images };
|
const event: BeforeAgentStartEvent = { type: "before_agent_start", prompt, images };
|
||||||
const handlerResult = await handler(event, ctx);
|
const handlerResult = await handler(event, ctx);
|
||||||
|
|
||||||
// Take the first message returned
|
if (handlerResult) {
|
||||||
if (handlerResult && (handlerResult as BeforeAgentStartEventResult).message && !result) {
|
const result = handlerResult as BeforeAgentStartEventResult;
|
||||||
result = handlerResult as BeforeAgentStartEventResult;
|
// Collect all messages
|
||||||
|
if (result.message) {
|
||||||
|
messages.push(result.message);
|
||||||
|
}
|
||||||
|
// Collect all systemPromptAppend strings
|
||||||
|
if (result.systemPromptAppend) {
|
||||||
|
systemPromptAppends.push(result.systemPromptAppend);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
|
@ -518,6 +532,14 @@ export class HookRunner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
// Return combined result
|
||||||
|
if (messages.length > 0 || systemPromptAppends.length > 0) {
|
||||||
|
return {
|
||||||
|
messages: messages.length > 0 ? messages : undefined,
|
||||||
|
systemPromptAppend: systemPromptAppends.length > 0 ? systemPromptAppends.join("\n\n") : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -608,6 +608,8 @@ export interface ToolResultEventResult {
|
||||||
export interface BeforeAgentStartEventResult {
|
export interface BeforeAgentStartEventResult {
|
||||||
/** Message to inject into context (persisted to session, visible in TUI) */
|
/** Message to inject into context (persisted to session, visible in TUI) */
|
||||||
message?: Pick<HookMessage, "customType" | "content" | "display" | "details">;
|
message?: Pick<HookMessage, "customType" | "content" | "display" | "details">;
|
||||||
|
/** Text to append to the system prompt for this agent run */
|
||||||
|
systemPromptAppend?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return type for session_before_switch handlers */
|
/** Return type for session_before_switch handlers */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue