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`.
- 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/<cwd-hash>/<agent>_<sessionId>.jsonl`.
- 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({
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) {

View File

@@ -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;

View File

@@ -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<Record<string, unknown>>(content);