@primp9053/pi-subagent
v0.2.8
Published
Concurrent sub-agents for pi. /sub <task> opens a fork-point selector so you can pick any user message as the sub's inherited context (Enter = latest); /subn <task> spawns from latest without the selector; status line shows main/sub1/sub2; alt+s cycles su
Readme
pi-subagent
Concurrent sub-agents for pi. Fork a background agent from your current session that runs without blocking the main agent — keep working in the main thread while subs run in parallel.
Why
pi's built-in /fork and /clone tear down the current session and switch
to the new one — the main agent stops. If you want to spawn an agent to handle a
side task while the main agent keeps working, there is no native path. This
extension adds one.
Verified by a concurrency PoC: two independent AgentSession instances created
via the public createAgentSession API run interleaved in a single process
(event timestamps overlap, both use tools, both complete, no provider conflicts).
The agent loop is per-instance and the provider registry is a shared read-only
route table — so multiple concurrent agents are safe.
Commands
/sub <task> Spawn a sub-agent. Pops a fork-point selector listing all
your user messages (latest first) so you can choose how much
history the sub inherits — pick any user message as the
sub's context. Enter = latest (full history, the default);
Esc cancels the spawn. Optional "name:<word> " prefix.
/subn <task> Fast path: spawn from the current latest state WITHOUT the
picker (the pre-picker behavior). Full current history.
/sub list Show all sub-agents and their status.
/sub view <id|name> Show a sub-agent's captured output (event log + last message).
While the main agent is streaming, the output appears IMMEDIATELY as a
widget above the editor (pi has no API to inject a chat block
mid-stream); the full chat block is queued and appears after the
current main turn ends (deliverAs: followUp). When idle, the block
appears in chat immediately.
/sub send <id|name> <msg> Continue interacting with an existing sub-agent. If the sub is still
running, the message is QUEUED (deliverAs: followUp) and processed
after the current turn ends — not rejected. The sub's AgentSession
accumulates messages, so it retains its prior conversation context
and continues on it (the model remembers the history).
/sub cancel <id|name|all> Abort + dispose a sub-agent (or all).The fork-point selector is a flat list of your user messages (pi's tree view is
an internal TUI component not exposed to extensions; the flat list is the
extension-accessible equivalent and matches what pi's own /fork shows).
Folding sub output
Two ways to fold a sub's output block:
alt+g(pi-subagent, in-place) — folds/unfolds ONLY the currently-viewed sub-output block. This is the fast path: it toggles just that one component and repaints the screen via a no-opsetToolsExpanded(currentValue)(which skips every message's rebuild because the value is unchanged, then callsui.requestRender()). Use/sub view <id>first so the block is on screen.Ctrl+O(pi global) — folds/unfolds ALL expandable messages at once. On long sessions this re-parses Markdown for every tool/custom block, which can look like "the whole TUI reloaded". Preferalt+gto fold just the sub block without touching the rest.
The terminal pi TUI does not support mouse-click on UI components, so folding is keyboard-driven, not click-to-fold.
Status line
While subs exist, ctx.ui.setStatus("pi-subagent", ...) renders a line like:
main · sub1▶3t · sub2✓(▶ running with tool count, ✓ done, ✗ error, ⊘ cancelled.)
Keyboard
alt+s — cycle which sub's output is shown (replays its log into the main chat
as a custom message). No subs → notifies "no sub-agents".
How it works
inputevent hook intercepts/sub ...and returns{ action: "handled" }so the command never reaches the main agent.- Spawning:
createAgentSession({ cwd, modelRegistry: ctx.modelRegistry (reused), model, sessionManager: branchedSession, tools }). The branched session file is built from the main session's current leaf viacreateBranchedSession(leafId)(same mechanism as/clone), so the sub inherits the full current history. - Background run:
sub.session.prompt(task)is fired without awaiting, so the main session's loop continues uninterrupted. Sub events are captured viasub.session.subscribe(...)to an in-memory log. - Status:
ctx.ui.setStatusupdates the footer. - Cancel:
sub.session.abort()+dispose(). - Cleanup: on
session_shutdown, all subs are aborted + disposed.
Known limitations (honest)
- Sub live streaming output is not in the main chat's streaming slot. The
main TUI chat is bound to the main session. Sub output is captured to an event
log and replayed on demand (or on completion) as a custom message
(
customType: "pi-subagent-output") viapi.sendMessage(display:true)+ a registered message renderer. - Keyboard "switching" switches which sub's log is shown, not the native chat view. The main chat always reflects the main session.
- A sub shares the main session's cwd, model, and a fixed tool set
(
read, bash, edit, write). It writes its own session file under~/.pi/agent/sessions/<encoded-cwd>/and is later resumable via pi's normal/resume. - Sub-agents are in-memory only for this extension's tracking (the underlying
session file persists, but the live
subsMap does not survive a pi restart).
Install
pi install npm:@primp9053/pi-subagentProject-local: pi install ./pi-subagent -l. Restart pi to load (ESM static
imports bind at process start; /reload does not reload extension code).
Compatibility
pi 0.80.x. Requires @earendil-works/pi-coding-agent and @earendil-works/pi-ai
as peer dependencies (present in any pi install).
License
MIT
