eval

package
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: Apache-2.0 Imports: 33 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 (
	SelectDiverged      = "diverged"       // candidate's tools differ from the control replay or from production
	SelectInvalidCall   = "invalid-call"   // candidate called a tool not offered, or missed a required argument
	SelectPrematureStop = "premature-stop" // production kept working; the candidate ended its turn
	SelectJudgeLoss     = "judge-loss"     // judge scored the candidate below the control by more than JudgeLoss
	SelectSample        = "sample"         // stratified random sample by depth
)

Selection reasons: why a step became a case.

View Source
const (
	MatchTools = "tools" // same multiset of tool names (or both stopped)
	MatchCalls = "calls" // same tools with identical canonical arguments
)

Matcher modes for StepCase.Match.

View Source
const (
	FromProduction = "production" // recovered from the session's next mirrored request
	FromControl    = "control"    // the control replay, when production's action was not recovered
)

Expected-action sources.

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)
	// 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
	// Step-level agent readings (ADR-0014). Fidelity and invalid-call rate are
	// analysis samples per session and arm (halo.unit = hashed session id);
	// agreement is a per-experiment gauge.
	MetricAgentFidelity      = "halo.eval.agent.fidelity"          // gauge per session and arm, 0..1
	MetricAgentInvalidCall   = "halo.eval.agent.invalid_call_rate" // gauge per session and arm, 0..1
	MetricAgentToolAgreement = "halo.eval.agent.tool_agreement"    // gauge, candidate vs control replay
	MetricAgentPrematureStop = "halo.eval.agent.premature_stop"    // gauge per arm, 0..1
)

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 DefaultAgentQuiet = time.Hour

DefaultAgentQuiet is OnlineOptions.AgentQuiet's default.

View Source
const DefaultTimeout = 10 * time.Minute

DefaultTimeout applies when a task sets none.

View Source
const RedactionRulesVersion = "1"

RedactionRulesVersion names the built-in rule set recorded in every case's provenance. Bump it whenever a built-in rule changes.

Variables

View Source
var DefaultKeepFields = []string{

	"model", "system", "messages", "tools", "tool_choice", "max_tokens", "temperature", "top_p", "top_k", "stop_sequences", "thinking", "anthropic_version",

	"instructions", "input", "max_output_tokens", "parallel_tool_calls", "reasoning", "text", "include",

	"contents", "systemInstruction", "generationConfig", "toolConfig",

	"inferenceConfig",
}

DefaultKeepFields are the top-level request fields a harvested case keeps: what the model needs to answer the step on each wire protocol. Everything else is dropped, because the rest (Claude Code's metadata.user_id, Codex's prompt_cache_key, user, safety_identifier, previous_response_id...) carries account and session identifiers, not the conversation.

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.

DefaultSelect is the selection when none is given: the step-level failures.

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 Fence added in v0.4.0

func Fence(parts []Part) (prompt, notice string, err error)

Fence lays parts out as the judge does, untrusted bodies inside a fresh per-call nonce delimiter, and returns the prompt plus the sentence (for the system prompt) saying fenced content is data, never instructions.

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 HistoryIDs added in v0.3.0

func HistoryIDs(path string) (graded, agent map[string]bool, err error)

HistoryIDs returns the pair ids already judged and the step ids whose agent samples were already exported (missing file = none).

func HistoryScores added in v0.3.0

func HistoryScores(path string) (map[string]PairScore, error)

HistoryScores returns the judge scores recorded per pair id in the history file (missing file = none; a later reading of a pair wins).

func OnlineTable

func OnlineTable(rs []OnlineResult) string

OnlineTable renders online results as a compact terminal table.

func SameCalls added in v0.3.0

func SameCalls(a, b Action) bool

SameCalls: SameTools with identical arguments (order-insensitive).

func SameTools added in v0.3.0

func SameTools(a, b Action) bool

SameTools: both stopped, or both called the same multiset of tools. A malformed step matches only another malformed step.

func SessionUnit added in v0.3.0

func SessionUnit(session string) string

SessionUnit is the analysis unit for a session: a hash, so raw harness session ids never leave the eval host.

func WriteStepCases added in v0.3.0

func WriteStepCases(dir string, cases []StepCase) error

WriteStepCases writes one <id>.json per case into dir (created 0700), each 0600. Every file is encoded and re-checked for surviving credentials before the first one is written, so a failure leaves no partial suite.

Types

type Action added in v0.3.0

type Action struct {
	Calls []Call `json:"calls,omitempty"`
	// Malformed: the model tried to call a tool and produced something the
	// API rejected (Gemini MALFORMED_FUNCTION_CALL, Converse malformed_tool_use).
	// An invalid call, not a stop.
	Malformed bool `json:"malformed,omitempty"`
	// Truncated: the output token cap cut the step; it says nothing about the
	// model's decision, so the step is excluded.
	Truncated bool `json:"truncated,omitempty"`
}

Action is one model step: the tool calls it made, in order. No calls means the model ended its turn (answered or gave up).

func (Action) Stopped added in v0.3.0

func (a Action) Stopped() bool

Stopped: the step ended the agent's turn.

type AgentArm added in v0.3.0

type AgentArm struct {
	// ToolCallRate: share of steps that called at least one tool.
	ToolCallRate float64 `json:"toolCallRate"`
	// InvalidToolRate: steps calling a tool the request did not offer
	// (hallucinated tool names).
	InvalidToolRate float64 `json:"invalidToolRate"`
	// SchemaErrorRate: steps whose arguments miss a required field or are
	// not a JSON object.
	SchemaErrorRate float64 `json:"schemaErrorRate"`
	// InvalidCallRate: steps with either problem (what the exported
	// halo.eval.agent.invalid_call_rate samples per session).
	InvalidCallRate float64 `json:"invalidCallRate"`
	// PrematureStopRate: among referenced steps where production kept
	// working, the share where this arm ended its turn instead.
	PrematureStopRate float64 `json:"prematureStopRate"`
	// Fidelity: agreement (same tools) with the action production actually
	// took. The control's fidelity is the re-sampling noise floor.
	Fidelity Rate `json:"fidelity"`
}

AgentArm is one arm's per-step behaviour.

type AgentResult added in v0.3.0

type AgentResult struct {
	Steps    int `json:"steps"`    // pairs whose both actions parsed
	Sessions int `json:"sessions"` // distinct sessions among them
	// MultiTurn: steps past the first turn (turns: all).
	MultiTurn int `json:"multiTurn"`
	// Excluded, not steps: Auxiliary requests that offer no tools (compaction
	// summaries, next-speaker checks: both arms just answer), Truncated steps
	// (an arm hit its output cap), Retries (byte-identical resends of a step).
	Auxiliary int `json:"auxiliary"`
	Truncated int `json:"truncated"`
	Retries   int `json:"retries"`
	// ToolAgreement / ArgAgreement: candidate vs control replay, same prefix.
	ToolAgreement Rate     `json:"toolAgreement"`
	ArgAgreement  Rate     `json:"argAgreement"`
	Control       AgentArm `json:"control"`
	Candidate     AgentArm `json:"candidate"`
	// Referenced: steps whose production action was recovered.
	Referenced int `json:"referenced"`
	// FidelityDelta: candidate - control fidelity, paired by step,
	// session-clustered CI. Below zero beyond the noise floor = regression.
	FidelityDelta MetricDelta `json:"fidelityDelta"`
	// Diverged: share of sessions where the candidate's action differed from
	// the control replay at some step; FirstDivergence is the mean step at
	// which that first happened (among diverged sessions).
	Diverged        float64       `json:"diverged"`
	FirstDivergence float64       `json:"firstDivergence"`
	ByDepth         []DepthBucket `json:"byDepth"`
	// PerStep are the analysis samples the exporter may send, one per step.
	PerStep []StepSample `json:"-"`
}

AgentResult is the step-level reading of one shadow experiment. Rates are step-weighted (every step counts once); the exported analysis samples are per session (see StepSample), a different estimand that weights sessions equally.

func AnalyzeAgent added in v0.3.0

func AnalyzeAgent(pairs []shadow.Pair, seed uint64) *AgentResult

AnalyzeAgent computes the step-level reading over pairs whose both sides answered. It returns nil when no pair's actions could be parsed.

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 Call added in v0.3.0

type Call struct {
	Name  string `json:"name"`
	Input string `json:"input,omitempty"`
}

Call is one tool invocation; Input is canonical JSON (sorted keys).

type CaseSummary added in v0.3.0

type CaseSummary struct {
	ID           string     `json:"id"`
	Reasons      []string   `json:"reasons"`
	ExpectedFrom string     `json:"expectedFrom"`
	Expected     []string   `json:"expectedTools"`
	Provenance   Provenance `json:"provenance"`
}

CaseSummary is a case without its content (safe to print or return over MCP).

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 DepthBucket added in v0.3.0

type DepthBucket struct {
	Steps         string  `json:"steps"` // e.g. "0", "1-4"
	N             int     `json:"n"`
	ToolAgreement float64 `json:"toolAgreement"`
}

DepthBucket is tool agreement by conversation depth.

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
	// AllowPlaintext reads plaintext rows even with Keys set (a store sealed
	// part way through its life). Off by default: with keys configured an
	// unsealed row is refused (shadow.ErrPlaintextRow).
	AllowPlaintext bool
}

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 NewHTTPLLM added in v0.4.0

func NewHTTPLLM(c *JudgeConfig) (*HTTPLLM, error)

NewHTTPLLM validates c (https or loopback http, a known wire, a pinned model) and builds its client, reading the credential from c.APIKeyEnv now. The copilot (ADR-0035) shares it with the judge.

func (*HTTPLLM) Complete

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

func (*HTTPLLM) CompleteUsage added in v0.4.0

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

CompleteUsage is Complete plus the provider's reported token usage (both wires name it usage.input_tokens and usage.output_tokens).

type HarvestOptions added in v0.3.0

type HarvestOptions struct {
	// Experiment limits the harvest to one experiment ("" = all).
	Experiment string
	// Select lists the reasons that make a step a case (default DefaultSelect).
	Select []string
	// Scores are judge scores per pair id (HistoryScores); needed by judge-loss.
	Scores map[string]PairScore
	// JudgeLoss: control minus candidate judge score above which a step is
	// selected (default 0.2).
	JudgeLoss float64
	// PerDepth caps the stratified sample per depth bucket (default 5).
	PerDepth int
	Seed     uint64
	// Redactor is required: nothing is harvested unredacted.
	Redactor *Redactor
	Now      func() time.Time
}

HarvestOptions selects and redacts steps.

type HarvestResult added in v0.3.0

type HarvestResult struct {
	Steps int `json:"steps"` // parsed steps considered
	// Skipped: selected steps whose expected action was itself invalid
	// (a tool not offered, a missing argument), so not a usable test.
	Skipped  int            `json:"skipped"`
	ByReason map[string]int `json:"byReason"`
	Cases    []StepCase     `json:"-"`
}

HarvestResult is the harvested cases plus what was looked at.

func Harvest added in v0.3.0

Harvest reads the pair store, selects steps and returns them as redacted cases. Any redaction failure fails the whole harvest: nothing partial.

func (*HarvestResult) Summary added in v0.3.0

func (h *HarvestResult) Summary() []CaseSummary

Summary lists the cases by provenance and expected tool names only.

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). It limits
	// the judge only: the step-level reading needs every step of a session.
	Skip map[string]bool
	// AgentSkip holds step pair ids whose agent samples were already exported.
	AgentSkip map[string]bool
	// AgentQuiet: a step with no recovered production action is exported only
	// once it is this old (its next step may still arrive). Default
	// DefaultAgentQuiet.
	AgentQuiet time.Duration
	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; history keeps them
	// (pair ids and numbers only) for harvest's judge-loss selection.
	Scores []PairScore `json:"scores,omitempty"`
	// 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"`
	// Agent is the step-level reading (ADR-0014): next-action agreement,
	// fidelity to production's action, tool-call validity. Nil when no pair's
	// actions parsed.
	Agent *AgentResult `json:"agent,omitempty"`
	// AgentPairIDs are the steps whose agent samples this run exports (each
	// step exactly once across runs: the history records them).
	AgentPairIDs []string `json:"agentPairIds,omitempty"`
	// AgentSteps are those samples (not kept in history).
	AgentSteps []StepSample `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 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

type PairScore struct {
	ID        string  `json:"id"`
	Session   string  `json:"-"`
	Control   float64 `json:"control"`
	Candidate float64 `json:"candidate"`
}

PairScore is one pair's judge scores; each is one analysis sample of halo.eval.judge.score for its arm, under unit = hashed session (pairs of one session are correlated; the analysis takes the per-session mean). History keeps the id and scores, never the raw session, so `halo eval harvest` can select the pairs where the candidate lost.

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 Provenance added in v0.3.0

type Provenance struct {
	PairID         string    `json:"pairId"`
	Experiment     string    `json:"experiment"`
	Session        string    `json:"session"`
	Depth          int       `json:"depth"`
	HarvestedAt    time.Time `json:"harvestedAt"`
	RedactionRules string    `json:"redactionRules"`
}

Provenance says where a case came from. Session is hashed (SessionUnit): a raw session id is never written.

type Rate added in v0.3.0

type Rate struct {
	Mean float64 `json:"mean"`
	CILo float64 `json:"ciLo"`
	CIHi float64 `json:"ciHi"`
	N    int     `json:"n"`
}

Rate is a proportion with a session-clustered bootstrap CI.

type Redactor added in v0.3.0

type Redactor struct {
	// contains filtered or unexported fields
}

Redactor rewrites a shadow step so it can be written to disk.

func NewRedactor added in v0.3.0

func NewRedactor(patterns, keepFields []string) (*Redactor, error)

NewRedactor builds the redactor: the built-in rules plus operator regexes (RE2; every match becomes [REDACTED:custom]) and extra top-level request fields to keep on top of DefaultKeepFields.

func (*Redactor) Action added in v0.3.0

func (r *Redactor) Action(a Action) (Action, error)

Action redacts the arguments of every call. JSON arguments are walked and re-encoded in canonical form (sorted keys, as argCanon produces); freeform input (custom tools) is redacted as text.

func (*Redactor) Request added in v0.3.0

func (r *Redactor) Request(body []byte) ([]byte, error)

Request keeps only the allowlisted top-level fields of a model request and redacts every string in them. Any failure is an error, never a pass-through.

func (*Redactor) Text added in v0.3.0

func (r *Redactor) Text(s string, heuristic bool) string

Text redacts one string. heuristic enables the high-entropy rule (off for opaque provider fields).

func (*Redactor) Version added in v0.3.0

func (r *Redactor) Version() string

Version is the rule-set id recorded in provenance: the built-in version, plus a hash of the operator's patterns and extra fields when there are any.

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 StepCase added in v0.3.0

type StepCase struct {
	ID       string          `json:"id"`
	Protocol string          `json:"protocol"`
	Request  json.RawMessage `json:"request"` // redacted
	// Tools are the offered tools: name -> required argument keys (null for
	// freeform and server tools).
	Tools        map[string][]string `json:"tools"`
	Expected     Action              `json:"expected"`
	ExpectedFrom string              `json:"expectedFrom"`
	// Alternatives are also accepted: the control replay, when it differed
	// from production's action but was valid (the incumbent's own choice).
	Alternatives []Action   `json:"alternatives,omitempty"`
	Match        string     `json:"match"`
	Reasons      []string   `json:"reasons"`
	Provenance   Provenance `json:"provenance"`
}

StepCase is one harvested step: the request the coding agent really sent, the tools it offered and the action to reproduce. Graded by GradeStep.

func LoadStepCases added in v0.3.0

func LoadStepCases(dir string) ([]StepCase, error)

LoadStepCases reads every *.json case in dir, sorted by file name.

type StepModel added in v0.3.0

type StepModel struct {
	URL    string // base URL; https, or http to loopback only
	Model  string
	APIKey string
	HTTP   *http.Client // default: 300s timeout
}

StepModel replays harvested steps against one model route: a gateway (the model is then a gateway alias) or an upstream (the model is its id). The request goes out on the case's own wire protocol, non-streaming, through the same rewrite halo-shadow uses (gateway.ForceNonStreaming, gateway.RewriteRequest).

func (*StepModel) Answer added in v0.3.0

func (m *StepModel) Answer(ctx context.Context, c StepCase) (Action, []byte, error)

Answer sends c's request to the model and returns the action it took and the raw response.

type StepResult added in v0.3.0

type StepResult struct {
	ID        string `json:"id"`
	Session   string `json:"session"`
	Depth     int    `json:"depth"`
	Pass      bool   `json:"pass"`
	SameTools bool   `json:"sameTools"`
	SameCalls bool   `json:"sameCalls"`
	Valid     bool   `json:"valid"`
	// PrematureStop: the expected action kept working and the model stopped.
	PrematureStop bool     `json:"prematureStop"`
	Actual        Action   `json:"actual"`
	Error         string   `json:"error,omitempty"`
	JudgeScore    *float64 `json:"judgeScore,omitempty"`
	JudgeError    string   `json:"judgeError,omitempty"`
	// contains filtered or unexported fields
}

StepResult is one case's grade.

func GradeStep added in v0.3.0

func GradeStep(c StepCase, a Action) StepResult

GradeStep is the deterministic action matcher: the answer matches when it is SameTools (MatchTools) or SameCalls (MatchCalls) with the expected action or any allowed alternative, and passes when it also calls only offered tools with their required arguments.

type StepRunOptions added in v0.3.0

type StepRunOptions struct {
	Parallel int // concurrent model calls (default 4)
	Seed     uint64
	// Judge and Rubric, both or neither: a secondary rubric score per answered
	// case. It never changes Pass.
	Judge  *Judge
	Rubric *Rubric
}

StepRunOptions configures RunSteps.

type StepSample added in v0.3.0

type StepSample struct {
	PairID, Session string
	Time            time.Time
	// HasRef: production's action was recovered; the fidelity fields are set
	// only then.
	HasRef                      bool
	ControlFidelity, CandFid    float64
	ControlInvalid, CandInvalid float64 // 1 when the arm's call was invalid
}

StepSample is one step's per-arm reading. It is exported once (tracked by pair id in the history) under unit = hashed session, so the per-unit mean the analysis takes is a per-session mean over its steps, and a rerun never re-weights a session.

type StepScorecard added in v0.3.0

type StepScorecard struct {
	Suite    string `json:"suite"`
	Model    string `json:"model"`
	Cases    int    `json:"cases"`
	Sessions int    `json:"sessions"`
	Errors   int    `json:"errors"`
	// Pass counts an errored case as failed; the other rates are over
	// answered cases (PrematureStop: those whose expected action continued).
	Pass          Rate         `json:"pass"`
	SameTools     Rate         `json:"sameTools"`
	SameCalls     Rate         `json:"sameCalls"`
	Valid         Rate         `json:"valid"`
	PrematureStop Rate         `json:"prematureStop"`
	Rubric        string       `json:"rubric,omitempty"`
	Judge         *Rate        `json:"judge,omitempty"`
	JudgeErrors   int          `json:"judgeErrors,omitempty"`
	Results       []StepResult `json:"results"`
}

StepScorecard is a step suite's reading against one model. Every interval is a session-clustered bootstrap: steps of one session are correlated.

func RunSteps added in v0.3.0

func RunSteps(ctx context.Context, suite string, cases []StepCase, m *StepModel, o StepRunOptions) (*StepScorecard, error)

RunSteps replays every case against m and scores the answers.

func (*StepScorecard) Table added in v0.3.0

func (sc *StepScorecard) Table() string

Table renders the scorecard for a terminal.

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 TokenUsage added in v0.4.0

type TokenUsage struct {
	InputTokens  int `json:"input_tokens"`
	OutputTokens int `json:"output_tokens"`
}

TokenUsage is the token count the provider reported for one completion; zero when the reply carried no usage block.

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