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
- Variables
- func AppendHistory(path string, entries ...HistoryEntry) error
- func CheckSecureURL(raw string) error
- func Drivers() map[string]Driver
- func GradedPairs(path string) (map[string]bool, error)
- func OnlineTable(rs []OnlineResult) string
- type ArmStats
- type ClaudeDriver
- type CodexDriver
- type Command
- type Comparison
- type Criterion
- type DockerRunner
- type Driver
- type Env
- type ExecResult
- type FilePairs
- type Gate
- type GeminiDriver
- type Grade
- type GraderSpec
- type HTTPLLM
- type HistoryEntry
- type Job
- type Judge
- type JudgeCache
- type JudgeConfig
- type JudgeInfo
- type LLM
- type LLMRequest
- type LocalRunner
- type Matrix
- type MatrixHarness
- type MatrixProvider
- type MetricDelta
- type OTLPExporter
- type OnlineOptions
- type OnlineResult
- type OpsResult
- type PairScore
- type PairSource
- type Part
- type Rubric
- type RunOptions
- type Runner
- type Scorecard
- type Suite
- type Task
- type TaskDiff
- type Thresholds
- type Trial
- type Usage
- type Variant
- type VariantScore
- type Verdict
Constants ¶
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.
const ( MetricPass = "pass_rate" MetricCost = "cost_usd" MetricTokens = "tokens" MetricWall = "wall_ms" MetricToolCalls = "tool_calls" )
Metric keys of Comparison.Deltas.
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.
const ( WireAnthropic = "anthropic-messages" WireOpenAI = "openai-responses" )
Judge wire protocols (names match policy Gateway.Protocols).
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) // Operational readings per arm over every pair (ADR-0004: refusal, latency, cost). MetricShadowErrorRate = "halo.eval.shadow.error_rate" // gauge, 0..1 MetricShadowLatency = "halo.eval.shadow.latency_ms" // gauge, mean MetricShadowOutputTokens = "halo.eval.shadow.output_tokens" // gauge, mean )
Online eval metric names (halo.* schema, see internal/telemetry).
const ( VerdictBetter = "better" VerdictWorse = "worse" VerdictNoDiff = "no_significant_difference" )
Verdicts for a paired comparison against control.
const DefaultTimeout = 10 * time.Minute
DefaultTimeout applies when a task sets none.
Variables ¶
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.
var ErrJudgeSchema = errors.New("judge reply failed schema validation")
ErrJudgeSchema marks a judge reply that failed strict parsing: a grader error, never a pass.
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 ¶
CheckSecureURL requires https, allowing plain http only to a loopback host (local gateways, tests): credentials must never cross a network in clear.
func GradedPairs ¶
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 ArmStats ¶
type ArmStats struct {
ErrorRate float64 `json:"errorRate"`
LatencyMS float64 `json:"latencyMs"`
OutputTokens float64 `json:"outputTokens"`
}
ArmStats summarises one arm over all pairs: ErrorRate is the share of sides that errored or answered non-200 (refusals and overloads included); LatencyMS and OutputTokens are means.
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 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.
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 ExecResult ¶
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 ¶
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.
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) Name ¶
func (GeminiDriver) Name() string
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 ¶
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.
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 ¶
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 ¶
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.
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.
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:"-"`
// Ops are the operational readings over every eligible pair, errored ones
// included (they are the point): the refusal, latency and cost regressions
// ADR-0004 says shadow catches beside quality.
Ops OpsResult `json:"ops"`
}
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 OpsResult ¶
type OpsResult struct {
Pairs int `json:"pairs"`
Control ArmStats `json:"control"`
Candidate ArmStats `json:"candidate"`
}
OpsResult is one experiment's per-arm operational reading.
type PairScore ¶
PairScore is one pair's judge scores; each is one analysis sample (unit = pair) of halo.eval.judge.score for its arm.
type PairSource ¶
PairSource yields halo-shadow pairs.
type Part ¶
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 ¶
LoadRubric reads and validates a rubric YAML file.
type RunOptions ¶
type RunOptions struct {
Parallel int
// Judge grades tasks' judge graders; required if any task has one.
Judge *Judge
}
RunOptions tunes RunSuiteWith.
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 ¶
BuildScorecard aggregates trials. The seed makes bootstrap CIs reproducible.
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 (*Suite) LoadTasks ¶
LoadTasks loads the suite's tasks from tasksRoot (default <suite dir>/../tasks).
func (*Suite) WithMatrix ¶
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.
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"`
// (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.
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%.
}
VariantScore aggregates one variant's trials.