From 84bcff94a0aa41f97faf16b0f2f7876825c39d7e Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Tue, 12 May 2026 16:35:37 -0400 Subject: [PATCH] refactor: load subagents from user config (~/.pi/subagents/) - Discover prompts from ~/.pi/subagents/*.md instead of bundled prompts/ - Only register the subagent tool when at least one subagent exists - Remove directory path from tool description (agents listed dynamically) --- AGENTS.md | 2 ++ index.ts | 13 +++++++++---- src/constants.ts | 3 ++- src/prompts.ts | 10 +++++----- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4263b49..cea4db8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,8 @@ This repo implements the `subagent` pi extension in `index.ts`. - Parent-facing tool: `subagent`. - Internal child-only tool: `subagent_finalize`. - Never register `subagent_finalize` in the parent context; only when `PI_SUBAGENT_CHILD=1`. +- Subagents are loaded from `~/.pi/subagents/*.md` (user config only). +- The `subagent` tool is only registered when at least one subagent exists. - Subagent sessions are sticky and persisted at: `~/.pi/subagent-sessions//_.jsonl`. - Omitting `sessionId` creates a new UUID-backed session. diff --git a/index.ts b/index.ts index dd32039..9be4a95 100644 --- a/index.ts +++ b/index.ts @@ -38,21 +38,26 @@ export default function (pi: ExtensionAPI) { }); } + const prompts = discoverPrompts(); + + // Skip Registration - Only register the tool if there's at least one subagent. + if (prompts.length === 0) return; + pi.registerTool({ name: "subagent", label: "Subagent", description: - "Delegate a task to a prompt-defined subagent from this extension's prompts/ directory. " + - `Available at startup: ${formatPromptList(discoverPrompts())}`, + "Delegate a task to a prompt-defined subagent. " + + `Available at startup: ${formatPromptList(prompts)}`, promptSnippet: - "Delegate tasks to subagents by name. Subagent prompts live in prompts/*.md and define approved_tools/denied_tools.", + "Delegate tasks to subagents by name. Subagent prompts define approved_tools/denied_tools.", parameters: SubagentParams, async execute(_toolCallId, params, signal, onUpdate, ctx) { // Validate Agent const validation = validateAgent( params.agent, - discoverPrompts(), + prompts, pi.getActiveTools(), ); if (!validation.ok) { diff --git a/src/constants.ts b/src/constants.ts index b3170e5..7fc06c9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,10 +1,11 @@ import * as path from "node:path"; import { fileURLToPath } from "node:url"; +import { homedir } from "node:os"; const SRC_DIR = path.dirname(fileURLToPath(import.meta.url)); export const EXTENSION_DIR = path.dirname(SRC_DIR); export const EXTENSION_ENTRY = path.join(EXTENSION_DIR, "index.ts"); -export const PROMPTS_DIR = path.join(EXTENSION_DIR, "prompts"); +export const SUBAGENTS_DIR = path.join(homedir(), ".pi", "subagents"); export const FINALIZE_TOOL_NAME = "subagent_finalize"; export const MAX_FINALIZE_RETRIES = 2; diff --git a/src/prompts.ts b/src/prompts.ts index 5d9cae7..9c82956 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -1,7 +1,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { parseFrontmatter } from "@mariozechner/pi-coding-agent"; -import { FINALIZE_TOOL_NAME, PROMPTS_DIR } from "./constants.ts"; +import { FINALIZE_TOOL_NAME, SUBAGENTS_DIR } from "./constants.ts"; import type { PromptConfig } from "./types.ts"; import { FinalizeStatus } from "./types.ts"; @@ -23,17 +23,17 @@ export function parseToolList(value: unknown): string[] { return []; } -// Discover Prompts - Load prompt markdown files from this extension's prompts dir. +// Discover Prompts - Load prompt markdown files from user config dir. export function discoverPrompts(): PromptConfig[] { - if (!fs.existsSync(PROMPTS_DIR)) return []; + if (!fs.existsSync(SUBAGENTS_DIR)) return []; const prompts: PromptConfig[] = []; - const entries = fs.readdirSync(PROMPTS_DIR, { withFileTypes: true }); + const entries = fs.readdirSync(SUBAGENTS_DIR, { withFileTypes: true }); for (const entry of entries) { if (!entry.name.endsWith(".md")) continue; if (!entry.isFile() && !entry.isSymbolicLink()) continue; - const filePath = path.join(PROMPTS_DIR, entry.name); + const filePath = path.join(SUBAGENTS_DIR, entry.name); const content = fs.readFileSync(filePath, "utf-8"); const { frontmatter, body } = parseFrontmatter>(content);