@mystilleef/pi-subagent 0.3.0 → 0.4.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 CHANGED
@@ -1,38 +1,81 @@
1
1
  # Subagent
2
2
 
3
- This `pi` extension provides a `subagent` tool designed for the
4
- [SPAE Framework](https://github.com/mystilleef/spae-framework).
3
+ `pi-subagent` adds isolated subagent orchestration to
4
+ [Pi](https://github.com/earendil-works/pi). It provides a `subagent`
5
+ tool and `/run` command for delegating work to specialized agents in
6
+ separate child Pi processes. Designed especially for the `SPAE`
7
+ framework, but doesn't require it.
8
+
9
+ ## Action
10
+
11
+ Agents in
12
+ [action](https://raw.githubusercontent.com/mystilleef/pi-subagent/main/assets/parallel-agents-demo.mp4).
5
13
 
6
14
  ## Installation
7
15
 
16
+ **Install from `npm`:**
17
+
8
18
  ```sh
9
19
  pi install npm:@mystilleef/pi-subagent
10
20
  ```
11
21
 
22
+ **Try temporarily without installing:**
23
+
24
+ ```sh
25
+ pi -e npm:@mystilleef/pi-subagent
26
+ ```
27
+
12
28
  ## Features
13
29
 
14
- - **Asynchronous:** Agents always run in background.
15
- - **Parallel:** Run more than one agents simultaneously.
16
- - **Simplicity:** No advanced orchestration workflows.
17
- - **Bloat-free:** No pre-installed agents.
30
+ - **Asynchronous:** Agents run in the background.
31
+ - **Parallel:** Run many agents simultaneously.
32
+ - **Isolated:** Each delegated task receives a separate context window.
33
+ - **Simple:** No complex orchestration workflow required.
34
+ - **Bloat-free:** No bundled agents.
18
35
 
19
36
  ## Usage
20
37
 
21
- Invoke an agent with:
38
+ **Run an agent with an optional task:**
22
39
 
23
40
  ```text
24
41
  /run agent [optional task]
25
42
  ```
26
43
 
27
- Stop running agents with:
44
+ **Examples:**
45
+
46
+ ```text
47
+ /run spec implement google login screen
48
+ /run plan
49
+ /run inspect
50
+ /run build
51
+ /run verify
52
+ ```
53
+
54
+ **Use natural language to launch agents in parallel:**
55
+
56
+ ```text
57
+ use the work agent to write a poem about linux; use the commit agent to make
58
+ commits; use the query agent to summarize the project.
59
+ ```
60
+
61
+ **Show active and completed jobs:**
62
+
63
+ ```text
64
+ /jobs
65
+ ```
66
+
67
+ **Cancel running `subagents`:**
28
68
 
29
69
  ```text
30
70
  /cancel-subagent
31
71
  ```
32
72
 
33
- ## Workflow
73
+ ## _SPAE_ Workflow
34
74
 
35
- The [SPAE Framework](https://github.com/mystilleef/spae-framework) emphasizes a structured workflow.
75
+ `pi-subagent` supports the
76
+ [`SPAE` Framework](https://github.com/mystilleef/spae-framework), but
77
+ doesn't require it. `SPAE` provides pre-built agents and skills for a
78
+ structured workflow.
36
79
 
37
80
  | Phase | Agent | Purpose |
38
81
  | ----- | ------------------------- | --------------------------------------------- |
@@ -42,4 +85,119 @@ The [SPAE Framework](https://github.com/mystilleef/spae-framework) emphasizes a
42
85
  | 4 | `/run build` | Carry out tasks from `PLAN.md` |
43
86
  | 5 | `/run verify` | Verify implementation against `SPEC.md` |
44
87
 
45
- Visit the [SPAE Framework](https://github.com/mystilleef/spae-framework) for pre-packaged agents and their associated skills.
88
+ ## Agent definitions
89
+
90
+ This package ships no agents. Define agents as Markdown files with YAML
91
+ `frontmatter` and a Markdown system prompt body.
92
+
93
+ **Discovery locations:**
94
+
95
+ - User-global agents: `~/.pi/agents/*.md`
96
+ - Project-local agents: nearest `.pi/agents/*.md`
97
+
98
+ **Required `frontmatter`:**
99
+
100
+ ```yaml
101
+ name: review
102
+ description: Review code for correctness and maintainability.
103
+ ```
104
+
105
+ **Optional `frontmatter`:**
106
+
107
+ ```yaml
108
+ tools: read, bash, edit
109
+ skills: code-review
110
+ thinking: medium
111
+ ```
112
+
113
+ **Accepted `thinking` values:**
114
+
115
+ - `off`
116
+ - `minimal`
117
+ - `low`
118
+ - `medium`
119
+ - `high`
120
+ - `xhigh`
121
+
122
+ ## Tool
123
+
124
+ The extension also registers a `subagent` tool for model-driven
125
+ delegation.
126
+
127
+ **Inputs:**
128
+
129
+ - `agent`: agent name.
130
+ - `task`: task prompt for the child agent.
131
+ - `agentScope`: optional lookup scope, one of `user`, `project`, or
132
+ `both`.
133
+ - `debug`: optional flag that includes full child messages in result
134
+ details.
135
+
136
+ ## Security
137
+
138
+ `Subagents` launch child `pi --json` processes. Agents, tools, and
139
+ extensions run with user permissions, so treat agent definitions like
140
+ executable automation.
141
+
142
+ **Trust guidance:**
143
+
144
+ - Review project-local agents before running them.
145
+ - Avoid delegating secrets unless the agent and tools need them.
146
+ - Prefer trusted repositories for shared agent definitions.
147
+ - Remember that child agents can call their configured tools.
148
+
149
+ ## Configuration and limits
150
+
151
+ **Environment variables:**
152
+
153
+ - `PI_SUBAGENT_DEPTH`: nested subagent depth guard. Nested calls stop at
154
+ depth `1`.
155
+ - `PI_SUBAGENT_MAX_OUTPUT_BYTES`: max returned output bytes. Default:
156
+ `50000`.
157
+ - `PI_SUBAGENT_MAX_OUTPUT_LINES`: max returned output lines. Default:
158
+ `500`.
159
+
160
+ ## Troubleshooting
161
+
162
+ **Missing agent:**
163
+
164
+ - Confirm the file lives under `~/.pi/agents/` or the nearest
165
+ `.pi/agents/`.
166
+ - Confirm `frontmatter` includes `name` and `description`.
167
+ - Confirm `/run` uses the `name` value, not the filename.
168
+
169
+ **Project-local agent prompt:**
170
+
171
+ - Pi may request confirmation before loading project-local agents when
172
+ UI context exists.
173
+
174
+ **Nested subagent blocked:**
175
+
176
+ - Nested delegation hits the `PI_SUBAGENT_DEPTH` safety limit.
177
+ - Run the child task directly from the parent session instead.
178
+
179
+ **Truncated output:**
180
+
181
+ - Raise `PI_SUBAGENT_MAX_OUTPUT_BYTES` or
182
+ `PI_SUBAGENT_MAX_OUTPUT_LINES`.
183
+ - Ask the child agent for a shorter summary.
184
+
185
+ ## Development
186
+
187
+ **Install dependencies:**
188
+
189
+ ```sh
190
+ bun install
191
+ ```
192
+
193
+ **Run full verification:**
194
+
195
+ ```sh
196
+ bun verify
197
+ ```
198
+
199
+ **Check npm package contents:**
200
+
201
+ ```sh
202
+ bun pack:smoke
203
+ ```
package/package.json CHANGED
@@ -1,14 +1,20 @@
1
1
  {
2
2
  "name": "@mystilleef/pi-subagent",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Pi subagent for the SPAE Framework",
5
5
  "author": "Lateef Alabi-Oki <mystilleef@gmail.com>",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/mystilleef/pi-subagent.git"
9
+ "url": "git+https://github.com/mystilleef/pi-subagent.git"
10
10
  },
11
+ "bugs": {
12
+ "url": "https://github.com/mystilleef/pi-subagent/issues"
13
+ },
14
+ "homepage": "https://github.com/mystilleef/pi-subagent#readme",
11
15
  "keywords": [
16
+ "pi-package",
17
+ "pi-extension",
12
18
  "pi",
13
19
  "agent",
14
20
  "subagent",
@@ -23,7 +29,8 @@
23
29
  },
24
30
  "type": "module",
25
31
  "engines": {
26
- "bun": ">=1.3.13"
32
+ "bun": ">=1.3.13",
33
+ "node": ">=18"
27
34
  },
28
35
  "files": [
29
36
  "src",
@@ -33,26 +40,37 @@
33
40
  "pi": {
34
41
  "extensions": [
35
42
  "./src/index.ts"
36
- ]
43
+ ],
44
+ "video": "https://raw.githubusercontent.com/mystilleef/pi-subagent/main/assets/parallel-agents-demo.mp4"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
37
48
  },
38
49
  "scripts": {
39
- "verify": "bun check && bun test",
40
- "coverage": "bun check && bun test --coverage",
41
- "check": "biome check --fix --unsafe . && tsc --noEmit",
50
+ "verify": "bun fix && bun check && bun test",
51
+ "coverage": "bun fix && bun check && bun test --coverage",
52
+ "check": "biome check . && tsc --noEmit",
53
+ "fix": "biome check --write --unsafe .",
54
+ "pack:smoke": "bun scripts/pack-smoke.ts",
42
55
  "migrate": "biome migrate --write",
43
- "release": "sh -c 'npm version \"$1\" && git push --follow-tags && bun publish --access public' --"
56
+ "release": "sh -c 'npm version \"$1\" -m \"chore(release): %s\" && git push --follow-tags' --"
44
57
  },
45
- "dependencies": {
46
- "@earendil-works/pi-agent-core": "^0.74.0",
47
- "@earendil-works/pi-ai": "^0.74.0",
48
- "@earendil-works/pi-coding-agent": "^0.74.0",
49
- "@earendil-works/pi-tui": "^0.74.0",
50
- "typebox": "^1.1.38"
58
+ "peerDependencies": {
59
+ "@earendil-works/pi-agent-core": "*",
60
+ "@earendil-works/pi-ai": "*",
61
+ "@earendil-works/pi-coding-agent": "*",
62
+ "@earendil-works/pi-tui": "*",
63
+ "typebox": "*"
51
64
  },
52
65
  "devDependencies": {
53
66
  "@biomejs/biome": "^2.4.15",
67
+ "@earendil-works/pi-agent-core": "^0.74.1",
68
+ "@earendil-works/pi-ai": "^0.74.1",
69
+ "@earendil-works/pi-coding-agent": "^0.74.1",
70
+ "@earendil-works/pi-tui": "^0.74.1",
54
71
  "@types/bun": "^1.3.14",
55
72
  "@types/node": "^25.8.0",
73
+ "typebox": "^1.1.38",
56
74
  "typescript": "^6.0.3"
57
75
  }
58
76
  }
@@ -15,7 +15,9 @@ export async function cancelSubagentCommandHandler(
15
15
  return;
16
16
  }
17
17
  const options = [
18
- ...jobs.map((job) => `${job.agentName} (${job.requestId})`),
18
+ ...jobs.map(
19
+ (job) => `${job.agentName} ${job.instanceName} (${job.requestId})`,
20
+ ),
19
21
  "All running subagents",
20
22
  ];
21
23
  const selection = await ctx.ui.select("Cancel subagent", options);
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  resetAgentDiscoveryCache,
8
8
  } from "./agent-cache.js";
9
9
  import { cancelSubagentCommandHandler } from "./cancel-command.js";
10
+ import { jobsCommandHandler } from "./jobs-command.js";
10
11
  import { renderSubagentProgress } from "./progress.js";
11
12
  import { renderSubagentResultMessage } from "./run.js";
12
13
  import { runCommandHandler } from "./run-command.js";
@@ -38,6 +39,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI) {
38
39
  "Cancel active /run subagents: /cancel-subagent [requestId|all]",
39
40
  handler: async (args, ctx) => cancelSubagentCommandHandler(ctx, args),
40
41
  });
42
+ pi.registerCommand("jobs", {
43
+ description: "List all /run jobs and their statuses: /jobs",
44
+ handler: async (args, ctx) => jobsCommandHandler(ctx, args),
45
+ });
41
46
  pi.registerTool({
42
47
  name: "subagent",
43
48
  label: "Subagent",
@@ -0,0 +1,164 @@
1
+ const DEFAULT_ADJECTIVES = [
2
+ "able",
3
+ "agile",
4
+ "alert",
5
+ "amber",
6
+ "ample",
7
+ "apt",
8
+ "arctic",
9
+ "avid",
10
+ "bold",
11
+ "brave",
12
+ "bright",
13
+ "brisk",
14
+ "calm",
15
+ "clever",
16
+ "cosmic",
17
+ "crisp",
18
+ "daring",
19
+ "dawn",
20
+ "eager",
21
+ "early",
22
+ "fair",
23
+ "fast",
24
+ "fierce",
25
+ "fine",
26
+ "fresh",
27
+ "gentle",
28
+ "golden",
29
+ "grand",
30
+ "happy",
31
+ "honest",
32
+ "jolly",
33
+ "keen",
34
+ "kind",
35
+ "lively",
36
+ "lucky",
37
+ "merry",
38
+ "mighty",
39
+ "nimble",
40
+ "noble",
41
+ "novel",
42
+ "patient",
43
+ "proud",
44
+ "quick",
45
+ "quiet",
46
+ "rapid",
47
+ "ready",
48
+ "sharp",
49
+ "smart",
50
+ "solid",
51
+ "steady",
52
+ "swift",
53
+ "tidy",
54
+ "vivid",
55
+ "warm",
56
+ "wise",
57
+ ] as const;
58
+
59
+ const DEFAULT_NOUNS = [
60
+ "badger",
61
+ "beacon",
62
+ "bison",
63
+ "brook",
64
+ "cedar",
65
+ "comet",
66
+ "coral",
67
+ "coyote",
68
+ "crane",
69
+ "dolphin",
70
+ "eagle",
71
+ "ember",
72
+ "falcon",
73
+ "finch",
74
+ "forest",
75
+ "fox",
76
+ "gecko",
77
+ "glade",
78
+ "harbor",
79
+ "hawk",
80
+ "heron",
81
+ "island",
82
+ "jaguar",
83
+ "koala",
84
+ "lagoon",
85
+ "lemur",
86
+ "lynx",
87
+ "maple",
88
+ "meadow",
89
+ "otter",
90
+ "panda",
91
+ "panther",
92
+ "pelican",
93
+ "phoenix",
94
+ "puma",
95
+ "raven",
96
+ "reef",
97
+ "river",
98
+ "salmon",
99
+ "sparrow",
100
+ "summit",
101
+ "tiger",
102
+ "valley",
103
+ "violet",
104
+ "walrus",
105
+ "willow",
106
+ "wolf",
107
+ "wren",
108
+ "yak",
109
+ "zephyr",
110
+ ] as const;
111
+
112
+ const usedInstanceNames = new Set<string>();
113
+
114
+ let adjectives: readonly string[] = DEFAULT_ADJECTIVES;
115
+ let nouns: readonly string[] = DEFAULT_NOUNS;
116
+ let randomSource: () => number = Math.random;
117
+
118
+ function normalizeRandomIndex(limit: number): number {
119
+ const value = randomSource();
120
+ if (!Number.isFinite(value)) return 0;
121
+ return Math.min(limit - 1, Math.max(0, Math.floor(value * limit)));
122
+ }
123
+
124
+ function nameAt(index: number): string {
125
+ const adjective = adjectives[Math.floor(index / nouns.length)];
126
+ const noun = nouns[index % nouns.length];
127
+ return `${adjective}-${noun}`;
128
+ }
129
+
130
+ export function generateSubagentInstanceName(): string {
131
+ const capacity = adjectives.length * nouns.length;
132
+ if (usedInstanceNames.size >= capacity) {
133
+ throw new Error(
134
+ "No unused subagent instance names remain for this session.",
135
+ );
136
+ }
137
+ const start = normalizeRandomIndex(capacity);
138
+ for (let offset = 0; offset < capacity; offset += 1) {
139
+ const candidate = nameAt((start + offset) % capacity);
140
+ if (!usedInstanceNames.has(candidate)) {
141
+ usedInstanceNames.add(candidate);
142
+ return candidate;
143
+ }
144
+ }
145
+ throw new Error("No unused subagent instance names remain for this session.");
146
+ }
147
+
148
+ export function resetSubagentInstanceNamesForTest() {
149
+ usedInstanceNames.clear();
150
+ adjectives = DEFAULT_ADJECTIVES;
151
+ nouns = DEFAULT_NOUNS;
152
+ randomSource = Math.random;
153
+ }
154
+
155
+ export function configureSubagentInstanceNamesForTest(options: {
156
+ adjectives?: readonly string[];
157
+ nouns?: readonly string[];
158
+ randomSource?: () => number;
159
+ }) {
160
+ usedInstanceNames.clear();
161
+ adjectives = options.adjectives ?? DEFAULT_ADJECTIVES;
162
+ nouns = options.nouns ?? DEFAULT_NOUNS;
163
+ randomSource = options.randomSource ?? Math.random;
164
+ }
@@ -0,0 +1,41 @@
1
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ import {
4
+ getAllProgressStates,
5
+ type SubagentProgressState,
6
+ } from "./progress-state.js";
7
+ import { listRunJobs } from "./run-registry.js";
8
+ import { renderRunsBoard } from "./ui.js";
9
+
10
+ export async function jobsCommandHandler(
11
+ ctx: ExtensionCommandContext,
12
+ _args: string,
13
+ ): Promise<void> {
14
+ const activeRequestIds = new Set(listRunJobs().map((j) => j.requestId));
15
+ const active: SubagentProgressState[] = [];
16
+ const completed: SubagentProgressState[] = [];
17
+ for (const s of getAllProgressStates()) {
18
+ if (activeRequestIds.has(s.requestId)) active.push(s);
19
+ else if (s.status !== "running") completed.push(s);
20
+ }
21
+ const all = [...active, ...completed];
22
+ if (all.length === 0) {
23
+ ctx.ui.notify("No /run jobs in this session.");
24
+ return;
25
+ }
26
+ const output = await ctx.ui.custom<string>(
27
+ (_tui, theme, _keybindings, done) => ({
28
+ invalidate() {},
29
+ render(width) {
30
+ const tuiLines = renderRunsBoard(all, theme, width).render(width);
31
+ const notifyWidth = Math.max(1, width - 2);
32
+ const notifyLines = renderRunsBoard(all, theme, notifyWidth).render(
33
+ notifyWidth,
34
+ );
35
+ done(notifyLines.join("\n"));
36
+ return tuiLines;
37
+ },
38
+ }),
39
+ );
40
+ ctx.ui.notify(output);
41
+ }
@@ -1,3 +1,4 @@
1
+ import type { ThemeColor } from "@earendil-works/pi-coding-agent";
1
2
  import {
2
3
  isStatusOnlyFailure,
3
4
  isStatusOnlySuccess,
@@ -9,11 +10,35 @@ import {
9
10
  } from "./normalize.js";
10
11
  import type { SubagentDetails } from "./types.js";
11
12
 
13
+ export type ThemeBg = "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
14
+
12
15
  export type ProgressStatus = "running" | "success" | "error" | "cancelled";
13
16
 
17
+ export const STATUS_COLOR: Record<ProgressStatus, ThemeColor> = {
18
+ success: "success",
19
+ error: "error",
20
+ cancelled: "error",
21
+ running: "accent",
22
+ };
23
+
24
+ export const STATUS_ICON: Record<ProgressStatus, string> = {
25
+ success: "✓",
26
+ error: "✗",
27
+ cancelled: "⊘",
28
+ running: "⟳",
29
+ };
30
+
31
+ export const STATUS_BG: Record<ProgressStatus, ThemeBg> = {
32
+ success: "toolSuccessBg",
33
+ error: "toolErrorBg",
34
+ cancelled: "toolErrorBg",
35
+ running: "toolPendingBg",
36
+ };
37
+
14
38
  export interface SubagentProgressState {
15
39
  requestId: string;
16
40
  agent: string;
41
+ instanceName?: string;
17
42
  taskPreview: string;
18
43
  status: ProgressStatus;
19
44
  startTime: number;
@@ -34,10 +59,12 @@ export function createProgressState(
34
59
  requestId: string,
35
60
  agent: string,
36
61
  task: string,
62
+ instanceName = requestId,
37
63
  ): void {
38
64
  store.set(requestId, {
39
65
  requestId,
40
66
  agent,
67
+ instanceName,
41
68
  taskPreview: makeTaskPreview(task),
42
69
  status: "running",
43
70
  startTime: Date.now(),
@@ -50,6 +77,9 @@ export function getProgressState(
50
77
  ): SubagentProgressState | undefined {
51
78
  return store.get(requestId);
52
79
  }
80
+ export function getAllProgressStates(): SubagentProgressState[] {
81
+ return [...store.values()].sort((a, b) => b.startTime - a.startTime);
82
+ }
53
83
 
54
84
  export function patchProgressState(
55
85
  requestId: string,
@@ -231,3 +261,53 @@ export function isToolCallPart(part: unknown): part is {
231
261
  typeof maybe.name === "string"
232
262
  );
233
263
  }
264
+
265
+ /**
266
+ * Format a millisecond duration for compact display.
267
+ * Renders sub-minute durations as decimal seconds (`45.2s`),
268
+ * longer durations as minutes and whole seconds (`2m 15s`).
269
+ */
270
+ export function formatElapsed(ms: number): string {
271
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
272
+ const mins = Math.floor(ms / 60000);
273
+ const secs = Math.floor((ms % 60000) / 1000);
274
+ return `${mins}m ${secs}s`;
275
+ }
276
+
277
+ /**
278
+ * Format a raw token count for compact inline display.
279
+ * Values below 1000 rendered as-is. Larger counts use `k`
280
+ * or `M` suffixes with one decimal place, stripping trailing `.0`.
281
+ */
282
+ export function formatTokenCount(count: number): string {
283
+ if (count < 1000) return String(count);
284
+ const unit = count >= 1_000_000 ? "M" : "k";
285
+ const divisor = count >= 1_000_000 ? 1_000_000 : 1000;
286
+ return `${trimTrailingZero((count / divisor).toFixed(1))}${unit}`;
287
+ }
288
+
289
+ function trimTrailingZero(value: string): string {
290
+ return value.endsWith(".0") ? value.slice(0, -2) : value;
291
+ }
292
+
293
+ export function formatContextPercent(state: SubagentProgressState): string {
294
+ const d = state.contextWindowTokens;
295
+ if (!d || d <= 0 || !Number.isFinite(d)) return "--%";
296
+ const n = state.contextTokens;
297
+ if (!n || n <= 0 || !Number.isFinite(n)) return "0%";
298
+ return `${Math.round((n / d) * 100)}%`;
299
+ }
300
+
301
+ /**
302
+ * Format the one-line statistics header for a subagent progress display.
303
+ * Includes tool count, context window usage, and elapsed time.
304
+ * When the subagent is still running (`durationMs` unset), elapsed is
305
+ * computed live from `startTime`.
306
+ *
307
+ * @returns Single line ending in `\n`, e.g. `"3 tools · 45% ctx · 12.3s\n"`
308
+ */
309
+ export function formatHeaderStats(state: SubagentProgressState): string {
310
+ const elapsedMs = state.durationMs ?? Date.now() - state.startTime;
311
+ const toolLabel = state.toolCount === 1 ? "tool" : "tools";
312
+ return `${state.toolCount} ${toolLabel} · ${formatContextPercent(state)} ctx · ${formatElapsed(elapsedMs)}\n`;
313
+ }