Backend Code Structure
June 26, 2026 · View on GitHub
This document describes package ownership for the Go backend. It defines where code belongs, how packages interact, and the architectural boundaries that keep the system maintainable.
Table of Contents
- Overview
- Architecture Layers
- Package-by-Package Ownership
- Interface Placement Rules
- Import Graph
- Adding New Code
- Examples
Overview
The backend is a layered hybrid architecture with clear separation between core business logic and external concerns:
graph TB
subgraph CLI["CLI Layer"]
CLI[internal/cli]
end
subgraph HTTP["HTTP Layer"]
HTTPD[internal/httpd]
end
subgraph Services["Service Layer"]
Project[internal/service/project]
Session[internal/service/session]
PR[internal/service/pr]
Review[internal/service/review]
end
subgraph Core["Core Layer"]
SessionMgr[internal/session_manager]
Lifecycle[internal/lifecycle]
Observe[internal/observe/*]
end
subgraph Data["Data Layer"]
Domain[internal/domain]
Ports[internal/ports]
Storage[internal/storage/sqlite]
CDC[internal/cdc]
end
subgraph Infra["Infrastructure Layer"]
Terminal[internal/terminal]
Adapters[internal/adapters/*]
Daemon[internal/daemon]
Config[internal/config]
end
CLI -->|calls| HTTPD
HTTPD -->|calls| Services
Services -->|calls| Core
Services -->|uses| Data
Core -->|uses| Data
Core -->|uses| Infra
HTTPD -->|uses| Data
Key Architectural Principles
- Domain stays pure — No infrastructure dependencies
- Ports define contracts — Interfaces consumed by core, implemented by adapters
- Services orchestrate — Controller-facing use cases over core and data
- Adapters are leaves — Implement ports, don't import core
- CLI/HTTP stay thin — Just protocol handling, all logic in daemon
Architecture Layers
Layer Interactions
flowchart LR
Protocol[Protocol Layer<br/>CLI + HTTP] -->|uses| Services[Service Layer<br/>Use Cases]
Services -->|commands| Core[Core Layer<br/>Session Manager + Lifecycle]
Services -->|queries| Data[Data Layer<br/>Domain + Ports + Storage]
Core -->|reads/writes| Data
Core -->|invokes| Infra[Infrastructure<br/>Adapters + Terminal]
Infra -->|implements| Ports
Dependency Rules
graph TD
Direction[Dependency Direction]
Direction --> Down["Top-down only"]
Direction --> No["No upward dependencies"]
CLI[CLI] -->|OK| HTTP[HTTP]
HTTP -->|OK| Services[Services]
Services -->|OK| Core[Core]
Services -->|OK| Storage[Storage]
Core -->|OK| Adapters[Adapters]
Bad1[Adapters] -.->|FORBIDDEN| Services
Bad2[Storage] -.->|FORBIDDEN| HTTP
Bad3[Core] -.->|FORBIDDEN| CLI
Package-by-Package Ownership
internal/domain
Purpose: Shared product vocabulary and durable fact records. The single source of truth for domain concepts.
graph TD
Domain[internal/domain] --> Contains[Contains]
Contains --> IDs[Shared IDs<br/>ProjectID, SessionID, IssueID]
Contains --> Enums[Status Enums<br/>SessionStatus, ActivityState]
Contains --> Records[Durable Records<br/>SessionRecord, PRRecord]
Contains --> Vocab[Product Vocabulary<br/>PR, Project, Review concepts]
DoesNot[Does NOT Contain] --> HTTP[HTTP DTOs]
DoesNot --> CLI[CLI Output]
DoesNot --> Generated[sqlc Generated Rows]
DoesNot --> External[External Payloads<br/>GitHub, Claude, etc.]
Belongs here:
- Shared IDs:
ProjectID,SessionID,IssueID - Enums and status vocabulary
- Durable fact records used across packages
- PR, tracker, project, session vocabulary
Does NOT belong here:
- HTTP request/response DTOs
- CLI output shapes
- OpenAPI wrapper types
- sqlc generated rows
- External system payloads (GitHub, tmux, agent-specific)
Rule of thumb: If AO would still use the concept after replacing HTTP, CLI, SQLite, GitHub, tmux, and every agent adapter, it belongs in domain.
internal/ports
Purpose: Narrow capability interfaces that connect core code to replaceable external systems.
graph LR
Core[Core Code] -->|consumes| Ports[Ports Interfaces]
Adapters[Adapters] -->|implement| Ports
Ports --> Examples[Examples]
Examples --> Runtime[Runtime<br/>Create, Destroy, IsAlive, Attach]
Examples --> Workspace[Workspace<br/>Create, Destroy, ValidatePath]
Examples --> Agent[Agent<br/>GetLaunchCommand, GetAgentHooks]
Examples --> SCM[SCM<br/>ListPRs, FetchPR, FetchChecks]
Examples --> PR[PR<br/>WriteSCMObservation]
Belongs here:
- Interfaces consumed by core packages, implemented by adapters
- Capability structs:
RuntimeConfig,WorkspaceConfig,SpawnConfig - Vocabulary at the boundary between core and adapters
Does NOT belong here:
- Resource read models (belongs in
service/*) - HTTP request/response DTOs (belongs in
httpd) - sqlc rows (belongs in
storage/sqlite) - One-off internal interfaces
Key Port Interfaces:
| Port | Purpose | Implementations |
|---|---|---|
Runtime | Process isolation | tmux, conpty |
Workspace | Git worktree management | gitworktree |
Agent | Agent launching | 23+ agent adapters |
SCM | PR/CI observation | github |
Tracker | Issue tracking | github (adapter only) |
AgentMessenger | Agent communication | Agent hooks |
PRWriter | PR persistence | pr.Manager |
internal/service/*
Purpose: Controller-facing application boundary. Owns product use cases and read-model assembly.
graph TD
subgraph Services
Project[project]
Session[session]
PR[pr]
Review[review]
end
subgraph Responsibilities
UseCases[Use Cases]
ReadModels[Read Models]
Validation[Validation]
Errors[User-Facing Errors]
end
Project --> Responsibilities
Session --> Responsibilities
PR --> Responsibilities
Review --> Responsibilities
subgraph NotHere
LowLevel[Low-level runtime control]
RawSQL[Raw sqlc rows]
HTTP[HTTP routing]
end
Current service packages:
graph LR
Controllers[HTTP Controllers] -->|call| Services
Services --> Project[internal/service/project<br/>Project CRUD]
Services --> Session[internal/service/session<br/>Session read-model assembly]
Services --> PR[internal/service/pr<br/>PR observation/actions]
Services --> Review[internal/service/review<br/>Code review]
Services -->|delegate to| SessionMgr[session_manager]
Services -->|query| Store[storage stores]
Belongs here:
- Resource use cases called by HTTP controllers and CLI
- Resource read models and command/result types
- Display-model assembly (e.g., session status derivation)
- 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 results
- HTTP routing, path parsing, status-code decisions
- Concrete external adapter details
Example: Project concepts live in internal/service/project, not in domain and not in internal/project.
internal/session_manager
Purpose: Internal session command engine. Owns multi-step session mutations and resource orchestration.
graph TD
Service[service/session] -->|commands| Mgr[session_manager]
Mgr -->|orchestrates| Resources[Resources]
Resources --> Workspace[Workspace Adapter]
Resources --> Runtime[Runtime Adapter]
Resources --> Agent[Agent Adapter]
Resources --> Storage[Storage Store]
Resources --> Lifecycle[Lifecycle Manager]
Resources --> Messenger[Agent Messenger]
Mgr -->|owns| Operations[Operations]
Operations --> Spawn[Spawn<br/>Create all resources]
Operations --> Kill[Kill<br/>Teardown all resources]
Operations --> Restore[Restore<br/>Relaunch terminated session]
Operations --> Send[Send<br/>Message to agent]
Belongs here:
- Multi-step session mutations with rollback
- Resource sequencing (workspace → runtime → agent)
- Resource teardown safety and cleanup
- 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
Intentional split: service/session is the product/API boundary; session_manager is the internal command engine.
internal/lifecycle
Purpose: Canonical write path for durable session lifecycle facts. Reduces observations into minimal persisted state.
graph LR
subgraph Inputs
RuntimeObs[Runtime Observations<br/>from Reaper]
ActivitySignals[Activity Signals<br/>from Agent Hooks]
SCMObs[SCM Observations<br/>from SCM Observer]
end
subgraph Lifecycle
LCM[Lifecycle Manager]
Reducer[Fact Reducer]
StateMachine[State Machine]
Nudge[Agent Nudge Engine]
end
subgraph Outputs
ActivityState[activity_state]
IsTerminated[is_terminated]
PRFacts[PR Facts]
Nudges[Agent Nudges]
end
RuntimeObs --> LCM
ActivitySignals --> LCM
SCMObs --> LCM
LCM --> Reducer
Reducer --> StateMachine
StateMachine --> ActivityState
StateMachine --> IsTerminated
SCMObs --> Nudge
Nudge --> Nudges
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 (use service layer instead)
- HTTP/CLI DTOs
- Direct adapter implementation details
- PR row persistence (use
pr.Manager)
Key invariant: The UI status is derived at read time by service code. Do not store display status in lifecycle or SQLite.
internal/observe/*
Purpose: Observation loops that poll external state and report facts to lifecycle.
graph TD
subgraph Observe
SCM[observe/scm<br/>SCM Observer]
Reaper[observe/reaper<br/>Runtime Reaper]
end
subgraph External
GitHub[GitHub API]
Runtimes[tmux/conpty]
end
subgraph Internal
LCM[Lifecycle Manager]
Store[SQLite Store]
end
SCM -->|polls| GitHub
SCM -->|writes| Store
SCM -->|notifies| LCM
Reaper -->|probes| Runtimes
Reaper -->|reports to| LCM
Current observation packages:
internal/observe/scm— SCM (GitHub) observer loopinternal/observe/reaper— Runtime liveness observation loop
Belongs here:
- Polling loops and observation logic
- External state transformation into domain facts
- Observation error handling and retry logic
Does NOT belong here:
- Product workflow decisions (belongs in service layer)
- Direct storage writes (use lifecycle instead)
internal/storage/sqlite
Purpose: SQLite setup, migrations, queries, and store implementations.
graph TD
Storage[storage/sqlite] --> Components[Components]
Components --> Setup[Connection Setup<br/>PRAGMAs]
Components --> Migrations[Goose Migrations]
Components --> SQLC[sqlc Queries + Generated Code]
Components --> Stores[Table-specific Stores]
Components --> Trans[Transactions + CDC]
Components -->|NOT| HTTP[HTTP Response Types]
Components -->|NOT| CLI[CLI Formatting]
Components -->|NOT| Product[Product Display Status Rules]
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
Rule: Generated sqlc types should stay behind store methods. Services should work with domain records or service read models, not generated rows.
internal/cdc
Purpose: Change-log polling and event broadcasting.
graph LR
SQLite[(SQLite)] -->|triggers| ChangeLog[change_log]
ChangeLog -->|tail| Poller[CDC Poller]
Poller -->|Event| Broadcaster[Broadcaster]
Broadcaster -->|fan-out| Subs[Subscribers]
Subs --> SSE[SSE Writer]
Subs --> Term[Terminal Fanout]
Subs --> Cache[Cache Invalidation]
Belongs here:
- Event type definitions for the CDC stream
- Poller and broadcaster logic
- Subscriber fan-out behavior
Does NOT belong here:
- Terminal byte streams (belongs in
internal/terminal) - Product workflow decisions (belongs in service layer)
- Database schema ownership (belongs in
storage/sqlite)
internal/terminal
Purpose: Terminal session protocol and PTY attach management used by the HTTP terminal mux.
graph TD
HTTP[httpd] -->|WebSocket| Terminal[terminal]
Terminal -->|creates| Attach[Attach Streams]
Attach -->|wraps| PTY[PTY Sessions]
PTY --> Unix[Unix: tmux attach<br/>via ptyexec]
PTY --> Windows[Windows: conpty<br/>loopback dial]
Terminal -->|manages| State[Session States]
State --> Liveness[Liveness gating]
State --> Backoff[Re-attach backoff]
Belongs here:
- Per-client attachment lifecycle
- Input/output framing independent of HTTP
- PTY-backed attach handling and terminal protocol tests
Does NOT belong here:
- HTTP-specific concerns (belongs in
httpd) - HTTP routing or WebSocket upgrade logic
Note: httpd adapts WebSocket connections to terminal interfaces. terminal should not import httpd.
internal/httpd
Purpose: HTTP protocol adapter. Handles routing, middleware, and request/response encoding.
graph TD
HTTPD[httpd] --> Components[Components]
Components --> Routing[Routing + Middleware]
Components --> Decode[Request Decoding]
Components --> Encode[Response Encoding]
Components --> Errors[API Error Envelopes]
Components --> OpenAPI[OpenAPI Generation]
Components --> WS[WebSocket for Terminal]
Components -->|calls| Services[service/*]
Services -->|NOT| Adapters[Direct Adapter Access]
Services -->|NOT| Storage[Direct SQLite Access]
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
Does NOT belong here:
- Direct adapter or SQLite store access
- Application read models shared with CLI (belongs in
service/*)
Rule: Controllers call service managers and translate service results/errors into HTTP responses.
internal/cli
Purpose: User-facing ao command. Thin client over the daemon HTTP API.
graph LR
CLI[cli] --> Operations[Operations]
Operations --> Discover[Discover Daemon]
Operations --> Call[Call HTTP API]
Operations --> Format[Format Output]
Operations --> Control[Process Control<br/>start/stop/status/doctor]
Operations -->|NOT| Direct[Direct Storage/DB Access]
Operations -->|NOT| Runtime[Direct Runtime Control]
Operations -->|NOT| Adapters[Direct Adapter Calls]
Belongs here:
- Daemon discovery
- HTTP API calls
- Command output formatting
- Process control: start/stop/status/doctor
Does NOT belong here:
- Duplicate daemon business logic (put in daemon service/API)
- Direct storage, runtime, or adapter access
Rule: If a command needs product behavior, put it in the daemon and have the CLI call that API path.
internal/adapters/*
Purpose: Concrete implementations of ports interfaces. Wraps external systems.
graph TD
Ports[Ports Interfaces] -->|implemented by| Adapters[Adapters]
Adapters --> Agent[agent/*<br/>23+ harnesses]
Adapters --> Runtime[runtime/*<br/>tmux, conpty]
Adapters --> Workspace[workspace/*<br/>gitworktree]
Adapters --> SCM[scm/*<br/>github]
Adapters --> Tracker[tracker/*<br/>github]
Agent --> Codex[codex]
Agent --> Claude[claude-code]
Agent --> Cursor[cursor]
Agent --> Aider[aider]
Agent -->|... 20+| More[more agents]
Adapter principles:
- Adapters are leaves in the import graph
- Adapters translate external behavior into AO ports/domain concepts
- Adapters should not own product workflows
- All adapter-written files must be gitignored
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/daemon
Purpose: Production composition root. Wires all dependencies together.
graph TD
Daemon[daemon] --> Responsibilities[Responsibilities]
Responsibilities --> Wire[Dependency Construction]
Responsibilities --> Register[Adapter Registration]
Responsibilities --> Startup[Startup Sequencing]
Responsibilities --> Shutdown[Shutdown Sequencing]
Responsibilities --> Cross[Cross-component Wiring]
Responsibilities -->|NOT| Business[Business Logic<br/>(put in service/lifecycle)]
Responsibilities -->|NOT| Adapter[Adapter Implementation<br/>(put in adapters/*)]
Belongs here:
- Production dependency construction
- Adapter registration
- Startup/shutdown sequencing
- Cross-component wiring
Does NOT belong here:
- Business logic (belongs in service, lifecycle, or manager packages)
- Adapter implementation details (belongs in adapters/*)
internal/config
Purpose: Environment-based daemon configuration.
graph LR
Config[config] --> Sources[Sources]
Sources --> Env[Environment Variables]
Sources --> Defaults[Built-in Defaults]
Sources --> Validate[Validation]
Config -->|provides| Settings[Settings]
Settings --> Port[AO_PORT]
Settings --> Timeout[AO_REQUEST_TIMEOUT]
Settings --> DataDir[AO_DATA_DIR]
Settings --> RunFile[AO_RUN_FILE]
Settings --> Agent[AO_AGENT]
Key environment variables:
AO_PORT— HTTP bind port (default: 3001)AO_REQUEST_TIMEOUT— Per-request timeout (default: 60s)AO_SHUTDOWN_TIMEOUT— Graceful shutdown cap (default: 10s)AO_RUN_FILE— PID/port handshake (default: ~/.ao/running.json)AO_DATA_DIR— SQLite data directory (default: ~/.ao/data)AO_AGENT— Compatibility agent adapter (default: claude-code)GITHUB_TOKEN— GitHub authentication
Interface Placement Rules
graph TD
Question{Where to define<br/>interface?}
Question --> Single{Only one package<br/>consumes it?}
Single -->|Yes| InPackage[Define in consuming<br/>package]
Single -->|No| Multiple{Multiple core packages<br/>need it?}
Multiple -->|Yes| Ports[Define in ports]
Multiple -->|No| HTTP{HTTP controllers<br/>need it?}
HTTP -->|Yes| Service[Define in service/*]
HTTP -->|No| Concrete[Return concrete type<br/>from constructor]
Rules:
- Single consumer → Define in the consuming package (smallest interface)
- Multiple core consumers → Define in
ports(shared capability) - HTTP controllers need resource → Use
service/*manager interface - Return from constructor → Return concrete type unless genuinely needed
Examples:
// Good: Interface near single consumer
type sessionGetter interface {
GetSession(ctx context.Context, id SessionID) (SessionRecord, bool, error)
}
// Good: Shared capability in ports
type Runtime interface {
Create(ctx context.Context, cfg RuntimeConfig) (RuntimeHandle, error)
Destroy(ctx context.Context, handle RuntimeHandle) error
IsAlive(ctx context.Context, handle RuntimeHandle) (bool, error)
}
// Good: Service interface for controllers
type Manager interface {
List(ctx context.Context) ([]Project, error)
Add(ctx context.Context, cfg Config) (Project, error)
Remove(ctx context.Context, id string) error
}
Import Graph
graph TD
CLI[cli] --> HTTPD[httpd]
HTTPD --> Services[service/*]
HTTPD --> Terminal[terminal]
Services --> SessionMgr[session_manager]
Services --> Lifecycle[lifecycle]
Services --> Storage[storage/sqlite]
Services --> Domain[domain]
Services --> Ports[ports]
SessionMgr --> Ports
SessionMgr --> Adapters[adapters/*]
SessionMgr --> Lifecycle
Lifecycle --> Ports
Lifecycle --> Storage
Lifecycle --> Domain
Observe[observe/*] --> Ports
Observe --> Storage
Observe --> Lifecycle
Storage --> Domain
Storage --> Ports
Adapters --> Ports
Adapters --> Domain
CDC[cdc] --> Storage
Terminal --> Ports
Daemon[daemon] --> All[All packages]
Key patterns:
- All arrows point downward (no cycles)
- Adapters and domain are leaves
- CLI and HTTPD don't touch storage directly
- Everything depends on ports and domain
Adding New Code
New HTTP Route
flowchart LR
AddRoute[Add HTTP Route] --> Route[Register in httpd]
Route --> Call[Call service/*]
Call --> Update[Update OpenAPI]
Update --> Test[Add tests]
Steps:
- Add controller in
httpd/controllers/ - Call a
service/*package - Update OpenAPI generation
- Add spec tests
New Product Resource
flowchart TD
NewResource[New Resource] --> Domain[Add IDs/vocab to domain]
Domain --> Service[Create service/resource]
Service --> Storage[Add storage queries]
Storage --> Ports[Add ports if needed]
Ports --> Adapter[Implement adapter if needed]
Steps:
- Add shared IDs/vocabulary to
domain - Create use cases in
service/<resource> - Add storage in
storage/sqlite - Add ports if external system needed
- Implement adapter in
adapters/<capability>/<impl> - Wire in
daemon
New Adapter
flowchart LR
NewAdapter[New Adapter] --> Port[Implement port interface]
Port --> Hooks[Implement hooks if agent]
Hooks --> Gitignore[Add .gitignore entries]
Gitignore --> Wire[Wire in daemon]
Wire --> Test[Add conformance tests]
Steps:
- Implement a
portsinterface underadapters/<capability>/<impl> - For agents: implement hooks with gitignored files
- Wire in
daemon - Add conformance tests
Examples
Example: Adding a Session Command
// In internal/service/session/service.go
func (s *Service) MyNewCommand(ctx context.Context, id SessionID) (Result, error) {
// 1. Validate input
// 2. Call session_manager
// 3. Enrich result
// 4. Return read model
}
// In internal/httpd/controllers/sessions.go
func (c *SessionsController) myNewCommand(w http.ResponseWriter, r *http.Request) {
// 1. Decode request
// 2. Call service
// 3. Encode response
}
Example: Adding a Port Interface
// In internal/ports/myfeature.go
package ports
type MyFeature interface {
DoSomething(ctx context.Context, cfg Config) (Result, error)
}
// In internal/adapters/myfeature/impl.go
package impl
import "github.com/aoagents/agent-orchestrator/backend/internal/ports"
type Impl struct { ... }
func (i *Impl) DoSomething(ctx context.Context, cfg ports.Config) (ports.Result, error) {
// Implementation
}
Example: Service Layer Pattern
// In internal/service/myresource/service.go
package service
// Service is the controller-facing boundary
type Service struct {
manager *manager.Manager // Internal command engine
store Store // Storage interface
}
// New constructs the service
func New(mgr *manager.Manager, store Store) *Service {
return &Service{manager: mgr, store: store}
}
// List returns enriched read models
func (s *Service) List(ctx context.Context) ([]MyResource, error) {
records, err := s.store.List(ctx)
if err != nil {
return nil, err
}
return s.enrich(records), nil
}
// Create performs a use case
func (s *Service) Create(ctx context.Context, cfg Config) (MyResource, error) {
// Business logic
result, err := s.manager.Create(ctx, cfg)
if err != nil {
return MyResource{}, err
}
return s.enrichOne(result), nil
}
Summary
Key takeaways:
- Domain stays pure — shared vocabulary only
- Ports define contracts — interfaces for external systems
- Services orchestrate — controller-facing use cases
- Adapters are leaves — implement ports, no core imports
- CLI/HTTP stay thin — protocol handling only
- Daemon wires it all — composition root
Always ask:
- Does this belong in domain (shared concept)?
- Does this belong in ports (shared capability)?
- Does this belong in service (use case)?
- Does this belong in adapters (external system)?
Never:
- Put HTTP types in domain
- Put display status in storage
- Put business logic in CLI
- Import core from adapters
- Import adapters from services