pi-better-subagents 0.1.2 → 0.1.4
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/LICENSE +21 -0
- package/README.md +34 -398
- package/package.json +9 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 1aboveio
|
|
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
CHANGED
|
@@ -1,420 +1,56 @@
|
|
|
1
1
|
# pi-better-subagents
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`pi-better-subagents` is a Pi extension for detached, sandboxed subagent runs that keep the foreground Pi session free.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
should be: **autonomous, non-blocking, and safe by default.** You delegate work
|
|
7
|
-
and keep going; each subagent runs on its own in an isolated process, confined to
|
|
8
|
-
what it needs, and reports back when it's done. No blocking waits, no
|
|
9
|
-
back-channel for it to stall on, no unbounded blast radius.
|
|
5
|
+
## Quick Answer
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
launch is the result · completion triggers fetch · the foreground never blocks
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Principles
|
|
16
|
-
|
|
17
|
-
- **The foreground never blocks.** Launching a subagent *is* the deliverable —
|
|
18
|
-
`subagent_spawn` starts a detached `pi -p` child and returns immediately,
|
|
19
|
-
leaving the session free for the human. When the child finishes, it sends a
|
|
20
|
-
lightweight trigger; the foreground calls `subagent_result` and presents the
|
|
21
|
-
result (as a `followUp`, never cutting into work in progress). The foreground
|
|
22
|
-
is nudged once, at completion — never on a wait/poll loop.
|
|
23
|
-
- **Subagents are autonomous; communication is one-way (parent → child).** The
|
|
24
|
-
parent front-loads everything the child needs into the spawn; the child runs to
|
|
25
|
-
completion and **returns a result**. There is no mid-task child→parent blocking
|
|
26
|
-
call for a subagent to waste wall-clock on — a child missing a piece of info
|
|
27
|
-
resolves it from what it was given, or records it unavailable and returns.
|
|
28
|
-
- **Safe by default.** Every subagent is OS-sandboxed — writes confined to its
|
|
29
|
-
working directory, reads and network open — and scoped to an explicit tool
|
|
30
|
-
allowlist. It can't corrupt the parent, escape its directory, or recurse into
|
|
31
|
-
more subagents without opt-in.
|
|
32
|
-
- **Observable.** A live status widget and on-demand queries show each run's
|
|
33
|
-
elapsed time and token/cost spend.
|
|
34
|
-
|
|
35
|
-
## Tools
|
|
36
|
-
|
|
37
|
-
| Tool | Blocks? | What it does |
|
|
38
|
-
|------|---------|--------------|
|
|
39
|
-
| `subagent_spawn` | never | Launch a task in a background subagent; returns a run id at once. Params: `prompt`, `name`, `model`, `tools` (allowlist), `exclude_tools`, `sandbox`, `sandbox_dir`, `callback`, `clean`, `cwd`, `git_clone_workspace`, `approve`, `allow_nested`. |
|
|
40
|
-
| `subagent_spawn_batch` | never | Launch several independent subagents at once. Each job becomes a normal run. Params: `batchName`, `shared` (options applied to every job), `jobs[]` (each needs `prompt`; same optional params as `subagent_spawn`), `onCapacity` (`reject` or `launch-available`). |
|
|
41
|
-
| `subagent_list` | never | List running/finished runs with status, model, elapsed, spend, and batch info. Params: `all`, `limit` (default 20, max 100; larger values are clamped), `status` (`running`, `completed`, `failed`, `killed`, `exited`, durable `orphaned`, `lost`). Degraded health facts (stale, long tool, compacting, model error, …) appear only when actionable. |
|
|
42
|
-
| `subagent_output` | never | Tail a run's live output as it stands right now. Includes a `[health: …]` diagnostic for orphaned, lost, and degraded running runs; healthy/quiet stays quiet. |
|
|
43
|
-
| `subagent_result` | never | Read a finished run's final output (says "still running" otherwise). For `orphaned` returns a non-final diagnostic plus best-current artifacts; for `lost` a terminal-unknown diagnostic plus best-available artifacts. In the TUI, the result is folded by default for display only; expanding the tool row shows the full bounded result, and the model-facing payload is unchanged. |
|
|
44
|
-
| `subagent_stop` | never | SIGTERM a running run's process group. |
|
|
45
|
-
|
|
46
|
-
## Non-blocking, by construction
|
|
47
|
-
|
|
48
|
-
- **Process isolation.** Each run is a `detached` + `unref`'d `pi -p` process.
|
|
49
|
-
Its context can't clog the parent, its crash can't corrupt parent state, and
|
|
50
|
-
its output is durable in a log file.
|
|
51
|
-
- **Completion triggers a turn that fetches the result.** When the child exits,
|
|
52
|
-
a lightweight trigger is sent with `pi.sendMessage(..., { deliverAs: "followUp", triggerTurn: true })` —
|
|
53
|
-
it waits until the foreground agent has no pending tool calls (never cutting
|
|
54
|
-
into work mid-stream), then the model calls `subagent_result` and presents the
|
|
55
|
-
result. The actual result is never embedded in the trigger to avoid double-display.
|
|
56
|
-
The run itself never blocks the foreground; the single nudge happens only at
|
|
57
|
-
the end. Prefer `callback:false` to finish quietly and read the result on demand
|
|
58
|
-
via `subagent_result`.
|
|
59
|
-
- **The prompt guidelines forbid polling.** The foreground agent is told, in the
|
|
60
|
-
tool guidelines, that spawning is done and it must not loop on `output`/`result`
|
|
61
|
-
or sleep to wait.
|
|
62
|
-
|
|
63
|
-
## Autonomy & safety
|
|
64
|
-
|
|
65
|
-
Every subagent is confined by default, and the confinement is **self-contained** —
|
|
66
|
-
it does not depend on any other extension being installed.
|
|
7
|
+
Use `pi-better-subagents` when you want Pi to launch independent agent work without blocking the current conversation. Each subagent runs in its own `pi -p` child process, reports back when finished, and keeps durable logs for later inspection.
|
|
67
8
|
|
|
68
|
-
|
|
69
|
-
`sandbox-exec` on macOS or [bubblewrap](https://github.com/containers/bubblewrap)
|
|
70
|
-
(`bwrap`) on Linux with a simple rule: **reads and network are open; writes are
|
|
71
|
-
confined to the working directory and host `/tmp`.** Kernel-enforced — unlike a
|
|
72
|
-
cooperative guardrail that matches tool inputs, this denies the write syscall
|
|
73
|
-
itself, so a crafted `bash` command can't escape it. `sandbox:false` lifts it;
|
|
74
|
-
`sandbox_dir` moves the writable root (and becomes the child's cwd). `/dev`
|
|
75
|
-
remains usable. macOS also permits pi state writes under `~/.pi`; Linux exposes
|
|
76
|
-
that directory read-only, so Linux children must put writable pi state in their
|
|
77
|
-
work directory or `/tmp`. Everything else (your home, the repo, `/etc`, …) is
|
|
78
|
-
read-only to the subagent.
|
|
79
|
-
- **Tool allowlist.** The child is scoped to an explicit set of tools, which also
|
|
80
|
-
decides what extension code loads (see below).
|
|
81
|
-
- **No runaway recursion.** A subagent cannot spawn its own subagents unless
|
|
82
|
-
`allow_nested:true` — and not by denying the tools after the fact: without that
|
|
83
|
-
flag this package isn't loaded in the child, so the tools don't exist.
|
|
9
|
+
## Core Features
|
|
84
10
|
|
|
85
|
-
|
|
11
|
+
- Non-blocking subagent launches.
|
|
12
|
+
- Default OS write sandboxing on macOS and Linux.
|
|
13
|
+
- Explicit tool allowlists for child sessions.
|
|
14
|
+
- Durable logs and result retrieval across reloads.
|
|
15
|
+
- Live background-work navigator for active runs.
|
|
86
16
|
|
|
87
|
-
|
|
88
|
-
on `subagent_spawn`. The parent prepares a fresh, self-contained Git clone whose
|
|
89
|
-
`.git/` directory lives **inside the sandbox writable root**, then runs the child
|
|
90
|
-
in that clone.
|
|
91
|
-
|
|
92
|
-
Why this matters: a linked Git worktree (created with `git worktree add`) has a
|
|
93
|
-
`.git` file that points back to administrative state under the main repository,
|
|
94
|
-
typically outside the sandbox directory. A sandbox that only allows writes under
|
|
95
|
-
the worktree directory therefore cannot support normal Git producer operations
|
|
96
|
-
such as fetch, rebase, commit, and push — the child stalls or fails when Git
|
|
97
|
-
tries to write metadata it cannot reach. `git_clone_workspace:true` avoids this
|
|
98
|
-
by cloning the repository with a real `.git/` directory inside the writable root.
|
|
99
|
-
|
|
100
|
-
The clone uses:
|
|
17
|
+
## Install
|
|
101
18
|
|
|
19
|
+
```sh
|
|
20
|
+
pi install npm:pi-better-subagents
|
|
102
21
|
```
|
|
103
|
-
git clone --reference-if-able <local-reference-repo> --dissociate \
|
|
104
|
-
<remote-url> <sandbox-workspace>
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
`--reference-if-able` borrows local objects from the parent repository during
|
|
108
|
-
setup; `--dissociate` removes the alternates link afterwards, so the clone is
|
|
109
|
-
self-contained and safe to delete. The clone source prefers the source
|
|
110
|
-
workspace's upstream remote URL (`origin` when set) so the disposable
|
|
111
|
-
workspace's `origin` points at the real remote rather than the parent working
|
|
112
|
-
tree — pushes therefore target upstream, not the sandboxed parent. The local
|
|
113
|
-
repository is used only as a reference (and as a content fallback when no
|
|
114
|
-
remote is configured). Source remotes are re-synced after clone. The
|
|
115
|
-
checked-out branch/commit matches the source workspace at spawn time.
|
|
116
|
-
Repo-local Git identity settings from the source (`user.name`, `user.email`,
|
|
117
|
-
and `user.signingkey` when set) are copied into the clone so ordinary commits
|
|
118
|
-
work without reconfiguring identity inside the disposable workspace.
|
|
119
|
-
|
|
120
|
-
If the source workspace is a linked worktree, the clone is prepared from the
|
|
121
|
-
main repository's object database and the requested branch/commit; the child is
|
|
122
|
-
never launched into the structurally broken linked-worktree sandbox. If clone
|
|
123
|
-
preparation fails, the spawn fails fast with a message explaining that the
|
|
124
|
-
linked-worktree Git metadata is outside the sandbox and recommending
|
|
125
|
-
`git_clone_workspace:true`.
|
|
126
|
-
|
|
127
|
-
Note that because children load only the extensions backing their tools, a
|
|
128
|
-
guardrails extension (e.g. `@aliou/pi-guardrails`) does **not** apply inside a
|
|
129
|
-
subagent unless you map a tool to it. The OS write sandbox above is what confines
|
|
130
|
-
the child, and it doesn't depend on any extension.
|
|
131
|
-
|
|
132
|
-
## Tool scoping (allowlist)
|
|
133
|
-
|
|
134
|
-
Precedence, highest first: the per-call `tools` param → `config.json`
|
|
135
|
-
`defaultTools` → a built-in default (`read, bash, edit, write, web_search,
|
|
136
|
-
web_fetch`; just `read, bash` in a `clean` child). `exclude_tools` subtracts on
|
|
137
|
-
top.
|
|
138
22
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
- `defaultModel` — model for spawns that don't specify one (`null` = inherit the
|
|
142
|
-
foreground model).
|
|
143
|
-
- `maxConcurrent` — how many subagents may run at once (**default 4**). A spawn
|
|
144
|
-
past the cap is rejected until a running one finishes.
|
|
145
|
-
|
|
146
|
-
## The allowlist also decides what LOADS
|
|
147
|
-
|
|
148
|
-
The `tools` allowlist does double duty: it is both what the child may call **and**
|
|
149
|
-
which extension *code* is loaded into it. A child launches as
|
|
23
|
+
Try it for one run:
|
|
150
24
|
|
|
25
|
+
```sh
|
|
26
|
+
pi -e npm:pi-better-subagents
|
|
151
27
|
```
|
|
152
|
-
pi -p --mode json --no-extensions -e <package backing a requested tool> ...
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
so a package that backs no requested tool never loads. With the default
|
|
156
|
-
allowlist (`read, bash, edit, write, web_search, web_fetch`) exactly one package
|
|
157
|
-
loads — the web-tools one — and `web_fetch` works normally.
|
|
158
|
-
|
|
159
|
-
Two maps in `config.json` drive it:
|
|
160
|
-
|
|
161
|
-
- `toolExtensions` — tool name → package(s) providing it. Built-ins (`read`,
|
|
162
|
-
`bash`, `edit`, `write`) need no entry.
|
|
163
|
-
- `providerExtensions` — provider → auth package. Model auth is not tool-shaped:
|
|
164
|
-
`xai/grok-4.5` needs `pi-xai-oauth` loaded whatever tools it was granted.
|
|
165
|
-
|
|
166
|
-
Ask for a tool with no mapping and the spawn still succeeds, but says so at
|
|
167
|
-
launch — the tool simply will not exist in the child.
|
|
168
|
-
|
|
169
|
-
`clean:true` is the narrowest case of the same mechanism: no extensions at all.
|
|
170
|
-
`allow_nested:true` is the one thing that loads *this* package into the child;
|
|
171
|
-
without it, nested spawning is impossible because the code isn't there.
|
|
172
|
-
|
|
173
|
-
`inheritExtensions: true` in `config.json` restores the old load-everything
|
|
174
|
-
behavior. It is **operator-only** — no spawn parameter can reach it, so the child
|
|
175
|
-
model cannot widen its own runtime. It also re-exposes the failure below.
|
|
176
|
-
|
|
177
|
-
### Why: a subagent that loads everything can die mid-turn reporting success
|
|
178
|
-
|
|
179
|
-
Loading every installed package means inheriting their startup side effects. A
|
|
180
|
-
package that replaces builtin `bash` with a `detached` + `unref()` spawn breaks
|
|
181
|
-
`pi -p`: on a parallel `bash` + `read` batch the in-process `read` finishes, the
|
|
182
|
-
unref'd `bash` doesn't hold the event loop, Node drains, and the child **exits 0
|
|
183
|
-
mid-turn** — no `tool_execution_end`, no `agent_end`. Historically, exit 0 was
|
|
184
|
-
indistinguishable from a clean finish, so all 17 observed mid-turn exits were
|
|
185
|
-
reported as ✓ completed. Finalization now requires terminal agent evidence and
|
|
186
|
-
no unmatched tool starts. Lifecycle validation classifies runs as `complete`,
|
|
187
|
-
`incomplete_no_terminal_event`, `incomplete_open_tools`, `failed_exit`, or
|
|
188
|
-
`killed`; incoherent exit-0 streams are recorded as failed with named lifecycle
|
|
189
|
-
diagnostics on `subagent_result` and attention wording on completion callbacks.
|
|
190
|
-
|
|
191
|
-
A tool allowlist alone cannot fix this. `--tools` restricts what the model may
|
|
192
|
-
*call*; the package already overrode builtin `bash` at startup, so the `bash` in
|
|
193
|
-
your allowlist **is** the broken one. Measured with the default 6 tools:
|
|
194
|
-
|
|
195
|
-
| runtime | tool starts / ends | terminal event |
|
|
196
|
-
|---|---|---|
|
|
197
|
-
| all extensions loaded | 2 / 1 | none — exits 0 mid-turn |
|
|
198
|
-
| `--no-extensions -e <web-tools>` | 3 / 3 | `agent_settled`, `web_fetch` OK |
|
|
199
|
-
|
|
200
|
-
A package *denylist* isn't expressible either: pi has only `-e <path>` (add one)
|
|
201
|
-
and `--no-extensions` (all off) — there is no "load all except X" flag. Naming
|
|
202
|
-
what you want is the only mechanism that excludes anything, and it excludes
|
|
203
|
-
future offenders too, with no name to keep updated.
|
|
204
|
-
|
|
205
|
-
### Known incompatibility: `pi-patty-bg-tasks`
|
|
206
|
-
|
|
207
|
-
**`pi-patty-bg-tasks` (tested at 1.1.6) is incompatible with subagents and must
|
|
208
|
-
not be loaded into a child.** It is the package that produced the failure above:
|
|
209
|
-
it replaces builtin `bash` and spawns `detached` + `proc.unref()`
|
|
210
|
-
(`src/spawn.ts`), which in print mode drains the event loop mid-turn. Bisected
|
|
211
|
-
against all 18 installed packages — alone it reproduces; every other package
|
|
212
|
-
alone is fine.
|
|
213
|
-
|
|
214
|
-
The default configuration already excludes it, structurally, because it backs no
|
|
215
|
-
requested tool. You only re-expose it by setting `inheritExtensions: true`, or by
|
|
216
|
-
mapping a tool to it in `toolExtensions`. Don't.
|
|
217
28
|
|
|
218
|
-
|
|
219
|
-
it, keep foreground subprocesses referenced until the tool promise settles, and
|
|
220
|
-
put genuinely detached work behind a separate background-task tool.
|
|
29
|
+
Linux sandboxing uses `bubblewrap` when available, for example from `sudo apt-get install bubblewrap`.
|
|
221
30
|
|
|
222
|
-
##
|
|
31
|
+
## When To Use
|
|
223
32
|
|
|
224
|
-
|
|
33
|
+
Use this package for independent coding, review, research, or verification work that can finish later. Do not use it for steps that need immediate foreground interaction or user clarification.
|
|
225
34
|
|
|
226
|
-
|
|
227
|
-
with elapsed time, the current tool, and running token/cost spend, ticking once
|
|
228
|
-
a second. It clears itself when the last run finishes. (TUI/RPC only; silent in
|
|
229
|
-
`-p`/print mode.)
|
|
230
|
-
- **On demand** — `subagent_list`, `subagent_output`, and `subagent_result` all
|
|
231
|
-
carry `elapsed · N tok (↑in ↓out) · $cost · tools`. The completion notice and
|
|
232
|
-
toast include the final elapsed + spend too.
|
|
233
|
-
- **Folded result display.** In interactive TUI sessions, `subagent_result`
|
|
234
|
-
renders a compact preview by default so long child answers do not flood the
|
|
235
|
-
transcript. Clicking the tool row, or using the row expand action, shows the
|
|
236
|
-
full bounded result. This is a display concern only: the tool still returns
|
|
237
|
-
the complete bounded `content` payload to the model.
|
|
35
|
+
## Compatibility
|
|
238
36
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
37
|
+
| Requirement | Support |
|
|
38
|
+
|-------------|---------|
|
|
39
|
+
| Pi | Required |
|
|
40
|
+
| Install method | `pi install npm:pi-better-subagents` |
|
|
41
|
+
| macOS sandboxing | Supported by default |
|
|
42
|
+
| Linux sandboxing | Uses `bubblewrap` when available |
|
|
43
|
+
| Development runtime | Node.js 22+ |
|
|
242
44
|
|
|
243
|
-
##
|
|
45
|
+
## Update Or Remove
|
|
244
46
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
opens in the overlay. Print/RPC modes do not install the navigator; tool access
|
|
249
|
-
is unchanged in every mode.
|
|
250
|
-
|
|
251
|
-
### Open
|
|
252
|
-
|
|
253
|
-
- With the editor **empty** and at least one non-dismissed current-parent run
|
|
254
|
-
still running, press `←` to focus the main-window subagent list above the
|
|
255
|
-
input line.
|
|
256
|
-
- If the editor contains text, `←` keeps normal cursor-left behavior.
|
|
257
|
-
- While running runs exist, the default footer shows `← subagents · N`. The
|
|
258
|
-
live widget also includes a secondary `← to navigate` hint on its title line
|
|
259
|
-
for terminals that do not render the default footer status. The hint clears
|
|
260
|
-
when no non-dismissed current-parent run is still running.
|
|
261
|
-
- While the main-window list is focused, the title hint changes to
|
|
262
|
-
`Enter to view · x to stop`; the selected row is marked with `›`. Press
|
|
263
|
-
`↓` from the bottom row to return to the input line.
|
|
264
|
-
- `↑` moves to the previous row when multiple running rows are shown. `↓`
|
|
265
|
-
moves toward the input line, returning to normal input from the bottom row.
|
|
266
|
-
- `Enter` opens the selected run's live detail view. `x` stops the selected
|
|
267
|
-
running run using shared `subagent_stop` semantics and dismisses it from the
|
|
268
|
-
navigator.
|
|
269
|
-
|
|
270
|
-
### Detail view
|
|
271
|
-
|
|
272
|
-
- Detail uses the same command-sheet treatment, with section rules for the
|
|
273
|
-
inspector groups and command bars at the top and bottom of the view.
|
|
274
|
-
- Shows status (colorized), model/effort, elapsed, tools, spend, and parsed
|
|
275
|
-
output, plus sectioned health: process identity/liveness, activity,
|
|
276
|
-
compaction, active tool, model call/error, last log write, thresholds, and
|
|
277
|
-
callback notification timestamps. Compaction, active tool, and model state
|
|
278
|
-
are separate sections. The view refreshes about once per second while open.
|
|
279
|
-
- Detail opened from the main-window list closes back to the main page; the
|
|
280
|
-
main-window selection remains on the viewed run when it is still visible.
|
|
281
|
-
- `x` arms Stop for a running run, or Dismiss for a terminal run.
|
|
282
|
-
- `esc` closes the detail view and returns to the main page.
|
|
283
|
-
|
|
284
|
-
### Two-press `x` stop/dismiss
|
|
285
|
-
|
|
286
|
-
- First `x` on the selected (list) or viewed (detail) run arms the action for three
|
|
287
|
-
seconds and shows a footer hint: `x again to stop <name>` while running, or
|
|
288
|
-
`x again to dismiss <name>` when terminal.
|
|
289
|
-
- Second `x` within the window, on the **same** run, acts:
|
|
290
|
-
- **Running** — stop the process group (shared `subagent_stop` semantics),
|
|
291
|
-
mark killed, then dismiss from the navigator.
|
|
292
|
-
- **Terminal** — dismiss only; terminal status is not rewritten.
|
|
293
|
-
- Changing selection, leaving the view, closing the overlay, arming timeout,
|
|
294
|
-
reload, and session teardown all disarm close and clear the confirm hint.
|
|
295
|
-
|
|
296
|
-
### Dismissal is navigator-only
|
|
297
|
-
|
|
298
|
-
Dismissed runs leave the navigator list and footer count. Logs, prompt, session
|
|
299
|
-
data, metadata, and id-based tool access stay intact. `subagent_list`,
|
|
300
|
-
`subagent_output`, `subagent_result`, and `subagent_stop` still resolve dismissed
|
|
301
|
-
run ids. Dismissal survives `/reload` as dismissed in the navigator.
|
|
302
|
-
|
|
303
|
-
### Reload and teardown
|
|
304
|
-
|
|
305
|
-
`/reload` and session restart reinstall the empty-editor wrapper without stacking
|
|
306
|
-
duplicate handlers, republish the footer count, and clear any leftover overlay
|
|
307
|
-
timers or close-confirm state. Session shutdown disposes open navigator timers
|
|
308
|
-
and clears navigator footer statuses (TUI only).
|
|
309
|
-
|
|
310
|
-
## Design notes
|
|
311
|
-
|
|
312
|
-
- Runtime lives outside any repo, under `$TMPDIR/pi-better-subagents/`
|
|
313
|
-
(`runs/<id>/` holds `output.log`, `prompt.md`, `meta.json`; `sessions/` holds
|
|
314
|
-
child session state). The `meta.json` sidecar is authoritative, so `list` /
|
|
315
|
-
`output` / `result` survive turns, `/reload`, and pi restarts.
|
|
316
|
-
- The child runs `--mode json`; `subagent_result` / `subagent_output` **parse**
|
|
317
|
-
the event stream and return just the final answer (plus which tools ran).
|
|
318
|
-
Non-JSON banner/warning lines fail to parse and are dropped, so the result is
|
|
319
|
-
clean. The prompt is passed as a **positional argument**, never `@file` — some
|
|
320
|
-
models refuse an @-attached file as untrusted content.
|
|
321
|
-
- The child gets **only** the explicit prompt — no silent parent-context bleed.
|
|
322
|
-
- `--approve` is **off by default** (headless runs can't prompt for trust).
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
## Parent-process scoping
|
|
326
|
-
|
|
327
|
-
The live widget, default `subagent_list`, concurrency cap, and `session_start` ticker only include runs this pi process spawned (`spawnPid === process.pid`). The on-disk registry stays machine-global for durability. `subagent_list` shows newest runs first and is capped at 20 rows by default; pass `limit:N` to request fewer or more rows, up to the documented maximum of 100 (larger values are clamped with a clear note). Pass `all:true` for a global view; it still respects the default or explicit limit. Pass `status:[...]` to filter by effective status: `running`, `completed`, `failed`, `killed`, transient `exited`, or durable `orphaned` / `lost`. Id-based `subagent_result` / `subagent_output` / `subagent_stop` still resolve any run id (cross-session recovery).
|
|
328
|
-
|
|
329
|
-
## Supervision health (`orphaned` / `lost`)
|
|
330
|
-
|
|
331
|
-
While a current-parent run is `running` or `orphaned`, a periodic health tick
|
|
332
|
-
reconciles process-group evidence only (see `docs/adr/0002-process-group-only-subagent-health.md`):
|
|
333
|
-
|
|
334
|
-
- **`orphaned`** — direct supervision of the child is broken, but related
|
|
335
|
-
process-group work may still be alive. Non-terminal and non-final; operationally
|
|
336
|
-
unhealthy immediately. The coordinator (when `callback:true`) gets one durable
|
|
337
|
-
ATTENTION follow-up naming `subagent_result` / `subagent_output` / `subagent_stop`
|
|
338
|
-
so it can inspect artifacts and decide whether to wait, stop, or retry. Human
|
|
339
|
-
`ui.notify` still fires when `callback:false`.
|
|
340
|
-
- **`lost`** — no related process remains and no coherent terminal completion was
|
|
341
|
-
observed. Terminal with unknown outcome (not the same as `failed`). Same one-shot
|
|
342
|
-
ATTENTION follow-up + diagnostic `subagent_result` path with best-available
|
|
343
|
-
artifacts.
|
|
344
|
-
|
|
345
|
-
Callbacks use the same non-interrupting `{ deliverAs: "followUp", triggerTurn: true }`
|
|
346
|
-
mechanics as completion, with distinct ATTENTION wording. Per-status markers on
|
|
347
|
-
`meta.json` (`orphanedCallbackSentAt` / `lostCallbackSentAt`) are written only after
|
|
348
|
-
a successful handoff and dedupe across reloads and repeated health ticks. Persisted
|
|
349
|
-
unmarked orphaned/lost states are recovered on the health ticker after `/reload`
|
|
350
|
-
even when process evidence does not produce a fresh transition.
|
|
351
|
-
|
|
352
|
-
### Surfacing health (tools + passive widget)
|
|
353
|
-
|
|
354
|
-
Multi-dimensional observations (`stale`, long tool, compacting / long compaction,
|
|
355
|
-
model error/retry, plus process `orphaned` / `lost`) are computed from durable
|
|
356
|
-
status + child-event evidence and surfaced on existing paths without a parallel
|
|
357
|
-
health model:
|
|
358
|
-
|
|
359
|
-
- **`subagent_list`** — durable `orphaned` / `lost` status brackets; degraded
|
|
360
|
-
compact facts only when actionable. Healthy/quiet rows stay on the compact format.
|
|
361
|
-
- **`subagent_output` / `subagent_result`** — `[health: …]` diagnostics for
|
|
362
|
-
orphaned, lost, and degraded running runs; #65 orphaned/lost result bodies kept.
|
|
363
|
-
- **Passive live widget** — healthy/quiet unchanged; degraded (and orphaned) may
|
|
364
|
-
show a short suffix. Still `setWidget` only — never focusable.
|
|
365
|
-
- **`callback:false`** suppresses coordinator follow-up only; human `ui.notify` and
|
|
366
|
-
TUI/passive visibility remain.
|
|
367
|
-
|
|
368
|
-
## Install
|
|
369
|
-
|
|
370
|
-
Linux sandboxing requires the system `bubblewrap` package. Install it before
|
|
371
|
-
launching sandboxed children:
|
|
372
|
-
|
|
373
|
-
```bash
|
|
374
|
-
# Debian/Ubuntu
|
|
375
|
-
sudo apt-get install bubblewrap
|
|
376
|
-
# Fedora/RHEL
|
|
377
|
-
sudo dnf install bubblewrap
|
|
378
|
-
# Arch Linux
|
|
379
|
-
sudo pacman -S bubblewrap
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
When `bwrap` is absent, explicit sandbox requests fail with an installation hint;
|
|
383
|
-
default-on sandboxing preserves the documented direct-execution fallback. Once
|
|
384
|
-
`bwrap` is selected, a launch failure fails closed rather than retrying the child
|
|
385
|
-
without confinement.
|
|
386
|
-
|
|
387
|
-
Symlink the project into pi's auto-discovered extensions dir:
|
|
388
|
-
|
|
389
|
-
```bash
|
|
390
|
-
ln -sfn "$PWD" ~/.pi/agent/extensions/pi-better-subagents
|
|
47
|
+
```sh
|
|
48
|
+
pi update npm:pi-better-subagents
|
|
49
|
+
pi remove npm:pi-better-subagents
|
|
391
50
|
```
|
|
392
51
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
> Do **not** add it to `settings.json`'s `extensions` array — a live pi session
|
|
396
|
-
> rewrites that file on its own saves and drops hand-added entries. Auto-discovery
|
|
397
|
-
> via the symlink is stable. Quick throwaway test without installing:
|
|
398
|
-
> `pi -e ./index.ts`.
|
|
399
|
-
|
|
400
|
-
## Tests
|
|
401
|
-
|
|
402
|
-
Unit tests (`node --test tests/*.test.mjs`) cover the pure logic — widget
|
|
403
|
-
rendering, completion delivery, and extension resolution.
|
|
404
|
-
|
|
405
|
-
Real integration smoke tests live in [`tests/`](tests/) — a subagent using
|
|
406
|
-
`web_fetch`, one driving `gh`, env inheritance through the sandbox, and headless
|
|
407
|
-
isolation surviving a parallel `bash` + `read` batch. See
|
|
408
|
-
[`tests/README.md`](tests/README.md).
|
|
409
|
-
|
|
410
|
-
## Roadmap
|
|
411
|
-
|
|
412
|
-
Tracked in [issues](https://github.com/1aboveio/pi-better-subagents/issues).
|
|
413
|
-
Near-term:
|
|
52
|
+
## More Detail
|
|
414
53
|
|
|
415
|
-
-
|
|
416
|
-
|
|
417
|
-
-
|
|
418
|
-
twice ([#2](https://github.com/1aboveio/pi-better-subagents/issues/2)) — **done**.
|
|
419
|
-
- Named agent-definition files (per-agent system prompt + tool allowlist) and
|
|
420
|
-
chain/parallel orchestration.
|
|
54
|
+
- Repository: https://github.com/1aboveio/pi-better-harness
|
|
55
|
+
- Detailed notes: https://github.com/1aboveio/pi-better-harness/blob/main/packages/pi-better-subagents/docs/usage.md
|
|
56
|
+
- License: https://github.com/1aboveio/pi-better-harness/blob/main/LICENSE
|
package/package.json
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-subagents",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Pi extension for detached, sandboxed subagent runs that keep the foreground session free.",
|
|
5
|
+
"license": "MIT",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"keywords": [
|
|
7
8
|
"pi-package",
|
|
8
9
|
"pi-extension",
|
|
9
|
-
"
|
|
10
|
+
"pi",
|
|
11
|
+
"subagents",
|
|
12
|
+
"sandboxed-agents"
|
|
10
13
|
],
|
|
11
14
|
"pi": {
|
|
12
|
-
"extensions": [
|
|
15
|
+
"extensions": [
|
|
16
|
+
"./index.ts"
|
|
17
|
+
],
|
|
13
18
|
"image": "https://raw.githubusercontent.com/1aboveio/pi-better-harness/main/docs/images/package-gallery/pi-better-subagents.png"
|
|
14
19
|
},
|
|
15
20
|
"repository": {
|