simple-subagents

Lightweight background subagents for Pi

Packages

Package details

extension

Install simple-subagents from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:simple-subagents
Package
simple-subagents
Version
0.12.0
Published
Aug 9, 2026
Downloads
2,440/mo · 1,514/wk
Author
aeturnal
License
MIT
Types
extension
Size
231.8 KB
Dependencies
0 dependencies · 5 peers
Pi manifest JSON
{
  "extensions": [
    "./src/index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

simple-subagents

Lightweight background Pi subagents. Start independent work in parallel, inspect it while it runs, and collect only the results you need.

Requires Node.js 22.19 or newer and Pi 0.82.x.

Install

pi install npm:simple-subagents

Use locally

Install the package persistently, then start Pi normally:

pi install .
pi

Or load the extension for a single run without installing it:

pi -e ./src/index.ts

Ask Pi naturally: “start three parallel subagents to review the tests, dependencies, and docs”; “show subagent status”; “wait for job-1 and job-3”; “cancel job-2”; or “collect job-1 and job-3.” /subagents opens a read-only inspection dashboard: arrows select jobs, Enter toggles compact details, v opens or closes the scrollable full view, Page Up/Page Down and Home/End scroll full details, c cancels queued or running work, and Escape returns from full view or closes the dashboard.

While jobs are queued or running, an above-editor tree shows each active subagent, its latest bounded activity, turns, tool uses, tokens, short model name, thinking level, and elapsed time. Running usage updates after each completed assistant response. On narrow terminals, activity and telemetry details drop away before the core lifecycle facts. Running rows use an animated spinner. Completed, failed, and cancelled rows remain visible for five seconds; /subagents remains the durable inspection view. A completed or cancelled generation does not close its session; explicitly close an open session when no further work is needed.

Model-turn and reasoning events appear as fixed activity such as Model turn started and Model reasoning. During a long reasoning stream, the extension refreshes one bounded activity timestamp at most every five seconds. It never captures or displays the model's reasoning text. Heartbeats depend on the selected provider and model emitting Pi reasoning events; the extension does not invent activity when no event arrives.

subagent_status exposes safe lifecycle metadata: state, timing, profile identity, access, launch/reported model, usage, and up to three recent activity summaries. It withholds the submitted task and child prompt, and never exposes complete captured output; after collection it may show only a bounded result preview. It also never returns stderr, error body, malformed protocol samples, or the profile prompt. A completed status points the parent to subagent_control to collect the result.

Tool-result details contain only bounded renderer metadata and never store complete job snapshots. Malformed protocol records are counted, but their raw text is discarded.

job-2 — running · running for 2m 14s
Task: Task details withheld.
Agent: reviewer · Access: read-only
Launch model: openai-codex/gpt-5.6-terra
Launch thinking: medium (profile)
Usage: input 28000, output 3000, cache read 0, cache write 0, cost 0.08, turns 6
Session: open · Generation: 1 · Work: running
Queued: no · Queued follow-up: no · Blocked by result: no · Unread reports: 0
Recent activity:
  4s ago   Completed read
  2s ago   Started lsp_diagnostics
  now      Model reasoning

Agents and access

The built-in generic profile is always available. Add user profiles at ~/.pi/agent/agents/*.md; project-scoped profiles are intentionally ignored. Profiles use frontmatter followed by the subagent’s system prompt:

---
name: reviewer
description: Review changed code
tools: read, grep
model: anthropic/claude-sonnet-4-5
thinking: medium
---
Return concise, line-referenced findings.

Per-job model and optional thinking

Model overrides remain available by default. A start task can temporarily override its child model without changing the profile or parent session:

{
  "task": "Review the authentication changes",
  "agent": "reviewer",
  "writeAccess": false,
  "model": "anthropic/claude-sonnet-4-5"
}

Per-job thinking overrides are disabled by default. Normal thinking precedence is profile thinking, then the parent session, then Pi or the model default. This keeps the parent agent from increasing child reasoning on each launch.

Model values are opaque Pi IDs or patterns. Thinking is passed separately through --thinking, not encoded in --model. Final model suffixes equal to a normalized thinking level are rejected in profile and job models; use the separate thinking field instead. ollama/llama3.1:8b remains valid. Pi performs provider translation and clamping, as well as pattern resolution, model availability, and provider credential checks.

Start and status views report Launch model and Launch thinking, which describe the arguments selected by this extension. Collected output reports Pi's Reported model separately; both model values are shown when resolution produces a different model ID. Overrides do not change the profile prompt, tools, access mode, working directory, parent model, or sibling jobs.

Use subagent_agents({}) when profile names or capabilities are unknown. It returns profiles in discovery order (built-in generic first), including configured model inheritance and the read-only and writable tool allowlists passed when child Pi starts. Children run with Pi extension discovery disabled. Profile tool lists therefore select built-in tools only; extension-provided web, MCP, diagnostic, nested-subagent, UI, and lifecycle behavior is unavailable. For research that needs external sources, have the parent fetch or clone them before starting a child that analyzes the local copies.

A writable launch allowlist does not authorize a job to write. The parent must still start that job with writeAccess: true, and configured write confirmation still applies. Discovery never returns profile system prompts, profile file paths, raw frontmatter, discovery diagnostics, credentials, or parent session context.

Jobs are read-only by default. The parent model can explicitly request write access for a job; writable jobs may ask for confirmation through ~/.pi/agent/simple-subagents.json:

{
  "confirmWrites": false,
  "allowThinkingOverrides": false
}

Set allowThinkingOverrides to true and run /reload when you intentionally want per-job control. The subagent_start task schema will then expose thinkingLevel, and precedence becomes job thinkingLevel, profile thinking, parent session, then Pi or the model default. Supported levels are off, minimal, low, medium, high, xhigh, and max.

confirmWrites defaults to false. Even when write access is requested, give concurrent writers non-overlapping work: all subagents share the same workspace, so overlapping writes can conflict.

Persistent sessions and public tools

subagent_start creates an open child session and starts generation 1. It accepts a tasks array of one to eight task objects. Each task requires task; agent defaults to generic, writeAccess defaults to false, and cwd and model are optional. thinkingLevel is available only when allowThinkingOverrides is enabled above.

subagent_start({
  tasks: [{
    task: "Review the authentication changes and report concise findings.",
    agent: "reviewer",
    writeAccess: false,
  }],
})

Use subagent_status({ id: "job-1" }) to inspect one job, or subagent_status({}) to list jobs. A terminal generation does not normally close its child: completed and cancelled work can be followed up in the same child context. Output never enters the parent conversation automatically; use subagent_control with collect when the result is needed.

Send, redirect, and help

subagent_send sends one message to one open session. Its input has exactly one id, one message (up to 50 KiB), and an optional delivery; it does not batch messages or provide conversation-history filters.

subagent_send({
  id: "job-1",
  message: "Now check whether the failing tests cover the missing authorization case.",
  delivery: "follow_up", // default
})

A follow_up starts the next generation when the session is idle. If the current generation is running, one follow-up can wait in that session's queue. A second queued follow-up is rejected. If a result is still uncollected, the queued follow-up waits until that result is collected or discarded. This one-result barrier prevents the next generation from replacing an unread result.

Use redirect only to steer a running generation without creating a new generation:

subagent_send({
  id: "job-1",
  message: "Prioritize the authorization middleware; stop reviewing unrelated files.",
  delivery: "redirect",
})

A redirect for an idle session is rejected and should be sent as follow_up instead. When a child is waiting for help, either delivery value is treated as its answer and resumes the same generation rather than creating or redirecting work.

Children can send structured progress or help_request reports. Read and clear unread reports with subagent_inbox; it accepts only an optional job ID, so it has no time, kind, or history filters:

subagent_inbox({ id: "job-1" })
subagent_inbox({}) // unread reports from all sessions, oldest first

Progress stays in this inbox and does not interrupt either model. A help request also enters the inbox, settles the child into waiting_for_parent, and adds one bounded message to the parent model's next-turn context without starting a parent turn. Each run auto-delivers at most one help request; extra reports stay inbox-only. Reply with subagent_send to let the child continue. A newly reported help request in that resumed run can repeat the same cycle.

Results, cancellation, and close

Use subagent_control for result and lifecycle actions:

subagent_control({ action: "collect", ids: ["job-1"] })
subagent_control({ action: "discard", ids: ["job-1"] })
subagent_control({ action: "cancel", ids: ["job-1"] })
subagent_control({ action: "close", ids: ["job-1"] })

collect returns a ready result and discard drops one; either action leaves the session open and releases a follow-up blocked by that result. cancel cancels an active generation first and leaves an already queued follow-up intact; only when no active generation takes precedence does it cancel a queued follow-up. It leaves the session open. subagent_wait only waits for work to settle or wait for help; it does not collect, discard, cancel, answer, or close a session. Only close ends an open session. It rejects new work, cancels active work, clears queued work, closes the child process, and releases its open-session slot. Close sessions explicitly when no more work is expected.

subagent_wait({ ids: ["job-1", "job-3"], until: "all", timeoutMs: 60_000 })

subagent_wait accepts one to eight job IDs. until is any or all (default all); timeoutMs is 100–300000 (default 60000). It is an event-driven pause for jobs expected to finish when no useful parent work can proceed. The parent cannot answer concurrently while it waits, so each call lasts at most 5 minutes. It returns as soon as the requested condition is satisfied; the timeout is only an upper bound. A timeout returns current states without cancelling work; do not immediately wait again—continue other work or return control. Aborting the parent turn does not cancel subagents.

Do not confuse a failed generation with a failed session. An open session with a failed generation can receive a later follow_up and retains child context. A session becomes failed after child loss, an open failure, or failed cleanup; it rejects subagent_send, never restarts automatically, and may retain a ready partial result for collection. An unexpected child exit adds one fixed, bounded notice to the parent model's next-turn context without starting a turn. Once its child is confirmed dead, close may be unnecessary or rejected.

Limits and parent lifecycle

At most four generations run at once and at most eight child sessions stay open. The manager keeps every opening, open, closing, or potentially live record plus at most the newest 64 non-live closed or failed records (SESSION_TOMBSTONE_MAX_ITEMS). Older terminal IDs are pruned and never reused. These bounded tombstones exist only during this parent extension/session lifetime; they are not persisted across parent replacement. Explicit close stays idempotent while its closed record is retained; a pruned ID is unknown. subagent_start accepts one to eight task objects; each retained task is limited to 32 KiB of UTF-8 text. subagent_control and subagent_wait each accept one to eight job IDs. Each session retains at most one queued follow-up and one uncollected result. Collected output is capped at 50 KiB; batched collection shares that 50 KiB cap. Each report message is capped at 4 KiB. Report IDs are terminal-safe and capped at 256 UTF-8 bytes; unsafe or longer IDs are replaced by a stable SHA-256 ID. Each session's memory-only inbox retains at most 128 reports and 50 KiB of full records, including messages, IDs, and fixed metadata. Progress is evicted before inactive help, while active help is protected. subagent_inbox returns at most 64 reports per call and up to 40 KiB of report text; later reads return remaining unread reports.

Duplicate report tracking keeps only the most recent 512 IDs and 16 KiB of IDs per generation in both the child runner and manager. Immediate duplicates are suppressed. Help replies and redirects keep the current generation's dedupe memory; a new follow-up generation resets it. Reading the inbox does not reset dedupe. Older IDs may be accepted again after they age out of either recent-ID bound.

The dashboard can cancel queued or running work, but collection, discard, sending, and close are parent-agent operations through the public tools. The dashboard never injects a result into the conversation.

Before a new, resumed, or forked parent session replaces one that has open children, Pi asks for confirmation to close all children. Declining cancels the replacement and preserves the old session. Confirming runs close on every potentially live child, closing live children and discarding ready results and queued work. A successful new, resume, or fork emits session_shutdown, reloads and rebinds extensions as a new instance, and then emits session_start; closed or dead records, unread reports, previews, and collectable partial results do not persist into that replacement instance. Collect or read anything needed before confirming replacement. No child process survives a confirmed replacement. If a close fails before actual exit is observed, replacement stays blocked until that exit. /reload and Pi exit cannot be cancelled; they make a best-effort escalating cleanup and do not guarantee every child exit.