open-subagent
v0.1.8
Published
Multi-provider subagents for Claude Code, powered by pi.dev
Maintainers
Readme
open-subagent
Multi-provider subagents for Claude Code, powered by pi.dev
(@earendil-works/pi-coding-agent). See SPEC.md for the full
design.
open-subagent is a stdio MCP server that exposes an open_subagent tool
which behaves like Claude Code's built-in Agent tool, except each "subagent
type" (profile) can run on any model pi supports — Anthropic, OpenAI,
Google, DeepSeek, Mistral, Groq, Bedrock, OpenRouter, and more.
Install
npm install -g open-subagentRequires Node.js >= 22.19.0 (pi's own minimum) and the pi binary on PATH:
curl -fsSL https://pi.dev/install.sh | sh
# or
npm i -g --ignore-scripts @earendil-works/pi-coding-agentnpm install cannot prompt for setup answers (no TTY during a package
install), so after installing, run the interactive setup once per project:
cd your-project
open-subagent initSetup (open-subagent init)
open-subagent init writes a starter .open-subagent/config.json with four
profiles — agent (catch-all), general (research/multi-step), explore
(read-only search), plan (read-only architecture planning) — all pointed at
one model you configure interactively:
provider name (e.g.
ollama,openai,anthropic)model id
base URL (optional — only for a custom/OpenAI-compatible endpoint; when given, it's merged into
~/.pi/agent/models.jsonas a custom provider, without touching any existing providers/models there)model — when a base URL is given,
initlists the models the endpoint actually serves and lets you pick one with an interactive list (arrow keys, type to filter, enter to select). Falls back to a numbered prompt when stdin isn't a TTY, and to typing an id when the endpoint won't list.context window in tokens —
inittries not to make you guess:- llama.cpp's
/props, whosedefault_generation_settings.n_ctxis the real per-slot figure (free, and usually instant); - the endpoint's model list — vLLM's
max_model_len, Ollama/OpenRouter'scontext_length, LM Studio'smax_context_length, LiteLLM'smax_input_tokens, …; - failing that, it offers to measure it — a deliberately oversized
request, refused during validation before any generation, with the limit
read out of the refusal (
error.n_ctx, or "maximum context length is N tokens"). Usually one request and about a second; - failing that, it asks you, defaulting to 131072.
This matters more than it looks. pi falls back to 16384 tokens for a model entry that declares no window, and an agent then stops with
stopReason: "length"mid-reasoning and returns nothing at all. Declaring more than the endpoint allows is just as bad — the server rejects the request outright. Note the number that matters is the context available to one request, which is not always what the server was configured with: llama.cpp divides its KV cache across--parallelslots, so a 65536-token cache serving 4 slots gives each request 16384. For the same reason, open-subagent only ever believes fields describing what the server serves: llama.cpp reportsn_ctx_train: 262144right next ton_ctx: 16384, and declaring the trained size would make every request fail.open-subagent doctorwarns about any profile whose model entry is missing acontextWindow;open-subagent doctor --probemeasures each profile against its live endpoint and flags any that declares more than the endpoint allows.- llama.cpp's
whether to deny Claude Code's native
Agenttool in this project (merged into.claude/settings.json'spermissions.deny, preserving everything else already there)whether to register the
open-subagentMCP server in this project's.mcp.json(merged in without clobbering other servers)whether to enable the dashboard (local web UI, off by default) and, if so, which port it listens on (default
8787) and whether to expose it to your LAN. The exposure question is only asked when you have already said yes to the dashboard, and defaults to no: the dashboard has no authentication, so binding0.0.0.0lets anyone who can reach the machine read every agent prompt, transcript and result.
It refuses to overwrite an existing .open-subagent/config.json unless you
confirm (or pass --yes), and prints a summary of files written plus a
reminder to restart your Claude Code session.
Every prompt can be skipped non-interactively with flags:
open-subagent init \
--provider openai --model gpt-5 --api-key '$OPENAI_API_KEY' \
--base-url https://api.example.com/v1 --context-window 131072 \
--disable-agent --mcp \
--dashboard --dashboard-port 8787 --no-dashboard-lan \
--yes--no-dashboard forces it off; --dashboard-port <n> implies --dashboard.
--dashboard-lan binds 0.0.0.0 and --no-dashboard-lan forces loopback;
--dashboard-host <h> sets any interface directly. Like --dashboard-port,
--dashboard-lan and --dashboard-host imply --dashboard, and any of them
suppresses the interactive exposure question.
--context-window <n> only affects models.json, so it needs --base-url; it
skips discovery entirely. --probe-context / --no-probe-context force the
measurement step on or off without prompting.
Configure
Profiles can also be hand-edited or defined directly, in either (or both) of:
- Global:
~/.config/open-subagent/config.json - Project:
<project>/.open-subagent/config.json
A project profile with the same display name fully replaces the matching global profile (wholesale, not field-merged).
{
"profiles": {
"agent": {
"description": "Catch-all for any task that doesn't fit a more specific profile. General coding and analysis on the configured model.",
"provider": "ollama",
"model": "qwen3.8-27B-coding-fast",
"apiKey": "<your-key-or-$ENV_VAR>",
"thinking": "medium"
},
"gpt-researcher": {
"description": "Deep research and web synthesis. Use for open-ended research questions.",
"provider": "openai",
"model": "gpt-5.2",
"apiKey": "$OPENAI_API_KEY",
"systemPrompt": "You are a research subagent. Return raw findings, not prose for a human.",
"tools": ["read", "grep", "find", "ls", "bash"],
"thinking": "high",
"readOnly": false,
"background": true
},
"gemini-reader": {
"description": "Huge-context file digestion on Gemini.",
"provider": "google",
"model": "gemini-3-pro",
"apiKey": "!pass show google-ai-key",
"readOnly": true
}
},
"defaults": {
"maxConcurrent": 4,
"stateDir": "~/.local/state/open-subagent"
}
}open-subagent init generates exactly this shape — see
.open-subagent/config.example.json for the full four-profile
(agent/general/explore/plan) starter template it's built from.
apiKey accepts:
- a literal string
$ENV_VARor${ENV_VAR}— resolved from the environment!command— executed via the shell, result cached in-memory for the life of the server process$$— escapes to a literal$
Full field reference: see SPEC.md §2.2.
Turn cap
A run is killed once it reaches defaults.maxTurns turn_end events
(default 500). Treat this as a runaway-loop backstop, not a work budget:
one turn is essentially one tool call, so the cap should sit far above any
task you actually expect to run. Bounding context growth is not a reason to
set it low — pi auto-compacts.
A profile may override it:
{
"profiles": {
"explore": { "maxTurns": 60 },
"agent": { "maxTurns": 800 }
},
"defaults": { "maxTurns": 500 }
}If you want to bound how long an agent may occupy one of your
maxConcurrent slots, set defaults.timeoutMs instead. Turns proxy spend;
wall-clock proxies occupancy, and on a self-hosted endpoint occupancy is
usually the scarce resource.
A capped run is not lost. The session file is recorded from the agent's
first event, so an agent killed by maxTurns or timeoutMs can be continued
with open_subagent_send, keeping its full transcript. Re-dispatching a fresh
agent instead throws that work away.
Requirements check
init verifies the pi binary before writing anything, and exits non-zero
if agents could not run — a config pointing at an unusable pi produces failures
that look like open-subagent bugs rather than installation problems. Pass
--ignore-pi-check to configure anyway; open-subagent doctor reports the
same check at any time.
It catches two cases that are otherwise hard to read:
- pi not installed / not on PATH. Spawning it fails with an empty stderr
and a null exit code, which looks like an agent that simply did nothing.
Worth knowing: the MCP server passes its own PATH to agents, which is not
necessarily your shell's — so a pi that works in your terminal can still be
invisible to a subagent.
OPEN_SUBAGENT_PI_BINpoints at a specific binary. - pi too old for the flags open-subagent passes, which shows up as
Error: Unknown option: -na. The usual root cause is neither pi nor open-subagent: pi >= 0.8 declaresengines: { node: ">=22.19.0" }, so npm running under an older Node quietly installs the last release that predates--nameand--no-approve. The check says so explicitly, and names the Node version it found.
The same diagnosis is applied at run time, so an agent failing for either reason reports the cause instead of an unexplained exit code.
Register with Claude Code
Add to .mcp.json (project) or run claude mcp add:
{
"mcpServers": {
"open-subagent": {
"command": "open-subagent",
"args": ["serve"],
"env": {}
}
}
}Usage
Once registered, the orchestrator model sees four tools:
open_subagent— spawn a subagent on a configured profile. Foreground (run_in_background: false) blocks and returns the agent's final text plus anagentId:continuation line and a<usage>block. Background (the default) returns immediately with the agentId and an output-file path.descriptionnames the run (it shows up inopen_subagent_listand the dashboard).depends_on: ["os-…"]holds the spawn until the named agents succeed, so dependent work can be ordered without telling an agent to poll for a file a sibling hasn't written yet. A dependency that fails or is killed cancels its dependents rather than releasing them: they are marked failed with askipped: depends_on …reason naming the agent that broke, because work premised on output that was never produced is worse than no work at all.open_subagent_output— fetch or poll an agent's result.wait: true(the default) blocks until it finishes, bounded bytimeout_ms(default 120s) after which it returns a progress snapshot rather than hanging — the agent keeps running, so just poll again.wait: falsereturns that snapshot immediately: status, elapsed time, turns, tool calls, the tool currently running, and an excerpt of the latest assistant text.open_subagent_list— every agent with its task, status, elapsed time, tool count and current activity. Optionalstatusfilter (queued/running/completed/failed/killed).open_subagent_send— continue a finished agent with a follow-up message, resuming its pi session with full transcript history.open_subagent_kill— stop a running or queued agent, with an optionalreasonrecorded as its status. A queued agent is cancelled before it ever spawns; a running one has its process group killed.
There is deliberately no way to steer an agent mid-run. Doing so would mean the
orchestrator reading partial transcripts to judge them, pulling the subagent's
context back into the parent and undoing the point of delegating. The lever that
costs nothing is ending a run, which is what open_subagent_kill is for;
otherwise the leverage is in the prompt you spawn with.
Background failures announce themselves. MCP is request/response — a server cannot push into the conversation — so a background agent that died used to be invisible until something thought to poll. Failures are now queued and prepended to the next tool result of any kind:
[open-subagent] 2 background agents failed since your last call:
os-3f2a: Cannot spawn pi: argument 18 contains a NUL byte...
os-9c1d: skipped: depends_on did not succeed - os-3f2a (failed)They are delivered once and then cleared.
Subagents run in the project directory and can read anything already on disk,
so point them at files instead of pasting file contents into the prompt. Like
Claude Code's native subagents they also load the project's CLAUDE.md /
AGENTS.md (set "contextFiles": false on a profile to opt out).
The orchestrator is never told which model backs a profile: results name the
profile only. Profiles are chosen by name and description, and knowing the
model behind one changes how much work gets delegated to it. The dashboard and
doctor still show models — a human is the audience there.
Run open-subagent doctor to print the resolved config (API keys masked to
their source, never their value), confirm the pi binary is on PATH, and
check every profile's declared context window. Add --probe to measure each
one against its live endpoint.
Run open-subagent init to (re-)generate the project config, register the
MCP server, or deny Claude Code's native Agent tool — see Setup above.
Run open-subagent dashboard to browse agents and transcripts in a browser —
see Dashboard below.
Dashboard
A small read-only web UI over the same registry and transcripts the tools use:
the list of agents this machine has run (named by their description), their
status/profile/model/access/tool counts/duration/tokens, each agent's
transcript, and its final output.
The transcript reads like a Claude Code session rather than a log dump: the
prompt in a collapsible panel, assistant replies rendered as markdown,
reasoning folded away behind a summary, and consecutive tool calls grouped
under one line (Ran 3 commands, Read 4 files (1 failed)) that expands to
show each call, its arguments and its output. Streaming deltas are dropped —
they are most of the raw stream and none of the content.
It is disabled by default. Enable it in .open-subagent/config.json (or the
global config):
{
"defaults": {
"dashboard": { "enabled": true, "port": 8787 }
}
}When enabled, the first session that starts spawns it as its own detached
process and logs the URL to stderr; every later session finds it and adopts it.
With "enabled": false (the default) no socket is opened and no HTTP code runs
at all.
One dashboard serves the whole machine. The state dir is machine-wide, so
the dashboard already lists agents from every session — it is not scoped to the
session that started it. It is deliberately not run in-process by serve: that
made every session race for the same port, and the winner took the dashboard
down with it when that one session exited. Running it detached means it
outlives whoever started it.
dashboard.json in the state dir records the running dashboard's pid, host and
port. A clean shutdown removes it; a crash leaves it behind with a dead pid,
which the next session detects and replaces.
Leftovers are cleaned up automatically. A dashboard still running from an older
install serves the code it was started with, so on the next serve it is
identified by /api/meta, stopped, and replaced with the current version —
otherwise an upgrade would appear to do nothing until someone noticed and
killed it by hand. Two things are never killed: a process on that port that
does not identify itself as open-subagent, and an MCP server (serve) from
0.1.5 or earlier whose Claude Code session is still running, since that would
strip a live session of its agents. Both cases are logged with the pid, and the
session runs without a dashboard rather than guessing.
A session can be alive and still useless, though — abandoned, or running under
a Node too old for pi — and then that refusal just leaves you killing pids by
hand. --reclaim is the deliberate override:
open-subagent dashboard --port 8787 --reclaimIt stops whatever open-subagent process holds the port, live session or not, reports what it killed, and takes over. It does not override the identity checks: the holder must answer as open-subagent over HTTP and look like open-subagent in its command line, so an unrelated service on the port is still never touched.
It binds 127.0.0.1 unless defaults.dashboard.host says otherwise. init
writes that field explicitly — including when it is the default — so a
generated config states its exposure rather than implying it. There is no
authentication of any kind: on a wider bind, everything the dashboard serves
(full prompts, transcripts, tool output, results) is readable by anyone who can
route to the machine, so restrict it at the firewall or leave it on loopback.
While bound to loopback it also rejects requests carrying a non-loopback Host
header, so a web page you visit can't reach it by pointing a domain at
127.0.0.1 — that guard cannot apply to a LAN bind.
To browse past agents outside a Claude Code session, run it standalone:
open-subagent dashboard --port 8787--host overrides the bind address for that run, and --reclaim takes the port
from a leftover process (see above).
Current limitations
| Native feature | open-subagent | Path |
|---|---|---|
| Haiku activity line in /tasks | elapsed time only (MCP limitation); use open_subagent_list | none available (harness-owned) |
| /tasks transcript steering | not available | RPC mode + open_subagent_steer |
| Permission prompts to user | pi -na + tool scoping instead | RPC extension_ui_request bridge |
| isolation: worktree | not yet | server-side git worktree add before spawn |
| Structured output | not yet | terminating-tool extension (§4.5) |
| fork mode (inherit parent convo) | n/a (parent convo not accessible to MCP) | — |
Development
npm install
npm run build
npm test # typechecks src + test, then runs vitest
npm run typecheckTests use a fake pi binary fixture (test/fixtures/fake-pi.mjs) that emits
canned --mode json JSONL events, so the suite does not require a real pi
install. Its event shapes are transcribed from real pi output and must stay
that way — an earlier fixture invented its own field names
(tool_name for toolName, output_tokens for output) and hid several
production bugs behind a green suite.
Set OPEN_SUBAGENT_PI_BIN to point at a pi binary that isn't on PATH.
