Skip to content

Execution Engines

New to engines?

For a plain-language comparison and decision tree, see Engines Explained. This page covers the detailed technical reference.

Overview

Execution engines wrap AI coding tools (Claude Code, OpenAI Codex, Aider) and produce engine-agnostic ExecutionSpec structs that the JobBuilder translates into Kubernetes Jobs. This decoupling enables testing without a cluster, supports multiple AI tools from a single controller, and opens the door to non-K8s runtimes in future.

Interface Summary

Property Value
Proto definition proto/engine.proto
Go interface pkg/engine/engine.go
Interface version 1
Role in lifecycle Called after guard rail validation to produce the K8s Job spec

Go Interface

type ExecutionEngine interface {
    // BuildExecutionSpec translates a task into a container spec.
    BuildExecutionSpec(task Task, config EngineConfig) (*ExecutionSpec, error)

    // BuildPrompt constructs the task prompt for the AI agent.
    BuildPrompt(task Task) (string, error)

    // Name returns the unique engine identifier.
    Name() string

    // InterfaceVersion returns the interface version.
    InterfaceVersion() int
}

Task

The input to all engine methods, populated from the ticketing backend:

type Task struct {
    ID          string            // Unique task identifier.
    TicketID    string            // Source ticket identifier.
    Title       string            // Short summary (used as prompt heading).
    Description string            // Full task description (main prompt content).
    RepoURL     string            // Repository the agent should work on.
    Labels      []string          // Labels from the source ticket.
    Metadata    map[string]string // Additional key-value pairs.
}

EngineConfig

Runtime configuration passed to the engine:

type EngineConfig struct {
    Image            string    // Container image override.
    TimeoutSeconds   int       // Active deadline for the K8s Job.
    ResourceRequests Resources // CPU and memory requests.
    ResourceLimits   Resources // CPU and memory limits.
    Env              map[string]string // Additional environment variables.
}

ExecutionSpec

The output — everything needed to create a K8s Job:

type ExecutionSpec struct {
    Image                 string            // Container image to run.
    Command               []string          // Entrypoint command and arguments.
    Env                   map[string]string // Plain-text environment variables.
    SecretEnv             map[string]string // Key=env var name, Value=K8s Secret name.
    ResourceRequests      Resources
    ResourceLimits        Resources
    Volumes               []VolumeMount
    ActiveDeadlineSeconds int               // Hard timeout for the Job.
}

TaskResult

The structured outcome of a completed task, written to /workspace/result.json:

type TaskResult struct {
    Success         bool        // Whether the task completed successfully.
    MergeRequestURL string      // URL of the created pull request.
    BranchName      string      // The branch containing changes.
    Summary         string      // Human-readable summary.
    TokenUsage      *TokenUsage // Input/output token counts.
    CostEstimateUSD float64     // Estimated cost in US dollars.
    ExitCode        int         // 0=success, 1=agent failure, 2=guard rail blocked.
}

Built-in Engines

Claude Code

The primary and recommended engine. Runs the Claude Code CLI in headless mode with full hook-based guard rail support.

Property Value
Engine name claude-code
Package pkg/engine/claudecode/
Default image ghcr.io/unitaryai/engine-claude-code:latest
Default timeout 7200 seconds (2 hours)
API key secret anthropic-api-key
Guard rails Pre-tool-use hooks via hooks.json
Max agentic turns 50 (configurable)

Configuration

config:
  engines:
    default: claude-code
    claude_code:
      image: "ghcr.io/unitaryai/engine-claude-code:v2.1.0"
      max_turns: 50
      model: "claude-sonnet-4-6"
      timeout_seconds: 3600
      fallback_model: haiku              # used when primary model is overloaded
      no_session_persistence: true       # disable session state between turns
      append_system_prompt: "Always run tests before committing."
      tool_whitelist:                    # only these tools are available
        - Bash
        - Read
        - Write
        - Edit
      tool_blacklist:                    # these tools are blocked
        - WebSearch
      json_schema: '{"type":"object","properties":{"success":{"type":"boolean"},"summary":{"type":"string"}},"required":["success","summary"]}'
      resource_requests:
        cpu: "500m"
        memory: "512Mi"
      resource_limits:
        cpu: "2"
        memory: "2Gi"
      skills:                            # custom skills — see Skills section below
        - name: create-changelog
          inline: |
            # Create Changelog
            Generate a CHANGELOG.md entry for the changes made.
        - name: review-checklist
          path: /opt/robodev/skills/review-checklist.md
        - name: deploy-guide
          configmap: deploy-skills       # load from a Kubernetes ConfigMap
          key: deploy-guide.md           # optional — defaults to <name>.md
      sub_agents:                        # see Sub-Agents section below
        - name: reviewer
          description: "Reviews code changes for correctness"
          prompt: "You are a code reviewer. Check for bugs, security issues, and style."
          model: haiku
        - name: architect
          description: "System architecture reviewer"
          configmap: architect-agent      # load prompt from ConfigMap
      agent_teams:                       # experimental multi-instance collaboration
        enabled: false
        mode: in-process                 # required for headless K8s containers
        max_teammates: 3
Field Type Default Description
image string ghcr.io/unitaryai/engine-claude-code:latest Container image override
timeout_seconds int 7200 Active deadline for the K8s Job
fallback_model string Model to use when the primary is overloaded (e.g. haiku)
no_session_persistence bool false Disable Claude Code session persistence between turns
append_system_prompt string Extra text appended to Claude Code's system prompt
tool_whitelist []string Only allow these Claude Code tools (via --allowedTools)
tool_blacklist []string Block these Claude Code tools (via --disallowedTools)
json_schema string built-in TaskResult schema JSON schema for structured output (via --json-schema)
skills []SkillConfig Custom skill files loaded into the agent — see Skills
sub_agents []SubAgentConfig Sub-agent definitions — see Sub-Agents
agent_teams AgentTeamsConfig disabled Experimental multi-instance collaboration — see Agent Teams

Command

The engine generates a claude CLI invocation in streaming JSON mode:

setup-claude.sh \
  -p "<prompt>" \
  --output-format stream-json \
  --max-turns 50 \
  --dangerously-skip-permissions \
  --verbose \
  --mcp-config /workspace/.mcp.json

The setup-claude.sh wrapper runs before claude to initialise the writable home directory — writing ~/.claude/settings.json, /workspace/.mcp.json, and any skill files. It then execs the real claude binary with the arguments above.

Guard Rails (Hooks)

Claude Code supports a hooks system that intercepts tool calls before execution. RoboDev generates a hooks.json configuration file and mounts it into the agent container at /config/hooks.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "/config/guard-rail-check.sh \"$TOOL_INPUT\""
      },
      {
        "matcher": "Write|Edit",
        "command": "/config/file-pattern-check.sh \"$TOOL_INPUT\""
      }
    ],
    "PostToolUse": [
      {
        "command": "/config/heartbeat.sh"
      }
    ]
  }
}

The guard rail check scripts validate each tool call against:

  • Destructive command detection — blocks rm -rf, DROP TABLE, git push --force, sudo, and similar dangerous commands.
  • Blocked file patterns — prevents reading or writing files matching patterns in blocked_file_patterns (e.g., *.env, *.key, *.pem).
  • Network restriction — optionally blocks curl, wget, and other network tools from contacting external hosts.

If a hook script exits with a non-zero code, Claude Code blocks the tool call and reports the violation to the agent, which can then adjust its approach.

The PostToolUse hook writes heartbeat telemetry to /workspace/heartbeat.json after every tool invocation, enabling the progress watchdog to monitor agent activity.

Environment Variables

Variable Source Description
ANTHROPIC_API_KEY K8s Secret robodev-anthropic-key API authentication
ROBODEV_TASK_ID Controller Unique task identifier
ROBODEV_TICKET_ID Controller Source ticket identifier
ROBODEV_REPO_URL Ticket Repository to work on
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC Engine Always set to 1
CLAUDE_SKILL_INLINE_<NAME> Engine Base64-encoded inline skill content (see Skills)
CLAUDE_SKILL_PATH_<NAME> Engine Path to a skill file on the image or ConfigMap mount (see Skills)
CLAUDE_SUBAGENT_PATH_<NAME> Engine Path to a ConfigMap-backed sub-agent file (see Sub-Agents)
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS Engine Set to 1 when agent teams are enabled
CLAUDE_CODE_MAX_TEAMMATES Engine Maximum teammate agents (agent teams)

Volume Mounts

Mount Path Writable Purpose
workspace /workspace Yes Repository checkout and working directory
config /config No Guard rail hooks and configuration
home /home/robodev Yes Writable home directory (emptyDir) for ~/.claude/ config and skills
tmp /tmp Yes Writable tmp (emptyDir) for Claude Code subprocess shell directories

Skills

Skills are custom Markdown instruction files that the agent can invoke via /skill-name in its prompts. They are written to ~/.claude/skills/<name>.md before the agent starts.

Each skill has a name (lowercase letters, digits, and hyphens only) and exactly one of:

  • inline — the Markdown content directly in the config. The controller base64-encodes it and passes it as the CLAUDE_SKILL_INLINE_<NAME> environment variable.
  • path — a path to a Markdown file on the container image (e.g. /opt/robodev/skills/review-checklist.md). The controller passes it as CLAUDE_SKILL_PATH_<NAME>.
  • configmap — the name of a Kubernetes ConfigMap containing the skill. The controller mounts the ConfigMap as a volume at /skills/<name>.md and sets CLAUDE_SKILL_PATH_<NAME> to the mount path. Optionally specify key to select a specific key within the ConfigMap (defaults to <name>.md).

At container startup, setup-claude.sh reads these environment variables, decodes/copies the files, and writes them to ~/.claude/skills/. The <NAME> suffix is converted to lowercase with hyphens (e.g. CLAUDE_SKILL_INLINE_CREATE_CHANGELOG~/.claude/skills/create-changelog.md).

Example — inline skill:

engines:
  claude_code:
    skills:
      - name: create-changelog
        inline: |
          # Create Changelog

          When asked to create a changelog entry:
          1. Read the existing CHANGELOG.md
          2. Determine the next version number from git tags
          3. Add a new section with today's date
          4. List all changes since the last release

Example — image-bundled skill:

engines:
  claude_code:
    skills:
      - name: security-review
        path: /opt/robodev/skills/security-review.md

Example — ConfigMap-backed skill:

engines:
  claude_code:
    skills:
      - name: deploy-guide
        configmap: deploy-skills    # K8s ConfigMap name
        key: deploy-guide.md        # optional — defaults to <name>.md

Create the ConfigMap separately:

kubectl create configmap deploy-skills \
  --from-file=deploy-guide.md=./skills/deploy-guide.md

ConfigMap skills are ideal for large skill files or when different teams manage their own skills independently of the controller configuration.

To bundle skills into the container image, add them to your custom Dockerfile:

FROM ghcr.io/unitaryai/engine-claude-code:latest
COPY skills/ /opt/robodev/skills/

Sub-Agents

Sub-agents allow the main Claude Code agent to delegate specialised subtasks to other agents during execution. Each sub-agent has its own system prompt, model, tool restrictions, and permission mode. Sub-agents use Claude Code's official --agents flag and ~/.claude/agents/ directory.

Sub-agents can be defined inline (prompt in the config) or loaded from a Kubernetes ConfigMap (for large prompts or independent management).

Configuration:

engines:
  claude_code:
    sub_agents:
      # Inline sub-agent — prompt is in the config.
      - name: reviewer
        description: "Reviews code changes for correctness and style"
        prompt: |
          You are a code reviewer. Check for:
          - Bugs and logic errors
          - Security vulnerabilities
          - Style and convention violations
        model: haiku
        tools:
          - Read
          - Grep
          - Glob
        max_turns: 10

      # ConfigMap-backed sub-agent — prompt loaded from a volume.
      - name: architect
        description: "Reviews system architecture decisions"
        configmap: architect-agent    # K8s ConfigMap name
        key: architect.md             # optional — defaults to <name>.md
        model: opus

      # Background sub-agent.
      - name: linter
        description: "Runs linting in the background"
        prompt: "Run the project linter and report issues."
        background: true
Field Type Default Description
name string Sub-agent identifier (required)
description string Short summary of the sub-agent's purpose (required)
prompt string Inline system prompt (mutually exclusive with configmap)
model string inherit Model to use: sonnet, opus, haiku, or inherit
tools []string Tool allowlist
disallowed_tools []string Tool denylist
permission_mode string default One of default, acceptEdits, dontAsk, bypassPermissions, plan
max_turns int Maximum agentic turns
skills []string Skill names to preload
background bool false Run as a background process
configmap string Load prompt from this Kubernetes ConfigMap
key string <name>.md Key within the ConfigMap

How it works:

  • Inline sub-agents are serialised as JSON and passed via --agents '{"name": {"description":"...", "prompt":"...", ...}}'.
  • ConfigMap sub-agents are volume-mounted at /subagents/<name>.md and copied to ~/.claude/agents/<name>.md by setup-claude.sh at container startup. Claude Code automatically discovers agent files in this directory.

Creating a ConfigMap for a sub-agent:

kubectl create configmap architect-agent \
  --from-file=architect.md=./agents/architect.md

The Markdown file can include YAML frontmatter for metadata (model, tools, etc.) following the Claude Code sub-agents specification.

Agent Teams

Experimental

Agent teams are an experimental Claude Code feature (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS). The API may change in future Claude Code releases.

Agent teams spawn multiple independent Claude Code instances that collaborate via shared task lists and inter-agent messaging, coordinated by a team lead. The team lead dynamically creates teammates based on the task — you do not pre-define agents.

This is fundamentally different from Sub-Agents, which are lightweight helpers within a single Claude Code session. Both features can be used simultaneously.

Configuration:

engines:
  claude_code:
    agent_teams:
      enabled: true
      mode: in-process        # required for headless K8s containers (no tmux)
      max_teammates: 3
Field Type Default Description
enabled bool false Activate agent teams mode
mode string in-process Teammate execution mode. Use in-process for headless containers (no tmux required). Alternative: tmux for interactive environments
max_teammates int 3 Maximum number of teammate agents the team lead can spawn

When enabled, the engine:

  1. Sets CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 and CLAUDE_CODE_MAX_TEAMMATES=<n> in the container environment.
  2. Appends --teammate-mode <mode> to the Claude CLI command.

The team lead then autonomously decides how many teammates to create, what roles they should have, and how to distribute work across them.


OpenAI Codex

Runs the OpenAI Codex CLI in fully autonomous mode.

Property Value
Engine name codex
Package pkg/engine/codex/
Default image ghcr.io/unitaryai/engine-codex:latest
Default timeout 7200 seconds (2 hours)
API key secret openai-api-key
Guard rails Prompt-embedded rules

Configuration

config:
  engines:
    default: codex
    codex:
      image: "ghcr.io/unitaryai/engine-codex:v1.0.0"
      timeout_seconds: 3600
      resource_requests:
        cpu: "500m"
        memory: "512Mi"
      resource_limits:
        cpu: "2"
        memory: "2Gi"

Command

codex \
  --quiet \
  --approval-mode full-auto \
  --full-stdout \
  "<prompt>"

Guard Rails (Prompt-Embedded)

Codex does not support a hooks system. Guard rails are appended directly to the task prompt:

## Guard Rails

You MUST follow these rules strictly:
- Do NOT execute destructive commands (e.g. rm -rf /, drop database, etc.)
- Do NOT modify or read files matching sensitive patterns (*.env, **/secrets/**, *.key, *.pem)
- Do NOT make network requests to external services other than the repository remote
- Do NOT install packages or dependencies without explicit instructions to do so
- Do NOT push commits directly; stage and commit changes locally only

Prompt-based guard rails are advisory

The AI model may not always respect prompt-embedded rules. For stricter enforcement, use the Claude Code engine with hook-based guards, or rely on the quality gate for post-completion validation.

Repository Context

Codex reads repository conventions from AGENTS.md (rather than CLAUDE.md). If an AGENTS.md file is present in the repository root, Codex will use it for coding conventions and project structure guidance.

Environment Variables

Variable Source Description
OPENAI_API_KEY K8s Secret openai-api-key API authentication
ROBODEV_TASK_ID Controller Unique task identifier
ROBODEV_TICKET_ID Controller Source ticket identifier
ROBODEV_REPO_URL Ticket Repository to work on

Aider

Runs the Aider CLI for AI-assisted coding. Aider supports multiple LLM providers — RoboDev can configure it to use either Anthropic or OpenAI models.

Property Value
Engine name aider
Package pkg/engine/aider/
Default image ghcr.io/unitaryai/engine-aider:latest
Default timeout 7200 seconds (2 hours)
API key secret anthropic-api-key (default) or openai-api-key
Guard rails Prompt-embedded rules

Configuration

config:
  engines:
    default: aider
    aider:
      image: "ghcr.io/unitaryai/engine-aider:v1.0.0"
      provider: "anthropic"   # or "openai"
      timeout_seconds: 3600
      resource_requests:
        cpu: "500m"
        memory: "512Mi"
      resource_limits:
        cpu: "2"
        memory: "2Gi"

Command

aider \
  --yes \
  --no-git \
  --message "<prompt>"

The --no-git flag is used because RoboDev manages git operations via the SCM backend, not via Aider's built-in git support.

Model Provider

Aider supports both Anthropic and OpenAI models. Configure the provider in the engine configuration:

Provider Environment Variable Secret Name
anthropic (default) ANTHROPIC_API_KEY anthropic-api-key
openai OPENAI_API_KEY openai-api-key

Repository Context

Aider reads coding conventions from .aider/conventions.md (rather than CLAUDE.md or AGENTS.md). If an .aider.conf.yml file is present in the repository root, Aider will use it for additional configuration (model selection, editor settings, etc.).

Guard Rails (Prompt-Embedded)

Like Codex, Aider does not support a hooks system. Guard rails are appended directly to the task prompt, identical in content to the Codex guard rails above.

OpenCode

OpenCode is a terminal-based AI coding agent. RoboDev runs it in headless mode inside Kubernetes Jobs.

Package: pkg/engine/opencode/

Property Value
Engine name opencode
Default image ghcr.io/unitaryai/engine-opencode:latest
Command opencode --non-interactive --message <prompt>
Interface version 1

Configuration

engines:
  default: opencode
  opencode:
    image: ghcr.io/unitaryai/engine-opencode:v1.0.0  # optional override
    auth:
      method: api_key
      api_key_secret: anthropic-credentials
    provider: anthropic  # or "openai", "google"

Providers

Provider Environment Variable K8s Secret Key
anthropic (default) ANTHROPIC_API_KEY anthropic-api-key
openai OPENAI_API_KEY openai-api-key
google GOOGLE_API_KEY google-api-key

Repository Context

OpenCode reads coding conventions from AGENTS.md in the repository root.

Guard Rails (Prompt-Embedded)

OpenCode does not support a hooks system. Guard rails are appended directly to the task prompt.

Cline

Community template — no pre-built image

Cline is a VS Code extension with no published headless CLI. The Go engine implementation (pkg/engine/cline/) and the Dockerfile (docker/engine-cline/) are provided as a community contribution template. No pre-built container image is published for Cline. Configuring cline as your engine will result in an image pull failure until a working headless integration is contributed. See Contributing if you want to help.

Cline is an AI coding agent with optional MCP (Model Context Protocol) and AWS Bedrock support. When a headless CLI becomes available, RoboDev can run it inside Kubernetes Jobs using the implementation in pkg/engine/cline/.

Package: pkg/engine/cline/

Property Value
Engine name cline
Default image ghcr.io/unitaryai/engine-cline:latest (not yet published)
Command cline --headless --task <prompt> --output-format json
Interface version 1

Configuration

engines:
  default: cline
  cline:
    image: ghcr.io/unitaryai/engine-cline:v1.0.0  # optional override
    auth:
      method: api_key
      api_key_secret: anthropic-credentials
    provider: anthropic  # or "openai", "google", "bedrock"
    mcp_enabled: true    # append --mcp flag

Providers

Provider Environment Variable K8s Secret Key
anthropic (default) ANTHROPIC_API_KEY anthropic-api-key
openai OPENAI_API_KEY openai-api-key
google GOOGLE_API_KEY google-api-key
bedrock AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY aws-access-key-id, aws-secret-access-key

Repository Context

Cline reads project-specific instructions from .clinerules in the repository root.

MCP Support

When mcp_enabled: true is set in the engine configuration, the --mcp flag is appended to the Cline command, enabling Model Context Protocol integration for tool use.

Guard Rails (Prompt-Embedded)

Cline does not support a hooks system. Guard rails are appended directly to the task prompt.

Engine Selection

The controller selects engines in this order:

  1. Per-ticket override — if the ticket metadata or labels specify an engine (e.g., a engine:codex label), that engine is used.
  2. Default engine — the engines.default configuration value.
  3. Fallbackclaude-code if no default is configured.

Comparison Matrix

Criterion Claude Code Codex Aider OpenCode Cline
Guard rail enforcement Hook-based (deterministic) Prompt-based (advisory) Prompt-based (advisory) Prompt-based (advisory) Prompt-based (advisory)
Sub-agent support Yes (official) No No No No
Multi-model support Anthropic only OpenAI only Anthropic + OpenAI Anthropic + OpenAI + Google Anthropic + OpenAI + Google + Bedrock
Agentic turns limit Configurable (max_turns) N/A N/A N/A N/A
Repository context file CLAUDE.md AGENTS.md .aider/conventions.md AGENTS.md .clinerules
Heartbeat telemetry Via PostToolUse hook Not built-in Not built-in Not built-in Not built-in
MCP server support Yes No No No Yes (via --mcp flag)
Pre-built image ❌ Community template

Recommendation: Use Claude Code as the default engine for its superior guard rail enforcement via hooks and built-in heartbeat telemetry. Use Codex or Aider when you need OpenAI models or have specific tool preferences. OpenCode supports Google models. Cline is a community template without a published image.

Writing a Custom Engine

To add a new engine, create a new package under pkg/engine/ and implement the ExecutionEngine interface:

package myengine

import (
    "fmt"
    "strings"

    "github.com/unitaryai/robodev/pkg/engine"
)

const (
    engineName       = "my-engine"
    interfaceVersion = 1
    defaultImage     = "ghcr.io/myorg/my-engine:latest"
)

// MyEngine implements engine.ExecutionEngine.
type MyEngine struct{}

func New() *MyEngine { return &MyEngine{} }

func (e *MyEngine) Name() string           { return engineName }
func (e *MyEngine) InterfaceVersion() int   { return interfaceVersion }

func (e *MyEngine) BuildExecutionSpec(task engine.Task, config engine.EngineConfig) (*engine.ExecutionSpec, error) {
    if task.ID == "" {
        return nil, fmt.Errorf("task ID must not be empty")
    }

    prompt, err := e.BuildPrompt(task)
    if err != nil {
        return nil, fmt.Errorf("building prompt: %w", err)
    }

    image := config.Image
    if image == "" {
        image = defaultImage
    }

    return &engine.ExecutionSpec{
        Image:   image,
        Command: []string{"my-cli", "--prompt", prompt},
        Env: map[string]string{
            "ROBODEV_TASK_ID":   task.ID,
            "ROBODEV_TICKET_ID": task.TicketID,
            "ROBODEV_REPO_URL":  task.RepoURL,
        },
        SecretEnv: map[string]string{
            "MY_API_KEY": "my-api-key-secret",
        },
        Volumes: []engine.VolumeMount{
            {Name: "workspace", MountPath: "/workspace"},
            {Name: "config", MountPath: "/config", ReadOnly: true},
        },
        ActiveDeadlineSeconds: config.TimeoutSeconds,
    }, nil
}

func (e *MyEngine) BuildPrompt(task engine.Task) (string, error) {
    if task.Title == "" {
        return "", fmt.Errorf("task title must not be empty")
    }

    var b strings.Builder
    b.WriteString("# Task: ")
    b.WriteString(task.Title)
    b.WriteString("\n\n")

    if task.Description != "" {
        b.WriteString("## Description\n\n")
        b.WriteString(task.Description)
        b.WriteString("\n\n")
    }

    b.WriteString("## Instructions\n\n")
    b.WriteString("Complete the task above. Work in /workspace.\n")
    b.WriteString("Write result.json to /workspace/result.json when finished.\n")

    return b.String(), nil
}

Register the engine with the controller at startup:

reconciler := controller.NewReconciler(cfg, logger,
    controller.WithEngine(claudecode.New()),
    controller.WithEngine(codex.New()),
    controller.WithEngine(aider.New()),
    controller.WithEngine(myengine.New()),
)

Testing

Write table-driven tests for both BuildExecutionSpec and BuildPrompt:

func TestMyEngine_BuildExecutionSpec(t *testing.T) {
    tests := []struct {
        name    string
        task    engine.Task
        config  engine.EngineConfig
        wantErr bool
    }{
        {
            name: "valid task produces spec",
            task: engine.Task{ID: "1", Title: "Fix bug"},
            config: engine.EngineConfig{TimeoutSeconds: 3600},
        },
        {
            name:    "empty ID returns error",
            task:    engine.Task{Title: "Fix bug"},
            wantErr: true,
        },
    }

    e := New()
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            spec, err := e.BuildExecutionSpec(tt.task, tt.config)
            if tt.wantErr {
                require.Error(t, err)
                return
            }
            require.NoError(t, err)
            assert.NotEmpty(t, spec.Image)
            assert.NotEmpty(t, spec.Command)
        })
    }
}

Protobuf Definition

The complete protobuf service is defined in proto/engine.proto. Note that engines are currently built-in only (Go), but the protobuf definition exists for future support of third-party engines via gRPC.

service ExecutionEngine {
    rpc Handshake(HandshakeRequest) returns (HandshakeResponse);
    rpc BuildExecutionSpec(BuildExecutionSpecRequest) returns (ExecutionSpec);
    rpc BuildPrompt(BuildPromptRequest) returns (BuildPromptResponse);
}