Skip to content

v0.5.0: Multi-Agent Support #67

@dean0x

Description

@dean0x

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 json

Key Adapter Differences

  1. Codex uses a subcommand (exec) rather than a flag for non-interactive mode
  2. Codex JSON is NDJSON (newline-delimited streaming) vs Claude/Gemini single JSON object
  3. Nesting prevention: Claude strips CLAUDECODE/CLAUDE_CODE_*, Gemini strips GEMINI_CLI, Codex uses sandbox isolation (no env var to strip)
  4. 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 codex to use Codex for a specific task
  • US-1.2: As an MCP client, I want to pass agent: "codex" to DelegateTask for programmatic agent selection
  • US-1.3: As a developer, I want to omit --agent and have the system default to Claude Code

Agent Visibility

  • US-2.1: As a developer, I want beat agents list to 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 DelegateTask calls without agent to 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 with codex exec --full-auto --json "prompt" (not Claude)
  • DelegateTask({ prompt: "..." }) without agent spawns Claude Code (backward compatible)
  • beat run "..." --agent codex delegates using the Codex adapter
  • beat run "..." without --agent delegates using Claude Code
  • DelegateTask({ prompt: "...", agent: "nonexistent" }) returns AGENT_NOT_FOUND error

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 list shows all registered agents with installed/not-found status
  • Agent names validated against registry at delegation time (not spawn time)

Task Lifecycle

  • TaskStatus response includes agent field
  • beat status <id> displays which agent ran the task
  • Agent field preserved through task retry (new retry task inherits agent from original)
  • Agent field preserved through task resume
  • Schedule with agent: "codex" in task template creates tasks with agent: "codex"
  • Task with continueFrom using different agent than dependency is allowed

Database

  • Migration adds agent TEXT column to tasks table
  • Existing tasks default to 'claude' after migration
  • New tasks with agent: "codex" persist and read back correctly

Error Handling

  • AGENT_NOT_FOUND error code exists with clear message including agent name and available agents
  • AGENT_MISCONFIGURED error code exists for invalid agent configurations
  • Agent crash mid-task emits TaskFailed with agent context in error
  • Agent crash for one task does not affect other running tasks

Backward Compatibility

  • All existing MCP tool calls without agent field work unchanged
  • All existing CLI commands without --agent work 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_WORKER and BACKBEAT_TASK_ID env 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions