A local, stdlib-only benchmark for evaluating email agents on realistic support, sales, and scheduling workflows.
The benchmark asks an agent to handle an inbound email thread using a fake but consistent company world: inbox threads, CRM records, calendar availability, and knowledge-base policies. A run is graded from the actions the agent records in a ledger plus the tool calls it made while gathering evidence.
This benchmark is meant to test whether an agent can:
- read the right email thread
- retrieve supporting CRM, calendar, and policy context
- choose the correct primary workflow action
- create the right draft, forward, escalation, meeting, send, or ignore record
- include required facts from the fake world
- avoid unsafe or unsupported claims
- use the expected classes of tools instead of guessing
It is not just a text-generation benchmark. The grader looks at durable workflow
state in ledger.json, not only at a free-form answer.
Each task requires a small workflow:
- Inspect the inbound thread.
- Use tools to gather missing context.
- Decide what action is safe and policy-compliant.
- Mutate the run ledger through an action tool or adapter.
- Produce artifacts that can be inspected after the run.
For example, a good scheduling agent should read the request, identify the account owner, check calendar availability, avoid busy slots, and either schedule the meeting or draft alternatives.
The benchmark data lives under data/:
data/inbox.json
Inbound threads and message history.
data/crm.json
Contacts, companies, customer tier, plans, renewal dates, and account owners.
data/calendar.json
Availability windows and busy events for company users.
data/kb/*.md
Policy documents for refunds, pricing, escalation, scheduling, and security.
Agents access this world through server/email_mcp.py or through the local
adapter arguments used by the starter agents.
The MCP server exposes read tools such as get_email_thread,
search_previous_emails, lookup_customer, lookup_company, search_crm,
search_kb, and get_calendar_availability. It also exposes action tools such as
create_draft, send_email, forward_email, schedule_meeting,
escalate_email, and mark_ignore.
Tasks live in tasks/tasks.json. Each task defines:
id: stable task id, usually matching the target thread idprompt: instruction passed to the agentthread_id: email thread being handledexpected_action: primary action the agent should takerequired_fact_ids: stable fact IDs that must be recorded in action evidencerequired_facts: legacy prose facts used as a non-blocking text checkforbidden_claims: claims that must not appearexpected_tool_categories: required evidence sources, such asinbox,crm,calendar, andkbexpected_ledger: expected counts for ledger action buckets
Human-readable notes for the starter tasks live in tasks/GROUND_TRUTH.md.
Agent adapters are configured in agents.json.
Current agents:
baseline_no_tools: reads only inbox data and does not mutate the ledger. It is expected to fail most action-oriented tasks and is useful as a baseline.scripted_tool_agent: deterministic smoke-test adapter that performs the intended workflow for the six MVP tasks. It validates the runner, ledger, tool-call logging, and grader.codex: real Codex CLI adapter that connects toserver/email_mcp.pyas a stdio MCP server. It requires a logged-in Codex CLI plus provider network access, and it runs from the per-run artifact directory so task data should be reached through MCP tools rather than direct file reads. The headless adapter uses--dangerously-bypass-approvals-and-sandboxbecause this Codex CLI build otherwise auto-cancels MCP tool calls in non-interactive runs.
The first two adapters are intentionally simple. Future model-backed adapters can
be added to agents.json as long as they use the same fake tools and write the
expected run artifacts through the provided paths.
From the repo root:
python3 runner.py --agents baseline_no_tools,scripted_tool_agent
python3 report.pyRun a subset:
python3 runner.py --agents scripted_tool_agent --tasks support_001,sched_001
python3 report.pyRun the real Codex MCP adapter:
python3 runner.py --agents codex --tasks support_001 --timeout 420
python3 report.pyWhen comparing agents, keep the evaluation mode the same. Do not compare an MCP agent against an uploaded-file/manual-chat run unless the report labels those as different modes.
Run repeated trials:
python3 runner.py --agents scripted_tool_agent --tasks support_001 --reps 3 --runs-dir runs
python3 report.py --runs-dir runsUseful help commands:
python3 runner.py --help
python3 report.py --help
python3 grading/grader.py --helpStart the local browser dashboard:
python3 web/app.pyThen open:
http://127.0.0.1:8798
The dashboard shows agents, starter and hard task catalogs, the latest pass/fail
results, and per-run artifacts. Select a result row to inspect ledger.json,
tool_calls.jsonl, result.json, stdout trace, and stderr. The run controls can
launch selected agents and tasks from the browser.
The runner writes one directory per agent and task:
runs/<agent>/<task>/
ledger.json
tool_calls.jsonl
trace.jsonl
stderr.log
result.json
For agents with uses_mcp: true, the run directory also includes
mcp_config.json.
Repeated runs add a suffix:
runs/<agent>/<task>_r1/
runs/<agent>/<task>_r2/
The runner also writes:
runs/summary.json
Artifact meanings:
ledger.json: workflow state created by the agent, including drafts, sent emails, forwards, escalations, meetings, ignored threads, CRM updates, and structured evidence fact IDs attached to actionstool_calls.jsonl: JSONL log of tool name, category, arguments, success, and error textmcp_config.json: per-run MCP server configuration for agents that need ittrace.jsonl: captured stdout from the agent commandstderr.log: captured stderr, timeout, or launch errorsresult.json: deterministic grade for one runsummary.json: list of run results from the latest runner invocation
result.json and report.py use these core fields:
passed: true when there are no grading reasonsreasons: human-readable failures, such as missing ledger actions, missing structured fact IDs, forbidden claims, missing tool categories, failed action tool calls, or calendar conflictswarnings: non-fatal issues, including malformed tool logs or failed tool callsaction_accuracy: whether the expected primary ledger action was recordedrequired_fact_ids_ok: whether required fact IDs were recorded in action evidenceforbidden_claims_ok: whether prohibited claims were avoidedcalendar_ok: whether scheduled meetings satisfy calendar constraintstool_categories_ok: whether the expected source categories were usedevidence_fact_ids: fact IDs recorded on the expected actionn_tool_calls: number of logged tool callsn_failed_tool_calls: number of logged failed tool callswall_s: wall-clock runtime in seconds, added byrunner.py
report.py summarizes the latest runs/summary.json when present, falling back
to runs/*/*/result.json files if no summary exists. It prints a task-by-agent
matrix, per-agent pass rate, action accuracy, evidence coverage, tool category
coverage, safety checks, calendar checks, average tool calls, failed tool call
counts, average wall time, and failure reasons.
- The benchmark has starter tasks in
tasks/tasks.jsonand a hard suite intasks/tasks_hard.json. Real CLI-agent runs depend on the configured CLI's local auth and network availability. - The deterministic scripted agent is a wiring check, not a real model result.
- Grading is intentionally exact and local. Required facts are checked through
structured evidence fact IDs when
required_fact_idsare present. Legacy prose fact checks are only used for older tasks that do not define structured fact IDs. Forbidden claims are still checked with string matching over the relevant ledger text. - The grader checks tool categories, not full reasoning quality.
- Tone, concision, and user experience quality are only indirectly evaluated.
runs/is ignored by git, so benchmark outputs are local unless explicitly copied elsewhere.
Use QUERY_GUIDE.md before adding or changing tasks. In short: create a
realistic thread, make the needed evidence available in CRM/calendar/KB data,
choose one expected primary action, and write grading fields that are specific
enough to be deterministic.