How to score your agent's behavior with eval
Murmur's eval system lets you run your capsule against a dataset of test cases and automatically score each session. Scores are written to eval.jsonl alongside trace.jsonl in each session workdir and can be compared across runs with mur eval diff. This guide shows how to wire up murmur-hook-eval, write a dataset, run it, and interpret the results.
The relevant manifest options are:
| Option | Controls |
|---|---|
| observability.eval.scorers | List of scorer configurations; at least one required to activate the hook |
| observability.eval.dataset_id | Label applied to every dataset_run record in eval.jsonl |
Step 1 — declare murmur-hook-eval in the manifest
murmur-hook-eval is a hook artifact — it runs at fixed lifecycle points and writes scores. Create a murmur.yaml file with runtime: hook and configure scorers under observability.eval:
name: my-agent
version: "0.1.0"
artifacts:
- name: murmur-driver-anthropic
version: "1.0.0"
runtime: driver
- name: murmur-hook-eval
version: "1.0.0"
runtime: hook
capabilities:
filesystem:
scope: . # the hook writes the session's eval.jsonl scores here; hooks get no filesystem by default
- name: murmur-tool-editor
version: "1.0.0"
runtime: tool
capabilities:
network:
allow:
- https://api.anthropic.com
filesystem:
allow:
- .
inference:
transport: http
endpoint: https://api.anthropic.com
model: claude-sonnet-5
api_key: ${ANTHROPIC_API_KEY}
driver:
artifact: murmur-driver-anthropic
observability:
eval:
dataset_id: my-dataset
scorers:
- type: exit_ok
name: success_check
- type: max_turns
name: turn_limit
max: 5
- type: max_tokens
name: response_size
max: 3000
- type: tool_sequence
name: tool_order
expected: [murmur-tool-editor]
name: my-agent
version: "0.1.0"
artifacts:
- name: murmur-driver-openai
version: "1.0.0"
runtime: driver
- name: murmur-hook-eval
version: "1.0.0"
runtime: hook
capabilities:
filesystem:
scope: . # the hook writes the session's eval.jsonl scores here; hooks get no filesystem by default
- name: murmur-tool-editor
version: "1.0.0"
runtime: tool
capabilities:
network:
allow:
- https://api.openai.com
filesystem:
allow:
- .
inference:
transport: http
endpoint: https://api.openai.com
model: o3-mini-high
api_key: ${OPENAI_API_KEY}
driver:
artifact: murmur-driver-openai
observability:
eval:
dataset_id: my-dataset
scorers:
- type: exit_ok
name: success_check
- type: max_turns
name: turn_limit
max: 5
- type: max_tokens
name: response_size
max: 3000
- type: tool_sequence
name: tool_order
expected: [murmur-tool-editor]
name: my-agent
version: "0.1.0"
artifacts:
- name: murmur-driver-deepseek
version: "1.0.0"
runtime: driver
- name: murmur-hook-eval
version: "1.0.0"
runtime: hook
capabilities:
filesystem:
scope: . # the hook writes the session's eval.jsonl scores here; hooks get no filesystem by default
- name: murmur-tool-editor
version: "1.0.0"
runtime: tool
capabilities:
network:
allow:
- https://api.deepseek.com
filesystem:
allow:
- .
inference:
transport: http
endpoint: https://api.deepseek.com
model: deepseek-r1
api_key: ${DEEPSEEK_API_KEY}
driver:
artifact: murmur-driver-deepseek
observability:
eval:
dataset_id: my-dataset
scorers:
- type: exit_ok
name: success_check
- type: max_turns
name: turn_limit
max: 5
- type: max_tokens
name: response_size
max: 3000
- type: tool_sequence
name: tool_order
expected: [murmur-tool-editor]
What each scorer does:
| Scorer type | Passes when |
|---|---|
exit_ok |
The session exited with status: ok (not failed or max_turns_reached) |
max_turns |
The session used at most max inference turns |
max_tokens |
Total input + output tokens did not exceed max |
tool_sequence |
The expected list is a subsequence of the observed tool calls (same order, gaps allowed) |
The name field for each scorer is the key that appears in eval.jsonl score records.
Step 2 — install dependencies
mur install
Different ways to install artifacts
mur install needs to know where to fetch artifacts from. You have two options:
Option A — configure a registry source in ~/.murmur/config.yaml:
registry:
default: official
sources:
- name: official
type: github
repo: <owner>/<repo>
token: "${GITHUB_TOKEN}"
Then install by artifact name and version:
mur install <artifact-name@version>
Option B — pass a full GitHub reference and skip configuration entirely:
mur install github:<username>/<repo>@<tag>
See Installing artifacts to learn more.
Step 3 — write a dataset
A dataset is a JSONL file — one JSON object per line, one line per test case. The conventional location is eval.jsonl in the project directory — the same folder as murmur.yaml, where mur eval run looks by default.
Create one task file in the project root:
task.md:
Read MURMUR.md, then draft a one-sentence summary of the Murmur project. Reply with only the final sentence.
The task starts with a tool call — the agent reads MURMUR.md (the capsule environment reference injected into every workdir) before answering. This is important: tool_sequence applies to every case in the dataset, so every case must call the tool. Grounding the task in a file read makes that natural.
The same task runs for all four cases — what differs is the case_id, which names the scorer each case is primarily meant to verify.
Then create eval.jsonl:
{ "case_id": "exit-check", "task_path": "task.md" }
{ "case_id": "turns-check", "task_path": "task.md" }
{ "case_id": "size-check", "task_path": "task.md" }
{ "case_id": "tool-check", "task_path": "task.md" }
Each task_path points to a file that is copied into the capsule workdir as task.md before the session starts. exit-check verifies success_check (clean exit); turns-check verifies turn_limit (read + answer takes two inference turns, well within five); size-check verifies response_size (MURMUR.md content adds roughly 1200 tokens to the second turn, bringing the total to ~2450, within the 3000-token budget); tool-check verifies tool_order (the editor was called before the answer).
The case_id is injected into the hook environment as MURMUR_CASE_ID and appears in every dataset_run record — it's how you identify each case in the output.
Step 4 — run the dataset
Run from the directory containing murmur.yaml:
mur eval run
Both arguments default: capsule to the current directory and dataset to eval.jsonl. To override either:
mur eval run ./other-capsule # explicit capsule
mur eval run --dataset ./other.jsonl # explicit dataset
mur eval run ./other-capsule --dataset ./other.jsonl # both explicit
mur eval run runs one capsule session per case, sequentially. For each case it:
- Stages the session — allocates a per-session workdir (
workdir/ses_xxx/) and loads artifacts - Copies
task_pathinto the session workdir astask.md - Launches the session
- Reads
eval.jsonlfrom the session workdir (written by the hook at session end) - Prints a result line
Output as cases complete:
Running 4 case(s) …
case: exit-check
result: pass session: ses_6801f81dd28b4a9daf434e8324c4793e
case: turns-check
result: pass session: ses_7902a92ee39c5baebf545f9435d5804f
case: size-check
result: pass session: ses_9b14c03df47e6a1c84523f7896d5920f
case: tool-check
result: pass session: ses_ab25d04ef58f7643bce9fbd67da681c3
After all cases:
── Summary ──────────────────────────────────────
pass: 4/4
exit-check pass response_size=1.00 success_check=1.00 tool_order=1.00 turn_limit=1.00 (/path/to/workdir/ses_6801...)
turns-check pass response_size=1.00 success_check=1.00 tool_order=1.00 turn_limit=1.00 (/path/to/workdir/ses_7902...)
size-check pass response_size=1.00 success_check=1.00 tool_order=1.00 turn_limit=1.00 (/path/to/workdir/ses_9b14...)
tool-check pass response_size=1.00 success_check=1.00 tool_order=1.00 turn_limit=1.00 (/path/to/workdir/ses_ab25...)
All four cases pass every scorer. Reading MURMUR.md takes one inference turn (the tool call) and answering takes a second, so turn_limit sees two turns. The tool result injects MURMUR.md's content into the second turn's context, which pushes total tokens to around 2450 — that's why max_tokens is set to 3000 rather than something tighter.
Step 5 — read the eval output
Each case produces its own eval.jsonl in the session workdir. Inspect the most recent session, or any session by suffix:
mur eval show # most recent session
mur eval show 6801 # session whose ID ends in "6801"
Output:
── Eval: workdir/ses_6801.../eval.jsonl ─────────────────────────────────────
── Scorers ──────────────────────────────────────
response_size 1/1 pass (100.0%)
success_check 1/1 pass (100.0%)
tool_order 1/1 pass (100.0%)
turn_limit 1/1 pass (100.0%)
── Overall ──────────────────────────────────────
result: pass
case: exit-check
dataset: my-dataset
── Score summary ────────────────────────────────
response_size 1.0000
success_check 1.0000
tool_order 1.0000
turn_limit 1.0000
── Worst events ─────────────────────────────────
(no failing events)
The Worst events section lists the individual scoring events that failed — useful for diagnosing which specific turn or tool call tripped a scorer. Here it's empty because all four scorers passed.
For programmatic use:
mur eval show --json
{
"overall": "pass",
"scorers": {
"response_size": { "pass": 1, "fail": 0, "total": 1, "pass_rate": 1.0 },
"success_check": { "pass": 1, "fail": 0, "total": 1, "pass_rate": 1.0 },
"turn_limit": { "pass": 1, "fail": 0, "total": 1, "pass_rate": 1.0 }
},
"dataset_run": { "overall": "pass", "scores": { "response_size": 1.0, "success_check": 1.0, "turn_limit": 1.0 } }
}
Step 6 — compare two eval runs
After re-running the dataset (for example, after changing the system prompt or swapping the model), compare the new results against a baseline to check for regressions:
mur eval diff 21f7 93a8
Output when both runs pass all scorers:
Scorer Run A Run B Delta
──────────────────────── ────────────── ────────────── ──────────────────────────
response_size 100.0% 100.0% =
success_check 100.0% 100.0% =
tool_order 100.0% 100.0% =
turn_limit 100.0% 100.0% =
overall pass pass
If a scorer regressed — for example response_size dropped from 100% to 0% after a prompt change — the delta would show -100.0pp (A better) and overall would change to fail. That is the signal to investigate.
Step 7 — interpret a failing scorer
Each failing scorer tells you something specific:
| Scorer fails | What it usually means |
|---|---|
exit_ok |
The session errored out or hit the turn cap — check the trace for the exit status |
max_turns |
The agent needed more turns than the budget allows — consider raising max or tightening the system prompt |
max_tokens |
The session used more tokens than the budget — the agent is producing verbose output; try a more constrained prompt or a lower-verbosity model |
tool_sequence |
The agent called tools in the wrong order or skipped a required tool — check the inference events in the trace |
When a scorer fails unexpectedly, open the companion trace for the same session:
mur trace show
Different ways to identify a session
mur trace show locates a session three ways:
No argument — most recent session:
mur trace show
Finds the lexicographically largest ses_* entry in workdir/ — always the most recently created session, because session IDs are UUID v7 (time-ordered).
Short suffix — type the last few characters:
mur trace show 3e4b
Matches any session whose ID ends with the given string. Use 4 or more characters to avoid ambiguity. Matching is case-insensitive. If the suffix matches more than one session, mur lists the candidates and asks you to be more specific.
Full session ID:
mur trace show ses_6801f81dd28b4a9daf434e8324c4793e
Resolves the session directly without scanning workdir/.
Legacy file path:
mur trace show path/to/trace.jsonl
Any argument containing / or ending in .jsonl is treated as a literal file path. Kept for backward compatibility.
Use --workdir <path> if your session directory is not ./workdir.
Other trace exploration commands
mur trace has four subcommands for exploring session output:
mur trace show — print the full trace for a session to the terminal. Accepts a session ID, suffix, or no argument (most recent session).
mur trace steps — show a turn-by-turn summary of what the agent did in a session. Pass --verbose to include a truncated summary of each tool's input.
mur trace diff — compare the traces of two sessions side by side. Useful for spotting behavioural regressions between runs.
mur trace report — generate a structured summary report from a session's trace. Covers token usage, tool calls, latency, and other session-level metrics.
Summary
| Step | What you do |
|---|---|
Declare murmur-hook-eval with runtime: hook |
Activates the eval hook for this capsule |
Grant it capabilities.filesystem.scope on its own entry |
Lets it write eval.jsonl; hooks have no filesystem access by default (see Hook capabilities) |
Declare murmur-tool-editor with runtime: tool |
Gives the agent file-read capability; required for tool_sequence cases |
Configure observability.eval.scorers |
Defines the scoring criteria — all scorers run for every case |
Write eval.jsonl |
One case per line, each with a case_id and task_path |
mur eval run |
Runs all cases against eval.jsonl and prints a pass/fail summary |
mur eval show <eval.jsonl> |
Shows which scorers passed, which failed, and why |
mur eval diff <a> <b> |
Compares two runs to check for regressions |