@chaotic1988/pi-subagent 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/agent.ts +163 -0
- package/docs/user-guide.md +127 -0
- package/index.ts +587 -0
- package/package.json +30 -0
- package/runner.ts +295 -0
- package/skills/authoring-agent-specs/SKILL.md +147 -0
- package/tests/agent.test.ts +74 -0
- package/tests/runner.test.ts +111 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# pi-subagent
|
|
2
|
+
|
|
3
|
+
A [pi](https://github.com/mariozechner/pi) extension for delegating tasks to subagents. Each subagent runs as an in-process SDK session with its own isolated context window — ideal for parallel work, specialist prompts, or keeping noisy subtasks out of your main session.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@chaotic1988/pi-subagent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
**1. Define an agent** (e.g. `~/.pi/agent/agents/reviewer.md`):
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
---
|
|
17
|
+
name: reviewer
|
|
18
|
+
description: Reviews code for quality and correctness
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
You are a thorough code reviewer. Focus on correctness, clarity, and edge cases.
|
|
22
|
+
Respond with a brief summary followed by a bullet list of specific issues.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**2. Use it in a task**
|
|
26
|
+
|
|
27
|
+
Single agent:
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"agent": "reviewer",
|
|
31
|
+
"task": "Review src/auth.ts for security issues"
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Parallel agents:
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"tasks": [
|
|
39
|
+
{ "agent": "reviewer", "task": "Review src/auth.ts" },
|
|
40
|
+
{ "agent": "reviewer", "task": "Review src/api.ts" },
|
|
41
|
+
{ "agent": "reviewer", "task": "Review src/utils.ts" }
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For full documentation — agent configuration, discovery rules, frontmatter reference, and more — see the [User Guide](docs/user-guide.md).
|
package/agent.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { DefaultPackageManager, type ExtensionContext, getAgentDir, parseFrontmatter, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export interface AgentConfig {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
tools?: string[];
|
|
9
|
+
model?: string;
|
|
10
|
+
thinkingLevel?: string;
|
|
11
|
+
/** undefined = no extensions; non-empty = exactly these extensions */
|
|
12
|
+
extensions?: string[];
|
|
13
|
+
/** undefined = no skills; non-empty = exactly these skill names */
|
|
14
|
+
skills?: string[];
|
|
15
|
+
systemPrompt: string;
|
|
16
|
+
source: "user" | "project";
|
|
17
|
+
filePath: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export function parseCommaList(value: unknown): string[] | undefined {
|
|
22
|
+
if (typeof value !== "string") return undefined;
|
|
23
|
+
const parts = value
|
|
24
|
+
.split(",")
|
|
25
|
+
.map((s) => s.trim())
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
return parts.length > 0 ? parts : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function substituteEnvVars(paths: string[] | undefined): string[] | undefined {
|
|
31
|
+
if (!paths) return paths;
|
|
32
|
+
return paths.map((p) => p.replace(/\$\{(\w+)\}/g, (match, name) => process.env[name] ?? match));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
|
36
|
+
const agents: AgentConfig[] = [];
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(dir)) {
|
|
39
|
+
return agents;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let entries: fs.Dirent[];
|
|
43
|
+
try {
|
|
44
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
45
|
+
} catch {
|
|
46
|
+
return agents;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
51
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
52
|
+
|
|
53
|
+
const filePath = path.join(dir, entry.name);
|
|
54
|
+
let content: string;
|
|
55
|
+
try {
|
|
56
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
57
|
+
} catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, unknown>>(content);
|
|
62
|
+
|
|
63
|
+
if (!frontmatter.name || !frontmatter.description) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
agents.push({
|
|
68
|
+
name: frontmatter.name as string,
|
|
69
|
+
description: frontmatter.description as string,
|
|
70
|
+
tools: parseCommaList(frontmatter.tools),
|
|
71
|
+
model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
|
|
72
|
+
thinkingLevel: typeof frontmatter.thinkingLevel === "string" ? frontmatter.thinkingLevel : undefined,
|
|
73
|
+
extensions: substituteEnvVars(parseCommaList(frontmatter.extensions)),
|
|
74
|
+
skills: parseCommaList(frontmatter.skills),
|
|
75
|
+
systemPrompt: body,
|
|
76
|
+
source,
|
|
77
|
+
filePath,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return agents;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isDirectory(p: string): boolean {
|
|
85
|
+
try {
|
|
86
|
+
return fs.statSync(p).isDirectory();
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getPackageAgentsDirs(
|
|
93
|
+
settings: SettingsManager,
|
|
94
|
+
packages: DefaultPackageManager,
|
|
95
|
+
scope: "user" | "project",
|
|
96
|
+
): string[] {
|
|
97
|
+
const pkgList =
|
|
98
|
+
scope === "user"
|
|
99
|
+
? settings.getGlobalSettings().packages ?? []
|
|
100
|
+
: settings.getProjectSettings().packages ?? [];
|
|
101
|
+
const dirs: string[] = [];
|
|
102
|
+
for (const pkg of pkgList) {
|
|
103
|
+
const source = typeof pkg === "string" ? pkg : pkg.source;
|
|
104
|
+
const root = packages.getInstalledPath(source, scope);
|
|
105
|
+
if (root) {
|
|
106
|
+
const agentsDir = path.join(root, "agents");
|
|
107
|
+
if (isDirectory(agentsDir)) dirs.push(agentsDir);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return dirs;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getExtensionAgentsDirs(baseDir: string): string[] {
|
|
114
|
+
const extDir = path.join(baseDir, "extensions");
|
|
115
|
+
if (!isDirectory(extDir)) return [];
|
|
116
|
+
const dirs: string[] = [];
|
|
117
|
+
for (const entry of fs.readdirSync(extDir, { withFileTypes: true })) {
|
|
118
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
119
|
+
const agentsDir = path.join(extDir, entry.name, "agents");
|
|
120
|
+
if (isDirectory(agentsDir)) dirs.push(agentsDir);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return dirs;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function discoverAgents(ctx: ExtensionContext): AgentConfig[] {
|
|
127
|
+
const agentDir = getAgentDir();
|
|
128
|
+
const settings = SettingsManager.create(ctx.cwd, agentDir);
|
|
129
|
+
const packages = new DefaultPackageManager({ cwd: ctx.cwd, agentDir, settingsManager: settings });
|
|
130
|
+
|
|
131
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
132
|
+
|
|
133
|
+
// User: lowest → highest precedence
|
|
134
|
+
for (const dir of [
|
|
135
|
+
...getPackageAgentsDirs(settings, packages, "user"),
|
|
136
|
+
...getExtensionAgentsDirs(agentDir),
|
|
137
|
+
path.join(agentDir, "agents"),
|
|
138
|
+
]) for (const agent of loadAgentsFromDir(dir, "user")) agentMap.set(agent.name, agent);
|
|
139
|
+
|
|
140
|
+
// Project: overwrites user; lowest → highest precedence
|
|
141
|
+
if (ctx.isProjectTrusted()) {
|
|
142
|
+
for (const dir of [
|
|
143
|
+
...getPackageAgentsDirs(settings, packages, "project"),
|
|
144
|
+
...getExtensionAgentsDirs(path.join(ctx.cwd, ".pi")),
|
|
145
|
+
path.join(ctx.cwd, ".pi", "agents"),
|
|
146
|
+
]) for (const agent of loadAgentsFromDir(dir, "project")) agentMap.set(agent.name, agent);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return Array.from(agentMap.values());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function formatAgentList(
|
|
153
|
+
agents: AgentConfig[],
|
|
154
|
+
maxItems: number,
|
|
155
|
+
): { text: string; remaining: number } {
|
|
156
|
+
if (agents.length === 0) return { text: "none", remaining: 0 };
|
|
157
|
+
const listed = agents.slice(0, maxItems);
|
|
158
|
+
const remaining = agents.length - listed.length;
|
|
159
|
+
return {
|
|
160
|
+
text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join("; "),
|
|
161
|
+
remaining,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# pi-subagent User Guide
|
|
2
|
+
|
|
3
|
+
## Concepts
|
|
4
|
+
|
|
5
|
+
pi-subagent lets you delegate tasks to **subagents** — in-process SDK sessions that run with their own isolated context window. Each subagent is a fresh session: it doesn't share your current conversation history, tools state, or context. This makes subagents ideal for parallelising independent work, keeping expensive or noisy subtasks out of your main context, or applying a specialist prompt to a specific problem.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Defining an Agent
|
|
10
|
+
|
|
11
|
+
An agent is a `.md` file with a YAML frontmatter header. The body of the file is appended to the agent's system prompt.
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
---
|
|
15
|
+
name: my-agent
|
|
16
|
+
description: Does a specific thing well
|
|
17
|
+
model: anthropic/claude-sonnet-4.6
|
|
18
|
+
tools: read,bash,edit
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
You are a specialist in X. Keep responses concise.
|
|
22
|
+
Always confirm before making destructive changes.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Frontmatter Fields
|
|
26
|
+
|
|
27
|
+
| Field | Required | Description |
|
|
28
|
+
|-------|----------|-------------|
|
|
29
|
+
| `name` | Yes | Identifier used when invoking the tool |
|
|
30
|
+
| `description` | Yes | Shown in agent listings |
|
|
31
|
+
| `model` | No | `<provider>/<id>` format (e.g. `anthropic/claude-sonnet-4.6`). Resolution failure (malformed, not found, no auth) halts the run with an error. Falls back to the SDK default if absent. |
|
|
32
|
+
| `thinkingLevel` | No | `off \| minimal \| low \| medium \| high \| xhigh`. Clamped to the model's supported levels by the SDK. Falls back to the SDK default if absent. |
|
|
33
|
+
| `tools` | No | Comma-separated list of allowed tools; all tools if absent |
|
|
34
|
+
| `extensions` | No | Omit or empty → none; list of paths → exactly those extensions loaded. Supports `${ENV_VAR}` substitution. |
|
|
35
|
+
| `skills` | No | Omit or empty → none; list of names → exactly those skills loaded |
|
|
36
|
+
|
|
37
|
+
### `extensions` and `skills`
|
|
38
|
+
|
|
39
|
+
Subagents start with **no extensions and no skills** by default. The agent config must list them explicitly.
|
|
40
|
+
|
|
41
|
+
| Frontmatter | Behaviour |
|
|
42
|
+
|-------------|-----------|
|
|
43
|
+
| Field absent or empty | No extensions/skills loaded |
|
|
44
|
+
| `extensions` with paths (`extensions: /path/a, /path/b`) | Exactly those extensions loaded. |
|
|
45
|
+
| `skills` with names (`skills: my-skill, other-skill`) | Exactly those skills loaded. |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Agent Discovery
|
|
50
|
+
|
|
51
|
+
pi-subagent scans several locations at startup. Agents are loaded in two phases: **user** first, then **project**. Within each phase, later entries overwrite earlier ones on name collision. Because the project phase runs second, **project agents win over user agents** with the same name.
|
|
52
|
+
|
|
53
|
+
Project-scoped agents are only loaded when the project is trusted.
|
|
54
|
+
|
|
55
|
+
| Phase | Order (low → high) | Scope |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| User | installed packages → user extensions → `~/.pi/agent/agents` | `"user"` |
|
|
58
|
+
| Project | installed packages → project extensions → `cwd/.pi/agents` | `"project"` |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Env-Var Substitution in Extension Paths
|
|
63
|
+
|
|
64
|
+
`${VAR_NAME}` tokens in `extensions` path lists are substituted from `process.env` at load time. Unknown variables are left as-is.
|
|
65
|
+
|
|
66
|
+
This is useful for extensions that bundle both an agent definition and skills: the extension can register its own install path as an environment variable, and the agent frontmatter can reference it without hardcoding an absolute path.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// In your extension's index.ts, set the variable before agents are loaded:
|
|
70
|
+
process.env.MY_EXT_DIR = __dirname;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
---
|
|
75
|
+
name: my-bundled-agent
|
|
76
|
+
description: Agent that uses extensions from the same package
|
|
77
|
+
extensions: ${MY_EXT_DIR}/extensions/my-ext
|
|
78
|
+
---
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This works regardless of whether the extension is installed globally, per-project, from git, or from npm.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Modes
|
|
86
|
+
|
|
87
|
+
### Single Mode
|
|
88
|
+
|
|
89
|
+
Dispatch one task to one named agent:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"agent": "my-agent",
|
|
94
|
+
"task": "Summarise the changes in the last 10 commits",
|
|
95
|
+
"cwd": "/path/to/repo"
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`cwd` is optional; the subagent inherits the caller's working directory if omitted.
|
|
100
|
+
|
|
101
|
+
### Parallel Mode
|
|
102
|
+
|
|
103
|
+
Dispatch multiple tasks concurrently (up to 8 tasks, max 4 running at once):
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"tasks": [
|
|
108
|
+
{ "agent": "reviewer", "task": "Review src/auth.ts for security issues" },
|
|
109
|
+
{ "agent": "reviewer", "task": "Review src/api.ts for security issues" },
|
|
110
|
+
{ "agent": "docs-writer", "task": "Write JSDoc for src/utils.ts" }
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Each task in the array accepts the same fields as single mode (`agent`, `task`, `cwd`).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Tips
|
|
120
|
+
|
|
121
|
+
- **Keep system prompts focused.** Subagents have isolated context — a tight, specific system prompt produces better results than a general-purpose one.
|
|
122
|
+
|
|
123
|
+
- **Use `skills:` to prevent skill bleed.** Subagents load no skills by default. If you want to keep it that way, omit the `skills` field or leave it empty.
|
|
124
|
+
|
|
125
|
+
- **Debug with session logs.** Every subagent run persists a session under `~/.pi/agent/sessions/`. Open the session in pi to replay the conversation and inspect what the subagent did.
|
|
126
|
+
|
|
127
|
+
- **Name agents clearly.** The calling agent selects subagents by name. A name like `security-reviewer` is easier to select correctly than `reviewer`.
|