Provider contract#
Each provider is a capability record, not a persona: what binary to
invoke, how to run it non-interactively, how to read back its structured
result and usage/cost, how it resumes, and whether it natively fans out
subagents. Shipped providers — claude, codex, opencode — each
implement the same Runner interface (internal/provider/provider.go).
Contract interface#
type Runner interface {
Run(ctx context.Context, binding Binding, req Request) (Result, error)
}
type Request struct {
WorkingDir string
SystemPrompt string
UserPrompt string
OutputSchema string
Model Model
Effort string
AllowedTools []string
}
type Result struct {
SessionID string
AssistantOutput string
Usage Usage // token/cost accounting; zero when unreported
}
One Runner.Run call is one turn. The runtime gives the runner a
fully-resolved request; the runner invokes the provider CLI under its own
pty, captures output, and returns the assistant’s output plus an optional
session ID for resume and a best-effort Usage (tokens + CostUSD). The
orchestrator accumulates Usage.CostUSD per provider to enforce spend
caps.
Capability record#
type BindingConfig struct {
Type, Bin, Binary string
Args []string
SupportsResume *bool
NativeFanout bool
// ...model/effort tier overrides
}
NativeFanout is the flag the orchestrator uses to decide whether a
parallel step-group should be delegated to one fan-out-capable agent
invocation rather than spawned as N Ralph-managed workers:
Provider |
NativeFanout |
Evidence |
|---|---|---|
|
true |
|
|
false (unconfirmed) |
|
|
true |
|
Stateful vs. stateless#
Provider |
State model |
Binding |
|---|---|---|
|
Stateful — session resume via |
|
|
Stateless — each turn is independent |
|
|
Stateless in v1 |
|
A stateful binding threads Result.SessionID into the next Request so
the provider reuses its own conversation context; stateless bindings
ignore it.
Resolution and validation#
ResolveBinding picks a provider by name, falling back to the built-in
capability record for claude/codex/opencode when no explicit
override is configured, and defaulting to claude when nothing is
specified. NewRunner maps the resolved binding’s Type to a concrete
Runner implementation. An unknown provider type fails loudly rather
than silently defaulting.
Only a shipped binary name (claude, codex, opencode) may be named
by a binding sourced from shared config; any other binary — a custom
declarative CLI, an absolute path, a wrapper — must come from an
operator-local override, never from something another party could hand
the supervisor. agy/Antigravity was evaluated and found to route
through a cloud control surface (cloudcode-pa.googleapis.com), so no
runner is registered for it — see Declarative provider
bindings for CLIs that don’t ship a
hand-written Go runner.
Adding a new built-in provider#
Create
internal/provider/<name>.goimplementingRunner.Register it in
NewRunner’s switch andbuiltInProvider.Add its capability record (
default<Name>Provider) with evidence forNativeFanout.Add a doctor check in
internal/doctor/checks.go.Document its state model + argv shape here.