Skip to content

Agent Traces and Agent Evaluation

The trace displays, the twenty evaluation schemes, and how they bind. It ships with the potato-skill skill:

/plugin marketplace add davidjurgens/potato-skill
/plugin install potato-skill@potato

The largest family in Potato and the one least like text annotation: about twenty schema types, eight trace displays, and sixty example projects (57 under examples/agent-traces/, 3 under examples/agent-testing/). Its binding model fails silently when you get it wrong.

How a trace scheme finds its steps

Almost every scheme in this family reads a list off the item and renders one card per entry. The key it reads is configurable and defaulted:

Key Default Read by
steps_key steps gui_trajectory, tool_call_review, process_reward, failure_attribution, agent_scorecard, trajectory_eval, trajectory_edit, multimodal_reasoning, emergent_behavior, handoff_review, agent_interaction_graph
turns_key varies consensus_tracking, context_attribution
agent_key varies the multi-agent schemes, for who is speaking
step_text_key varies which field of a step holds the text to show

Almost nothing in this family has a required field. rubric_eval needs criteria; the rest need only name and description. So a scheme pointed at a key your data does not have renders an empty card list, validate --strict passes, and the boot log says nothing. If a trace widget comes up blank, check the key name before anything else:

python -c "import json;d=json.load(open('data/traces.json'));print(list(d[0]))"

Answers are stored as a hidden-input JSON list keyed by step index, and only judged steps are saved — the list is not positional, so do not read it as one entry per step.

Routing from what they said

They said Reach for
"was each step right" trajectory_eval (taxonomy + severity) or tool_call_review (right tool / args / ordering)
"where did it first go wrong" process_reward in first_error mode
"label every step for PRM training" process_reward in per_step mode
"fix the bad steps, don't just score them" trajectory_edit — produces SFT/DPO data
"which agent caused the failure" failure_attribution
"rate each agent and the team" agent_scorecard
"did the handoff lose something" handoff_review
"are they colluding / groupthinking / drifting" emergent_behavior
"who used the shared resource when" tool_contention
"how does the conversation use earlier context" context_attribution
"did they actually reach consensus" consensus_tracking
"draw me the interaction graph" agent_interaction_graph
"did the click land on the right button" gui_trajectory
"score it against a rubric" rubric_eval (needs criteria)
"keep or drop this trace" triage
"is this generated video physically plausible" rollout_evaluation
"does our judge agree with our humans" judge_alignment, judge_calibration
"compare two models' answers" arena, or a pairwise / bws scheme
"traces arrive from production" trace_ingestion (/api/traces/*) + automation + triage
"judge it while it runs" live_agent, live_coding_agent, agent_proxy

Picking the display

The scheme decides the question; the display decides what the annotator sees. They are chosen separately, and several schemes render their own cards and need no display field at all.

Display For Span target
agent_trace Generic steps with type badges yes, per step
coding_trace Diffs, terminal blocks, a file tree yes
cot_trace Long chain-of-thought with a progress rail no
eval_trace Three panes: reasoning, calls, final answer yes
web_agent_trace Screenshots with SVG overlays no
multi_agent_discussion Several agents, colour-coded, filterable yes
conversation_tree Branching, collapsible no
interactive_chat Live chat, then the trace yes
live_agent, live_coding_agent Streaming, with intervention controls no

"Highlight the sentence where the reasoning broke" therefore works on eval_trace and coding_trace and not on cot_trace or web_agent_trace. If the researcher wants spans over a long chain of thought, that is cot_trace for reading plus a separate span scheme over the text field, or process_reward if what they actually want is per-step judgements.

Ask the registry rather than the flag: display_registry.get_span_target_capable_types() returns twelve; list_displays()'s supports_span_target says nine, because pdf, spreadsheet and agent_trace anchor spans their own way and are missing from it.

Quirks worth knowing before you build

gui_trajectory coordinates default to normalized. Each step may carry x/y for the grounding marker, read as 0..1 unless you set coord_space: pixels. A trace exported in pixels with the default setting is read as fractions, so a click at 420 becomes left: 42000% and the marker leaves the picture entirely. The card's own coordinate readout is the tell — click @ 42000%, 12000% beside a screenshot 640 pixels wide.

gui_trajectory takes the screenshot from screenshot, not screenshot_url. The step key is screenshot, falling back to image, and screenshot_key on the scheme renames it. screenshot_url is what the live agent runner writes into the steps it produces, so a hand-authored trace that copies that spelling gets "no screenshot" on every card while the file itself serves fine over /screenshots/. Check the route with curl before you suspect the file; if it answers 200, the key is what is wrong.

multimodal_reasoning infers the step type from the keys, and every type falls back to content. An image step reads image, image_url, url, src, then content; a tool step's arguments read args, arguments, tool_call.args, input, then content, and its name reads tool, name, tool_call.name, then the literal string tool; a text step reads text, content, reasoning. So a trace keyed uniformly on content renders correctly as long as each step carries an explicit type — the image draws and the tool call prints.

What uniform keying breaks is the type inference. Without type, the type comes from the presence of image/image_url or tool/tool_calls/tool_call, and a step that has neither is text. Drop type from a content-only trace and every card is labelled TEXT, with the image's URL printed as a line of prose rather than fetched. type_key names the field holding the type; the per-step content keys are not configurable.

Screenshots come from a second static root. Trace data that sets screenshot_url: screenshots/step_000.png is served from <task_dir>/screenshots/, which is separate from media_directory and not configurable. Miss it and every step image and filmstrip thumbnail 404s.

process_reward has two modes and they are different tasks. first_error (the default) asks for the first step that goes wrong and cascades from there; per_step labels every step. allow_neutral is only meaningful in per_step, because the cascade has no place for it. inline_with_trace puts the controls inside the trace cards instead of beside them, and ai_prelabel seeds suggestions the annotator corrects.

rubric_eval is the only one with a required field: criteria.

tool_contention needs times on its calls. A call is {agent, tool, start, end, resource} and the seconds are what make a contention: same resource, overlapping interval. Feed it {agent, tool, resource} and every call sits at 0-0 and can never overlap; from 2.8.2-11 it says the times are missing rather than reporting a clean trace. Self-pairs are skipped from the same release, so coder <-> coder cards no longer appear.

The display reads different keys from the schemes. Both halves of the page take the step list, and they disagree about what a step is called: the schemes take agent_key and steps_key, while multi_agent_discussion, agent_trace, cot_trace and eval_trace take speaker_key and text_key, defaulting to speaker and text. Steps shaped {index, agent, text} therefore render correctly in the questions and as a column of grey "U" avatars with no names in the trace above them. Set speaker_key: agent in display_options. (list_displays() did not admit the option existed until 2.8.2-11; on an older checkout, read it off the renderer class.)

eval_trace's third pane needs an answer-shaped speaker. The Final Answer pane matches a step whose speaker or tool name reads like an answer — final answer, send_message, respond, response, finish, submit, conclusion — and otherwise falls back to the last action in the trace. A step whose speaker is the obvious final matches none of those, so the pane shows the last tool call and the agent's real answer is rendered as an observation hanging off it. Name the last step Agent (Final Answer) or similar.

process_reward needs step_text_key when the steps are dicts. Pair it with cot_segmentation — the intended pairing, and what examples/agent-traces/cot-process-reward/config.yaml ships — and each step is a dict of {index, text, type, char_start, char_end}. Set step_text_key: text and the widget shows the prose; leave it at its default and the annotator is asked to judge the serialized dict. cot_segmentation also stamps every step it makes type: observation, which is what the cot_trace display badges them.

A linked act saves without its link. In consensus_tracking and context_attribution, an act named in linked_acts asks for a second click on the turn it refers to. Skip that click and the act is stored with no ref, Next advances, and the only sign is a badge reading agreement where a complete one reads agreement -> #1. The widget marks the card ct-awaiting-ref and nothing consults it. If the reference is the point of the study, count the ref-less acts at export before you count anything else.

required on these now counts the cells, from 2.8.2-11. Each widget writes what is still missing onto its own form and the banner beside Next reads it, so a scorecard covering four agents on two dimensions plus two team dimensions blocks with "Score each agent and the team as a whole (1 of 10 scored)" instead of passing on the first click. Verified on that config. The banner carries role="alert", so a screen reader gets it too.

The media widgets were not part of that change: video_annotation, temporal_grounding and tiered_annotation still treat one mark as an answer. modalities.md has the detail.

trajectory_eval's score ignores everything but severity. Mark all five steps incorrect, give each an error type, and skip the severity, and the widget shows Score: 100 / 100 and stores "score":100 — a flawless number on a trajectory the annotator condemned at every step. A correct step with a stray severity, meanwhile, is deducted. If you export score, make severity mandatory in the instructions and recompute from steps before you believe it.

trajectory_edit's require_reason_on_edit does not require a reason. It decides whether the reason field is drawn, nothing more: an edit with "reason": "" saves and Next advances. Read reason at export and treat blank as unexplained.

code_review stores empty comments. + Add Comment appends a row immediately and the row persists whether or not anything is typed into it — {"category":"bug","file":"","line":null,"text":""}. Drop rows with empty text before counting review comments, and expect "verdict": "" from an annotator who rated files and never picked one.

World-model rollouts

rollout_evaluation is the odd one out and worth reading the module docstring for. Several videos of the same scenario — real recording, model rollouts, optionally a counterfactual under an intervention — on one frame-locked timeline.

Its design decision is worth repeating to a researcher who asks for a plausibility scale: the primitive is a break-point, not a score. Rating a generated video 3/5 "produces a number that cannot be checked, cannot be localised, and cannot be used to fix anything." A frame index plus a violation type can be checked against the tensor and compared between annotators.

Three layers, three questions: violations (where does it break, and why), preference (which rollout is better), counterfactual (is the divergence plausible given the intervention). The third is the one that separates a world model from a video generator: a beautiful continuation that ignores the intervention is a failure no plausibility rating detects.

Three defaults that are on unless you turn them off: blind (hide generator names), shuffle (permute panel order per annotator), and require_clean — which refuses a submission that leaves a stream neither marked nor explicitly cleared, because "no marks" otherwise conflates "watched it, found nothing" with "never got to it".

Blinding and the DOM

rollout_evaluation blinds server-side: under blinding the labels are positional and the server chose the positions. Every other scheme does not. The whole data row is serialized into the page — data-instance-fields on the display container and a data-instance-json block — so a field you leave out of instance_display is still in the DOM and still readable by anyone who looks.

To withhold something from annotators, withhold it from the data file. Omitting it from the display is presentation, not access control.

Judges and automatic evaluators

Sixteen evaluators are registered and run without a human:

from potato.evaluators.registry import get_supported_evaluators
get_supported_evaluators()
# exact_match, contains, regex_match, json_valid, json_schema_match,
# edit_distance, embedding_similarity, tool_use, tool_call_accuracy,
# trajectory_match, llm_trajectory_judge, agent_as_judge, rubric_dag,
# groundedness, answer_relevance, context_relevance

Use them where the answer is checkable and save the annotators for what is not. The human side of the same question is judge_alignment (score items with a judge, compare against the humans, track agreement) and judge_calibration (auto-label a sample, have humans re-label the same items). If a researcher says "we want to know if we can trust the judge", that is the pair — not another rating scheme.

Live agents

live_agent and live_coding_agent run an agent inside the annotation page so annotators judge it as it happens; agent_proxy makes an agent the subject of annotation.

live_coding_agent runs the agent's tool calls inside a container. Two defaults decide whether the session can do anything: sandbox_image is python:3.12-slim and sandbox_network is none. Together they mean the agent has whatever that image ships and no way to install more — asked to run the tests, it gets pytest: not found and an exit code of 127, and burns its turns looking for a runner. Build or name an image that already carries the tooling the task needs: with pytest in the image the same session runs the tests, edits the file and re-runs them to green. working_dir is copied into a per-session workspace under .potato-sandboxes/, so the repository you point at is not the one the agent edits; check the copy, not the original, when you want to see what it did.

The task box the annotator sees is seeded from the field the display is bound to. Bind it to the field holding the task text, not to the one naming the repository, or the box arrives holding the repository name and the annotator has to retype the task before starting the agent. Which directory the agent gets is working_dir's job, so the key is free to carry the prompt.

live_agent drives a real browser, so it needs playwright and a browser installed (pip install playwright && playwright install chromium). Its endpoint_type takes anthropic_vision, ollama_vision or openai_vision — the last is what an OpenAI-compatible server such as vLLM needs, and the shipped example's comment names only the first two. The system prompt has to ask for an action that is an object, {"type": "click", ...}, because that is what the runner expects; a prompt that asks for a bare string gets one back and the session ends at step 0. These open control surfaces, so treat them the way you would treat anything that executes: do not expose them on a deployment that anyone with the URL can register for, and check deploying.md before handing out a link.

Verifying one

The failure mode is an empty card list, and an empty card list looks like a short trace.

  1. Count what rendered against what is in the data. If the item has eleven steps, eleven cards should exist. This is the check that catches a wrong steps_key, and nothing else does.
  2. Judge one step, navigate away and back. Answers are stored keyed by index with only-judged steps saved; a restore bug shows up as answers attaching to the wrong card.
  3. Read user_state.json and confirm the stored JSON list has the shape the export expects.
  4. For gui_trajectory, look at where the marker landed. That is the whole point of the scheme, and it is the thing coord_space silently breaks.