@gotgenes/pi-subagents 1.0.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.
Files changed (86) hide show
  1. package/.markdownlint-cli2.yaml +19 -0
  2. package/.prettierignore +5 -0
  3. package/.release-please-manifest.json +3 -0
  4. package/AGENTS.md +85 -0
  5. package/CHANGELOG.md +495 -0
  6. package/LICENSE +21 -0
  7. package/README.md +528 -0
  8. package/dist/agent-manager.d.ts +108 -0
  9. package/dist/agent-manager.js +390 -0
  10. package/dist/agent-runner.d.ts +93 -0
  11. package/dist/agent-runner.js +428 -0
  12. package/dist/agent-types.d.ts +48 -0
  13. package/dist/agent-types.js +136 -0
  14. package/dist/context.d.ts +12 -0
  15. package/dist/context.js +56 -0
  16. package/dist/cross-extension-rpc.d.ts +46 -0
  17. package/dist/cross-extension-rpc.js +54 -0
  18. package/dist/custom-agents.d.ts +14 -0
  19. package/dist/custom-agents.js +127 -0
  20. package/dist/default-agents.d.ts +7 -0
  21. package/dist/default-agents.js +119 -0
  22. package/dist/env.d.ts +6 -0
  23. package/dist/env.js +28 -0
  24. package/dist/group-join.d.ts +32 -0
  25. package/dist/group-join.js +116 -0
  26. package/dist/index.d.ts +13 -0
  27. package/dist/index.js +1731 -0
  28. package/dist/invocation-config.d.ts +22 -0
  29. package/dist/invocation-config.js +15 -0
  30. package/dist/memory.d.ts +49 -0
  31. package/dist/memory.js +151 -0
  32. package/dist/model-resolver.d.ts +19 -0
  33. package/dist/model-resolver.js +62 -0
  34. package/dist/output-file.d.ts +24 -0
  35. package/dist/output-file.js +86 -0
  36. package/dist/prompts.d.ts +29 -0
  37. package/dist/prompts.js +72 -0
  38. package/dist/schedule-store.d.ts +36 -0
  39. package/dist/schedule-store.js +144 -0
  40. package/dist/schedule.d.ts +109 -0
  41. package/dist/schedule.js +338 -0
  42. package/dist/settings.d.ts +66 -0
  43. package/dist/settings.js +130 -0
  44. package/dist/skill-loader.d.ts +24 -0
  45. package/dist/skill-loader.js +93 -0
  46. package/dist/types.d.ts +164 -0
  47. package/dist/types.js +5 -0
  48. package/dist/ui/agent-widget.d.ts +134 -0
  49. package/dist/ui/agent-widget.js +451 -0
  50. package/dist/ui/conversation-viewer.d.ts +35 -0
  51. package/dist/ui/conversation-viewer.js +252 -0
  52. package/dist/ui/schedule-menu.d.ts +16 -0
  53. package/dist/ui/schedule-menu.js +95 -0
  54. package/dist/usage.d.ts +50 -0
  55. package/dist/usage.js +49 -0
  56. package/dist/worktree.d.ts +36 -0
  57. package/dist/worktree.js +139 -0
  58. package/docs/decisions/0001-deferred-patches.md +75 -0
  59. package/package.json +68 -0
  60. package/prek.toml +24 -0
  61. package/release-please-config.json +22 -0
  62. package/src/agent-manager.ts +482 -0
  63. package/src/agent-runner.ts +625 -0
  64. package/src/agent-types.ts +164 -0
  65. package/src/context.ts +58 -0
  66. package/src/cross-extension-rpc.ts +95 -0
  67. package/src/custom-agents.ts +136 -0
  68. package/src/default-agents.ts +123 -0
  69. package/src/env.ts +33 -0
  70. package/src/group-join.ts +141 -0
  71. package/src/index.ts +1894 -0
  72. package/src/invocation-config.ts +40 -0
  73. package/src/memory.ts +165 -0
  74. package/src/model-resolver.ts +81 -0
  75. package/src/output-file.ts +96 -0
  76. package/src/prompts.ts +105 -0
  77. package/src/schedule-store.ts +143 -0
  78. package/src/schedule.ts +365 -0
  79. package/src/settings.ts +186 -0
  80. package/src/skill-loader.ts +102 -0
  81. package/src/types.ts +176 -0
  82. package/src/ui/agent-widget.ts +533 -0
  83. package/src/ui/conversation-viewer.ts +261 -0
  84. package/src/ui/schedule-menu.ts +104 -0
  85. package/src/usage.ts +60 -0
  86. package/src/worktree.ts +162 -0
package/README.md ADDED
@@ -0,0 +1,528 @@
1
+ # @tintinweb/pi-subagents
2
+
3
+ A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-agents** to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Run them in foreground or background, steer them mid-run, resume completed sessions, and define your own custom agent types.
4
+
5
+ > **Status:** Early release.
6
+
7
+ <img width="600" alt="pi-subagents screenshot" src="https://github.com/tintinweb/pi-subagents/raw/master/media/screenshot.png" />
8
+
9
+
10
+ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
11
+
12
+
13
+ ## Features
14
+
15
+ - **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`) — feels native
16
+ - **Parallel background agents** — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 4) and smart group join (consolidated notifications)
17
+ - **Live widget UI** — persistent above-editor widget with animated spinners, live tool activity, token counts, and colored status icons
18
+ - **Conversation viewer** — select any agent in `/agents` to open a live-scrolling overlay of its full conversation (auto-follows new content, scroll up to pause)
19
+ - **Custom agent types** — define agents in `.pi/agents/<name>.md` with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions
20
+ - **Mid-run steering** — inject messages into running agents to redirect their work without restarting
21
+ - **Session resume** — pick up where an agent left off, preserving full conversation context
22
+ - **Graceful turn limits** — agents get a "wrap up" warning before hard abort, producing clean partial results instead of cut-off output
23
+ - **Case-insensitive agent types** — `"explore"`, `"Explore"`, `"EXPLORE"` all work. Unknown types fall back to general-purpose with a note
24
+ - **Fuzzy model selection** — specify models by name (`"haiku"`, `"sonnet"`) instead of full IDs, with automatic filtering to only available/configured models
25
+ - **Context inheritance** — optionally fork the parent conversation into a sub-agent so it knows what's been discussed
26
+ - **Persistent agent memory** — three scopes (project, local, user) with automatic read-only fallback for agents without write tools
27
+ - **Git worktree isolation** — run agents in isolated repo copies; changes auto-committed to branches on completion
28
+ - **Skill preloading** — inject named skills into agent system prompts, discovered from `.pi/skills/`, `.agents/skills/`, and global locations (Pi-standard `<name>/SKILL.md` directory layout supported)
29
+ - **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
30
+ - **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show full output. Group completions render each agent individually
31
+ - **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
32
+ - **Cross-extension RPC** — other pi extensions can spawn and stop subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`). Standardized reply envelopes with protocol versioning. Emits `subagents:ready` on load
33
+ - **Schedule subagents** — pass `schedule` to the `Agent` tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results land via the same `subagent-notification` followUp path as manual background completions; manage via `/agents → Scheduled jobs`
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pi install npm:@tintinweb/pi-subagents
39
+ ```
40
+
41
+ Or load directly for development:
42
+
43
+ ```bash
44
+ pi -e ./src/index.ts
45
+ ```
46
+
47
+ ## Quick Start
48
+
49
+ The parent agent spawns sub-agents using the `Agent` tool:
50
+
51
+ ```
52
+ Agent({
53
+ subagent_type: "Explore",
54
+ prompt: "Find all files that handle authentication",
55
+ description: "Find auth files",
56
+ run_in_background: true,
57
+ })
58
+ ```
59
+
60
+ Foreground agents block until complete and return results inline. Background agents return an ID immediately and notify you on completion.
61
+
62
+ ### Scheduling
63
+
64
+ Add a `schedule` field to register the agent to fire later instead of running now:
65
+
66
+ ```
67
+ Agent({
68
+ subagent_type: "Explore",
69
+ prompt: "Look at recent commits and summarize what changed since last week",
70
+ description: "Weekly commit review",
71
+ schedule: "0 0 9 * * 1", // 9am every Monday (6-field cron)
72
+ })
73
+ ```
74
+
75
+ Schedule formats:
76
+
77
+ - **Cron** — 6-field (`second minute hour day-of-month month day-of-week`), e.g. `"0 0 9 * * 1"` for 9am every Monday, `"0 */15 * * * *"` for every 15 minutes.
78
+ - **Interval** — `"5m"`, `"1h"`, `"30s"`, `"2d"`. Fires repeatedly at that interval.
79
+ - **One-shot relative** — `"+10m"`, `"+2h"`, `"+1d"`. Fires once at that future time.
80
+ - **One-shot absolute** — full ISO timestamp, e.g. `"2026-12-25T09:00:00.000Z"`.
81
+
82
+ When a schedule fires, the spawn runs in background and its completion notification arrives in the conversation through the same `subagent-notification` followUp path as a manually-spawned background agent — your parent agent reasons about the result the same way.
83
+
84
+ Schedules are **session-scoped**: they reset on `/new` and restore on `/resume`. List and cancel via `/agents → Scheduled jobs` (creation is the `Agent` tool's job — there is no parallel manual-create wizard). Storage at `<cwd>/.pi/subagent-schedules/<sessionId>.json` with PID-based file locking for cross-instance safety.
85
+
86
+ **Disable the feature entirely**: `/agents → Settings → Scheduling → disabled` removes `schedule` from the `Agent` tool spec (no LLM-context cost), hides the menu entry, and stops any active scheduler. The schema-level removal takes effect on the next pi session; the runtime kill is immediate. Re-enable from the same menu.
87
+
88
+ Restrictions:
89
+ - `schedule` cannot be combined with `inherit_context` (no parent conversation exists at fire time) or `resume` (schedules create fresh agents).
90
+ - `run_in_background` is forced to `true`.
91
+ - Scheduled fires bypass the `maxConcurrent` queue so a 5-minute interval cannot be deferred behind long-running manual agents.
92
+ - **Headless `pi -p` doesn't wait for scheduled subagents.**
93
+
94
+ ## UI
95
+
96
+ The extension renders a persistent widget above the editor showing all active agents:
97
+
98
+ ```
99
+ ● Agents
100
+ ├─ ⠹ Agent Refactor auth module · ⟳5≤30 · 5 tool uses · 33.8k token (62%) · 12.3s
101
+ │ ⎿ editing 2 files…
102
+ ├─ ⠹ Explore Find auth files · ⟳3 · 3 tool uses · 12.4k token (8%) · 4.1s
103
+ │ ⎿ searching…
104
+ ├─ ⠹ Agent Long-running task · ⟳42 · 38 tool uses · 91.0k token (84% · ↻2) · 2m17s
105
+ │ ⎿ reading…
106
+ └─ 2 queued
107
+ ```
108
+
109
+ The token field is annotated with two optional signals inside parens:
110
+ - **`NN%`** — context-window utilization (color-coded: <70% dim, 70–85% warning, ≥85% error). Omitted when the model has no declared `contextWindow`, or briefly right after compaction.
111
+ - **`↻N`** — number of times the session has compacted, when > 0. Stays dim; the percent's color carries urgency.
112
+
113
+ Individual agent results render Claude Code-style in the conversation:
114
+
115
+ | State | Example |
116
+ |-------|---------|
117
+ | **Running** | `⠹ ⟳3≤30 · 3 tool uses · 12.4k token (8%)` / `⎿ searching, reading 3 files…` |
118
+ | **Completed** | `✓ ⟳8 · 5 tool uses · 33.8k token (62%) · 12.3s` / `⎿ Done` |
119
+ | **Wrapped up** | `✓ ⟳50≤50 · 50 tool uses · 89.1k token (84% · ↻2) · 45.2s` / `⎿ Wrapped up (turn limit)` |
120
+ | **Stopped** | `■ ⟳3 · 3 tool uses · 12.4k token (8%)` / `⎿ Stopped` |
121
+ | **Error** | `✗ ⟳3 · 3 tool uses · 12.4k token (8%)` / `⎿ Error: timeout` |
122
+ | **Aborted** | `✗ ⟳55≤50 · 55 tool uses · 102.3k token (95% · ↻3)` / `⎿ Aborted (max turns exceeded)` |
123
+
124
+ Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.
125
+
126
+ Background agent completion notifications render as styled boxes:
127
+
128
+ ```
129
+ ✓ Find auth files completed
130
+ ⟳3 · 3 tool uses · 12.4k token · 4.1s
131
+ ⎿ Found 5 files related to authentication...
132
+ transcript: .pi/output/agent-abc123.jsonl
133
+ ```
134
+
135
+ Group completions render each agent as a separate block. The LLM receives structured `<task-notification>` XML for parsing, while the user sees the themed visual.
136
+
137
+ ## Default Agent Types
138
+
139
+ | Type | Tools | Model | Prompt Mode | Description |
140
+ |------|-------|-------|-------------|-------------|
141
+ | `general-purpose` | all 7 | inherit | `append` (parent twin) | Inherits the parent's full system prompt — same rules, CLAUDE.md, project conventions |
142
+ | `Explore` | read, bash, grep, find, ls | haiku (falls back to inherit) | `replace` (standalone) | Fast codebase exploration (read-only) |
143
+ | `Plan` | read, bash, grep, find, ls | inherit | `replace` (standalone) | Software architect for implementation planning (read-only) |
144
+
145
+ The `general-purpose` agent is a **parent twin** — it receives the parent's entire system prompt plus a sub-agent context bridge, so it follows the same rules the parent does. Explore and Plan use standalone prompts tailored to their read-only roles.
146
+
147
+ Default agents can be **ejected** (`/agents` → select agent → Eject) to export them as `.md` files for customization, **overridden** by creating a `.md` file with the same name (e.g. `.pi/agents/general-purpose.md`), or **disabled** per-project with `enabled: false` frontmatter.
148
+
149
+ ## Custom Agents
150
+
151
+ Define custom agent types by creating `.md` files. The filename becomes the agent type name. Any name is allowed — using a default agent's name overrides it.
152
+
153
+ Agents are discovered from two locations (higher priority wins):
154
+
155
+ | Priority | Location | Scope |
156
+ |----------|----------|-------|
157
+ | 1 (highest) | `.pi/agents/<name>.md` | Project — per-repo agents |
158
+ | 2 | `$PI_CODING_AGENT_DIR/agents/<name>.md` (default `~/.pi/agent/agents/<name>.md`) | Global — available everywhere |
159
+
160
+ Project-level agents override global ones with the same name, so you can customize a global agent for a specific project. The global location follows the upstream `PI_CODING_AGENT_DIR` env var — set it to relocate all pi-coding-agent state (agents, skills, settings) to a custom directory.
161
+
162
+ ### Example: `.pi/agents/auditor.md`
163
+
164
+ ```markdown
165
+ ---
166
+ description: Security Code Reviewer
167
+ tools: read, grep, find, bash
168
+ model: anthropic/claude-opus-4-6
169
+ thinking: high
170
+ max_turns: 30
171
+ ---
172
+
173
+ You are a security auditor. Review code for vulnerabilities including:
174
+ - Injection flaws (SQL, command, XSS)
175
+ - Authentication and authorization issues
176
+ - Sensitive data exposure
177
+ - Insecure configurations
178
+
179
+ Report findings with file paths, line numbers, severity, and remediation advice.
180
+ ```
181
+
182
+ Then spawn it like any built-in type:
183
+
184
+ ```
185
+ Agent({ subagent_type: "auditor", prompt: "Review the auth module", description: "Security audit" })
186
+ ```
187
+
188
+ ### Frontmatter Fields
189
+
190
+ All fields are optional — sensible defaults for everything.
191
+
192
+ | Field | Default | Description |
193
+ |-------|---------|-------------|
194
+ | `description` | filename | Agent description shown in tool listings |
195
+ | `display_name` | — | Display name for UI (e.g. widget, agent list) |
196
+ | `tools` | all 7 | Comma-separated built-in tools: read, bash, edit, write, grep, find, ls. `none` for no tools |
197
+ | `extensions` | `true` | Inherit MCP/extension tools. `false` to disable |
198
+ | `skills` | `true` | Inherit skills from parent. Can be a comma-separated list of skill names to preload (see [Skill Preloading](#skill-preloading) for discovery locations) |
199
+ | `memory` | — | Persistent agent memory scope: `project`, `local`, or `user`. Auto-detects read-only agents |
200
+ | `disallowed_tools` | — | Comma-separated tools to deny even if extensions provide them |
201
+ | `isolation` | — | Set to `worktree` to run in an isolated git worktree |
202
+ | `model` | inherit parent | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
203
+ | `thinking` | inherit | off, minimal, low, medium, high, xhigh |
204
+ | `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
205
+ | `prompt_mode` | `replace` | `replace`: body is the full system prompt (no AGENTS.md / CLAUDE.md inheritance). `append`: body appended to parent's prompt (agent acts as a "parent twin" — inherits parent's AGENTS.md / CLAUDE.md) |
206
+ | `inherit_context` | `false` | Fork parent conversation into agent |
207
+ | `run_in_background` | `false` | Run in background by default |
208
+ | `isolated` | `false` | No extension/MCP tools, only built-in |
209
+ | `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project) |
210
+
211
+ Frontmatter is authoritative. If an agent file sets `model`, `thinking`, `max_turns`, `inherit_context`, `run_in_background`, `isolated`, or `isolation`, those values are locked for that agent. `Agent` tool parameters only fill fields the agent config leaves unspecified.
212
+
213
+ ## Tools
214
+
215
+ ### `Agent`
216
+
217
+ Launch a sub-agent.
218
+
219
+ | Parameter | Type | Required | Description |
220
+ |-----------|------|----------|-------------|
221
+ | `prompt` | string | yes | The task for the agent |
222
+ | `description` | string | yes | Short 3-5 word summary (shown in UI) |
223
+ | `subagent_type` | string | yes | Agent type (built-in or custom) |
224
+ | `model` | string | no | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
225
+ | `thinking` | string | no | Thinking level: off, minimal, low, medium, high, xhigh |
226
+ | `max_turns` | number | no | Max agentic turns. Omit for unlimited (default) |
227
+ | `run_in_background` | boolean | no | Run without blocking |
228
+ | `resume` | string | no | Agent ID to resume a previous session |
229
+ | `isolated` | boolean | no | No extension/MCP tools |
230
+ | `isolation` | `"worktree"` | no | Run in an isolated git worktree |
231
+ | `inherit_context` | boolean | no | Fork parent conversation into agent |
232
+
233
+ ### `get_subagent_result`
234
+
235
+ Check status and retrieve results from a background agent.
236
+
237
+ | Parameter | Type | Required | Description |
238
+ |-----------|------|----------|-------------|
239
+ | `agent_id` | string | yes | Agent ID to check |
240
+ | `wait` | boolean | no | Wait for completion |
241
+ | `verbose` | boolean | no | Include full conversation log |
242
+
243
+ ### `steer_subagent`
244
+
245
+ Send a steering message to a running agent. The message interrupts after the current tool execution.
246
+
247
+ | Parameter | Type | Required | Description |
248
+ |-----------|------|----------|-------------|
249
+ | `agent_id` | string | yes | Agent ID to steer |
250
+ | `message` | string | yes | Message to inject into agent conversation |
251
+
252
+ ## Commands
253
+
254
+ | Command | Description |
255
+ |---------|-------------|
256
+ | `/agents` | Interactive agent management menu |
257
+
258
+ The `/agents` command opens an interactive menu:
259
+
260
+ ```
261
+ Running agents (2) — 1 running, 1 done ← only shown when agents exist
262
+ Agent types (6) ← unified list: defaults + custom
263
+ Create new agent ← manual wizard or AI-generated
264
+ Settings ← max concurrency, max turns, grace turns, join mode
265
+ ```
266
+
267
+ - **Agent types** — unified list with source indicators: `•` (project), `◦` (global), `✕` (disabled). Select an agent to manage it:
268
+ - **Default agents** (no override): Eject (export as `.md`), Disable
269
+ - **Default agents** (ejected/overridden): Edit, Disable, Reset to default, Delete
270
+ - **Custom agents**: Edit, Disable, Delete
271
+ - **Disabled agents**: Enable, Edit, Delete
272
+ - **Eject** — writes the embedded default config as a `.md` file to project or personal location, so you can customize it
273
+ - **Disable/Enable** — toggle agent availability. Disabled agents stay visible in the list (marked `✕`) and can be re-enabled
274
+ - **Create new agent** — choose project/personal location, then manual wizard (step-by-step prompts for name, tools, model, thinking, system prompt) or AI-generated (describe what the agent should do and a sub-agent writes the `.md` file). Any name is allowed, including default agent names (overrides them)
275
+ - **Settings** — configure max concurrency, default max turns, grace turns, and join mode at runtime
276
+
277
+ ## Graceful Max Turns
278
+
279
+ Instead of hard-aborting at the turn limit, agents get a graceful shutdown:
280
+
281
+ 1. At `max_turns` — steering message: *"Wrap up immediately — provide your final answer now."*
282
+ 2. Up to 5 grace turns to finish cleanly
283
+ 3. Hard abort only after the grace period
284
+
285
+ | Status | Meaning | Icon |
286
+ |--------|---------|------|
287
+ | `completed` | Finished naturally | `✓` green |
288
+ | `steered` | Hit limit, wrapped up in time | `✓` yellow |
289
+ | `aborted` | Grace period exceeded | `✗` red |
290
+ | `stopped` | User-initiated abort | `■` dim |
291
+
292
+ ## Concurrency
293
+
294
+ Background agents are subject to a configurable concurrency limit (default: 4). Excess agents are automatically queued and start as running agents complete. The widget shows queued agents as a collapsed count.
295
+
296
+ Foreground agents bypass the queue — they block the parent anyway.
297
+
298
+ ## Join Strategies
299
+
300
+ When background agents complete, they notify the main agent. The **join mode** controls how these notifications are delivered. It applies only to background agents.
301
+
302
+ | Mode | Behavior |
303
+ |------|----------|
304
+ | `smart` (default) | 2+ background agents spawned in the same turn are auto-grouped into a single consolidated notification. Solo agents notify individually. |
305
+ | `async` | Each agent sends its own notification on completion (original behavior). Best when results need incremental processing. |
306
+ | `group` | Force grouping even when spawning a single agent. Useful when you know more agents will follow. |
307
+
308
+ **Timeout behavior:** When agents are grouped, a 30-second timeout starts after the first agent completes. If not all agents finish in time, a partial notification is sent with completed results and remaining agents continue with a shorter 15-second re-batch window for stragglers.
309
+
310
+ **Configuration:**
311
+ - Configure join mode in `/agents` → Settings → Join mode
312
+
313
+ ## Persistent Settings
314
+
315
+ Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, default join mode) persist across pi restarts. Two files, merged on load:
316
+
317
+ - **Global:** `~/.pi/agent/subagents.json` — your machine-wide defaults. Edit by hand; the `/agents` menu never writes here.
318
+ - **Project:** `<cwd>/.pi/subagents.json` — per-project overrides. Written by `/agents` → Settings.
319
+
320
+ **Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `4`, default max turns unlimited, grace turns `5`, join mode `smart`).
321
+
322
+ **Example — global defaults for a beefy machine:**
323
+
324
+ ```bash
325
+ mkdir -p ~/.pi/agent
326
+ cat > ~/.pi/agent/subagents.json <<'EOF'
327
+ {
328
+ "maxConcurrent": 16,
329
+ "graceTurns": 10
330
+ }
331
+ EOF
332
+ ```
333
+
334
+ Every project now starts with concurrency 16 and grace 10, without ever touching the menu. Individual projects can still override via `/agents` → Settings.
335
+
336
+ **Failure behavior:** missing file is silent; malformed JSON logs a `[pi-subagents] Ignoring malformed settings at …` warning to stderr; invalid/out-of-range field values are dropped per-field; write failures downgrade the `/agents` toast to a warning with `(session only; failed to persist)`.
337
+
338
+ ## Events
339
+
340
+ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions can react:
341
+
342
+ | Event | When | Key fields |
343
+ |-------|------|------------|
344
+ | `subagents:created` | Background agent registered | `id`, `type`, `description`, `isBackground` |
345
+ | `subagents:started` | Agent transitions to running (including queued→running) | `id`, `type`, `description` |
346
+ | `subagents:completed` | Agent finished successfully | `id`, `type`, `durationMs`, `tokens` (lifetime `{ input, output, total }`), `toolUses`, `result` |
347
+ | `subagents:failed` | Agent errored, stopped, or aborted | same as completed + `error`, `status` |
348
+ | `subagents:steered` | Steering message sent | `id`, `message` |
349
+ | `subagents:compacted` | Agent's session successfully compacted | `id`, `type`, `description`, `reason` (`"manual"` / `"threshold"` / `"overflow"`), `tokensBefore`, `compactionCount` |
350
+ | `subagents:scheduled` | Schedule lifecycle change | `{ type: "added" \| "removed" \| "updated" \| "fired" \| "error", … }` (job/agentId/error fields per type) |
351
+ | `subagents:scheduler_ready` | Scheduler bound to session, enabled jobs armed | `sessionId`, `jobCount` |
352
+ | `subagents:ready` | Extension loaded and RPC handlers registered | — |
353
+ | `subagents:settings_loaded` | Persisted settings applied at extension init | `settings` (merged global + project) |
354
+ | `subagents:settings_changed` | `/agents` → Settings mutation was applied | `settings`, `persisted` (`boolean` — `false` on write failure) |
355
+
356
+ `tokens.total` = `input + output + cacheWrite`. `cacheRead` is excluded — each turn's `cacheRead` is the cumulative cached prefix re-read on that one API call, so summing per-message would over-count it. Use `contextUsage.percent` (surfaced as `(NN%)` in the widget) for current context size.
357
+
358
+ ## Cross-Extension RPC
359
+
360
+ Other pi extensions can spawn and stop subagents programmatically via the `pi.events` event bus, without importing this package directly.
361
+
362
+ All RPC replies use a standardized envelope: `{ success: true, data?: T }` on success, `{ success: false, error: string }` on failure.
363
+
364
+ ### Discovery
365
+
366
+ Listen for `subagents:ready` to know when RPC handlers are available:
367
+
368
+ ```typescript
369
+ pi.events.on("subagents:ready", () => {
370
+ // RPC handlers are registered — safe to call ping/spawn/stop
371
+ });
372
+ ```
373
+
374
+ ### Ping
375
+
376
+ Check if the subagents extension is loaded and get the protocol version:
377
+
378
+ ```typescript
379
+ const requestId = crypto.randomUUID();
380
+ const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, (reply) => {
381
+ unsub();
382
+ if (reply.success) console.log("Protocol version:", reply.data.version);
383
+ });
384
+ pi.events.emit("subagents:rpc:ping", { requestId });
385
+ ```
386
+
387
+ ### Spawn
388
+
389
+ Spawn a subagent and receive its ID:
390
+
391
+ ```typescript
392
+ const requestId = crypto.randomUUID();
393
+ const unsub = pi.events.on(`subagents:rpc:spawn:reply:${requestId}`, (reply) => {
394
+ unsub();
395
+ if (!reply.success) {
396
+ console.error("Spawn failed:", reply.error);
397
+ } else {
398
+ console.log("Agent ID:", reply.data.id);
399
+ }
400
+ });
401
+ pi.events.emit("subagents:rpc:spawn", {
402
+ requestId,
403
+ type: "general-purpose",
404
+ prompt: "Do something useful",
405
+ options: { description: "My task", run_in_background: true },
406
+ });
407
+ ```
408
+
409
+ ### Stop
410
+
411
+ Stop a running agent by ID:
412
+
413
+ ```typescript
414
+ const requestId = crypto.randomUUID();
415
+ const unsub = pi.events.on(`subagents:rpc:stop:reply:${requestId}`, (reply) => {
416
+ unsub();
417
+ if (!reply.success) console.error("Stop failed:", reply.error);
418
+ });
419
+ pi.events.emit("subagents:rpc:stop", { requestId, agentId: "agent-id-here" });
420
+ ```
421
+
422
+ Reply channels are scoped per `requestId`, so concurrent requests don't interfere.
423
+
424
+ ## Persistent Agent Memory
425
+
426
+ Agents can have persistent memory across sessions. Set `memory` in frontmatter to enable:
427
+
428
+ ```yaml
429
+ ---
430
+ memory: project # project | local | user
431
+ ---
432
+ ```
433
+
434
+ | Scope | Location | Use case |
435
+ |-------|----------|----------|
436
+ | `project` | `.pi/agent-memory/<name>/` | Shared across the team (committed) |
437
+ | `local` | `.pi/agent-memory-local/<name>/` | Machine-specific (gitignored) |
438
+ | `user` | `~/.pi/agent-memory/<name>/` | Global personal memory |
439
+
440
+ Memory uses a `MEMORY.md` index file and individual memory files with frontmatter. Agents with write tools get full read-write access. **Read-only agents** (no `write`/`edit` tools) automatically get read-only memory — they can consume memories written by other agents but cannot modify them. This prevents unintended tool escalation.
441
+
442
+ The `disallowed_tools` field is respected when determining write capability — an agent with `tools: write` + `disallowed_tools: write` correctly gets read-only memory.
443
+
444
+ ## Worktree Isolation
445
+
446
+ Set `isolation: worktree` to run an agent in a temporary git worktree:
447
+
448
+ ```
449
+ Agent({ subagent_type: "refactor", prompt: "...", isolation: "worktree" })
450
+ ```
451
+
452
+ The agent gets a full, isolated copy of the repository. On completion:
453
+ - **No changes:** worktree is cleaned up automatically
454
+ - **Changes made:** changes are committed to a new branch (`pi-agent-<id>`) and returned in the result
455
+
456
+ If the worktree cannot be created (not a git repo, no commits, or `git worktree add` fails), the `Agent` tool returns a clear error instead of running unisolated — `isolation: "worktree"` is a strict guarantee, not a hint. Initialize git and commit at least once, or omit `isolation`.
457
+
458
+ ## Skill Preloading
459
+
460
+ Skills can be preloaded by name and injected into the agent's system prompt:
461
+
462
+ ```yaml
463
+ ---
464
+ skills: api-conventions, error-handling
465
+ ---
466
+ ```
467
+
468
+ **Discovery roots** (checked in this order, first match wins):
469
+
470
+ | Scope | Path | Source |
471
+ |---|---|---|
472
+ | Project | `<cwd>/.pi/skills/` | Pi-standard |
473
+ | Project | `<cwd>/.agents/skills/` | [Agent Skills spec](https://agentskills.io/integrate-skills) |
474
+ | User | `$PI_CODING_AGENT_DIR/skills/` (default `~/.pi/agent/skills/`) | Pi-standard |
475
+ | User | `~/.agents/skills/` | [Agent Skills spec](https://agentskills.io/integrate-skills) |
476
+ | User | `~/.pi/skills/` | Legacy (pre-Pi) |
477
+
478
+ **Per root, a skill named `foo` resolves to the first of:**
479
+
480
+ - `<root>/foo.md` — flat file at the top level
481
+ - `<root>/foo/SKILL.md` — directory skill (top-level)
482
+ - `<root>/*/.../foo/SKILL.md` — directory skill, found by recursive descent
483
+
484
+ Recursion skips dotfile directories and `node_modules`. A directory that itself contains a `SKILL.md` is treated as a single skill — we don't descend into it. Traversal is byte-order sorted for deterministic resolution across filesystems.
485
+
486
+ **Security:** symlinks are rejected at every layer (root, flat file, skill directory, `SKILL.md` inside a skill directory) — intentional deviation from Pi, which follows symlinks. Skill names with path-traversal characters (`..`, `/`, `\`, spaces, leading dot, >128 chars) are rejected.
487
+
488
+ ## Tool Denylist
489
+
490
+ Block specific tools from an agent even if extensions provide them:
491
+
492
+ ```yaml
493
+ ---
494
+ tools: read, bash, grep, write
495
+ disallowed_tools: write, edit
496
+ ---
497
+ ```
498
+
499
+ This is useful for creating agents that inherit extension tools but should not have write access.
500
+
501
+ ## Architecture
502
+
503
+ ```
504
+ src/
505
+ index.ts # Extension entry: tool/command registration, rendering
506
+ types.ts # Type definitions (AgentConfig, AgentRecord, etc.)
507
+ default-agents.ts # Embedded default agent configs (general-purpose, Explore, Plan)
508
+ agent-types.ts # Unified agent registry (defaults + user), tool name resolution
509
+ agent-runner.ts # Session creation, execution, graceful max_turns, steer/resume
510
+ agent-manager.ts # Agent lifecycle, concurrency queue, completion notifications
511
+ cross-extension-rpc.ts # RPC handlers for cross-extension spawn/ping via pi.events
512
+ group-join.ts # Group join manager: batched completion notifications with timeout
513
+ custom-agents.ts # Load user-defined agents from .pi/agents/*.md
514
+ memory.ts # Persistent agent memory (resolve, read, build prompt blocks)
515
+ skill-loader.ts # Preload skills (Pi-standard + Agent Skills spec layouts)
516
+ output-file.ts # Streaming output file transcripts for agent sessions
517
+ worktree.ts # Git worktree isolation (create, cleanup, prune)
518
+ prompts.ts # Config-driven system prompt builder
519
+ context.ts # Parent conversation context for inherit_context
520
+ env.ts # Environment detection (git, platform)
521
+ ui/
522
+ agent-widget.ts # Persistent widget: spinners, activity, status icons, theming
523
+ conversation-viewer.ts # Live conversation overlay for viewing agent sessions
524
+ ```
525
+
526
+ ## License
527
+
528
+ MIT — [tintinweb](https://github.com/tintinweb)
@@ -0,0 +1,108 @@
1
+ /**
2
+ * agent-manager.ts — Tracks agents, background execution, resume support.
3
+ *
4
+ * Background agents are subject to a configurable concurrency limit (default: 4).
5
+ * Excess agents are queued and auto-started as running agents complete.
6
+ * Foreground agents bypass the queue (they block the parent anyway).
7
+ */
8
+ import type { Model } from "@earendil-works/pi-ai";
9
+ import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import { type ToolActivity } from "./agent-runner.js";
11
+ import type { AgentInvocation, AgentRecord, IsolationMode, SubagentType, ThinkingLevel } from "./types.js";
12
+ export type OnAgentComplete = (record: AgentRecord) => void;
13
+ export type OnAgentStart = (record: AgentRecord) => void;
14
+ export type OnAgentCompact = (record: AgentRecord, info: CompactionInfo) => void;
15
+ export type CompactionInfo = {
16
+ reason: "manual" | "threshold" | "overflow";
17
+ tokensBefore: number;
18
+ };
19
+ interface SpawnOptions {
20
+ description: string;
21
+ model?: Model<any>;
22
+ maxTurns?: number;
23
+ isolated?: boolean;
24
+ inheritContext?: boolean;
25
+ thinkingLevel?: ThinkingLevel;
26
+ isBackground?: boolean;
27
+ /**
28
+ * Skip the maxConcurrent queue check for this spawn — start immediately even
29
+ * if the configured concurrency limit would otherwise queue it. Used by the
30
+ * scheduler so a fired job can't be deferred past its trigger window.
31
+ */
32
+ bypassQueue?: boolean;
33
+ /** Isolation mode — "worktree" creates a temp git worktree for the agent. */
34
+ isolation?: IsolationMode;
35
+ /** Resolved invocation snapshot captured for UI display. */
36
+ invocation?: AgentInvocation;
37
+ /** Parent abort signal — when aborted, the subagent is also stopped. */
38
+ signal?: AbortSignal;
39
+ /** Called on tool start/end with activity info (for streaming progress to UI). */
40
+ onToolActivity?: (activity: ToolActivity) => void;
41
+ /** Called on streaming text deltas from the assistant response. */
42
+ onTextDelta?: (delta: string, fullText: string) => void;
43
+ /** Called when the agent session is created (for accessing session stats). */
44
+ onSessionCreated?: (session: AgentSession) => void;
45
+ /** Called at the end of each agentic turn with the cumulative count. */
46
+ onTurnEnd?: (turnCount: number) => void;
47
+ /** Called once per assistant message_end with that message's usage delta. */
48
+ onAssistantUsage?: (usage: {
49
+ input: number;
50
+ output: number;
51
+ cacheWrite: number;
52
+ }) => void;
53
+ /** Called when the session successfully compacts. */
54
+ onCompaction?: (info: CompactionInfo) => void;
55
+ }
56
+ export declare class AgentManager {
57
+ private agents;
58
+ private cleanupInterval;
59
+ private onComplete?;
60
+ private onStart?;
61
+ private onCompact?;
62
+ private maxConcurrent;
63
+ /** Queue of background agents waiting to start. */
64
+ private queue;
65
+ /** Number of currently running background agents. */
66
+ private runningBackground;
67
+ constructor(onComplete?: OnAgentComplete, maxConcurrent?: number, onStart?: OnAgentStart, onCompact?: OnAgentCompact);
68
+ /** Update the max concurrent background agents limit. */
69
+ setMaxConcurrent(n: number): void;
70
+ getMaxConcurrent(): number;
71
+ /**
72
+ * Spawn an agent and return its ID immediately (for background use).
73
+ * If the concurrency limit is reached, the agent is queued.
74
+ */
75
+ spawn(pi: ExtensionAPI, ctx: ExtensionContext, type: SubagentType, prompt: string, options: SpawnOptions): string;
76
+ /** Actually start an agent (called immediately or from queue drain). */
77
+ private startAgent;
78
+ /** Start queued agents up to the concurrency limit. */
79
+ private drainQueue;
80
+ /**
81
+ * Spawn an agent and wait for completion (foreground use).
82
+ * Foreground agents bypass the concurrency queue.
83
+ */
84
+ spawnAndWait(pi: ExtensionAPI, ctx: ExtensionContext, type: SubagentType, prompt: string, options: Omit<SpawnOptions, "isBackground">): Promise<AgentRecord>;
85
+ /**
86
+ * Resume an existing agent session with a new prompt.
87
+ */
88
+ resume(id: string, prompt: string, signal?: AbortSignal): Promise<AgentRecord | undefined>;
89
+ getRecord(id: string): AgentRecord | undefined;
90
+ listAgents(): AgentRecord[];
91
+ abort(id: string): boolean;
92
+ /** Dispose a record's session and remove it from the map. */
93
+ private removeRecord;
94
+ private cleanup;
95
+ /**
96
+ * Remove all completed/stopped/errored records immediately.
97
+ * Called on session start/switch so tasks from a prior session don't persist.
98
+ */
99
+ clearCompleted(): void;
100
+ /** Whether any agents are still running or queued. */
101
+ hasRunning(): boolean;
102
+ /** Abort all running and queued agents immediately. */
103
+ abortAll(): number;
104
+ /** Wait for all running and queued agents to complete (including queued ones). */
105
+ waitForAll(): Promise<void>;
106
+ dispose(): void;
107
+ }
108
+ export {};