agentmesh
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@agentmeshSpawn an OpenCode worker to fix the failing auth tests, then wait for its result."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
agentmesh
A protocol-native adapter that lets one coding agent act as a master and command a pool of heterogeneous worker agents (OpenCode, Pi, Codex, Gemini, Claude Code, …). Roles are decided by configuration, not by agent type — so an OpenCode master can drive Pi workers, a Pi master can drive OpenCode workers, or anything else, with the same code.
┌─────────────┐ MCP tools ┌──────────────────────────┐ ACP (JSON-RPC/stdio) ┌──────────────┐
│ master agent │ ─────────────▶ │ agentmesh adapter │ ───────────────────────▶ │ worker: pi │
│ (opencode/pi)│ spawn/dispatch│ • MCP server (in) │ via acpx runtime ├──────────────┤
│ ...any MCP │ ◀───────────── │ • Orchestrator │ ◀─────────────────────── │ worker: │
│ client │ results │ • Worker registry │ streamed events │ opencode │
└─────────────┘ │ • git-worktree isolation│ ├──────────────┤
└──────────────────────────┘ │ worker: ... │
└──────────────┘Why this shape
Two protocols do the heavy lifting, so we don't reinvent either:
ACP (Agent Client Protocol) — Zed's open JSON-RPC-over-stdio protocol for driving coding agents. OpenCode (
opencode acp), Pi (npx pi-acp), Gemini, Codex, Claude Code, Cursor, Qwen and more all speak it. We reuseacpxas the ACP client, so one integration drives every worker.MCP (Model Context Protocol) — how the master gains orchestration superpowers. The adapter exposes tools (
spawn_worker,dispatch_task, …); any MCP-capable agent that connects becomes a master with zero changes to itself. Pi 1.0 and OpenCode both support MCP.
This is deliberately lighter than agent-orchestrator IDEs (Electron/GUI) and tmux-screen-scraping supervisors: the master stays a normal agent, workers stay normal agents, and the adapter is just the protocol bridge + lifecycle manager between them.
Related MCP server: agent-orchestrator
Status
Working and verified end-to-end: an 8-state persistent task machine (SQLite),
async task lifecycle (task_submit / task_wait / task_result / …), a
human-in-the-loop approval flow for worker permission/elicitation requests,
and a remote Worker Daemon for cross-machine workers over an authenticated,
TLS-capable channel — driven against real local OpenCode and Pi agents as well
as deterministic mocks. See Verification.
Install
cd agentmesh
npm installRequires Node.js ≥ 22.13. Real workers additionally require the upstream CLI
(pi, opencode, …) installed and authenticated on the machine.
Try it (no API keys, no cost)
npm run demo # Scene A: sync delegate-and-wait; Scene B: submit→waiting→approve→succeeded
npm test # 36 tests: state machine, persistence, timeout, cancel, approvals, recursion guard, remote daemon + resume + rotation + audit
npm run smoke # drives the real MCP server over stdio through the full task lifecycledemo ends with Scene A CLOSED ✅ | Scene B CLOSED ✅ — the master→worker→result
round trip plus the async approval loop.
To exercise the real agents on this machine (trivial prompt, minimal cost):
npx tsx scripts/real-smoke.ts # opencode + pi through submit → wait → resultConfiguration — agentmesh.yaml
Everything under profiles describes workers the master can spawn.
workdir: .agentmesh # worktrees + acpx session state
defaults:
permissionMode: approve-all # deny-all | approve-reads | approve-all
profiles:
- name: mock-analyst # deterministic, offline
driver: mock
mock: { delayMs: 20, toolCalls: 2 }
- name: pi # real Pi worker over ACP
driver: acpx
agent: pi # acpx built-in -> `npx pi-acp`
worktree: true # isolate parallel edits
- name: opencode # real OpenCode worker over ACP
driver: acpx
agent: opencode # acpx built-in -> `npx -y opencode-ai acp`
worktree: true
# custom launch command + model:
# - name: opencode-sonnet
# driver: acpx
# agent: opencode-custom
# command: ["npx", "-y", "opencode-ai", "acp"]
# model: anthropic/claude-sonnet-4-5agent can be any acpx built-in name (pi, opencode, codex, claude,
gemini, cursor, qwen, qoder, …) or a custom key paired with command.
Task lifecycle & the 8-state machine
Every delegation is a task that moves through a persistent state machine:
queued ─▶ starting ─▶ running ─▶ succeeded
│ ▲
waiting│ │ (approve / respond)
▼ │
(loops back to running)
running/waiting ─▶ failed | cancelled | timed_out (terminal)Terminal states: succeeded, failed, cancelled, timed_out. Tasks and their
event logs are persisted to SQLite (node:sqlite, no native deps) under
workdir, so a task submitted by one process can be queried by the next.
Two ways to delegate:
Async (primary):
task_submitreturns ataskIdimmediately. Polltask_status, block ontask_wait, readtask_result, or pagetask_events.task_waitalso returns as soon as a task enterswaitingso the master can act on an approval instead of blocking to the timeout.Sync (convenience):
dispatch_tasksubmits and waits for the terminal result in one call.
Approvals (human-in-the-loop)
When a worker hits a risky tool (ACP permission request) or asks for input (ACP
elicitation), its task moves to waiting and blocks. The master then:
pending_approvals { taskId? }→ lists blocking requests (kind,id).task_approve { approvalId, decision }→allow_once | allow_always | reject_once | reject_always | cancel, ortask_respond { approvalId, action, content? }→ answer an elicitation.
Approving resumes the task synchronously back to running; rejecting fails it.
Cancelling or timing out a task auto-resolves its pending approvals.
Security: worker output and permission requests are untrusted data, never instructions. Keep
permissionModeatapprove-reads/deny-allfor untrusted work, require human approval for irreversible operations, and never commit secrets or expose the ACP bridge to a network without auth + TLS.
MCP tools exposed to the master
tool | purpose |
| configured worker profiles + live workers |
| one worker's status + recent event log |
| start a worker (optionally in a git worktree) |
| close a worker and clean its worktree |
| submit a task, return a |
| current status of a task |
| block until terminal or |
| final text / status / error of a task |
| persisted event log after a sequence number |
| request cancellation (settles once the worker confirms) |
| list tasks, optionally filtered by status |
| approval/elicitation requests blocking a |
| resolve a permission request (allow/deny/cancel) |
| answer an elicitation request |
| synchronous convenience: submit + wait for the terminal result |
Attach a real master
Build, then point your master agent's MCP config at the server. The command is
always: node <abs>/dist/cli.js mcp --config <abs>/agentmesh.yaml.
npm run buildOpenCode (opencode.json / ~/.config/opencode/opencode.json) — shape:
{
"mcp": {
"agentmesh": {
"type": "local",
"command": ["node", "/ABS/PATH/agentmesh/dist/cli.js", "mcp", "--config", "/ABS/PATH/agentmesh/agentmesh.yaml"],
"enabled": true
}
}
}Pi (.pi/settings.json or ~/.pi/agent/settings.json) — Pi 1.0 has native
MCP; add agentmesh as a local/stdio MCP server with the same command.
Verify the exact MCP-server config keys against the current OpenCode/Pi docs — the command line above is what matters and is stable.
Then just talk to your master: "spawn an opencode worker to fix the failing
tests while a pi worker writes the changelog." It will call dispatch_task.
CLI
npx tsx src/cli.ts demo
npx tsx src/cli.ts list --config agentmesh.yaml
npx tsx src/cli.ts mcp --config agentmesh.yaml
# synchronous delegate-and-wait
npx tsx src/cli.ts dispatch --profile pi --task "fix the auth bug" --worktree
# async task lifecycle (persisted to SQLite)
npx tsx src/cli.ts submit --profile pi --prompt "fix the auth bug" # prints taskId
npx tsx src/cli.ts tasks --config agentmesh.yaml
npx tsx src/cli.ts approve --approval-id A --decision allow_once
# run workers on another machine (see "Remote Worker Daemon")
npx tsx src/cli.ts daemon --config agentmesh.yaml --host 127.0.0.1 --port 8790 \
--token-env AGENTMESH_DAEMON_TOKENNote:
submitfrom the one-shot CLI persists the task but its live run ends with the process. For real async use, submit through the long-livedmcpserver (or a resident orchestrator) so the run continues in the background.
Remote Worker Daemon (cross-machine workers)
Run workers on another machine — a GPU box, a build server, a colleague's
checkout — without ever putting an ACP stdio bridge on the network. The daemon
wraps the same local stack (Orchestrator + TaskManager + acpx drivers) behind
an authenticated HTTP+WebSocket endpoint; on the master side a remote worker is
just another driver: remote profile, so dispatch, approvals, events and
persistence behave identically to a local worker.
master (machine A) worker daemon (machine B)
┌───────────────────┐ WSS + Bearer token ┌──────────────────────────────────┐
│ driver: remote │ ─────────────────────▶ │ agentmesh daemon │
│ profile ──────────┼── submit/approve/… ───▶│ • HTTP /healthz, /profiles │
│ RemoteWorkerDriver │ ◀───────────────────── │ • WS /ws (typed task events) │
└───────────────────┘ task events, result │ • local Orchestrator + acpx │──▶ ACP stdio (LOCAL ONLY)
└──────────────────────────────────┘Start the daemon (machine B)
export AGENTMESH_DAEMON_TOKEN="$(openssl rand -hex 32)" # value never goes in config
npx tsx src/cli.ts daemon \
--config agentmesh.yaml \
--host 0.0.0.0 --port 8790 \
--token-env AGENTMESH_DAEMON_TOKEN \
--tls-cert ./certs/daemon.pem --tls-key ./certs/daemon-key.pemFlags: --host --port --token-env --tls-cert --tls-key --tls-ca
(enables mTLS) --allow-insecure (explicit opt-out of the TLS requirement for
non-loopback binds). The same keys can live under a daemon: block in
agentmesh.yaml. Two probes are available:
GET /healthz— unauthenticated{ ok, daemonId, protocol }liveness.GET /profiles— authenticated; lists the profiles the daemon exposes.
Point a master at it (machine A)
profiles:
- name: big-gpu-impl
driver: remote
remote:
endpoint: https://gpu.example.com:8790 # wss:// derived automatically
tokenEnv: AGENTMESH_REMOTE_TOKEN # value read from env, never stored
remoteProfile: opencode # which daemon-side profile to run
# ca: ./certs/daemon-ca.pem # trust a self-signed daemon certThen the master drives it exactly like a local worker — task_submit { profile: "big-gpu-impl" }, task_wait, task_approve when a remote permission request
arrives. Remote events are streamed back and persisted locally.
Reconnect & reconciliation
If a connection drops mid-run, the master must not guess the outcome. The daemon persists every task to its local store, so a client can re-ask for the authoritative record with a one-shot query — no live turn socket required:
import { queryRemoteTask } from "agentmesh";
const rec = await queryRemoteTask(
{ endpoint: "https://gpu.example.com:8790", tokenEnv: "AGENTMESH_REMOTE_TOKEN" },
taskId,
);
// rec is the real TaskRecord, or undefined if the daemon has no such task.Because the daemon cancels orphaned tasks when a socket closes, a task that was
running at drop time reconciles to cancelled — never a false succeeded.
RemoteWorkerDriver.queryTask(remote, taskId) exposes the same call.
For a still-running task you'd rather resume than re-run, start the daemon
with an orphan grace window (orphanGraceMs) and re-attach: the daemon replays
every persisted event after a sequence boundary, then streams live events to
completion — no re-submission, no duplicated work.
import { attachRemoteTask } from "agentmesh";
const rec = await attachRemoteTask(
{ endpoint: "https://gpu.example.com:8790", tokenEnv: "AGENTMESH_REMOTE_TOKEN" },
taskId,
{
afterSequence: lastSeq, // resume where we left off (0 = full replay)
onEvent: (ev) => render(ev), // replayed + live events, deduped by sequence
onPermissionRequest: async (req) => decide(req), // approvals still round-trip
},
);
// rec is the authoritative terminal TaskRecord.With orphanGraceMs: 0 (the default) a drop cancels the task immediately and
queryRemoteTask reads back cancelled; with a grace window the task survives a
brief disconnect and attachRemoteTask picks its stream back up. Aborting an
attach detaches without cancelling the remote task.
Resumption can also be transparent: set resume: true on a remote profile and
runTurn itself re-attaches on a mid-run drop (bounded by reconnectAttempts /
reconnectDelayMs), dedupes replayed events by sequence, and resolves normally —
the master never sees the blip. Pair it with a daemon orphanGraceMs window.
profiles:
- name: big-gpu-impl
driver: remote
remote:
endpoint: https://gpu.example.com:8790
tokenEnv: AGENTMESH_REMOTE_TOKEN
remoteProfile: opencode
resume: true # auto re-attach if the socket drops mid-turn
reconnectAttempts: 3
reconnectDelayMs: 500Security model
Token required. The daemon refuses to start if
tokenEnvis unset or the variable is empty; every upgrade/request is checked with a constant-time comparison (timingSafeEqual). Tokens are read from the environment — never written to config or committed to version control.Zero-downtime key rotation.
tokenEnvmay name several comma-separated environment variables, and each variable may hold several comma-separated tokens, so the daemon can accept an old and a new secret at once. SendSIGHUP(or calldaemon.reloadTokens()) to re-read them without restarting — add the new secret, migrate clients, then drop the old one. Reload refuses an empty set so the daemon can never silently become unauthenticated or lock itself out.Workdir whitelist.
daemon.allowCwdrestricts where a remote task may run; acwdoutside it (including../traversal and sibling-prefix tricks like/srv/repos_evil) is rejected before any worker starts.TLS for anything non-loopback.
assertSafeBindrefuses to bind a non-loopback host without TLS unless you pass--allow-insecure. Loopbackhttp://is allowed for local dev/tests.ACP stdio stays local. The daemon runs ACP in-process on machine B; only typed task events cross the wire. There is no raw stdio passthrough.
No orphans. If the WebSocket drops, the daemon cancels any non-terminal task it was running for that connection — unless an
orphanGraceMswindow is configured, during which the task stays alive so a client canattachand resume it. Either way a dropped task is never silently reported as success.Structured audit trail. Every control-plane decision — auth success/failure (with peer address), connection open/close, submit and its whitelist/profile verdicts, approvals, cancels, attaches, orphan handling, and credential reloads — is appended as NDJSON to
daemon.auditPath(default<workdir>/daemon-audit.ndjson). Secrets are never written: a token is reduced to a 12-hex non-reversible fingerprint, and untrusted task/agent text is not logged at all. Writes are buffered so a flood of bad-token attempts can't block the event loop. SetauditPath: ""to disable.Recursion guard. A master is itself an agent, so a delegation chain (or a master↔worker loop) could recurse forever.
limits.maxDelegationDepth(defaultcaps it: agentmesh reads its own level from
$AGENTMESH_DELEGATION_DEPTH(0 for a top-level master) and injects level+1 into every agent it spawns, so a nested agentmesh inherits the chain depth across process boundaries. A submit whose effective depth reaches the cap fails withdelegation_depth_exceededwithout spawning a worker; a caller-stamped depth can only raise it, never lower the inherited base.
Remote tests
test/remote.test.ts boots an ephemeral loopback daemon against mocks and drives
a real RemoteWorkerDriver client through it — dispatch + streamed events,
approval round-trip, cancel, bad-token rejection, cwd-whitelist refusal, the
/healthz vs /profiles auth split, reconnect reconciliation (completed-task
query, unknown-id → undefined, and a dropped mid-run connection reconciling to
cancelled not succeeded), push-based resumption (attach replays a
dropped-but-grace task to success, afterSequence replays only newer events,
unknown-task attach rejects, and runTurn transparently re-attaches after a
mid-run drop when resume: true), zero-downtime key rotation (multiple valid
tokens accepted at once over both HTTP and the WS upgrade, and reloadTokens()
rotating live while refusing an empty set), a structured audit trail (records
auth/submit/refusal decisions and asserts no token ever appears in cleartext),
and unit coverage of assertSafeBind / assertCwdAllowed. 18 remote tests, no
network, no API cost.
Architecture
src/
types.ts domain types (WorkerProfile, TaskRecord, TaskStatus, TaskEvent, ...)
config.ts YAML load + zod validation
statemachine.ts 8-state transitions + terminal/legality guards
worktree.ts git-worktree isolation for parallel workers
registry.ts worker lifecycle + bounded event log
approvals.ts ApprovalBroker: bridges permission/elicitation to the master
tasks.ts TaskManager: async 8-state lifecycle, timeout/cancel, approvals
store/
memory.ts in-memory TaskStore (tests, persist:false)
sqlite.ts durable TaskStore on node:sqlite (default)
orchestrator.ts the core facade: workers + tasks + store + drivers
drivers/
factory.ts picks a driver per profile (acpx/remote lazily constructed)
acpxDriver.ts real ACP workers via acpx runtime (+ permission/elicitation hooks)
mockDriver.ts deterministic offline worker (demo/tests, incl. requireApproval)
remote/
protocol.ts versioned wire messages (submit/approve/respond/cancel + events)
security.ts token compare, cwd whitelist, TLS load, safe-bind guard
daemon.ts WorkerDaemon: HTTP+WS shim wrapping a local Orchestrator
client.ts RemoteWorkerDriver: a remote profile as a normal WorkerDriver
mcp/server.ts exposes the orchestrator as 15 MCP tools over stdio
cli.ts demo | list | mcp | dispatch | submit | tasks | daemonRole symmetry: nothing in the code says "opencode is master". The master is whoever has the MCP server attached; workers are whoever the adapter drives. Swap the two and it still works.
Verification
Checked on this machine (Node v26, Windows):
tsc --noEmit— clean.npm test— 36/36 pass:test/orchestrator.test.ts— spawn/dispatch, event streaming, worktree.test/tasks.test.ts(12) — happy path, worker failure,timeout → timed_out,cancel → cancelled,waiting → approve → succeeded,reject → failed, idempotency de-dup, SQLite restart-and-requery, store column-merge safety, and the recursion guard (depth-stamped success within the cap, refusal at the cap with no worker spawned, and an inherited env depth tripping the cap).test/remote.test.ts(18) — loopback daemon ↔RemoteWorkerDriver: dispatch + streamed events, approval round-trip, cancel, bad-token rejection, cwd-whitelist refusal,/healthzvs/profilesauth split, reconnect reconciliation (completed-task query, unknown-id →undefined, dropped mid-run →cancellednotsucceeded), push-based resumption (attachreplays to success,afterSequenceboundary, unknown-task reject, transparentrunTurnre-attach), zero-downtime key rotation (multi-token accept over HTTP + WS, livereloadTokens(), empty-set refusal), a structured NDJSON audit trail (decisions recorded, token never in cleartext), and unit guards forassertSafeBind/assertCwdAllowed.
npm run demo— Scene A (sync dispatch to two workers) and Scene B (submit → waiting → approve → succeeded) both close.npm run smoke— real MCP stdio server through the full lifecycle; event sequenceaccepted, started, output…, waiting, resumed, output…, completed.agentmesh daemonboot check —/healthzopen,/profiles401 without a token and 200 withAuthorization: Bearer …, clean SIGINT shutdown.scripts/real-smoke.ts— real local OpenCode and Pi driven throughtask_submit → task_wait → task_result; both returnOK, reachsucceeded, and persist their tasks + events to SQLite (2 tasks / 27 events observed).
Roadmap
Done:
✅ Async task lifecycle with an 8-state machine + SQLite persistence.
✅ Human-in-the-loop approvals (permission + elicitation) via the broker.
✅ Real ACP workers (OpenCode, Pi) verified end-to-end.
✅ Remote Worker Daemon: run workers on other machines over an authenticated, TLS-capable channel (ACP stdio never crosses the network).
Next:
Parallel fan-out across concurrent workers + a real queue owner.
Master-side prompt/skill pack so the master delegates well by default.
Cross-worker handoff (pipe one worker's diff into another's context).
Structured result contracts (JSON output schema per task) + cost/usage roll-up from ACP
usage_updateevents.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Agent-native collaboration network: orchestrate a team of long-running agents from any MCP client.
Control plane for autonomous software labor. Agents claim objectives over MCP with audit trail.
AI work orchestration for plans, tasks, teams, and coding-agent dispatch.
Public MCP for agent verification, work discovery and governed interoperability.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceMulti-agent AI orchestrator that runs parallel coding agents in isolated sessions with self-improving intelligence, exposed via an MCP server for task execution and management.MIT
- AlicenseNot gradedqualityDmaintenanceEnables multi-model leader-worker agent orchestration, workflow execution, and deterministic validation via structured MCP tools.8 npmApache 2.0
- AlicenseAqualityAmaintenanceEnables orchestrator agents to route coding tasks to native CLI worker agents via MCP, providing tools for agent discovery, task submission, waiting, inspection, and cancellation.53MIT
- AlicenseAqualityAmaintenanceEnables users to delegate text and code tasks across multiple AI models with automatic failover, maintain shared mission memory and checkpoints across MCP harnesses, and coordinate parallel role-based subagents. It also supports model discovery, provider inventory, health checks, and stdio/HTTP tool access while leaving patches and tests to the harness.24MIT