@nijaru/pi-subagents
Task-first child delegation for Pi over the public RPC protocol, with background completion, steering, persistence, and bounded lifecycle.
Package details
Install @nijaru/pi-subagents from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@nijaru/pi-subagents- Package
@nijaru/pi-subagents- Version
0.0.20- Published
- Nov 4, 2026
- Downloads
- 2,374/mo · 297/wk
- Author
- nijaru
- License
- MIT
- Types
- extension, skill
- Size
- 181.2 KB
- Dependencies
- 0 dependencies · 4 peers
Pi manifest JSON
{
"skills": [
"./skills"
],
"extensions": [
"./extensions/pi-subagents/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-subagents
Task-first delegation for Pi. A parent session starts a fresh Pi child over the public RPC protocol, optionally joins it, and keeps working while it runs.
The child is a real pi --mode rpc process with its own session, tools, model, extensions, and working directory. The extension does not load Pi's SDK internals or ship a second runtime.
Install
pi install npm:@nijaru/pi-subagents
Requires Pi 1.3.0+ from a Node/npm installation (with the pi CLI) and Node.js 22.19+. Standalone Pi binaries are not supported. Restart Pi or use /reload.
Tool
One tool, subagent, with six commands:
| Command | Purpose |
|---|---|
run |
Start a child and join it for up to joinTimeoutMs (60 seconds by default). If the join expires, the child continues in the background. |
spawn |
Start a child and return its handle immediately. |
wait |
Join one or more children. Wakes when any selected child finishes. |
status |
Inspect one child or list retained children. Does not consume results. |
stop |
Cancel a child and join its cleanup. |
steer |
Queue a steering message into a running child. Delivered after its current assistant turn and tool calls. |
Every child gets a short session-scoped handle (sa-<n>). wait, status, stop, and steer accept the handle or the full UUID.
{"command":"run","prompt":"Review the parser changes. Report concrete regressions with file/line and evidence. Do not edit files.","tools":["read"]}
{"command":"spawn","prompt":"Implement the parser regression test. Own only tests/parser.test.ts and run it.","tools":["read","edit","bash"],"persist":true}
{"command":"steer","id":"sa-4821","message":"Also check the empty-input case."}
{"command":"wait","ids":["sa-4821"]}
run is the synchronous convenience: it starts and joins. spawn is the background form. A run whose join expires returns a running handle; it does not stop the child. Cancelling a run call cancels its child and joins cleanup, even beyond the join budget; cancelling a wait leaves children running.
Task prompts are wrapped in a Task: envelope and steering messages in a Steering instruction: envelope before they reach RPC. This prevents RPC from interpreting a leading /command, /skill:..., or /template as a child command. The task content itself is otherwise unchanged.
Completion delivery
Unread completions request a handle announcement and continuation at the next successful active boundary. The next top-level non-subagent tool result also advertises unread handles. Each child gets at most one boundary announcement attempt per parent run; a later extension may discard the draft. An idle parent is never woken.
Notices contain handles, not reports, and never consume them. Use wait to retrieve reports after a notice. A completed run, wait, or stop marks its returned terminal reports collected locally; status does not. Collected reports become eligible for clearing or eviction. Collection does not confirm that a later tool-result transformation preserved the report, that it reached model context, or that the model read it. Unread results remain discoverable in the inbox even if a continuation was aborted or its notice was dropped.
An assistant error, an unsuccessful boundary, or an observed abort suppresses boundary announcements for the rest of that parent run, even if an automatic retry later succeeds. Explicit retrieval and inbox advertisements remain available. A new parent run rearms announcement attempts. The no-idle-wake policy is deliberate: Pi can trigger an idle turn, but does not expose a reliable late idle-abort signal to revoke that work safely.
/subagents lists retained children and unread results. /subagents clear drops completed, read handles.
Time budgets
| Budget | Default | Scope |
|---|---|---|
startupTimeoutMs |
30s (PI_SUBAGENT_STARTUP_TIMEOUT_MS) |
RPC process, model selection, tool probe, and readiness |
executionTimeoutMs |
Off; PI_SUBAGENT_TIMEOUT_MS opts in |
Task preflight and model/tool work, starting when the task is submitted |
joinTimeoutMs |
60s (PI_SUBAGENT_FOREGROUND_MS) |
How long one run blocks; at most 10m |
timeoutMs |
5m | How long one wait joins; never changes a child deadline |
Execution deadlines are off by default. Set executionTimeoutMs for a task-specific cutoff, or PI_SUBAGENT_TIMEOUT_MS for an opt-in global default; explicit arguments take precedence. Configured limits must be positive integer milliseconds, at most two hours. Invalid execution-limit environment values fail before task submission; unset the variable to disable that default. Without a limit, a child can keep running, spending, and occupying its admission slot until it finishes, is stopped, or its parent shuts down. Pi/provider request, connection, idle, and retry limits still apply.
joinTimeoutMs is a foreground join budget, not an execution deadline. It is capped at 10 minutes; an out-of-range PI_SUBAGENT_FOREGROUND_MS value falls back to the 60-second default.
status exposes lastActivityAt (parent-observed Unix milliseconds) and, while work is active, activity (model, thinking, text, tool, retry, or compaction) and currentTool (one observed tool-call name; calls can overlap). Model/token, tool, retry, and compaction lifecycle events refresh the timestamp, including thinking before visible text. retry means observed Pi retry backoff, not hidden provider-internal retries; compaction means observed summarization work. Metadata contains no reasoning text, tool arguments, or tool results. Current activity clears when execution stops; the last timestamp survives failure and cleanup. Successful reports omit activity timestamps; running and failed reports retain them. These are observations, not stall detection: a quiet tool or provider can still be working.
Interrupted execution also retains interruption: the observed activity, an activeToolCount of calls with a start but no observed end, and one currentTool name when available. This snapshot is frozen before cleanup; it contains no arguments or results. A start event does not prove a tool executed, and a missing end does not prove its effects failed. Reports warn to reconcile workspace/external state before retrying when this count is nonzero. The count and sampled name are not a complete side-effect ledger. Batch metadata is bounded too: details may omit a session path with omittedFields: ["sessionFile"], never return a truncated resume target. Inspect that child alone with status to retrieve it.
Every armed phase exposes its effective deadline: { phase, startedAt, expiresAt }, with Unix millisecond timestamps after defaults and environment settings are resolved. Startup protection and its metadata clear when a task is submitted without an execution limit. An explicitly configured execution deadline—or a failed-startup deadline—remains in terminal details; running and failed text reports show it too. Activity never extends the cutoff. The library does not automatically retry, extend the budget, or send a wrap-up prompt: queued steering cannot interrupt a long model request or tool call.
At startup/execution expiry the child is stopped and its result retains bounded visible assistant text across messages, including unfinished text. Completed text blocks replace their interim deltas even if the whole assistant message never finishes. Successful results retain completed assistant reports from that execution, including answers before and after steering or follow-ups; tool-use narration is excluded. The combined report is byte-bounded, with a retained prefix and explicit truncation metadata. Join/wait expiry leaves the child running. errorKind distinguishes startup, provider, protocol, deadline, cancelled, and cleanup; timeoutPhase names the expired deadline. Reported runtime excludes process-tree cleanup; cleanupMs records the excluded portion.
Child execution
- Public RPC boundary. The child is spawned as
pi --mode rpc; the parent speaks only the documented JSONL protocol. Nojiti, no SDK bootstrap, and no private Pi module imports. - Exact startup checks. After session restoration and extension startup, the parent explicitly sets the requested model, then effort, through RPC. Unsupported explicit effort fails before model work. A private readiness command and typed custom session entry verify the active tool set; a final
get_stateverifies the effective model and explicit effort before the task is submitted. The child probe also blocks calls outside the allowlist. Tool registration shares the caller's startup deadline, with no separate ten-second cap. Missing tools exhaust startup without submitting the task. - Task preflight. Pi can compact saved history and run extension hooks before acknowledging a task prompt. This work uses the optional execution deadline, not a separate 30-second control-acknowledgment cutoff.
- Full Pi configuration. The child loads its own settings, extensions, skills, MCP servers, and project instructions. A fresh conversation does not mean an empty system prompt.
- Isolation. Each child runs in its own detached process group. A detached watchdog terminates the group if the parent dies without running shutdown hooks. Normal completion closes child stdin and waits for orderly Pi shutdown before ending the watchdog and sweeping descendants; cancellation and timeouts stop the tree promptly. The concurrency slot is held through confirmed cleanup; an unconfirmed cleanup keeps it reserved.
- Steering. Steering messages use the RPC
steercommand and are delivered after the current assistant turn's tool calls. A call made during startup joins the same readiness outcome; it does not introduce another timeout. Cancelling the call stops waiting, not the child. Once sent, an instruction cannot be recalled through this tool. A cancelled or lost acknowledgment has unknown acceptance: the instruction may already have taken effect, so do not resend automatically. Queue acceptance does not mean delivery: a deadline can stop the child first, and undelivered steering is not restored by resuming a saved conversation. - Persistence. New children save their session by default;
persist: falsedisables retention. The session is stored under the agent directory (~/.pi/agent/subagents/sessions/<id>.jsonlby default) and returnssessionFile.resume: "sa-4821"continues a stopped child's saved conversation with a new handle and defaults to its originalcwd.resume: "<path>"supports recovery after handles are cleared or the parent restarts. Explicit paths require the session's originalcwd; a mismatched workspace or the active parent session is rejected before launch. Relative resume paths resolve against the parent cwd. The path appears in the model-facingrun/spawn/wait/statustext as well as details, including on failure. After the child stops, continue with a focused prompt. Resume follows normal launch rules for tools, model, effort, and trust; it does not inherit those options from the old handle.persistandresumecannot be supplied together. Inspect saved history and any external tool effects before resuming failed mutating work; a saved conversation does not roll those effects back. A tool can apply a mutation and be interrupted before its result is saved. Pi may supply a synthetic missing-result error on resume to satisfy provider message ordering; this does not establish whether the operation happened. Saved Pi conversations may contain sensitive prompts and tool results and are not full token traces; failed startup can return a path before any session file exists. - Trust. RPC mode cannot show Pi's project-trust prompt. Project-local resources are skipped unless a saved decision applies or the call sets
trustProject: true, which passes--approveto the child. Parent permission state is not inherited.
Children inherit the parent process environment, including credentials and tool configuration. Parent session-identity variables are removed and child identity is set explicitly. This is ordinary subprocess inheritance, not credential filtering.
The child is a full Pi process, so extensions and a child with shell access can produce effects outside the managed process group. Tool allowlists and subprocesses are not sandboxes.
Session ownership
Persisted and resumed sessions have an exclusive, cooperative <sessionFile>.subagent-lock held from before launch through cleanup. Competing subagent parents reject contention rather than queueing. These locks do not protect against an ordinary Pi process opening the same file; do not open a session elsewhere while a child owns it.
Normal completion, cancellation, and failed startup release ownership only after confirmed cleanup; leader exit alone is insufficient. A hard parent crash or unconfirmed cleanup leaves the lock in place. Confirmation covers the managed root group and captured descendants, not all possible external effects. If the leader exits before its descendant tree can be observed, cleanup is unconfirmed rather than assumed safe. Locks never expire automatically: verify that the old child has stopped before manually removing an orphaned lock. The lock records the parent PID and child run ID. This conservative recovery avoids allowing two writers while an old child is still alive.
Terminal reports expose cleanup.status: confirmed or unconfirmed. Unconfirmed cleanup retains the admission slot and any session lease, even after the report is collected. Such handles cannot be resumed, cleared, or evicted in that parent session. cleanup.taskOutcome preserves the outcome before cleanup; a completed task becomes a failed execution if cleanup cannot be confirmed, while timeout/provider causes remain primary. Reconcile process and workspace state before recovery; a new parent session clears handle reservations, not orphaned lock files.
Tools and context
Defaults are the parent's active tools among read, grep, find, ls, web_search, web_fetch, web_research, resolve-library-id, and query-docs. Mutation tools (bash, edit, write) must be requested explicitly. tools: [] is reasoning-only.
Explicit tools must be active or callable in the parent and are passed to the child as an exact allowlist. All unselected tools are also passed through --exclude-tools, so nested/codemode calls cannot reach them. The child probe must report the same active set before the task starts and blocks tool calls outside the allowlist, including nested executeTool() calls. Children are leaves: the subagent tool cannot be passed to a child, and nested calls are rejected.
model is an exact provider/model-id; otherwise the parent's model is inherited. thinking selects a reasoning effort supported by that model. An explicit unsupported level fails before prompting. A different model uses its own Pi defaults when thinking is omitted.
cwd defaults to the parent cwd, except handle-based resume uses the child's original cwd. A fresh child starts a new conversation; the prompt must carry scope, evidence, constraints, expected output, and checks. Parent history, runtime-only tools, and permission-hook state are not copied.
Limits
| Resource | Limit |
|---|---|
| Admission slots | 4 per parent session, including cleanup-unconfirmed children; excess starts are rejected |
| Retained handles | 32; oldest completed, read handles are evicted first; unread results are never evicted |
| Startup | 30 seconds by default, up to 5 minutes |
| Execution deadline | Off by default; optional configured cutoff up to 2 hours |
| One wait | 5 minutes by default, at most 10 |
| Task prompt | 100 KiB |
| Tool response and details | 50 KiB each |
| Child stderr | 50 KiB, keeping both ends |
Child reports remain bounded in memory; they are not spilled to disk. Text, structured results, and renderer details derive from the same budgeted report view. outputTruncation records shortened UTF-8 output. Launch metadata omitted for space is absent and listed in omittedFields, not replaced with empty values. Session paths are whole or absent; inspect one child with status for an omitted path. A persisted session is separate from that report.
Usage
Child usage includes assistant calls, completed compaction summaries, and usage reported by child tools. Interrupted calls retain the latest reported cumulative usage; updates are not added repeatedly. Providers that report usage only at finalization can leave interrupted usage unknown.
After cleanup, each child's usage is added once to the next parent tool result, including failed results. Repeated status, wait, or stop calls do not charge it again.
Pi cannot attach usage to a custom completion message. Background costs enter native totals on the next parent tool result; until then they remain visible in child details. Quit, reload, or session replacement discards unreported usage.
Parent shutdown
All children stop on quit, reload, or session replacement. Background work requires a live parent process. On POSIX, the parent-death watchdog enforces cleanup even after SIGKILL; Windows cancellation is force-only and has no equivalent job-object guarantee.
Development
bun install --frozen-lockfile
bun run check
Pi loads the TypeScript extension directly; there is no build step. Checks use a fake RPC child for deterministic lifecycle tests and a real Pi CLI with a local mock provider for the public-protocol integration test. No live model calls are needed.
RPC preserves the normal Pi CLI runtime while supporting steering and session continuation. Print/JSON mode is simpler for one-shot jobs but cannot steer a running task; an SDK worker would require a separate bootstrap, and an in-process session would share the parent's failure domain. Terminal attachment and independent background survival are outside this tool's scope.
Release notes
0.0.x is pre-release: tool schema and result shape are not promised across patch releases.
- 0.0.20:
- Execution deadlines are off by default. Set
executionTimeoutMsorPI_SUBAGENT_TIMEOUT_MSto opt in; task preflight follows the same policy. Startup protection and join budgets remain separate. - Resume a saved conversation by handle or path, and retain completed answers across steering. Model and effort are explicitly established and verified before task submission.
- Child execution has one lifecycle owner. Cleanup uncertainty retains admission and session ownership without hiding the task outcome; RPC queues and public reports are bounded.
- Migration: remove
notify,stdout, and syntheticstate.exitCodeusage. Result omissions and cleanup disposition are explicit; see Migration.
- Execution deadlines are off by default. Set
- 0.0.19:
- Migration: children now save recoverable conversations by default, including prompts and tool results; use
persist: falsefor ephemeral work. - Recovery: results expose effective deadlines and interruption observations. Failed reports retain earlier visible work and authoritative completed text blocks, with truthful Unicode/truncation accounting. Activity distinguishes Pi retry backoff and compaction. Oversized batch details may explicitly omit session paths; inspect an individual child with
statusfor its full path. - Compatibility: support Pi 1.5 schemas and host keybindings while retaining Pi 1.3 support; preserve provider-billed usage totals and provenance.
- Hard execution limits are unchanged. No automatic wrap-up, retry, or budget extension is added.
- Migration: children now save recoverable conversations by default, including prompts and tool results; use
- 0.0.13: replace the custom SDK child bootstrap with Pi's public RPC protocol. Add
steer, persisted/resumable child sessions (persist,resume), and opt-intrustProject. Removejiti, private Pi imports, and the custom fd-3 child protocol. Add an active-tool probe so missing tools fail before prompting. Wrap prompts and steering to prevent RPC command expansion, exclude all unselected tools from nested calls, preserve orderly child shutdown hooks, ignore oversizedagent_endhistory, retain the latest partial report, and expose persisted session paths in model-facing text. Notices announce handles without consuming reports. Persisted sessions have exclusive ownership, startup operations share one deadline, and children inherit the ordinary process environment.
Migration
0.0.20
Execution deadlines are now opt-in. Omit
executionTimeoutMsand leavePI_SUBAGENT_TIMEOUT_MSunset for no execution cutoff. Valid environment configuration still provides a global default; explicit arguments override it. Invalid execution-limit environment values now fail instead of falling back silently. Startup, foreground handoff, wait, and cleanup budgets are unchanged.Initial task preflight no longer has a separate 30-second RPC acknowledgment cutoff; optional execution deadlines govern preflight as well as model/tool work.
Result consumers must stop reading synthetic
state.exitCodeand unusedstdout; usestate.outcome,errorKind, andstderr. Optionalcleanupdescribes resource disposition separately from the task outcome.Budget-omitted
prompt,cwd, andtoolsare now absent rather than empty placeholders.omittedFieldsidentifies omitted launch metadata or session paths. Model text and structured results use the same report excerpt.Cleanup-unconfirmed children retain admission and session ownership even after collection. They are not resumable or eligible for clearing/eviction in that parent session.
Remove
notifyfrom calls. All unread completions now use active-boundary announcements with inbox fallback, without waking idle parents. Notices still requirewaitto collect reports.Successful
outputmay contain several completed answers from one execution, separated by blank lines, rather than only the last answer after steering. It remains bounded; inspectoutputTruncationfor loss.resumeaccepts a retainedsa-Nhandle or an explicit session path. Handle resume creates a new handle in the original workspace by default and leaves the earlier report readable. Explicitpersist(includingfalse) cannot be combined withresume.
From the SDK child
PI_SUBAGENT_RUNNER,PI_SUBAGENT_BIN, andPI_BINare no longer used. The child requires the installed Pi CLI.- Children now use a real Pi RPC process. Project trust follows RPC rules; pass
trustProject: truefor a one-shot--approveor save a trust decision. - Child sessions are saved by default;
persist: falseopts out, andresume: "<sessionFile>"continues one. Existing SDK child handles do not migrate. steeris available only while a child is running. After completion, resume its persisted session to continue the conversation.- Tool availability is checked by a public extension probe before the prompt; a missing built-in or MCP tool exhausts startup without starting model work.
- Completion notices no longer embed reports or implicitly acknowledge them. Retrieve reports with
wait;statusremains observational. PI_SUBAGENT_PASSTHROUGH_ENVis retired. Child processes inherit ordinary environment configuration; session-identity variables are replaced, not copied.