455 lines
24 KiB
Markdown
455 lines
24 KiB
Markdown
# pi-interactive-subagents
|
|
|
|
Async subagents for [pi](https://github.com/badlogic/pi-mono) — spawn, orchestrate, and manage sub-agent sessions in multiplexer panes. **Fully non-blocking** — the main agent keeps working while subagents run in the background.
|
|
|
|
https://github.com/user-attachments/assets/30adb156-cfb4-4c47-84ca-dd4aa80cba9f
|
|
|
|
## How It Works
|
|
|
|
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.
|
|
|
|
```
|
|
╭─ Subagents ──────────────────────────── 2 running ─╮
|
|
│ 00:23 Scout: Auth (scout) active · bash 7m │
|
|
│ 00:45 Scout: DB (scout) waiting 2m │
|
|
╰────────────────────────────────────────────────────╯
|
|
```
|
|
|
|
For parallel execution, just call `subagent` multiple times — they all run concurrently:
|
|
|
|
```typescript
|
|
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
|
|
```
|
|
|
|
## Install
|
|
|
|
```bash
|
|
pi install git:github.com/HazAT/pi-interactive-subagents
|
|
```
|
|
|
|
Supported multiplexers:
|
|
|
|
- [cmux](https://github.com/manaflow-ai/cmux)
|
|
- [tmux](https://github.com/tmux/tmux)
|
|
- [zellij](https://zellij.dev)
|
|
- [WezTerm](https://wezfurlong.org/wezterm/) (terminal emulator with built-in multiplexing)
|
|
|
|
Start pi inside one of them:
|
|
|
|
```bash
|
|
cmux pi
|
|
# or
|
|
tmux new -A -s pi 'pi'
|
|
# or
|
|
zellij --session pi # then run: pi
|
|
# or
|
|
# just run pi inside WezTerm — no wrapper needed
|
|
```
|
|
|
|
Optional: set `PI_SUBAGENT_MUX=cmux|tmux|zellij|wezterm` to force a specific backend.
|
|
|
|
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`):
|
|
|
|
```bash
|
|
export PI_SUBAGENT_SHELL_READY_DELAY_MS=2500
|
|
```
|
|
|
|
Subagent panes are created without stealing keyboard focus (cmux, tmux). Launch commands target child surfaces by explicit ID, so focus and command delivery are independent. Note: the `interactive` option controls parent status notifications, not terminal focus.
|
|
|
|
## What's Included
|
|
|
|
### Extensions
|
|
|
|
**Subagents** — 4 main-session tools + 1 command, plus 1 subagent-only tool:
|
|
|
|
| Tool | Description |
|
|
| -------------------- | ------------------------------------------------------------------------------------------- |
|
|
| `subagent` | Spawn a sub-agent in a dedicated multiplexer pane (async — returns immediately) |
|
|
| `subagent_interrupt` | Interrupt a running Pi-backed subagent's current turn |
|
|
| `subagents_list` | List available agent definitions |
|
|
| `subagent_resume` | Resume a previous sub-agent session (async) |
|
|
|
|
| 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 mux 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`, or any `fork: true` spawn) 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, or per-spawn with `interactive: true|false` on the tool call.
|
|
|
|
#### 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
|
|
// Named agent with defaults from agent definition
|
|
subagent({ name: "Scout", agent: "scout", task: "Analyze the codebase..." });
|
|
|
|
// Force a full-context fork for this spawn
|
|
subagent({ name: "Fix", fork: true, task: "Fix the bug where..." });
|
|
|
|
// Interactive worker the user can drive in its own pane
|
|
subagent({ name: "Worker", agent: "worker", task: "Implement the dark mode toggle" });
|
|
|
|
// Custom working directory
|
|
subagent({ name: "Designer", agent: "game-designer", cwd: "agents/game-designer", task: "..." });
|
|
```
|
|
|
|
### Parameters
|
|
|
|
| Parameter | Type | Default | Description |
|
|
| ---------------------- | ------- | -------------- | ------------------------------------------------------------------------------------------------- |
|
|
| `name` | string | required | Display name (shown in widget and pane title) |
|
|
| `task` | string | required | Task prompt for the sub-agent |
|
|
| `agent` | string | — | Load defaults from agent definition |
|
|
| `fork` | boolean | `false` | Force the full-context fork mode for this spawn, overriding any agent `session-mode` frontmatter |
|
|
| `interactive` | boolean | derived | Mark this spawn as interactive (don't wake the parent on stall/recovery). Defaults to the agent's `interactive` frontmatter, otherwise the inverse of `auto-exit`. |
|
|
| `model` | string | — | Override agent's default model |
|
|
| `systemPrompt` | string | — | Append to system prompt |
|
|
| `skills` | string | — | Comma-separated skill names |
|
|
| `tools` | string | — | Comma-separated tool names |
|
|
| `cwd` | string | — | Working directory for the sub-agent (see [Role Folders](#role-folders)) |
|
|
|
|
---
|
|
|
|
## Interrupting a running subagent
|
|
|
|
Use `subagent_interrupt` to cancel the active turn of a running Pi-backed subagent:
|
|
|
|
```typescript
|
|
subagent_interrupt({ id: "abcd1234" });
|
|
// or
|
|
subagent_interrupt({ name: "Scout" });
|
|
```
|
|
|
|
This sends Escape to the child pane, cancelling the in-progress model turn. The subagent session stays alive — the pane, session file, and background polling all remain intact. After the interrupt, the widget immediately moves the child back to `waiting`, and stale pre-interrupt snapshots are ignored. If the child starts work later, newer snapshots return it to `active`; completion, failure, and `caller_ping` still flow through normally.
|
|
|
|
This is a turn-level interrupt, not a method for forcibly terminating a subagent session.
|
|
|
|
> **Note:** Only Pi-backed subagents are supported. Claude-backed runs will return an error.
|
|
|
|
---
|
|
|
|
## caller_ping — Child-to-Parent Help Request
|
|
|
|
The `caller_ping` tool lets a subagent request help from its parent agent. When called, the child session **exits** and the parent receives a notification with the help message. The parent can then **resume** the child session with a response using `subagent_resume`.
|
|
|
|
**`caller_ping` parameters:**
|
|
- `message` (required): What you need help with
|
|
|
|
**`subagent_resume` parameters:**
|
|
- `sessionPath` (required): Path to the child session `.jsonl` file
|
|
- `name` (optional): Display name for the resumed pane (defaults to `Resume`)
|
|
- `message` (optional): Follow-up prompt to send after resuming
|
|
- `autoExit` (optional): Whether the resumed session should auto-exit after its next response. Defaults to `true` for autonomous follow-up work; set `false` when resuming for an interactive handoff.
|
|
|
|
**Interaction flow:**
|
|
1. Child calls `caller_ping({ message: "Not sure which schema to use" })`
|
|
2. Child session exits (like `subagent_done`)
|
|
3. Parent receives a steer notification: *"Sub-agent Worker needs help: Not sure which schema to use"*
|
|
4. Parent resumes the child session via `subagent_resume` with the response
|
|
5. Child picks up where it left off with the parent's guidance
|
|
|
|
**Example:**
|
|
```typescript
|
|
// Inside a worker subagent
|
|
await caller_ping({
|
|
message: "Found two conflicting migration files — should I use v1 or v2?"
|
|
});
|
|
// Session exits here. Parent receives the ping, then resumes this session
|
|
// with guidance like "Use v2, v1 is deprecated"
|
|
```
|
|
|
|
> **Note:** `caller_ping` is only available inside subagent contexts. Calling it from a standalone pi session returns an error.
|
|
|
|
---
|
|
|
|
## Custom Agents
|
|
|
|
Place a `.md` file in `.pi/agents/` (project) or `~/.pi/agent/agents/` (global):
|
|
|
|
```markdown
|
|
---
|
|
name: my-agent
|
|
description: Does something specific
|
|
model: anthropic/claude-sonnet-4-6
|
|
thinking: minimal
|
|
tools: read, edit, write, safe_bash, web_search
|
|
session-mode: lineage-only
|
|
---
|
|
|
|
# My Agent
|
|
|
|
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
|
|
|
|
| Field | Type | Description |
|
|
| ------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `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_interrupt`, `subagents_list`, `subagent_resume`) 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 — no `subagent_done` call needed. 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", ... })`. |
|
|
|
|
---
|
|
|
|
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`.
|
|
|
|
### `session-mode`
|
|
|
|
Choose how a subagent session starts:
|
|
|
|
- `standalone` — default fresh session with no lineage link to the caller
|
|
- `lineage-only` — fresh blank child session with `parentSession` linkage, but no copied turns from the caller
|
|
- `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.
|
|
|
|
`fork: true` on the tool call always forces the `fork` mode for that specific spawn.
|
|
|
|
```yaml
|
|
---
|
|
name: worker
|
|
session-mode: lineage-only
|
|
---
|
|
```
|
|
|
|
### `auto-exit`
|
|
|
|
When set to `true`, the agent session shuts down automatically as soon as the agent finishes its turn — no explicit `subagent_done` call is needed.
|
|
|
|
**Behavior:**
|
|
|
|
- The session closes after the agent's final message (on the `agent_end` event)
|
|
- 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 see "Complete your task autonomously." rather than instructions to call `subagent_done`
|
|
|
|
**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. Bare spawns with no agent defs (e.g. a `fork: true` spawn) are treated as interactive.
|
|
|
|
**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
|
|
---
|
|
```
|
|
|
|
Or per spawn:
|
|
|
|
```typescript
|
|
subagent({ name: "Scout", agent: "scout", interactive: true, task: "..." });
|
|
```
|
|
|
|
---
|
|
|
|
## 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.
|
|
|
|
### 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_interrupt`, `subagents_list`, `subagent_resume`) — 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/
|
|
├── agents/
|
|
│ ├── game-designer/
|
|
│ │ └── CLAUDE.md ← "You are a game designer..."
|
|
│ ├── sre/
|
|
│ │ ├── CLAUDE.md ← "You are an SRE specialist..."
|
|
│ │ └── .pi/skills/ ← SRE-specific skills
|
|
│ └── narrative/
|
|
│ └── CLAUDE.md ← "You are a narrative designer..."
|
|
```
|
|
|
|
```typescript
|
|
subagent({ name: "Game Designer", cwd: "agents/game-designer", task: "Design the combat system" });
|
|
subagent({ name: "SRE", cwd: "agents/sre", task: "Review deployment pipeline" });
|
|
```
|
|
|
|
Set a default `cwd` in agent frontmatter:
|
|
|
|
```yaml
|
|
---
|
|
name: game-designer
|
|
cwd: ./agents/game-designer
|
|
tools: read, write, edit, safe_bash
|
|
---
|
|
```
|
|
|
|
---
|
|
|
|
## 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
|
|
|
|
- [pi](https://github.com/badlogic/pi-mono) — the coding agent
|
|
- One supported multiplexer:
|
|
- [cmux](https://github.com/manaflow-ai/cmux)
|
|
- [tmux](https://github.com/tmux/tmux)
|
|
- [zellij](https://zellij.dev)
|
|
- [WezTerm](https://wezfurlong.org/wezterm/)
|
|
|
|
```bash
|
|
cmux pi
|
|
# or
|
|
tmux new -A -s pi 'pi'
|
|
# or
|
|
zellij --session pi # then run: pi
|
|
# or
|
|
# just run pi inside WezTerm
|
|
```
|
|
|
|
Optional backend override:
|
|
|
|
```bash
|
|
export PI_SUBAGENT_MUX=cmux # or tmux, zellij, wezterm
|
|
```
|
|
|
|
---
|
|
|
|
## Acknowledgements
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## License
|
|
|
|
MIT
|