ADR-003: AgentBackend Trait Abstraction
Status
Accepted
Context
Currently we target Claude Code CLI (claude -p) as the agent backend. However, we want the architecture to support future backends (e.g., other LLM CLIs, HTTP APIs, local models) without rewriting the agent engine.
Decision
Define an AgentBackend trait and an AgentProcess trait. The orchestrator interacts only with these traits.
Trait Design
#![allow(unused)] fn main() { trait AgentBackend: Send + Sync { fn id(&self) -> &str; fn spawn_session(&self, config: &SessionConfig) -> Result<Box<dyn AgentProcess>>; } trait AgentProcess: Send { fn pid(&self) -> u32; async fn wait(&mut self) -> ExitOutcome; fn terminate(&self) -> Result<()>; // SIGTERM fn kill(&self) -> Result<()>; // SIGKILL fn supports_injection(&self) -> bool; } }
Current Implementation: ClaudeBackend
- Spawns
claude -p <prompt> --dangerously-skip-permissions --model <model> --output-format json - Sets working directory to agent's worktree
- Injects SWARM_* environment variables
- Isolates process in own process group via
setsid(pre_exec) - Redirects stdout+stderr to agent's
current.log
Alternatives Considered
| Alternative | Why rejected |
|---|---|
| Direct Claude coupling | No testing, no future extensibility |
| Plugin system (dylib) | Over-engineered for v1 |
| Config-driven command template | Less type-safe, harder to test |
Consequences
- Easy to write a
MockBackendfor integration tests. - Adding a new backend requires only implementing two traits.
supports_injection()returns false for Claude (no stdin injection); future backends may return true.
ExitOutcome
#![allow(unused)] fn main() { enum ExitOutcome { Success, Error { code: Option<i32>, signal: Option<i32> }, Timeout, } }