Run Trees
Every opentine execution is a Run — a tree of Steps where each step points to its parent via parent_id. This tree structure captures the full decision history of an agent: what it thought, which tools it called, what the model returned, and how it finished.
The Run Struct
A Run is a msgspec Struct (tagged for polymorphic serialization) with these fields:
id— unique identifier for the runsteps— ordered list ofStepobjectsstatus— aRunStatusenum:running,paused,completed, orfailedmodel_info— which model was usedsystem_promptanduser_prompt— the prompts that started the runcreated_at— timestamp of creationmetadata— arbitrary dict for your own data
Creating a Run and Adding Steps
Use run.add_step() to append steps to the tree. The first step you add without a parent_id becomes a root step. Subsequent steps reference their parent to form the tree.
1from opentine import Run
2
3# Create a new run
4run = Run(
5 id="research-01",
6 model_info="claude-sonnet-4-20250514",
7 system_prompt="You are a research assistant.",
8 user_prompt="Find recent papers on RLHF",
9)
10
11# Add a root step (no parent_id)
12think_step = run.add_step(
13 kind="think",
14 inputs={"thought": "I should search for RLHF papers from 2024-2025."},
15)
16
17# Add child steps under the root
18tool_step = run.add_step(
19 kind="tool",
20 inputs={"tool": "search", "query": "RLHF papers 2024"},
21 outputs={"results": ["paper_a", "paper_b"]},
22 parent_id=think_step.id,
23 duration=1.2,
24 cost=0.0,
25)
26
27model_step = run.add_step(
28 kind="model",
29 inputs={"prompt": "Summarize these papers..."},
30 outputs={"text": "Recent RLHF advances include..."},
31 parent_id=tool_step.id,
32 duration=3.4,
33 cost=0.003,
34)
35
36done_step = run.add_step(
37 kind="done",
38 inputs={},
39 outputs={"summary": "Found 2 relevant papers on RLHF."},
40 parent_id=model_step.id,
41)
Navigating the Tree
The Run class provides four navigation methods that let you walk the tree in any direction:
root_steps()— returns all steps with no parent (the top-level entry points)children(sid)— returns the direct children of a given stepancestors(sid)— walks from a step up to the root, returning the full ancestor chainget_step(sid)— looks up a single step by its ID, returningNoneif not found
1# Get all top-level steps (no parent)
2roots = run.root_steps()
3# => [Step(id="a1b2c3...", kind="think", ...)]
4
5# Get direct children of a step
6kids = run.children(think_step.id)
7# => [Step(id="d4e5f6...", kind="tool", ...)]
8
9# Walk up the tree from any step
10chain = run.ancestors(done_step.id)
11# => [model_step, tool_step, think_step]
12# (ordered from immediate parent to root)
13
14# Look up any step by ID
15step = run.get_step(tool_step.id)
16# => Step(kind="tool", inputs={"tool": "search", ...}, ...)
Branching
A tree isn't limited to a single chain of steps. One parent can have multiple children, representing parallel tool calls or alternative reasoning paths. This is what makes the tree structure more powerful than a flat list.
1# A single think step can lead to multiple tool calls
2think = run.add_step(
3 kind="think",
4 inputs={"thought": "I need both a web search and a database lookup."},
5)
6
7search_step = run.add_step(
8 kind="tool",
9 inputs={"tool": "web_search", "query": "RLHF 2024"},
10 parent_id=think.id,
11)
12
13db_step = run.add_step(
14 kind="tool",
15 inputs={"tool": "db_lookup", "query": "rlhf"},
16 parent_id=think.id,
17)
18
19# Both search_step and db_step are children of think
20run.children(think.id)
21# => [search_step, db_step]
Cost and Duration
Every Run exposes two convenience properties that aggregate across all steps in the tree:
# Aggregate cost and duration across all steps
print(run.total_cost) # => 0.003
print(run.total_duration) # => 4.6These are computed properties — they sum step.cost and step.duration across the entire steps list. When you fork a run, the forked run's totals only reflect its own steps.
Why Trees?
A flat log tells you what happened. A tree tells you why it happened — which thought led to which tool call, which tool call fed which model response. This structure enables forking (branch from any point), replay (re-execute from a step), and diffing (compare two runs structurally).