@leocavalcante/pi-subagents
v0.1.3
Published
Delegate tasks to isolated Pi agents, singly, in parallel, or in a chain.
Downloads
133
Readme
pi-subagents
A Pi extension that delegates tasks to separate Pi processes with isolated context windows.
Extracted from Leo Cavalcante's dotfiles and based on the subagent example bundled with Pi 1.0.0. Adds session-owned background execution.
Contents
- Quick start
- Installation
- Agent definitions
- Usage
- Named chain outputs
- Model and thinking overrides
- Tool allowlist overrides
- Inherited environment filtering
- Batch concurrency
- Task deadlines
- Background execution
- Usage accounting
- Output capture and cleanup
- Security
- Contributing
- Origin and license
Quick start
Install the published npm package and reload Pi:
pi install npm:@leocavalcante/[email protected]If Pi is already running, enter /reload so it loads the newly installed extension.
Create the personal agent directory if it does not already exist. On macOS/Linux, run:
mkdir -p ~/.pi/agent/agentsIn Windows PowerShell, run:
New-Item -ItemType Directory -Force -Path (Join-Path $HOME ".pi/agent/agents") | Out-NullThen save a personal agent definition as ~/.pi/agent/agents/reviewer.md:
---
name: reviewer
description: Review code and report actionable findings
---
Review the requested code without changing files. Report findings with file and line references.Then ask Pi to delegate a task:
Use reviewer to review the authentication code for security issues. Do not modify files.See Installation for local installs and Windows setup, and Agent definitions for project agents and optional settings.
Installation
Requires Pi and Node.js 22.19.0 or newer. Tested with Pi 1.0.4 and 1.1.0 paired with Pi Codemode 1.1.0, and with Pi 1.2.0. Fresh installs of Pi 1.0.4 or 1.1.0 may resolve incompatible Pi Codemode 1.2.0; see the upstream bug report. To install an older Pi with its compatible Codemode version, run the matching command:
# Pi 1.0.4
npm install --global @earendil-works/[email protected] @earendil-works/[email protected]
# Pi 1.1.0
npm install --global @earendil-works/[email protected] @earendil-works/[email protected]Alternatively, use Pi 1.2.0. Pi provides the runtime dependencies, so no build step is needed.
The npm package is published as version 0.1.3. Install that version with:
pi install npm:@leocavalcante/[email protected]To install unreleased changes from the current GitHub main branch instead, use:
pi install git:github.com/leocavalcante/pi-subagentsFrom the checkout root, install the local copy with:
pi install .Run /reload in Pi after installation. Remove any previous copy or symlink at ~/.pi/agent/extensions/subagent before installing the package to avoid registering the tool twice.
For development without changing Pi settings, symlink the checkout instead:
mkdir -p ~/.pi/agent/extensions
ln -s "$PWD" ~/.pi/agent/extensions/subagentRun that command from the checkout, with no existing subagent directory. Use either a package install or the symlink, not both. On Windows, use PowerShell and a directory junction instead of the POSIX ln -s command:
$extensions = Join-Path $HOME ".pi/agent/extensions"
New-Item -ItemType Directory -Force $extensions | Out-Null
$link = Join-Path $extensions "subagent"
if (Test-Path -LiteralPath $link) { throw "Remove the existing subagent path first: $link" }
New-Item -ItemType Junction -Path $link -Target (Get-Location).PathRun this from the checkout root. A junction avoids requiring Windows symbolic-link privileges; remove it with Remove-Item (Join-Path $HOME ".pi\agent\extensions\subagent") when you no longer need the local install.
Agent definitions
Keep personal agent definitions in ~/.pi/agent/agents/*.md. They are not bundled with this package.
---
name: worker
description: General-purpose worker
---
Complete the delegated task. Report the changes made and checks run.An agent can set model, thinking, tools, optional discovery tags, a default timeoutMs, a default working directory with cwd, and inherited environment variables to remove with unsetEnv in its frontmatter. Agent cwd paths resolve relative to the parent session's working directory, not the definition file. Model selectors are limited to 512 UTF-8 bytes and reject control characters so they remain safe to pass as child-process arguments:
model: anthropic/claude-sonnet-4-5
thinking: high
tools: [read, bash]
timeoutMs: 120000
tags: [security, code-review]
cwd: packages/app
unsetEnv: [AWS_SECRET_ACCESS_KEY, PRIVATE_TOKEN]Tags are ASCII slugs using letters, digits, dots, underscores, or hyphens, beginning with a letter or digit; input is normalized to lowercase. Tags are discovery labels, limited to 32 labels of at most 64 bytes each and 2 KiB total, and searchable with subagent_agents. Dispatch can also use them to select an agent with agentTags; this does not change the agent's execution settings.
An agent with no model configured—or with an empty or whitespace-only model value—inherits the parent's active model and thinking level. An explicit thinking overrides the inherited level or the level in a model suffix. Valid levels are off, minimal, low, medium, high, xhigh, and max; Pi clamps them to the model's capabilities.
Without tools, it uses Pi's defaults. Both tools: read, bash and tools: [read, bash] work. Set tools: [] to disable the initial tool selection. Entries cannot contain commas or control characters because the child CLI parses a comma-separated list; they also cannot start with + or -, which Pi interprets as modifiers to the default tool selection rather than allowlist names. Tool arrays are limited to 256 entries; comma-separated values and the normalized --tools argument are each capped at 4 KiB of UTF-8 text. This is not a sandbox; extensions can still change the selection.
Definitions need non-empty name and description fields. Agent timeoutMs defaults must be whole milliseconds from 1 to 86400000 (24 hours); call-level values can override or explicitly disable a definition's deadline. Agent cwd defaults must be non-empty, well-formed Unicode paths with no NUL characters and at most 32,767 UTF-16 code units; dispatch paths can override them. A configured working directory is a convenience, not a filesystem sandbox, and does not restrict access to other paths. Agent unsetEnv defaults list portable environment-variable names, with at most 128 names and 4 KiB total; calls and task entries can add more names to remove but cannot restore variables removed by the agent. PI_SUBAGENTS_NESTING_DEPTH is reserved for the recursion limit and cannot be removed. Agent definition files must be valid UTF-8, and metadata escapes that produce unpaired UTF-16 surrogates are rejected rather than silently normalized. Agent names are limited to 256 UTF-8 bytes and cannot contain terminal or bidirectional formatting controls; this keeps identifiers safe in trust prompts, diagnostics, and UI output. Descriptions are limited to 1 KiB of UTF-8 text so listing metadata remains compact. Untrusted metadata shown in listings and project-agent confirmations escapes default-ignorable Unicode code points for display while preserving zero-width joiners, non-joiners, variation selectors attached to emoji or ideographic bases, and registered emoji tag flag sequences; structured metadata remains unchanged. Invalid YAML or configuration skips that definition instead of breaking discovery for all agents. Duplicate names in the same directory produce a diagnostic naming both files. The last valid definition in locale-independent lexical filename order still wins. Intentional project-over-personal overrides do not produce a duplicate warning.
Project-local definitions live in .pi/agents/*.md. The tool loads only personal agents by default. Set agentScope: "project" or "both" to include project agents. Project definitions override personal definitions of the same name when using "both". Project agent directories and files may use symlinks that resolve within the project root; links resolving outside it are skipped and reported as diagnostics. Hard-linked project agent files are rejected because their content's origin cannot be established from the project path. Personal agent symlinks and hard links are unchanged.
Agent discovery is bounded for both personal and project directories: it scans at most 4096 directory entries, loads at most 256 Markdown definitions, and reads at most 512 KiB per file and 4 MiB of content per directory. A directory exceeding either scan or definition count is skipped entirely; oversized files are skipped, and reaching the aggregate content limit skips remaining files in locale-independent lexical filename order. A missing personal-agent directory is treated as empty; permission and other inspection failures are reported as diagnostics rather than silently appearing empty. At most 64 diagnostics are retained per listing, with one notice when further diagnostics are omitted. These limits keep malformed or unexpectedly large agent directories from consuming unbounded time or memory during discovery.
Usage
Use subagent_agents to list agent descriptions, tags, source paths, model, thinking, tool settings, configured timeout, working-directory and filtered-environment defaults, and invalid definitions without running an agent:
{}The default scope is "user". To include project definitions:
{ "agentScope": "both" }Results are sorted by name and paginated in groups of at most 10. The response includes offset, limit, totalAgents, and nextOffset; pass the returned nextOffset to continue, keeping the same scope and filter. For example, use { "agentScope": "both", "offset": 10 } for the next default-sized page. You may request a smaller limit from 1 to 10. To inspect one known definition without paging through earlier results, pass its exact, case-sensitive name, such as { "agentScope": "both", "name": "worker" }. To search agent definitions by text, pass query, such as { "query": "authentication" }; it performs case-insensitive literal substring matching in names, descriptions, definition filenames (not parent directories), tags, explicitly configured tool allowlists, cwd paths, unsetEnv environment-variable names, timeoutMs values (as decimal milliseconds), model selectors, and thinking levels using Unicode 17.0 default (non-Turkic) full case folding and NFC normalization (so canonically equivalent composed and decomposed forms match). This includes Greek sigma, German sharp s (ß/ẞ → ss), long s, and ligature expansions; for example, STRASSE matches Straße. Queries are trimmed and limited to 256 UTF-8 bytes. Search does not infer runtime-default tools, the parent-session cwd, inherited model/thinking settings, or unspecified deadlines; timeout values are matched as decimal milliseconds. It only searches the names of filtered environment variables, never their values. Choose one of name, query, tag, or tags. To return agents with one exact tag, pass tag, such as { "tag": "security" }; tag matching is case-insensitive, matches a complete label, and normalizes input to lowercase. To require several exact tags at once, pass tags, such as { "tags": ["security", "code-review"] }; every listed tag must match, and duplicate labels are ignored after lowercase normalization. Multi-tag filters accept 1 to 32 labels and at most 2 KiB total. Neither tag filter can be combined with name or query. Filtering happens after scope precedence, so a project definition overrides a same-named personal definition when using both; pagination totals then describe only the matching definitions. Display rows are clipped to keep responses bounded. Structured paths and cwd values are capped at 4 KiB and diagnostic messages at 512 UTF-8 bytes; longer values are marked as truncated. These listing-only limits do not change the paths used to launch agents. Each page reports discovery diagnostics and excludes system prompt bodies. Project lookup starts at the parent session's working directory and checks ancestors for the nearest .pi/agents directory. Missing and non-directory candidates are skipped; if a nearer candidate cannot be inspected because of an access or I/O error, discovery stops with a diagnostic rather than silently falling back to an ancestor. A task's or agent's cwd changes where its child runs, not which definitions are loaded.
Filter by capability tag
To find agents with an exact tag, such as security, pass tag (matching is case-insensitive):
{ "agentScope": "both", "tag": "security" }If there are more results than fit on one page, continue with the returned nextOffset, keeping the same scope and tag. For example:
{ "agentScope": "both", "tag": "security", "offset": 10 }Filter by multiple capability tags
To find agents that have every requested exact tag, pass tags as a non-empty array. Matching is case-insensitive; duplicate labels are ignored after lowercase normalization:
{ "agentScope": "both", "tags": ["security", "code-review"] }All tag filters run after scope precedence and before pagination. Continue with the returned nextOffset, keeping the same agentScope and tags. The tags filter cannot be combined with name, query, or the single-tag tag filter.
Dispatch using capability tags
Instead of copying an exact name from discovery results, pass agentTags with the subagent tool. It uses the same case-insensitive all-tags intersection; duplicates are ignored. The selector must match exactly one in-scope agent. No match or an ambiguous match is reported without launching a child; add tags to disambiguate or pass agent explicitly. Scope precedence is applied first, and normal project-agent trust approval still applies to the selected definition.
{ "agentTags": ["security", "code-review"], "task": "Review the authentication code without changing files." }Parallel task entries and chain steps can each use agentTags instead of agent, so a batch can mix exact names and unique capability selectors.
Ask Pi to delegate:
Use worker to review the authentication code without changing files.
Run two workers in parallel: one to inspect authentication, one to inspect tests.
Use a chain: worker reviews the code, then worker evaluates the findings.The subagent tool accepts exactly one mode. Select each task's agent with either an exact agent name or agentTags, an array of exact capability labels that must all match. Selection applies after scope precedence and dispatch requires exactly one matching definition: no match or multiple matches fail before any child starts. Project-agent trust approval still applies to the resolved definition. Empty arrays, blank tasks, partial single-task parameters, and unknown agents are rejected before any child starts:
| Mode | Parameters | Execution |
| --- | --- | --- |
| Single | { agent, task } or { agentTags, task } | One task |
| Parallel | { tasks: [{ agent or agentTags, task }, ...] } | Up to 8 tasks, up to 4 concurrent |
| Chain | { chain: [{ agent or agentTags, task, id? }, ...] } | Up to 32 sequential steps, with {previous} and named output references |
At the first chain step, {previous} is an empty string. A first task containing only that placeholder is rejected before approval or launch. If a later substitution makes the task blank, the chain stops at that step without starting another child.
Use a non-empty cwd path to set a working directory; empty strings are rejected rather than treated as the parent directory. Relative paths resolve from the parent session's working directory. Precedence is entry cwd, top-level call cwd, agent frontmatter cwd, then the parent session directory. In parallel and chain modes, an entry's cwd overrides both top-level and agent defaults; top-level cwd overrides agent defaults. Project-agent approval lists the effective resolved child working directories, including dispatch overrides and the parent session fallback; it also summarizes each requested project's effective model, thinking level, initial tool selection, and runtime deadline. The tool selection is not a sandbox. When approval is required for an untrusted project, details are never silently truncated: if the complete prompt exceeds 32 KiB, the call fails before approval or child launch, so reduce the request or configuration and retry. Discovery and approval still use the parent session and are not affected by the child working directory. Paths are limited to 32,767 UTF-16 code units, including the resolved path after a relative path is joined to the parent working directory. Tasks reach children through stdin, so prompts do not depend on the operating system's command-line argument limit. Task, model-selector, and working-directory strings must be well-formed Unicode; isolated UTF-16 surrogates are rejected instead of being silently replaced by platform encoders. Each task is limited to 4 MiB of UTF-8 text, including chain context after substitution; all task text submitted in one dispatch is limited to 16 MiB total. Expanded chain task text is also limited to 16 MiB total across all steps, checked before each substitution is allocated.
Foreground execution is the default. Progress streams into the parent session at most 10 times per second per child; the first update is immediate and the latest pending output is flushed at completion. Throttling affects notifications only: all accepted child events are still parsed, accounted for, and captured. Ctrl+O expands tool output. Use Pi's configured interrupt key (Escape by default in Pi 1.1) to cancel a foreground subagent operation. Ctrl+C is Pi's app.clear binding (clear the editor on the first press, exit on the second), not its interrupt key, and does not cancel a foreground subagent. Model-facing text is capped at 50 KiB per response, including headers and truncation notices. The same limit applies to progress updates, single tasks, chains, and entire parallel batches. Parallel tasks share the available text budget so every task's status remains visible. Captured results remain in tool details, and chains pass the full captured final text to the next step.
Collapsed tool output shows bounded text previews rather than wrapping an entire long line. Expand with Ctrl+O to see captured final text. All modes show process, protocol, and model failure diagnoses, including stderr when no explicit error message is available. Tool arguments remain compact previews in either view.
Named chain outputs
Give a chain step an optional id to reuse its output in any later step:
{
"chain": [
{ "id": "review", "agent": "worker", "task": "Review the authentication code without changing files." },
{ "id": "tests", "agent": "worker", "task": "Inspect the tests without changing files." },
{ "agent": "worker", "task": "Compare the code review with the test findings. Review: {steps.review}\nTests: {steps.tests}" }
]
}{previous} still means the immediately preceding step's captured final text. {steps.ID} means the captured final text from an earlier step with that ID. IDs are case-sensitive and unique within a chain. Use 1 to 64 ASCII characters: a leading letter, then letters, digits, underscores, or hyphens.
The tool rejects invalid IDs, duplicate IDs, and unknown, self, or forward references before approval or child launch. A failed step still stops the chain. If substitution produces a blank task, the next child does not start.
References use full captured text, not the model-facing preview. Substitution happens once, so placeholders and dollar sequences inside an agent's output stay literal. This works in foreground and background chains, including silent jobs. Captured step results include stepId, and output pages include it when present. Paginate a named step by stepId; use zero-based taskIndex for parallel batches, unnamed steps, or positional lookup.
Model and thinking overrides
Set model or thinking on a call without editing the agent definition:
{
"agent": "worker",
"task": "Review the authentication tests without changing files.",
"model": "anthropic/claude-sonnet-4-5",
"thinking": "low"
}Parallel and chain entries accept the same fields. Each field resolves independently, with precedence: entry, top-level call, agent frontmatter, then parent session. Set model: null on a call or entry to ignore the agent's configured model and inherit the parent's active model; an entry-level null also overrides a top-level batch model. Set thinking: null to ignore the agent's configured thinking level. It inherits the parent's thinking only when the resolved model is also inherited from the parent; otherwise it leaves thinking unset so Pi uses its configured default or model suffix. Entry-level null overrides top-level batch thinking. Omitted values use the normal precedence.
Model selectors support Pi's provider/id and :thinking syntax and are limited to 512 UTF-8 bytes without control characters. A resolved thinking setting overrides a model suffix. These options work in foreground and background modes; project-agent approval remains required regardless of these overrides.
Tool allowlist overrides
Use tools to tailor the child's initial tool selection for a particular dispatch without editing the agent definition:
{
"agent": "worker",
"task": "Inspect the tests without changing files.",
"tools": ["read", "grep"]
}A comma-separated string such as "read, grep" is also accepted. In parallel and chain modes, the top-level value is the default for each child and an entry's value overrides it. Tool selection precedence is entry, top-level call, agent frontmatter, then the child Pi defaults. Omitted values inherit that precedence. Set tools: [] to disable the initial tool selection, or set tools: null on a call or entry to ignore the agent's configured allowlist and use child Pi defaults; an entry-level null also overrides a top-level selection.
Overrides use the same validation as agent definitions: at most 256 entries and 4 KiB of normalized UTF-8 tool names, with no commas, controls, or leading +/- modifiers in individual names. This is a convenience for selecting tools, not a sandbox; extensions can still change the selection. Tool overrides work in foreground and background modes and do not bypass project-agent approval.
Inherited environment filtering
The child Pi process inherits the parent environment by default. Set unsetEnv on an agent or dispatch to omit known variables from the child's initial environment:
{
"agent": "worker",
"task": "Inspect the repository without using cloud credentials.",
"unsetEnv": ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"]
}Agent defaults, top-level call values, and parallel/chain entry values combine additively. Lists use portable ASCII environment-variable names, allow at most 128 entries and 4 KiB, and are applied case-insensitively on Windows. The internal PI_SUBAGENTS_NESTING_DEPTH variable cannot be removed. Agent listings and project-agent approval prompts show variable names, never their values.
This only filters variables inherited at child startup; it is not a sandbox. A child can set variables for its own subprocesses, and it still has the parent's OS permissions to read files or obtain credentials elsewhere. Use trusted agents and do not put secrets in delegated tasks or prompts.
Batch concurrency
Parallel mode accepts concurrency from 1 to 4, with a default of 4. Use concurrency: 1 to run independent tasks serially without chaining their output. All batches still share the same four-process budget; this option never raises it. Single and chain modes reject this option.
Task deadlines
Set timeoutMs to bound a child's runtime:
{
"agent": "worker",
"task": "Review authentication without changing files.",
"timeoutMs": 120000
}There is no deadline by default. Numeric values must be whole milliseconds between 1 and 86400000, up to 24 hours. The timer starts at child spawn; permission prompts, prompt-file preparation, and waiting for a process slot do not count.
On Windows, first-use supervisor compilation happens before child spawn, is outside the task deadline, and has no independent deadline. If PowerShell or Add-Type stalls, calls waiting on compilation may hang indefinitely. Canceling a caller detaches only that caller; the shared compilation keeps running, so later invocations in the same runtime also wait for it. It is not safe to terminate PowerShell without verified process-tree containment, since compiler helpers might remain running.
An agent definition may set timeoutMs as its default, shown by subagent_agents; a per-entry or top-level call value overrides it. Set timeoutMs to null on a call or entry to explicitly disable that deadline, including any lower-priority default. Omitted values inherit by precedence: entry, top-level call, agent frontmatter, then no deadline.
For parallel and chain modes, a top-level timeoutMs is the default for each child, not a deadline for the entire batch. An entry's timeoutMs overrides that default; an entry set to null opts that child out. Deadlines also work with background execution.
A timeout terminates the child using the same cleanup as cancellation and returns a failed result with timedOut: true in details. On POSIX, cleanup signals the child's process group and escalates from SIGTERM to SIGKILL after one second. On Windows, after compilation the supervisor joins a kill-on-close outer Job Object, creates Pi suspended with only its standard I/O handles inherited, assigns Pi to a nested kill-on-close Job Object, and then resumes it. This contains child-launched processes before Pi executes. Cancellation and timeout terminate the supervisor, causing its outer Job Object to terminate remaining members; after normal leader exit, the supervisor terminates and waits for remaining inner-job processes to exit. Job setup or cleanup failures fail closed; the supervisor waits at most 30 seconds for job settlement before reporting failure. Windows uses an immediate termination request instead of the POSIX escalation delay. Cleanup may still wait up to one second for inherited pipes to drain. Background jobs report failed, not canceled. Chains stop at the timed-out step; other parallel tasks continue. Explicit user cancellation still reports canceled.
Background execution
Add background: true to any mode. The tool returns a job ID immediately, allowing the parent to continue working. For example:
{
"agent": "worker",
"task": "Review the authentication code without changing files.",
"background": true
}Or ask Pi:
Run a worker in the background to review authentication while you inspect the tests.By default, a finished job sends its result as a follow-up. If the parent is busy, Pi queues it after the current work. If idle, Pi starts a turn to handle the result. Failed and canceled jobs also report back.
Set notify: false for a silent background job:
{
"agent": "worker",
"task": "Review the authentication code without changing files.",
"background": true,
"notify": false
}Silent jobs do not send automatic follow-ups, including failures and cancellations, and do not trigger another parent turn. Results remain available through subagent_jobs status, output, and wait. The notify flag is background-only, defaults to true, and applies to the whole job in every mode. Launch details and job metadata expose the policy, and the job list marks silent jobs.
Use a silent job with a bounded wait when the parent will consume its result explicitly. Silent jobs have the same process, capture, and retention limits. If their output is evicted, there is no completion-message copy to recover.
The subagent_jobs tool manages jobs:
{ "action": "list" }
{ "action": "status", "jobId": "<job ID>" }
{ "action": "wait", "jobId": "<job ID>", "timeoutMs": 30000 }
{ "action": "output", "jobId": "<finished job ID>", "taskIndex": 0, "offset": 0, "limit": 16384 }
{ "action": "output", "jobId": "<finished chain job ID>", "stepId": "review", "offset": 0, "limit": 16384 }
{ "action": "cancel", "jobId": "<job ID>" }
{ "action": "forget", "jobId": "<finished job ID>" }
{ "action": "clear" }Job IDs are generated UUIDs (36 characters); jobId inputs are bounded to that length.
Status returns retained progress or the final result. Job-level error diagnostics are capped at 2 KiB without splitting UTF-8 characters. If both the run and completion delivery throw, the diagnostic keeps both causes. Do not poll continuously; non-silent jobs send completion messages automatically. Cancellation is idempotent and may briefly show canceling while child processes exit.
forget removes one finished job record. clear removes all finished records without canceling active jobs. Forgetting an active job is rejected; cancel it and wait for cleanup first. These operations do not erase completion messages or Pi session history.
Background jobs have their own abort controllers. Ctrl+C on the parent's turn does not cancel them. Use subagent_jobs to cancel a job. On POSIX, cancellation signals the child's process group, including ordinary tool descendants, and escalates from SIGTERM to SIGKILL after one second. On Windows, cancellation terminates the supervisor; its kill-on-close outer Job Object terminates Pi and its descendants.
Foreground calls and background jobs share a limit of four direct child processes per extension runtime. Extension-mediated nested delegation is limited to two child-process levels below the original Pi process; a child at the limit cannot start more subagents through this extension. This bounds recursive tool fan-out while preserving a bounded amount of nested delegation. It is not an OS-level process limit: child tools can still launch processes directly. At most 32 additional task requests can wait for a process slot; excess requests fail with a retryable queue-full result rather than growing an unbounded queue. Queued tasks can be canceled. There can be up to eight active background jobs, with at most eight tasks in each parallel batch. The extension retains the latest 32 finished job records for inspection. Finished output in this registry has a shared 32 MiB budget, estimated from serialized message records, retained model metadata, task text, stderr, and result text. Oldest output is evicted first; an individually oversized result is not retained. Job state remains available with outputEvicted: true, and enabled completion delivery still receives the result before eviction. This registry budget is not a heap-memory limit or a cap on Pi's own session history.
Completion and status text are capped at 50 KiB including headers and truncation notices, with captured output retained in details. Jobs live only in the current Pi session. Quit, /reload, and session replacement cancel active jobs and discard job history. Jobs do not survive a Pi restart.
Background execution requires a long-lived TUI or RPC session. Print and JSON modes must use foreground execution so they do not exit with unfinished work.
Wait for a background job
Use action: "wait" when the next operation needs a particular job's result. It waits for the job to finish cleanup, without polling. timeoutMs defaults to 30000 and accepts whole milliseconds from 1 to 60000. This limits the wait, not the child's runtime.
The response includes the same snapshot as status, plus timedOut. A completed, failed, or canceled job returns timedOut: false. If the wait expires first, it returns the current state with timedOut: true; the background job stays active. Unknown or forgotten IDs return an error immediately.
At most 32 pending waits are allowed across all jobs in one session. When that limit is reached, additional wait calls return a retryable error without affecting the job. Reads of already-finished jobs do not use a pending-wait slot.
Canceling the waiting tool call only stops the wait. To cancel the job, use action: "cancel". Waiters also finish when session shutdown completes job cleanup. Waiting does not consume a child-process slot, add usage charges, or send an extra completion message. Automatic follow-ups still arrive as usual unless the job is silent.
In a codemode script:
const result = await tools.subagent_jobs({
action: "wait", jobId: "<job ID>", timeoutMs: 30000
});
if (result.error) throw new Error(result.error);
return { state: result.job.state, timedOut: result.timedOut };Use one bounded wait when needed rather than a repeated status or wait loop.
Read captured job output
After a job finishes, action: "output" reads a page of a task's captured final text or failure diagnosis. The default stream: "result" preserves this behavior; set stream: "stderr" to page the task's separately captured stderr without mixing it into the final answer. output.stderrTruncated indicates whether stderr reached its 64 KiB capture limit (the captured text includes a truncation notice). Failed tasks also expose retained assistant and tool-result text under an explicit partial, unverified label; it is not evidence that the task completed successfully or that its side effects were rolled back. output.partial marks task output as incomplete or failed; model turns stopped by their output-token limit are failures, with retained text shown only as partial, unverified output. Deferred provider responses are not polled by subagent dispatch and are reported as failures rather than completed answers. output.timedOut indicates whether the runtime deadline fired, and output.timeoutMs reports the effective per-task deadline when configured. output.exitCode is the extension-reported task exit code (protocol violations and timeouts use 1), while output.processExitCode, when available, reports the child's actual exit code (or null if it exited without a numeric code). Protocol failures include bounded failureContext fields for event type, a safe role label, JSONL record number, and last valid assistant stop reason. Invalid payload contents and arbitrary role strings are not echoed. Select a named chain step with stepId, or use zero-based taskIndex (default 0); the selectors are mutually exclusive. Use taskIndex for parallel batches and unnamed chain steps. Status reports resultCount while results are retained.
offset is a UTF-8 byte offset, not a character count. Start at 0 and use the returned nextOffset until it is null. limit is 4 to 32768 bytes, with a default of 16384. Pages never split a UTF-8 character. Active jobs, unavailable captures, invalid indices, and offsets inside a character return an error. Evicted output cannot be recovered from this registry.
Job operations also return structured data to codemode scripts. List returns { action, jobs }, inspection returns { action, job }, and output adds an output page. Errors include error. Job metadata contains state and timestamps, plus cumulative usage when available. Compact usage totals remain inspectable even if captured output is evicted while the job record is retained; metadata does not include the full message capture. Page responses do not copy the full capture into session history again.
For a completed job:
const result = await tools.subagent_jobs({ action: "output", jobId: "<job ID>", taskIndex: 0 });
if (result.error) throw new Error(result.error);
text(result.output.text);
store("next-output-offset", result.output.nextOffset);
// Inspect bounded stderr instead of the default final-result stream.
const logs = await tools.subagent_jobs({ action: "output", jobId: "<job ID>", taskIndex: 0, stream: "stderr" });
if (logs.error) throw new Error(logs.error);
text(logs.output.text);Read additional pages only when needed; keep the same stepId or taskIndex selector and advance offset with each page. Non-silent jobs send completion messages automatically, so do not poll for them.
Usage accounting
Foreground results report cumulative child token and cost totals through Pi's standard tool-result usage field. This includes completed assistant attempts and nested model usage in canonical message_end tool results, even when a task fails or earlier message history is evicted. Legacy tool_result_end copies do not add usage again and are deduplicated from retained history only when an adjacent canonical copy is identical. Reasoning tokens and one-hour cache writes are subsets, not additional tokens.
Background totals are retained with job results and are also available as observational usage in subagent_jobs metadata when available. Compact usage totals remain inspectable after captured output is evicted, as long as the job record is retained; they are not added to Pi's session statistics. Launch acknowledgements, progress updates, and job inspection never report billable usage again. Parent-aborted foreground calls do not return a billable tool result.
Malformed usage and arithmetic overflow fail the task without exposing provider payloads. An overflow while combining batch totals marks the response as an error and preserves individual captures, but omits aggregate usage.
Output capture and cleanup
Each child has bounded output capture:
- Each child may emit at most 100,000 JSONL records, including blank records. Exceeding the count fails the task, stops parsing subsequent output, and initiates child termination while pipes continue to drain.
- JSONL records can be up to 8 MiB in UTF-8, with at most 65,536 structural tokens (container starts, commas, and colons outside strings) and 128 nesting levels. Breadth and nesting are checked before parsing; over-limit records fail the task, are discarded through the next newline, and do not stop stream draining.
- Child stdout is capped at 128 MiB per task across all records. If a chunk would exceed the limit, the task fails and that chunk and later output are drained and discarded while existing process cleanup terminates the child.
- Stderr retains its first 64 KiB, including a truncation notice. Further stderr is drained and discarded.
- Message history retains at most 128 recent messages and 16 MiB of source JSON records per task. Parallel batches and chains share an additional 32 MiB aggregate history budget, distributed across their tasks/steps; a single-task dispatch keeps the 16 MiB per-task limit.
Results and tool details report capture truncation. If the final assistant message is not retained in bounded history, the task fails rather than passing an incomplete or empty result into a chain. These are capture limits, separate from the 50 KiB model-visible output limit shared across all text blocks in one response. Discarded records and evicted history are not preserved elsewhere.
Pre-spawn failures, such as an unwritable prompt file or a synchronous spawn error, return a failed task result with the setup phase and a safe error code. They preserve earlier chain results and let independent batch tasks continue. Cancellation still aborts the operation. Raw setup-error messages are not exposed because they may contain private prompt data.
A zero exit code alone is not success. The child must emit a completed assistant message; a final toolUse turn without a subsequent assistant response is incomplete and fails the task (a chain stops at that step). Malformed UTF-8 or JSONL, escaped unpaired surrogates, JSON structure beyond 65,536 tokens, nesting beyond 128 levels, numbers outside JavaScript's finite range, or malformed user, assistant, or tool-result messages (including metadata, content, or usage where applicable) cause the task to fail without crashing the parent. Redacted thinking blocks are supported, and a successful retry clears errors from earlier attempts.
On POSIX, when a child exits, the extension sends SIGTERM to remaining members of its process group and gives them one second to exit before sending SIGKILL. This applies even when descendants close or ignore the output pipes, so child-launched processes in that group do not survive the invocation. Keep the delegated process running and use background: true to decouple long-lived work from the caller. Processes that escape the group are not terminated. On Windows, the supervisor creates the target suspended and assigns it to a nested kill-on-close Job Object before resuming it. It terminates remaining job members after normal leader exit and confirms the job is empty before reporting success; cancellation and deadlines terminate the supervisor, whose outer kill-on-close Job Object handles the process tree. If compilation, containment, or cleanup fails, launch fails closed rather than claiming success. Job settlement is bounded to 30 seconds. Inherited output pipes get one second to drain, then the extension closes its pipe ends so they cannot hold the invocation open indefinitely. Results report this pipe cleanup.
Security
Context isolation is not a sandbox. Children run with the same OS permissions as Pi and can read or modify files. Include relevant context and task restrictions in each delegation. The child pi process inherits the parent process's environment by default. Use unsetEnv to omit known variables from the child's startup environment; this does not prevent the child from setting variables itself or reading credentials from files and other sources. Do not put secrets in delegated tasks or prompts, and do not rely on environment filtering as a security boundary.
Agent system prompts are written briefly to a temporary UTF-8 file so the child CLI can append them, then removed during invocation cleanup. Transient removal failures get bounded retries, and failed cleanup is retried at process exit. On POSIX, the temporary directory and file grant no group or other access. On Windows, access follows the ACLs inherited from the configured temporary directory; secure that directory if prompts must be private to your account. Temporary-file permissions do not protect against other processes running as the same user or against the delegated child.
Only enable project-local agents in repositories you trust. Untrusted project agents require an interactive approval or an already-trusted Pi project; headless sessions fail closed for untrusted project agents. Approval cannot be disabled through a tool argument.
The foreground subagent result renderer escapes terminal control characters and invisible Unicode formatting characters in child output and tool-call previews before displaying them, while preserving valid emoji and script-joining sequences; captured messages and structured details remain unchanged. The background completion-message renderer also escapes child output and failure details for TUI display, while preserving the raw follow-up content and structured details. The subagent_jobs call and result renderers likewise escape untrusted job IDs, captured output, and failure text only for display; model-facing content and job details remain unchanged.
Authenticate on each machine with Pi's /login. Do not put credentials, sessions, or machine-local Pi state in this repository. To report a vulnerability privately, see SECURITY.md.
Contributing
For local development checks and the maintainer release process, see CONTRIBUTING.md.
Origin and license
Based on the Pi subagent example bundled with @earendil-works/pi-coding-agent 1.0.0.
MIT license. The original copyright notice for Mario Zechner is preserved in LICENSE.
