Skip to main content
Most LLM evals stop at the final string. Agent evals can’t: the same output can come from a clean three-step plan or a thirty-call death spiral, and you want to fail the death spiral. multivon-eval ships a framework-agnostic trace model, three tracer adapters, and eight evaluators that score the trajectory itself. This page covers the data model, the tracers, the evaluators, and a runnable LangGraph example.

The trace data model

Two dataclasses, defined in multivon_eval/case.py:
An agent trace is list[AgentStep], attached to an EvalCase as agent_trace. The semantic unit is one AgentStep per LLM/agent turn, not per framework event. A ReAct loop that thinks, decides to call three tools in parallel, and then incorporates the results produces ONE step with three ToolCall entries, not three steps. Every shipped tracer enforces this, and it’s what the evaluators assume.
EvalCase.expected_tool_calls is the assertion surface. ToolCall.arguments and ToolCall.result are captured automatically by the tracers — you don’t write them by hand unless you’re using ManualTracer.

Tracer adapters

A tracer wraps your model_fn so the suite gets back not just a string but a list[AgentStep]. All three implement the AgentTracer ABC from multivon_eval/integrations/base.py.

LangGraphTracer

Callback-based tracer for LangGraph compiled graphs. Step boundary = each on_llm_start, so tools fire into the step that decided to call them.

OpenAIAgentsTracer

Two modes: post-hoc capture(result) parses RunResult.new_items; live run_hooks() uses per-run isolated buffers.

ManualTracer

For custom agents and frameworks without integration. You call tracer.step(...) inside your agent code.

LangGraphTracer

A CallbackTracer subclass: it builds a BaseCallbackHandler and injects it via config={"callbacks": [...]} on the compiled graph’s invoke. Implementation in integrations/langgraph.py.
The tracer uses LangGraph metadata (langgraph_node, langgraph_checkpoint_ns, graph:step:N tags) to attribute calls to the right node. The tools node of a ReAct graph collapses into the preceding LLM turn, which is the semantic unit evaluators score against.
Known v1 limitations (file an issue if you hit them): graph.stream(...) and graph.ainvoke(...) emit the same callback events but aren’t end-to-end verified; parallel branches via Send share _current_step and may cross-attribute — use a separate tracer instance per branch; multi-agent handoffs land as adjacent steps with no first-class handoff event.
Install with pip install 'multivon-eval[langgraph]'. Compatible with LangGraph ≥ 0.2, verified through 0.5+.

OpenAIAgentsTracer

For the OpenAI Agents SDK. Two integration paths, both shipped from integrations/openai_agents.py. Post-hoc (default, recommended): the tracer reads RunResult.new_items after the run completes. No global state, no thread-safety concerns.
Live RunHooks (when you need event-time interception — streaming, cancel-on-guardrail):
Each run_hooks() call returns a RunHooksBase with its own buffer — concurrent runs do not interleave. merge is idempotent: the second call is a no-op because the first clears the buffer. Install with pip install 'multivon-eval[openai-agents]'. Items the SDK ships but the tracer doesn’t fully model yet (CompactionItem, ToolApprovalItem, MCP / ComputerCall / CodeInterpreter / ToolSearch items) are preserved as visible [ItemClassName] markers in the trace rather than silently dropped.

ManualTracer

The fallback for any agent that isn’t LangGraph or the OpenAI SDK: you record steps explicitly. Source in integrations/manual.py.
step() returns a _StepRecorder context manager that flushes on exit. record_tool_call and record_output are also available without a step block; they create implicit single-call steps. ManualTracer is what powers most custom agents in production. It’s the lowest-friction adapter and has zero framework dependencies.
Even when a framework adapter exists, ManualTracer is useful for fallback coverage: if your agent has a code path the framework adapter doesn’t see (a direct API call, a non-LangChain tool), record those steps manually and the evaluators will score them alongside the auto-captured ones.

The eight agent-trace evaluators

All eight live in multivon_eval/evaluators/agent.py. Two are deterministic; six use an LLM judge (the QAG eval pattern documented in LLM-judge evaluators). Every evaluator returns a skipped pass, not a 0.0, when the case shape doesn’t fit it (e.g. no agent_trace, no expected_tool_calls). This was hardened in 0.9.0.

ToolCallAccuracy and its three input shapes

The most-used evaluator on the list. The shape of expected_tool_calls triggers three different behaviors, formalized in 0.9.0:
1

None — skip

expected_tool_calls=None returns a skipped pass with the reason “Requires case.expected_tool_calls — set it (or [] to assert no tools) to enable ToolCallAccuracy.” You haven’t asserted anything, so the evaluator doesn’t score.
2

Empty list — assert no tools

expected_tool_calls=[] says “the agent must NOT call any tools.” If the trace has zero calls → score 1.0 (“Correctly called no tools”). If any tool fires → score 0.0 with the unexpected calls listed. This is the trivial-question assertion: “for what’s 2 + 2, don’t reach for a calculator.”
3

Populated list — real expectation

expected_tool_calls=["lookup_order", "issue_refund"] is the standard case. Requires agent_trace; skips if missing. Default scoring is fraction of expected tools called. Two modifiers tune the strictness:
  • require_order=True — positional match instead of set match. Use when call order matters (auth before query, validate before commit).
  • penalize_unexpected=Truescore = matched / (expected ∪ unexpected), so every extra tool drags the score down. Use for negative cases like “the agent must NOT call refund_order on an already-refunded order” where extra calls are the failure mode you care about.

The judge-driven six

The remaining seven are sketched here; for the judge resolution logic, see Judge calibration and the rest of Statistical rigor.
  • ToolArgumentAccuracy runs a per-call yes/no judge prompt: “Are these arguments appropriate and well-formed for <tool> given the task?” Capped at the first 8 calls to bound judge cost.
  • PlanQuality is a 5-question QAG over the full trace: addresses task, logical order, no redundancy, follows from prior steps, expert-efficient.
  • TaskCompletion is a 4-question QAG. It includes a negated check (“Did the agent fail / error?”) so an error trace can’t slip through with a confident partial-credit score.
  • StepFaithfulness judges each step: “Does this step follow logically from the task and prior steps, without introducing contradictions or hallucinated information?” Catches reasoning drift that other evaluators miss.
  • ToolCallNecessity asks per call: “Is this call strictly necessary, or redundant given prior calls?” Detects the “call tools just in case” failure mode. An empty trace is a PASS (nothing to flag as redundant), not a skip, distinguishing “no expectation set” from “agent correctly did nothing.”
  • TrajectoryEfficiency is a 3-question QAG plus an error-recovery bonus: if any tool result contains error, an extra judge call asks whether the agent recovered well, penalizing 0.2 if not.
  • AgentMemoryEval is the multi-session memory check. It requires case.context (prior session summary) and scores correct recall, no hallucination, no use of stale info. Aligned with AMA-Bench (2025).

Runnable example: LangGraph + ToolCallAccuracy + TrajectoryEfficiency

End-to-end: a tiny LangGraph agent, two cases (one positive, one negative), a ManualTracer fallback for an adversarial case that bypasses LangGraph.
agent_eval.py
The "openai:gpt-4o-mini" string-model syntax resolves through langchain.chat_models.init_chat_model, which lives in the full langchain package — pip install 'multivon-eval[langgraph]' alone raises an ImportError asking for it. Either pip install langchain langchain-openai as well, or pass a model instance (create_react_agent(model=ChatOpenAI(model="gpt-4o-mini"), ...)) to skip the string resolver.
If LangGraph isn’t available — or you want to score a case the framework adapter can’t see — fall back to ManualTracer:
The same ToolCallAccuracy and TrajectoryEfficiency instances score both — the evaluators don’t care which tracer captured the trace.

Inspecting and debugging traces

AgentTracer.format_trace() and tracer.print_trace() pretty-print a captured trace. Use them inside a notebook when a case fails and you want to see what the agent actually did:
CaseResult.agent_trace was added in 0.7.0 so notebooks can iterate steps from the report without reaching back into the suite.

Importing existing traces

If your traces already live in LangSmith, LangFuse, Phoenix, Datadog, or any other observability store, you don’t have to re-run the agent. AgentTracer’s sibling abstraction CaseImporter (defined in the same integrations/base.py) pulls runs as EvalCase objects with agent_trace populated, and importer.as_model_fn(cases) gives you a passthrough that replays the stored outputs in order:
load_traces accepts field aliases for the common platforms — LangSmith’s query/answer/retrieved_context, LangFuse’s prompt/completion, Phoenix’s input/output all auto-rename to the canonical shape. See Bootstrap workflow for the end-to-end import → score → ship loop.