@jopqior/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.
- package/CHANGELOG.md +2705 -0
- package/LICENSE +21 -0
- package/README.md +503 -0
- package/dist/public.d.ts +331 -0
- package/dist/settings.d.ts +82 -0
- package/docs/architecture/architecture.md +1566 -0
- package/docs/architecture/client-server-opportunities.md +127 -0
- package/docs/architecture/history/phase-1-api-boundary.md +8 -0
- package/docs/architecture/history/phase-10-structural-decomposition.md +141 -0
- package/docs/architecture/history/phase-11-closure-to-class.md +100 -0
- package/docs/architecture/history/phase-12-complexity-test-fixtures.md +55 -0
- package/docs/architecture/history/phase-13-remaining-smells.md +88 -0
- package/docs/architecture/history/phase-14-strip-policy.md +49 -0
- package/docs/architecture/history/phase-15-domain-model-evolution.md +73 -0
- package/docs/architecture/history/phase-16-invert-dependencies.md +144 -0
- package/docs/architecture/history/phase-17-core-consolidation.md +214 -0
- package/docs/architecture/history/phase-18-reconsider-ui.md +166 -0
- package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
- package/docs/architecture/history/phase-2-remove-scheduling.md +9 -0
- package/docs/architecture/history/phase-20-result-delivery.md +245 -0
- package/docs/architecture/history/phase-21-classification-model-boundary.md +107 -0
- package/docs/architecture/history/phase-3-remove-rpc-groupjoin.md +11 -0
- package/docs/architecture/history/phase-4-implement-service.md +8 -0
- package/docs/architecture/history/phase-5-decompose-index.md +42 -0
- package/docs/architecture/history/phase-7-encapsulation.md +173 -0
- package/docs/architecture/history/phase-8-testability.md +103 -0
- package/docs/architecture/history/phase-9-observation-ctx.md +122 -0
- package/docs/comparison-with-upstream.md +77 -0
- package/docs/configuration.md +364 -0
- package/docs/decisions/0001-deferred-patches.md +80 -0
- package/docs/decisions/0002-extensions-on-a-minimal-core.md +125 -0
- package/docs/decisions/0003-publish-bundled-type-declarations.md +71 -0
- package/docs/decisions/0004-reconsider-ui-direction.md +279 -0
- package/docs/decisions/0005-subagent-record-admission-policy.md +106 -0
- package/docs/decisions/0006-inherited-prompt-is-identity-only.md +104 -0
- package/docs/decisions/0007-transcript-viewer-is-not-an-overlay.md +228 -0
- package/docs/decisions/0008-inherited-region-is-shared-parts.md +81 -0
- package/docs/decisions/0009-portable-inheritance-is-provider-scoped.md +116 -0
- package/package.json +91 -0
- package/src/config/agent-types.ts +135 -0
- package/src/config/custom-agents.ts +151 -0
- package/src/config/default-agents.ts +121 -0
- package/src/config/invocation-config.ts +167 -0
- package/src/config/thinking-level.ts +58 -0
- package/src/debug.ts +14 -0
- package/src/handlers/index.ts +3 -0
- package/src/handlers/interrupt.ts +58 -0
- package/src/handlers/lifecycle.ts +71 -0
- package/src/handlers/widget-events.ts +49 -0
- package/src/index.ts +292 -0
- package/src/layered-settings.ts +105 -0
- package/src/lifecycle/child-lifecycle.ts +115 -0
- package/src/lifecycle/child-shutdown.ts +105 -0
- package/src/lifecycle/concurrency-limiter.ts +55 -0
- package/src/lifecycle/create-subagent-session.ts +335 -0
- package/src/lifecycle/parent-snapshot.ts +119 -0
- package/src/lifecycle/run-listeners.ts +37 -0
- package/src/lifecycle/selection-scope.ts +116 -0
- package/src/lifecycle/spawn-selection.ts +259 -0
- package/src/lifecycle/subagent-manager.ts +546 -0
- package/src/lifecycle/subagent-session.ts +347 -0
- package/src/lifecycle/subagent-state.ts +404 -0
- package/src/lifecycle/subagent.ts +885 -0
- package/src/lifecycle/turn-limits.ts +13 -0
- package/src/lifecycle/usage.ts +60 -0
- package/src/lifecycle/workspace-bracket.ts +76 -0
- package/src/lifecycle/workspace.ts +46 -0
- package/src/observation/composite-subagent-observer.ts +74 -0
- package/src/observation/notification.ts +430 -0
- package/src/observation/outcome-delivery.ts +239 -0
- package/src/observation/record-observer.ts +78 -0
- package/src/observation/renderer.ts +161 -0
- package/src/observation/subagent-events-observer.ts +148 -0
- package/src/runtime.ts +137 -0
- package/src/service/service-adapter.ts +201 -0
- package/src/service/service.ts +246 -0
- package/src/session/ask-parent-tool.ts +69 -0
- package/src/session/content-items.ts +53 -0
- package/src/session/context.ts +80 -0
- package/src/session/conversation.ts +49 -0
- package/src/session/env.ts +40 -0
- package/src/session/model-resolver.ts +126 -0
- package/src/session/notify-parent-tool.ts +83 -0
- package/src/session/package-exclusions.ts +75 -0
- package/src/session/prompts.ts +231 -0
- package/src/session/provider-inheritance.ts +56 -0
- package/src/session/selection-catalogue.ts +143 -0
- package/src/session/session-config.ts +202 -0
- package/src/session/session-dir.ts +38 -0
- package/src/settings.ts +447 -0
- package/src/tools/agent-tool.ts +305 -0
- package/src/tools/background-spawner.ts +83 -0
- package/src/tools/foreground-runner.ts +159 -0
- package/src/tools/get-result-renderer.ts +119 -0
- package/src/tools/get-result-report.ts +84 -0
- package/src/tools/get-result-tool.ts +192 -0
- package/src/tools/helpers.ts +118 -0
- package/src/tools/result-renderer.ts +153 -0
- package/src/tools/spawn-config.ts +192 -0
- package/src/tools/steer-tool.ts +109 -0
- package/src/types.ts +143 -0
- package/src/ui/agent-widget.ts +333 -0
- package/src/ui/bounded-lines.ts +45 -0
- package/src/ui/display.ts +180 -0
- package/src/ui/glyphs.ts +62 -0
- package/src/ui/session-navigation.ts +150 -0
- package/src/ui/session-navigator.ts +255 -0
- package/src/ui/subagents-settings.ts +179 -0
- package/src/ui/transcript-content.ts +374 -0
- package/src/ui/widget-renderer.ts +301 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tintinweb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
# @jopqior/pi-subagents
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@jopqior/pi-subagents) [](https://github.com/Jopqior/gotgenes-pi-packages/actions/workflows/ci.yml) [](https://opensource.org/licenses/MIT) [](https://www.typescriptlang.org/) [](https://pnpm.io/) [](https://pi.mariozechner.at/)
|
|
4
|
+
|
|
5
|
+
A [pi](https://pi.dev) extension that gives pi **a focused, in-process sub-agent core** — autonomous agents that run inside the same pi runtime (no spawned subprocesses), plus a typed API and lifecycle events other extensions build on.
|
|
6
|
+
Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level.
|
|
7
|
+
Run them in foreground or background, steer them mid-run, resume completed sessions, and define your own custom agent types.
|
|
8
|
+
|
|
9
|
+
> Originally forked from [`tintinweb/pi-subagents`](https://github.com/tintinweb/pi-subagents) by [@tintinweb](https://github.com/tintinweb), now an independently maintained hard fork.
|
|
10
|
+
> See [Comparison with upstream](./docs/comparison-with-upstream.md) for a feature-by-feature comparison and guidance on which to choose.
|
|
11
|
+
|
|
12
|
+
<img width="600" alt="pi-subagents screenshot" src="https://github.com/gotgenes/pi-subagents/raw/main/media/screenshot.png" />
|
|
13
|
+
|
|
14
|
+
<https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543>
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **In-process & native** — agents run inside the same pi runtime (no spawned subprocesses), sharing tool names, calling conventions, and UI patterns (`subagent`, `get_subagent_result`, `steer_subagent`) — feels native
|
|
19
|
+
- **Parallel background agents** — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 4) and individual completion notifications
|
|
20
|
+
- **Live widget UI** — persistent above-editor widget with animated spinners, live tool activity, token counts, and colored status icons
|
|
21
|
+
- **Session transcripts** — open any subagent's full session transcript (running or with its session released) in pi's native read-only viewer via `/subagents:sessions`
|
|
22
|
+
- **Custom agent types** — define agents in `.pi/agents/<name>.md` with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions
|
|
23
|
+
- **Mid-run steering** — inject messages into running agents to redirect their work without restarting
|
|
24
|
+
- **Session resume** — pick up where an agent left off, preserving full conversation context.
|
|
25
|
+
An agent given an isolated workspace by a `WorkspaceProvider` is resumable while that workspace is live — which, for an agent that ended its turn with a question, lasts until you answer it
|
|
26
|
+
- **Ask-back** — an agent that needs information only you have calls `ask_parent` and ends its turn, and every result surfaces the question with the exact `resume` call that answers it; once that agent can no longer be resumed, the result says so and why instead of naming a call that would be refused
|
|
27
|
+
- **Mid-run updates** — an agent that finds something material calls `notify_parent` and keeps working; the message arrives on its own while you are idle and that agent is still running, and otherwise rides that agent's own result, so you hear it exactly once and never as a stale prompt to steer an agent that has finished
|
|
28
|
+
- **Graceful turn limits** — agents get a "wrap up" warning before hard abort, producing clean partial results instead of cut-off output
|
|
29
|
+
- **Case-insensitive agent types** — `"explore"`, `"Explore"`, `"EXPLORE"` all work.
|
|
30
|
+
Unknown types fall back to general-purpose with a note
|
|
31
|
+
- **Fuzzy model selection** — specify models by name (`"haiku"`, `"sonnet"`) instead of full IDs, with automatic filtering to only available/configured models
|
|
32
|
+
- **Context inheritance** — optionally fork the parent conversation into a sub-agent so it knows what's been discussed
|
|
33
|
+
- **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML.
|
|
34
|
+
Expandable to show full output
|
|
35
|
+
- **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `resuming`, `resumed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pi install npm:@jopqior/pi-subagents
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or load directly for development:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pi -e ./src/index.ts
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
The parent agent spawns sub-agents using the `subagent` tool:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
subagent({
|
|
55
|
+
subagent_type: "Explore",
|
|
56
|
+
prompt: "Find all files that handle authentication",
|
|
57
|
+
description: "Find auth files",
|
|
58
|
+
run_in_background: true,
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Foreground agents block until complete and return results inline.
|
|
63
|
+
Background agents return an ID immediately and notify you on completion.
|
|
64
|
+
|
|
65
|
+
## UI
|
|
66
|
+
|
|
67
|
+
The extension renders a persistent widget above the editor showing active background agents (foreground runs are rendered inline by the `subagent` tool's progress stream):
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
● Agents
|
|
71
|
+
├─ ⠹ Agent Refactor auth module · ↻5≤30 · 5 tool uses · 33.8k token (62%) · 12.3s
|
|
72
|
+
│ ⎿ editing 2 files…
|
|
73
|
+
├─ ⠹ Explore Find auth files · ↻3 · 3 tool uses · 12.4k token (8%) · 4.1s
|
|
74
|
+
│ ⎿ searching…
|
|
75
|
+
├─ ⠹ Agent Long-running task · ↻42 · 38 tool uses · 91.0k token (84% · ⇊2) · 2m17s
|
|
76
|
+
│ ⎿ reading…
|
|
77
|
+
└─ 2 queued
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The token field is annotated with two optional signals inside parens:
|
|
81
|
+
|
|
82
|
+
- **`NN%`** — context-window utilization (color-coded: <70% dim, 70–85% warning, ≥85% error).
|
|
83
|
+
Omitted when the model has no declared `contextWindow`, or briefly right after compaction.
|
|
84
|
+
- **`⇊N`** — number of times the session has compacted, when > 0.
|
|
85
|
+
Stays dim; the percent's color carries urgency.
|
|
86
|
+
|
|
87
|
+
Individual agent results render inline in the conversation:
|
|
88
|
+
|
|
89
|
+
| State | Example |
|
|
90
|
+
| -------------- | ---------------------------------------------------------------------------------------- |
|
|
91
|
+
| **Running** | `⠹ ↻3≤30 · 3 tool uses · 12.4k token (8%)` / `⎿ searching, reading 3 files…` |
|
|
92
|
+
| **Completed** | `✓ ↻8 · 5 tool uses · 33.8k token (62%) · 12.3s` / `⎿ Done` |
|
|
93
|
+
| **Wrapped up** | `✓ ↻50≤50 · 50 tool uses · 89.1k token (84% · ⇊2) · 45.2s` / `⎿ Wrapped up (turn limit)` |
|
|
94
|
+
| **Stopped** | `■ ↻3 · 3 tool uses · 12.4k token (8%)` / `⎿ Stopped` |
|
|
95
|
+
| **Error** | `✗ ↻3 · 3 tool uses · 12.4k token (8%)` / `⎿ Error: timeout` |
|
|
96
|
+
| **Aborted** | `✗ ↻55≤50 · 55 tool uses · 102.3k token (95% · ⇊3)` / `⎿ Aborted (max turns exceeded)` |
|
|
97
|
+
|
|
98
|
+
Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.
|
|
99
|
+
|
|
100
|
+
Background agent completion notifications render as styled boxes:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
✓ Find auth files completed
|
|
104
|
+
↻3 · 3 tool uses · 12.4k token · 4.1s
|
|
105
|
+
⎿ Found 5 files related to authentication...
|
|
106
|
+
transcript: .pi/output/agent-abc123.jsonl
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The LLM receives structured `<task-notification>` XML for parsing, while the user sees the themed visual.
|
|
110
|
+
|
|
111
|
+
## Tools
|
|
112
|
+
|
|
113
|
+
### `subagent`
|
|
114
|
+
|
|
115
|
+
Launch a sub-agent.
|
|
116
|
+
|
|
117
|
+
| Parameter | Type | Required | Description |
|
|
118
|
+
| ------------------- | ------------ | -------- | ---------------------------------------------------------------- |
|
|
119
|
+
| `prompt` | string | yes | The task for the agent |
|
|
120
|
+
| `description` | string | yes | Short 3-5 word summary (shown in UI) |
|
|
121
|
+
| `subagent_type` | string | yes | Agent type (built-in or custom) |
|
|
122
|
+
| `model` | string | no | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
|
|
123
|
+
| `thinking` | string | no | Thinking level: off, minimal, low, medium, high, xhigh, max |
|
|
124
|
+
| `max_turns` | number | no | Max agentic turns. Omit for the agent's own limit |
|
|
125
|
+
| `run_in_background` | boolean | no | Run without blocking |
|
|
126
|
+
| `resume` | string | no | Agent ID to resume a previous session |
|
|
127
|
+
| `inherit_context` | boolean | no | Fork parent conversation into agent |
|
|
128
|
+
|
|
129
|
+
These five parameters win over the agent file's own values, which fill whichever the call leaves unset.
|
|
130
|
+
An agent file can withhold one with [`locked`](./docs/configuration.md#locking-fields-against-callers); the result then names the agent and the parameters it ignored.
|
|
131
|
+
|
|
132
|
+
### `get_subagent_result`
|
|
133
|
+
|
|
134
|
+
Check status and retrieve results from a background agent.
|
|
135
|
+
|
|
136
|
+
| Parameter | Type | Required | Description |
|
|
137
|
+
| ---------- | ------- | -------- | ----------------------------- |
|
|
138
|
+
| `agent_id` | string | yes | Agent ID to check |
|
|
139
|
+
| `wait` | boolean | no | Wait for completion |
|
|
140
|
+
| `verbose` | boolean | no | Include full conversation log |
|
|
141
|
+
|
|
142
|
+
The result renders as a compact three-line summary — status, stats, description, and a one-line preview.
|
|
143
|
+
Press `Ctrl+O` to expand it to the full report, bounded so a long result cannot fill the terminal; the expanded view names the transcript path when it withholds anything.
|
|
144
|
+
The complete report, including the conversation `verbose` requests, always reaches the model regardless of what the terminal shows.
|
|
145
|
+
|
|
146
|
+
### `steer_subagent`
|
|
147
|
+
|
|
148
|
+
Send a steering message to a running agent.
|
|
149
|
+
The message interrupts after the current tool execution.
|
|
150
|
+
|
|
151
|
+
| Parameter | Type | Required | Description |
|
|
152
|
+
| ---------- | ------ | -------- | ----------------------------------------- |
|
|
153
|
+
| `agent_id` | string | yes | Agent ID to steer |
|
|
154
|
+
| `message` | string | yes | Message to inject into agent conversation |
|
|
155
|
+
|
|
156
|
+
## Commands
|
|
157
|
+
|
|
158
|
+
| Command | Description |
|
|
159
|
+
| --------------------- | ----------------------------------------------------------------------------------- |
|
|
160
|
+
| `/subagents:settings` | Configure subagent settings (concurrency, turn limits, retention, interrupt policy) |
|
|
161
|
+
| `/subagents:sessions` | View a subagent's session transcript (read-only) |
|
|
162
|
+
|
|
163
|
+
### `/subagents:settings`
|
|
164
|
+
|
|
165
|
+
Interactive list to tune runtime settings — max concurrency, default max turns, grace turns, the two session-retention windows, and whether ESC aborts every subagent.
|
|
166
|
+
The numeric settings open an input prompt; the abort-on-ESC entry is a direct flip.
|
|
167
|
+
Changes persist across pi restarts (see [Persistent Settings](./docs/configuration.md#persistent-settings)).
|
|
168
|
+
|
|
169
|
+
### `/subagents:sessions`
|
|
170
|
+
|
|
171
|
+
Pick any subagent — running, or completed with its live session already released — and read its full session transcript in pi's native per-entry viewer.
|
|
172
|
+
Read-only: no steering, no session takeover (steering lives in the `steer_subagent` tool and the background widget).
|
|
173
|
+
|
|
174
|
+
Creating and editing agent definitions is not a command — write an agent `.md` file in your editor, or ask a pi session to generate one (see [Custom Agents](./docs/configuration.md#custom-agents)).
|
|
175
|
+
|
|
176
|
+
## Graceful Max Turns
|
|
177
|
+
|
|
178
|
+
Instead of hard-aborting at the turn limit, agents get a graceful shutdown:
|
|
179
|
+
|
|
180
|
+
1. At `max_turns` — steering message: _"Wrap up immediately — provide your final answer now."_
|
|
181
|
+
2. Up to 5 grace turns to finish cleanly
|
|
182
|
+
3. Hard abort only after the grace period
|
|
183
|
+
|
|
184
|
+
| Status | Meaning | Icon |
|
|
185
|
+
| ----------- | ----------------------------- | ---------- |
|
|
186
|
+
| `completed` | Finished naturally | `✓` green |
|
|
187
|
+
| `steered` | Hit limit, wrapped up in time | `✓` yellow |
|
|
188
|
+
| `aborted` | Grace period exceeded | `✗` red |
|
|
189
|
+
| `stopped` | User-initiated abort | `■` dim |
|
|
190
|
+
|
|
191
|
+
## Concurrency
|
|
192
|
+
|
|
193
|
+
Background agents are subject to a configurable concurrency limit (default: 4).
|
|
194
|
+
Excess agents are automatically queued and start as running agents complete.
|
|
195
|
+
The widget shows queued agents as a collapsed count.
|
|
196
|
+
|
|
197
|
+
Foreground agents bypass the queue — they block the parent anyway.
|
|
198
|
+
|
|
199
|
+
Stopping a still-queued agent produces the same completion notification a running agent's stop does.
|
|
200
|
+
Because that agent never started, the notification says so and offers no result to collect.
|
|
201
|
+
|
|
202
|
+
## Child session lifecycle
|
|
203
|
+
|
|
204
|
+
A child session runs in the parent's process but is a full Pi session with its own extension set.
|
|
205
|
+
It receives the standard pair of session lifecycle events:
|
|
206
|
+
|
|
207
|
+
| Event | When | Reason |
|
|
208
|
+
| ------------------ | --------------------------------------------------- | ----------- |
|
|
209
|
+
| `session_start` | Extensions are bound, before the child's first turn | `"startup"` |
|
|
210
|
+
| `session_shutdown` | The child session is disposed | `"quit"` |
|
|
211
|
+
|
|
212
|
+
Disposal happens when the retention window for a finished agent expires, when completed records are cleared at session start or switch, when the parent session shuts down, or when child extension binding fails partway.
|
|
213
|
+
It does **not** happen the moment an agent finishes: the session is retained so the agent can be resumed, per the `consumedSessionRetentionMinutes` and `unconsumedSessionRetentionMinutes` settings above.
|
|
214
|
+
|
|
215
|
+
The shutdown event is dispatched and awaited **before** the child's `AgentSession` is disposed, so a handler still has a live context and can close what it opened — stdio subprocesses, sockets, timers, file handles.
|
|
216
|
+
Each child's shutdown is bounded: a handler that never resolves is abandoned after a few seconds and disposal proceeds, so one misbehaving extension cannot stall the parent's teardown or Pi's exit.
|
|
217
|
+
|
|
218
|
+
If you author an extension that runs in children, note that its `session_shutdown` handler now fires **once per child session** in addition to once for the parent.
|
|
219
|
+
A handler that flushes a log, writes a summary, or closes a shared resource should be safe to run repeatedly within one process.
|
|
220
|
+
Before this behavior existed, children fired `session_start` with no matching shutdown, so extension-owned resources accumulated for the life of the parent process.
|
|
221
|
+
|
|
222
|
+
## Events
|
|
223
|
+
|
|
224
|
+
Agent lifecycle events are emitted via `pi.events.emit()` so other extensions can react:
|
|
225
|
+
|
|
226
|
+
| Event | When | Key fields |
|
|
227
|
+
| ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
228
|
+
| `subagents:created` | Background agent registered | `id`, `type`, `description`, `isBackground` |
|
|
229
|
+
| `subagents:started` | Agent transitions to running (including queued→running) | `id`, `type`, `description` |
|
|
230
|
+
| `subagents:completed` | Agent finished successfully | `id`, `type`, `durationMs`, `tokens` (lifetime `{ input, output, total }`), `toolUses`, `result` |
|
|
231
|
+
| `subagents:failed` | Agent errored, stopped, or aborted | same as completed + `error`, `status` |
|
|
232
|
+
| `subagents:resuming` | Resume started, from either front door | `id`, `type`, `description` |
|
|
233
|
+
| `subagents:resumed` | Resumed run reached a terminal state (completed/error) | same as completed + `error`, `status` (`buildEventData` shape) — `status`/`error` discriminate |
|
|
234
|
+
| `subagents:steered` | Steering message sent | `id`, `message` |
|
|
235
|
+
| `subagents:compacted` | Agent's session successfully compacted | `id`, `type`, `description`, `reason` (`"manual"` / `"threshold"` / `"overflow"`), `tokensBefore`, `compactionCount` |
|
|
236
|
+
| `subagents:settings_loaded` | Persisted settings applied at extension init | `settings` (merged global + project) |
|
|
237
|
+
| `subagents:settings_changed` | `/subagents:settings` mutation was applied | `settings`, `persisted` (`boolean` — `false` on write failure) |
|
|
238
|
+
|
|
239
|
+
`tokens.total` = `input + output + cacheWrite`.
|
|
240
|
+
`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.
|
|
241
|
+
Use `contextUsage.percent` (surfaced as `(NN%)` in the widget) for current context size.
|
|
242
|
+
|
|
243
|
+
## Per-spawn model and thinking selection
|
|
244
|
+
|
|
245
|
+
Interactive model and thinking choice lives in a companion package, not this core.
|
|
246
|
+
This fork's `@jopqior/pi-subagents-model-selector` registers a `SpawnSelectionProvider` so every new in-process run asks the operator before a workspace or child session is created.
|
|
247
|
+
The selected pair overrides model and thinking defaults, explicit arguments, and `locked:` values for those two fields only.
|
|
248
|
+
Installing this core without the companion leaves ordinary resolution unchanged.
|
|
249
|
+
See [`@jopqior/pi-subagents-model-selector`](../pi-subagents-model-selector/README.md) for load order, nested routing, and non-interactive refusal.
|
|
250
|
+
|
|
251
|
+
## Worktree Isolation
|
|
252
|
+
|
|
253
|
+
Worktree isolation lives in a companion package, not this core.
|
|
254
|
+
Install [`@gotgenes/pi-subagents-worktrees`](https://github.com/gotgenes/pi-packages/tree/main/packages/pi-subagents-worktrees) and list the agent types you want isolated in its `worktreeAgents` config — opted-in agents run in a temporary git worktree, and their changes are saved to a branch on completion.
|
|
255
|
+
The earlier `isolation: "worktree"` spawn flag and `isolation:` frontmatter key were removed from the core.
|
|
256
|
+
|
|
257
|
+
## Removed: agent memory and skill preloading
|
|
258
|
+
|
|
259
|
+
Persistent agent memory (the `memory:` frontmatter key) and skill preloading (the `skills:` frontmatter key) were removed when the core was slimmed down.
|
|
260
|
+
Children inherit the parent's skills and extensions by default, so the `isolated`, `extensions`, and `skills` frontmatter keys no longer exist.
|
|
261
|
+
Package-level extension opt-outs live in the [`excludedExtensionPackages`](./docs/configuration.md#excluding-package-extensions-from-children) setting rather than agent frontmatter.
|
|
262
|
+
|
|
263
|
+
## Migrating from `disallowed_tools`
|
|
264
|
+
|
|
265
|
+
The `disallowed_tools` frontmatter field has been removed.
|
|
266
|
+
Use [`@gotgenes/pi-permission-system`](https://github.com/gotgenes/pi-permission-system)'s `permission:` frontmatter instead — it provides richer semantics (allow/ask/deny vs. binary hide):
|
|
267
|
+
|
|
268
|
+
```yaml
|
|
269
|
+
# Before (no longer supported)
|
|
270
|
+
disallowed_tools: bash
|
|
271
|
+
|
|
272
|
+
# After
|
|
273
|
+
permission:
|
|
274
|
+
bash: deny
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Permission System Integration
|
|
278
|
+
|
|
279
|
+
When [`@gotgenes/pi-permission-system`](https://github.com/gotgenes/pi-permission-system) is installed, this extension integrates automatically:
|
|
280
|
+
|
|
281
|
+
- **Per-agent permission policies** — define `permission:` in agent YAML frontmatter to set allow/ask/deny rules per agent type.
|
|
282
|
+
The permission system resolves the agent name from the `<active_agent>` tag in the child system prompt.
|
|
283
|
+
- **Tool filtering** — the permission system's `before_agent_start` handler removes denied tools from the child session before the agent starts.
|
|
284
|
+
- **`ask`-state forwarding** — when a child session triggers an `ask` permission, the prompt forwards to the parent session's UI.
|
|
285
|
+
The parent approves or denies, and the child resumes.
|
|
286
|
+
- **Deterministic child detection** — this extension publishes `subagents:child:session-created` before `bindExtensions()` fires; the permission system subscribes and registers the child session synchronously, so detection does not rely on env vars or filesystem heuristics.
|
|
287
|
+
- **Unguarded children are announced** — this extension also publishes `subagents:child:bound` once a child's extensions have bound; the permission system uses it to notice a child that loaded no permission node of its own — the case [`excludedExtensionPackages`](docs/configuration.md#excluding-package-extensions-from-children) can create — and warns rather than letting it run ungated in silence.
|
|
288
|
+
|
|
289
|
+
No configuration is required.
|
|
290
|
+
When `@gotgenes/pi-permission-system` is not installed, the lifecycle events have no subscriber — a harmless no-op.
|
|
291
|
+
|
|
292
|
+
## For Extension Authors
|
|
293
|
+
|
|
294
|
+
This package exposes two public subpath exports for companion extensions to import from the published tarball.
|
|
295
|
+
|
|
296
|
+
### `@jopqior/pi-subagents` — cross-extension service contract
|
|
297
|
+
|
|
298
|
+
Access the subagent service from another extension at runtime:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
const { getSubagentsService } = await import("@jopqior/pi-subagents");
|
|
302
|
+
const svc = getSubagentsService();
|
|
303
|
+
svc?.spawn("Explore", "Check for stale TODOs");
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Declare this package as an optional peer dependency.
|
|
307
|
+
See `src/service/service.ts` for the full `SubagentsService` interface, the `WorkspaceProvider` seam, and `registerSpawnSelectionProvider`.
|
|
308
|
+
|
|
309
|
+
#### `spawn` contract
|
|
310
|
+
|
|
311
|
+
`spawn` returns the new agent's id immediately — it never waits for the run.
|
|
312
|
+
Use `getRecord(id)` to poll, `steer` to send a message, and the `subagents:completed` event to learn when it finished.
|
|
313
|
+
|
|
314
|
+
The agent type is canonicalized, so `"explore"` and `"Explore"` reach the same agent.
|
|
315
|
+
An unrecognized type falls back to `general-purpose` rather than throwing, matching the `subagent` tool's behavior.
|
|
316
|
+
|
|
317
|
+
It throws in four cases:
|
|
318
|
+
|
|
319
|
+
- there is no active session, so there is no parent to spawn from;
|
|
320
|
+
- a `model` string does not resolve against the session's model registry;
|
|
321
|
+
- a `thinkingLevel` is not one of `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`;
|
|
322
|
+
- the named agent type exists but is disabled (`enabled: false`).
|
|
323
|
+
|
|
324
|
+
Agent frontmatter never overrides an option you pass.
|
|
325
|
+
It fills `model`, `thinkingLevel`, and `maxTurns` when you omit them; `inheritContext` is the exception, and defaults to `false` whatever the agent file declares.
|
|
326
|
+
An agent file's [`locked`](./docs/configuration.md#locking-fields-against-callers) frontmatter does not apply here — it guards against a model guessing harness settings, and an SDK caller is not that.
|
|
327
|
+
A registered spawn-selection provider still asks after this resolution and overrides `model` and `thinkingLevel` for the new run.
|
|
328
|
+
|
|
329
|
+
Background mode follows the caller's degree of commitment.
|
|
330
|
+
Omit `foreground` and the agent's own `run_in_background` frontmatter decides, defaulting to background when the agent declares nothing.
|
|
331
|
+
Pass `foreground` explicitly and it wins outright, whatever the frontmatter says.
|
|
332
|
+
|
|
333
|
+
A spawned agent is a first-class citizen of the runtime: it appears in the background widget, carries its parent's session identity so permission prompts route correctly, and nests its session file under the parent's.
|
|
334
|
+
|
|
335
|
+
#### `registerSpawnSelectionProvider` contract
|
|
336
|
+
|
|
337
|
+
Register the single provider this session's subagent tree will consult before creating any **new** child session.
|
|
338
|
+
`spawn()` still returns the id immediately.
|
|
339
|
+
The admitted record exists while the operator chooses, but neither workspace nor child session is created until selection succeeds.
|
|
340
|
+
|
|
341
|
+
Capture the service instance once during extension initialization.
|
|
342
|
+
Do not look the locator up again on later events.
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
const service = getSubagentsService();
|
|
346
|
+
const registration = service.registerSpawnSelectionProvider(chooser);
|
|
347
|
+
if (registration.kind === "inherited") return;
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
On the tree's root session, the result is `owned` and `dispose()` revokes that generation's lease.
|
|
351
|
+
A second registration on an active root throws.
|
|
352
|
+
On a descendant, the result is `inherited`: the supplied provider is not installed, `dispose()` is a no-op, and the root's ownership is untouched — including when the inherited lease is already revoked.
|
|
353
|
+
|
|
354
|
+
`undefined` from `select()` is user cancellation, never an approval that keeps inherited values.
|
|
355
|
+
Infrastructure failures (no UI, empty catalogue, invalid pair) must reject.
|
|
356
|
+
Resume does not call the provider.
|
|
357
|
+
|
|
358
|
+
#### `getRecord` / `listAgents` contract
|
|
359
|
+
|
|
360
|
+
Both return `SubagentRecord`, a by-value snapshot: nothing in it changes after you receive it, and writing to it cannot reach the agent.
|
|
361
|
+
Poll again for fresh data.
|
|
362
|
+
|
|
363
|
+
The snapshot carries identity (`id`, `type`, `description`), lifecycle status (`status`, `startedAt`, `completedAt`, `result`, `error`), the resolved spawn facts (`isBackground`, `maxTurns`), cumulative metrics (`toolUses`, `turnCount`, `compactionCount`, `lifetimeUsage`), and `outputFile` — the path to the agent's session JSONL, which you can read with Pi's own `parseSessionEntries`.
|
|
364
|
+
|
|
365
|
+
It deliberately withholds momentary activity (the tools running right now, the partial response text, whether the run is awaiting a human model/thinking selection) and this package's internal bookkeeping.
|
|
366
|
+
A pulled snapshot of momentary state would be stale on arrival; [decision 0005](docs/decisions/0005-subagent-record-admission-policy.md) records the full policy and what would reopen it.
|
|
367
|
+
|
|
368
|
+
`SubagentRecord` and `SubagentsService` are types this package produces and you read — not contracts to implement.
|
|
369
|
+
A new field is therefore a minor release; use a cast or a `Partial<>` for a test double rather than implementing either type.
|
|
370
|
+
|
|
371
|
+
#### `resume` contract
|
|
372
|
+
|
|
373
|
+
`resume(id, prompt, options?)` continues a settled agent's session, and is the one service call that waits: it resolves when the resumed run reaches a terminal state, carrying the terminal snapshot.
|
|
374
|
+
A caller that does not need the outcome can ignore the promise.
|
|
375
|
+
|
|
376
|
+
It never throws and never rejects.
|
|
377
|
+
A resume that could not start resolves to `{ kind: "refused", reason }` instead, promptly — the checks are synchronous and no turn loop runs:
|
|
378
|
+
|
|
379
|
+
| `reason` | Meaning |
|
|
380
|
+
| -------------------- | --------------------------------------------------------------- |
|
|
381
|
+
| `unknown-agent` | No record answers to that id (records are cleared per session) |
|
|
382
|
+
| `still-running` | The agent has not settled; wait, or `steer` it while it runs |
|
|
383
|
+
| `no-session` | The agent never had a session to continue |
|
|
384
|
+
| `session-released` | Its session was released after the retention window |
|
|
385
|
+
| `workspace-disposed` | Its isolated workspace is gone, so a resume cannot re-enter it |
|
|
386
|
+
|
|
387
|
+
A resumed run that _fails_ is still `{ kind: "resumed" }`; the snapshot carries `status: "error"` and the message.
|
|
388
|
+
Refused means nothing started.
|
|
389
|
+
|
|
390
|
+
By default the resumed outcome is announced to the parent like any other background completion.
|
|
391
|
+
Pass `claimOutcome: true` to declare that your extension is delivering it, which suppresses that announcement — do this only if you will actually carry the result to the parent, or it reaches nobody.
|
|
392
|
+
|
|
393
|
+
Pass `signal` to cancel the resumed turn loop.
|
|
394
|
+
`abort(id)` does not reach it: a resume does not run under the record's own abort controller.
|
|
395
|
+
|
|
396
|
+
### `@jopqior/pi-subagents/settings` — layered config loader
|
|
397
|
+
|
|
398
|
+
Extensions that store configuration in JSON files can use the shared layered loader, which reads a global file (`<agentDir>/<filename>`) and a project file (`<cwd>/.pi/<filename>`) and merges them — project wins on conflicts, missing files are silent, malformed files warn and fall back:
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { loadLayeredSettings, type LayeredSettingsSource } from "@jopqior/pi-subagents/settings";
|
|
402
|
+
|
|
403
|
+
interface MyConfig { enabled?: boolean; limit?: number }
|
|
404
|
+
|
|
405
|
+
function sanitize(raw: unknown): Partial<MyConfig> {
|
|
406
|
+
if (!raw || typeof raw !== "object") return {};
|
|
407
|
+
const r = raw as Record<string, unknown>;
|
|
408
|
+
const out: Partial<MyConfig> = {};
|
|
409
|
+
if (typeof r.enabled === "boolean") out.enabled = r.enabled;
|
|
410
|
+
if (typeof r.limit === "number") out.limit = r.limit;
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const config = loadLayeredSettings<MyConfig>({
|
|
415
|
+
agentDir, // Pi runtime agent home directory
|
|
416
|
+
cwd, // project root — project file lives at <cwd>/.pi/<filename>
|
|
417
|
+
filename: "my-extension.json",
|
|
418
|
+
sanitize,
|
|
419
|
+
warnLabel: "my-extension", // prefix for the malformed-file stderr warning
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
`loadLayeredSettings` returns `Partial<T>` (all fields optional); apply your defaults after the call.
|
|
424
|
+
It never throws — all error conditions produce a `console.warn` and return `{}`.
|
|
425
|
+
|
|
426
|
+
### Extensions that append to the system prompt
|
|
427
|
+
|
|
428
|
+
If your extension appends to the system prompt from a `before_agent_start` handler, your parent-session block does **not** ride into child sessions.
|
|
429
|
+
A child inherits only the stable part of the parent's prompt — everything Pi assembled ahead of the skills catalogue — so anything appended after that is dropped.
|
|
430
|
+
See [What a child inherits from the parent's prompt](./docs/configuration.md#what-a-child-inherits-from-the-parents-prompt) for the full layer breakdown.
|
|
431
|
+
|
|
432
|
+
This is usually invisible to you, because your handler runs in the child too: a child binds the parent's extension set, and its turn loop fires `before_agent_start` the same way the parent's does.
|
|
433
|
+
An unconditional appender therefore writes a fresh block built for the child's own session — which is what you want, since the parent's copy named the parent's directory, model, and session.
|
|
434
|
+
|
|
435
|
+
Two cases need care:
|
|
436
|
+
|
|
437
|
+
- A handler gated on something a child lacks — an interactive UI, a terminal, or state your extension cached at `session_start` — appends nothing in the child.
|
|
438
|
+
That child now carries no block at all, where previously it inherited one built for the parent.
|
|
439
|
+
If your guidance applies to children, make the handler unconditional or derive its inputs from the event context rather than from cached session state.
|
|
440
|
+
- An extension excluded from children through [`excludedExtensionPackages`](./docs/configuration.md#excluding-package-extensions-from-children) contributes nothing to a child by design, and no longer leaks its parent-session block in either.
|
|
441
|
+
|
|
442
|
+
Extensions that _shape_ the prompt at the provider boundary rather than appending to it are unaffected — the region they rewrite is the identity a child inherits verbatim.
|
|
443
|
+
|
|
444
|
+
An extension that states something **per session** — which tools this session may call, which skills it loaded — should append it rather than edit the inherited identity, even when Pi wrote its own copy up there.
|
|
445
|
+
Editing that region rewrites bytes the child inherited from its parent, which ends the prefix the two share; `@gotgenes/pi-permission-system` relocates the `Available tools:` and `Guidelines:` sections to the end of the prompt for exactly this reason ([#890](https://github.com/gotgenes/pi-packages/issues/890)).
|
|
446
|
+
|
|
447
|
+
## Scope and non-goals
|
|
448
|
+
|
|
449
|
+
**Purpose.**
|
|
450
|
+
A minimal, in-process sub-agent core.
|
|
451
|
+
It spawns a child session derived from the parent, runs the turn loop, streams and collects the result, gates concurrency, supports resume, and publishes its lifecycle.
|
|
452
|
+
Everything else is a consumer.
|
|
453
|
+
|
|
454
|
+
**In scope.**
|
|
455
|
+
Defects in the surfaces the core already owns, completeness of the public lifecycle-event contract, and internal work toward the minimal-core target.
|
|
456
|
+
Anything attaching to the core either subscribes to a lifecycle event, or registers a provider if it must return a value the core consumes — see [ADR-0002](./docs/decisions/0002-extensions-on-a-minimal-core.md).
|
|
457
|
+
|
|
458
|
+
**Non-goals.**
|
|
459
|
+
|
|
460
|
+
- _Capability the fork deliberately left behind._
|
|
461
|
+
Scheduling, cross-extension RPC, model-scope enforcement, and a built-in tool denylist belong to upstream — see [Relationship to upstream](#relationship-to-upstream).
|
|
462
|
+
- _Policy about what a child may do._
|
|
463
|
+
Tool restriction is allow/ask/deny in a permission layer, not a binary hide in a spawner — see [Migrating from `disallowed_tools`](#migrating-from-disallowed_tools).
|
|
464
|
+
- _Widening a child's tool allowlist with **capability** tools on the agent's behalf._
|
|
465
|
+
An agent's `tools:` frontmatter is the only thing that admits a capability tool, and no settings key may name one, because a settings-level list would hand a read-only `Explore` agent write-capable tools from a file its author never saw.
|
|
466
|
+
The core does install its own protocol in every child — the `<active_agent>` tag, the parent-context prefix, and the `ask_parent` / `notify_parent` tools — none of which reaches the filesystem, the shell, or the network.
|
|
467
|
+
- _A global run-mode default._
|
|
468
|
+
Foreground or background is a per-invocation argument and a per-agent frontmatter key; a global flip changes every existing agent file at once.
|
|
469
|
+
- _Provider seams with no consumer._
|
|
470
|
+
A seam nobody supplies is a speculative abstraction that taxes every reader; the architecture may admit one without shipping it until a real consumer exists.
|
|
471
|
+
|
|
472
|
+
The [architecture doc](./docs/architecture/architecture.md#scope-and-non-goals) carries the full inventory, including the removed UI surfaces and the reasoning behind each.
|
|
473
|
+
|
|
474
|
+
**Where adjacent requests belong.**
|
|
475
|
+
Tool restriction and per-agent permission policy → [@gotgenes/pi-permission-system](https://www.npmjs.com/package/@gotgenes/pi-permission-system).
|
|
476
|
+
Worktree isolation → [@gotgenes/pi-subagents-worktrees](https://www.npmjs.com/package/@gotgenes/pi-subagents-worktrees).
|
|
477
|
+
Per-spawn model and thinking selection → this fork's `@jopqior/pi-subagents-model-selector` (local companion).
|
|
478
|
+
Timed dispatch, telemetry, and alternate UIs → a consumer over the lifecycle events and the typed service.
|
|
479
|
+
A batteries-included alternative → upstream [`tintinweb/pi-subagents`](https://github.com/tintinweb/pi-subagents).
|
|
480
|
+
|
|
481
|
+
## Documentation
|
|
482
|
+
|
|
483
|
+
| Document | Contents |
|
|
484
|
+
| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
485
|
+
| [Configuration](./docs/configuration.md) | Default agent types, custom agent files and their frontmatter fields, and the `subagents.json` settings file |
|
|
486
|
+
| [Architecture](./docs/architecture/architecture.md) | Design principles, domain decomposition, module dependency flow, Mermaid diagrams, and the improvement roadmap |
|
|
487
|
+
| [Comparison with upstream](./docs/comparison-with-upstream.md) | Feature-by-feature comparison against the current upstream release |
|
|
488
|
+
|
|
489
|
+
## Architecture
|
|
490
|
+
|
|
491
|
+
This extension is a minimal, composable core: it owns agent spawning, execution, and result retrieval, and exposes a typed `SubagentsService` plus lifecycle events that other extensions build on.
|
|
492
|
+
|
|
493
|
+
## Relationship to upstream
|
|
494
|
+
|
|
495
|
+
This package is an independently maintained hard fork of [`tintinweb/pi-subagents`](https://github.com/tintinweb/pi-subagents) by [@tintinweb](https://github.com/tintinweb).
|
|
496
|
+
It has diverged substantially in scope and architecture: a minimal core with a typed service API and lifecycle events, with tool-restriction policy and worktree isolation delegated to companion packages.
|
|
497
|
+
Upstream remains the batteries-included option, keeping scheduling, cross-extension RPC, model-scope enforcement, and a built-in tool denylist in a single package.
|
|
498
|
+
|
|
499
|
+
See [Comparison with upstream](./docs/comparison-with-upstream.md) for a full feature-by-feature comparison against the current upstream release and guidance on which to choose.
|
|
500
|
+
|
|
501
|
+
## License
|
|
502
|
+
|
|
503
|
+
MIT — [tintinweb](https://github.com/tintinweb) (upstream) and [Chris Lasher](https://github.com/gotgenes) (fork)
|