205 lines
8.9 KiB
Markdown
205 lines
8.9 KiB
Markdown
# Agent Orchestrator — Technical Architecture
|
|
|
|
This document explains how the various parts of the Agent Orchestrator communicate with each other: where HTTP is used, where WebSocket is used, and what each carries.
|
|
|
|
---
|
|
|
|
## System Overview
|
|
|
|
```mermaid
|
|
graph TB
|
|
subgraph Browser["Browser / Dashboard"]
|
|
UI["React Dashboard\n(Next.js App Router)"]
|
|
XTerm["xterm.js\nTerminal UI"]
|
|
end
|
|
|
|
subgraph NextJS["Next.js Server — :3000 (single process)"]
|
|
subgraph HTTPAPI["① HTTP REST /api/* — request / response"]
|
|
Sessions["GET /api/sessions\nGET /api/sessions/:id\nPOST /api/sessions/:id/message\nPOST /api/sessions/:id/restore\nPOST /api/sessions/:id/kill\nPOST /api/spawn\nGET /api/projects GET /api/agents\nGET /api/issues POST /api/prs/:id/merge\nPOST /api/webhooks/**"]
|
|
Patches["GET /api/sessions/patches\n(lightweight: id, status, activity,\nattentionLevel, lastActivityAt)"]
|
|
end
|
|
SessionMgr["Session Manager\n(reads flat files in\n~/.agent-orchestrator/)"]
|
|
end
|
|
|
|
subgraph MuxServer["② WebSocket Server — :14801 (separate Node process)"]
|
|
MuxWS["ws://host:14801/mux\nMultiplexed — two sub-channels\nover one connection"]
|
|
TermMgr["TerminalManager\n(node-pty → tmux PTY)"]
|
|
Broadcaster["SessionBroadcaster\n(setInterval every 3s →\nGET /api/sessions/patches)"]
|
|
end
|
|
|
|
subgraph Agents["AI Agents (one tmux window each)"]
|
|
ClaudeCode["Claude Code"]
|
|
Codex["Codex"]
|
|
Aider["Aider"]
|
|
OpenCode["OpenCode"]
|
|
end
|
|
|
|
subgraph External["External Services"]
|
|
GitHub["GitHub API"]
|
|
Linear["Linear API"]
|
|
end
|
|
|
|
%% ① HTTP — user actions & data fetching
|
|
UI -- "① HTTP GET/POST\n(on demand: load sessions,\nsend message, spawn, merge PR…)" --> Sessions
|
|
|
|
%% ② WebSocket terminal sub-channel
|
|
XTerm -- "② WS sub-channel 'terminal'\nkeystrokes → {ch:terminal, type:data}\noutput ← {ch:terminal, type:data}" --> MuxWS
|
|
MuxWS --> TermMgr
|
|
TermMgr -- "PTY read/write" --> ClaudeCode
|
|
TermMgr -- "PTY read/write" --> Codex
|
|
TermMgr -- "PTY read/write" --> Aider
|
|
TermMgr -- "PTY read/write" --> OpenCode
|
|
|
|
%% ② WebSocket sessions sub-channel
|
|
Broadcaster -- "HTTP GET /api/sessions/patches\nevery 3s" --> Patches
|
|
Patches -- "reads" --> SessionMgr
|
|
Broadcaster -- "② WS sub-channel 'sessions'\n{ch:sessions, type:snapshot,\n sessions:[{id,status,activity,\n attentionLevel,lastActivityAt}]}" --> MuxWS
|
|
MuxWS -- "session patches\n→ useSessionEvents()\n→ useMuxSessionActivity()" --> UI
|
|
|
|
%% Mux auto-recovery calls back to Next.js
|
|
TermMgr -- "① HTTP POST /api/sessions/:id/restore\n(auto-recovery when tmux dies)" --> Sessions
|
|
|
|
%% External
|
|
Sessions -- "REST calls" --> GitHub
|
|
Sessions --> Linear
|
|
GitHub -- "POST /api/webhooks/**" --> Sessions
|
|
```
|
|
|
|
---
|
|
|
|
## Communication Channels
|
|
|
|
### 1. HTTP / REST — `/api/*` on port 3000
|
|
|
|
Used for all request-response interactions. The browser calls these on demand; the CLI and the WebSocket server also use them.
|
|
|
|
| Endpoint | Method | Purpose |
|
|
|----------|--------|---------|
|
|
| `/api/sessions` | GET | List all sessions (with PR / issue metadata) |
|
|
| `/api/sessions/light` | GET | Lightweight session list (minimal fields) |
|
|
| `/api/sessions/patches` | GET | Ultra-light patches (id, status, activity, attentionLevel) — polled by the WS server every 3s |
|
|
| `/api/sessions/:id` | GET | Full session detail |
|
|
| `/api/sessions/:id/message` | POST | Send a message/command to a live agent |
|
|
| `/api/sessions/:id/restore` | POST | Respawn a terminated session |
|
|
| `/api/sessions/:id/kill` | POST | Terminate a running session |
|
|
| `/api/sessions/:id/files` | GET | Browse workspace files |
|
|
| `/api/sessions/:id/diff/**` | GET | File diff view |
|
|
| `/api/sessions/:id/sub-sessions` | GET / POST | List / create sub-sessions (forked agents) |
|
|
| `/api/spawn` | POST | Spawn a new agent session |
|
|
| `/api/projects` | GET | List configured projects |
|
|
| `/api/agents` | GET | List registered agent plugins |
|
|
| `/api/issues` | GET | Fetch backlog issues |
|
|
| `/api/backlog` | GET | Backlog summary |
|
|
| `/api/prs/:id/merge` | POST | Merge a PR |
|
|
| `/api/observability` | GET | Health and metrics summary |
|
|
| `/api/verify` | POST | Verify environment setup |
|
|
| `/api/setup-labels` | POST | Set up GitHub labels |
|
|
| `/api/webhooks/**` | POST | Inbound webhooks from GitHub / GitLab |
|
|
|
|
---
|
|
|
|
### 2. WebSocket (Multiplexed) — `ws://localhost:14801/mux`
|
|
|
|
A **bidirectional multiplexed channel** on a separate Node.js process. A single WebSocket connection carries two independent sub-channels:
|
|
|
|
- **`terminal` channel** — raw PTY I/O for xterm.js
|
|
- **`sessions` channel** — real-time session status patches (fed by `SessionBroadcaster` polling `/api/sessions/patches` every 3s)
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant XTerm as xterm.js
|
|
participant MuxClient as MuxProvider (browser)
|
|
participant MuxWS as WS Server :14801/mux
|
|
participant PTY as node-pty (tmux)
|
|
participant Next as Next.js :3000
|
|
|
|
MuxClient->>MuxWS: connect ws://localhost:14801/mux
|
|
|
|
Note over MuxClient,MuxWS: Open a terminal
|
|
MuxClient->>MuxWS: {ch:"terminal", id:"sess-1", type:"open"}
|
|
MuxWS->>PTY: attach tmux PTY
|
|
MuxWS-->>MuxClient: {ch:"terminal", id:"sess-1", type:"opened"}
|
|
|
|
Note over MuxClient,MuxWS: Terminal I/O
|
|
XTerm->>MuxClient: user keystrokes
|
|
MuxClient->>MuxWS: {ch:"terminal", id:"sess-1", type:"data", data:"ls\r"}
|
|
MuxWS->>PTY: write to PTY
|
|
PTY-->>MuxWS: output bytes
|
|
MuxWS-->>MuxClient: {ch:"terminal", id:"sess-1", type:"data", data:"file1 file2\r\n"}
|
|
MuxClient-->>XTerm: render output
|
|
|
|
Note over MuxWS,Next: Session patches (every 3s)
|
|
MuxWS->>Next: GET /api/sessions/patches
|
|
Next-->>MuxWS: [{id, status, activity, attentionLevel, lastActivityAt}]
|
|
MuxWS-->>MuxClient: {ch:"sessions", type:"snapshot", sessions:[...]}
|
|
MuxClient-->>MuxClient: useSessionEvents() + useMuxSessionActivity() update React state
|
|
|
|
Note over MuxWS,Next: Auto-recovery (session dead)
|
|
MuxWS->>Next: POST /api/sessions/sess-1/restore
|
|
Next-->>MuxWS: 200 OK
|
|
MuxWS->>PTY: reattach to new tmux session
|
|
```
|
|
|
|
**Message types:**
|
|
|
|
| Direction | Channel | Type | Payload |
|
|
|-----------|---------|------|---------|
|
|
| Client→Server | `terminal` | `open` | `{ id }` |
|
|
| Client→Server | `terminal` | `data` | `{ id, data: string }` |
|
|
| Client→Server | `terminal` | `resize` | `{ id, cols, rows }` |
|
|
| Client→Server | `terminal` | `close` | `{ id }` |
|
|
| Client→Server | `subscribe` | — | `{ topics: ["sessions"] }` |
|
|
| Client→Server | `system` | `ping` | — |
|
|
| Server→Client | `terminal` | `opened` | `{ id }` |
|
|
| Server→Client | `terminal` | `data` | `{ id, data: string }` |
|
|
| Server→Client | `terminal` | `exited` | `{ id, code }` |
|
|
| Server→Client | `terminal` | `error` | `{ id, message }` |
|
|
| Server→Client | `sessions` | `snapshot` | `{ sessions: SessionPatch[] }` |
|
|
| Server→Client | `system` | `pong` | — |
|
|
|
|
---
|
|
|
|
## Process Map
|
|
|
|
```mermaid
|
|
graph LR
|
|
subgraph Host
|
|
CLI["ao CLI\n(packages/cli)"]
|
|
Next["Next.js\npackages/web — :3000"]
|
|
MuxSrv["Terminal WS Server\npackages/web/server — :14801"]
|
|
end
|
|
|
|
subgraph Storage["Flat-file Storage"]
|
|
Sessions2["~/.agent-orchestrator/\n{hash}-{project}/\n sessions/{id} ← key-value\n worktrees/{id}/\n archive/{id}_{ts}/"]
|
|
end
|
|
|
|
CLI -- "pnpm ao start\nspawns both servers" --> Next
|
|
CLI -- "spawns" --> MuxSrv
|
|
Next -- "reads / writes" --> Sessions2
|
|
MuxSrv -- "GET /api/sessions/patches (every 3s)" --> Next
|
|
MuxSrv -- "POST /api/sessions/:id/restore (recovery)" --> Next
|
|
```
|
|
|
|
The CLI (`ao start`) forks two long-running processes:
|
|
- **Next.js** on `:3000` — serves the dashboard and all REST routes
|
|
- **Terminal WS server** on `:14801` — handles multiplexed WebSocket + PTY management + session patch polling
|
|
|
|
Both processes share no in-memory state; coordination happens through flat files in `~/.agent-orchestrator/` and HTTP calls from the WS server to Next.js.
|
|
|
|
---
|
|
|
|
## Data Flow Summary
|
|
|
|
| Scenario | Protocol | Path |
|
|
|----------|----------|------|
|
|
| Load dashboard | HTTP GET | Browser → `:3000/` (SSR page) |
|
|
| List sessions | HTTP GET | Browser → `:3000/api/sessions` |
|
|
| Spawn new agent | HTTP POST | Browser → `:3000/api/spawn` |
|
|
| Send message to agent | HTTP POST | Browser → `:3000/api/sessions/:id/message` |
|
|
| Real-time session status | WebSocket | Browser ← `:14801/mux` `sessions` sub-channel (pushed every 3s) |
|
|
| Terminal output / input | WebSocket | Browser ↔ `:14801/mux` `terminal` sub-channel (bidirectional) |
|
|
| WS server fetches patches | HTTP GET | `:14801` → `:3000/api/sessions/patches` (every 3s) |
|
|
| WS server restores session | HTTP POST | `:14801` → `:3000/api/sessions/:id/restore` |
|
|
| GitHub notifies of CI / PR | HTTP POST | GitHub → `:3000/api/webhooks/github` |
|
|
| CLI queries sessions | HTTP GET | `ao` CLI → `:3000/api/sessions` |
|