External Coding Agents¶
ModexAgent's ReAct agents are in-process Python. But the industry is full of coding-agent CLIs — OpenCode is the one integrated today — that already know how to read AGENTS.md, discover skills, run bash tools, and resume their own sessions. Re-implementing that machinery inside a ReAct agent is wasteful.
External coding agent integration (ADR-0022) lets you register such a CLI as the NORMAL main agent of its own dedicated pool. Your existing ReAct agents reach it through the same task tool they already use to talk to any peer. The external agent replies through modexctl, a small CLI shipped with the bot. Everything shows up in the WebUI alongside your other agents.
Native vs external, side by side¶
Both kinds of pool sit on the same peer wire. What's inside differs:
flowchart TD
subgraph Native["Native pool — ReAct"]
NMA["Main Agent<br/>(in-process Python)"]
NRT["Graph runtime · Memory · Tools · Approval"]
NSA["Subagent(s)"]
NMA --- NRT
NMA ---|task| NSA
end
subgraph External["External pool — OpenCode"]
EMA["Harness<br/>(framework-side)"]
ECLI["Shared opencode serve process<br/>(own sessions, tools, permissions)"]
EMA -.->|"HTTP / SSE"| ECLI
end
NMA <-->|"peer wire<br/>task / modexctl send"| EMA
NRT ~~~ EMA
NSA ~~~ EMA
A native pool is one box: the framework owns the agent, its memory, its tools, and its approval flow. An external pool is two: a thin framework-side harness wraps the provider CLI, which owns its own sessions, tools, and permission model. The peer wire on top is symmetric from the framework's view — but the tools the two sides use to send messages are not.
| Native ReAct agent | External coding agent | |
|---|---|---|
| Runs as | In-process Python | External CLI in a shared opencode serve process |
| Pool role | Main agent or subagent | Main agent of its own single-node pool, or a subagent inside a react pool (ADR-0027) |
| Reaches peers with | task tool |
modexctl send CLI shim |
| Memory | Framework memory (session + opt-in long-term) | Provider's own session |
| Tool execution | Framework ToolManager |
Provider's own tools (bash, edit, …) |
| Risky-action approval | Framework GraphInterrupt + approval UI |
Provider's own permission model |
| WebUI transcript | Direct emit | Projected from provider events |
| Resumes via | Framework session store | Provider-minted session id |
The asymmetry is deliberate. The framework does not re-implement what the provider already does well (file editing, shell, session resume); it just routes messages in and out and renders the provider's event stream in your WebUI.
One constraint falls out of that split: an external pool's tree is single-node. The external main agent has no framework tool surface, so it cannot dispatch subagent tasks — declaring children under it is a configuration error the framework rejects at boot (see below).
One shared OpenCode process¶
The harness does not spawn a CLI process per pool or per turn. A process-wide manager owns a single opencode serve process shared by every OpenCode pool and every session:
- Lazy start and self-healing. The server starts on first use. Every subsequent use re-checks liveness, and a background health watchdog polls the server and respawns it if it dies — with a grace window when sessions are mid-turn, so an active session is never cut off by a transient health blip.
- Orphan reaping and a PID registry. Every spawned process is recorded in an on-disk PID registry (one JSON file per PID). On the first use after a restart, processes the bot itself spawned in a previous run and lost to a crash are reaped. A live process owned by another bot process is never killed — two bot instances can run side by side, each managing only its own.
- External server mode. Set the
OPENCODE_HOSTenvironment variable and the manager connects to an already-runningopencode serveat that address instead of spawning its own — useful when you run the CLI as a supervised service of its own. - Event-driven turns. A persistent SSE reader per working directory streams provider events — text, reasoning, tool calls, errors — into the canonical event model the WebUI renders. Turn completion is decided by a state machine that waits for the whole provider session tree to go quiet, not by polling.
- Child sessions are first-class. When OpenCode forks its own internal subagent sessions, the harness discovers them from the event stream and registers them as real ModexAgent sessions. They appear nested under their parent in the WebUI session tree, with their own transcripts.
- Per-session identity without per-session processes. The shared process's environment is frozen at first spawn, so the harness injects identity another way: a small OpenCode plugin stamps every bash call with its session id, and
modexctlresolves the matching per-session environment snapshot. Concurrent sessions cannot cross-talk.
External coding agents as subagents (ADR-0027)¶
External coding agents are not limited to pool main agents. Per
ADR-0027,
a subagent declared inside a react pool can opt into the external coding
harness — execution_strategy and provider_kind are declared per agent
node, so one tree can mix a native main agent with an external CLI
subagent:
# config/scopes/bot.yml — a react pool with an external subagent
workspace:
pools:
coder:
agents:
orchestrator:
description: >-
Plans implementation work and delegates it.
coder:
description: >-
External coding subagent that implements the planned work in
its own self-directed coding context.
execution_strategy: external # subagent-level opt-in
provider_kind: opencode
The shipped declaration keeps its external agent as the single-node
opencode pool main (the snippet above is the opt-in form, trimmed to the
fields that matter). The parent reaches the external subagent through the
same task tool it uses for any subagent. The subagent runs the external
CLI, does its work with its own tools and session, and its final reply is
forwarded back to the caller automatically when the turn ends.
Subagent addressing rules still apply
As a subagent, the external coding agent follows the same addressing
rules as any subagent: the only target it can address is its parent. It
does not participate in cross-pool peer messaging and has no
communication tools of its own — its final reply is delivered for it,
and modexctl send is reserved for asking the parent a question or
requesting a decision mid-turn. Peer messaging remains a main-agent-only
capability — external agents that need to be full peers are configured
as main agents of their own dedicated pools (see the opencode pool in
Multi-Agent).
How to use it¶
1. Register an external pool¶
Declare the pool in config/scopes/bot.yml with execution_strategy: external. The shipped declaration's opencode pool, trimmed:
# config/scopes/bot.yml
workspace:
pools:
opencode:
peers:
- default # explicit peer declaration required
agents:
opencode:
description: >-
Autonomous software engineering agent with full file and
terminal access.
execution_strategy: external # opt-in; default is "react"
provider_kind: opencode
The same pool can be edited from the WebUI Settings → Scope tab — the scope editor writes the declaration back to bot.yml and takes effect after a restart.
External pools are single-node
An external pool rejects subagents: the pool's tree is validated when the declaration is assembled at boot, and any agent declared under the external main agent is a fatal configuration error. If you want an external CLI and subagents in the same tree, declare the external agent as a subagent inside a react pool instead (ADR-0027, above).
Availability gating
If the provider CLI (opencode) is not on PATH, the pool's main-agent registration is skipped at startup with a warning — the pool stays structurally intact, and other pools are unaffected. Install the CLI and restart to enable the pool.
2. Talk to it from another agent¶
Nothing changes on the sending side. A peer main agent calls the same task tool it uses for any peer:
task(target_agent="opencode", content="refactor utils.py for me")
The message lands in the external pool's inbox, the harness wakes the provider, the provider does its work, and the response comes back through the same peer channel.
3. Watch it in the WebUI¶
External agent sessions appear in the WebUI session list alongside every other session — the shipped pool's agent is named opencode, so its sessions carry that suffix. Streaming output — text, reasoning, tool calls and results, errors — renders the same way it does for any agent, and subagent sessions the provider forks internally show up nested in the session tree. You do not need to leave the UI to inspect what the external CLI is doing.
4. The external agent talks back¶
The provider's LLM has no communication tools. Instead, the harness injects a small set of instructions into its system prompt telling it about a CLI shim:
modexctl send --to <peer_name> --content "<your reply>"
modexctl agents # list routable peers
modexctl ships with the bot, not the framework package: it is a thin client over the bot's HTTP control plane (ADR-0035). The harness injects the MODEX_* environment variables that tell it which session, workspace, and agent it is acting for — the operator writes none of this. The provider discovers the shim, the peer names, and the rule that stdout is observed but not delivered through the injected prompt, and calls modexctl send from its own bash tool when it needs to reply.
Supported providers¶
| Provider | Status | Transport |
|---|---|---|
| OpenCode | shipped | One shared opencode serve process per bot; persistent SSE event stream per working directory |
OpenCode is the only provider you can construct today: the harness accepts exactly one provider_kind value, opencode, and rejects any other at assembly. A pi value still lingers in the provider-kind enum from an earlier revision of this integration, but its backend is deleted — it is not a usable provider. Claude Code, Codex, and Cursor appear in the code only as named candidates for future integration.
The provider seam itself — the backend interface, the event parser, the env builder, the OS layer, and the CLI shim — remains provider-agnostic, so adding a provider is a local change: a new provider package plus one value in the ProviderKind enum.
What it doesn't do¶
- No framework memory layer on external agents. The provider's own session is the source of truth. The transcript you see in the WebUI is a UI projection — the external CLI never reads it back. This applies to both external pool mains and external subagents.
- No framework approval on external pools. Risky-action gating uses the provider's own permission model — the harness configures OpenCode to auto-allow permissions so no runtime prompts fire — not the framework's
GraphInterrupt. The framework's approval UI does not fire for external pool work. - No cross-workspace routing.
modexctl sendroutes within one workspace's inbox. Multi-workspace topologies are out of scope. - Status / log / token-usage events are not rendered. The transcript projection ships text, reasoning, tool calls, tool results, and errors; session-status events drive turn completion behind the scenes but stay off the transcript.
Where to next¶
- External agents sit on the cross-pool peer topology defined in Multi-Agent.
- They do not run the Graph Engine — that's why framework approval doesn't apply to them.
- Why external pools have no framework memory: see Memory.
- The declaration file that hosts the
opencodepool is covered in Scope & Assembly. - For the full design and rationale, see ADR-0022 (main-agent integration), ADR-0027 (subagent extension), and ADR-0035 (the
modexctlcontrol plane) in the framework repo.