diff --git a/README.md b/README.md index ad0a1d5b..a52e76d5 100644 --- a/README.md +++ b/README.md @@ -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 `/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` @@ -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` | diff --git a/src/agent-manager.ts b/src/agent-manager.ts index fceaa14d..8dcaabf3 100644 --- a/src/agent-manager.ts +++ b/src/agent-manager.ts @@ -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. */ @@ -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); @@ -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 }) => { diff --git a/src/agent-runner.ts b/src/agent-runner.ts index 80e3d66e..c7e49dd5 100644 --- a/src/agent-runner.ts +++ b/src/agent-runner.ts @@ -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; /** @@ -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; @@ -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?.();