updated readme and test

This commit is contained in:
Amos
2026-08-25 02:21:20 +03:00
parent 076021b603
commit c3e8b53c07
2 changed files with 102 additions and 350 deletions
+101 -349
View File
@@ -1,440 +1,192 @@
# pi-interactive-subagents # pi-interactive-subagents
Async subagents for [pi](https://github.com/badlogic/pi-mono) — spawn, orchestrate, and manage sub-agent sessions in tmux panes. **Fully non-blocking** — the main agent keeps working while subagents run in the background. Async subagents for [pi](https://github.com/badlogic/pi-mono), running in tmux panes. Spawn a sub-agent, keep working in the main session, and get the result steered back when it finishes. Fully non-blocking.
## How It Works **tmux-only fork.** See [Acknowledgements](#acknowledgements) for the upstream project, which also supports cmux, zellij, and WezTerm.
Call `subagent()` and it **returns immediately**. The sub-agent runs in its own terminal pane. A live widget above the input shows all running agents with their current state — `starting`, `active`, `waiting`, `stalled`, or `running`. When a sub-agent finishes, its result is **steered back** into the main session as an async notification — triggering a new turn so the agent can process it. ## How it works
`subagent()` returns immediately. The sub-agent runs in its own tmux pane — a right split off the parent pi pane, so pane creation never steals keyboard focus. A live widget above the input tracks every running sub-agent, and when one finishes, its result is steered into the main session as a notification that triggers a new turn.
``` ```
╭─ Subagents ──────────────────────────── 2 running ─╮ ╭─ Subagents ──────────────────────────── 2 running ─╮
│ 00:23 Scout: Auth (scout) active · bash 7m │ │ 00:23 scout active · bash 7m │
│ 00:45 Scout: DB (scout) waiting 2m │ │ 00:45 scout-2 waiting 2m │
╰────────────────────────────────────────────────────╯ ╰────────────────────────────────────────────────────╯
``` ```
For parallel execution, just call `subagent` multiple times — they all run concurrently: Spawn several in parallel — they run concurrently and steer results back independently as each finishes.
```typescript Panes are kept evenly sized: the extension re-applies an `even-horizontal` layout after every spawn and exit (debounced). The layout is a single constant, `SUBAGENT_TMUX_LAYOUT` in `pi-extension/subagents/tmux.ts` — change it to any named tmux layout (`main-vertical`, `tiled`, …).
subagent({ name: "Scout: Auth", agent: "scout", task: "Analyze auth module" });
subagent({ name: "Scout: DB", agent: "scout", task: "Map database schema" });
// Both return immediately, results steer back independently
```
If your shell startup is slow and subagent commands sometimes get dropped before the prompt is ready, set `PI_SUBAGENT_SHELL_READY_DELAY_MS` to a higher value (defaults to `500`): If your shell startup is slow and launch commands get dropped before the prompt is ready, raise the delay:
```bash ```bash
export PI_SUBAGENT_SHELL_READY_DELAY_MS=2500 export PI_SUBAGENT_SHELL_READY_DELAY_MS=2500 # default: 500
``` ```
Subagent panes are created without stealing keyboard focus: splits target the parent pi pane by explicit id, so focus and pane creation are independent. Launch commands target child panes by id as well. ## Tools
### Pane layout | Tool | Description |
| --- | --- |
| `subagent` | Spawn a sub-agent in a dedicated tmux pane (async) |
| `subagent_message` | Message a sub-agent by name — steers it if running, resumes its session if finished |
| `subagents_list` | List available agent definitions |
| `ask_question` | *(sub-agent sessions only)* Ask the orchestrator a question and wait for the reply |
Each subagent is a horizontal split off the parent pi pane. Left to itself, tmux halves the target pane on every split and dumps freed space onto a neighbor on close, so panes drift to wildly uneven widths. To prevent that, the extension automatically re-applies an even layout to the subagent window after every spawn and every exit (debounced so a burst of parallel spawns or staggered exits collapses into a single layout call). There is also a `/subagent <agent> <task>` command for spawning directly.
The layout is a single constant in `pi-extension/subagents/tmux.ts`: ### Spawning
```ts
const SUBAGENT_TMUX_LAYOUT = "even-horizontal";
```
Change it to switch the geometry, then reload the extension (or restart pi):
| Value | Geometry |
| ------------------ | ------------------------------------------------------------------------ |
| `even-horizontal` | Equal columns across the window (matches a manual `Ctrl+b` then `Alt+1`). Default. |
| `main-vertical` | One large main pane on the left, subagents tiled in a column on the right. |
| `tiled` | Grid of roughly equal panes — scales best when many subagents run at once. |
The value is passed straight to `tmux select-layout`, so any named tmux layout works. An unrecognized value is ignored silently (the resize is best-effort and never blocks spawning), so double-check the spelling if a change seems to have no effect.
## What's Included
### Extensions
**Subagents** — 3 main-session tools + 1 command, plus 1 subagent-only tool:
| Tool | Description |
| ------------------ | ------------------------------------------------------------------------------------------------- |
| `subagent` | Spawn a sub-agent in a dedicated tmux pane (async — returns immediately) |
| `subagent_message` | Steer a running subagent or resume a finished one — by `name` either way |
| `subagents_list` | List available agent definitions |
| Command | Description |
| -------------------------- | ------------------------------------ |
| `/subagent <agent> <task>` | Spawn a named agent directly |
### Bundled Agents
| Agent | Model | Tools | Role |
| -------------- | ------ | ------------------------------------------------ | ----------------------------------------------------------------- |
| **scout** | Haiku | `read`, `grep`, `find`, `ls` | Fast read-only codebase recon — maps files, patterns, conventions |
| **researcher** | Sonnet | `web_search`, `web_fetch` | Web research — searches and synthesizes a sourced brief |
| **worker** | Sonnet | `read`, `write`, `edit`, `safe_bash`, `web_search`, `web_fetch` + spawning | General implementer — writes code, runs commands; may dispatch `scout`/`researcher` to protect its context |
The **worker** is the only bundled agent granted the spawning toolset, and it may only spawn `scout` and `researcher` (see [`subagent_agents`](#subagent_agents)). `scout` and `researcher` are autonomous (`auto-exit: true`); `worker` runs interactively.
Agent discovery follows priority: **project-local** (`.pi/agents/`) > **global** (`~/.pi/agent/agents/`) > **package-bundled**. Override any bundled agent by placing your own version in the higher-priority location.
---
## Async Subagent Flow
```
1. Agent calls subagent() → returns immediately ("started")
2. Sub-agent runs in tmux pane → widget shows live status
3. User keeps chatting → main session fully interactive
4. Sub-agent finishes → result steered back as a normal completion/failure
5. Main agent processes result → continues with new context
```
Multiple subagents run concurrently — each steers its result back independently as it finishes. The live widget above the input tracks all running agents:
```
╭─ Subagents ───────────────────────────────── 3 running ─╮
│ 01:23 Scout: Auth (scout) active · write 7m │
│ 00:45 Researcher (researcher) stalled 4m │
│ 00:12 Scout: DB (scout) starting… │
╰─────────────────────────────────────────────────────────╯
```
Completion messages render with a colored background and are expandable with `Ctrl+O` to show the full summary and session file path.
### In-progress status updates
The widget tracks each Pi-backed sub-agent from a child-written runtime snapshot and labels it with a coarse state:
- `starting` — launched, but no valid child snapshot has been observed yet
- `active` — the child is doing observed runtime work: agent turn, provider request, streaming, or tool execution
- `waiting` — the child finished a turn and is intentionally open for more input or another stage
- `stalled` — the parent has gone too long without a valid current child snapshot and can no longer trust the run is healthy
- `running` — fallback for backends without child snapshots (e.g. Claude)
These labels are no longer derived from session-file growth. Session JSONL is still used for transcript, resume, lineage, and result extraction, but Pi-backed liveness now comes from a small activity snapshot written by the child extension. A fixed internal watchdog marks a run as `stalled` when valid snapshots never appear, stop being readable, or stop matching the current child; valid long-running `active` or `waiting` states do not become `stalled` just because time passes. When a run enters `stalled` or recovers from it, the parent agent receives a steer message so it can react. All other status transitions stay in the widget only.
**Interactive subagents stay silent.** Long-running user-driven subagents (e.g. `worker`) do not wake the parent session on `stalled`/`recovered` transitions — the user is working directly in the subagent's pane, and a steer message there would just burn an orchestrator turn on a no-op "still waiting" ping. The widget still updates normally, and child snapshots are still recorded/classified regardless of the `interactive` setting. By default, agents with `auto-exit: true` are treated as autonomous and get stall pings; agents without it are treated as interactive and stay quiet. Override per-agent with `interactive: true|false` in frontmatter.
#### Configuration
Status display is controlled by `config.json` in the extension directory. Copy `config.json.example` to get started:
```bash
cp config.json.example config.json
```
```json
{
"status": {
"enabled": true
}
}
```
`config.json` is gitignored so local overrides don't get committed.
---
## Spawning Subagents
```typescript ```typescript
// Spawn an agent by its profile name subagent({ agent: "scout", task: "Analyze the auth module" });
subagent({ agent: "scout", task: "Analyze the codebase..." });
// Cosmetic pane label (defaults to the agent name when omitted)
subagent({ agent: "worker", name: "dark-mode", task: "Implement the dark mode toggle" }); subagent({ agent: "worker", name: "dark-mode", task: "Implement the dark mode toggle" });
// Custom working directory
subagent({ agent: "game-designer", cwd: "agents/game-designer", task: "..." });
``` ```
### Parameters | Parameter | Type | Default | Description |
| --------- | ---- | ------- | ----------- |
| `agent` | string | required | Which agent to spawn (must be known and permitted) |
| `task` | string | required | Task prompt |
| `name` | string | agent name | Display name for the pane and widget. Must be unique — duplicates are auto-suffixed (`scout`, `scout-2`, …) |
| `model` | string | agent's model | Override the model for this spawn |
| `cwd` | string | agent's `cwd` | Working directory (see [Role folders](#role-folders)) |
The schema is intentionally small: each agent has a fixed, well-defined loadout (model, tools, system prompt) baked into its definition, so per-spawn override knobs were removed. You pick an `agent` and give it a `task`. ### Messaging
| Parameter | Type | Default | Description | `subagent_message` is addressed **by name only**. Names are unique per session and persist after a sub-agent finishes, so the same name works either way:
| --------- | ------ | -------------- | -------------------------------------------------------------------------------------------- |
| `agent` | string | required | Which agent to spawn. Must be a known/permitted agent (see [`subagents_list`](#subagents_list)). Loads that agent's fixed profile. |
| `task` | string | required | Task prompt for the sub-agent |
| `name` | string | _agent name_ | Optional cosmetic label for the pane and widget row. Defaults to the agent name. Does not select the agent. |
| `model` | string | — | Override the agent's default model for this spawn |
| `cwd` | string | — | Working directory for the sub-agent (see [Role Folders](#role-folders)) |
---
## Messaging a subagent
`subagent_message` is the single tool for talking to a subagent after it has been spawned, and it is addressed **by name only**. Names are unique within your session and persist after a subagent finishes, so the *same* name works whether the subagent is running or finished — you never juggle two kinds of handle:
```typescript ```typescript
subagent_message({ name: "Scout", message: "Also check the auth middleware" }); subagent_message({ name: "scout", message: "Also check the auth middleware" });
``` ```
- **If it's still running** — the message is typed directly into the live pane (newlines flattened to spaces so it fires as one turn) and picked up at the next turn boundary. The call returns immediately and does **not**, by itself, produce a new result — the subagent's eventual completion still arrives as a steer message. The widget moves the child to `waiting` until it resumes work. - **Running** — the message is typed into the live pane (newlines flattened) and picked up at the next turn boundary. The call returns immediately; the eventual completion still arrives as a steer message.
- **If it has finished** — the message resumes that session and continues it, exactly like a fresh `subagent` spawn: fire-and-forget async, always autonomous, result delivered later as a steer message. The resumed run reclaims its original name (it *is* that subagent, continued). - **Finished** — the session is resumed with the message as the follow-up task, like a fresh spawn: fire-and-forget, always autonomous, result steered back later. The resumed run reclaims its original name.
**Names are unique per session, persisted to disk.** Every spawn records its name → session-file mapping in a per-session registry at `artifacts/<yourSessionId>/subagent-registry.json`. Defaulted names auto-suffix against the *whole* registry (not just currently-running subagents), so a third `scout` is `scout-3` even after `scout` and `scout-2` have finished — every name stays addressable forever, including across a pi restart. A nested subagent that spawns its own children gets its own registry keyed by its own session id, so uniqueness is scoped per spawner. Every spawn records name → session file in `artifacts/<sessionId>/subagent-registry.json`, so names stay addressable across pi restarts. A nested sub-agent that spawns children gets its own registry keyed by its own session id. Resume is refused with a clear error (listing known names) if the name isn't registered, the session file is gone, or the session predates sandboxed resume.
**Resume replays the original sandbox.** A resumed subagent is *not* relaunched as a bare `pi --session`. At spawn time the extension snapshots the agent's fully-resolved loadout — the `--tools` allowlist, backing extensions, model + thinking level, identity/system prompt, spawn whitelist, cwd, and config dir — into a `<session>.loadout.json` sidecar next to the session file. Resume reads that snapshot and reconstructs the exact same `--no-extensions` + `--tools` sandbox, so the reincarnated process gets the same restricted toolset it originally ran with instead of pi's default (all global extensions + the full toolset). The snapshot is stored, not re-derived from the agent `.md` by name, so resume stays faithful even if the agent definition was later edited, moved, or deleted. **Resume replays the original sandbox.** At spawn time the fully-resolved loadout — tool allowlist, backing extensions, model, thinking level, system prompt, spawn whitelist, cwd — is snapshotted to `<session>.loadout.json`. Resume rebuilds the exact same restricted process from that snapshot rather than relaunching unrestricted.
> **Guard:** naming a still-running subagent never spawns a second process on the same session file — it just steers the live one. There is no hard-abort tool; to forcibly stop a subagent, use its pane directly. ### ask_question
> **Refusal:** resume is refused with a clear error when the name isn't in this session's registry (the error lists the known names), when the mapped session file is gone, or when the session has no loadout snapshot (it predates sandboxed resume). Re-run the task as a fresh subagent instead. A sub-agent can ask its orchestrator a single freeform question when requirements are ambiguous or a decision materially affects the work. The session **stays open** (parked as `waiting`) instead of exiting; the parent is notified with the sub-agent's name, replies via `subagent_message({ name, message })`, and the reply arrives as the sub-agent's next turn. Parallel questions are supported — each waiting sub-agent has its own name.
**`subagent_message` parameters:** If the reply arrives while the sub-agent is still mid-turn, it is absorbed into the current turn — either way the question is marked answered and the session exits normally when the work is done. If the parent never replies, the pane stays open until a human closes it. Only available inside sub-agent sessions.
- `name` (required) — exact display name of the subagent. Steers it if running, resumes its session if finished.
- `message` (required) — the instruction or next task to deliver.
--- ## Bundled agents
## ask_question — Subagent Asks the Orchestrator | Agent | Model | Tools | Role |
| ----- | ----- | ----- | ---- |
| **scout** | `openrouter/z-ai/glm-5.3` | `read`, `grep`, `find`, `ls` | Fast read-only codebase recon |
| **researcher** | `openrouter/z-ai/glm-5.3` | `web_search`, `web_fetch`, `safe_bash` | Web research, synthesized into a sourced brief |
| **worker** | `openrouter/z-ai/glm-5.3` | `read`, `write`, `edit`, `bash`, `web_search`, `web_fetch` + spawning | General implementer; may spawn `scout` and `researcher` |
The `ask_question` tool lets a subagent ask its parent (the orchestrator that spawned it) a single freeform question — for ambiguous requirements, a decision that materially affects the work, a blocker, or confirmation only the parent has. It is framed like the top-level `ask_user_question` tool so agents reach for it proactively instead of guessing. All three are autonomous (`auto-exit: true`) and carry their identity in the system prompt (`system-prompt: append`).
Unlike the old `caller_ping`, **the session stays open while it waits.** Auto-exit is suppressed for that turn, the subagent parks in a `waiting` state, and the parent replies with `subagent_message` — which lands as the subagent's next turn so it continues right where it paused. ## Custom agents
**`ask_question` parameters:** Place a `.md` file in `.pi/agents/` (project) or `~/.pi/agent/agents/` (global). Discovery priority: **project > global > package-bundled** — a project-local file overrides a bundled agent with the same name.
- `question` (required): The single freeform question. Include enough context to answer it directly. No options/multiple-choice — ask one question per call.
**Interaction flow:**
1. Child calls `ask_question({ question: "Found two migration files — use v1 or v2?" })`
2. The child's turn ends but the **session stays open** (parked as `waiting`); the tool returns *"Question sent — stop and wait for the reply."*
3. The parent's watcher picks up the question and steers a notification: *"Sub-agent worker-2 asks: Found two migration files — use v1 or v2?"* plus the unique subagent name
4. Parent replies via `subagent_message({ name, message })` — the same name steers it while it runs or resumes it if it has since exited
5. The reply arrives as the subagent's next message and it continues with the parent's guidance
**Parallel questions are supported.** Each subagent has its own session and its own watcher, so multiple subagents can be waiting on answers at once. Default names are deduped at spawn (`worker`, `worker-2`, …), and every notification surfaces that unique name, so each reply targets the right subagent.
**Example:**
```typescript
// Inside a worker subagent
await ask_question({
question: "Found two conflicting migration files — should I use v1 or v2?"
});
// Session stays open. Parent answers with subagent_message({ name: "worker", message: "Use v2…" }),
// which arrives as this subagent's next turn.
```
> **Note:** `ask_question` is only available inside subagent contexts. Calling it from a standalone pi session returns an error.
>
> If the parent never replies, the subagent stays open indefinitely (shown as `waiting`) until the human closes its pane — mirroring interactive-agent behavior. There is no timeout.
---
## Custom Agents
Place a `.md` file in `.pi/agents/` (project) or `~/.pi/agent/agents/` (global):
```markdown ```markdown
--- ---
name: my-agent name: my-agent
description: Does something specific description: Does something specific
model: anthropic/claude-sonnet-4-6 model: openrouter/z-ai/glm-5.3
thinking: minimal thinking: medium
tools: read, edit, write, safe_bash, web_search tools: read, edit, write, safe_bash, web_search
session-mode: lineage-only session-mode: lineage-only
auto-exit: true
--- ---
# My Agent
You are a specialized agent that does X... You are a specialized agent that does X...
``` ```
The `tools` field is a strict allowlist. The child process is launched with extension discovery disabled (`--no-extensions`) and only the extensions backing the listed tools are loaded back in. An agent that lists no spawning tools and no `subagent_agents` simply cannot spawn anything. ### Frontmatter reference
### Frontmatter Reference | Field | Type | Description |
| ----- | ---- | ----------- |
| `name` | string | Agent name (used in `agent: "my-agent"`) |
| `description` | string | Shown in `subagents_list` |
| `model` | string | Default model |
| `thinking` | string | `minimal`, `low`, `medium`, or `high` |
| `tools` | string | Strict tool allowlist. Built-ins: `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`. Extension-backed: `web_search`, `web_fetch`, `safe_bash`, `video_extract`, `youtube_search`, `google_image_search`. Only the extensions backing the listed tools are loaded into the child |
| `subagent_agents` | string | Comma-separated agent names this agent may spawn. **Presence of this field grants the spawning toolset** (`subagent`, `subagent_message`, `subagents_list`) and restricts spawn targets to the list. Omit it and the agent cannot spawn at all |
| `skills` | string | Comma-separated skill names to auto-load |
| `session-mode` | string | `standalone` (default), `lineage-only`, or `fork` — see below |
| `system-prompt` | string | `append` or `replace`: pass the body as the child's `--append-system-prompt` / `--system-prompt`. Omit and the body is prepended to the task prompt instead |
| `auto-exit` | boolean | Auto-shutdown when the agent finishes (see below) |
| `interactive` | boolean | Whether stall/recovery transitions wake the parent (see below) |
| `cwd` | string | Default working directory |
| `disable-model-invocation` | boolean | Hide from `subagents_list`; still spawnable by explicit name |
| `cli` | string | `claude` runs the agent via the Claude Code CLI instead of pi |
| Field | Type | Description | ### session-mode
| ------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | string | Agent name (used in `agent: "my-agent"`) |
| `description` | string | Shown in `subagents_list` output |
| `model` | string | Default model (e.g. `anthropic/claude-sonnet-4-6`) |
| `thinking` | string | Thinking level: `minimal`, `medium`, `high` |
| `tools` | string | Strict allowlist of tool names. Built-ins (`read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`) plus custom-extension tools (`web_search`, `web_fetch`, `safe_bash`, `video_extract`, `youtube_search`, `google_image_search`). Only the extensions backing these tools are loaded into the child. |
| `subagent_agents` | string | Comma-separated list of agent names this agent may spawn. **Presence of this field grants the full spawning toolset** (`subagent`, `subagent_message`, `subagents_list`) and restricts spawn targets to the listed agents. Omit it and the agent cannot spawn at all. |
| `skills` | string | Comma-separated skill names to auto-load |
| `session-mode` | string | Default child-session mode: `standalone`, `lineage-only`, or `fork` |
| `auto-exit` | boolean | Auto-shutdown when the agent finishes its turn. If the user sends any input, auto-exit is permanently disabled and the user takes over the session. Recommended for autonomous agents (scout, researcher); not for interactive ones (worker). Also determines the default value of `interactive` (see below). |
| `interactive` | boolean | derived | Override whether stall/recovery transitions wake the parent session. Defaults to the inverse of `auto-exit`: autonomous agents (`auto-exit: true`) are non-interactive and get stall pings; agents without `auto-exit` are interactive and stay quiet. Explicit values take precedence. |
| `cwd` | string | Default working directory (absolute or relative to project root) |
| `disable-model-invocation` | boolean | Hide this agent from discovery surfaces like `subagents_list`. The agent still remains directly invokable by explicit name via `subagent({ agent: "name", ... })`. |
--- - `standalone` — fresh session, no lineage link to the caller (default)
- `lineage-only` — fresh session with `parentSession` linkage for discovery/fork UX, but no copied turns
- `fork` — child session seeded with the caller's conversation context
Discovery still resolves precedence before visibility filtering. If a project-local hidden agent has the same name as a visible global or bundled agent, the hidden project agent wins and the lower-precedence agent does not appear in `subagents_list`. ### auto-exit
### `session-mode` With `auto-exit: true`, the session shuts down when the agent's turn ends — the agent just writes its final message and stops (there is no "done" tool). The last assistant message becomes the summary returned to the parent. Recommended for all autonomous agents.
Choose how a subagent session starts: Notes:
- `standalone` — default fresh session with no lineage link to the caller - **Manual input does not strand an auto-exit sub-agent.** If a human types into the pane, the session still closes once that turn completes normally — only an escape/abort leaves it open.
- `lineage-only` — fresh blank child session with `parentSession` linkage, but no copied turns from the caller - **Auto-exit is suppressed while work is in flight:** the session parks as `waiting` instead of exiting when an `ask_question` is still unanswered, or when the agent's own child sub-agents are still running (a worker can stop after dispatching children and stays open until the last result returns).
- `fork` — linked child session seeded with the caller's prior conversation context
`lineage-only` is useful when you want session discovery and fork lineage UX to show the relationship later, but you do **not** want the child to inherit the parent's turns. Set the mode per agent via the `session-mode` frontmatter field below. ### interactive
```yaml Controls whether `stalled`/`recovered` status transitions send a steer message to the parent session. Defaults to the inverse of `auto-exit`: autonomous agents get stall pings; user-driven agents stay quiet (the user is already working in that pane — the widget still updates). Set explicitly to override.
---
name: worker
session-mode: lineage-only
---
```
### `auto-exit` ## Tool access control
When set to `true`, the agent session shuts down automatically as soon as the agent finishes its turn. The agent just writes its final message and stops; there is no "done" tool to call. The last assistant message becomes the summary returned to the parent. Access is **whitelist-only**. Every sub-agent process is launched with `--no-extensions` (extension discovery disabled) and `--tools <allowlist>`; only the extensions backing the listed tools are loaded back in explicitly. There is no default toolset and no deny-list — an agent gets exactly what its frontmatter lists. The restriction survives resume via the loadout snapshot.
**Behavior:** Spawns must name a known agent at **every** depth. A top-level session may spawn anything discoverable; a sub-agent may only spawn the agents in its `subagent_agents` list (enforced via `PI_SUBAGENT_ALLOWED`). There is no agentless spawn route, so a child can never escalate to a full-toolset profile by omitting its agent.
- The session closes after the agent's final message (on the `agent_end` event) Extensions can register additional tools for sub-agents at runtime via `registerToolExtension(name, path)` on the `__pi_interactive_subagents` process global.
- If the user sends **any input** before the agent finishes, auto-exit is permanently disabled for that session — the user takes over interactively
- The modeHint injected into the agent's task is adjusted accordingly: autonomous agents are told to simply stop when finished, while interactive agents are told the session ends when the user exits the pane
**Auto-exit is suppressed while work is still in flight.** Even an `auto-exit` agent will *not* shut down at the end of a turn when either: ## Role folders
- it has a pending [`ask_question`](#ask_question--subagent-asks-the-orchestrator) awaiting the orchestrator's reply, or `cwd` starts a sub-agent in a directory with its own config, so role-specific setups (CLAUDE.md, skills, extensions) apply:
- it spawned **its own child subagents** (e.g. a `worker` delegating to `scout`/`researcher`) that haven't finished yet.
In both cases the session parks as `waiting` instead of exiting, and resumes when the next turn lands (the reply, or a child's result steered back). This is what lets a worker write *"I'll wait for the results"* and stop without killing the session out from under its children — it stays open until the **last** child reports back, then the worker's final summary is returned to the orchestrator as usual. Child liveness is read from the spawning extension's live count, so a nested agent that never spawned anything still exits immediately as before.
**When to use:**
- ✅ Autonomous agents (scout, researcher) that run to completion
- ❌ Interactive agents (worker) where the user drives the session
```yaml
---
name: scout
auto-exit: true
---
```
### `interactive`
Controls whether status transitions (`stalled`, `recovered`) wake the parent session with a steer message.
**Default:** the inverse of `auto-exit`. Autonomous agents (`auto-exit: true`) are non-interactive and ping the parent on stall/recovery; agents without `auto-exit` are interactive and stay quiet.
**Why it exists:** Interactive agents can run for minutes or hours while the user thinks, types, and reads in the subagent's pane. Child snapshots still update the widget, but stalled/recovered supervision messages rarely need to wake the parent for user-driven sessions. Skipping the steer keeps the parent quiet until the child actually finishes.
**When to override:**
- Set `interactive: false` on an agent that doesn't auto-exit but you still want stall pings for
- Set `interactive: true` on an autonomous agent you'd rather check on yourself
```yaml
---
name: worker
# interactive defaults to true because auto-exit is not set
---
```
---
## Tool Access Control
Access is **whitelist-only**. Every sub-agent process is launched with `--no-extensions` (extension discovery disabled) and `--tools <allowlist>`. Only the tools named in the agent's `tools` frontmatter are exposed, and only the extensions that register those tools are loaded back in via explicit `--extension` flags. There is no global default toolset and no deny-list to maintain — an agent gets exactly what it asks for.
This restriction survives **resume**: the resolved sandbox is snapshotted to a `<session>.loadout.json` sidecar at spawn and replayed verbatim when the session is later resumed via `subagent_message({ name })`. A session with no snapshot is refused rather than relaunched unrestricted, so there is no path by which a resume silently escalates a sub-agent back to the full toolset + all global extensions.
### Spawns must name a known agent
The agent whitelist is enforced at **every** depth, not just for restricted sub-agents:
- A **restricted sub-agent** (one launched with `PI_SUBAGENT_ALLOWED`) may only spawn the agents pinned in its `subagent_agents` list.
- A **top-level session** may spawn any agent that appears in `subagents_list` (every discoverable definition).
Every `subagent` call must set `agent` to a name in the caller's permitted set. A missing `agent`, or one that doesn't resolve to a real definition (e.g. `agent: "wizard"`), is rejected — there is no agentless spawn route, so a child can never launch with an unrestricted, full-toolset profile by omitting its agent.
### Granting the ability to spawn: `subagent_agents`
Spawning is **off by default**. An agent cannot spawn sub-agents unless its frontmatter declares a `subagent_agents` list. Presence of that field:
1. Grants the full spawning toolset (`subagent`, `subagent_message`, `subagents_list`) — you do **not** list these in `tools`.
2. Loads this extension into the child process.
3. Restricts the child to spawning **only** the named agents. The child's `subagents_list` is filtered to that set, and `subagent` calls for any other agent are rejected. Enforced via the `PI_SUBAGENT_ALLOWED` env var.
```yaml
---
name: worker
tools: read, write, edit, safe_bash, web_search, web_fetch
subagent_agents: scout, researcher
---
```
### Recommended Configuration
| Agent | `subagent_agents` | Rationale |
| ---------- | ------------------- | ------------------------------------------------------ |
| worker | `scout, researcher` | Delegates recon/research to protect its own context |
| scout | _(omitted)_ | Read-only recon; should gather context, not spawn |
| researcher | _(omitted)_ | Web research only; should research, not spawn |
---
## Role Folders
The `cwd` parameter lets sub-agents start in a specific directory with its own configuration:
``` ```
project/ project/
├── agents/ └── agents/
│ ├── game-designer/ ├── game-designer/ ← CLAUDE.md, .pi/…
│ │ └── CLAUDE.md ← "You are a game designer..." └── sre/ ← CLAUDE.md, .pi/…
│ ├── sre/
│ │ ├── CLAUDE.md ← "You are an SRE specialist..."
│ │ └── .pi/skills/ ← SRE-specific skills
│ └── narrative/
│ └── CLAUDE.md ← "You are a narrative designer..."
``` ```
```typescript ```typescript
subagent({ name: "Game Designer", cwd: "agents/game-designer", task: "Design the combat system" }); subagent({ agent: "worker", cwd: "agents/sre", task: "Review the deployment pipeline" });
subagent({ name: "SRE", cwd: "agents/sre", task: "Review deployment pipeline" });
``` ```
Set a default `cwd` in agent frontmatter: Set a per-agent default with `cwd:` in frontmatter.
```yaml ## Status widget & configuration
---
name: game-designer The widget tracks each sub-agent from a runtime activity snapshot written by the child: `starting`, `active` (turn/provider/tool work), `waiting` (open for input or another stage), `stalled` (no valid snapshot for too long), or `running` (fallback). Sub-agent sessions also show their own tools widget — toggle it with `Ctrl+Alt+O`. Completion messages expand with `Ctrl+O`.
cwd: ./agents/game-designer
tools: read, write, edit, safe_bash Status display is configured via `config.json` in the extension directory (copy `config.json.example`; it's gitignored):
---
```json
{
"status": { "enabled": true }
}
``` ```
---
## Tools Widget
Every sub-agent session displays a compact tools widget showing the agent's allowlisted tools. Toggle with `Ctrl+Alt+O`:
```
[scout] — 4 tools (Ctrl+Alt+O) ← collapsed
[scout] — 4 available (Ctrl+Alt+O to collapse) ← expanded
read, grep, find, ls
```
---
## Requirements ## Requirements
- [pi](https://github.com/badlogic/pi-mono) — the coding agent - [pi](https://github.com/badlogic/pi-mono)
- [tmux](https://github.com/tmux/tmux) - [tmux](https://github.com/tmux/tmux)
```bash ```bash
tmux new -A -s pi 'pi' tmux new -A -s pi 'pi'
``` ```
> This fork is **tmux-only**. It was forked from [HazAT/pi-interactive-subagents](https://github.com/HazAT/pi-interactive-subagents), which also supports cmux, zellij, and WezTerm — look there if you need another multiplexer.
---
## Acknowledgements ## Acknowledgements
Forked from [HazAT/pi-interactive-subagents](https://github.com/HazAT/pi-interactive-subagents), which originated the subagent architecture, the multi-multiplexer surface layer, and the status widget. The sub-agent status supervision and turn-only interruption features were inspired by [RepoPrompt](https://repoprompt.com/)'s sub-agent snapshot polling and run cancellation features. Forked from [HazAT/pi-interactive-subagents](https://github.com/HazAT/pi-interactive-subagents), which originated the subagent architecture, the multi-multiplexer surface layer, and the status widget; its supervision features were inspired by [RepoPrompt](https://repoprompt.com/).
---
## License ## License
+1 -1
View File
@@ -1195,7 +1195,7 @@ describe("subagent discovery", () => {
for (const t of ["subagent", "subagent_message", "subagents_list"]) { for (const t of ["subagent", "subagent_message", "subagents_list"]) {
assert.ok(tools.has(t), `expected spawning tool ${t} in worker allowlist`); assert.ok(tools.has(t), `expected spawning tool ${t} in worker allowlist`);
} }
assert.ok(tools.has("safe_bash"), "expected worker to keep safe_bash"); assert.ok(tools.has("bash"), "expected worker to keep bash");
}); });
it("scout and researcher are not granted spawning tools", () => { it("scout and researcher are not granted spawning tools", () => {