Files
harness-tools/extensions/interactive-subagents/README.md
T

228 lines
14 KiB
Markdown

## Forked from [amosblomqvist/pi-interactive-subagents](https://github.com/amosblomqvist/pi-interactive-subagents)
# pi-interactive-subagents
Async subagents for [pi](https://github.com/badlogic/pi-mono), running in Herdr tabs, Orca terminal tabs, or tmux panes. Spawn a sub-agent, keep working in the main session, and get the result steered back when it finishes. Fully non-blocking.
The backend is selected automatically: Herdr is used when Pi runs in a Herdr-managed pane (`HERDR_ENV=1`), then Orca when Pi runs in an Orca-managed terminal (`ORCA_TERMINAL_HANDLE`), otherwise tmux is used when available.
## How it works
`subagent()` returns immediately. In Herdr, the sub-agent runs in a new unfocused tab in the current workspace. In Orca, it runs in a new unfocused terminal tab in the current worktree, started in the parent Pi's working directory. In tmux, it runs in a right split off the parent Pi pane. 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
⟳ 00:23 scout active · bash 7m
○ 00:45 scout-2 waiting 2m
```
Spawn several in parallel — they run concurrently and steer results back independently as each finishes.
Running subagents survive a parent `/reload`. Their terminal sessions and completion watchers stay active, then rebind result delivery and the status widget to the fresh Pi extension runtime. Replacing or closing the parent session still cancels its running subagents. This lifecycle behavior requires Pi 0.68 or newer.
tmux 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`, …). Herdr and Orca subagents use separate tabs, so they do not resize the parent tab.
If your shell startup is slow and launch commands get dropped before the prompt is ready, raise the delay:
```bash
export PI_SUBAGENT_SHELL_READY_DELAY_MS=2500 # defaults: Herdr 1000, Orca 500, tmux 500
```
## Tools
| Tool | Description |
| --- | --- |
| `subagent` | Spawn a sub-agent in a dedicated terminal surface (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 |
There is also a `/subagent <agent> <task>` command for spawning directly.
### Spawning
```typescript
subagent({ agent: "scout", task: "Analyze the auth module" });
subagent({ agent: "worker", name: "dark-mode", task: "Implement the dark mode toggle" });
```
| 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)) |
### Messaging
`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:
```typescript
subagent_message({ name: "scout", message: "Also check the auth middleware" });
```
- **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.
- **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.
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.** 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.
### ask_question
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.
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.
## Bundled agents
| Agent | Model | Tools | Role |
| ----- | ----- | ----- | ---- |
| **scout** | `openai-codex/gpt-5.6-luna` (`xhigh`) | `read`, `grep`, `find`, `ls` | Fast read-only codebase recon |
| **researcher** | `openai-codex/gpt-5.6-luna` (`xhigh`) | `web_search`, `web_fetch`, `safe_bash` | Web research, synthesized into a sourced brief |
| **worker** | `openai-codex/gpt-5.6-luna` (`xhigh`) | `read`, `write`, `edit`, `bash`, `web_search`, `web_fetch` + spawning | General implementer; may spawn `scout` and `researcher` |
All three are autonomous (`auto-exit: true`) and carry their identity in the system prompt (`system-prompt: append`).
## Custom agents
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.
```markdown
---
name: my-agent
description: Does something specific
model: openai-codex/gpt-5.6-luna
thinking: xhigh
tools: read, edit, write, safe_bash, web_search
session-mode: lineage-only
auto-exit: true
---
You are a specialized agent that does X...
```
### 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 |
### session-mode
- `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
### auto-exit
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.
Notes:
- **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.
- **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).
### interactive
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.
## Tool access control
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.
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.
Extensions can register additional tools for sub-agents at runtime via `registerToolExtension(name, path)` on the `__pi_interactive_subagents` process global. Hosts that compose widgets can optionally use its reload-safe `statusWidget` surface:
```typescript
const { statusWidget } = globalThis.__pi_interactive_subagents;
const onChange = () => rerender();
const release = statusWidget.claim(onChange);
statusWidget.hasContent(); // whether any subagent rows exist
statusWidget.getBodyRowCount(); // uncapped number of running agents
statusWidget.render(width, maxBodyRows, ctx.ui.theme); // compact unboxed lines; cap includes an overflow row
statusWidget.isClaimed(onChange); // true only for the current provider claim
release(); // restore the standalone subagent-status widget
```
`render(width, maxBodyRows?, theme?)` accepts Pi's current theme as an optional structural value. The standalone widget receives and uses its own Pi theme automatically. When no theme is passed, a legacy ANSI fallback keeps status colors usable. The header is a compact unboxed `Subagents <count> running` row; body rows remain width-safe, and `maxBodyRows` counts body rows including the overflow summary, not the header. Semantic row colors match Pi's Todo conventions: accent header, muted count, warning active/running, error stalled, dim starting/provider tags/elapsed time, and accent waiting.
`claim()` replaces any prior claim, and an old release function cannot release a newer claim. Standalone-widget clearing/restoration is deferred to a microtask, so hosts may release and immediately re-claim without a transient fallback widget. `isClaimed(onChange)` checks both callback identity and the current runtime ownership, so hosts can detect invalidation after reload/session rebind. Claims are cleared during session shutdown, including `/reload`, so a fresh host runtime must claim again rather than retaining a stale callback. Calling `render()` directly does not claim or change the standalone widget.
## Role folders
`cwd` starts a sub-agent in a directory with its own config, so role-specific setups (CLAUDE.md, skills, extensions) apply:
```
project/
└── agents/
├── game-designer/ ← CLAUDE.md, .pi/…
└── sre/ ← CLAUDE.md, .pi/…
```
```typescript
subagent({ agent: "worker", cwd: "agents/sre", task: "Review the deployment pipeline" });
```
Set a per-agent default with `cwd:` in frontmatter.
## Status widget & configuration
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`.
Status display is configured via `config.json` in the extension directory (copy `config.json.example`; it's gitignored):
```json
{
"status": { "enabled": true }
}
```
## Install
```bash
pi install npm:@_jb/pi-interactive-subagents
```
## Requirements
- [Pi](https://github.com/earendil-works/pi-mono)
- One supported terminal manager:
- [Herdr](https://herdr.dev), with Pi launched inside Herdr
- [Orca](https://orca.build), with Pi launched inside an Orca-managed terminal
- [tmux](https://github.com/tmux/tmux)
Herdr requires no extension configuration once its Pi integration is installed. The extension detects `HERDR_ENV=1` and creates subagents as unfocused tabs in the current workspace.
Orca requires no extension configuration either. The extension detects `ORCA_TERMINAL_HANDLE` and creates subagents as unfocused terminal tabs in the current worktree via the `orca` CLI (`ORCA_CLI_COMMAND` is honored when set). New tabs start in the parent Pi's working directory with a bash shell.
For tmux:
```bash
tmux new -A -s pi 'pi'
```
## 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; its supervision features were inspired by [RepoPrompt](https://repoprompt.com/).
## License
MIT