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

AlternativeWhy rejected
Direct Claude couplingNo testing, no future extensibility
Plugin system (dylib)Over-engineered for v1
Config-driven command templateLess type-safe, harder to test

Consequences

  • Easy to write a MockBackend for 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,
}
}