@johnnywu/pi-subagents 1.0.0 → 1.1.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/CHANGELOG.md +14 -0
- package/README.md +109 -1
- package/extensions/env-utils.ts +26 -0
- package/extensions/subagent-render.ts +2 -21
- package/extensions/subagent-tool.ts +10 -48
- package/extensions/tool-args.ts +21 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.1.0](https://github.com/jwu/pi-subagents/compare/v1.0.1...v1.1.0) (2026-05-31)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **subagent:** list available agents in system prompt Guidelines ([317ed58](https://github.com/jwu/pi-subagents/commit/317ed5845379d3edd535bc03dab8460b29a82164))
|
|
7
|
+
|
|
8
|
+
## [1.0.1](https://github.com/jwu/pi-subagents/compare/v1.0.0...v1.0.1) (2026-05-31)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* add spacing before subagent result status ([21932f5](https://github.com/jwu/pi-subagents/commit/21932f5cc7a59f504c60f36a6375f322c46acf8c))
|
|
14
|
+
|
|
1
15
|
# 1.0.0 (2026-05-30)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -2,12 +2,120 @@
|
|
|
2
2
|
|
|
3
3
|
Sub-agents extension for [pi](https://github.com/badlogic/pi-mono) coding agent.
|
|
4
4
|
|
|
5
|
+
Delegates tasks to isolated pi child processes — each running with its own model, system prompt, and tool set. Sub-agents inherit zero conversation context; all necessary context must be provided in the task description.
|
|
6
|
+
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
10
|
pi install npm:@johnnywu/pi-subagents
|
|
9
11
|
```
|
|
10
12
|
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
### 1. Define an agent
|
|
16
|
+
|
|
17
|
+
Create a `.pi/agents/code-reviewer.md` file in your project:
|
|
18
|
+
|
|
19
|
+
```markdown
|
|
20
|
+
---
|
|
21
|
+
name: code-reviewer
|
|
22
|
+
description: Reviews code changes for correctness and style
|
|
23
|
+
tools: read, grep, find, ls, bash
|
|
24
|
+
model: anthropic/claude-sonnet-4-6
|
|
25
|
+
thinking: low
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
You are a code reviewer. When given a diff or file list, read the relevant
|
|
29
|
+
files and provide a concise review covering:
|
|
30
|
+
|
|
31
|
+
- Logic errors and edge cases
|
|
32
|
+
- Style and consistency issues
|
|
33
|
+
- Performance concerns
|
|
34
|
+
- Test coverage gaps
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Use it
|
|
38
|
+
|
|
39
|
+
The `subagent` tool is automatically registered. In a pi session:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Review the changes in src/auth.ts using the code-reviewer agent
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or instruct pi to delegate:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Run the code-reviewer agent on the last three commits
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Agent configuration
|
|
52
|
+
|
|
53
|
+
Agents are Markdown files with YAML frontmatter.
|
|
54
|
+
|
|
55
|
+
| Field | Required | Default | Description |
|
|
56
|
+
|-------|----------|---------|-------------|
|
|
57
|
+
| `name` | **yes** | — | Unique agent identifier |
|
|
58
|
+
| `description` | no | — | Human-readable summary |
|
|
59
|
+
| `tools` | no | _none_ | Comma-separated tool whitelist (`read, write, bash, grep`, etc.) |
|
|
60
|
+
| `model` | no | parent's model | Provider/model-id (`anthropic/claude-sonnet-4-6`) |
|
|
61
|
+
| `thinking` | no | `off` | Reasoning level: `off`, `low`, `medium`, `high` |
|
|
62
|
+
| `systemPrompt` | no | `replace` | How the body is applied: `replace` (default system prompt) or `append` |
|
|
63
|
+
| `allowedAgents` | no | _all_ | Comma-separated list of sub-agents this agent may spawn |
|
|
64
|
+
| `maxDepth` | no | `10` | Maximum recursion depth (`0` = no sub-agents, `1` = one level, etc.) |
|
|
65
|
+
|
|
66
|
+
The Markdown body after the frontmatter is the agent's system prompt.
|
|
67
|
+
|
|
68
|
+
### Example with all fields
|
|
69
|
+
|
|
70
|
+
```markdown
|
|
71
|
+
---
|
|
72
|
+
name: orchestrator
|
|
73
|
+
description: High-level planner that delegates to specialists
|
|
74
|
+
tools: subagent, read, grep, find
|
|
75
|
+
model: anthropic/claude-sonnet-4-6
|
|
76
|
+
thinking: high
|
|
77
|
+
systemPrompt: replace
|
|
78
|
+
allowedAgents: code-reviewer, refactor, test-writer
|
|
79
|
+
maxDepth: 2
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
You are an orchestrator. Break complex tasks into sub-tasks and delegate
|
|
83
|
+
them to specialist agents. Combine their results and report a summary.
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Agent discovery
|
|
87
|
+
|
|
88
|
+
Agents are discovered from two locations (project overrides global):
|
|
89
|
+
|
|
90
|
+
| Scope | Path |
|
|
91
|
+
|-------|------|
|
|
92
|
+
| Global | `~/.pi/agent/agents/*.md` |
|
|
93
|
+
| Project | `.pi/agents/*.md` |
|
|
94
|
+
|
|
95
|
+
Only `.md` files are scanned. Files are parsed at extension load time; parse errors produce warnings but don't block other agents.
|
|
96
|
+
|
|
97
|
+
## Recursion control
|
|
98
|
+
|
|
99
|
+
Sub-agents can spawn their own sub-agents (if the `subagent` tool is in their whitelist). Two mechanisms prevent unbounded recursion:
|
|
100
|
+
|
|
101
|
+
**`maxDepth`** — Hard limit counting from the originating agent. `maxDepth: 0` means the agent cannot spawn sub-agents. `maxDepth: 1` allows one level, etc. Defaults to `10` when the agent has `subagent` in tools.
|
|
102
|
+
|
|
103
|
+
**`allowedAgents`** — Whitelist enforced by the parent before spawning. A child process never sees agent names outside its parent's whitelist.
|
|
104
|
+
|
|
105
|
+
These are passed via environment variables (`PI_SUBAGENT_DEPTH`, `PI_SUBAGENT_MAX_DEPTH`, `PI_SUBAGENT_ALLOWED`).
|
|
106
|
+
|
|
107
|
+
## Session storage
|
|
108
|
+
|
|
109
|
+
Sub-agent sessions are saved as `.jsonl` files for post-hoc debugging:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
~/.pi/agent/sessions/--{safe-project-path}--/subagents/
|
|
113
|
+
├── 2026-05-31T03-47-16-502Z_019e7c24-a395-707a-a262-ec5b1664ffa7.jsonl
|
|
114
|
+
└── ...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Each file contains one JSON object per line — session headers, messages, tool calls, and usage data. Parent pi sessions live in the same project directory (no `subagents/` subdirectory).
|
|
118
|
+
|
|
11
119
|
## Development
|
|
12
120
|
|
|
13
121
|
```bash
|
|
@@ -23,7 +131,7 @@ bun run typecheck
|
|
|
23
131
|
# Format
|
|
24
132
|
bun run format
|
|
25
133
|
|
|
26
|
-
# Release (
|
|
134
|
+
# Release (requires GH_TOKEN and NPM_TOKEN)
|
|
27
135
|
bun run release
|
|
28
136
|
```
|
|
29
137
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type RecursionEnv = Partial<
|
|
2
|
+
Record<'PI_SUBAGENT_ALLOWED' | 'PI_SUBAGENT_DEPTH' | 'PI_SUBAGENT_MAX_DEPTH', string>
|
|
3
|
+
>;
|
|
4
|
+
|
|
5
|
+
export function parseEnvNumber(value: string | undefined): number | undefined {
|
|
6
|
+
if (value === undefined) return undefined;
|
|
7
|
+
const parsed = Number(value);
|
|
8
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function allowedAgentNames(env: RecursionEnv): Set<string> | undefined {
|
|
12
|
+
const raw = env?.PI_SUBAGENT_ALLOWED;
|
|
13
|
+
if (!raw) return undefined;
|
|
14
|
+
return new Set(
|
|
15
|
+
raw
|
|
16
|
+
.split(',')
|
|
17
|
+
.map((name) => name.trim())
|
|
18
|
+
.filter(Boolean),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isPastMaxDepth(env: RecursionEnv): boolean {
|
|
23
|
+
const depth = parseEnvNumber(env?.PI_SUBAGENT_DEPTH);
|
|
24
|
+
const maxDepth = parseEnvNumber(env?.PI_SUBAGENT_MAX_DEPTH);
|
|
25
|
+
return depth !== undefined && maxDepth !== undefined && depth > maxDepth;
|
|
26
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { homedir } from 'node:os';
|
|
2
1
|
import type { AgentProgress } from './subagent-executor.ts';
|
|
2
|
+
import { numberArg, preview, shortenPath, stringArg } from './tool-args.ts';
|
|
3
3
|
|
|
4
4
|
export interface SubagentCallArgs {
|
|
5
5
|
agent?: string;
|
|
@@ -41,10 +41,6 @@ export function contextUsageSeverity(usage: {
|
|
|
41
41
|
return 'dim';
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function preview(text: string, length: number): string {
|
|
45
|
-
return text.length > length ? `${text.slice(0, length)}...` : text;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
44
|
function elapsedSeconds(ms: number): number {
|
|
49
45
|
return Math.round(ms / 1000);
|
|
50
46
|
}
|
|
@@ -85,26 +81,10 @@ function indent(text: string, spaces: number): string {
|
|
|
85
81
|
.join('\n');
|
|
86
82
|
}
|
|
87
83
|
|
|
88
|
-
function shortenPath(value: unknown): string | undefined {
|
|
89
|
-
if (typeof value !== 'string') return undefined;
|
|
90
|
-
const home = homedir();
|
|
91
|
-
return value.startsWith(`${home}/`) ? `~/${value.slice(home.length + 1)}` : value;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
84
|
function quote(value: string | undefined): string {
|
|
95
85
|
return value ? JSON.stringify(value) : '';
|
|
96
86
|
}
|
|
97
87
|
|
|
98
|
-
function stringArg(args: Record<string, unknown>, key: string): string | undefined {
|
|
99
|
-
const value = args[key];
|
|
100
|
-
return typeof value === 'string' ? value : undefined;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function numberArg(args: Record<string, unknown>, key: string): number | undefined {
|
|
104
|
-
const value = args[key];
|
|
105
|
-
return typeof value === 'number' ? value : undefined;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
88
|
function pathArg(args: Record<string, unknown>, fallback?: string): string {
|
|
109
89
|
return shortenPath(args.path ?? args.file_path) ?? fallback ?? '...';
|
|
110
90
|
}
|
|
@@ -221,6 +201,7 @@ export function formatSubagentResultLines(
|
|
|
221
201
|
const toolLines = formatToolLineItems(progress, options);
|
|
222
202
|
const usage = formatUsage(progress);
|
|
223
203
|
const lines: SubagentResultLine[] = [
|
|
204
|
+
{ text: '', kind: 'blank', singleLine: false },
|
|
224
205
|
{ text: statusLine, kind: 'status', singleLine: false },
|
|
225
206
|
...toolLines,
|
|
226
207
|
];
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { homedir } from 'node:os';
|
|
2
1
|
import {
|
|
3
2
|
getMarkdownTheme,
|
|
4
3
|
keyHint,
|
|
@@ -17,6 +16,8 @@ import {
|
|
|
17
16
|
formatUsage,
|
|
18
17
|
type SubagentResultLine,
|
|
19
18
|
} from './subagent-render.ts';
|
|
19
|
+
import { numberArg, preview, shortenPath, stringArg } from './tool-args.ts';
|
|
20
|
+
import { allowedAgentNames, isPastMaxDepth, type RecursionEnv } from './env-utils.ts';
|
|
20
21
|
|
|
21
22
|
const SubagentParams = {
|
|
22
23
|
type: 'object',
|
|
@@ -36,10 +37,6 @@ type SubagentParamsType = {
|
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
type RegisterablePi = Pick<ExtensionAPI, 'registerTool'>;
|
|
39
|
-
type RecursionEnv = Partial<
|
|
40
|
-
Record<'PI_SUBAGENT_ALLOWED' | 'PI_SUBAGENT_DEPTH' | 'PI_SUBAGENT_MAX_DEPTH', string>
|
|
41
|
-
>;
|
|
42
|
-
|
|
43
40
|
export interface RegisterSubagentToolOptions {
|
|
44
41
|
agents: AgentConfig[];
|
|
45
42
|
run?: typeof runSubagent;
|
|
@@ -70,54 +67,11 @@ function toProgressResult(progress: AgentProgress) {
|
|
|
70
67
|
};
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
function parseEnvNumber(value: string | undefined): number | undefined {
|
|
74
|
-
if (value === undefined) return undefined;
|
|
75
|
-
const parsed = Number(value);
|
|
76
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function allowedAgentNames(env: RecursionEnv): Set<string> | undefined {
|
|
80
|
-
const raw = env?.PI_SUBAGENT_ALLOWED;
|
|
81
|
-
if (!raw) return undefined;
|
|
82
|
-
return new Set(
|
|
83
|
-
raw
|
|
84
|
-
.split(',')
|
|
85
|
-
.map((name) => name.trim())
|
|
86
|
-
.filter(Boolean),
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function isPastMaxDepth(env: RecursionEnv): boolean {
|
|
91
|
-
const depth = parseEnvNumber(env?.PI_SUBAGENT_DEPTH);
|
|
92
|
-
const maxDepth = parseEnvNumber(env?.PI_SUBAGENT_MAX_DEPTH);
|
|
93
|
-
return depth !== undefined && maxDepth !== undefined && depth > maxDepth;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
70
|
type CollapsedTheme = {
|
|
97
71
|
fg: (name: ThemeColor, text: string) => string;
|
|
98
72
|
bold: (text: string) => string;
|
|
99
73
|
};
|
|
100
74
|
|
|
101
|
-
function preview(text: string, length: number): string {
|
|
102
|
-
return text.length > length ? `${text.slice(0, length)}...` : text;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function shortenPath(value: unknown): string | undefined {
|
|
106
|
-
if (typeof value !== 'string') return undefined;
|
|
107
|
-
const home = homedir();
|
|
108
|
-
return value.startsWith(`${home}/`) ? `~/${value.slice(home.length + 1)}` : value;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function stringArg(args: Record<string, unknown>, key: string): string | undefined {
|
|
112
|
-
const value = args[key];
|
|
113
|
-
return typeof value === 'string' ? value : undefined;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function numberArg(args: Record<string, unknown>, key: string): number | undefined {
|
|
117
|
-
const value = args[key];
|
|
118
|
-
return typeof value === 'number' ? value : undefined;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
75
|
function styledPathArg(
|
|
122
76
|
args: Record<string, unknown>,
|
|
123
77
|
theme: CollapsedTheme,
|
|
@@ -263,11 +217,19 @@ export function registerSubagentTool(
|
|
|
263
217
|
: options.agents;
|
|
264
218
|
const runner = options.run ?? runSubagent;
|
|
265
219
|
|
|
220
|
+
const agentNames = agents
|
|
221
|
+
.map((a) => a.name)
|
|
222
|
+
.sort()
|
|
223
|
+
.join(', ');
|
|
224
|
+
const promptGuidelines =
|
|
225
|
+
agentNames.length > 0 ? [`Available subagents: ${agentNames}`] : undefined;
|
|
226
|
+
|
|
266
227
|
pi.registerTool({
|
|
267
228
|
name: 'subagent',
|
|
268
229
|
label: 'Subagent',
|
|
269
230
|
description: 'Delegate a task to a named sub-agent running in an isolated pi process.',
|
|
270
231
|
promptSnippet: 'Delegate isolated tasks with subagent({ agent, task, cwd? }).',
|
|
232
|
+
promptGuidelines,
|
|
271
233
|
parameters: SubagentParams,
|
|
272
234
|
|
|
273
235
|
async execute(
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
|
|
3
|
+
export function preview(text: string, length: number): string {
|
|
4
|
+
return text.length > length ? `${text.slice(0, length)}...` : text;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function shortenPath(value: unknown): string | undefined {
|
|
8
|
+
if (typeof value !== 'string') return undefined;
|
|
9
|
+
const home = homedir();
|
|
10
|
+
return value.startsWith(`${home}/`) ? `~/${value.slice(home.length + 1)}` : value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function stringArg(args: Record<string, unknown>, key: string): string | undefined {
|
|
14
|
+
const value = args[key];
|
|
15
|
+
return typeof value === 'string' ? value : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function numberArg(args: Record<string, unknown>, key: string): number | undefined {
|
|
19
|
+
const value = args[key];
|
|
20
|
+
return typeof value === 'number' ? value : undefined;
|
|
21
|
+
}
|