From 6e87692f148aa41f4d9178978b0fab1af9c65e6f Mon Sep 17 00:00:00 2001 From: NovusEdge Date: Sun, 21 Jun 2026 23:56:47 +0300 Subject: [PATCH 1/2] feat: emit subagents:session_ready event with session object Add a new lifecycle event that fires when an agent's session is created and ready for use. This enables extensions that need to attach state to subagent sessions (memory systems, context managers, telemetry) to do so cleanly without relying on internal APIs. Event payload: - id: agent ID - type: subagent type (Explore, Plan, etc.) - session: the AgentSession object - record: the full AgentRecord for metadata Use case: Memory/context systems like Veil that fork parent context for subagents and merge findings back on completion. --- README.md | 3 ++- src/agent-manager.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ad0a1d5b..15320e56 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`, `session_ready`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity. The `session_ready` event exposes the full `AgentSession` object for extensions that need to attach state (e.g., memory systems, context managers) - **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,7 @@ 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: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..b2cfb696 100644 --- a/src/agent-manager.ts +++ b/src/agent-manager.ts @@ -279,6 +279,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 }) => { From 7bbe194a31ff4adfc20e6aae95742530d44347f5 Mon Sep 17 00:00:00 2001 From: NovusEdge Date: Mon, 22 Jun 2026 00:12:48 +0300 Subject: [PATCH 2/2] feat: add tools_resolve event for tool injection Allows extensions to inject additional tools into subagent sessions before they are created. Two mechanisms: 1. SpawnOptions.onToolsResolve callback - per-spawn tool injection 2. subagents:tools_resolve event - global hook for all spawns The event payload includes a mutable `tools` array that extensions can modify to add their own tools (e.g., memory/context tools). Use case: Veil injects veil_recall, veil_remember, etc. into subagents so they can use the parent's context system. Event fires after built-in tool filtering but before session creation. --- README.md | 3 ++- src/agent-manager.ts | 3 +++ src/agent-runner.ts | 18 +++++++++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 15320e56..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`, `session_ready`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity. The `session_ready` event exposes the full `AgentSession` object for extensions that need to attach state (e.g., memory systems, context managers) +- **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,7 @@ 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` | diff --git a/src/agent-manager.ts b/src/agent-manager.ts index b2cfb696..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); 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?.();