Skip to content

TraceBuilder & TraceTree

The TraceBuilder class provides a fluent API for manually constructing Trace objects. TraceTree enables multi-agent trace analysis including delegation chains, cross-agent queries, and aggregate metrics.

from attest import TraceBuilder
builder = TraceBuilder(agent_id="my-agent")
ParameterTypeDefaultDescription
agent_idstr | NoneNoneIdentifier for the agent producing this trace

A unique trace_id is auto-generated as trc_{uuid_hex[:12]}.

# Keyword arguments
builder.set_input(user_message="hello", context="greeting")
# From a dict
builder.set_input_dict({"user_message": "hello", "context": "greeting"})

Four step types correspond to the operations an agent performs:

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,
)

Record a tool/function invocation.

builder.add_tool_call(
name="search",
args={"query": "weather in tokyo"},
result={"temperature": 22, "condition": "sunny"},
)

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 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},
))

All add_* methods share the same parameter signature:

ParameterTypeDefaultDescription
namestrrequiredStep identifier (model name, tool name, etc.)
argsdict[str, Any] | NoneNoneInput arguments
resultdict[str, Any] | NoneNoneOutput result
metadatadict[str, Any] | NoneNoneArbitrary metadata
started_at_msint | NoneNoneStart timestamp (epoch ms)
ended_at_msint | NoneNoneEnd timestamp (epoch ms)
agent_idstr | NoneNoneAgent that produced this step
agent_rolestr | NoneNoneRole of the agent (e.g., “planner”, “executor”)
# Keyword arguments
builder.set_output(message="The weather in Tokyo is 22C and sunny.")
# From a dict
builder.set_output_dict({"message": "The weather in Tokyo is 22C and sunny."})
builder.set_metadata(
total_tokens=1500,
cost_usd=0.003,
latency_ms=1200,
model="gpt-4.1",
timestamp="2025-02-22T10:30:00Z",
)
ParameterTypeDefaultDescription
total_tokensint | NoneNoneTotal token count
cost_usdfloat | NoneNoneTotal cost in USD
latency_msint | NoneNoneWall-clock latency
modelstr | NoneNonePrimary model used
timestampstr | NoneNoneISO 8601 timestamp

Link a trace to a parent for multi-agent scenarios:

builder.set_parent_trace_id("trc_abc123def456")
trace = builder.build()

Returns a Trace dataclass. The builder is not consumed — calling build() multiple times produces independent traces sharing the same trace_id.

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)
# Use with expect() DSL
expect(result).output_contains("Tokyo").cost_under(0.01)

The Trace object produced by build():

FieldTypeDescription
trace_idstrUnique identifier
outputdict[str, Any]Agent output (required)
schema_versionintProtocol version (always 1)
agent_idstr | NoneAgent identifier
inputdict[str, Any] | NoneAgent input
stepslist[Step]Ordered execution steps
metadataTraceMetadata | NoneAggregate metrics
parent_trace_idstr | NoneParent trace link
FieldTypeDescription
typestrOne of: llm_call, tool_call, retrieval, agent_call
namestrStep identifier
argsdict[str, Any] | NoneInput arguments
resultdict[str, Any] | NoneOutput result
sub_traceTrace | NoneNested trace for agent_call steps
metadatadict[str, Any] | NoneArbitrary metadata
started_at_msint | NoneStart timestamp
ended_at_msint | NoneEnd timestamp
agent_idstr | NoneAgent identifier
agent_rolestr | NoneAgent role

Both Trace and Step provide to_dict() and from_dict() for JSON serialization:

# Serialize
trace_dict = trace.to_dict()
# Deserialize
restored = Trace.from_dict(trace_dict)

TraceTree wraps a root Trace and provides multi-agent analysis over the tree formed by agent_call steps with nested sub_trace objects.

from attest import TraceTree
tree = TraceTree(root=trace)

Or from an AgentResult:

tree = result.trace_tree()

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 # 2

List of (parent_agent_id, child_agent_id) pairs.

tree.delegations
# [("orchestrator", "researcher"), ("orchestrator", "writer")]

Sum of total_tokens across all traces in the tree.

tree.aggregate_tokens # 4500

Sum of cost_usd across all traces.

tree.aggregate_cost # 0.012

Sum of latency_ms across all traces.

tree.aggregate_latency # 3200

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)

Return all traces in depth-first order.

all_traces = tree.flatten()
# [root_trace, researcher_trace, writer_trace]

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}")
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.")
with delegate("writer") as child:
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) # 1
print(tree.delegations) # [("orchestrator", "researcher"), ("orchestrator", "writer")]

The expect() DSL includes Layer 7 (trace tree) assertions that operate on the tree structure:

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)

See the Expect DSL reference for the full list of trace tree assertions.