@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 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 (local, requires GH_TOKEN and NPM_TOKEN)
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnnywu/pi-subagents",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Sub-agents extension for pi coding agent.",
5
5
  "homepage": "https://github.com/jwu/pi-subagents#readme",
6
6
  "repository": {