provider#

import "github.com/jbcom/radioactive-ralph/internal/provider"

Package provider adapts configured CLI backends into radioactive_ralph’s provider-neutral worker execution contract.

Index#

Variables#

DefaultPromptPatterns are the regexes superviseAgent uses out of the box to recognize an interactive permission/clarification prompt in a CLI’s output — the shapes seen from Claude Code, Codex, opencode, and generic POSIX-confirmation prompts (“(y/n)”, “Y/n”, etc.). Callers with a provider-specific prompt shape should extend, not replace, this list.

var DefaultPromptPatterns = []*regexp.Regexp{
    regexp.MustCompile(`(?i)\(y/n\)`),
    regexp.MustCompile(`(?i)\[y/n\]`),
    regexp.MustCompile(`(?i)continue\?`),
    regexp.MustCompile(`(?i)proceed\?`),
    regexp.MustCompile(`(?i)permission`),
    regexp.MustCompile(`(?i)approve`),
    regexp.MustCompile(`(?i)allow this`),
    regexp.MustCompile(`(?i)do you want to`),
    regexp.MustCompile(`(?i)waiting for`),
    regexp.MustCompile(`(?i)press enter`),
}

DefaultStallTimeout is the default ceiling on how long superviseAgent will wait for output from an agent before treating it as stalled and killing it. Individual callers may override via WatchdogConfig.StallTimeout. It is a var (not a const) solely so tests can shrink it to keep watchdog tests fast without threading a StallTimeout override through every runner call site; production code should never reassign it outside of tests.

var DefaultStallTimeout = 3 * time.Minute

ErrAgentBlocked is returned by superviseAgent (and wrapped with the triggering reason) when the control invariant fires: the agent produced a signal (an interactive prompt or a stall) that means it can no longer be trusted to make forward progress non-interactively. superviseAgent ALWAYS kills the agent before returning this error — callers must never wait on it themselves.

var ErrAgentBlocked = errors.New("provider: agent blocked (killed by watchdog)")

ErrStreamJSONLineTooLong reports that a stream-json provider emitted a single frame larger than declarativeStreamJSONLineMax (16MiB). The turn is failed (and retried) rather than completed: the CLI was killed mid-stream, so any text parsed before the oversized frame is PARTIAL, and reporting it as a successful turn would let the judgment-only acceptance check (mechanicalAcceptanceCheck: non-empty output ⇒ done) mark a step complete on the strength of a forcibly-terminated worker. That partial text is discarded entirely — it reaches neither AssistantOutput nor rawOutput — so a killed turn can never satisfy verification.

var ErrStreamJSONLineTooLong = errors.New("provider: stream-json line exceeded 16MiB limit")

func DefaultWatchdogConfig#

func DefaultWatchdogConfig() agent.WatchdogConfig

DefaultWatchdogConfig returns a WatchdogConfig seeded with DefaultStallTimeout and DefaultPromptPatterns. Runners call this (rather than constructing agent.WatchdogConfig{} directly) so every provider gets the same baseline prompt/stall detection unless a caller has a reason to override it. Use this ONLY for providers whose output is free-form pane text where a raw interactive prompt could actually appear (see StreamJSONWatchdogConfig for the structured-output case).

func StreamJSONWatchdogConfig#

func StreamJSONWatchdogConfig() agent.WatchdogConfig

StreamJSONWatchdogConfig is the watchdog config for providers driven in a structured stream-json mode (claude/opencode: `–output-format stream-json`). Their normal output is JSON frames whose text can innocently contain prompt-like words (“permission”, “continue?”), which content-blind matching would misread and KILL a valid turn. It keeps the prompt patterns but sets SkipPromptMatchOnJSONLines: patterns are matched ONLY on lines that are NOT valid JSON, so a legitimate JSON frame is never a false prompt while a GENUINE raw interactive prompt (never valid JSON) is still caught immediately — not merely by the slower stall timeout.

func ValidateBinding#

func ValidateBinding(binding Binding) error

ValidateBinding validates the parts of a binding that can be checked without spawning a provider turn.

type Binding#

Binding is one resolved provider selection after repo config, local overrides, and per-variant overrides have been applied.

type Binding struct {
    Name   string
    Config BindingConfig

    // BinaryFromLocal is true when Config.Binary was set by the gitignored
    // local.toml provider_binary override rather than by committed
    // config.toml. Committed config may only name a shipped provider
    // binary (claude/codex); an arbitrary binary must come from
    // local.toml, so a pull request cannot point the runtime at
    // /bin/sh. ValidateBinding enforces this.
    BinaryFromLocal bool
}

func ResolveBinding#

func ResolveBinding(cfg File, local Local, fromConfig VariantFile) (Binding, error)

ResolveBinding picks the provider for one variant.

type BindingConfig#

BindingConfig is one provider’s capability record: what binary to run, how to invoke it non-interactively, how to read back its structured result and resume a session, and whether it can fan out work itself.

This is NOT a persona/variant — variants are removed entirely per spec §10. A BindingConfig describes only what the underlying CLI/API can do.

type BindingConfig struct {
    Type   string   `toml:"type"`
    Bin    string   `toml:"bin"`
    Binary string   `toml:"binary"`
    Args   []string `toml:"args"`

    // OutputFile is a declarative-runner args token target (see
    // declarative.go); it is unrelated to the agent.Options.ResultPath
    // hybrid-I/O path used by the claude/codex/opencode runners.
    OutputFile string `toml:"output_file"`

    TurnTimeout    string `toml:"turn_timeout"`
    MaxRetries     int    `toml:"max_retries"`
    SessionIDRegex string `toml:"session_id_regex"`

    HaikuModel  string `toml:"haiku_model"`
    SonnetModel string `toml:"sonnet_model"`
    OpusModel   string `toml:"opus_model"`

    LowEffort    string `toml:"low_effort"`
    MediumEffort string `toml:"medium_effort"`
    HighEffort   string `toml:"high_effort"`
    MaxEffort    string `toml:"max_effort"`

    SupportsResume        *bool `toml:"supports_resume"`
    UseAppendSystemPrompt *bool `toml:"use_append_system_prompt"`

    // NativeFanout is true when the bound CLI/API can itself fan out
    // subagents, workflows, or parallel work — spec §9/§10: the
    // orchestrator uses this to decide whether to delegate a parallel
    // step-group to one fan-out-capable agent rather than spawning N
    // Ralph-managed workers. Evidence per provider is documented next to
    // each Default*Provider constructor below. Providers with unverified
    // or absent fan-out support default to false — the flag must never
    // be optimistically set.
    NativeFanout bool `toml:"native_fanout"`
}

type ClaudeRunner#

ClaudeRunner executes a single `claude -p` turn under Ralph’s own pty via internal/agent, per spec §2/§3: Ralph owns the pty (agent.Start), the pane/output stream is for human/watchdog observation, and the structured result is read back from a file Ralph passes to the CLI — never scraped from the rendered pane.

claude has no native “write result to a file” flag (verified against `claude –help` on the installed 2.1.211 CLI: –output-format json/stream-json both write to stdout only). So the ResultPath file here is Ralph-side, not CLI-native: the runner tees every stdout line (which IS the stream-json frames — the same content a human pane would show) into req’s ResultPath file as it arrives, then parses that file’s accumulated content for the terminal result frame. This keeps the “never scrape the rendered pane for data” invariant: ResultPath holds the same raw JSON lines the CLI emitted, not a re-rendered terminal.

type ClaudeRunner struct{}

func (ClaudeRunner) Run#

func (ClaudeRunner) Run(ctx context.Context, binding Binding, req Request) (Result, error)

Run spawns `claude -p –input-format stream-json –output-format stream-json` under agent.Start, feeds req.UserPrompt on stdin via a one-shot input file (claude in –input-format stream-json mode reads a JSON-line user message from stdin), tees stdout into a ResultPath file, and parses the terminal result frame from that file for Usage.

type CodexRunner#

CodexRunner executes a single `codex exec` turn.

codex, like claude/opencode, now runs under Ralph’s own pty via internal/agent so its pane/stream goes through the same superviseAgent-enforced watchdog (spec §1’s never-block control invariant): a stalled or (despite –dangerously-bypass-approvals-and-sandbox) unexpectedly interactive codex process is killed rather than left to hang, exactly like claude/opencode. codex’s own native result channel — the –output-last-message file — is unaffected: it is still the authoritative source for AssistantOutput, read back from disk after the process exits. The pty pane output itself carries no structured result for codex (unlike claude/opencode’s stream-json), so onLine here has nothing to parse; it exists purely so superviseAgent’s watchdog has output to observe for stall/prompt detection.

type CodexRunner struct{}

func (CodexRunner) Run#

func (CodexRunner) Run(ctx context.Context, binding Binding, req Request) (Result, error)

Run executes one non-interactive Codex turn.

type DeclarativeRunner#

DeclarativeRunner executes a config-defined provider binding. It supports a small set of framing modes that cover the common provider CLI shapes without requiring a custom Go runner.

type DeclarativeRunner struct{}

func (DeclarativeRunner) Run#

func (DeclarativeRunner) Run(ctx context.Context, binding Binding, req Request) (Result, error)

Run executes one declarative provider turn.

type File#

File is the provider package’s own minimal config surface: enough for ResolveBinding to read DefaultProvider and look up a named provider’s BindingConfig. A later phase may replace this with a direct internal/vconfig-backed decode; the shape here matches what committed config.toml historically expressed for the equivalent keys.

type File struct {
    DefaultProvider string
    Providers       map[string]BindingConfig
}

type Local#

Local is the provider package’s local-override surface: just enough for ResolveBinding’s local-binary-override lookup (the gitignored local.toml escape hatch for pointing a provider at a non-shipped binary).

type Local struct {
    ProviderBinary   string
    ProviderBinaries map[string]string
}

func (Local) BinaryFor#

func (l Local) BinaryFor(providerName string) (string, bool)

BinaryFor resolves the local-override binary for providerName. A per-provider override takes precedence over the single legacy ProviderBinary field.

type Model#

Model is a provider-neutral model-tier selector.

type Model string

Model tiers. Values match the retired variant.Model constants so existing provider config (haiku_model/sonnet_model/opus_model keys) keeps meaning the same thing.

const (
    ModelHaiku  Model = "haiku"
    ModelSonnet Model = "sonnet"
    ModelOpus   Model = "opus"
)

type OpencodeRunner#

OpencodeRunner executes a single `opencode run –format json` turn under Ralph’s own pty via internal/agent, per spec §9 (“opencode bound via its local `run` path only”) and §3 (hybrid I/O).

Verified against the installed `opencode` 1.18.3 CLI on 2026-07-16: `opencode run [message..] –format json` emits one JSON object per line on stdout (never a file — there is no output-file flag), each with a top-level “type”: “step_start” | “text” | “step_finish” | others. The assistant reply lives in `type”:”text”` frames’ part.text; token/cost usage lives in the `type”:”step_finish”` frame’s part.tokens (input/output/cache.read) and part.cost. `–session`/`–continue` resumes a session, `–variant` sets reasoning effort, `–dir` sets the working directory, `–model` takes `provider/model`.

Like ClaudeRunner, there is no CLI-native result-file flag, so ResultPath is Ralph-side: the runner tees recognized JSON frames from the pty’s Output() into the ResultPath file as they arrive, then parses the accumulated file for the terminal step_finish frame’s usage.

type OpencodeRunner struct{}

func (OpencodeRunner) Run#

func (OpencodeRunner) Run(ctx context.Context, binding Binding, req Request) (Result, error)

Run spawns `opencode run <prompt> –format json` and blocks until the step_finish frame (or process exit) closes the turn.

type Request#

Request is the provider-neutral execution contract for one worker turn.

type Request struct {
    WorkingDir   string
    SystemPrompt string
    UserPrompt   string
    OutputSchema string
    Model        Model
    Effort       string
    AllowedTools []string
}

type Result#

Result captures the observable output of one provider turn.

type Result struct {
    SessionID       string
    AssistantOutput string
    Usage           Usage
}

type Runner#

Runner executes one provider turn.

type Runner interface {
    Run(ctx context.Context, binding Binding, req Request) (Result, error)
}

func NewRunner#

func NewRunner(binding Binding) (Runner, error)

NewRunner returns the runtime implementation for a provider type.

type Usage#

Usage captures the token/cost accounting for one provider turn. Fields are zero when the provider does not report them. Coverage today: the claude runner populates Usage from the stream-json result frame; codex and declarative bindings report zero (their CLIs surface usage differently and are not yet parsed). CostUSD is authoritative when non-zero; the runtime accumulates it for spend-cap enforcement, so a capped variant on an unreported provider still requires a cap value but its cost is not yet metered. Extending codex parsing is the follow-up to close that gap.

type Usage struct {
    InputTokens       int
    OutputTokens      int
    CachedInputTokens int
    CostUSD           float64
}

type VariantFile#

VariantFile is the provider package’s per-binding-request input — despite the name (kept for config-key compatibility with existing committed config.toml files), it carries no persona: it is just the provider override for one binding request.

type VariantFile struct {
    Provider string
}

Generated by gomarkdoc