Appearance
NATS-Based Arm Agent System β
This document describes the NATS messaging system used for distributed arm management in Coleo. This allows arms to run on multiple hosts while being coordinated by a central API server.
Overview β
The arm agent system uses NATS as a lightweight message queue for communication between the API server and distributed arm agents. This architecture enables:
- Server Restart Resilience: Arms keep running even if the API server restarts
- Multi-Host Support: Arms can run on different machines than the API server
- Decoupled Architecture: Agents manage local arms independently
- Real-time Events: Status changes propagate instantly via pub/sub
Architecture β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β API Server (Host A) β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββ β
β β ArmClient β β WebSocket Server β β
β β - Agent trackingβ β - Dashboard connections β β
β β - Command send β β - Event broadcasting β β
β β - Event receive β β β β
β ββββββββββ¬ββββββββββ ββββββββββββββββββββββββββββββββββββ β
βββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β NATS Protocol (nats://host:4222)
β
βββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββββ
β NATS Server β
β (Docker: nats:2.10-alpine) β
β β
β Topics: β
β - coleo.agent.register (agent registration) β
β - coleo.agent.heartbeat (agent liveness) β
β - coleo.agent.{id}.command (commands to specific agent) β
β - coleo.brain.messages (arm -> brain ingress) β
β - coleo.events.* (JetStream event history) β
βββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββ΄ββββββββ¬ββββββββββββββββββββ
β β β
βββββΌββββββββββββ βββΌββββββββββββββββ βββΌββββββββββββββββ
β ArmAgent β β ArmAgent β β ArmAgent β
β (Host A) β β (Host B) β β (Host C) β
β β β β β β
β βββββββββββββ β β βββββββββββββ β β βββββββββββββ β
β β OpenCode β β β β OpenCode β β β β OpenCode β β
β β Arm 1 β β β β Arm 2 β β β β Arm 3 β β
β βββββββββββββ β β βββββββββββββ β β βββββββββββββ β
β βββββββββββββ β β β β βββββββββββββ β
β β OpenCode β β β β β β OpenCode β β
β β Arm 4 β β β β β β Arm 5 β β
β βββββββββββββ β β β β βββββββββββββ β
βββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββComponents β
NATS Server β
A lightweight, high-performance message broker. We run it in Docker for reliability:
yaml
# docker-compose.yml
services:
nats:
image: nats:2.10-alpine
command: ["--jetstream", "--http_port", "8222"]
ports:
- "4222:4222" # Client connections
- "8222:8222" # MonitoringNATS provides:
- Pub/Sub: For broadcasting events to all subscribers
- Request/Reply: For sending commands and waiting for responses
- JetStream: Durable event and message persistence for replay/recovery
ArmClient (API Server Side) β
Located in src/nats/arm-client.ts, the ArmClient runs in the API server and:
- Tracks Connected Agents: Maintains a registry of all connected ArmAgents
- Routes Commands: Sends spawn/kill/prompt commands to the correct agent
- Receives Events: Subscribes to arm events and forwards them to WebSocket clients
- Load Balancing: Selects the best agent for new arm placement
typescript
// Key methods
armClient.spawnArm(agentId, armId, options) // Spawn an arm on specific agent
armClient.killArm(armId) // Kill an arm (routes to correct agent)
armClient.sendPrompt(armId, prompt) // Send prompt to an arm
armClient.findBestAgent(harness) // Find agent with capacity
armClient.getAgents() // List all connected agentsArmAgent (Host Side) β
Located in src/agent/arm-agent.ts, the ArmAgent runs as a daemon on each host that will run arms:
- Manages Local Arms: Spawns and manages OpenCode processes locally
- Executes Commands: Receives and executes spawn/kill/prompt commands
- Reports Status: Sends heartbeats and arm status updates
- Survives Restarts: Arms keep running if the agent restarts
bash
# Start an agent daemon
coleo agent start --nats-url nats://localhost:4222Message Types β
Agent Registration β
When an agent starts, it publishes its info:
typescript
interface AgentInfo {
agentId: string; // Unique agent identifier
hostname: string; // Machine hostname
platform: string; // darwin, linux, windows
startedAt: string; // ISO timestamp
version: string; // Agent version
capabilities: string[]; // Supported harnesses: ["opencode", "opencode-api"]
maxArms: number; // Maximum concurrent arms
}Topic: coleo.agent.register
Agent Heartbeat β
Agents send periodic heartbeats (every 30 seconds by default):
typescript
interface AgentHeartbeat {
agentId: string;
timestamp: string;
activeArms: string[]; // List of arm IDs currently running
load: {
cpu: number; // CPU usage (0-1)
memory: number; // Memory usage (0-1)
};
}Topic: coleo.agent.heartbeat
Commands (Request/Reply) β
Commands are sent to specific agents using request/reply pattern:
typescript
// Spawn a new arm
interface SpawnArmCommand {
type: 'spawn';
requestId: string;
armId: string;
name: string;
domain: string;
harness: string;
provider?: string;
model?: string;
workDir?: string;
}
// Kill an arm
interface KillArmCommand {
type: 'kill';
requestId: string;
armId: string;
}
// Send prompt to arm
interface SendPromptCommand {
type: 'prompt';
requestId: string;
armId: string;
prompt: string;
}
// Response format
interface CommandResponse<T = unknown> {
requestId: string;
success: boolean;
error?: string;
data?: T;
}Topic: coleo.agent.{agentId}.command
Arm Events + Brain Ingress β
Agents publish arm lifecycle events:
typescript
interface ArmSpawnedEvent {
type: 'arm.spawned';
armId: string;
agentId: string;
state: ArmState;
}
interface ArmKilledEvent {
type: 'arm.killed';
armId: string;
agentId: string;
}
interface ArmStatusChangedEvent {
type: 'arm.status_changed';
armId: string;
agentId: string;
oldStatus: ArmStatus;
newStatus: ArmStatus;
}Topics:
coleo.arm.{armId}.event(lifecycle/log style per-arm events)coleo.brain.messages(validated arm-to-brain operational messages)coleo.events.*(JetStream-backed event history)
Communication Flows β
Spawning an Arm β
CLI/UI API Server NATS ArmAgent
β β β β
β POST /api/arms/spawnβ β β
β βββββββββββββββββββ>β β β
β β β β
β β findBestAgent() β β
β β (select agent) β β
β β β β
β β Request: spawn β β
β β ββββββββββββββββ>β β
β β β βββββββββββββββ>β
β β β β
β β β spawn arm β
β β β locally β
β β β β
β β β Reply: success β
β β β <ββββββββββββββββ
β β <βββββββββββββββββ β
β β β β
β β β Pub: arm.spawnedβ
β β β <ββββββββββββββββ
β β <βββββββββββββββββ β
β β β β
β β broadcast to β β
β β WebSocket clientsβ β
β β β β
β 200 OK {armId} β β β
β <ββββββββββββββββββββ β βSending a Prompt β
CLI/UI API Server NATS ArmAgent
β β β β
β POST /arms/{id}/prompt β β
β βββββββββββββββββββ>β β β
β β β β
β β getAgentForArm() β β
β β (lookup mapping) β β
β β β β
β β Request: prompt β β
β β ββββββββββββββββ>β β
β β β βββββββββββββββ>β
β β β β
β β β send to PTY β
β β β β
β β β Reply: success β
β β β <ββββββββββββββββ
β β <βββββββββββββββββ β
β β β β
β 200 OK β β β
β <ββββββββββββββββββββ β βAgent Failure Detection β
API Server NATS ArmAgent
β β β
β β heartbeat β
β <βββββββββββββββββ <ββββββββββββββββ
β β β
β update lastSeen β β
β β β
β β heartbeat β
β <βββββββββββββββββ <ββββββββββββββββ
β β β
β ... β β
β β β
β (no heartbeat β β (agent dies)
β for 90 sec) β β
β β β
β mark agent stale β β
β broadcast β β
β agent.disconnected β
β β βDatabase Schema β
The arms table includes agent tracking columns:
sql
-- Migration 014: Add agent_id and host for distributed arm management
ALTER TABLE arms ADD COLUMN agent_id TEXT;
ALTER TABLE arms ADD COLUMN host TEXT;
CREATE INDEX idx_arms_agent ON arms(agent_id);Configuration β
Environment Variables β
| Variable | Default | Description |
|---|---|---|
COLEO_NATS_HOST | 127.0.0.1 | Local NATS bind and client host used by the API server and agents. |
COLEO_NATS_PORT | 4222 | Local NATS client port. |
COLEO_NATS_HTTP_PORT | 8222 | Local NATS monitoring port. |
COLEO_NATS_URL | derived from host/port | External NATS URL override. If unset, coleo serve bootstraps project-local NATS. |
Use separate NATS and monitoring ports for each local project. Otherwise a second project can connect to an existing NATS listener and share JetStream events unintentionally. coleo init can generate available values and write them to the project's mise.toml.
API Server Config β
For distributed arm orchestration and brain message ingress, NATS + JetStream are required:
opencode-apiandopencodeare daemon-managed and require at least one connectedArmAgentopencode-tuican still be spawned locally for operator-visible sessions- The Observatory Arms view and the CLI both use
POST /api/arms/:id/spawn, so a stopped or unspawned arm profile can be started remotely from the browser once anArmAgentis connected
If NATS is unavailable, distributed arm management and stream-backed activity/history are unavailable.
Agent Config β
bash
coleo agent start \
--nats-url nats://localhost:4222 \ # NATS server URL
--max-arms 10 \ # Max concurrent arms
--heartbeat-interval 30000 \ # Heartbeat interval (ms)
--verbose # Debug loggingRunning the System β
Local Development (Single Host) β
bash
# Start the API server
# If COLEO_NATS_URL is unset, this will auto-start local NATS
coleo serveWith Distributed Arms β
bash
# Terminal 1: Start API server
# If COLEO_NATS_URL is unset, this will auto-start local NATS
coleo serve
# Terminal 2: Start agent (same or different host)
coleo agent start --nats-url nats://localhost:4222Multi-Host Setup β
bash
# On server (192.168.1.100):
docker compose up -d nats
coleo serve
# On laptop:
coleo agent start --nats-url nats://192.168.1.100:4222
# On desktop:
coleo agent start --nats-url nats://192.168.1.100:4222Monitoring β
NATS Monitoring β
NATS provides a monitoring endpoint at port 8222:
bash
# Check NATS health
curl http://localhost:8222/healthz
# View connections
curl http://localhost:8222/connz
# View subscriptions
curl http://localhost:8222/subszAPI Endpoints β
bash
# List connected agents
curl http://localhost:8080/api/agents
# Get specific agent
curl http://localhost:8080/api/agents/{agentId}
# List arms on an agent
curl http://localhost:8080/api/agents/{agentId}/armsComparison with Garden System β
This NATS-based system is a simpler precursor to the full "Garden" concept described in distributed.md:
| Feature | NATS Agents | Gardens (Future) |
|---|---|---|
| Scope | Arm management only | Full environment |
| Auth | Simple API key | Token-based join flow |
| Features | Spawn, kill, prompt | + MCP servers, env vars |
| Persistence | JetStream-backed events/messages | Local state file |
| Offline mode | No | Yes (buffered) |
The NATS system provides the foundation for distributed arms. The Garden concept builds on top of this to provide a complete remote development environment.
Troubleshooting β
Agent Can't Connect to NATS β
[NATS] Connection attempt 1 failed: Error: connect ECONNREFUSEDSolution: Ensure NATS is running and accessible:
bash
docker compose up -d nats
curl http://localhost:8222/healthzCommands Timing Out β
[ArmClient] Command timed out after 30000msPossible causes:
- Agent is not running or disconnected
- Agent is overloaded
- Network issues between server and agent
Debug:
bash
# Check agent status
curl http://localhost:8080/api/agents
# Check NATS connections
curl http://localhost:8222/connzArms Not Appearing After Agent Restart β
ArmAgent performs best-effort process recovery on startup:
opencode-api: recoverable when process + port are still available- PTY/TUI harnesses: re-attachment is limited and may require manual restart
- API-initiated
arm recoverwill reattach only when the runtime is confirmed by a live agent; otherwise it restarts the arm on a reachable compatible agent.
If recovery fails, restart the affected arm from the API/CLI.
Future Enhancements β
- Agent State Persistence: Save running arm info to recover after restart
- Arm Migration: Move arms between agents
- Resource Limits: CPU/memory limits per agent
- Encrypted Transport: TLS for NATS connections
- Agent Groups: Logical grouping for placement policies
