refactor: switch pi-subagents to generic subagents
This commit is contained in:
10
README.md
10
README.md
@@ -1,6 +1,6 @@
|
||||
# pi-subagents
|
||||
|
||||
`pi-subagents` is a Pi extension package that runs subagent tasks in separate child sessions and ships the prompts and wrapper code needed to execute those runs.
|
||||
`pi-subagents` is a Pi extension package that runs generic subagent tasks in separate child sessions and ships workflow prompt templates plus the wrapper code needed to execute those runs.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -26,6 +26,14 @@ pi install https://gitea.rwiesner.com/pi/pi-subagents
|
||||
- default: background child process runner
|
||||
- optional tmux runner: set `{"runner":"tmux"}` in `.pi/subagents.json` or `~/.pi/agent/subagents.json`
|
||||
|
||||
## What a subagent run gets
|
||||
|
||||
- delegated task text
|
||||
- selected model
|
||||
- optional working directory
|
||||
|
||||
Child runs are normal Pi sessions. This package does not add built-in role behavior, markdown-discovered subagents, per-agent tool restrictions, or appended role prompts.
|
||||
|
||||
## Requirements
|
||||
|
||||
- default process runner: no tmux requirement
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
description: Implement, review, then revise using subagents
|
||||
description: Implement, review, then revise using generic subagents
|
||||
---
|
||||
Use the `subagent` tool in chain mode:
|
||||
|
||||
1. `worker` to implement: $@
|
||||
2. `reviewer` to review `{previous}` and identify issues
|
||||
3. `worker` to revise the implementation using `{previous}`
|
||||
1. Run a generic subagent to implement: $@
|
||||
2. Run a generic subagent to review `{previous}` and identify issues
|
||||
3. Run a generic subagent to revise the implementation using `{previous}`
|
||||
|
||||
User request: $@
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
description: Scout, plan, and implement using subagents
|
||||
description: Inspect, plan, and implement using generic subagents
|
||||
---
|
||||
Use the `subagent` tool to handle this request in three stages:
|
||||
Use the `subagent` tool in chain mode:
|
||||
|
||||
1. Run `scout` to inspect the codebase for: $@
|
||||
2. Run `planner` in chain mode, using `{previous}` from the scout output to produce a concrete implementation plan
|
||||
3. Run `worker` in chain mode, using `{previous}` from the planner output to implement the approved plan
|
||||
1. Run a generic subagent to inspect the codebase for: $@
|
||||
2. Run a generic subagent to turn `{previous}` into a concrete implementation plan for: $@
|
||||
3. Run a generic subagent to implement the approved plan using `{previous}`
|
||||
|
||||
User request: $@
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
description: Scout the codebase, then produce a plan using subagents
|
||||
description: Inspect the codebase, then produce a plan using generic subagents
|
||||
---
|
||||
Use the `subagent` tool in chain mode:
|
||||
|
||||
1. `scout` to inspect the codebase for: $@
|
||||
2. `planner` to turn `{previous}` into an implementation plan
|
||||
1. Run a generic subagent to inspect the codebase for: $@
|
||||
2. Run a generic subagent to turn `{previous}` into a concrete implementation plan for: $@
|
||||
|
||||
User request: $@
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir, writeFile, mkdtemp } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { BUILTIN_AGENTS } from "./builtin-agents.ts";
|
||||
import { discoverAgents } from "./agents.ts";
|
||||
|
||||
test("discoverAgents returns built-ins and lets user markdown override by name", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "pi-subagents-agents-"));
|
||||
const agentDir = join(root, "agent-home");
|
||||
const userAgentsDir = join(agentDir, "agents");
|
||||
await mkdir(userAgentsDir, { recursive: true });
|
||||
await writeFile(
|
||||
join(userAgentsDir, "scout.md"),
|
||||
`---\nname: scout\ndescription: User scout\nmodel: openai/gpt-5\n---\nUser override prompt`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = discoverAgents(join(root, "repo"), {
|
||||
scope: "user",
|
||||
agentDir,
|
||||
builtins: BUILTIN_AGENTS,
|
||||
});
|
||||
|
||||
const scout = result.agents.find((agent) => agent.name === "scout");
|
||||
assert.equal(scout?.source, "user");
|
||||
assert.equal(scout?.description, "User scout");
|
||||
assert.equal(scout?.model, "openai/gpt-5");
|
||||
});
|
||||
|
||||
test("discoverAgents lets project agents override user agents when scope is both", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "pi-subagents-agents-"));
|
||||
const repo = join(root, "repo");
|
||||
const agentDir = join(root, "agent-home");
|
||||
const userAgentsDir = join(agentDir, "agents");
|
||||
const projectAgentsDir = join(repo, ".pi", "agents");
|
||||
await mkdir(userAgentsDir, { recursive: true });
|
||||
await mkdir(projectAgentsDir, { recursive: true });
|
||||
|
||||
await writeFile(join(userAgentsDir, "worker.md"), `---\nname: worker\ndescription: User worker\n---\nUser worker`, "utf8");
|
||||
await writeFile(join(projectAgentsDir, "worker.md"), `---\nname: worker\ndescription: Project worker\n---\nProject worker`, "utf8");
|
||||
|
||||
const result = discoverAgents(repo, {
|
||||
scope: "both",
|
||||
agentDir,
|
||||
builtins: BUILTIN_AGENTS,
|
||||
});
|
||||
|
||||
const worker = result.agents.find((agent) => agent.name === "worker");
|
||||
assert.equal(worker?.source, "project");
|
||||
assert.equal(worker?.description, "Project worker");
|
||||
assert.equal(result.projectAgentsDir, projectAgentsDir);
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
import { existsSync, readdirSync, readFileSync, statSync, type Dirent } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
||||
import { BUILTIN_AGENTS, type AgentDefinition } from "./builtin-agents.ts";
|
||||
import type { AgentScope } from "./schema.ts";
|
||||
|
||||
export interface AgentDiscoveryOptions {
|
||||
scope?: AgentScope;
|
||||
agentDir?: string;
|
||||
builtins?: AgentDefinition[];
|
||||
}
|
||||
|
||||
export interface AgentDiscoveryResult {
|
||||
agents: AgentDefinition[];
|
||||
projectAgentsDir: string | null;
|
||||
}
|
||||
|
||||
function loadMarkdownAgents(dir: string, source: "user" | "project"): AgentDefinition[] {
|
||||
if (!existsSync(dir)) return [];
|
||||
|
||||
let entries: Dirent[];
|
||||
try {
|
||||
entries = readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const agents: AgentDefinition[] = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.name.endsWith(".md")) continue;
|
||||
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
||||
|
||||
const filePath = join(dir, entry.name);
|
||||
const content = readFileSync(filePath, "utf8");
|
||||
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
||||
if (!frontmatter.name || !frontmatter.description) continue;
|
||||
|
||||
agents.push({
|
||||
name: frontmatter.name,
|
||||
description: frontmatter.description,
|
||||
tools: frontmatter.tools?.split(",").map((tool) => tool.trim()).filter(Boolean),
|
||||
model: frontmatter.model,
|
||||
systemPrompt: body.trim(),
|
||||
source,
|
||||
filePath,
|
||||
});
|
||||
}
|
||||
|
||||
return agents;
|
||||
}
|
||||
|
||||
function findNearestProjectAgentsDir(cwd: string): string | null {
|
||||
let current = cwd;
|
||||
while (true) {
|
||||
const candidate = join(current, ".pi", "agents");
|
||||
try {
|
||||
if (statSync(candidate).isDirectory()) return candidate;
|
||||
} catch {}
|
||||
|
||||
const parent = dirname(current);
|
||||
if (parent === current) return null;
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
export function discoverAgents(cwd: string, options: AgentDiscoveryOptions = {}): AgentDiscoveryResult {
|
||||
const scope = options.scope ?? "user";
|
||||
const builtins = options.builtins ?? BUILTIN_AGENTS;
|
||||
const userAgentDir = join(options.agentDir ?? getAgentDir(), "agents");
|
||||
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
||||
|
||||
const sources = new Map<string, AgentDefinition>();
|
||||
for (const agent of builtins) sources.set(agent.name, agent);
|
||||
|
||||
if (scope !== "project") {
|
||||
for (const agent of loadMarkdownAgents(userAgentDir, "user")) {
|
||||
sources.set(agent.name, agent);
|
||||
}
|
||||
}
|
||||
|
||||
if (scope !== "user" && projectAgentsDir) {
|
||||
for (const agent of loadMarkdownAgents(projectAgentsDir, "project")) {
|
||||
sources.set(agent.name, agent);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
agents: [...sources.values()],
|
||||
projectAgentsDir,
|
||||
};
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
export interface AgentDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
tools?: string[];
|
||||
model?: string;
|
||||
systemPrompt: string;
|
||||
source: "builtin" | "user" | "project";
|
||||
filePath?: string;
|
||||
}
|
||||
|
||||
export const BUILTIN_AGENTS: AgentDefinition[] = [
|
||||
{
|
||||
name: "scout",
|
||||
description: "Fast codebase recon and compressed context gathering",
|
||||
tools: ["read", "grep", "find", "ls", "bash"],
|
||||
model: "claude-haiku-4-5",
|
||||
systemPrompt: "You are a scout. Explore quickly, summarize clearly, and avoid implementation.",
|
||||
source: "builtin",
|
||||
},
|
||||
{
|
||||
name: "planner",
|
||||
description: "Turns exploration into implementation plans",
|
||||
tools: ["read", "grep", "find", "ls"],
|
||||
model: "claude-sonnet-4-5",
|
||||
systemPrompt: "You are a planner. Produce implementation plans, file lists, and risks.",
|
||||
source: "builtin",
|
||||
},
|
||||
{
|
||||
name: "reviewer",
|
||||
description: "Reviews code and identifies correctness and quality issues",
|
||||
tools: ["read", "grep", "find", "ls", "bash"],
|
||||
model: "claude-sonnet-4-5",
|
||||
systemPrompt: "You are a reviewer. Inspect code critically and report concrete issues.",
|
||||
source: "builtin",
|
||||
},
|
||||
{
|
||||
name: "worker",
|
||||
description: "General-purpose implementation agent",
|
||||
model: "claude-sonnet-4-5",
|
||||
systemPrompt: "You are a worker. Execute the delegated task completely and report final results clearly.",
|
||||
source: "builtin",
|
||||
},
|
||||
];
|
||||
@@ -23,7 +23,7 @@ test("monitorRun streams normalized events and resolves when result.json appears
|
||||
await appendFile(eventsPath, `${JSON.stringify({ type: "tool_call", toolName: "read", args: { path: "a.ts" } })}\n`, "utf8");
|
||||
await writeFile(
|
||||
resultPath,
|
||||
JSON.stringify({ runId: "run-1", exitCode: 0, finalText: "done", agent: "scout", task: "inspect auth" }, null, 2),
|
||||
JSON.stringify({ runId: "run-1", exitCode: 0, finalText: "done", task: "inspect auth" }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@ test("README documents local install, git install, and tmux PATH requirement", (
|
||||
const readme = readFileSync(resolve(packageRoot, "README.md"), "utf8");
|
||||
assert.match(readme, /pi install \/absolute\/path\/to\/subagents/);
|
||||
assert.match(readme, /pi install https:\/\/gitea\.rwiesner\.com\/pi\/pi-subagents/);
|
||||
assert.match(readme, /generic subagent/i);
|
||||
assert.doesNotMatch(readme, /specialized built-in roles/i);
|
||||
assert.doesNotMatch(readme, /markdown agent discovery/i);
|
||||
assert.match(readme, /tmux.*PATH/i);
|
||||
});
|
||||
|
||||
|
||||
@@ -16,5 +16,6 @@ test("package.json exposes the extension and workflow prompt templates", () => {
|
||||
const content = readFileSync(join(packageRoot, "prompts", name), "utf8");
|
||||
assert.match(content, /^---\ndescription:/m);
|
||||
assert.match(content, /subagent/);
|
||||
assert.doesNotMatch(content, /\b(?:scout|planner|reviewer|worker)\b/);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,8 +5,6 @@ import { createConfiguredRunSingleTask } from "./runner.ts";
|
||||
function makeResult(finalText: string) {
|
||||
return {
|
||||
runId: "run-1",
|
||||
agent: "scout",
|
||||
agentSource: "builtin" as const,
|
||||
task: "inspect auth",
|
||||
exitCode: 0,
|
||||
finalText,
|
||||
|
||||
Reference in New Issue
Block a user