# Agent Orchestrator Architecture Agent Orchestrator is a long-running Go daemon that supervises multiple parallel AI coding agent sessions. Each session runs in an isolated git worktree with its own runtime, while the daemon coordinates lifecycle, observes external state, and routes feedback. ## Table of Contents - [Mental Model](#mental-model) - [System Overview](#system-overview) - [Core Architectural Principles](#core-architectural-principles) - [Component Architecture](#component-architecture) - [Data Flows](#data-flows) - [Persistence and CDC](#persistence-and-cdc) - [Status Derivation](#status-derivation) - [Lifecycle Management](#lifecycle-management) - [Observation Loops](#observation-loops) - [HTTP Layer](#http-layer) - [Terminal Multiplexing](#terminal-multiplexing) --- ## Mental Model The fundamental architecture follows a simple three-stage pipeline: ```mermaid flowchart LR A[OBSERVE
External Facts] --> B[UPDATE
Durable Facts] B --> C[DERIVE
Display Status / ACT] ``` **Key insight:** Display status is never stored. It is computed at read time from durable facts. ### Durable Session Facts The only persistent session state is: - `activity_state` — What the agent last reported (`active`, `idle`, `waiting_input`, `exited`) - `is_terminated` — Whether the session should be treated as over - PR facts — `pr`, `pr_checks`, `pr_comment` tables ### What is NOT Durable Display status like `working`, `needs_input`, `ci_failed`, `mergeable` are **computed at read time** by the service layer from the durable facts above. --- ## System Overview ```mermaid graph TB subgraph Frontend FE[Electron + React UI] CLI[ao CLI] end subgraph HTTP["HTTP Daemon (127.0.0.1)"] Controllers[REST Controllers] SSE[SSE Events] Terminal[Terminal WebSocket] end subgraph Core["Core Services"] SessionSvc[Session Service] ProjectSvc[Project Service] PRSvc[PR Service] ReviewSvc[Review Service] SessionMgr[Session Manager] LCM[Lifecycle Manager] end subgraph Observe["Observation Layer"] SCMObserver[SCM Observer] Reaper[Runtime Reaper] end subgraph Storage["Persistence Layer"] SQLite[(SQLite DB)] CDC[CDC Poller] Broadcaster[Event Broadcaster] end subgraph Adapters["Adapters"] AgentAdapter[Agent Adapters] RuntimeAdapter[Runtime tmux/conpty] WorkspaceAdapter[Workspace git worktree] SCMAdapter[SCM GitHub] end FE -->|REST/SSE| Controllers CLI -->|REST| Controllers Controllers --> SessionSvc Controllers --> ProjectSvc Controllers --> PRSvc SessionSvc --> SessionMgr SessionMgr --> LCM SessionMgr --> AgentAdapter SessionMgr --> RuntimeAdapter SessionMgr --> WorkspaceAdapter LCM --> SQLite LCM --> AgentAdapter SCMObserver --> SCMAdapter SCMObserver --> SQLite SCMObserver --> LCM Reaper --> RuntimeAdapter Reaper --> SQLite Reaper --> LCM CDC -->|poll| SQLite CDC --> Broadcaster Broadcaster --> SSE Terminal --> RuntimeAdapter ``` --- ## Core Architectural Principles ### 1. Port-Based Design Core code never depends on concrete implementations. All external systems are accessed through port interfaces defined in `backend/internal/ports/`: ```mermaid graph LR Core[Core Services] -->|consumes| Ports[Port Interfaces] Adapters[Adapters] -->|implement| Ports External[External Systems] -->|wrapped by| Adapters ``` ### 2. Durable Facts, Derived Status Storage layer persists minimal facts. Service layer computes display status on-demand: ```mermaid flowchart LR SQLite[(SQLite)] -->|raw facts| Service[Session Service] Service -->|compute| Status[Display Status] Service -->|enrich| UI[Dashboard/UI] SQLite -->|activity_state| Service SQLite -->|is_terminated| Service SQLite -->|PR facts| Service SQLite -->|runtime_handle| Service ``` ### 3. Observer Pattern Observation is separated from action: - **Observe layer** — SCM Observer, Runtime Reaper poll external state - **Lifecycle layer** — Reduces observations into durable facts - **Service layer** — Computes display status from facts ### 4. Change Data Capture All durable changes flow through a CDC pipeline: ```mermaid flowchart LR DB[(SQLite)] -->|triggers| ChangeLog[change_log table] ChangeLog -->|tail| Poller[CDC Poller] Poller -->|Event| Broadcaster[Event Broadcaster] Broadcaster -->|fan-out| Subscribers[Subscribers] Subscribers -->|SSE| Clients[Dashboard Clients] ``` --- ## Component Architecture ### Package Layout ``` backend/internal/ ├── domain/ # Shared vocabulary and durable fact records ├── ports/ # Inbound/outbound interfaces ├── service/ # Controller-facing services │ ├── project/ # Project CRUD │ ├── session/ # Session read-model assembly │ ├── pr/ # PR observation service │ └── review/ # Code review service ├── session_manager/ # Internal session command engine ├── lifecycle/ # Durable session fact reducer ├── observe/ # Observation loops │ ├── scm/ # SCM (GitHub) observer │ └── reaper/ # Runtime liveness observer ├── storage/ # SQLite persistence │ └── sqlite/ # DB, migrations, queries, stores ├── cdc/ # Change-log poller and broadcaster ├── httpd/ # HTTP API, controllers, terminal mux ├── terminal/ # Terminal session protocol ├── adapters/ # Concrete adapter implementations │ ├── agent/ # 23+ agent harnesses │ ├── runtime/ # tmux/conpty runtimes │ ├── workspace/ # git worktree │ ├── scm/ # GitHub │ └── tracker/ # GitHub tracker ├── daemon/ # Production wiring └── config/ # Environment-based configuration ``` ### Core Data Flow ```mermaid sequenceDiagram participant UI as Dashboard participant HTTP as HTTP Controller participant Svc as Session Service participant Mgr as Session Manager participant LCM as Lifecycle Manager participant Agent as Agent Adapter participant Runtime as Runtime Adapter participant WS as Workspace Adapter participant DB as SQLite participant CDC as CDC Broadcaster UI->>HTTP: POST /sessions HTTP->>Svc: Spawn(config) Svc->>Mgr: Spawn(config) Note over Mgr: 1. Create session row Mgr->>DB: Insert session DB->>CDC: trigger change_log CDC->>UI: SSE session.created Note over Mgr: 2. Create workspace Mgr->>WS: Create(project, branch) WS->>WS: git worktree add Note over Mgr: 3. Launch runtime Mgr->>Runtime: Create(session) Runtime->>Runtime: Start tmux/conpty Note over Mgr: 4. Start agent Mgr->>Agent: GetLaunchCommand() Agent-->>Mgr: launch command Mgr->>Runtime: Execute(agent command) Note over Mgr: 5. Mark spawned Mgr->>LCM: MarkSpawned(handle) LCM->>DB: Update activity_state DB->>CDC: trigger change_log CDC->>UI: SSE session.updated Mgr-->>Svc: Session(created) Svc-->>HTTP: Session response HTTP-->>UI: 201 Created ``` --- ## Data Flows ### Session Spawn Flow ```mermaid flowchart TD Start([User spawns session]) --> Validate[Validate project config] Validate --> CreateRow[Create session row in SQLite] CreateRow --> CreateWS[Create git worktree] CreateWS --> CreateRT[Launch runtime tmux/conpty] CreateRT --> GetCmd[Get agent launch command] GetCmd --> ExecAgent[Execute agent in runtime] ExecAgent --> MarkSpawned[MarkSpawned in LCM] MarkSpawned --> Trigger1[CDC: session.created] Trigger1 --> Trigger2[CDC: session.updated] Trigger2 --> Done([Session running]) ``` ### Observation Flow ```mermaid flowchart TD subgraph SCM["SCM Observer Loop"] Poll1[Poll PRs every 30s] Poll1 --> Fetch[Fetch from GitHub API] Fetch --> Diff[Semantic diff vs local] Diff --> Changed{Changed?} Changed -->|Yes| WritePR[Write PR/check/comment] Changed -->|No| Wait1[Wait for tick] WritePR --> NotifyLCM[Notify Lifecycle Manager] NotifyLCM --> Trigger1[CDC event] Trigger1 --> Wait1 Wait1 --> Poll1 end subgraph Reaper["Runtime Reaper Loop"] Poll2[Poll every 5s] Poll2 --> Probe[Probe each runtime] Probe --> Report[Report fact to LCM] Report --> Trigger2[CDC event] Trigger2 --> Wait2[Wait for tick] Wait2 --> Poll2 end LCM[Lifecycle Manager] -->|consumes| NotifyLCM LCM -->|consumes| Report ``` ### Feedback Routing Flow ```mermaid sequenceDiagram participant SCM as SCM Observer participant LCM as Lifecycle Manager participant Agent as Agent Adapter participant Runtime as Runtime Adapter SCM->>SCM: Observe PR comment SCM->>LCM: ApplySCMObservation() LCM->>LCM: Detect actionable feedback LCM->>Agent: SendNudge(feedback) SCM->>SCM: Observe CI failure SCM->>LCM: ApplySCMObservation() LCM->>LCM: Detect actionable feedback LCM->>Agent: SendNudge(CI failure) SCM->>SCM: Observe merge conflict SCM->>LCM: ApplySCMObservation() LCM->>LCM: Detect actionable feedback LCM->>Agent: SendNudge(merge conflict) Note over Agent,Runtime: Agent receives nudges via
runtime messages or hooks ``` --- ## Persistence and CDC ### SQLite Schema ```mermaid erDiagram projects ||--o{ sessions : owns sessions ||--o{ pull_requests : owns pull_requests ||--o{ pr_checks : has pull_requests ||--o{ pr_review_threads : has pull_requests ||--o{ pr_comments : has sessions ||--o{ notifications : has change_log }|--|| projects : tracks change_log }|--|| sessions : tracks change_log }|--|| pull_requests : tracks projects { string id PK string name string repo jsonb config } sessions { string id PK string project_id FK string harness string activity_state boolean is_terminated jsonb metadata } pull_requests { string id PK string session_id FK integer number string state string title boolean draft boolean mergeable } pr_checks { string id PK string pr_id FK string name string status string conclusion } change_log { bigint seq PK string table_name string row_id string operation jsonb old_data jsonb new_data } ``` ### CDC Pipeline ```mermaid flowchart LR DB[(SQLite)] -->|INSERT/UPDATE/DELETE| Trigger[DB Trigger] Trigger -->|append| ChangeLog[change_log] ChangeLog -->|poll| Poller[CDC Poller] Poller -->|decode| Decoder[Event Decoder] Decoder -->|Event| Broadcaster[Broadcaster] Broadcaster -->|callback| Sub1[Terminal Fanout] Broadcaster -->|callback| Sub2[SSE Writer] Broadcaster -->|callback| Sub3[Cache Invalidation] Poller -->|watermark| Watermark[seq tracking] Watermark -->|resume position| Poller ``` --- ## Status Derivation ### Display Status Precedence The `service.Session` computes display status from durable facts using this precedence (highest to lowest): ```mermaid flowchart TD CheckTerm{is_terminated?} CheckTerm -->|Yes| PRMerged{PR merged?} CheckTerm -->|No| CheckWait{activity_state
== waiting_input?} PRMerged -->|Yes| Merged[merged] PRMerged -->|No| Terminated[terminated] CheckWait -->|Yes| NeedsInput[needs_input] CheckWait -->|No| CheckPR{Has PR facts?} CheckPR -->|Yes| PRPipeline[PR Pipeline Check] CheckPR -->|No| CheckActive{activity_state
== active?} PRPipeline --> PRState{PR State} PRState -->|ci failed| CIFailed[ci_failed] PRState -->|draft| Draft[draft] PRState -->|changes requested| Changes[changes_requested] PRState -->|not mergeable| Conflict[merge_conflict] PRState -->|mergeable| Mergeable[mergeable] PRState -->|approved| Approved[approved] PRState -->|review pending| ReviewPending[review_pending] PRState -->|open| PROpen[pr_open] CheckActive -->|Yes| Working[working] CheckActive -->|No| CheckSignal{Signal capable
&& no signal?} CheckSignal -->|Yes| NoSignal[no_signal] CheckSignal -->|No| Idle[idle] ``` ### PR Pipeline States ```mermaid flowchart LR PR[Open PR] --> CI{CI Status} CI -->|failing| CIFailed[ci_failed] CI -->|pending| CIPending[ci_pending] CI -->|passing| Review{Reviews} Review -->|changes requested| Changes[changes_requested] Review -->|approved| Mergeable{Mergeable?} Mergeable -->|conflict| Conflict[merge_conflict] Mergeable -->|yes| Merged[Mergeable] PR -.->|draft| Draft[Draft State] ``` --- ## Lifecycle Management ### Lifecycle Manager Responsibilities The `lifecycle.Manager` is the **canonical write path** for all session lifecycle facts: ```mermaid flowchart TD subgraph Inputs["Observation Inputs"] RuntimeObs[Runtime Observations] ActivitySignals[Agent Activity Signals] SCMObs[SCM Observations] end subgraph LCM["Lifecycle Manager"] Reducer[Fact Reducer] StateMachine[Activity State Machine] Termination[Termination Logic] Nudge[Agent Nudge Engine] end subgraph Outputs["Durable Facts"] ActivityState[activity_state] IsTerminated[is_terminated] PRFacts[PR Facts Table] end RuntimeObs --> Reducer ActivitySignals --> Reducer SCMObs --> Reducer Reducer --> StateMachine StateMachine --> Termination Termination --> ActivityState Termination --> IsTerminated SCMObs --> Nudge Nudge -->|route| Agent[Agent Adapter] ``` ### Session State Machine ```mermaid stateDiagram-v2 [*] --> Spawning: Spawn() Spawning --> Active: MarkSpawned Active --> Idle: activity_state = idle Active --> Working: activity_state = active Active --> Waiting: activity_state = waiting_input Active --> Exited: activity_state = exited Working --> Active: work completes Waiting --> Active: user responds Idle --> Active: agent starts work Exited --> Terminated: process exit Active --> Terminated: Kill() Waiting --> Terminated: Kill() Idle --> Terminated: Kill() Terminated --> [*] note right of Active Agent is working Runtime alive end note note right of Waiting Agent needs input Waiting for user end note note right of Terminated Session over Runtime cleaned up end note ``` ### Termination Guardrails The lifecycle manager only terminates when **all** conditions are met: ```mermaid flowchart TD Check{Can terminate?} Check -->|No| Keep[Keep running] Check -->|Yes| AllDead{Runtime AND
process dead?} AllDead -->|No| Keep AllDead -->|Yes| NoRecent{No recent
activity?} NoRecent -->|No| Keep NoRecent -->|Yes| NoPR{No merged PR
ownership?} NoPR -->|No| Keep NoPR -->|Yes| Terminate[Mark terminated] Terminate --> Cleanup[Trigger cleanup] Cleanup --> CDC[CDC event] CDC --> UI[Dashboard update] ``` **Key principle:** Failed probes are NOT proof of death. A session is only terminated when the runtime and process are **both** clearly dead and recent activity doesn't contradict that. --- ## Observation Loops ### SCM Observer ```mermaid flowchart TD Start([Observer Start]) --> Immediate[Immediate Poll] Immediate --> Loop{Tick every 30s} Loop --> ListRepos[List active repos] ListRepos --> CheckCreds{Credentials
available?} CheckCreds -->|No| Disabled[Disabled mode] CheckCreds -->|Yes| Fetch[Fetch PRs via ETags] Fetch --> ListPRs[List open PRs] ListPRs --> Discover[Discover new PRs] Discover --> FetchDetailed[Fetch detailed PR data] FetchDetailed --> FetchChecks[Fetch CI checks] FetchChecks --> FetchReviews[Fetch review threads] FetchReviews --> Write[Write to SQLite] Write --> Notify[Notify Lifecycle] Notify --> Trigger[CDC event] Disabled --> Loop Trigger --> Loop ``` ### Runtime Reaper ```mermaid flowchart TD Start([Reaper Start]) --> Loop{Tick every 5s} Loop --> List[List non-terminated
sessions] List --> ForEach[For each session] ForEach --> GetHandle{Has runtime
handle?} GetHandle -->|No| Skip[Skip session] GetHandle -->|Yes| Probe[Probe runtime] Probe --> Result{Probe result} Result -->|Error| ReportFailed[Report ProbeFailed] Result -->|Alive| ReportAlive[Report ProbeAlive] Result -->|Dead| ReportDead[Report ProbeDead] ReportFailed --> Apply[ApplyRuntimeObservation] ReportAlive --> Apply ReportDead --> Apply Apply --> LCM[Lifecycle Manager] LCM --> Update[Update facts] Update --> CDC[CDC event] Skip --> NextSession{More sessions?} CDC --> NextSession NextSession -->|Yes| ForEach NextSession -->|No| Loop ``` ### Observation Integration ```mermaid flowchart LR subgraph External["External State"] GitHub[GitHub API] Runtimes[tmux/conpty] end subgraph Observers["Observation Layer"] SCM[SCM Observer] Reaper[Runtime Reaper] end subgraph Core["Core Processing"] LCM[Lifecycle Manager] PRMgr[PR Manager] end subgraph Storage["Persistence"] SQLite[(SQLite)] end GitHub --> SCM Runtimes --> Reaper SCM --> PRMgr PRMgr --> SQLite PRMgr --> LCM Reaper --> LCM LCM --> SQLite ``` --- ## HTTP Layer ### API Structure ```mermaid flowchart TD subgraph HTTPD["HTTP Daemon"] Router[Router + Middleware] Router --> API[REST API] Router --> Events[SSE Events] Router --> Terminal[Terminal WebSocket] end subgraph Controllers["Controllers"] Sessions[Sessions Controller] Projects[Projects Controller] PRs[PRs Controller] Reviews[Reviews Controller] end subgraph Services["Services"] SessionSvc[Session Service] ProjectSvc[Project Service] PRSvc[PR Service] ReviewSvc[Review Service] end API --> Sessions API --> Projects API --> PRs API --> Reviews Sessions --> SessionSvc Projects --> ProjectSvc PRs --> PRSvc Reviews --> ReviewSvc Events -->|subscribe| CDC[CDC Broadcaster] Terminal --> TerminalMux[Terminal Manager] ``` ### Request Flow ```mermaid sequenceDiagram participant Client participant Router participant Controller participant Service participant Manager participant Store participant DB Client->>Router: POST /api/v1/sessions Router->>Router: Middleware (auth, logging) Router->>Controller: handler(w, r) Controller->>Controller: decode JSON Controller->>Service: Spawn(config) Service->>Manager: Spawn(config) Manager->>Store: Create session Store->>DB: INSERT INTO sessions DB->>Store: session record Store->>Manager: session record Manager->>Manager: Create workspace Manager->>Manager: Launch runtime Manager->>Service: Session response Service->>Controller: enriched session Controller->>Controller: encode JSON Controller->>Client: 201 Created + Session ``` --- ## Terminal Multiplexing ### Terminal Architecture ```mermaid flowchart TD subgraph Frontend Browser[Browser Terminal] end subgraph HTTPD WS[WebSocket Handler] end subgraph Terminal Mux[Terminal Mux] Sessions[Session States] end subgraph Runtime TMux[tmux Runtime] ConPTY[conpty Runtime] end Browser -->|WebSocket| WS WS -->|attach| Mux Mux --> Sessions Sessions -->|create| TMux Sessions -->|create| ConPTY TMux -->|PTY attach| Mux ConPTY -->|loopback dial| Mux Mux -->|frame| WS WS -->|binary| Browser ``` ### Attach Flow ```mermaid sequenceDiagram participant Client as Browser participant WS as WebSocket Handler participant Mux as Terminal Mux participant Runtime as tmux/conpty Client->>WS: WebSocket upgrade WS->>Mux: Attach(session, rows, cols) Mux->>Runtime: Attach(handle, rows, cols) Runtime->>Runtime: Create PTY Runtime->>Runtime: Spawn tmux attach loop Data Loop Runtime->>Mux: PTY output Mux->>WS: Binary frame WS->>Client: WebSocket message Client->>WS: User input WS->>Mux: Input frame Mux->>Runtime: Write to PTY end Client->>WS: Close WS->>Mux: Detach Mux->>Runtime: Close PTY ``` --- ## Load-Bearing Rules These rules are **load-bearing** — changing them breaks fundamental architectural assumptions: 1. **Never store display status** — Status is derived from durable facts at read time 2. **Never treat failed probes as death** — A failed probe is a fact, not a termination signal 3. **Never force-delete dirty worktrees** — User data safety over cleanup convenience 4. **All app state under ~/.ao** — No OS-default app-data locations 5. **Daemon binds to 127.0.0.1 only** — No network exposure, ever 6. **CLI is thin** — All logic lives in the daemon, CLI is just an HTTP client 7. **CDC is source-truth for events** — DB triggers write to change_log, poller fans out 8. **Adapters are leaves** — Adapters never import core packages, only ports and domain 9. **Hooks are gitignored** — Every file an adapter writes must be in .gitignore 10. **Migrations never change** — Add new migrations, never modify existing ones --- ## Summary Agent Orchestrator's architecture is designed around: - **Separation of concerns** — Observation, persistence, and display are distinct layers - **Port-based design** — Core code depends on interfaces, not implementations - **Durable minimalism** — Store only facts, compute everything else - **Event-driven updates** — CDC broadcasts changes to all subscribers - **Isolation** — Each session in its own worktree with its own runtime - **Safety** — Conservative termination, path validation, gitignored hooks This architecture enables parallel AI agents to work safely while maintaining complete visibility and control.