eval

package
v0.1.0-rc.2 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: Apache-2.0 Imports: 30 Imported by: 0

Documentation

Overview

Package eval is Halos's offline replay harness: it runs a suite of coding tasks against harness variants (CLI version x model x settings) in isolated environments, scores each run with the task's own success check, and renders a scorecard with bootstrap CIs and a paired comparison.

Task format (evals/tasks/<name>/task.yaml) and rough Harbor mapping:

id          -> Harbor task name
repo        -> environment/ (Dockerfile COPY/clone of the project under test)
setup       -> environment build/setup steps
prompt      -> instruction.md
check       -> tests/test.sh  (exit 0 = pass; we do not read a reward file)
graders     -> extra pluggable graders (command, file, diff, judge); see grade.go
timeout     -> [agent] timeout
budget_usd  -> no Harbor equivalent; passed to the CLI's spend cap
tags        -> metadata tags

Index

Constants

View Source
const (
	GateShip  = "ship"  // candidate is non-inferior on every gated metric
	GateHold  = "hold"  // inconclusive: rerun with more trials or tasks
	GateBlock = "block" // candidate is confidently worse
)

Regression verdicts of the gate, worst last.

View Source
const (
	MetricPass      = "pass_rate"
	MetricCost      = "cost_usd"
	MetricTokens    = "tokens"
	MetricWall      = "wall_ms"
	MetricToolCalls = "tool_calls"
)

Metric keys of Comparison.Deltas.

View Source
const (
	GraderCommand = "command" // run exits 0
	GraderFile    = "file"    // assertions on one file's existence/content
	GraderDiff    = "diff"    // assertions on which files the agent changed
	GraderJudge   = "judge"   // LLM-as-judge rubric over the prompt and the change
)

Grader types.

View Source
const (
	WireAnthropic = "anthropic-messages"
	WireOpenAI    = "openai-responses"
)

Judge wire protocols (names match policy Gateway.Protocols).

View Source
const (
	MetricJudgeScore   = "halo.eval.judge.score"    // gauge per pair and arm (halo.unit = pair id), 0..1
	MetricJudgeDelta   = "halo.eval.judge.delta"    // gauge, candidate - control
	MetricJudgeWinRate = "halo.eval.judge.win_rate" // gauge
	MetricPairsGraded  = "halo.eval.pairs.graded"   // sum (delta)
	MetricJudgeErrors  = "halo.eval.judge.errors"   // sum (delta)
)

Online eval metric names (halo.* schema, see internal/telemetry).

View Source
const (
	VerdictBetter = "better"
	VerdictWorse  = "worse"
	VerdictNoDiff = "no_significant_difference"
)

Verdicts for a paired comparison against control.

View Source
const DefaultTimeout = 10 * time.Minute

DefaultTimeout applies when a task sets none.

Variables

View Source
var DefaultSandboxProbes = map[string][]string{"codex": {"codex", "sandbox", "linux", "--full-auto", "true"}}

DefaultSandboxProbes: codex runs `true` under the same workspace-write (Landlock + seccomp) sandbox `codex exec --sandbox workspace-write` uses. Never danger-full-access (AGENTS.md invariant 1): a host that cannot sandbox codex is reported, not worked around. Verified against codex-cli 0.99.0 without Landlock (Docker Desktop): exit 101, "Sandbox(LandlockRestrict)" (testdata/codex/sandbox-probe-no-landlock.txt). The exit-0 case on a Landlock host is not yet recorded.

View Source
var ErrJudgeSchema = errors.New("judge reply failed schema validation")

ErrJudgeSchema marks a judge reply that failed strict parsing: a grader error, never a pass.

View Source
var ErrSandboxUnavailable = errors.New("sandbox unavailable (Landlock)")

ErrSandboxUnavailable marks a cell the host cannot measure: the harness's own sandbox does not work here (codex workspace-write needs Landlock, which e.g. Docker Desktop's linuxkit kernel lacks). Such trials are never run and never scored as failures; the gate holds instead (see Trial.Unavailable).

Functions

func AppendHistory

func AppendHistory(path string, entries ...HistoryEntry) error

AppendHistory appends entries to the JSONL file at path (created 0644).

func CheckSecureURL

func CheckSecureURL(raw string) error

CheckSecureURL requires https, allowing plain http only to a loopback host (local gateways, tests): credentials must never cross a network in clear.

func Drivers

func Drivers() map[string]Driver

Drivers returns the built-in drivers keyed by Variant.Harness.

func GradedPairs

func GradedPairs(path string) (map[string]bool, error)

GradedPairs returns the pair ids already recorded in the history file (missing file = none).

func OnlineTable

func OnlineTable(rs []OnlineResult) string

OnlineTable renders online results as a compact terminal table.

Types

type ClaudeDriver

type ClaudeDriver struct{}

ClaudeDriver runs `claude -p` with stream-json output.

func (ClaudeDriver) Command

func (ClaudeDriver) Command(t *Task, v Variant) Command

Command: verified against claude 2.1.280 in test/uat (`halo eval run` with the DockerRunner): --restricted keeps Write/Edit, stream-json needs --verbose.

func (ClaudeDriver) Name

func (ClaudeDriver) Name() string

func (ClaudeDriver) Parse

func (ClaudeDriver) Parse(out []byte) (Usage, error)

Parse reads the final `result` message of the stream.

func (ClaudeDriver) SettingsPath

func (ClaudeDriver) SettingsPath() string

SettingsPath is Claude Code's Linux managed-settings location.

type CodexDriver

type CodexDriver struct{}

CodexDriver runs `codex exec --json` with the prompt on stdin.

func (CodexDriver) Command

func (CodexDriver) Command(t *Task, v Variant) Command

Command: --skip-git-repo-check is required because the trial's /work is not a trusted git repo (codex 0.58.0 exits 1 "Not inside a trusted directory" without it; seen in test/uat).

func (CodexDriver) Name

func (CodexDriver) Name() string

func (CodexDriver) Parse

func (CodexDriver) Parse(out []byte) (Usage, error)

Parse reads JSONL events. Codex reports no cost, so CostUSD stays 0. Event shapes verified against codex-rs/exec/src/exec_events.rs (FACTS.md).

Tool errors: a command_execution item that completed with a non-zero exit_code or status failed/declined (codex 0.99 emits both, e.g. exit_code 1 + status "failed"; test/uat/testdata), a failed MCP/other tool item, or a tool item that started but never completed (cut off by a timeout).

Not countable here: a call whose sandbox cannot start at all. On a kernel without Landlock, codex 0.99 emits NOTHING for the exec_command (no item.started, no error item; it exits 0) and only the model sees "Sandbox(LandlockRestrict)". DockerRunner's sandbox preflight catches that case instead and marks the cell unavailable.

func (CodexDriver) SettingsPath

func (CodexDriver) SettingsPath() string

SettingsPath is the verified Unix managed_config.toml location (internal/harness/FACTS.md).

type Command

type Command struct {
	Args  []string
	Stdin string
}

Command is an argv plus optional stdin, run inside the environment.

type Comparison

type Comparison struct {
	Variant string  `json:"variant"`
	Control string  `json:"control"`
	Tasks   int     `json:"tasks"`
	Delta   float64 `json:"delta"` // mean per-task pass rate: variant - control
	CILo    float64 `json:"ciLo"`
	CIHi    float64 `json:"ciHi"`
	Verdict string  `json:"verdict"`

	// Deltas are paired bootstrap CIs per metric over non-flaky tasks
	// (MetricPass, MetricCost, ...); Gate is the ship/hold/block verdict.
	Deltas map[string]MetricDelta `json:"deltas,omitempty"`
	Gate   *Gate                  `json:"gate,omitempty"`
}

Comparison is a paired (by task) pass-rate comparison vs the control.

type Criterion

type Criterion struct {
	Name        string  `yaml:"name" json:"name"`
	Description string  `yaml:"description" json:"description"`
	Weight      float64 `yaml:"weight,omitempty" json:"weight,omitempty"` // default 1
}

Criterion is one scored dimension of a rubric.

type DockerRunner

type DockerRunner struct {
	Bin string // default "docker"
	// Image returns the image for a variant; default
	// ghcr.io/dshakes/eval-<harness>:<version>.
	Image func(Variant) string
	// PassEnv names host env vars forwarded into the agent step only.
	PassEnv []string
	// Network is passed as --network; default "none".
	Network string
	// Memory (default "4g"), CPUs (default "2") and User (default
	// "1000:1000", non-root) bound each container.
	Memory, CPUs, User string
	// SandboxProbes maps harness -> a command that must exit 0 inside a
	// container hardened like the trials before any trial of that harness
	// runs (once per runner and image). nil = DefaultSandboxProbes; an empty
	// map disables probing.
	SandboxProbes map[string][]string
	// contains filtered or unexported fields
}

DockerRunner isolates each trial in hardened containers by shelling out to the docker CLI. Each phase gets a FRESH container from the image, with only /work carried over (docker cp out, then in):

setup  -> container A (task-author shell; no secrets)
agent  -> container B (PassEnv secrets via exec -e; nothing A started survives)
check  -> container C (no secrets; cannot read B's /proc/<pid>/environ)

Every container is removed (`docker rm -f`, killing all its processes) before the next phase starts, on Close, and when a step's context ends.

Remote repos are cloned on the HOST (protocol-allowlisted) and copied in, so the containers need no egress for setup/check (Network "none").

Secrets: PassEnv should hold short-lived gateway tokens (minted per run and scoped to the Halos gateway), never long-lived provider keys: the agent step runs model-driven tool calls that can read its own environment. Recommended networking: set Network to a docker network whose only egress is the Halos gateway; it applies to every phase's container.

Files are streamed in as a tar whose entries are owned by User: plain `docker cp` keeps the host uid (e.g. 501), leaving the non-root agent unable to edit files in place. Verified end to end against a real daemon with the evals/images/fake stub image.

func (*DockerRunner) Start

func (d *DockerRunner) Start(ctx context.Context, j Job) (Env, error)

type Driver

type Driver interface {
	Name() string
	Command(t *Task, v Variant) Command
	// SettingsPath is where the rendered managed settings must appear inside
	// the environment for the CLI to pick them up.
	SettingsPath() string
	Parse(stdout []byte) (Usage, error)
}

Driver knows how to run one harness headlessly and read its output. Unknown output fields are always tolerated.

type Env

type Env interface {
	Exec(ctx context.Context, c Command) (ExecResult, error)
	Close() error
}

Env is an isolated, prepared workspace with the task repo checked out.

type ExecResult

type ExecResult struct {
	Stdout, Stderr []byte
	ExitCode       int
}

ExecResult is the outcome of one command. A non-zero ExitCode is not an error; err from Exec means the command could not run or was cancelled.

type FilePairs

type FilePairs struct {
	Path string
	Keys [][]byte
}

FilePairs reads halo-shadow's JSONL pair store, decrypting sealed rows with Keys (current and retired). An unreadable row fails the read: grading a silently partial sample would bias it.

func (FilePairs) Pairs

func (f FilePairs) Pairs(ctx context.Context) ([]shadow.Pair, error)

type Gate

type Gate struct {
	Verdict   string     `json:"verdict"` // ship | hold | block
	Reasons   []string   `json:"reasons"`
	Flaky     []TaskDiff `json:"flaky,omitempty"`
	Regressed []TaskDiff `json:"regressed,omitempty"`
	Tasks     []TaskDiff `json:"tasks,omitempty"`
}

Gate is the regression verdict of one comparison (or, on the scorecard, the worst over all comparisons).

type GeminiDriver

type GeminiDriver struct{}

GeminiDriver runs `gemini -p ... --output-format json`. Flags, settings path and the JSON stats shape are verified against the docs and source (internal/harness/FACTS.md); not run against a real CLI.

func (GeminiDriver) Command

func (GeminiDriver) Command(t *Task, v Variant) Command

func (GeminiDriver) Name

func (GeminiDriver) Name() string

func (GeminiDriver) Parse

func (GeminiDriver) Parse(out []byte) (Usage, error)

func (GeminiDriver) SettingsPath

func (GeminiDriver) SettingsPath() string

type Grade

type Grade struct {
	Grader string   `json:"grader"` // type, plus path/rubric when useful
	Pass   bool     `json:"pass"`
	Score  *float64 `json:"score,omitempty"` // judge only
	Rubric string   `json:"rubric,omitempty"`
	Detail string   `json:"detail,omitempty"`
	// Error is a grader failure (e.g. judge schema violation): never a pass.
	Error string `json:"error,omitempty"`
}

Grade is one grader's outcome on one trial.

type GraderSpec

type GraderSpec struct {
	Type string `yaml:"type" json:"type"`
	// command
	Run string `yaml:"run,omitempty" json:"run,omitempty"`
	// file: Path relative to the work dir; Exists defaults to true.
	Path        string   `yaml:"path,omitempty" json:"path,omitempty"`
	Exists      *bool    `yaml:"exists,omitempty" json:"exists,omitempty"`
	Contains    []string `yaml:"contains,omitempty" json:"contains,omitempty"`
	NotContains []string `yaml:"not_contains,omitempty" json:"notContains,omitempty"`
	Matches     string   `yaml:"matches,omitempty" json:"matches,omitempty"` // RE2
	// diff: globs (path.Match, slash-separated) over changed paths.
	MaxFilesChanged int      `yaml:"max_files_changed,omitempty" json:"maxFilesChanged,omitempty"`
	MustChange      []string `yaml:"must_change,omitempty" json:"mustChange,omitempty"`
	MustNotChange   []string `yaml:"must_not_change,omitempty" json:"mustNotChange,omitempty"`
	// judge: rubric file relative to the declaring task or suite dir (e.g.
	// ../rubrics/x.yaml), never outside the evals root.
	Rubric string `yaml:"rubric,omitempty" json:"rubric,omitempty"`
	// contains filtered or unexported fields
}

GraderSpec is one entry of a task's `graders:`. Fields apply per type.

type HTTPLLM

type HTTPLLM struct {
	BaseURL, Wire, APIKey string
	HTTP                  *http.Client // default: 120s timeout
}

HTTPLLM speaks the Anthropic Messages or OpenAI Responses wire to the gateway. Temperature is 0: the judge should be as repeatable as the provider allows. UNVERIFIED against a live gateway; tests use httptest.

func (*HTTPLLM) Complete

func (h *HTTPLLM) Complete(ctx context.Context, r LLMRequest) (string, error)

type HistoryEntry

type HistoryEntry struct {
	Time     time.Time      `json:"time"`
	Kind     string         `json:"kind"` // offline | online
	Suite    string         `json:"suite,omitempty"`
	Gate     *Gate          `json:"gate,omitempty"`
	Variants []VariantScore `json:"variants,omitempty"`
	Online   *OnlineResult  `json:"online,omitempty"`
}

HistoryEntry is one line of the scorecard history (JSONL): an offline suite run or an online reading, appended so trends survive across runs.

func OfflineHistory

func OfflineHistory(sc *Scorecard, at time.Time) HistoryEntry

OfflineHistory summarises sc as a history entry (trials omitted).

type Job

type Job struct {
	Task    *Task
	Variant Variant
	// Settings is the host path of the rendered managed settings (may be empty)
	// and SettingsDest where it must appear in the environment.
	Settings     string
	SettingsDest string
}

Job is one (task x variant x repeat) trial to set up an environment for.

type Judge

type Judge struct {
	LLM   LLM
	Model string
	Cache *JudgeCache // may be nil
}

Judge grades text against a rubric with a pinned model.

func NewJudge

func NewJudge(c *JudgeConfig, suiteDir string) (*Judge, error)

NewJudge builds the judge for c (nil config = no judge). The credential is read from c.APIKeyEnv at call time of this function only.

func (*Judge) Grade

func (j *Judge) Grade(ctx context.Context, r *Rubric, parts []Part) (Verdict, error)

Grade scores parts against r. Cached by (rubric id, version, file hash, model, prompt version, content hash), never by the per-call nonce; only verdicts that parsed are cached, so a schema failure is retried next time rather than frozen, and every hit is re-validated against r.

type JudgeCache

type JudgeCache struct {
	Dir string
	// contains filtered or unexported fields
}

JudgeCache memoises verdicts in memory and, when Dir is set, on disk (one JSON file per key) so reruns do not pay for the same judgement twice.

type JudgeConfig

type JudgeConfig struct {
	URL   string `yaml:"url" json:"url"`     // gateway base URL; /v1/messages or /v1/responses is appended
	Wire  string `yaml:"wire" json:"wire"`   // anthropic-messages | openai-responses
	Model string `yaml:"model" json:"model"` // exact, pinned model id (no "latest" aliases)
	// APIKeyEnv names the env var holding the gateway credential.
	APIKeyEnv string `yaml:"api_key_env,omitempty" json:"apiKeyEnv,omitempty"`
	// Cache is a directory (relative to the suite) for cached verdicts.
	Cache string `yaml:"cache,omitempty" json:"cache,omitempty"`
}

JudgeConfig is a suite's `judge:` block: a pinned model reached through the configured gateway.

type JudgeInfo

type JudgeInfo struct {
	Model   string   `json:"model"`
	Wire    string   `json:"wire,omitempty"`
	Rubrics []string `json:"rubrics"`
}

JudgeInfo pins what graded the run: the judge model and rubric id@version.

type LLM

type LLM interface {
	Complete(ctx context.Context, req LLMRequest) (string, error)
}

LLM sends one prompt and returns the reply text.

type LLMRequest

type LLMRequest struct {
	Model, System, Prompt string
	MaxTokens             int
}

LLMRequest is one judge completion.

type LocalRunner

type LocalRunner struct{}

LocalRunner runs in temp directories on the host. It provides no isolation beyond a private copy of the repo: use it for tests, not untrusted agents. The settings file is copied next to the repo and its path exported to commands as HALO_SETTINGS_FILE.

func (LocalRunner) Start

func (LocalRunner) Start(ctx context.Context, j Job) (Env, error)

type Matrix

type Matrix struct {
	// Baseline is the control cell name; default the first cell.
	Baseline  string           `yaml:"baseline,omitempty" json:"baseline,omitempty"`
	Harnesses []MatrixHarness  `yaml:"harnesses" json:"harnesses"`
	Providers []MatrixProvider `yaml:"providers,omitempty" json:"providers,omitempty"`
}

Matrix is one suite run across harness x model x provider. Each harness lists the models it can drive (codex does not run a Claude alias), and a provider either applies to every model under its own alias or, when it has Aliases, only to the models it maps (e.g. bedrock: {sonnet: sonnet-bedrock}). Providers are realised as gateway model aliases: the gateway routes each alias to its upstream, so the CLI side stays provider-agnostic.

func (*Matrix) Cells

func (m *Matrix) Cells(suiteDir string) []Variant

Cells expands the matrix into variants named <harness>@<version>/<model>[/<provider>], in declaration order.

type MatrixHarness

type MatrixHarness struct {
	Harness  string   `yaml:"harness" json:"harness"`
	Version  string   `yaml:"version" json:"version"`
	Models   []string `yaml:"models" json:"models"`
	Settings string   `yaml:"settings,omitempty" json:"settings,omitempty"`
}

MatrixHarness is one CLI at a pinned version and the models to run it on.

type MatrixProvider

type MatrixProvider struct {
	Name    string            `yaml:"name" json:"name"`
	Aliases map[string]string `yaml:"aliases,omitempty" json:"aliases,omitempty"`
}

MatrixProvider names an upstream. Aliases maps model -> gateway alias for that provider; empty means the model alias itself.

type MetricDelta

type MetricDelta struct {
	Mean     float64 `json:"mean"`
	CILo     float64 `json:"ciLo"`
	CIHi     float64 `json:"ciHi"`
	Baseline float64 `json:"baseline"`
}

MetricDelta is a paired (by task) bootstrap estimate of candidate - baseline for one metric; Baseline is the baseline's mean per-task value.

type OTLPExporter

type OTLPExporter struct {
	URL   string // collector base URL; /v1/metrics is appended
	Token string // bearer token (optional)
	HTTP  *http.Client
}

OTLPExporter posts halo.eval.* metrics to an OTel collector over OTLP/HTTP JSON. Point it at the collector's eval receiver (`halo telemetry collector-config --eval-receiver`, 4320, token HALO_OTLP_EVAL_TOKEN) so readings carry halo.source=eval. Never give it the gateway token: rows on the gateway receiver are stamped halo.source=gateway, which the controller trusts for auto-kill. The CLI receiver works too, but its rows are client-controlled (halo.source=cli) and are not shown as evidence.

func (*OTLPExporter) Export

func (x *OTLPExporter) Export(ctx context.Context, rs []OnlineResult, start, now time.Time) error

Export sends one data point per metric per result, stamped at now.

type OnlineOptions

type OnlineOptions struct {
	Judge  *Judge
	Rubric *Rubric
	// Sample caps graded pairs per experiment (0 = all); the sample is a
	// seeded shuffle, so a rerun with the same seed grades the same pairs.
	Sample int
	Seed   uint64
	// Experiment limits grading to one experiment ("" = all).
	Experiment string
	// Skip holds pair ids already graded (from the history file).
	Skip map[string]bool
	Now  func() time.Time
	// ControlNames maps experiment -> its control variant name (from policy).
	// halo-shadow does not label the control side, and analysis matches arms by
	// the experiment's variant names; default "control".
	ControlNames map[string]string
}

OnlineOptions configures one online grading pass.

type OnlineResult

type OnlineResult struct {
	Experiment  string  `json:"experiment"`
	Rubric      string  `json:"rubric"`
	JudgeModel  string  `json:"judgeModel"`
	Pairs       int     `json:"pairs"`  // eligible pairs seen
	Graded      int     `json:"graded"` // pairs with both sides judged
	JudgeErrors int     `json:"judgeErrors"`
	Control     float64 `json:"controlScore"`
	Candidate   float64 `json:"candidateScore"`
	// Delta is candidate - control score, paired by pair, with a bootstrap CI.
	Delta   MetricDelta `json:"delta"`
	WinRate float64     `json:"winRate"` // share of pairs the candidate scored higher (ties count half)
	// ControlVariant/CandidateVariant name the arms (shadow target variants).
	ControlVariant   string   `json:"controlVariant"`
	CandidateVariant string   `json:"candidateVariant"`
	PairIDs          []string `json:"pairIds"`
	// Scores are the per-pair readings the exporter sends (not kept in history).
	Scores []PairScore `json:"-"`
}

OnlineResult is one experiment's online quality reading.

func RunOnline

func RunOnline(ctx context.Context, src PairSource, o OnlineOptions) ([]OnlineResult, error)

RunOnline samples pairs per experiment and grades both sides with the rubric. Pairs where either side errored are skipped (that is a reliability signal the gateway metrics already carry, not a quality one). A judge error on either side drops the pair from the scores and is counted.

type PairScore

type PairScore struct {
	ID                 string
	Control, Candidate float64
}

PairScore is one pair's judge scores; each is one analysis sample (unit = pair) of halo.eval.judge.score for its arm.

type PairSource

type PairSource interface {
	Pairs(ctx context.Context) ([]shadow.Pair, error)
}

PairSource yields halo-shadow pairs.

type Part

type Part struct {
	Title     string
	Body      string
	Untrusted bool
}

Part is one section of a judge prompt. Untrusted parts (anything written by the agent or model under evaluation, or by a client) are fenced as data.

type Rubric

type Rubric struct {
	ID           string      `yaml:"id" json:"id"`
	Version      string      `yaml:"version" json:"version"`
	Instructions string      `yaml:"instructions" json:"instructions"`
	Criteria     []Criterion `yaml:"criteria" json:"criteria"`
	// PassThreshold is the weighted score (0..1) at or above which a judge
	// grader passes; default 0.7.
	PassThreshold float64 `yaml:"pass_threshold,omitempty" json:"passThreshold"`
	// contains filtered or unexported fields
}

Rubric is a versioned grading rubric. Bump Version whenever the meaning changes; scorecards record id@version.

func LoadRubric

func LoadRubric(path string) (*Rubric, error)

LoadRubric reads and validates a rubric YAML file.

func (*Rubric) Ref

func (r *Rubric) Ref() string

Ref is "id@version", as recorded in scorecards.

type RunOptions

type RunOptions struct {
	Parallel int
	// Judge grades tasks' judge graders; required if any task has one.
	Judge *Judge
}

RunOptions tunes RunSuiteWith.

type Runner

type Runner interface {
	Start(ctx context.Context, j Job) (Env, error)
}

Runner provisions isolated environments.

type Scorecard

type Scorecard struct {
	Suite       string         `json:"suite"`
	Control     string         `json:"control"`
	Variants    []VariantScore `json:"variants"`
	Comparisons []Comparison   `json:"comparisons"`
	Trials      []Trial        `json:"trials"`

	Seed uint64 `json:"seed"`
	K    int    `json:"k,omitempty"`
	// Gate is the worst comparison gate; nil with a single variant.
	Gate       *Gate       `json:"gate,omitempty"`
	Thresholds *Thresholds `json:"thresholds,omitempty"`
	Judge      *JudgeInfo  `json:"judge,omitempty"`
}

Scorecard is the full result of a suite run.

func BuildScorecard

func BuildScorecard(s *Suite, trials []Trial, seed uint64) (*Scorecard, error)

BuildScorecard aggregates trials. The seed makes bootstrap CIs reproducible.

func (*Scorecard) JSON

func (sc *Scorecard) JSON() ([]byte, error)

JSON renders the scorecard as indented JSON.

func (*Scorecard) Markdown

func (sc *Scorecard) Markdown() string

Markdown renders a human-readable scorecard, suitable as a PR comment.

func (*Scorecard) Table

func (sc *Scorecard) Table() string

Table renders a compact fixed-width terminal table (one row per variant).

type Suite

type Suite struct {
	Name string `yaml:"name" json:"name"`
	// Tasks are task directory names under <suite dir>/../tasks.
	Tasks []string `yaml:"tasks" json:"tasks"`
	// Control names the baseline variant; defaults to the first.
	Control  string    `yaml:"control,omitempty" json:"control"`
	Repeats  int       `yaml:"repeats,omitempty" json:"repeats"`
	Variants []Variant `yaml:"variants" json:"variants"`
	// Seed is the base seed for per-trial seeds and bootstrap CIs. No pinned
	// CLI exposes a sampling seed, so it makes the run order and statistics
	// reproducible, not the agent itself.
	Seed uint64 `yaml:"seed,omitempty" json:"seed,omitempty"`
	// K is the k of pass@k and pass^k; default Repeats.
	K int `yaml:"k,omitempty" json:"k,omitempty"`
	// Gate holds the ship/hold/block thresholds.
	Gate Thresholds `yaml:"gate,omitempty" json:"gate"`
	// Judge configures the LLM-as-judge grader used by tasks' judge graders.
	Judge *JudgeConfig `yaml:"judge,omitempty" json:"judge,omitempty"`
	// Matrix, used by `halo eval run --matrix`, replaces Variants with the
	// harness x model x provider cross product.
	Matrix *Matrix `yaml:"matrix,omitempty" json:"matrix,omitempty"`
	// Graders apply to every task in addition to its own (e.g. a judge
	// rubric for the whole suite, so shared tasks stay judge-free).
	Graders []GraderSpec `yaml:"graders,omitempty" json:"graders,omitempty"`

	// Dir is the directory of the suite file; set by LoadSuite.
	Dir string `yaml:"-" json:"-"`
}

Suite lists tasks and the variants to compare.

func LoadSuite

func LoadSuite(path string) (*Suite, error)

LoadSuite reads a suite YAML file.

func (*Suite) LoadTasks

func (s *Suite) LoadTasks(tasksRoot string) ([]*Task, error)

LoadTasks loads the suite's tasks from tasksRoot (default <suite dir>/../tasks).

func (*Suite) Validate

func (s *Suite) Validate() error

Validate checks the suite and applies defaults.

func (*Suite) WithMatrix

func (s *Suite) WithMatrix() (*Suite, error)

WithMatrix returns a copy of s whose variants are the matrix cells and whose control is the matrix baseline.

type Task

type Task struct {
	ID        string        `yaml:"id" json:"id"`
	Repo      string        `yaml:"repo" json:"repo"` // git URL, or directory relative to the task dir
	Setup     []string      `yaml:"setup,omitempty" json:"setup,omitempty"`
	Prompt    string        `yaml:"prompt" json:"prompt"`
	Check     string        `yaml:"check" json:"check"` // shell command, exit 0 = pass
	Timeout   time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"`
	BudgetUSD float64       `yaml:"budget_usd,omitempty" json:"budgetUSD,omitempty"`
	MaxTurns  int           `yaml:"max_turns,omitempty" json:"maxTurns,omitempty"`
	Tags      []string      `yaml:"tags,omitempty" json:"tags,omitempty"`
	// Graders all have to pass too; check may be empty when graders are set.
	Graders []GraderSpec `yaml:"graders,omitempty" json:"graders,omitempty"`

	// Dir is the directory holding task.yaml; set by LoadTask.
	Dir string `yaml:"-" json:"-"`
}

Task is one eval task.

func LoadTask

func LoadTask(dir string) (*Task, error)

LoadTask reads <dir>/task.yaml.

func (*Task) RepoDir

func (t *Task) RepoDir() (string, error)

RepoDir resolves a local repo path against the task dir, refusing paths (including via symlinks) that leave it.

func (*Task) Validate

func (t *Task) Validate() error

Validate checks required fields.

type TaskDiff

type TaskDiff struct {
	Task      string  `json:"task"`
	Baseline  float64 `json:"baseline"`  // pass rate over trials
	Candidate float64 `json:"candidate"` // pass rate over trials
	Status    string  `json:"status"`    // regressed | improved | same | flaky
	Reason    string  `json:"reason,omitempty"`
}

TaskDiff is one task's per-arm pass rates.

type Thresholds

type Thresholds struct {
	// MaxPassDrop is the non-inferiority margin on the paired per-task pass
	// rate: ship needs the CI lower bound >= -MaxPassDrop. Default 0.05.
	MaxPassDrop          *float64 `yaml:"max_pass_drop,omitempty" json:"maxPassDrop,omitempty"`
	MaxCostIncrease      *float64 `yaml:"max_cost_increase,omitempty" json:"maxCostIncrease,omitempty"`
	MaxTokensIncrease    *float64 `yaml:"max_tokens_increase,omitempty" json:"maxTokensIncrease,omitempty"`
	MaxWallIncrease      *float64 `yaml:"max_wall_increase,omitempty" json:"maxWallIncrease,omitempty"`
	MaxToolCallsIncrease *float64 `yaml:"max_tool_calls_increase,omitempty" json:"maxToolCallsIncrease,omitempty"`
	// TaskRegression is the per-task pass-rate drop that marks a task as
	// regressed (default 0.5); more than MaxRegressedTasks (default 0) blocks.
	TaskRegression    float64 `yaml:"task_regression,omitempty" json:"taskRegression"`
	MaxRegressedTasks int     `yaml:"max_regressed_tasks,omitempty" json:"maxRegressedTasks"`
	// MinTasks is the fewest non-flaky tasks a verdict other than hold needs
	// (default 1).
	MinTasks int `yaml:"min_tasks,omitempty" json:"minTasks"`
}

Thresholds is a suite's `gate:` block. Pass-rate margins are absolute (0.05 = 5 percentage points); cost/tokens/wall/tool-call limits are relative increases over the baseline mean (0.2 = +20%) and are only gated when set.

type Trial

type Trial struct {
	Task       string  `json:"task"`
	Variant    string  `json:"variant"`
	Repeat     int     `json:"repeat"`
	Pass       bool    `json:"pass"`
	CostUSD    float64 `json:"costUSD"`
	Turns      int     `json:"turns"`
	WallMs     int64   `json:"wallMs"`
	Tokens     int64   `json:"tokens"`
	ToolErrors int     `json:"toolErrors"`
	ToolCalls  int     `json:"toolCalls"`
	// Seed is derived from the suite seed, task and repeat (identical across
	// variants, so arms stay paired). Recorded; no pinned CLI accepts one.
	Seed uint64 `json:"seed,omitempty"`
	// Grades are the task's graders' results (absent for check-only tasks).
	Grades []Grade `json:"grades,omitempty"`
	// Unavailable is set when the cell could not be measured on this host
	// (ErrSandboxUnavailable): the trial never ran, is excluded from every
	// statistic, and makes the gate hold. Never a failure.
	Unavailable string `json:"unavailable,omitempty"`
	// GraderError is set when a grader could not produce a verdict (e.g. the
	// judge's reply failed schema validation). The trial does not pass.
	GraderError string `json:"graderError,omitempty"`
	// Error records setup/agent/parse problems. The trial is still scored by
	// the check command where possible; infra failures count as not passed.
	Error string `json:"error,omitempty"`
}

Trial is the scored outcome of one (task x variant x repeat) run.

func RunSuite

func RunSuite(ctx context.Context, s *Suite, tasks []*Task, r Runner, drivers map[string]Driver, parallel int) ([]Trial, error)

RunSuite runs every task x variant x repeat with up to parallel concurrent trials and returns trials in deterministic order. It returns ctx.Err() if cancelled before finishing.

func RunSuiteWith

func RunSuiteWith(ctx context.Context, s *Suite, tasks []*Task, r Runner, drivers map[string]Driver, o RunOptions) ([]Trial, error)

RunSuiteWith is RunSuite with a judge for rubric graders.

func RunTrial

func RunTrial(ctx context.Context, r Runner, d Driver, t *Task, v Variant, repeat int) Trial

RunTrial provisions an environment, runs setup, the agent, then the task's success check, and returns the scored Trial. It never returns an error: failures are recorded in Trial.Error so one bad trial cannot sink a suite.

type Usage

type Usage struct {
	CostUSD    float64
	Turns      int
	DurationMs int64
	Tokens     int64 // input + output (+ cache) tokens
	ToolErrors int
	ToolCalls  int
	Failed     bool   // harness reported the run as failed/errored
	Error      string // message when Failed
}

Usage is what a driver extracts from a headless run's output.

type Variant

type Variant struct {
	Name    string `yaml:"name" json:"name"`
	Harness string `yaml:"harness" json:"harness"` // claude | codex | gemini
	Version string `yaml:"version" json:"version"`
	Model   string `yaml:"model" json:"model"` // gateway model alias
	// Provider labels the upstream the alias routes to (matrix runs); recorded only.
	Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`
	// Settings is a path (relative to the suite file) to the rendered managed
	// settings for this variant, e.g. the output of `halo render`.
	Settings string `yaml:"settings,omitempty" json:"settings,omitempty"`
	// Dir is the suite dir Settings must live in; set by LoadSuite.
	Dir string `yaml:"-" json:"-"`
}

Variant is one arm of a suite: a harness at a version on a model, with an optional rendered managed-settings file mounted into the environment.

type VariantScore

type VariantScore struct {
	Name        string  `json:"name"`
	Trials      int     `json:"trials"`
	Pass1       float64 `json:"pass1"`
	Pass1CILo   float64 `json:"pass1CILo"`
	Pass1CIHi   float64 `json:"pass1CIHi"`
	CostPerTask float64 `json:"costPerTaskUSD"` // mean cost per trial
	WallP50Ms   int64   `json:"wallP50Ms"`
	WallP95Ms   int64   `json:"wallP95Ms"`
	Tokens      int64   `json:"tokens"`
	ToolErrors  int     `json:"toolErrors"`
	Errors      int     `json:"errors"` // trials with a recorded Error

	Harness  string `json:"harness,omitempty"`
	Version  string `json:"version,omitempty"`
	Model    string `json:"model,omitempty"`
	Provider string `json:"provider,omitempty"`
	// PassAtK / PassHatK are the per-task unbiased pass@k (any of k trials
	// passes) and pass^k (all k pass), averaged over tasks.
	K           int      `json:"k,omitempty"`
	PassAtK     float64  `json:"passAtK"`
	PassHatK    float64  `json:"passHatK"`
	ToolCalls   int      `json:"toolCalls"`
	GradeErrors int      `json:"gradeErrors"`
	JudgeScore  *float64 `json:"judgeScore,omitempty"` // mean judge score over judged trials
	// Unavailable counts trials this host could not measure (Trial.Unavailable,
	// e.g. codex without Landlock). They are excluded from every statistic; a
	// variant with no measured trials shows UnavailableReason, not 0%.
	Unavailable       int    `json:"unavailable,omitempty"`
	UnavailableReason string `json:"unavailableReason,omitempty"`
}

VariantScore aggregates one variant's trials.

type Verdict

type Verdict struct {
	Score     float64            `json:"score"` // weighted mean of Scores
	Scores    map[string]float64 `json:"scores"`
	Rationale string             `json:"rationale"`
	Cached    bool               `json:"cached,omitempty"`
}

Verdict is a parsed, validated judge reply.

func ParseVerdict

func ParseVerdict(r *Rubric, reply string) (Verdict, error)

ParseVerdict strictly parses reply against r: a single JSON object (only surrounding whitespace allowed) {"scores": {<every criterion>: 0..1}, "rationale": "<non-empty>"} with no unknown fields or criteria.

Jump to

Keyboard shortcuts

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