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
|
||||||
|
|
||||||
`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
|
## Install
|
||||||
|
|
||||||
@@ -26,6 +26,14 @@ pi install https://gitea.rwiesner.com/pi/pi-subagents
|
|||||||
- default: background child process runner
|
- default: background child process runner
|
||||||
- optional tmux runner: set `{"runner":"tmux"}` in `.pi/subagents.json` or `~/.pi/agent/subagents.json`
|
- 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
|
## Requirements
|
||||||
|
|
||||||
- default process runner: no tmux requirement
|
- 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:
|
Use the `subagent` tool in chain mode:
|
||||||
|
|
||||||
1. `worker` to implement: $@
|
1. Run a generic subagent to implement: $@
|
||||||
2. `reviewer` to review `{previous}` and identify issues
|
2. Run a generic subagent to review `{previous}` and identify issues
|
||||||
3. `worker` to revise the implementation using `{previous}`
|
3. Run a generic subagent to revise the implementation using `{previous}`
|
||||||
|
|
||||||
User request: $@
|
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: $@
|
1. Run a generic subagent to inspect the codebase for: $@
|
||||||
2. Run `planner` in chain mode, using `{previous}` from the scout output to produce a concrete implementation plan
|
2. Run a generic subagent to turn `{previous}` into a concrete implementation plan for: $@
|
||||||
3. Run `worker` in chain mode, using `{previous}` from the planner output to implement the approved plan
|
3. Run a generic subagent to implement the approved plan using `{previous}`
|
||||||
|
|
||||||
User request: $@
|
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:
|
Use the `subagent` tool in chain mode:
|
||||||
|
|
||||||
1. `scout` to inspect the codebase for: $@
|
1. Run a generic subagent to inspect the codebase for: $@
|
||||||
2. `planner` to turn `{previous}` into an implementation plan
|
2. Run a generic subagent to turn `{previous}` into a concrete implementation plan for: $@
|
||||||
|
|
||||||
User request: $@
|
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 appendFile(eventsPath, `${JSON.stringify({ type: "tool_call", toolName: "read", args: { path: "a.ts" } })}\n`, "utf8");
|
||||||
await writeFile(
|
await writeFile(
|
||||||
resultPath,
|
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",
|
"utf8",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ test("README documents local install, git install, and tmux PATH requirement", (
|
|||||||
const readme = readFileSync(resolve(packageRoot, "README.md"), "utf8");
|
const readme = readFileSync(resolve(packageRoot, "README.md"), "utf8");
|
||||||
assert.match(readme, /pi install \/absolute\/path\/to\/subagents/);
|
assert.match(readme, /pi install \/absolute\/path\/to\/subagents/);
|
||||||
assert.match(readme, /pi install https:\/\/gitea\.rwiesner\.com\/pi\/pi-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);
|
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");
|
const content = readFileSync(join(packageRoot, "prompts", name), "utf8");
|
||||||
assert.match(content, /^---\ndescription:/m);
|
assert.match(content, /^---\ndescription:/m);
|
||||||
assert.match(content, /subagent/);
|
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) {
|
function makeResult(finalText: string) {
|
||||||
return {
|
return {
|
||||||
runId: "run-1",
|
runId: "run-1",
|
||||||
agent: "scout",
|
|
||||||
agentSource: "builtin" as const,
|
|
||||||
task: "inspect auth",
|
task: "inspect auth",
|
||||||
exitCode: 0,
|
exitCode: 0,
|
||||||
finalText,
|
finalText,
|
||||||
|
|||||||
Reference in New Issue
Block a user