feat: add pluggable multi-CLI backend system

Implement BackendAdapter interface with four CLI backends:
- ClaudeCodeBackend (extracted from AgentRuntime)
- CodexBackend (OpenAI Codex CLI)
- GeminiBackend (Google Gemini CLI)
- OpenCodeBackend (OpenCode CLI)

Add BackendRegistry for resolution/creation via AGENT_BACKEND env var.
Refactor AgentRuntime to delegate to BackendAdapter instead of
hardcoding Claude CLI. Update GatewayConfig with new env vars
(AGENT_BACKEND, BACKEND_CLI_PATH, BACKEND_MODEL, BACKEND_MAX_TURNS).

Includes 10 property-based test files and unit tests for edge cases.
This commit is contained in:
2026-02-22 23:41:30 -05:00
parent f2247ea3ac
commit 453389f55c
25 changed files with 3262 additions and 195 deletions

View File

@@ -0,0 +1,77 @@
# Tasks
## Task 1: Create BackendAdapter interface and shared types
- [x] 1.1 Create `src/backends/types.ts` with `BackendAdapter` interface, `BackendAdapterConfig`, `BackendEventResult`, `StreamCallback`, and `BackendName` type
- [x] 1.2 Export all types from `src/backends/index.ts` barrel file
## Task 2: Implement ClaudeCodeBackend
- [x] 2.1 Create `src/backends/claude-backend.ts` implementing `BackendAdapter`
- [x] 2.2 Extract CLI spawning logic from `AgentRuntime.runClaude()` into `execute()` method with arg building for `-p`, `--output-format json`, `--dangerously-skip-permissions`, `--append-system-prompt-file`, `--allowedTools`, `--max-turns`, and `--resume`
- [x] 2.3 Implement `validate()` to check CLI binary is executable
- [x] 2.4 Implement JSON array output parser extracting `session_id` from `system/init` and `result` from `result` objects
- [x] 2.5 Write property test: Claude backend required flags (Property 1)
- [x] 🧪 PBT: *For any* prompt, system prompt, and tools list, generated args contain all required flags
## Task 3: Implement CodexBackend
- [x] 3.1 Create `src/backends/codex-backend.ts` implementing `BackendAdapter`
- [x] 3.2 Implement `execute()` with `codex exec` subcommand, `--json`, `--dangerously-bypass-approvals-and-sandbox`, `--cd`, and `codex exec resume <id>` for sessions
- [x] 3.3 Implement newline-delimited JSON parser extracting final assistant message
- [x] 3.4 Write property test: Codex backend required flags (Property 2)
- [x] 🧪 PBT: *For any* prompt and working directory, generated args contain exec, --json, --dangerously-bypass-approvals-and-sandbox, and --cd
## Task 4: Implement GeminiBackend
- [x] 4.1 Create `src/backends/gemini-backend.ts` implementing `BackendAdapter`
- [x] 4.2 Implement `execute()` with prompt as positional arg, `--output-format json`, `--approval-mode yolo`, and `--resume` for sessions
- [x] 4.3 Implement JSON output parser extracting response text
- [x] 4.4 Write property test: Gemini backend required flags (Property 3)
- [x] 🧪 PBT: *For any* prompt, generated args contain the prompt positionally, --output-format json, and --approval-mode yolo
## Task 5: Implement OpenCodeBackend
- [x] 5.1 Create `src/backends/opencode-backend.ts` implementing `BackendAdapter`
- [x] 5.2 Implement `execute()` with `opencode run` subcommand, `--format json`, `--model`, and `--session <id> --continue` for sessions
- [x] 5.3 Implement JSON event parser extracting final response text
- [x] 5.4 Write property test: OpenCode backend required flags (Property 4)
- [x] 🧪 PBT: *For any* prompt and optional model, generated args contain run, --format json, and --model when configured
## Task 6: Implement BackendRegistry
- [x] 6.1 Create `src/backends/registry.ts` with `resolveBackendName()` and `createBackend()` functions
- [x] 6.2 `resolveBackendName` accepts "claude", "codex", "gemini", "opencode", defaults to "claude" for undefined, throws for invalid values
- [x] 6.3 `createBackend` instantiates the correct backend implementation from a `BackendName`
- [x] 6.4 Write property test: Backend name resolution (Property 7)
- [x] 🧪 PBT: *For any* string, resolveBackendName returns correct BackendName for valid values, "claude" for undefined, and throws for invalid
## Task 7: Cross-backend property tests
- [x] 7.1 Write property test: Session resume args across backends (Property 5)
- [x] 🧪 PBT: *For any* backend and session ID, session flags appear when ID is provided and are absent when not
- [x] 7.2 Write property test: Output parsing extracts correct fields (Property 6)
- [x] 🧪 PBT: *For any* valid backend-specific JSON output, parser produces BackendEventResult with correct responseText and sessionId
- [x] 7.3 Write property test: Non-zero exit code produces error result (Property 8)
- [x] 🧪 PBT: *For any* backend, non-zero exit code, and stderr string, result has isError=true and responseText contains stderr
## Task 8: Update GatewayConfig
- [x] 8.1 Add `agentBackend`, `backendCliPath`, `backendModel`, `backendMaxTurns` fields to `GatewayConfig` interface in `src/config.ts`
- [x] 8.2 Update `loadConfig()` to read `AGENT_BACKEND`, `BACKEND_CLI_PATH`, `BACKEND_MODEL`, `BACKEND_MAX_TURNS` env vars with defaults
- [x] 8.3 Deprecate `claudeCliPath` field (keep for backward compat, map to `backendCliPath` when `AGENT_BACKEND=claude`)
## Task 9: Refactor AgentRuntime
- [x] 9.1 Add `BackendAdapter` parameter to `AgentRuntime` constructor
- [x] 9.2 Replace `executeClaude()` and `runClaude()` with calls to `this.backend.execute()`
- [x] 9.3 Implement `BackendEventResult` → gateway `EventResult` mapping in a helper method
- [x] 9.4 Remove `ClaudeJsonResponse` interface and Claude-specific parsing from `AgentRuntime`
- [x] 9.5 Write property test: EventResult mapping preserves semantics (Property 9)
- [x] 🧪 PBT: *For any* BackendEventResult and channel ID, mapping sets error or responseText correctly based on isError
- [x] 9.6 Write property test: Session ID storage after backend execution (Property 10)
- [x] 🧪 PBT: *For any* channel ID and BackendEventResult with sessionId, SessionManager contains that sessionId after processing
## Task 10: Startup validation and wiring
- [x] 10.1 Update main entry point to call `resolveBackendName()` and `createBackend()` from config
- [x] 10.2 Call `backend.validate()` at startup; log error with backend name and path, exit(1) on failure
- [x] 10.3 Inject the `BackendAdapter` instance into `AgentRuntime` constructor
- [x] 10.4 Write unit tests for startup validation flow (valid backend, invalid backend name, missing CLI binary)
## Task 11: Unit tests for edge cases
- [x] 11.1 Write unit tests for each backend's `validate()` method (binary exists vs missing)
- [x] 11.2 Write unit tests for timeout behavior (process killed after queryTimeoutMs)
- [x] 11.3 Write unit tests for session corruption detection and cleanup
- [x] 11.4 Write unit tests for default config values when env vars are unset
- [x] 11.5 Write unit tests for unsupported option warning (e.g., ALLOWED_TOOLS on backends without tool filtering)