Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

.nornir/bakeoff.md

H5 — Agent × model bake-off MATRIX (nornir bakeoff)

Design + guide + idiot-guide for the H5 epic: ONE standardized way to pit a task against a cross-product of agents × models, land the results in the warehouse, and watch which agent+model combo won in a viz Leaderboard pane that shows the agent × model grid. Mirrors the nornir test run→persist→view shape and the release_events / test_results warehouse-row pattern, and reuses the H1 MCP-exercising + C6 run→persist→live shape.

1. What it is

nornir bakeoff run --task "…" --agents a,b --models x,y runs ONE task across the agents × models cross-product, recording one agent_model_runs row per (task, agent, model) cell — its answer plus score, wall_ms, tokens/s, cost, mcp_tool_calls — into the warehouse under a fresh run_id, and prints the leaderboard. The agent is the caller/harness identity (claude, kilo, a local-LLM driver); the model is what it drove. The viz 🏆 Leaderboard tab reads the same table and renders the agent × model grid with the winning cell highlighted; CLI parity is total — the pane shows nothing nornir bakeoff leaderboard --matrix can't print.

A model-only bake-off is the 1×N matrix under a single local-LLM agent, so the pre-matrix usage (--models … with no --agents) still works unchanged.

nornir bakeoff demo                                          # seed a mock matrix
nornir bakeoff run --task "Fix the bug" --agents claude,kilo --models opus,mistral
nornir bakeoff run --task "…" --mock                         # offline canned cells
nornir bakeoff run --task "…" --models a,b,c                 # 1-agent (model-only)
nornir bakeoff leaderboard                                   # newest run, ranked
nornir bakeoff leaderboard --matrix                          # agent × model grid
nornir bakeoff leaderboard <run_id|model|agent> --json       # raw rows

The --prompt / --prompt-id flags are kept as visible aliases of --task / --task-id for back-compat.

nornir bakeoff run --task T --agents a,b --models x,y
└─ ModelCaller
   ├─[--mock]  MockCaller (canned ModelAnswer per (agent,model) cell) ─┐
   └─[default] OllamaCaller (POST {host}/api/generate) ────────────────┤
                                                                       ▼
   run_bakeoff_matrix: one AgentModelRunRow per (agent,model) cell (shared run_id + ts)
   └─▶ append_agent_model_runs → Iceberg agent_model_runs
      ├─▶ matrix_grid() ranks cells → MatrixGrid
      │   ├─▶ render_matrix / render_leaderboard (CLI)
      │   └─▶ 🏆 Leaderboard pane grid (state_json.matrix)
      └─▶ leaderboard() flat ranking
          └─▶ render_matrix / render_leaderboard (CLI)

2. The bake-off backend (src/warehouse/agent_model_runs.rs)

The only thing that touches an actual LLM / agent harness is the [ModelCaller] trait — now agent-aware (the matrix's first axis):

pub trait ModelCaller {
    fn call(&self, agent: &str, model: &str, prompt: &str) -> Result<ModelAnswer>;
}

ModelAnswer carries the economics: output, latency_ms, tokens_in/out, tokens_per_s, score, cost_usd, mcp_tool_calls. Two impls ship:

run_bakeoff_matrix(caller, run_id, ts, task_id, task, agents, models) is the pure cross-product→call→row pipeline both the CLI and the tests share. It does NOT touch the warehouse (the caller appends the returned rows), so a test runs a full matrix with a MockCaller and asserts the rows before any I/O. Row order is the (agent, model) cross-product — stable + testable. run_bakeoff(...) is the single-agent shim (agents = [AGENT_UNSET]). tokens_per_s is taken from the backend when supplied, else derived from tokens_out / (latency_ms/1000).

The default model set (default_models()) is the embedding-model registry's ids (src/vector/embed_registry.rs); the default agent set (default_agents()) is a single local-LLM agent. When the vector feature is off, the model default falls back to the same ids as a literal.

3. Warehouse table agent_model_runs

Schema: iceberg_schema::agent_model_runs (field ids 1–16). One row per (run_id, agent, model, prompt_id) cell:

column type meaning
run_id string bake-off-run identity (groups a run's rows) — partition key
ts_micros timestamptz run time (shared across a run's rows)
agent string? matrix axis 1 — caller/harness (claude/kilo/local-llm); legacy null → -
model string matrix axis 2 — the model driven (ollama tag / registry id)
prompt_id string stable task handle (so two runs compare)
prompt string the task text
output string the cell's completion ("" = none, never null)
latency_ms double wall-clock call time (wall_ms)
tokens_in long prompt token count
tokens_out long completion token count
tokens_per_s double decode throughput
score double 0.0..=1.0 quality — set by the codegen judge (see .nornir/bakeoff-judge.md) when --judge-crate is passed; 0.0 if unscored
ok bool did the cell answer without error?
error string? error detail when ok=false (null on success)
cost_usd double? USD price of the cell (0.0 = free/local)
mcp_tool_calls long? MCP tool calls the agent made answering

Fields 14–16 (agent, cost_usd, mcp_tool_calls) are OPTIONAL so the schema is migration-safe: rows written before the matrix carry none, and the reader defaults them back to agent = "-" (AGENT_UNSET), cost_usd = 0.0, mcp_tool_calls = 0. The serde struct also #[serde(default)]s them, so a thin client decoding a pre-matrix server's JSON still parses.

Partitioned by run_id: a bake-off writes every cell in one append (one batch = one partition), and the leaderboard reads a whole run back, so run_id prunes scans. Per-model / per-agent history reads do a residual filter.

Writer/reader (src/warehouse/agent_model_runs.rs, mirrors test_results.rs):

4. The viz Leaderboard pane (src/viz/leaderboard_tab.rs)

The 🏆 Leaderboard tab reads agent_model_runs read-only + lock-tolerant locally, or over the Viz.BakeoffResults gRPC remotely (decoding into the same AgentModelRunRow, so the grid renders byte-identically thin):

LAW #6 — the pane exposes exactly what it renders as data via state_json()["leaderboard"]: the run summaries, the flat leaderboard (with agent/model/cell/cost_usd/mcp_tool_calls), and the matrix grid (agents, models, cells with per-cell rank, winner) — folded into the app's NORNIR_VIZ_STATE dump and the $NORNIR_VIZ_TRACE stream.

5. Tests (LAW #1 — inject real values, assert real output)

None require a running LLM or Claude — the backend is mockable by construction.

6. CLI parity

GUI (🏆 Leaderboard pane) CLI twin
run list (newest first) + winning cell nornir bakeoff leaderboard
agent × model grid for a run nornir bakeoff leaderboard --matrix
ranked cells for a run nornir bakeoff leaderboard <run_id>
per-model / per-agent history nornir bakeoff leaderboard <model|agent>
raw rows nornir bakeoff leaderboard --json
(seed a demo matrix) nornir bakeoff demo
(produces the data) nornir bakeoff run --task … [--agents …] [--models …] [--mock] [--matrix]