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)
This commit is contained in:
2026-05-12 16:35:37 -04:00
parent 5c5cdb3aec
commit 84bcff94a0
4 changed files with 18 additions and 10 deletions

View File

@@ -9,6 +9,8 @@ This repo implements the `subagent` pi extension in `index.ts`.
- Parent-facing tool: `subagent`. - Parent-facing tool: `subagent`.
- Internal child-only tool: `subagent_finalize`. - Internal child-only tool: `subagent_finalize`.
- Never register `subagent_finalize` in the parent context; only when `PI_SUBAGENT_CHILD=1`. - 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: - Subagent sessions are sticky and persisted at:
`~/.pi/subagent-sessions/<cwd-hash>/<agent>_<sessionId>.jsonl`. `~/.pi/subagent-sessions/<cwd-hash>/<agent>_<sessionId>.jsonl`.
- Omitting `sessionId` creates a new UUID-backed session. - Omitting `sessionId` creates a new UUID-backed session.

View File

@@ -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({ pi.registerTool({
name: "subagent", name: "subagent",
label: "Subagent", label: "Subagent",
description: description:
"Delegate a task to a prompt-defined subagent from this extension's prompts/ directory. " + "Delegate a task to a prompt-defined subagent. " +
`Available at startup: ${formatPromptList(discoverPrompts())}`, `Available at startup: ${formatPromptList(prompts)}`,
promptSnippet: 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, parameters: SubagentParams,
async execute(_toolCallId, params, signal, onUpdate, ctx) { async execute(_toolCallId, params, signal, onUpdate, ctx) {
// Validate Agent // Validate Agent
const validation = validateAgent( const validation = validateAgent(
params.agent, params.agent,
discoverPrompts(), prompts,
pi.getActiveTools(), pi.getActiveTools(),
); );
if (!validation.ok) { if (!validation.ok) {

View File

@@ -1,10 +1,11 @@
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { homedir } from "node:os";
const SRC_DIR = path.dirname(fileURLToPath(import.meta.url)); const SRC_DIR = path.dirname(fileURLToPath(import.meta.url));
export const EXTENSION_DIR = path.dirname(SRC_DIR); export const EXTENSION_DIR = path.dirname(SRC_DIR);
export const EXTENSION_ENTRY = path.join(EXTENSION_DIR, "index.ts"); 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 FINALIZE_TOOL_NAME = "subagent_finalize";
export const MAX_FINALIZE_RETRIES = 2; export const MAX_FINALIZE_RETRIES = 2;

View File

@@ -1,7 +1,7 @@
import * as fs from "node:fs"; import * as fs from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
import { parseFrontmatter } from "@mariozechner/pi-coding-agent"; 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 type { PromptConfig } from "./types.ts";
import { FinalizeStatus } from "./types.ts"; import { FinalizeStatus } from "./types.ts";
@@ -23,17 +23,17 @@ export function parseToolList(value: unknown): string[] {
return []; 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[] { export function discoverPrompts(): PromptConfig[] {
if (!fs.existsSync(PROMPTS_DIR)) return []; if (!fs.existsSync(SUBAGENTS_DIR)) return [];
const prompts: PromptConfig[] = []; const prompts: PromptConfig[] = [];
const entries = fs.readdirSync(PROMPTS_DIR, { withFileTypes: true }); const entries = fs.readdirSync(SUBAGENTS_DIR, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
if (!entry.name.endsWith(".md")) continue; if (!entry.name.endsWith(".md")) continue;
if (!entry.isFile() && !entry.isSymbolicLink()) 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 content = fs.readFileSync(filePath, "utf-8");
const { frontmatter, body } = const { frontmatter, body } =
parseFrontmatter<Record<string, unknown>>(content); parseFrontmatter<Record<string, unknown>>(content);