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

383 lines
12 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` holds shared product vocabulary and durable fact records.
- `service/*` owns controller-facing product use cases and read models.
- `session_manager` owns internal session mutations and resource orchestration.
- `lifecycle` owns the durable session fact reducer.
- `ports` defines narrow capability interfaces consumed by core code.
- `adapters/*` implements those capabilities with real external systems.
- `storage/sqlite` and `cdc` own persistence and change delivery.
- `httpd` and `cli` own protocol concerns.
- `daemon` wires the production graph together.
## Package Roles
### `internal/domain`
`domain` is AO's shared product language. Keep it stable and free of
infrastructure imports.
Belongs here:
- shared IDs such as `ProjectID`, `SessionID`, and `IssueID`;
- shared enums and status vocabulary;
- durable fact records that multiple packages must agree on;
- PR, tracker, project, and session vocabulary that is not transport-specific.
Does not belong here:
- HTTP request/response DTOs;
- CLI output shapes;
- OpenAPI wrapper/envelope types;
- sqlc generated rows;
- GitHub, Zellij, 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, Zellij, 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.
Current examples:
```txt
internal/service/project
internal/service/session
internal/service/pr
```
Belongs here:
- resource use cases called by HTTP controllers and CLI-backed API flows;
- resource read models and command/result types;
- display-model assembly, such as 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.
For example, project API concepts live in `internal/service/project`, not in
`domain` and not in a top-level `internal/project` package.
### `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 such as not found, terminated, or 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.
The UI status is derived at read time by service code. Do not store display
status in lifecycle or SQLite.
### `internal/ports`
`ports` contains narrow capability interfaces and shared adapter-facing structs.
It connects core code to replaceable systems.
Current capability examples:
- `Runtime`
- `Workspace`
- `Agent`
- `AgentResolver`
- `AgentMessenger`
- `PRWriter`
Belongs here:
- interfaces consumed by core packages and implemented by adapters;
- capability structs such as `RuntimeConfig`, `WorkspaceConfig`, and
`SpawnConfig`;
- vocabulary needed at the boundary between core orchestration and adapters.
Does not belong here:
- resource read models like project/session API responses;
- HTTP request/response DTOs;
- sqlc rows;
- concrete adapter options;
- one-off interfaces that only a single package needs internally.
Keep `ports` capability-oriented. It should not become the dumping ground for
every manager, DTO, and resource contract.
### `internal/adapters/*`
Adapters are concrete implementations of external systems.
Current examples:
```txt
internal/adapters/agent/claudecode
internal/adapters/agent/codex
internal/adapters/agent/opencode
internal/adapters/runtime/zellij
internal/adapters/workspace/gitworktree
internal/adapters/scm/github
internal/adapters/tracker/github
```
Adapters should be leaves in the import graph. They translate external behavior
into AO ports and domain concepts; they should not own product workflows.
Good:
```txt
session_manager -> ports.Runtime
adapters/runtime/zellij -> ports + domain
adapters/workspace/gitworktree -> ports + domain
daemon -> adapters + services + storage
```
Avoid:
```txt
domain -> adapters
service/session -> adapters/runtime/zellij
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/session management used by
the HTTP mux.
Belongs here:
- terminal session lifecycle;
- input/output framing independent of HTTP;
- PTY-backed session handling;
- ring buffers and terminal protocol tests.
`httpd` adapts WebSocket connections to terminal interfaces; `terminal` should
not import `httpd`.
### `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.
HTTP-only request/response wrappers belong in `httpd` or
`httpd/controllers`. Application read models shared by controller and CLI flows
belong in the owning `service/*` package.
### `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.
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.
## Interface Placement
Prefer interfaces near their consumers, except for shared capabilities.
- If only one package consumes an abstraction, define the smallest interface in
that package.
- If multiple core packages consume a replaceable capability, define it in
`ports`.
- If HTTP controllers need a resource service, use the owning `service/*`
manager interface.
- Return concrete types from constructors unless callers genuinely need an
interface.
## Current Tree
The current main-line shape is:
```txt
backend/
cmd/ao/ # CLI entrypoint
main.go # daemon entrypoint compatibility
sqlc.yaml
internal/domain/ # shared product vocabulary and durable facts
internal/ports/ # capability interfaces
internal/service/
project/ # project API/use-case boundary
session/ # session API/use-case boundary
pr/ # PR observation/action service
internal/session_manager/ # internal session command engine
internal/lifecycle/ # durable lifecycle fact reducer
internal/observe/reaper/ # runtime observation loop
internal/storage/sqlite/ # DB, migrations, queries, generated sqlc, stores
internal/cdc/ # change_log poller and broadcaster
internal/terminal/ # terminal session protocol and PTY handling
internal/httpd/ # HTTP API, controllers, OpenAPI, terminal mux
internal/cli/ # user-facing ao command
internal/daemon/ # production wiring and shutdown
internal/config/ # daemon env/default config
internal/adapters/ # concrete agent/runtime/workspace/SCM/tracker adapters
```
## Adding New Code
Use these defaults:
- New HTTP route: add controller/API code in `httpd`, call a `service/*`
package, and update OpenAPI generation/spec tests.
- New product resource: put shared IDs/vocabulary in `domain`, use cases and
read models in `service/<resource>`, storage in `storage/sqlite`, and external
system seams in `ports`.
- New adapter: implement a `ports` interface under `adapters/<capability>/<impl>`
and wire it in `daemon`.
- New persisted fact: add a migration, sqlc query, store method, domain record or
event vocabulary, and CDC behavior when the UI/API must observe it.
- New CLI command: keep command parsing/formatting in `cli`; call the daemon API
rather than reimplementing daemon behavior.
## 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.
When a type is ambiguous, ask whether it is a product use-case/read model or an
HTTP wire wrapper. Product use-case/read models belong in `service/project`;
HTTP wire wrappers belong in `httpd`.