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 run
  • steps — ordered list of Step objects
  • status — a RunStatus enum: running, paused, completed, or failed
  • model_info — which model was used
  • system_prompt and user_prompt — the prompts that started the run
  • created_at — timestamp of creation
  • metadata — 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.

run_tree_example.py
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 step
  • ancestors(sid) — walks from a step up to the root, returning the full ancestor chain
  • get_step(sid) — looks up a single step by its ID, returning None if not found
navigation.py
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.

branching.py
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:

aggregates.py
# Aggregate cost and duration across all steps
print(run.total_cost)      # => 0.003
print(run.total_duration)  # => 4.6

These 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).