agent-orchestrator/docs/backend-code-structure.md

561 lines
18 KiB
Markdown

# Backend Code Structure
This document describes package ownership for the Go backend. It is about where code belongs. See [architecture.md](architecture.md) for lifecycle behavior, status derivation, persistence, CDC, and invariants.
## Goal
The backend is a local daemon that supervises coding-agent sessions. The code needs clear homes for product workflows, protocol surfaces, persistence, and replaceable external systems without turning any single package into a catch-all.
The current structure is a layered hybrid:
```
domain → shared product vocabulary and durable fact records
service/* → controller-facing product use cases and read models
session_manager → internal session mutations and resource orchestration
lifecycle → durable session fact reducer
ports → narrow capability interfaces consumed by core code
adapters/* → implementations of those capabilities
storage/sqlite → persistence and change delivery
httpd → HTTP protocol concerns
cli → CLI protocol concerns
daemon → production composition root
```
## Package Architecture Overview
```mermaid
graph TB
subgraph EntryPoints["Entry Points"]
CLI["cmd/ao<br/>CLI entrypoint"]
Main["main.go<br/>Daemon entrypoint"]
end
subgraph CoreLayer["Core Layer"]
Domain["domain<br/>Shared vocabulary"]
Ports["ports<br/>Capability interfaces"]
Services["service/*<br/>Product use cases"]
SessionMgr["session_manager<br/>Internal commands"]
Lifecycle["lifecycle<br/>Fact reducer"]
end
subgraph AdapterLayer["Adapter Layer"]
AgentAdapters["adapters/agent/*<br/>23+ agents"]
RuntimeAdapters["adapters/runtime/*<br/>tmux + conpty"]
WorkspaceAdapters["adapters/workspace/*<br/>git worktree"]
SCMAdapters["adapters/scm/*<br/>GitHub"]
TrackerAdapters["adapters/tracker/*<br/>GitHub"]
end
subgraph Infrastructure["Infrastructure"]
Storage["storage/sqlite<br/>Persistence"]
CDC["cdc<br/>Change delivery"]
Terminal["terminal<br/>PTY protocol"]
HTTPD["httpd<br/>HTTP API"]
CLI2["cli<br/>CLI commands"]
Daemon["daemon<br/>Composition root"]
Config["config<br/>Configuration"]
end
CLI --> CLI2
Main --> Daemon
Daemon --> Services
Daemon --> AdapterLayer
Daemon --> Infrastructure
Services --> Domain
Services --> Ports
Services --> SessionMgr
SessionMgr --> Ports
SessionMgr --> Lifecycle
AdapterLayer --> Ports
AdapterLayer --> Domain
HTTPD --> Services
CLI2 --> HTTPD
Services --> Storage
Lifecycle --> Storage
Storage --> CDC
Terminal --> RuntimeAdapters
HTTPD --> Terminal
```
## Package Relationships
```mermaid
graph LR
Controllers["httpd/controllers"] -->|calls| Services["service/*"]
Controllers2["cli commands"] -->|calls| HTTP["httpd"]
Services -->|orchestrates| SessionMgr["session_manager"]
Services -->|queries| Storage["storage/sqlite"]
SessionMgr -->|uses| Runtime["ports.Runtime"]
SessionMgr -->|uses| Workspace["ports.Workspace"]
SessionMgr -->|uses| Agent["ports.Agent"]
SessionMgr -->|reports to| Lifecycle["lifecycle"]
Runtime -->|implemented by| Tmux["adapters/runtime/tmux"]
Runtime -->|implemented by| Conpty["adapters/runtime/conpty"]
Lifecycle -->|persists to| Storage
Storage -->|triggers| CDC["cdc"]
CDC -->|broadcasts to| Frontend["Frontend subscribers"]
```
## Package Roles
### `internal/domain`
`domain` is AO's shared product language. Keep it stable and free of infrastructure imports.
**Belongs here:**
- Shared IDs: `ProjectID`, `SessionID`, `IssueID`
- Shared enums and status vocabulary
- Durable fact records that multiple packages must agree on
- PR, tracker, project, and session vocabulary (not transport-specific)
**Does not belong here:**
- HTTP request/response DTOs
- CLI output shapes
- OpenAPI wrapper/envelope types
- sqlc generated rows
- GitHub, tmux, Claude, Codex, or OpenCode payloads
- One-resource controller helper types
**Rule of thumb:** If AO would still use the concept after replacing HTTP, the CLI, SQLite, GitHub, the tmux/conpty runtime, and every agent adapter, and more than one package needs the exact vocabulary, it may belong in `domain`.
### `internal/service/*`
`service` packages are the controller-facing application boundary.
**Belongs here:**
- Resource use cases called by HTTP controllers and CLI-backed API flows
- Resource read models and command/result types
- Display-model assembly (session status derived from session and PR facts)
- Resource-specific validation and user-facing errors
- Small store interfaces consumed by the service
**Does not belong here:**
- Low-level runtime/workspace/agent process control
- Raw sqlc generated rows as public service results
- HTTP routing, path parsing, status-code decisions, or OpenAPI generation
- Concrete external adapter details
### `internal/session_manager`
`session_manager` owns internal session commands: spawn, restore, kill, cleanup, and send-related orchestration over runtime, workspace, agent, storage, messenger, and lifecycle dependencies.
**Belongs here:**
- Multi-step session mutations
- Rollback/cleanup sequencing when spawn partially succeeds
- Resource teardown safety
- Internal errors (not found, terminated, not restorable)
**Does not belong here:**
- HTTP request decoding
- CLI formatting
- Controller-facing list/get read-model assembly
- Terminal WebSocket framing
The split is intentional: `service/session` is the product/API boundary; `session_manager` is the internal command engine.
### `internal/lifecycle`
`lifecycle` is the canonical write path for durable session lifecycle facts. It reduces runtime observations, activity signals, spawn completion, termination, and PR observations into small persisted facts.
**Belongs here:**
- Updates to lifecycle-owned session facts
- Guardrails around runtime/activity observations
- Lifecycle-triggered agent nudges for actionable PR facts
**Does not belong here:**
- Display status persistence
- HTTP/CLI DTOs
- Direct adapter implementation details
- PR row persistence
### `internal/ports`
`ports` contains narrow capability interfaces and shared adapter-facing structs. It connects core code to replaceable systems.
**Capability interfaces:**
- `Runtime` — Create/Destroy/IsAlive for tmux/conpty sessions
- `Workspace` — Git worktree creation/destruction
- `Agent` — Agent launch, restore, hooks, session info
- `Attacher` — Terminal streaming attachment
- `PRWriter` — PR fact persistence
- `AgentResolver` — Agent binary resolution
- `AgentMessenger` — Message delivery to agents
**Belongs here:**
- Interfaces consumed by core packages and implemented by adapters
- Capability structs: `RuntimeConfig`, `WorkspaceConfig`, `SpawnConfig`
- Vocabulary needed at the boundary between core orchestration and adapters
**Does not belong here:**
- Resource read models (project/session API responses)
- HTTP request/response DTOs
- sqlc rows
- Concrete adapter options
- One-off interfaces that only a single package needs internally
### `internal/adapters/*`
Adapters are concrete implementations of external systems. They should be leaves in the import graph.
**Runtime Adapters:**
```mermaid
graph LR
RunSelect["runtimeselect"] -->|Darwin/Linux| Tmux["tmux<br/>Unix PTY integration"]
RunSelect -->|Windows| Conpty["conpty<br/>ConPTY + B1 protocol"]
Tmux -->|implements| RuntimePort["ports.Runtime"]
Conpty -->|implements| RuntimePort
Tmux -->|uses| PtyExec["ptyexec<br/>Unix PTY spawning"]
Conpty -->|uses| PtyHost["Pty-host server<br/>Ring buffer + Registry"]
```
**Current adapters:**
```txt
internal/adapters/agent/claudecode
internal/adapters/agent/codex
internal/adapters/agent/opencode
internal/adapters/runtime/tmux # Darwin/Linux
internal/adapters/runtime/conpty # Windows
internal/adapters/runtime/runtimeselect # Platform selector
internal/adapters/runtime/ptyexec # PTY spawning
internal/adapters/workspace/gitworktree
internal/adapters/scm/github
internal/adapters/tracker/github
internal/adapters/reviewer/claudecode
```
**Good dependencies:**
```
session_manager → ports.Runtime
adapters/runtime/tmux → ports + domain
adapters/workspace/gitworktree → ports + domain
daemon → adapters + services + storage
```
**Avoid:**
```
domain → adapters
service/session → adapters/runtime/tmux
httpd/controllers → storage/sqlite/store
adapters/* → httpd
```
### `internal/storage/sqlite`
`storage/sqlite` owns SQLite setup, migrations, sqlc generated code, and store implementations.
**Belongs here:**
- Connection setup and PRAGMAs
- Goose migrations
- sqlc queries and generated code
- Table-specific store methods
- Transactions and CDC-triggered persistence behavior
**Does not belong here:**
- HTTP response types
- CLI output formatting
- Product display status rules
- External adapter logic
Generated sqlc types should stay behind store methods. Services and lifecycle code should work with domain records or service read models, not generated rows.
### `internal/cdc`
`cdc` owns `change_log` polling and event broadcasting. SQLite triggers append durable events to `change_log`; the poller tails that table and fans events out to subscribers.
**Belongs here:**
- Event type definitions for the CDC stream
- Poller and broadcaster logic
- Subscriber fan-out behavior
**Does not belong here:**
- Terminal byte streams
- Product workflow decisions
- Database schema ownership
### `internal/terminal`
`terminal` owns the terminal session protocol and PTY attach management used by the HTTP mux. The runtime is selected by `runtimeselect`: tmux on Darwin/Linux, conpty on Windows.
```mermaid
sequenceDiagram
participant WS as WebSocket Client
participant Mux as Terminal Mux
participant Term as terminal package
participant Runtime as Runtime Adapter
WS->>Mux: WebSocket upgrade
Mux->>Term: Create attach session
Term->>Runtime: Runtime.Attach(handle, rows, cols)
alt tmux (Darwin/Linux)
Runtime->>Runtime: tmux attach-session -t <id>
Runtime->>Runtime: PTY stream via ptyexec
else conpty (Windows)
Runtime->>Runtime: Dial loopback TCP
Runtime->>Runtime: B1 protocol handshake
Runtime->>Runtime: MsgTerminalData (scrollback)
end
Runtime-->>Term: Stream
Term-->>Mux: Framed protocol
Mux-->>WS: WebSocket messages
```
**Belongs here:**
- Per-client attachment lifecycle (liveness gating, re-attach backoff)
- Input/output framing independent of HTTP
- PTY-backed attach handling and terminal protocol tests
### `internal/httpd`
`httpd` is the HTTP protocol adapter.
**Belongs here:**
- Routing and middleware
- HTTP request decoding and response encoding
- Path/query parameter handling
- Status-code mapping
- API error envelopes
- OpenAPI generation and serving
- WebSocket upgrade handling for terminal mux
Controllers call service managers and translate service results/errors into HTTP responses. Controllers should not reach directly into concrete adapters or the SQLite store.
### `internal/cli`
`cli` owns the user-facing `ao` command. It should stay thin:
- Discover the local daemon
- Call the daemon's loopback HTTP API
- Format command output
- Start/stop/status/doctor process control
The CLI should not duplicate daemon business logic. If a command needs product behavior, put the behavior in the daemon service/API path and have the CLI call that path.
### `internal/daemon`
`daemon` is the production composition root. It wires config, logging, SQLite, CDC, lifecycle, reaper, runtime, terminal manager, services, HTTP, and shutdown.
```mermaid
graph TD
Daemon["daemon package"] --> Config["config"]
Daemon --> Logging["slog logger"]
Daemon --> Storage["storage/sqlite"]
Daemon --> CDC["cdc"]
Daemon --> Lifecycle["lifecycle"]
Daemon --> Reaper["observe/reaper"]
Daemon --> Runtime["runtimeselect"]
Daemon --> Terminal["terminal"]
Daemon --> Services["service/*"]
Daemon --> HTTPD["httpd"]
Config -->|"env vars"| Daemon
Storage -->|"triggers"| CDC
CDC -->|"events"| HTTPD
```
**Belongs here:**
- Production dependency construction
- Adapter registration
- Startup/shutdown sequencing
- Cross-component wiring
**Does not belong here:**
- Business logic that should be testable in service, lifecycle, or manager packages
- Adapter implementation details
## Current Tree
```mermaid
graph TB
Root["backend/"]
Root --> Cmd["cmd/ao/<br/># CLI entrypoint"]
Root --> Main["main.go<br/># Daemon entrypoint"]
Root --> Sqlc["sqlc.yaml"]
Root --> Domain["internal/domain/<br/># Shared vocabulary"]
Root --> Ports["internal/ports/<br/># Capability interfaces"]
Root --> Service["internal/service/"]
Service --> Proj["project/<br/># Project API"]
Service --> Sess["session/<br/># Session API"]
Service --> PR["pr/<br/># PR service"]
Service --> Review["review/<br/># Code review"]
Root --> SessMgr["internal/session_manager/<br/># Internal commands"]
Root --> Life["internal/lifecycle/<br/># Fact reducer"]
Root --> Observe["internal/observe/"]
Observe --> SCM["scm/<br/># GitHub observer"]
Observe --> Reap["reaper/<br/># Liveness observer"]
Root --> Store["internal/storage/sqlite/<br/># DB + stores"]
Root --> CDC2["internal/cdc/<br/># Change delivery"]
Root --> Term["internal/terminal/<br/># PTY protocol"]
Root --> HTTPD2["internal/httpd/<br/># HTTP API"]
Root --> CLI2["internal/cli/<br/># CLI commands"]
Root --> Daemon2["internal/daemon/<br/># Composition"]
Root --> Cfg["internal/config/<br/># Config"]
Root --> Adapters["internal/adapters/"]
Adapters --> Agent["agent/<br/># 23+ agents"]
Adapters --> Runtime2["runtime/"]
Runtime2 --> Tmux2["tmux<br/># Darwin/Linux"]
Runtime2 --> Conpty2["conpty<br/># Windows"]
Runtime2 --> RS["runtimeselect<br/># Selector"]
Runtime2 --> PE["ptyexec<br/># PTY spawning"]
Adapters --> WS["workspace/gitworktree"]
Adapters --> SCM2["scm/github"]
Adapters --> Track["tracker/github"]
Adapters --> Rev["reviewer/claudecode"]
```
## Interface Placement
Prefer interfaces near their consumers, except for shared capabilities.
- **Single consumer:** Define the smallest interface in that package
- **Multiple core consumers:** Define it in `ports`
- **Resource service:** Use the owning `service/*` manager interface
- **Return types:** Return concrete types from constructors unless callers genuinely need an interface
## Adding New Code
Use these defaults:
```mermaid
graph LR
NewCode["New Code"] --> Choice{"What type?"}
Choice -->|HTTP route| HTTPRoute["Add to httpd/<br/>Call service/*<br/>Update OpenAPI"]
Choice -->|Product resource| Product["domain + service/<br/>storage + ports"]
Choice -->|Adapter| AdapterPath["adapters/<capability>/<impl><br/>Implement ports<br/>Wire in daemon"]
Choice -->|Persisted fact| Fact["migration + sqlc<br/>store + domain<br/>CDC trigger"]
Choice -->|CLI command| CLIPath["cli parsing/formatting<br/>Call daemon API"]
```
## Project Routes Example
Project-owned concepts live in `internal/service/project`:
- Project read models
- Project add/remove command types
- Project validation and user-facing errors
- The `Manager` contract consumed by HTTP controllers
`internal/httpd/controllers` remains responsible for:
- Route registration
- JSON decoding/encoding
- HTTP status codes and error envelopes
- Mapping service errors to responses
## Dependency Rules
```mermaid
graph LR
subgraph Allowed["Allowed Dependencies"]
Controllers["httpd/controllers"] -->|✓| Services["service/*"]
Services -->|✓| SessionMgr["session_manager"]
Services -->|✓| Storage["storage/sqlite"]
SessionMgr -->|✓| Ports["ports"]
Adapters["adapters/*"] -->|✓| Ports
Adapters -->|✓| Domain["domain"]
end
subgraph Avoid["Avoid Dependencies"]
Services2["service/*"] -.->|✗| Adapters2["adapters/*"]
Domain2["domain"] -.->|✗| Adapters2
Httpd["httpd"] -.->|✗| Storage2["storage/sqlite"]
Adapters2 -.->|✗| Httpd
end
```
**Key rules:**
- Controllers call services, not storage directly
- Services call session manager, not adapters directly
- Adapters implement ports, don't depend on HTTP/storage
- Domain stays pure, no infrastructure dependencies
- Terminal and httpd are separate (terminal should not import httpd)
## Runtime Architecture Details
### Platform Selection
The `runtimeselect` package automatically chooses the appropriate runtime based on the platform:
```go
func New(_ *slog.Logger) Runtime {
if runtime.GOOS != "windows" {
return tmux.New(tmux.Options{}) // Darwin/Linux
}
return conpty.New(conpty.Options{}) // Windows
}
```
No configuration needed — the system handles this automatically.
### tmux Implementation (Darwin/Linux)
**File:** `internal/adapters/runtime/tmux/tmux.go`
- Creates detached tmux sessions
- Uses `tmux send-keys` for input delivery
- Uses `tmux capture-pane` for scrollback
- Spawns `tmux attach-session` for terminal streaming
- Sessions survive daemon restart (tmux persistence)
### conpty Implementation (Windows)
**Files:** `internal/adapters/runtime/conpty/*.go`
- Spawns detached pty-host process
- Uses custom B1 binary protocol over loopback TCP
- Implements ring buffer for scrollback
- File-based registry for crash recovery
- Direct TCP connection for terminal streaming (no CLI attach)
## Related Documentation
- [Architecture](architecture.md) — System architecture and data flows
- [AGENTS.md](../AGENTS.md) — Contributor and worker-agent contract
- [CLI Reference](cli/README.md) — Complete CLI command documentation