pi-interactive-subagents
Async subagents for pi — 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:
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
pi install git:github.com/HazAT/pi-interactive-subagents
Supported multiplexers:
Start pi inside one of them:
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):
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). 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 yetactive— the child is doing observed runtime work: agent turn, provider request, streaming, or tool executionwaiting— the child finished a turn and is intentionally open for more input or another stagestalled— the parent has gone too long without a valid current child snapshot and can no longer trust the run is healthyrunning— 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:
cp config.json.example config.json
{
"status": {
"enabled": true
}
}
config.json is gitignored so local overrides don't get committed.
Spawning Subagents
// 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) |
Interrupting a running subagent
Use subagent_interrupt to cancel the active turn of a running Pi-backed subagent:
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.jsonlfilename(optional): Display name for the resumed pane (defaults toResume)message(optional): Follow-up prompt to send after resumingautoExit(optional): Whether the resumed session should auto-exit after its next response. Defaults totruefor autonomous follow-up work; setfalsewhen resuming for an interactive handoff.
Interaction flow:
- Child calls
caller_ping({ message: "Not sure which schema to use" }) - Child session exits (like
subagent_done) - Parent receives a steer notification: "Sub-agent Worker needs help: Not sure which schema to use"
- Parent resumes the child session via
subagent_resumewith the response - Child picks up where it left off with the parent's guidance
Example:
// 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_pingis 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):
---
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 |
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 callerlineage-only— fresh blank child session withparentSessionlinkage, but no copied turns from the callerfork— 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.
---
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_endevent) - 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
---
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: falseon an agent that doesn't auto-exit but you still want stall pings for - Set
interactive: trueon an autonomous agent you'd rather check on yourself
---
name: worker
# interactive defaults to true because auto-exit is not set
---
Or per spawn:
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:
- Grants the full spawning toolset (
subagent,subagent_interrupt,subagents_list,subagent_resume) — you do not list these intools. - Loads this extension into the child process.
- Restricts the child to spawning only the named agents. The child's
subagents_listis filtered to that set, andsubagentcalls for any other agent are rejected. Enforced via thePI_SUBAGENT_ALLOWEDenv var.
---
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..."
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:
---
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
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:
export PI_SUBAGENT_MUX=cmux # or tmux, zellij, wezterm
Acknowledgements
The sub-agent status supervision and turn-only interruption features were inspired by RepoPrompt's sub-agent snapshot polling and run cancellation features.
License
MIT