pi-subagents-extension

Standalone subagent delegation for Pi — six-part contracts, path-scoped gates, model-class routing with honest fallback, live action feed, hash-only ledger. Zero runtime dependencies. Published as pi-subagents-extension on npm (the plain pi-subagents name

Packages

Package details

extensionskill

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

$ pi install npm:pi-subagents-extension
Package
pi-subagents-extension
Version
1.0.0
Published
Aug 15, 2026
Downloads
160/mo · 14/wk
Author
cgarrot
License
MIT
Types
extension, skill
Size
1.4 MB
Dependencies
0 dependencies · 0 peers
Pi manifest JSON
{
  "extensions": [
    "./index.ts"
  ],
  "skills": [
    "./skills"
  ]
}

Security note

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

README

pi-subagents

Standalone, modular extraction of the ZOB harness subagents system (delegate_agent / delegate_task) — a mirror of the pi-mesh pattern.

  • Pure core — the core (core/gates/registry/models/lanes/engine/shared) imports nothing from @earendil-works/*. Pi only appears in the thin extension adapter (src/extension, with local pi-types.ts).
  • Hot session pool (lanes) — a bounded pool of warm child sessions.
  • Model routing with fallback — explicit A→B→parent chain, no silent fallback; fallback carries an explicit model_fallback status.
  • Ported gates — the ZOB delegation gates carried over.

Install / use on another machine

As a Pi package (recommended — the extension auto-loads):

pi install npm:pi-subagents-extension
# or from git, pinned to a release tag
pi install git:github.com/cgarrot/pi-subagents@v1.0.0

From source (clone and run):

git clone git@github.com:cgarrot/pi-subagents.git
cd pi-subagents
npm install
npm run build          # tsc → dist/ (build needed for CLI + tests; the Pi
                       # extension itself loads from src/ directly)

This gives the project the delegate_task / delegate_agent tools, the subagents CLI, the default agent cards (agents/) and the subagents-routing skill (skills/). Run npm run smoke for a headless E2E demo with the fake-pi fixture (no real pi needed).

Releases & publishing

  • Versioning follows semver from package.json (v1.0.0 = tag + npm version).
  • The Release GitHub Action publishes to npm automatically on v* tags (requires the NPM_TOKEN repository secret): npm version patch|minor|major && git push && git push --tags. prepublishOnly runs the full build + test suite before every publish.
  • The package is published as pi-subagents-extension on npmjs.org (publishConfig.access is public; the plain pi-subagents name is already taken on npm by another project) and is a Pi package (pi manifest + pi-package keyword), so pi install npm:pi-subagents-extension works on any machine and the package appears in the pi.dev gallery automatically.

Target layout

packages/pi-subagents/
├── package.json               # pi-subagents-extension 0.1.0, zero runtime deps
├── tsconfig.json              # strict, ESM, NodeNext, outDir dist
├── .gitignore                 # node_modules, dist
├── index.ts                   # re-export default from src/extension/index.js
├── LICENSE                    # MIT
├── README.md                  # this file
├── src/
│   ├── core/index.ts          # pure core (no Pi import)
│   ├── gates/index.ts         # ported gates
│   ├── registry/index.ts      # registry
│   ├── models/index.ts        # model routing types
│   ├── lanes/index.ts         # LanePool — single mutable state
│   ├── engine/index.ts        # execution engine
│   ├── extension/
│   │   ├── index.ts           # thin Pi adapter (default export)
│   │   └── pi-types.ts        # local Pi type shims
│   └── cli/index.ts           # CLI entry
├── child/README.md            # child adapter notes
├── agents/                    # default subagent cards (see agents/README.md)
│   ├── explore.md planner.md implementer.md oracle.md qa.md
│   └── README.md              # override guide
├── skills/
│   └── subagents-routing/
│       └── SKILL.md           # routing skill for pi-subagents users
├── scripts/
│   ├── subagents-smoke.mjs    # headless E2E smoke (fake-pi, no real pi)
│   └── run-tests.mjs          # portable recursive test runner
└── test/                      # unit + engine + lanes + gates + models suites

Single mutable state: the LanePool

The only mutable state in the system is the LanePool (src/lanes). Every hot child session lives in exactly one lane; every operation flows through the pool. Everything else is derived or stateless, which keeps the system testable and auditable.

v1 verdict (delivered): spawn-per-task + stable session

Per the stdin spike verdict, pi exits after a task (stdin is one-shot — there is no built-in multi-task loop). Therefore v1 dispatches each task as a fresh one-shot spawn (pi --mode json -p --no-extensions), but reuses a STABLE --session path per lane. A chain of tasks on the same lane keeps context continuity through the same session file: one boot per lane for N tasks (demonstrated headless by the smoke: two chain steps share one sessionPath).

The v2 warm-loop (one long-lived pi serving N tasks via a child extension) is a separate future spike, out of scope here.

F8 (fresh-per-run sessions): single/parallel dispatches default to a FRESH per-run session file <agent>-<runId>.jsonl under .pi/agent-sessions/ (one unified session dir — same as the LanePool default, the CLI doctor, and this README) — successive runs to the same agent never stack onto one shared session. Continuity is preserved where it matters: a chain shares one lane/session across its steps, and continue_run resumes the original run's session file (byte-offset). Passing fresh: false to delegate_task restores the legacy stable agent-named lane session (<agent>.jsonl).

Model routing

Resolution priority: explicitModel → agentModel → class model → parentModel.

  • Explicit model is verified against an injected verified model catalog; it blocks with model_unavailable (no child spawned) unless it is present with resolutionStatus === 'verified'. No silent fallback.
  • Class routing (cheap | balanced | capable) builds the honest fallback chain A → B → parent; the parent is the last-resort sentinel and is never probed. The chosen model + direct/fallback status are always exposed.
  • The model probe is an optional injected hook fired off the critical path (fire-and-forget, never awaited before spawn) with a shared long-TTL cache.

Parent-model inheritance (F3)

A child dispatched without an explicit model inherits the parent/session model (spawned with --model <parentModel>), resolved as:

  1. the live session model when the host exposes it on the session context (ctx.model, optional);
  2. otherwise the pi settings defaultModel — project <repoRoot>/.pi/settings.json over the global ~/.pi/agent/settings.json.

Default class models are quota-safe: every class (cheap, balanced, capable) maps to the resolved parent model — the one model guaranteed available for this session — so a class-routed child never silently switches provider. Hosts wanting per-class routing pass an explicit classModels map (which replaces the default entirely).

Verified model catalog: .pi/model-catalog.json (F2)

Explicit model: overrides are verified against the catalog file read at every preflight from <repoRoot>/.pi/model-catalog.json (schema zob.model-catalog.v1; a catalog created or updated mid-session is honored without restarting). Only entries with resolutionStatus === "verified" pass; anything else blocks with model_unavailable and remediation naming this path. Example catalog:

{
  "schema": "zob.model-catalog.v1",
  "models": {
    "ollama-cloud/deepseek-v4-flash:preview": { "resolutionStatus": "verified" },
    "openai/gpt-4o": { "resolutionStatus": "verified" },
    "anthropic/claude-opus-4": { "resolutionStatus": "resolved" }
  }
}

(anthropic/claude-opus-4 above is intentionally NOT verified: it would be blocked — verify a model's current provider availability before flipping its resolutionStatus to verified.)

Invariants (I1–I15)

  • I1 — ledger hash-only, bodyStored: false; never store raw bodies.
  • I2 — six-part contract validated before any spawn.
  • I3allowed_paths repo-relative only, forbidden deny-only, broad roots refused.
  • I4 — one child = one isolated pi process (--no-extensions + child adapter), never a shell.
  • I5 — honest statuses queued → running → complete | failed | aborted | preflight_failed.
  • I6 — output validated against the output contract (*.v1 + final marker) before acceptance.
  • I7 — zero loop: anti-duplicate, abort SIGTERM → 5s → SIGKILL, named timeouts.
  • I8 — every bound is a named constant.
  • I9 — core (core/gates/registry/models/lanes/engine/shared) imports nothing from @earendil-works/*; extension = thin adapter with local pi-types.ts.
  • I10 — model probe: no silent fallback (fallback = explicit routing with model_fallback status).
  • I11 — injectable runner (spawner/stdio) → deterministic tests without Pi.
  • I12 — optional hooks (goal, mesh, models) never required by the core.
  • I13 — model probe off the critical path + shared long-TTL cache.
  • I14 — honest fallback chain A→B→parent with model_fallback status.
  • I15 — bounded hot lane pool (idle TTL, SIGTERM→SIGKILL, max size).

Tools

The package ports the ZOB delegation tool family onto the pure core (extension adapter src/extension, CLI src/cli).

Tool / command Surface Notes
delegate_task extension tool strict single child with preflight + write gates
delegate_agent extension tool single / parallel / chain child agents
get_delegation_run engine (BackgroundRunRegistry) read current background run state
await_delegation_run engine bounded wait for background settlement
subagents (CLI) src/cli/subagents.ts admin surface over the pure core

subagents CLI commands:

subagents agents   [--scope project|user|both]
subagents catalog
subagents contract "<six-part task>"
subagents runs     [--sort active|latest|duration|agent]
subagents doctor   [--timing]
subagents --help

Configuration: .subagents

Runtime behaviour is configured via an optional .subagents file at the repo root (project-local defaults) plus injected options at the engine boundary:

  • model.classescheap | balanced | capable preferred model ids and the per-class fallback chain.
  • model.verifiedCatalog — explicit-model allowlist (only resolutionStatus: verified models may be requested explicitly).
  • lanes.maxParallel — concurrency cap (default 4).
  • lanes.killGraceMs — abort grace between SIGTERM and SIGKILL (default 5000).
  • lanes.sessionDir — session-file dir (default <cwd>/.pi/agent-sessions).
  • ledger.dir — hash-only ledger dir (default <repoRoot>/.pi/logs/runs).
  • spawn — injectable spawner; tests inject the fake-pi fixture (I11).

The exact .subagents schema is resolved by the extension adapter layer; the core only consumes injected options (never reads config itself), preserving the pure-core invariant.

Default agents

Five generic default cards ship under agents/: explore, planner, implementer, oracle, qa — each with model_class cheap|balanced|capable. They are the fallback when a project defines none. See agents/README.md for frontmatter fields and how to override them with project-local .pi/agents.

Note: agent discovery reads *.md under an agent dir. The package's agents/README.md is documentation only; a project agent dir should hold only agent cards (or exclude docs) so no spurious readme agent is found.

Scripts

npm run build            # tsc -p tsconfig.json
npm test                 # build + node --test dist/test
npm run smoke            # scripts/subagents-smoke.mjs (headless E2E, fake-pi)
npm run ci               # build + test + smoke

Smoke (headless, no real pi)

node scripts/subagents-smoke.mjs drives the full delegation pipeline with the fake-pi fixture (test/fixtures/bin/fake-pi.mjs) injected as the spawner:

  1. builds a body-free catalog (registry) over the default agents;
  2. validates a six-part contract and proves preflight blocks before any spawn (no child launched on gate failure);
  3. runs a single task through the engine + LanePool;
  4. runs a chain (2 steps) proving {previous} substitution + the same sessionPath (one boot for N tasks);
  5. runs parallel dispatch under a concurrency cap;
  6. verifies hash-only / body-free ledger entries (bodyStored: false, raw bodies stripped to sha-256);
  7. runs a background delegation and inspects it via get_delegation_run.

It exits 0 only when every check passes, otherwise prints the failure and exits non-zero. Requires a prior npm run build.

Limitations

  • v2 warm-loop (one long-lived pi serving N tasks via a child extension) is a future spike; v1 is spawn-per-task with a stable per-lane session.
  • Session files grow: each lane appends NDJSON to a stable *.jsonl session file, so long-running chains accumulate session content. v2 will add rollover / compaction.
  • Agent discovery reads *.md: a documentation file inside an agent dir is parsed as an (empty) agent; keep agent dirs to cards only.
  • Extension adapter (src/extension) and CLI are the thin Pi-facing layer and are the only modules allowed to touch pi-types.ts.