Before you begin
Before you start, make sure you have:- Working eval: An
Evalcall that runs, as in Run your first eval. - Agent entry point: A function that takes a user message and returns the agent’s reply, with its tools callable from your code.
- Side effects listed: The tools that send email, issue refunds, write to production, or change anything else.
1
Wrap agent as task
Scorers see the case and, of the run, only what the task returns. Give the agent a thin adapter that returns the answer plus the tool calls it made.Record tool calls at the tool boundary, where they execute, not from what the agent says it did: a model’s own account can differ from what ran. Here each tool is traced and records its calls into a per-case list. The list is a context variable, so concurrent cases do not mix their calls.Pass the logger to
Eval as trace_logger in Python or logger in TypeScript, so your agent’s spans nest under each case. Model calls, tool spans, tokens, cost, and latency then land on the case’s trace without the task returning them.Model and framework spans appear when their instrumentation is installed. In Python, auto_instrument() enables installed OpenTelemetry instrumentors for providers such as OpenAI and Anthropic, and frameworks such as LangChain, CrewAI, and the OpenAI Agents SDK. In TypeScript, wrap the Vercel AI SDK with wrapAISDK, or set ATLAN_INSTRUMENT=pi to trace a pi agent (see Use pi with Atlan). See Agent frameworks. Every eval case is traced in full: Eval refuses a logger that samples or drops content.2
Stub tools with side effects
Give the agent a stand-in for each such tool. It records the call, so scorers still see it, and returns a plausible result without acting.Pass the stand-ins where your agent looks up its tools, such as its tool registry or constructor. Keep each stand-in’s name and signature identical to the real tool’s, so the tool schema the model sees does not change.
3
Score tool calls
Put what a good trajectory is in each case’s In TypeScript, declare
expected: the expected tools and their arguments. These scorers read output["tool_calls"] (output.toolCalls in TypeScript) and expected["tools"] as lists of {name, args}. Both languages read the same expected keys, so one dataset serves both.expected optional and return null when it is missing; a required expected fails tsc --strict. Compare structured values with same, not raw JSON.stringify: expected read from a dataset comes back with its object keys in a different order, and a raw comparison scores 0 on every case. tool_arguments matches each expected call to at most one actual call, so one call cannot satisfy two expected calls to the same tool.Pick the scorers for the questions you need answered:Compare arguments on the fields that matter. Normalise or drop volatile ones, such as timestamps and request IDs, or the score measures noise.
4
Run eval on agent
Run the agent, the scorers, and
Eval from one file. Set subject_kind and subject_id so the run appears on the agent’s page in the app, and so you can list its history.subject_kind is agent, harness, or skill, and subject_id must be an artifact of that kind you can read.The experiment does not record which version of the agent ran. Put the agent version and prompt revision in config, or pin them with a context manifest. In the CI action, Python Eval records the commit, branch, and pull request in config.git, and the action’s model input in config.model, unless your config already has a git or model key; a TypeScript eval records them only through the results-file shim.If the task raises, that case records an error and its scorers do not run; the run still returns. If no case produces a score, Eval completes the experiment and then raises, with the experiment ID on the exception. Pass require_scores=False (requireScores: false) to get the result back instead.5
Verify trajectory and cost
-
Check scores: No entry in
run.resultshas anerror, andrun.summary["scores"]has one entry per scorer. -
Check trace: Each case’s trace has this shape:
-
Check cost and tokens: They live on the traces, not in the summary. Query them per experiment with trace stats. A scalar query returns one number per measure; a table query grouped by
score_namereturns the mean of each score across the run’s score spans.Token and cost values come from model spans that report usage; an agent without instrumented model calls shows0. To enforce a budget, compare these numbers in your CI script. The CI action reports them but never fails a check on them.
Eval runs each case once. To measure variance, run the same eval several times with a trial field in config and compare the means:0 where your agent allows it. A gate threshold narrower than the run-to-run spread fails at random: widen it, or gate on more cases.6
Score multi-turn conversation (optional)
Put the user turns in Scorers can judge the final turn, every turn, or the whole trajectory. In a dataset, store the turns under the
input as a list. The task replays them against your agent with the history so far, and returns the final reply as answer plus every tool call across the turns. It keeps the single-turn output shape, so the scorers above work on both.input_key, for example {"question": ["...", "..."]}. To serve single-turn and multi-turn records from one dataset, normalise first: turns = question if isinstance(question, list) else [question].Troubleshooting
Agent eval failures include a task that raises on every case, a TypeScript comparison that scores0 because of key order, and a logger that samples or drops content. See Eval errors.
Next steps
- Gate pull requests with evals: Run this eval on every pull request and fail the check on a regression.
See also
- Write code scorers: Scorer return shapes and a catalog to copy.
- Configure eval runs: Concurrency, hooks, resume, and pinned context.
- Compare two agent versions: Diff a baseline and a candidate case by case.