pi-subagents-extension
v1.0.0
Published
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
Maintainers
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 localpi-types.ts). - Hot session pool (lanes) — a bounded pool of warm child sessions.
- Model routing with fallback — explicit
A→B→parentchain, no silent fallback; fallback carries an explicitmodel_fallbackstatus. - 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/[email protected]From source (clone and run):
git clone [email protected]: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
ReleaseGitHub Action publishes to npm automatically onv*tags (requires theNPM_TOKENrepository secret):npm version patch|minor|major && git push && git push --tags.prepublishOnlyruns the full build + test suite before every publish. - The package is published as
pi-subagents-extensionon npmjs.org (publishConfig.accessis public; the plainpi-subagentsname is already taken on npm by another project) and is a Pi package (pimanifest +pi-packagekeyword), sopi install npm:pi-subagents-extensionworks 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 suitesSingle 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 withresolutionStatus === 'verified'. No silent fallback. - Class routing (
cheap | balanced | capable) builds the honest fallback chainA → B → parent; the parent is the last-resort sentinel and is never probed. The chosen model +direct/fallbackstatus 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:
- the live session model when the host exposes it on the session context
(
ctx.model, optional); - otherwise the pi settings
defaultModel— project<repoRoot>/.pi/settings.jsonover 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.
- I3 —
allowed_pathsrepo-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 localpi-types.ts. - I10 — model probe: no silent fallback (fallback = explicit routing with
model_fallbackstatus). - 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→parentwithmodel_fallbackstatus. - 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 --helpConfiguration: .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.classes—cheap | balanced | capablepreferred model ids and the per-class fallback chain.model.verifiedCatalog— explicit-model allowlist (onlyresolutionStatus: verifiedmodels may be requested explicitly).lanes.maxParallel— concurrency cap (default 4).lanes.killGraceMs— abort grace betweenSIGTERMandSIGKILL(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
.subagentsschema 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
*.mdunder an agent dir. The package'sagents/README.mdis documentation only; a project agent dir should hold only agent cards (or exclude docs) so no spuriousreadmeagent 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 + smokeSmoke (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:
- builds a body-free catalog (registry) over the default agents;
- validates a six-part contract and proves preflight blocks before any spawn (no child launched on gate failure);
- runs a single task through the engine + LanePool;
- runs a chain (2 steps) proving
{previous}substitution + the samesessionPath(one boot for N tasks); - runs parallel dispatch under a concurrency cap;
- verifies hash-only / body-free ledger entries (
bodyStored: false, raw bodies stripped to sha-256); - 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
piserving 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
*.jsonlsession 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 touchpi-types.ts.
