@nicknisi/pi-subagents
First-party subagent dispatch and fleet: fan out parallel child agents and inspect their runs — no pi-subagents dependency
Package details
Install @nicknisi/pi-subagents from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@nicknisi/pi-subagents- Package
@nicknisi/pi-subagents- Version
0.2.1- Published
- Aug 9, 2026
- Downloads
- 190/mo · 190/wk
- Author
- nicknisi
- License
- MIT
- Types
- extension
- Size
- 161.1 KB
- Dependencies
- 2 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@nicknisi/pi-subagents
First-party subagent dispatch and fleet for pi — fan out parallel child agents and inspect their runs, with no dependency on pi-subagents. Children are hermetic in-process agent sessions spawned through @nicknisi/pi-shared's runtime (createAgentSession under the hood): fast to start, version-matched to the running pi, and unable to spawn children of their own.
What it adds
dispatchtool (model-facing) — fan out up to 8 child agents in parallel. Each task gets its own prompt, optional label/model/system-prompt, and a tool allowlist (default read-only:read,grep,find,ls). Typed per-task results aggregate into one tool result.background: trueruns detached and surfaces completion via a transcript message. Tasks whose allowlist includesedit,write, orbashmutate the shared working tree: they must declareallowTreeMutation: true(otherwise that task is refused) and always run sequentially, one at a time, after the parallel read-only batch completes — never concurrently with each other or the read-only batch.fleettool (model-facing) —listrecent runs (live + persisted, across extensions using the shared runtime) or fetch aresultby runId. This is how the model checks on background dispatches./fleetcommand — user-facing run table.- Fleet radar overlay + statusline —
Alt+Ctrl+F(rebind via~/.pi/agent/keybindings.json) opens a tmux-choose-tree-style overlay listing every run as a per-child lane: status, model, current tool, token burn, last activity.Enterinspects the live run transcript;ccancels the focused run (wired into the cascading-cancellation registry);Esccloses. While any run is in flight, an ambient footer segment (ctx.ui.setStatus) shows liveworking · done · failedcounts. /patchescommand — staging area for worktree-subagent.patchhandoffs. Opens a keyboard-driven overlay over every pending patch with diffstat and a pre-flight stamp (clean/conflicts/stale, checked viagit apply --checkwithout applying).Enterapplies the whole patch (git apply --3way);eexpands the full diff with per-hunk navigation (n/p);sapplies the focused hunk;ddiscards. Apply/discard decisions persist to~/.pi/agent/subagent-patches/state.jsonso/patchessurvives restart.&dispatch prefix —&scout how does auth workat position zero dispatches a single subagent inline (reusing the same spawn/cancel path as thedispatchtool). Live progress shows in a widget above the editor; the final result lands as a collapsiblesubagents:inlinetranscript block rendered with the same vocabulary as a dispatch tool result, and the answer reaches the model's context. Each dispatch is captured as a session custom entry so it survives restart./again [amendment]command — re-fires the last&dispatch verbatim, or with the amendment appended.
Usage
Ask naturally:
Dispatch three reviewers: one for correctness, one for tests, one for unnecessary complexity.
Dispatch a background scout to map the auth module while we keep working.
Check the fleet for that background run's result.
or directly:
/fleet
Inline & dispatch
Prefix a prompt with & to dispatch a single subagent inline — the run reuses the same spawn/cancel path as the dispatch tool, but is driven from the editor instead of the model:
&scout how does auth work
Live progress shows in a widget above the editor; when the child settles, the result lands in the transcript as a collapsible block (same vocabulary as a dispatch tool result) and the answer is added to the model's context. The dispatch is recorded as a session entry, so /again can re-fire it:
/again
/again focus only on the JWT path
/again re-fires the last & dispatch verbatim, or with an amendment appended. Cancellation: an inline run is registered in the cascading-cancellation registry, so it is aborted on session shutdown and can be cancelled mid-flight from the fleet radar (Alt+Ctrl+F → c) or the fleet tool — it is not aborted by a bare Esc (the input event fires while idle, so no agent abort signal is available to thread in).
Worktree isolation
Builder tasks should prefer worktree: true over allowTreeMutation: true:
{
"tasks": [
{
"task": "Implement the parser in packages/foo",
"tools": ["read", "edit", "write", "bash", "grep"],
"worktree": true
}
]
}
The child runs in a detached worktree at ~/.pi/agent/subagent-worktrees/<runId> from current HEAD. Its writes never touch your working tree, mutating tools stay parallel (no allowTreeMutation, no serialization), and on completion the full change set — including new untracked files — is captured as an untruncated patch at ~/.pi/agent/subagent-runs/subagents/<runId>.patch. Integration is your call (the central-integrator pattern): inspect the patch, git apply what you want. fleet action: 'result' shows the worktree path, patch path, and changed-file count. Fails fast if the cwd isn't a git repo.
/patches staging area
Instead of hunting for .patch files by hand, run /patches. It scans every completed worktree run's patch alongside its run artifact, pre-flights each one without applying (git apply --check), and stamps it:
clean— applies cleanly to the current tree.conflicts—--checkfails; the context no longer matches (inspect before applying).stale— a modified (non-created) target file no longer exists in the working tree.
Keys in the overlay: ↑↓ select, Enter applies the whole patch (git apply --3way), e expands the full diff with per-hunk navigation, s applies the focused hunk, d discards, Esc closes. Apply/discard decisions persist to ~/.pi/agent/subagent-patches/state.json, so already-applied or discarded patches don't re-appear after restart.
Cuts (honest): s applies a single focused hunk (reconstructed as a sub-patch with its file header and git apply --3way-ed); multi-hunk selection is not implemented. The full-diff view is capped at 2000 lines (truncated with a marker) so an enormous patch can't swamp the overlay. The pre-flight stamp is a heuristic: stale vs conflicts is decided by whether a modified target file still exists, not by a true base-commit comparison (the run record doesn't store the base commit).
Worktree cleanup policy
- Completed / failed / empty / schema-invalid runs (the child finished): the worktree and its
.patchare kept side-by-side under~/.pi/agent/subagent-runs/subagents/for a 7-day inspection window, then removed together by the startup GC sweep (the.jsonartifact, the.patch, and the worktree itself). This is the "kept until artifact GC" window the fleet result view refers to. - Aborted runs (Esc/interrupt,
fleetcancel, or host shutdown): the child did not complete, so no.patchis captured and the worktree is removed immediately — an interrupt or parent exit can never leak a detached worktree. The worktree path is dropped from the run record, so/fleetnever advertises a path that no longer exists. - Hard exit (SIGKILL, crash, power loss) that skips the graceful shutdown handler: any
running/queuedrecord whosehostPidis no longer alive is reaped asabortedon the next host startup, and its worktree is removed at reap time — so even an ungraceful kill cannot strand a worktree long-term (the 7-day age GC is the final backstop). - Unclaimed
.patchfiles: a.patchonly exists for a finished run that changed files, and is always a sibling of its<runId>.jsonartifact. It is removed exactly when its artifact is removed — either by the 7-day age GC, or never (if the artifact is still fresh). There is no code path that deletes an artifact but leaves its patch behind, and no code path that writes a patch without an artifact.
Cascading cancellation
Esc/interrupt deterministically kills all running children — foreground and background alike:
- Foreground tasks are aborted through the tool
signalpi passes todispatch'sexecute(); the abort propagates to the childAgentSession.abort(). - Background tasks deliberately carry no tool signal (pi aborts tool signals once
execute()returns, which would kill them prematurely), so they are tracked in a single liverunId → AbortControllerregistry. Asession_shutdownhandler (quit / reload //new//resume//fork) walks that registry and aborts every active controller, so quitting pi or replacing the session cannot orphan a background run. - The
fleettool'scancelaction aborts a single run by runId prefix. - Worktree runs that are aborted tear their worktree down immediately (see the cleanup policy above); completed runs keep theirs for the inspection window.
The one residual exposure is a hard SIGKILL of the host: in-process children die instantly (they share the host's event loop), but their worktrees and running records linger until the next host startup reaps them. There is no way to intercept SIGKILL from an extension; the startup reap + 7-day GC are the backstop.
How it works
dispatch maps each task to a spawn() call on a shared in-process runtime (namespace: "subagents"). Foreground tasks run concurrently (the runtime caps parallelism, default 4) and their results return as the tool output. Background tasks use spawnDetached() and report completion via a transcript message.
Every run persists a record to ~/.pi/agent/subagent-runs/subagents/<runId>.json (status, timing, usage, bounded output). Because pi isolates module state per extension, this directory is the cross-extension fleet view: any extension using @nicknisi/pi-shared with the same artifacts root shows up in /fleet.
Standard pi session mirror
Every dispatch run is also dual-written as a standard pi session JSONL via pi's real SessionManager (from @earendil-works/pi-coding-agent), into the default sessions dir (~/.pi/agent/sessions/<encoded-cwd>/), with the session header's parentSession set to the owning pi session's file path (read from ctx.sessionManager.getSessionFile() inside the dispatch tool). This means a subagent run shows up in pi's native /resume list, can be inspected with /tree, and can be branched/forked with --fork — exactly like a session you drove yourself. The fleet tool's result view prints the mirror path (session: <path> (pi /resume, /tree, --fork)).
This is additive dual-write, not a replacement: the bespoke .json run store above is unchanged, and the fleet/registry still read it (it carries bounded output, transcripts, worktree/patch info, and the cross-extension fleet view that the sessions dir doesn't encode). The session mirror carries the full message transcript instead.
Compat caveats:
- The mirror is written only when the owning pi session is persisted (i.e.
getSessionFile()returns a path). Inpi -pprint mode or other in-memory hosts there is no owning session file, so no mirror is written — the bespoke.jsonrecord is still the source of truth. SessionManagercreates the JSONL lazily — only once the first assistant message is appended. A run that crashed before producing any assistant turn (kindcrashed/emptywith no assistant message) leaves no session file on disk;record.sessionFileis left undefined in that case rather than advertising a path to nothing.- The mirror reflects the child's messages as pi sees them (user prompt → assistant turns → tool results). It does not carry the subagent-specific metadata (runId, namespace, transcript summary, worktree/patch paths) — that lives only on the bespoke
.jsonartifact, which is why both are kept. - Worktree-isolated runs mirror with the worktree's cwd (where the child actually ran), not the caller's cwd. The session header's
cwdis honest about where the work happened.
Children are hermetic by construction: no user extensions, skills, prompt templates, themes, or AGENTS.md context load unless explicitly requested. Tool scoping is likewise by construction — a child receives exactly its allowlist, and no spawn capability exists as a tool, so children cannot recurse. The ecosystem recursion guard (PI_SUBAGENT_DEPTH / PI_SUBAGENT_CHILD) is honored: inside a pi-subagents child, spawns are refused.
spawn() never rejects; results are a discriminated union (ok | crashed | empty | schema_invalid | aborted). See packages/shared/README.md for the full runtime API.
Configuration
None. No config files, no environment variables.
Caveats
- In-process means no crash isolation. Children share the parent session's event loop and memory; a pathological child can hurt the host. Untrusted or heavy parallel work should stay on pi-subagents (or a future RPC transport) until this platform grows an isolation option.
- Background completion is a notification, not a turn. The completion message lands in the transcript but doesn't drive the agent — the model learns results when it next acts (or when asked to check
fleet). - The fleet is per-machine, per-agent-dir. Records live under
~/.pi/agent/subagent-runs/and are garbage-collected at startup after 7 days (along with their patches and worktrees). - Background runs live only as long as the host session. They are detached in-process children; cancel them via the
fleettool (action: 'cancel'), or let thesession_shutdownhandler abort them deterministically on quit/reload/session-replacement (see Cascading cancellation above). Running records left by a hard exit are reaped asaborted(and their worktrees removed) on the next host startup. - Depends on pi SDK internals (
createAgentSession,DefaultResourceLoaderflags,SessionManager.inMemory) that could change across pi versions — runtime-aliased to the host at load time, but type-level drift would surface at extension load. - The recursion guard has a hole. The in-process depth guard only covers spawns made through the shared runtime; a child that itself shells out to
pi -pvia bash starts a fresh process with none of that context — the same exposure as any pi session with bash access.