580 lines
19 KiB
Markdown
580 lines
19 KiB
Markdown
---
|
|
status: done
|
|
claimed-by: "019fba91-eabf-76ae-b086-a37ac061d6e8"
|
|
claimed-at: "2026-07-31T22:23:32-04:00"
|
|
completed-at: "2026-07-31T22:23:42-04:00"
|
|
parent: "[[002-pi-subagents-map]]"
|
|
blocked-by: []
|
|
tags:
|
|
- ticket/task/afk
|
|
---
|
|
|
|
# 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:
|
|
|
|
```text
|
|
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.json` through Pi's `CONFIG_DIR_NAME`.
|
|
- Global named agents: `~/.pi/agent/agents/*.md`.
|
|
- Trusted project named agents: `.pi/agents/*.md` through Pi's `CONFIG_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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```yaml
|
|
name: review
|
|
description: Review code and report risks.
|
|
```
|
|
|
|
Optional frontmatter:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
1. Trusted project `.pi/agents/*.md`.
|
|
2. 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 `agent` is 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:
|
|
|
|
1. Spawn-call override.
|
|
2. Named agent frontmatter `context`.
|
|
3. Config `defaultContext`.
|
|
4. 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:
|
|
|
|
1. Spawn-call override.
|
|
2. Named agent frontmatter `tools`.
|
|
3. Config `defaultTools`.
|
|
4. 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:
|
|
|
|
1. Spawn-call override.
|
|
2. Named agent frontmatter.
|
|
3. Config default if later added.
|
|
4. 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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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`.
|
|
- `orphaned` if 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:
|
|
|
|
- `startMs` guards process startup and prompt acceptance.
|
|
- `idleMs` guards no-progress hangs when nonzero.
|
|
- `runMs` guards 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:
|
|
|
|
1. Send RPC `abort` when protocol is alive.
|
|
2. End stdin when appropriate.
|
|
3. Send SIGTERM to the process group on Unix.
|
|
4. Use `taskkill /T /F` on Windows.
|
|
5. Escalate to SIGKILL after a short grace period on Unix.
|
|
6. Resolve as `cancelled` unless 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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
1. Collapsed live summary by default.
|
|
2. Expanded live inspector on demand or by configuration.
|
|
3. 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.json` syntax.
|
|
- Unknown built-in profile override attempts.
|
|
- Unknown default tool profile.
|
|
- Duplicate agent names in one precedence tier.
|
|
- Invalid frontmatter.
|
|
- Agent `context` outside `allowedContexts`.
|
|
- 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:
|
|
|
|
```bash
|
|
nix eval .#nixosConfigurations.neogaia.config.modules.agents.pi.enable
|
|
```
|
|
|
|
```bash
|
|
nix eval .#nixosConfigurations.neogaia.config.home-manager.users.alexion.home.file.'"/home/alexion/.pi/agent/extensions"'.source
|
|
```
|
|
|
|
```bash
|
|
nix flake check
|
|
```
|
|
|
|
After implementation, verify inside Pi:
|
|
|
|
- `/reload` loads the extension.
|
|
- A single ad hoc subagent spawn returns a child id immediately.
|
|
- A batch spawn returns multiple child ids immediately.
|
|
- `subagent_list` shows active and recent terminal entries.
|
|
- `subagent_status` shows current lifecycle state.
|
|
- `subagent_result` returns still-running before completion and final result after completion.
|
|
- `subagent_cancel` cancels a running child.
|
|
- A `fork` run receives parent active-branch context.
|
|
- An `independent` run 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.
|