TraceBuilder & TraceTree
Use TraceBuilder when you already know what happened and want to record it without a provider adapter. It creates the trace that assertions check; TraceTree lets you inspect nested agent runs and sum their recorded metrics.
TraceBuilder
Section titled “TraceBuilder”Constructor
Section titled “Constructor”from attest import TraceBuilder
builder = TraceBuilder(agent_id="my-agent")| Parameter | Type | Default | Description |
|---|---|---|---|
agent_id | str | None | None | Identifier for the agent producing this trace |
A unique trace_id is auto-generated as trc_{uuid_hex[:12]}.
Use builder.set_trace_id(trace_id: str) to override the generated ID. Builder setters and step-addition methods return the builder for chaining; build() returns the trace.
Setting Input
Section titled “Setting Input”# Keyword argumentsbuilder.set_input(user_message="hello", context="greeting")
# From a dictbuilder.set_input_dict({"user_message": "hello", "context": "greeting"})Adding Steps
Section titled “Adding Steps”Four step types correspond to the operations an agent performs:
add_llm_call
Section titled “add_llm_call”Record a language model invocation.
builder.add_llm_call( name="gpt-4.1", args={"messages": [{"role": "user", "content": "hello"}]}, result={"content": "Hi there!", "model": "gpt-4.1"}, metadata={"temperature": 0.7}, started_at_ms=1708000000000, ended_at_ms=1708000001500,)add_tool_call
Section titled “add_tool_call”Record a tool/function invocation.
builder.add_tool_call( name="search", args={"query": "weather in tokyo"}, result={"temperature": 22, "condition": "sunny"},)add_retrieval
Section titled “add_retrieval”Record a RAG retrieval operation.
builder.add_retrieval( name="vector-search", args={"query": "refund policy", "top_k": 5}, result={"documents": ["Policy doc 1", "Policy doc 2"]},)add_step
Section titled “add_step”Add a raw Step object directly.
from attest import Step
builder.add_step(Step( type="tool_call", name="calculator", args={"expression": "2+2"}, result={"value": 4},))Step Parameters
Section titled “Step Parameters”add_llm_call, add_tool_call, and add_retrieval share these parameters. add_step(step: Step) instead accepts a complete step, including nested trace or trace-v2 evidence.
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | required | Step identifier (model name, tool name, etc.) |
args | dict[str, Any] | None | None | Input arguments |
result | dict[str, Any] | None | None | Output result |
metadata | dict[str, Any] | None | None | Arbitrary metadata |
started_at_ms | int | None | None | Start timestamp (epoch ms) |
ended_at_ms | int | None | None | End timestamp (epoch ms) |
agent_id | str | None | None | Agent that produced this step |
agent_role | str | None | None | Role of the agent (e.g., “planner”, “executor”) |
Setting Output
Section titled “Setting Output”# Keyword argumentsbuilder.set_output(message="The weather in Tokyo is 22C and sunny.")
# From a dictbuilder.set_output_dict({"message": "The weather in Tokyo is 22C and sunny."})Setting Metadata
Section titled “Setting Metadata”builder.set_metadata( total_tokens=1500, cost_usd=0.003, latency_ms=1200, model="gpt-4.1", timestamp="2025-02-22T10:30:00Z",)| Parameter | Type | Default | Description |
|---|---|---|---|
total_tokens | int | None | None | Total token count |
cost_usd | float | None | None | Total cost in USD |
latency_ms | int | None | None | Wall-clock latency |
model | str | None | None | Primary model used |
timestamp | str | None | None | ISO 8601 timestamp |
set_metadata() merges supplied non-None values with existing metadata. Input/output setters replace their dictionaries. No builder method automatically measures token usage, cost or latency.
Parent Trace ID
Section titled “Parent Trace ID”Link a trace to a parent for multi-agent scenarios:
builder.set_parent_trace_id("trc_abc123def456")Building
Section titled “Building”trace = builder.build()Returns a Trace dataclass with schema_version=1. The builder is not consumed. Each build() copies the step list, but the steps, input/output dictionaries and metadata are shared references, not deep independent snapshots. Do not mutate them after recording a trace.
Complete Example
Section titled “Complete Example”from attest import TraceBuilder, expect, AgentResult
builder = TraceBuilder(agent_id="weather-agent")
builder.set_input(query="weather in tokyo")
builder.add_llm_call( name="gpt-4.1", args={"messages": [{"role": "user", "content": "weather in tokyo"}]}, result={"content": "Let me check the weather."},)
builder.add_tool_call( name="get_weather", args={"city": "tokyo"}, result={"temp_c": 22, "condition": "sunny"},)
builder.add_llm_call( name="gpt-4.1", args={"messages": [{"role": "user", "content": "summarize weather data"}]}, result={"content": "Tokyo is 22C and sunny."},)
builder.set_output(message="Tokyo is 22C and sunny.")builder.set_metadata(total_tokens=350, cost_usd=0.001, latency_ms=800)
trace = builder.build()result = AgentResult(trace=trace)
# Evaluate with the pytest fixture: attest.evaluate(chain)def test_weather_trace(attest): attest.evaluate(expect(result).output_contains("Tokyo").cost_under(0.01))Trace Dataclass
Section titled “Trace Dataclass”The Trace object produced by build():
| Field | Type | Description |
|---|---|---|
trace_id | str | Unique identifier |
output | dict[str, Any] | Agent output (required) |
schema_version | int | Trace schema version (builder emits 1; captured traces can be 2); not the JSON-RPC protocol version |
agent_id | str | None | Agent identifier |
input | dict[str, Any] | None | Agent input |
steps | list[Step] | Ordered execution steps |
metadata | TraceMetadata | None | Aggregate metrics |
parent_trace_id | str | None | Parent trace link |
conversation_id | str | None | Trace-v2 conversation identity |
turns | list[Turn] | Trace-v2 turns and their step indices |
handoffs | list[Handoff] | Trace-v2 agent handoffs and their step indices |
Step Dataclass
Section titled “Step Dataclass”| Field | Type | Description |
|---|---|---|
type | str | One of: llm_call, tool_call, retrieval, agent_call |
name | str | Step identifier |
args | dict[str, Any] | None | Input arguments |
result | dict[str, Any] | None | Output result |
sub_trace | Trace | None | Nested trace for agent_call steps |
metadata | dict[str, Any] | None | Arbitrary metadata |
started_at_ms | int | None | Start timestamp |
ended_at_ms | int | None | End timestamp |
agent_id | str | None | Agent identifier |
agent_role | str | None | Agent role |
span_id, parent_span_id | str | None | Trace-v2 span identity and parent |
tool_call_id | str | None | Trace-v2 provider tool-call identity |
retry_of | int | None | Trace-v2 index of the retried step |
error | StepError | None | Trace-v2 error type and optional message |
exchange | Exchange | None | Trace-v2 request/response evidence |
The extra trace-v2 types live in attest._proto.types: StepError(type, message=None), Exchange(request, response), Turn(index, step_indices=[], span_id=None, input=None, output=None) and Handoff(from_agent, to_agent, step_index). Construct a schema-2 Trace directly or preserve one from capture when using this evidence; TraceBuilder still emits schema 1.
Serialization
Section titled “Serialization”Trace, Step and TraceMetadata provide to_dict(). Trace.from_dict() reconstructs a trace and its nested steps; Step has no standalone from_dict() method. Import Trace from attest for the example below.
# Serializetrace_dict = trace.to_dict()
# Deserializerestored = Trace.from_dict(trace_dict)TraceTree
Section titled “TraceTree”TraceTree wraps a root Trace and provides multi-agent analysis over the tree formed by agent_call steps with nested sub_trace objects.
Constructor
Section titled “Constructor”from attest import TraceTree
tree = TraceTree(root=trace)Or from an AgentResult:
tree = result.trace_tree()Properties
Section titled “Properties”agents
Section titled “agents”List all agent_id values in the tree (depth-first).
tree.agents # ["orchestrator", "researcher", "writer"]Maximum nesting depth. Root with no children = 0.
tree.depth # 2delegations
Section titled “delegations”List of (parent_agent_id, child_agent_id) pairs.
tree.delegations# [("orchestrator", "researcher"), ("orchestrator", "writer")]aggregate_tokens
Section titled “aggregate_tokens”Sum of total_tokens across all traces in the tree.
tree.aggregate_tokens # 4500aggregate_cost
Section titled “aggregate_cost”Sum of cost_usd across all traces.
tree.aggregate_cost # 0.012aggregate_latency
Section titled “aggregate_latency”Sum of latency_ms across all traces.
tree.aggregate_latency # 3200Methods
Section titled “Methods”find_agent(agent_id: str) -> Trace | None
Section titled “find_agent(agent_id: str) -> Trace | None”Find a sub-trace by agent_id. Returns None if not found.
researcher_trace = tree.find_agent("researcher")if researcher_trace: print(researcher_trace.output)flatten() -> list[Trace]
Section titled “flatten() -> list[Trace]”Return all traces in depth-first order.
all_traces = tree.flatten()# [root_trace, researcher_trace, writer_trace]all_tool_calls() -> list[Step]
Section titled “all_tool_calls() -> list[Step]”Return all tool_call steps across the entire tree.
tools = tree.all_tool_calls()for step in tools: print(f"{step.agent_id}: {step.name}")Multi-Agent Example
Section titled “Multi-Agent Example”from attest import TraceBuilder, TraceTree, delegate, agent
@agent("orchestrator")def orchestrator(builder, task): builder.add_llm_call( name="gpt-4.1", args={"messages": [{"role": "user", "content": task}]}, result={"content": "I'll delegate this to specialists."}, )
with delegate("researcher") as child: child.add_tool_call( name="search", args={"query": task}, result={"documents": ["doc1", "doc2"]}, ) child.set_output(message="Found 2 relevant documents.", documents=["doc1", "doc2"])
with delegate("writer") as child: child.set_input(documents=["doc1", "doc2"]) child.add_llm_call( name="gpt-4.1", args={"messages": [{"role": "user", "content": "summarize docs"}]}, result={"content": "Summary of findings."}, ) child.set_output(message="Summary of findings.")
return {"message": "Task complete: Summary of findings."}
result = orchestrator(task="research quantum computing")tree = result.trace_tree()
print(tree.agents) # ["orchestrator", "researcher", "writer"]print(tree.depth) # 1print(tree.delegations) # [("orchestrator", "researcher"), ("orchestrator", "writer")]TraceTree Assertions
Section titled “TraceTree Assertions”The expect() DSL includes trace tree assertions (layer 3) that operate on the tree structure:
chain = ( expect(result) .agent_called("researcher") .agent_called("writer") .delegation_depth(2) .agent_output_contains("researcher", "documents") .cross_agent_data_flow("researcher", "writer", "documents") .follows_transitions([("orchestrator", "researcher"), ("orchestrator", "writer")]) .aggregate_cost_under(0.05) .aggregate_tokens_under(10000))attest.evaluate(chain) # inside a pytest test using the `attest` fixtureSee the Expect DSL reference for the full list of trace tree assertions.