adapter

package
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 9, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package adapter bridges brigada and each agent CLI. One adapter per CLI: it builds the headless command (fresh or resume), parses the CLI's event stream into progress events, detects limit-hit and auth errors, and finds the session id needed to resume a session. It never modifies the CLI and never holds credentials itself; the user signs in through the vendor's own flow.

Every adapter implements the interface whole, both parse methods included: ParseEvent reads the CLI's stdout and ParseErrEvent its stderr, because some failures print only there — a resume that cannot be found before any JSON, a limit or an auth message — and must still become error events. A line with nothing to act on returns false from either method; only the CLI's own errors are errors.

Index

Constants

View Source
const (
	TierLight    = "light"
	TierStandard = "standard"
	TierHeavy    = "heavy"
)

Task tiers describe how heavy a model a task needs.

Variables

View Source
var ResumeCapableHarnesses = map[string]bool{
	"claude":   true,
	"codex":    true,
	"opencode": true,

	"fake": true,
}

ResumeCapableHarnesses is the set of harnesses a foreman may run on (design §4.1): the foreman's turns wake headless and resume the project's session, so choosing a foreman refuses a harness whose CLI cannot resume headless and carry the foreman's MCP bridge. This is the one place that set lives; the engine consults it at setup.

View Source
var TaskEffortLevels = []string{"low", "medium", "high", "max"}

TaskEffortLevels is brigada's task vocabulary, independent of harness levels.

Functions

func AgentFamily

func AgentFamily(harness, model string) (string, error)

AgentFamily uses the model's family, including known CLI defaults. OpenCode's default can serve any provider, so its family is unknown (empty, without error). Kimi serves Moonshot models unless the name identifies another known family. For other unknown aliases it returns the historical harness fallback and an error: standalone runners support custom aliases, while init requires known models.

func AgentTier

func AgentTier(harness, model string) string

AgentTier uses the model's table tier. CLI defaults (codex, kimi and opencode) are standard because their model name is not known to brigada.

func CredentialEnv

func CredentialEnv(harness string) string

CredentialEnv names the environment variable a harness's credential comes from, or "" when the harness logs in some other way (a stored login, an OAuth session). A runner started without the variable cannot authenticate that harness, which the needs-you line for an auth break says.

func EffortForHarness

func EffortForHarness(harness, taskEffort string) string

EffortForHarness maps a task's brigada effort level to the CLI's control. An empty result leaves the CLI variant or effort unspecified.

func EffortLevels

func EffortLevels(harness string) []string

EffortLevels returns the effort levels brigada accepts for harness; nil when there is no fixed list (OpenCode), or no effort control (Kimi).

func IsInfraText

func IsInfraText(text string) bool

IsInfraText reports whether a failure's text names a connection or DNS failure. The words alone prove nothing about when they were said: the runner keeps the infra kind only for a run that produced no output before it.

func IsModelRejectedText

func IsModelRejectedText(text string) bool

IsModelRejectedText reports whether a failure's text says the provider rejected the model: unsupported for the account, or nonexistent.

func ModelFamily

func ModelFamily(model string) (string, error)

ModelFamily identifies a model independently of the CLI/provider serving it. Unknown aliases need an explicit model name in init, rather than a guessed family.

func ModelTier

func ModelTier(model string) string

ModelTier labels a model by its name. Heavy matches take precedence; names without a matching segment serve the standard tier.

func ParseToolScope

func ParseToolScope(value string) ([]string, error)

ParseToolScope parses a Claude worker tool scope in its flag, config and project-file spelling: Claude Code tool patterns separated by "+", or "none" for the file tools alone. The result is non-nil for none, so a caller can tell an explicit none from no scope at all. A pattern containing a comma cannot ride an --agent value, which splits on commas.

func ProgressEvents

func ProgressEvents(events []Event) []string

ProgressEvents renders the last few progress events for a heartbeat: one line each, newest last, so the engine's record of a run names what the worker was doing, not just when.

func ResumeCapable

func ResumeCapable(harness string) bool

ResumeCapable reports whether harness's CLI can resume a session headless (§4.1), which the foreman's harness must.

func ToolArgs

func ToolArgs(raw json.RawMessage) string

ToolArgs renders a tool call's arguments for an Event's Args: the raw JSON the CLI's stream carried, compacted so two identical calls read identical whatever whitespace the stream put between them. Empty input, or input that is not JSON, renders empty — no arguments identifiable, nothing to compare.

func ValidateEffort

func ValidateEffort(harness, level string) error

ValidateEffort checks level against harness's accepted set. The empty level (unspecified) is always valid. The error is a usage-grade message naming what is accepted.

func ValidateTaskEffort

func ValidateTaskEffort(level string) error

ValidateTaskEffort accepts an unset effort, which uses the agent's level.

func ValidateTier

func ValidateTier(tier string) error

ValidateTier accepts an unset tier, which means standard.

Types

type Adapter

type Adapter interface {
	// Name is the adapter's identifier, one per CLI ("claude", "kimi",
	// "fake"): the name a runner's advertisement pairs it with (§5.1)
	// and the agents table keys on (§11).
	Name() string

	// Command builds the headless command for job, to run in workdir.
	// resumeSessionID is empty for a fresh run; otherwise the command
	// resumes that harness session with its context kept (§5.3).
	//
	// The environment is the adapter's to build, under one policy: the
	// worker's own login state is its business — the ambient
	// environment, the vendor CLI's tokens and configuration, are what
	// it needs to run (§5.3) — but the adapter
	// must not add brigada's own secrets to it. The runner enforces the
	// line from its side: whatever variables its git credentials ride
	// (Config.GitEnv) are stripped from the command's environment
	// before it starts, whether the adapter put them there or they came
	// from the ambient set.
	Command(job Job, workdir, resumeSessionID string) (*exec.Cmd, error)

	// ParseEvent parses one line of the CLI's JSON event stream. false
	// means the line is not an event the runner tracks — a banner, a
	// blank line, anything the CLI prints that is not one of its
	// events. Only the CLI's own error event is an error: a stray line
	// on its stdout must not fail an otherwise good run.
	ParseEvent(line []byte) (Event, bool)

	// ParseErrEvent parses one line of the CLI's stderr, which rides
	// the same event pipeline as stdout: some failures print only there
	// — a resume-not-found before any JSON, a limit or an auth message
	// — and must become error events all the same. false means the line
	// carries nothing to act on, like a progress note or a warning.
	ParseErrEvent(line []byte) (Event, bool)

	// ResultText returns the worker's final message from the events it
	// parsed: the text the result contract (§5.4) is read from.
	ResultText(events []Event) string

	// ProbeCommand builds the cheap probe of the CLI for the
	// broken-agent recovery (§8 "Health"): a command bound to ctx — a
	// version or status check — that exits 0 when the CLI answers. The
	// runner runs it against an agent broken by repeated failures with a
	// short deadline; a passing probe clears the agent, a failing one
	// backs the next probe off. The command is built with ctx so the
	// deadline kills it: a hung CLI reads as a failing probe, never a
	// stuck runner.
	ProbeCommand(ctx context.Context) *exec.Cmd
}

Adapter is what one CLI adapter provides (design §5.2): it builds the headless command for a job, parses the CLI's event stream into progress events, and finds the final message and the session id in what the CLI printed. An adapter never modifies the CLI and never holds credentials itself.

type ErrorKind

type ErrorKind string

ErrorKind sorts an error event: a quota limit hit (the account cools and the job moves to another agent), an authentication failure (the agent is broken until a probe passes), a resume that could not happen (the session is gone or forked; §5.6 restarts fresh in place), and everything else.

const (
	ErrLimit ErrorKind = "limit"
	ErrAuth  ErrorKind = "auth"
	// ErrResumeFailed is a resume that could not happen: the session the
	// command was told to resume is gone or its conversation cannot be
	// continued. What answers it is not another resume but a fresh run in
	// place, carrying the previous attempt's context in its prompt (§5.6) — a session cannot be rebuilt from its id alone.
	ErrResumeFailed ErrorKind = "resume_failed"
	ErrOther        ErrorKind = "other"
	// ErrInfra is the CLI failing to reach its provider at all: the
	// connection or the name lookup failed before the run produced
	// anything. The environment's failure, never the agent's: the job is
	// retried as a transient error and no health strike is charged.
	ErrInfra ErrorKind = "infra"
	// ErrModel is the provider rejecting the model itself: the account
	// has no access to it, or it does not exist (a 400
	// invalid_request_error). The agent cannot work until the user
	// fixes its model or the account, so it stays out like an auth break
	// and no probe clears it.
	ErrModel ErrorKind = "model"
	// ErrOutputLimit is a run cut off at its output token limit: the
	// model stopped for length, not for lack of work. It is not a quota
	// limit (ErrLimit) and cools no account; the runner raises it only
	// when the cut-off left the run without a result.
	ErrOutputLimit ErrorKind = "output_limit"
)

type Event

type Event struct {
	Usage *Usage
	Kind  EventKind
	Time  time.Time // when the adapter parsed it; the runner's clock
	Text  string
	// Args is a tool call's arguments as the CLI's stream carried them,
	// empty for a CLI that prints none (or a kind that has none): with
	// Text it names the call, so the runner's loop watch (§5.6) tells
	// two calls of one tool apart.
	Args string
	// SessionID may accompany any kind when the CLI puts its id in
	// every envelope (including an error before a session-start event).
	SessionID string
	ErrorKind ErrorKind
	// LimitUntil is a limit error's reset time, RFC3339, when the CLI's
	// text carried one machine-readably; empty when unknown (§4.4).
	LimitUntil string
	// Final marks an error event as the CLI's closing verdict on the run
	// (codex's turn.failed): it names the failure over any earlier error
	// event of the same stream.
	Final bool
	// OutputLimit rides a usage event whose model step ended at the
	// output token limit. A later usage event without it clears it, so
	// only the run's last step counts.
	OutputLimit *OutputLimit
	// Notice marks an error event that is only a warning while the
	// turn has not started (codex's startup notices): the runner, which
	// tracks each job's turn through TurnMarker, downgrades it to a
	// warning until then.
	Notice bool
}

Event is one parsed line of the CLI's event stream. Which fields carry data depends on Kind: tool calls fill Text (the tool name), a message fills Text, a session id fills SessionID, an error fills ErrorKind and Text.

func (Event) IsProgress

func (e Event) IsProgress() bool

IsProgress reports whether the event is progress in the sense of §5.6: a tool call starting or ending, or a message. Sessions and errors are not progress.

type EventKind

type EventKind string

EventKind is the shape of one progress event (§5.6): what the runner tracks for heartbeats and, later, stall detection.

const (
	// EvToolStart is a tool call beginning; one in flight suspends the
	// stall clock (§5.6).
	EvToolStart EventKind = "tool_start"
	// EvUsage reports counters without advancing the progress clock.
	EvUsage EventKind = "usage"
	// EvToolEnd is a tool call finishing.
	EvToolEnd EventKind = "tool_end"
	// EvMessage is an assistant message.
	EvMessage EventKind = "message"
	// EvSession carries the harness's own session id, written back on
	// complete so the next session-mode job resumes it (§4.2).
	EvSession EventKind = "session"
	// EvError is the CLI reporting a failure whose kind sorts what the
	// engine does with it (§8: quota, auth, the rest).
	EvError EventKind = "error"
	// EvWarning is a CLI notice that is not a failure: the run goes on,
	// and the runner does not take it for the run's error.
	EvWarning EventKind = "warning"
)

type Interactive

type Interactive interface {
	// AttachCommand builds the interactive command for job, to run in
	// workdir on the caller's terminal: the adapter wires none of the
	// process's streams — stdin is the user's, and the caller connects
	// it. The opening prompt (job.Prompt) reaches the CLI the way it
	// takes one interactively; the MCP config and the model ride as in
	// Command. resumeSessionID is the key's current harness session,
	// empty when the foreman has never run; newSessionID is the id the
	// client generated for the session this chat forks into — the CLI
	// names it itself, so the client knows what to write back on detach
	// (§4.2) without reading the CLI's own files.
	AttachCommand(job Job, workdir, resumeSessionID, newSessionID string) (*exec.Cmd, error)
}

Interactive is the optional adapter interface for the attach (§4.2 item 1): an adapter whose CLI can carry an interactive terminal session — the user's own chat with the foreman, not a headless run — implements it beside Adapter. The client asserts it when it attaches and builds the command through it; a harness whose adapter does not implement it cannot be attached to, and the client says so rather than falling back to a headless run, which is no chat at all.

type Job

type Job struct {
	Prompt string
	Model  string
	// Review selects the harness's read-only review policy, where supported.
	Review bool
	// Effort is the reasoning-effort level for the run, one of the harness's
	// accepted levels (ValidateEffort); empty adds nothing. An adapter whose
	// CLI has no effort control refuses a non-empty one.
	Effort string
	// MCPConfig names the harness's MCP config for the run: a config
	// file's path, offered to the CLI through its own flag. It is where
	// the `brigada mcp` bridge (§12.2) will hand the foreman's harness
	// brigada's tool server; empty — every job today — adds nothing.
	MCPConfig string
	// ReadDirs names absolute directories outside the worktree the
	// worker must be able to read but never write: the extra
	// repositories' checkouts (§5.8). The adapter opens them to the
	// worker as its CLI allows, and the checkouts' read-only files
	// refuse writes all the same; codex's and dsh's sandboxes already
	// allow reads outside the workspace, and kimi has no directory
	// flag, so those adapters ignore it. Empty adds nothing.
	ReadDirs []string
	// Tools is the worker's tool scope in Claude's spelling
	// (`Bash(go test:*)`), for a harness that maps it onto its own
	// permission rules; nil is the Go default scope. Claude takes its
	// scope from its adapter's config instead and ignores this.
	Tools []string
	// StatusLine is a shell command whose one line of output an
	// interactive session shows in its status bar, re-run every few
	// seconds (§10): the attach sets it to `brigada status --line`, so
	// the user sees what the workers are doing while they chat with the
	// foreman. Only AttachCommand reads it, and an adapter whose CLI has
	// no status bar ignores it; empty adds nothing.
	StatusLine string
}

Job is what an adapter needs to build one headless run (§5.2): the composed prompt and the model to run on. The prompt is the engine's product — a fixed, versioned template plus verbatim data (§1.4) — and the adapter passes it through untouched.

type OutputLimit added in v1.1.0

type OutputLimit struct {
	OutputTokens    int64
	ReasoningTokens int64
}

OutputLimit is the spend of a model step cut off at its output limit.

type SessionWriteback

type SessionWriteback interface {
	// WritebackSessionID names the session id an ended attach leaves the
	// key on, bound to ctx for the discovery a fresh chat needs.
	WritebackSessionID(ctx context.Context, workdir, resumeSessionID string) (string, error)
}

SessionWriteback is the optional adapter interface for a CLI whose interactive resume keeps its own session id — one the client cannot mint for it the way claude's --session-id takes a caller's id: the release of the attach writes back the id this returns instead of the id the client minted. resumeSessionID is the id the chat resumed — returned unchanged, for the CLI continues under it — and "" means the chat started fresh, its id discovered from the CLI's own session list in workdir, which is why the caller runs this after the CLI exits. An error is the caller's to warn about and answer with the minted id, which the CLI does not know: its next headless turn then refuses the resume and starts fresh in place (§5.6).

type ShellScoped added in v1.1.0

type ShellScoped interface {
	// ShellCommands reports the commands a work or fix job's shell may
	// run, in scope order, as the command prefixes the scope names
	// (`go test` for `Bash(go test:*)`). tools is Job.Tools. An empty
	// list with whole false means no shell at all; whole true means the
	// scope names the whole shell, which refuses nothing.
	ShellCommands(tools []string) (commands []string, whole bool)
}

ShellScoped is the optional adapter interface for a harness whose permission rules limit a worker's shell to named commands: the runner asks it what the scope in force is, so the worker's prompt can say so. codex, kimi and dsh run in OS sandboxes, implement nothing, and keep their prompt unchanged.

type TurnMarker

type TurnMarker interface {
	// StartsTurn reports whether the stdout line opens the turn.
	StartsTurn(line []byte) bool
}

TurnMarker is the optional adapter interface for a CLI whose stream opens the turn with a line of its own. The adapter itself is shared by concurrent jobs and keeps no stream state; the runner asks it of each stdout line and keeps the job's turn.

type Usage

type Usage struct {
	InputTokens       *int64   `json:"input_tokens,omitempty"`
	OutputTokens      *int64   `json:"output_tokens,omitempty"`
	CachedInputTokens *int64   `json:"cached_input_tokens,omitempty"`
	ReasoningTokens   *int64   `json:"reasoning_tokens,omitempty"`
	CostUSD           *float64 `json:"cost_usd,omitempty"`
	ModelCalls        *int64   `json:"model_calls,omitempty"`
	// CostCumulative marks CostUSD as the running total of a resumable
	// session, not this invocation's own spend; the engine turns it into
	// a per-job delta. Unset for adapters whose cost is per invocation.
	CostCumulative bool `json:"cost_cumulative,omitempty"`
}

Usage contains only counters reported by this invocation. Nil fields are unknown, including on older runners; a reported zero remains known. InputTokens is all input, cache reads included; cached input and reasoning tokens are breakdowns of input and output. ModelCalls counts the model requests the CLI reported, the unit of request-limited plans.

func Calls

func Calls(n int64) *Usage

Calls is a Usage that only counts n model requests.

func ParseUsage

func ParseUsage(raw json.RawMessage, input, output, cached, reasoning, cost string) *Usage

ParseUsage reads selected numeric fields independently. Malformed, negative and missing counters are ignored without discarding the enclosing event.

func (*Usage) Add

func (u *Usage) Add(v *Usage)

Add sums only known fields, preserving unknown versus zero.

Directories

Path Synopsis
Package claude drives the Claude Code CLI headless (design §5.2): one run per job, `claude -p` with stream-json output, resumed by session id when the job carries one.
Package claude drives the Claude Code CLI headless (design §5.2): one run per job, `claude -p` with stream-json output, resumed by session id when the job carries one.
Package codex drives the Codex CLI headless (design §5.2): one run per job, `codex exec --json`, resumed by thread id when the job carries one.
Package codex drives the Codex CLI headless (design §5.2): one run per job, `codex exec --json`, resumed by thread id when the job carries one.
Package dsh drives DeepSeek Harness (npm @deepseek-ai/dsh, verified on 0.2.1-rc.2) headless: one `dsh --profile headless --json` per worker job, the prompt on stdin, resumed by session id.
Package dsh drives DeepSeek Harness (npm @deepseek-ai/dsh, verified on 0.2.1-rc.2) headless: one `dsh --profile headless --json` per worker job, the prompt on stdin, resumed by session id.
Package fake is a scripted CLI adapter for tests (design §5.2): it drives a fake worker — a shell script each test writes — instead of a real agent CLI, so the runner's tests exercise the lease loop, the workspace, the result contract and the check without any vendor CLI.
Package fake is a scripted CLI adapter for tests (design §5.2): it drives a fake worker — a shell script each test writes — instead of a real agent CLI, so the runner's tests exercise the lease loop, the workspace, the result contract and the check without any vendor CLI.
Package kimi drives the Kimi Code CLI headless (design §5.2): one run per job, `kimi -p` with stream-json output, resumed by session id when the job carries one.
Package kimi drives the Kimi Code CLI headless (design §5.2): one run per job, `kimi -p` with stream-json output, resumed by session id when the job carries one.
Package opencode drives OpenCode 1.19.5 headless: one `opencode run` per worker job, resumed by session id, and the bare TUI for an attached foreman chat.
Package opencode drives OpenCode 1.19.5 headless: one `opencode run` per worker job, resumed by session id, and the bare TUI for an attached foreman chat.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL