Files
pi-interactive-subagents/README.md
T

24 KiB

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 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:

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 .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:

// 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):

---
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 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.

---
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
---
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
---
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:

  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.
---
name: worker
tools: read, write, edit, safe_bash, web_search, web_fetch
subagent_agents: scout, researcher
---
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