Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
- **Skill preloading** — inject named skills into agent system prompts, discovered from `.pi/skills/`, `.agents/skills/`, and global locations (Pi-standard `<name>/SKILL.md` directory layout supported)
- **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
- **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show full output. Group completions render each agent individually
- **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
- **Event bus** — lifecycle events (`subagents:created`, `started`, `tools_resolve`, `session_ready`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity. The `tools_resolve` event allows injecting additional tools before session creation; `session_ready` exposes the `AgentSession` for attaching state (e.g., memory systems)
- **Cross-extension RPC** — other pi extensions can spawn and stop subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`). Standardized reply envelopes with protocol versioning. Emits `subagents:ready` on load
- **Schedule subagents** — pass `schedule` to the `Agent` tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results land via the same `subagent-notification` followUp path as manual background completions; manage via `/agents → Scheduled jobs`
- **Model scope enforcement** — opt-in validation that subagent model choices stay within your pi `enabledModels` allowlist (sourced from `/scoped-models`, with both global and project-local pi settings honored). Caller-supplied out-of-scope → hard error to orchestrator; frontmatter-pinned out-of-scope → warning + runs anyway (frontmatter authoritative). Toggle via `/agents → Settings → Scope models`
Expand Down Expand Up @@ -418,6 +418,8 @@ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions ca
|-------|------|------------|
| `subagents:created` | Background agent registered | `id`, `type`, `description`, `isBackground` |
| `subagents:started` | Agent transitions to running (including queued→running) | `id`, `type`, `description` |
| `subagents:tools_resolve` | Tools resolved, before session creation (mutable) | `type`, `tools` (array, mutable), `agentId` |
| `subagents:session_ready` | Agent session created and ready for use | `id`, `type`, `session` (AgentSession), `record` (AgentRecord) |
| `subagents:completed` | Agent finished successfully (background and foreground) | `id`, `type`, `durationMs`, `tokens` (lifetime `{ input, output, total }`), `toolUses`, `result` |
| `subagents:failed` | Agent errored, stopped, or aborted (background and foreground) | same as completed + `error`, `status` |
| `subagents:steered` | Steering message sent | `id`, `message` |
Expand Down
5 changes: 5 additions & 0 deletions src/agent-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ interface SpawnOptions {
onTextDelta?: (delta: string, fullText: string) => void;
/** Called when the agent session is created (for accessing session stats). */
onSessionCreated?: (session: AgentSession) => void;
/** Called after tools are resolved, before session creation. Allows injecting additional tools. */
onToolsResolve?: (tools: any[], agentType: string) => any[];
/** Called at the end of each agentic turn with the cumulative count. */
onTurnEnd?: (turnCount: number) => void;
/** Called once per assistant message_end with that message's usage delta. */
Expand Down Expand Up @@ -260,6 +262,7 @@ export class AgentManager {
},
onTurnEnd: options.onTurnEnd,
onTextDelta: options.onTextDelta,
onToolsResolve: options.onToolsResolve,
onAssistantUsage: (usage) => {
addUsage(record.lifetimeUsage, usage);
options.onAssistantUsage?.(usage);
Expand All @@ -279,6 +282,8 @@ export class AgentManager {
record.pendingSteers = undefined;
}
options.onSessionCreated?.(session);
// Emit event for extensions that need session access (e.g., memory systems)
pi.events.emit("subagents:session_ready", { id, type, session, record });
},
})
.then(({ responseText, session, aborted, steered }) => {
Expand Down
18 changes: 17 additions & 1 deletion src/agent-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,12 @@ export interface RunOptions {
/** Called on streaming text deltas from the assistant response. */
onTextDelta?: (delta: string, fullText: string) => void;
onSessionCreated?: (session: AgentSession) => void;
/**
* Called after tools are resolved but before session creation.
* Allows injecting additional tools (e.g., memory/context tools).
* Return the modified tools array.
*/
onToolsResolve?: (tools: any[], agentType: string) => any[];
/** Called at the end of each agentic turn with the cumulative count. */
onTurnEnd?: (turnCount: number) => void;
/**
Expand Down Expand Up @@ -552,7 +558,7 @@ export async function runAgent(
// set, so listing the exact final set here means the session is correctly
// scoped from the first instant — no post-construction narrowing required.
const builtinToolNameSet = new Set(toolNames);
const allowedTools = [...toolNames, ...extensionToolNames].filter((t) => {
let allowedTools = [...toolNames, ...extensionToolNames].filter((t) => {
if (EXCLUDED_TOOL_NAMES.includes(t)) return false;
if (disallowedSet?.has(t)) return false;
if (builtinToolNameSet.has(t)) return true;
Expand All @@ -562,6 +568,16 @@ export async function runAgent(
return !noExtensions;
});

// Allow external tool injection (e.g., memory tools)
// Callback takes precedence, then event-based injection
if (options.onToolsResolve) {
allowedTools = options.onToolsResolve(allowedTools, type);
}
// Emit event for extensions to inject tools
const toolsEvent = { type, tools: allowedTools, agentId: options.agentId };
options.pi.events.emit("subagents:tools_resolve", toolsEvent);
allowedTools = toolsEvent.tools;

const settingsManager = SettingsManager.create(configCwd, agentDir);
const configuredSessionDir = resolveConfiguredSessionDir(agentConfig?.sessionDir, effectiveCwd);
const defaultSessionDir = process.env.PI_CODING_AGENT_SESSION_DIR ?? settingsManager.getSessionDir?.();
Expand Down