19 KiB
status, claimed-by, claimed-at, completed-at, parent, blocked-by, tags
| status | claimed-by | claimed-at | completed-at | parent | blocked-by | tags | |
|---|---|---|---|---|---|---|---|
| done | 019fba91-eabf-76ae-b086-a37ac061d6e8 | 2026-07-31T22:23:32-04:00 | 2026-07-31T22:23:42-04:00 | 002-pi-subagents-map |
|
Implementation ready specification
Question
Synthesize the resolved route into an implementation-ready specification for the new neutral Pi subagent extension, including architecture, configuration schema, runtime model, context modes, status UI, lifecycle handling, trust boundaries, deployment seam, and verification plan.
Goal
Build a new neutral Pi extension that can start context-clean subagents frequently. The extension provides mechanism only. Named agents, prompts, tool profiles, and policy live in Pi configuration. The extension must not bundle opinionated subagent types. Herdr support is out of the version-one core and may be added later as an adapter or replacement UI.
Non-goals
Do not implement Herdr integration in version one. Do not claim sandbox isolation. Do not install Pi packages, npm packages, or git packages without explicit user consent. Do not auto-write default config files. Do not make the extension aware of Nix or Home Manager. Do not ship opinionated named agents with the neutral extension. Do not add persistent subagent lifetimes in version one. Do not add summary-seeded context mode in version one. Do not write separate result or artifact files in version one.
Repository placement
Implement the extension under:
modules/agents/pi/extensions/subagents/
├── index.ts
├── agents.ts
├── config.ts
├── runner.ts
├── supervisor.ts
├── status.ts
├── types.ts
└── ui.ts
index.ts is the Pi extension entry point.
agents.ts loads and validates named Markdown agent definitions.
config.ts loads global and trusted project config.
runner.ts implements the subprocess RPC child runner.
supervisor.ts owns child lifecycle, cancellation, timers, and status records.
status.ts defines child status records, durable entries, and status/result APIs.
types.ts holds shared types and schemas.
ui.ts owns the optional built-in Pi TUI presentation.
The existing modules/agents/pi/pi.nix recursively links modules/agents/pi/extensions to ~/.pi/agent/extensions, so the extension is deployed through the existing durable dotfiles seam.
Runtime files
The extension reads these Pi-native runtime paths:
- Global config:
~/.pi/agent/subagents.json. - Trusted project config:
.pi/subagents.jsonthrough Pi'sCONFIG_DIR_NAME. - Global named agents:
~/.pi/agent/agents/*.md. - Trusted project named agents:
.pi/agents/*.mdthrough Pi'sCONFIG_DIR_NAME.
Missing config files are normal. Missing agent directories are normal. The extension must not create them automatically.
The dotfiles module may later link plain files, render JSON from Nix attrs, or expose typed Nix options. That module design is out of scope for the extension.
Configuration model
Config file ownership
Use dedicated JSON config files rather than unknown keys in Pi settings.json.
Global config is loaded from ~/.pi/agent/subagents.json.
Project config is loaded from .pi/subagents.json only when ctx.isProjectTrusted() is true.
Project config overrides global config.
Do not implement tighten-only merge semantics.
Trusted project config may loosen or tighten global defaults.
Config schema
Version one config shape:
{
"defaultContext": "independent",
"defaultTools": "read-only",
"maxConcurrent": 3,
"timeouts": {
"startMs": 30000,
"idleMs": 0,
"runMs": 0
},
"ui": {
"enabled": true,
"defaultExpanded": false,
"showTranscriptMilestones": true
},
"toolProfiles": {
"local-review": {
"activeTools": ["read", "grep", "find", "ls"],
"bash": "deny"
}
}
}
0 timeout values mean disabled.
Unknown config keys should be ignored with a warning, not fatal.
Invalid known values should disable the affected config file and surface a diagnostic.
Built-in defaults
In-code defaults apply when no config file exists or a field is absent.
The in-code default context is independent.
The in-code default tool profile is read-only.
The implementation may choose an initial maxConcurrent, but it must be configurable.
UI config
ui settings gate only the bundled optional UI components.
They must not disable the status data model, status endpoints, neutral events, or durable child records.
Another extension should be able to replace the default UI by consuming the same status data.
Named agent definitions
Named agents are Markdown files with YAML frontmatter and a Markdown body.
They are the only durable named-agent definition format in version one.
Do not define named agents inline in subagents.json.
Required frontmatter:
name: review
description: Review code and report risks.
Optional frontmatter:
context: independent
model: inherit
thinking: high
tools: read-only
allowedContexts:
- independent
hidden: false
name is the canonical identity.
File names are storage only, but warn when the filename stem does not match name.
Names should be lowercase slugs with letters, numbers, and hyphens.
description is used for discovery, listing, and model-facing selection help.
The Markdown body is the base prompt or instruction text for the subagent.
Definition precedence
Load definitions in this order:
- Trusted project
.pi/agents/*.md. - User
~/.pi/agent/agents/*.md.
Project definitions override user definitions with the same name. Duplicate names inside one precedence tier are configuration errors. Do not resolve duplicates by filesystem order.
Ad hoc runtime subagents
Version one supports ad hoc runtime subagents.
A spawn request may omit agent and provide only prompt.
This creates a one-off subagent that is not persisted as a named definition.
Ad hoc subagents inherit global and project defaults unless the spawn call overrides context, model, thinking, or tools.
Status should label ad hoc subagents with a generated short label such as ad-hoc <child-id>.
Context modes
Version one supports exactly two context modes:
independent.fork.
independent is the global default.
Parent conversation context is opt-in through fork.
independent
independent receives:
- The spawn
prompt. - The selected named agent body if
agentis present. - Normal trusted project context such as
AGENTS.md. - Explicit attachments or snippets if the spawn request supports them later.
independent does not receive:
- Parent transcript.
- Generated parent summary.
- Hidden parent branch context.
fork
fork receives the full active-branch transcript snapshot as-is.
It does not strip tool output.
It does not summarize before spawning.
It lets child Pi handle normal compaction if needed.
It appends a small wrapper that explains the delegated role, context boundary, and expected return shape.
Avoid the phrase "child agent" in user-facing text.
Prefer "subagent", "delegated agent", or "worker".
fork does not require special confirmation in version one.
Tool power and context inheritance are separate concerns.
Context precedence
Resolve context mode in this order:
- Spawn-call override.
- Named agent frontmatter
context. - Config
defaultContext. - In-code default
independent.
If a named agent has allowedContexts, reject a spawn that requests a context outside that list.
Tool profiles
Agent frontmatter tools references a named tool profile.
Spawn calls may also override tools with a profile name.
Tool profiles compile to Pi active tools and optional tool-call gates.
This uses Pi-native mechanisms such as pi.setActiveTools() and tool_call blocking.
Do not build a broad permission DSL in version one.
Built-in profile names are reserved and cannot be overridden by config. User config may add custom profiles and select the default profile.
Version-one built-in profiles:
none.read-only.read-only-with-safe-bash.full-tools.
none has no tools.
read-only is local-only and includes read, grep, find, and ls.
read-only-with-safe-bash is local-only and adds bash with a read-only command allowlist.
full-tools means the normal full local Pi tool surface.
full-tools is not a sandbox permission level.
full-tools is not guarded by an extension confirmation prompt during the subagent run.
Version one does not restrict full-tools from being a global default, agent default, or spawn override.
Built-in profiles do not include web or network access except insofar as full-tools exposes ordinary bash.
A later web-research profile can be user-defined when an explicit web tool or bash-network policy exists.
Default tool profile resolution order:
- Spawn-call override.
- Named agent frontmatter
tools. - Config
defaultTools. - In-code default
read-only.
Model and thinking
Optional model can be inherit or a Pi model selector.
Optional thinking maps to Pi reasoning or effort levels such as off, minimal, low, medium, high, xhigh, and max.
The selected model may clamp or ignore unsupported thinking values.
Resolution order:
- Spawn-call override.
- Named agent frontmatter.
- Config default if later added.
- Parent Pi session values.
Do not require model or thinking in named definitions.
Spawn tools
Exact tool names are intentionally deferred to implementation, but version one must expose these surfaces:
- Single spawn.
- Batch spawn.
- List subagents.
- Get subagent status.
- Get subagent result.
- Cancel subagent.
Spawn requests use a single prose field named prompt.
Do not split prose into task and instructions.
Single spawn request shape:
{
"prompt": "Review the staged diff for rollback risks.",
"agent": "review",
"context": "independent",
"model": "inherit",
"thinking": "high",
"tools": "read-only"
}
prompt is required.
agent is optional.
If agent is present, the named definition supplies the base prompt and defaults while prompt supplies the per-call request.
If agent is absent, the spawn is ad hoc.
Batch spawn request shape:
{
"subagents": [
{ "agent": "review", "prompt": "Review API risks." },
{ "prompt": "Independently sanity-check the deployment plan." }
]
}
Batch entries use the same shape as single spawn entries.
Non-blocking behavior
All spawns are non-blocking in version one. Single spawn and batch spawn return after spawn acceptance with child ids and initial metadata. The extension must not block the parent tool call until the subagent completes. The main agent decides whether and when to list, poll, retrieve, or cancel results.
Spawn acceptance result should include:
- Child id.
- Agent name or ad hoc label.
- Context mode.
- Tool profile.
- Model and thinking if resolved.
- Initial lifecycle state.
- Status/result retrieval hint.
Result retrieval
Subagent results arrive through:
- Status data model.
- Neutral events.
- Durable milestone entries.
- Explicit result tool.
The parent receives final textual result plus compact metadata by default. Metadata includes agent name, context mode, elapsed time, stop reason, and child status/session id. Do not copy the child transcript into the parent context by default. Do not copy selected evidence snippets into the parent context by default.
subagent_list should show active subagents plus bounded recent terminal history.
The default recent terminal history should be small, such as the last 10 terminal children in the current parent session.
Results and status persist through Pi session state and child session storage only in version one. Do not write separate result files or artifact files.
Runtime architecture
Use a ChildRunner interface so runtime can evolve without changing tools or status.
Version one implements subprocess-rpc.
An in-process SDK runner is an explicit later optimization.
Keep subprocess mode as permanent fallback.
ChildRunner responsibilities:
- Start a child Pi RPC process.
- Send a prompt command.
- Stream RPC events.
- Normalize lifecycle events.
- Abort or terminate the child.
- Return normalized terminal result to the supervisor.
Use Pi RPC mode instead of print mode.
RPC preserves prompt bytes, exposes events, supports abort, supports get_state, and is intended for embedding.
For fork, serialize the parent session header plus active branch entries to a temporary JSONL file.
Launch child Pi using that file as the starting session.
For independent, launch child Pi without parent session transcript.
Supervisor architecture
The Supervisor is the only owner of child records, process handles, timers, event subscriptions, and lifecycle transitions.
Tool handlers must not spawn untracked child processes independently.
Lifecycle states:
queued.starting.running.settling.completed.failed.cancelled.timed_out.orphanedif reload loses process ownership and no reattach protocol exists.
Use event-driven transitions.
Do not implement unbounded sleep-loop polling.
Use Pi RPC events, process lifecycle, abort signals, and bounded timers.
Use agent_settled as semantic completion.
Use process close for subprocess resource cleanup.
Timeouts:
startMsguards process startup and prompt acceptance.idleMsguards no-progress hangs when nonzero.runMsguards total runtime when nonzero.
Timeout expiry marks the child as timed_out and runs the same cancellation path.
Cancellation is idempotent. For subprocess RPC cancellation:
- Send RPC
abortwhen protocol is alive. - End stdin when appropriate.
- Send SIGTERM to the process group on Unix.
- Use
taskkill /T /Fon Windows. - Escalate to SIGKILL after a short grace period on Unix.
- Resolve as
cancelledunless a terminal result already exists.
On parent session_shutdown, cancel supervised children by default.
Detached persistence is out of version one.
Status data model
Define a public status record that another extension can consume:
interface SubagentStatus {
id: string;
label: string;
agent?: string;
adHoc: boolean;
context: "independent" | "fork";
state: "queued" | "starting" | "running" | "settling" | "completed" | "failed" | "cancelled" | "timed_out" | "orphaned";
cwd: string;
model?: string;
thinking?: string;
tools: string;
startedAt: string;
completedAt?: string;
elapsedMs: number;
lastEvent?: string;
lastEventAt?: string;
stopReason?: string;
resultAvailable: boolean;
childSession?: string;
error?: string;
}
Expose status through registered tools and extension-local events.
Persist durable milestone entries with pi.appendEntry().
Durable status entries do not participate in LLM context.
Do not expose secrets, full environment, provider credentials, full prompts, or full child tool output in status rows.
Optional built-in UI
The default UI is optional and replaceable. It consumes the status data model. It must not be required for subagent lifecycle correctness.
Use three progressive layers:
- Collapsed live summary by default.
- Expanded live inspector on demand or by configuration.
- Durable transcript milestone entries for historical record.
Collapsed summary should show quick counts such as 2 running · 1 queued.
Expanded inspector should show child id, label, context mode, lifecycle state, elapsed time, model, tool profile, trust/source, last event, and result availability.
Transcript milestones should record spawn accepted, completed, failed, cancelled, and timed out.
Do not stream every child event into the parent transcript.
UI config gates only these components. Status tools and data remain available when UI is disabled.
Trust and security
Project config and project agent definitions are honored only after project trust. User/global config is in the user's local trust boundary. The extension must not claim sandboxing. Subagents run with the local user's permissions unless the user later routes them through an actual sandbox backend.
Do not inherit project-local extensions, project packages, or arbitrary project resources into child processes by default unless Pi's normal trusted project startup does so and the implementation explicitly documents it.
Sanitize child process environment.
Strip stale parent PI_SESSION_ID, PI_SESSION_FILE, PI_PROVIDER, PI_MODEL, and PI_REASONING_LEVEL unless intentionally projecting parent metadata.
Pass provider credentials only as required by child Pi.
The extension does not add runtime confirmation gates for fork or full-tools in version one.
If a request is ambiguous, the main agent may ask the user before spawning.
The extension should avoid over-prescribing agent judgment.
Diagnostics and validation
Startup or reload diagnostics should report:
- Invalid
subagents.jsonsyntax. - Unknown built-in profile override attempts.
- Unknown default tool profile.
- Duplicate agent names in one precedence tier.
- Invalid frontmatter.
- Agent
contextoutsideallowedContexts. - Unknown tool profile references.
Invalid project config should not break user/global config. Invalid agent definitions should be skipped with diagnostics rather than crashing the extension.
Tests and verification
Unit tests should cover:
- Config merge order.
- Project trust gating for project config and project agents.
- Agent frontmatter parsing.
- Duplicate name detection.
- Tool profile resolution.
- Built-in profile name reservation.
- Context resolution order.
- Spawn request validation.
- Non-blocking spawn acceptance result.
- Status state transitions.
- Result retrieval before and after completion.
- Cancellation idempotence.
- Timeout handling.
- Malformed RPC event handling.
- Oversized RPC line handling.
- Process exit before settlement.
- Settlement before process exit.
- Parent shutdown cleanup.
Manual verification:
nix eval .#nixosConfigurations.neogaia.config.modules.agents.pi.enable
nix eval .#nixosConfigurations.neogaia.config.home-manager.users.alexion.home.file.'"/home/alexion/.pi/agent/extensions"'.source
nix flake check
After implementation, verify inside Pi:
/reloadloads the extension.- A single ad hoc subagent spawn returns a child id immediately.
- A batch spawn returns multiple child ids immediately.
subagent_listshows active and recent terminal entries.subagent_statusshows current lifecycle state.subagent_resultreturns still-running before completion and final result after completion.subagent_cancelcancels a running child.- A
forkrun receives parent active-branch context. - An
independentrun does not receive parent transcript. - Disabling UI does not disable status/result tools.
Open implementation choices left to coding
Choose exact tool names in the implementation.
Choose exact JSON schema names for timeout fields and UI toggles.
Choose exact read-only bash allowlist for read-only-with-safe-bash, using Pi plan-mode as the reference.
Choose the default maxConcurrent value.
Choose final status entry custom type names.
These are implementation details, not remaining design blockers.