Skip to main content
Evals measure what an agent (or a bare model, or a single skill) can do with the robot’s memory. Two kinds:
  • Passive — the world is a frozen memory2 recording. Deterministic, cheap, repeatable. Run these constantly.
  • Interactive — a live robot or sim; actions change the world; scoring samples the live memory2 store while the agent works.
memory2 is the source of truth for everything an eval sees: context selectors return real Streams, and interactive scoring reads a real Store.

Quick start (CLI)

Each run prints a per-case table and writes results.jsonl, summary.json, and per-case transcripts to ~/.local/state/dimos/evals/run-*/.

Your first eval, end to end

Build a tiny recording (any memory2 store works — this is the same API the robot’s Recorder uses; see dimos/memory2/intro.md for the full Stream API):
session=evals ansi=false no-result
session=evals ansi=false
A passive eval is one Python literal. context is a tuple of callables that receive the opened Store and return the mem2 Streams the model may see — anything the Stream API expresses (windows, filters, single frames) works, and the runner evenly downsamples each selected stream to context_budget observations before encoding:
session=evals ansi=false no-result
Run it. chat_model= injects any LangChain chat model — here a canned fake so this document runs offline; drop the argument to use the production model config (gpt-5.6-luna, same construction as the deployed McpClient):
session=evals ansi=false
That’s the whole loop: dataset -> context streams -> encoded prompt -> model -> parse -> score -> artifacts.

Scoring

Scores are floats in [0, 1]; passed = score >= threshold. Scorers are plain functions (expected, got) -> float — a custom heuristic is a lambda, not a class:
session=evals ansi=false
  • exact — equality (the default). Pair with a parser (yes_no, choice, int) so formatting noise doesn’t fail a correct answer.
  • within(band) — graded numeric credit: 1.0 exact, 0.5 halfway, 0 outside.
  • ramp(distance, band) — same ramp over meters; msg types support arithmetic, so physical scorers stay one-liners: lambda s: ramp((GOAL - s.streams.odom.last().data.position).length(), band=0.5)
  • judge(rubric) — LLM-as-judge with partial credit, wrapping the langchain/openevals standard (inputs/reference_outputs convention, so external VQA benchmarks map on natively).
Interactive evals score a series (one sample per interval_s); aggregate reduces it:
session=evals ansi=false
final = “where did it end up”, floor = “never left the zone”, mean = “how good was it throughout”.

Interactive evals

The case names its environment (reproducibility); score reads the live store the robot’s Recorder writes, sampled every interval_s:
session=evals ansi=false no-result
The result carries the full (t, score) series — “reached the bed at t=50s and stayed” and “grazed it at the deadline” score differently under floor vs final.

Running

  • CLI: dimos evals run <dotted.suite> [--tags nav --blind --limit 5 --model gpt-4o]
  • Python: EvalRunner(...).run(SUITE, tags=frozenset({"encoding"}))
  • pytest: suites are importable lists — @pytest.mark.parametrize("case", SUITE) and assert on passed (gate live-model tests with skipif_no_openai).
  • MCP: the EvalModule skills run_evals / list_eval_suites return the summary + run dir, so a coding agent can run evals, grep transcripts, edit prompts/encodings, and run again.
  • Blind ablation: EvalRunner(blind=True) withholds all observations. A case that still passes blind is guessable — fix its distractors. Run every new suite sighted and blind once before trusting it.
  • Preflight: before anything runs, every case is checked against the rig — a missing stream fails with "No stream 'lidar'. Available: [...]", a case needing MCP/sim fails with what’s missing. Errors are per-case; one broken case never kills a run.