Forking
Forking is opentine's core value proposition. When an agent run goes wrong at step 23, you don't throw away 22 good steps and start over. You call run.fork(from_step_id) to create a new run that keeps the entire ancestor chain up to the fork point, then continue from there.
How Forking Works
When you call run.fork(from_step_id), opentine:
- Walks the ancestor chain from the given step back to the root using
ancestors() - Creates a new
Runcontaining only those ancestor steps (plus the fork-point step itself), in order from root to fork point - Sets the new run's status to
running - Generates a new run ID (or uses one you provide via
new_run_id)
Because steps are content-addressed, the copied steps retain their original IDs. The forked run is a valid, independent run that can be saved, resumed, or forked again.
1from opentine import Run
2
3# Original run: 5 steps deep
4run = Run(id="original", model_info="claude-sonnet-4-20250514",
5 system_prompt="Research assistant", user_prompt="Find RLHF papers")
6
7step_1 = run.add_step(kind="think", inputs={"thought": "Search for papers"})
8step_2 = run.add_step(kind="tool", inputs={"tool": "search", "query": "RLHF"},
9 outputs={"results": ["paper_a"]},
10 parent_id=step_1.id, duration=1.0)
11step_3 = run.add_step(kind="model", inputs={"prompt": "Summarize paper_a"},
12 outputs={"text": "Summary..."},
13 parent_id=step_2.id, duration=2.5, cost=0.002)
14step_4 = run.add_step(kind="tool", inputs={"tool": "fetch", "url": "bad_url"},
15 outputs={"error": "404 Not Found"},
16 parent_id=step_3.id, duration=0.3)
17step_5 = run.add_step(kind="error", inputs={},
18 outputs={"message": "Failed to fetch paper"},
19 parent_id=step_4.id)
20
21# Fork from step 3 — keep the good work, discard the failure
22forked = run.fork(from_step_id=step_3.id)
What the Fork Contains
The forked run includes exactly the steps on the path from root to fork point. Sibling branches and descendant steps after the fork point are excluded. This keeps the fork minimal and clean.
1# The forked run contains steps 1, 2, and 3 — the ancestor chain
2print(len(forked.steps)) # => 3
3print(forked.status) # => RunStatus.running
4
5# Step IDs are preserved (content-addressed, same inputs = same ID)
6assert forked.steps[0].id == step_1.id
7assert forked.steps[1].id == step_2.id
8assert forked.steps[2].id == step_3.id
9
10# The forked run gets a new unique ID
11print(forked.id) # => "fork-a8c3e1..."
12
13# Or specify your own ID
14forked2 = run.fork(from_step_id=step_3.id, new_run_id="retry-01")
15print(forked2.id) # => "retry-01"
Retry Pattern
The most common use of forking is retrying after a failure. An agent runs a complex task, hits an error partway through, and you want to try a different approach without re-executing all the successful steps.
1# Agent run that failed at step 23
2run = agent.run_sync("Complex research task")
3# => 20 good steps, then step 21 made a bad tool call,
4# steps 22-23 spiraled into an error
5
6# Find the last good step
7good_steps = [s for s in run.steps if s.kind != "error"]
8last_good = good_steps[-1] # step 20
9
10# Fork and continue with a different approach
11forked = run.fork(from_step_id=last_good.id)
12
13# Now add new steps to the forked run
14forked.add_step(
15 kind="think",
16 inputs={"thought": "Previous approach failed. Try a different tool."},
17 parent_id=last_good.id,
18)
19
20# Or hand the forked run back to the agent to continue
21result = agent.continue_run(forked)
Cost Savings
Because forked runs reuse existing steps, you avoid re-executing expensive model calls and slow tool invocations. The savings scale with how deep the fork point is — if 80% of your steps were good, you only pay for the remaining 20%.
1# Original run: 25 steps, $0.12 total
2print(run.total_cost) # => 0.12
3print(run.total_duration) # => 45.3 seconds
4
5# Fork from step 20 — only pay for new steps going forward
6forked = run.fork(from_step_id=step_20.id)
7print(forked.total_cost) # => 0.08 (cost of steps 1-20 only)
8print(forked.total_duration) # => 32.1 (duration of steps 1-20 only)
9
10# The 5 failed steps ($0.04) are not in the fork.
11# New steps from step 20 onward might cost $0.02 instead.
12# Total spend: $0.12 (original) + $0.02 (new) = $0.14
13# Without forking: $0.12 (original) + $0.12 (full re-run) = $0.24
The content-addressed step IDs mean there's no ambiguity about which steps are reused. If a step in the fork has the same ID as in the original, it's the same computation with the same result. Downstream systems can use this for caching — skip re-executing any step whose ID already exists in a result store.
Forking a Fork
Forked runs are just regular runs. You can fork them again, creating a chain of experiments branching from the same base computation. Each fork is independent and can be saved or shared separately.