-
Notifications
You must be signed in to change notification settings - Fork 0
Closed
Labels
Description
Problem
Backbeat is hardcoded to Claude Code. The ClaudeProcessSpawner is the only agent implementation, and users cannot leverage other AI coding agents. This limits users who want:
- Capability matching — use the best agent for each task type
- Cost optimization — route simple tasks to cheaper agents
- Vendor flexibility — avoid lock-in to a single AI provider
Solution
Pluggable agent system with a registry, per-task agent selection, and three built-in adapters: Claude Code, Codex CLI, and Gemini CLI.
Scope
Included in v0.5.0
| # | Feature | Description |
|---|---|---|
| 1 | AgentAdapter interface | Replaces ProcessSpawner as the abstraction for agent spawning. Handles different CLI args, env vars, output formats, and nesting prevention per agent. |
| 2 | AgentRegistry | In-memory map of agent name → AgentAdapter. Code-defined (no config files). Ships with three built-in adapters. |
| 3 | agent field on Task/TaskRequest |
Optional field, defaults to "claude". Enables per-task agent selection. |
| 4 | Database migration: agent column |
ALTER TABLE tasks ADD COLUMN agent TEXT DEFAULT 'claude'. Existing tasks default to 'claude'. |
| 5 | MCP: agent param on DelegateTask |
Optional string field validated against registry. |
| 6 | CLI: --agent flag on beat run |
beat run "fix bug" --agent codex |
| 7 | WorkerPool agent dispatch | Selects correct adapter based on task.agent when spawning. |
| 8 | Security: agent name validation | Whitelist against hardcoded registry. No arbitrary command injection. |
| 9 | beat agents list command |
Show registered agents with availability status. |
| 10 | Error codes | AGENT_NOT_FOUND, AGENT_MISCONFIGURED with clear, actionable error messages. |
| 11 | TaskStatus shows agent | Which agent ran/will run the task (MCP response + CLI output). |
| 12 | Agent preserved through retry/resume | Agent field inherited when retrying or resuming a task. |
| 13 | Schedule agent support | ScheduleTask accepts agent field in task template. |
Deferred (future enhancements)
- Agent config file (
~/.backbeat/agents.json) — user-defined custom agents - Per-project agent config — project-level agent overrides
- Agent auto-discovery — PATH scanning to detect installed agents
- Agent capability metadata — structured metadata (supports resume, context window, cost tier)
- Output format normalization — agent-specific output parsing into common format
- Per-agent resource profiles — agent-specific timeout/memory/CPU defaults
Out of Scope (v0.6.0+)
- Agent failover / fallback chains (v0.6.0)
- Smart routing / task-agent matching (v0.6.0)
- Rate limit detection / cooldown tracking (v0.6.0)
- Workflow recipes / templates (v0.7.0)
- Mid-task agent switching, cost tracking, REST API
Built-in Agent Adapters
Agent Reference
| Claude Code | Codex CLI | Gemini CLI | |
|---|---|---|---|
| Command | claude |
codex |
gemini |
| Non-interactive | -p "prompt" |
exec "prompt" (subcommand) |
-p "prompt" |
| Auto-accept | --dangerously-skip-permissions |
--full-auto |
--yolo |
| JSON output | --output-format json |
--json |
--output-format json |
| API key env var | ANTHROPIC_API_KEY |
OPENAI_API_KEY |
GEMINI_API_KEY / GOOGLE_API_KEY |
| Nesting env var to strip | CLAUDECODE + CLAUDE_CODE_* |
None (sandbox isolation) | GEMINI_CLI |
| Prompt delivery | Positional arg after -p |
Positional arg after exec |
Positional arg after -p |
| Exit: success | 0 |
0 |
0 |
| JSON format | Single JSON object | NDJSON (streaming) | Single JSON object |
Spawn Commands
# Claude Code
claude -p "prompt" --dangerously-skip-permissions --output-format json
# Codex CLI
codex exec --full-auto --json "prompt"
# Gemini CLI
gemini -p "prompt" --yolo --output-format jsonKey Adapter Differences
- Codex uses a subcommand (
exec) rather than a flag for non-interactive mode - Codex JSON is NDJSON (newline-delimited streaming) vs Claude/Gemini single JSON object
- Nesting prevention: Claude strips
CLAUDECODE/CLAUDE_CODE_*, Gemini stripsGEMINI_CLI, Codex uses sandbox isolation (no env var to strip) - All three pass prompts as positional CLI args — no stdin delivery needed
User Stories
Per-Task Agent Selection
- US-1.1: As a developer, I want to run
beat run "fix bug" --agent codexto use Codex for a specific task - US-1.2: As an MCP client, I want to pass
agent: "codex"toDelegateTaskfor programmatic agent selection - US-1.3: As a developer, I want to omit
--agentand have the system default to Claude Code
Agent Visibility
- US-2.1: As a developer, I want
beat agents listto show registered agents and whether they're installed - US-2.2: As a developer, I want
beat status <id>to show which agent ran/will run a task - US-2.3: As a developer, I want to filter tasks by agent:
beat list --agent codex
Backward Compatibility
- US-3.1: As a developer upgrading from v0.4.0, I want zero changes — everything defaults to Claude Code
- US-3.2: As an MCP client, I want existing
DelegateTaskcalls withoutagentto work unchanged
Error Handling
- US-4.1: As a developer, I want a clear error when I specify a nonexistent agent, including the list of available agents
- US-4.2: As a developer, I want validation at delegation time (not spawn time) for immediate feedback
Acceptance Criteria
Agent Selection
-
DelegateTask({ prompt: "...", agent: "codex" })spawns Codex CLI withcodex exec --full-auto --json "prompt"(not Claude) -
DelegateTask({ prompt: "..." })withoutagentspawns Claude Code (backward compatible) -
beat run "..." --agent codexdelegates using the Codex adapter -
beat run "..."without--agentdelegates using Claude Code -
DelegateTask({ prompt: "...", agent: "nonexistent" })returnsAGENT_NOT_FOUNDerror
Agent Registry
- Registry contains three built-in agents:
claude,codex,gemini - Each adapter uses the correct command, args, auto-accept flags, and env var stripping per the reference table
-
beat agents listshows all registered agents with installed/not-found status - Agent names validated against registry at delegation time (not spawn time)
Task Lifecycle
-
TaskStatusresponse includesagentfield -
beat status <id>displays which agent ran the task - Agent field preserved through task retry (new retry task inherits
agentfrom original) - Agent field preserved through task resume
- Schedule with
agent: "codex"in task template creates tasks withagent: "codex" - Task with
continueFromusing different agent than dependency is allowed
Database
- Migration adds
agent TEXTcolumn to tasks table - Existing tasks default to
'claude'after migration - New tasks with
agent: "codex"persist and read back correctly
Error Handling
-
AGENT_NOT_FOUNDerror code exists with clear message including agent name and available agents -
AGENT_MISCONFIGUREDerror code exists for invalid agent configurations - Agent crash mid-task emits
TaskFailedwith agent context in error - Agent crash for one task does not affect other running tasks
Backward Compatibility
- All existing MCP tool calls without
agentfield work unchanged - All existing CLI commands without
--agentwork unchanged - Database migration preserves all existing task data
- All existing test suites pass without modification
- No breaking changes to public MCP tool schemas
Security
- Agent names validated against hardcoded whitelist (no arbitrary command injection)
- Each agent's nesting prevention env vars are correctly stripped
-
BACKBEAT_WORKERandBACKBEAT_TASK_IDenv vars set for all agents
Architecture Notes
Current State (v0.4.0)
The ProcessSpawner interface is already the abstraction boundary:
// src/core/interfaces.ts
export interface ProcessSpawner {
spawn(prompt: string, workingDirectory: string, taskId?: string): Result<{ process: ChildProcess; pid: number }>;
kill(pid: number): Result<void>;
}Only ClaudeProcessSpawner (src/implementations/process-spawner.ts) knows about Claude Code. The rest of the system (WorkerPool, handlers, events, persistence) is already agent-agnostic.
Key Files to Modify
| File | Change |
|---|---|
src/core/interfaces.ts |
New AgentAdapter interface extending/replacing ProcessSpawner |
src/core/domain.ts |
Add agent?: string to Task, TaskRequest, Worker |
src/core/errors.ts |
Add AGENT_NOT_FOUND, AGENT_MISCONFIGURED error codes |
src/implementations/process-spawner.ts |
Refactor into ClaudeAdapter, extract agent-specific logic |
src/implementations/ |
New codex-adapter.ts, gemini-adapter.ts |
src/implementations/ |
New agent-registry.ts |
src/implementations/database.ts |
Migration adding agent column |
src/implementations/task-repository.ts |
Read/write agent field |
src/implementations/event-driven-worker-pool.ts |
Use registry to select adapter per task |
src/adapters/mcp-adapter.ts |
agent field on DelegateTask, ScheduleTask schemas |
src/cli.ts / src/cli/commands/ |
--agent flag, beat agents list command |
src/bootstrap.ts |
Wire AgentRegistry into DI container |
src/services/task-manager.ts |
Validate agent exists at delegation time |
Breaking Changes
None. The agent field is optional everywhere and defaults to "claude". Existing MCP clients, CLI scripts, scheduled tasks, and database records all continue working without modification.
Reactions are currently unavailable