Forking¶
A fork is a branch from a parent run at a specific event. The fork shares the parent's event log up to the fork point; from there, it has its own independent log. The two runs can be diffed, configured differently, and inspected side-by-side without touching the parent's state.
Forking is what lets the framework answer "what would have happened if I'd done X differently?" — a question agentic systems need to answer routinely and most frameworks can't answer at all. The shared-lineage model plus the cache layer makes fork cheap (no LLM re-execution for the shared prefix) and honest (the fork's lineage is verifiable from the event log).
The shared-lineage model¶
A fork copies events from the parent run, in order, up to the
--at-event cutoff. The cutoff is inclusive — events at the
cutoff id and before are in the fork; events after are not.
parent: evt_001 ... evt_042 evt_043 evt_044 ... (continues)
|
+- fork from evt_042
v
fork: evt_001 ... evt_042 evt_045 evt_046 ... (fork's own work)
The fork's events 1 through 42 are the parent's. Event 45 onward is the fork's own; event ids don't collide because the fork has its own run id and its own monotonic id generator.
The shared prefix doesn't re-execute when the fork starts. The framework replays the prefix against the fork's in-memory graph, then resumes live execution from the cutoff. LLM and tool responses for the shared prefix are served from the cache — no new LLM calls, no new tool calls — which keeps fork cheap.
The CLI surface¶
activegraph fork <parent-url> \
--run-id <parent-run> \
--at-event <event-id> \
--label <human-readable> \
--set <pack>.<setting>=<value> \
--record
Three flags shape the fork:
--at-event— the cutoff. Required.--set <key>=<value>— override a pack setting in the fork. The key is a dotted path into pack settings only (diligence.confidence_threshold_for_review=0.9is in scope;runtime.budget.max_cost_usd=10is out of scope). Multiple--setflags compose. Syntax and "pack was not loaded by this fork point" errors fail at fork time; unknown setting paths and type coercion errors fail when the pack reloads and Pydantic validates the resulting settings.--record— mark the fork as a re-recording. Behaviors whose prompts changed since the parent run will be re-recorded rather than cache-hit; new cache entries land in the fork's events.
--set records a fork-local pack.settings_overridden event,
then normal pack loading applies and validates that override before
post-fork behaviors run. The semantics — pack settings only,
auditable event-log override, fail loud on typos — are documented in the
CLI reference.
The Python surface¶
The same primitive is a first-class runtime method — the shape an agent uses to test a change against its own history before adopting it:
from activegraph import Runtime
rt = Runtime.load("sqlite:///assistant.db")
# Pick a fork point: every event object carries the id that
# at_event= expects (v1.3 — no need to read the store directly).
fork_point = rt.trace.events()[-1].id
fork = rt.fork(
at_event=fork_point, # inclusive cutoff, a real event id
label="candidate-pack-test",
replay_llm_cache=True, # serve the shared prefix from cache
replay_tool_cache=True,
)
# The fork is an independent Runtime: load a candidate pack into it,
# run it, and compare against the parent.
fork.load_pack(candidate_pack)
fork.run_until_idle()
diff = rt.diff(fork) # structural: events + final state
for d in diff.divergent_objects:
print(d.summary())
Two requirements, both enforced with structured errors:
fork() needs a SQLite-backed runtime
(incompatible-runtime-state
explains the migration path from Postgres or in-memory), and
at_event= must be a real event id from the parent's log
(event-not-found-error
lists what was available). Forks of forks work the same way.
rt.diff(other) is structural only — divergent objects, divergent
relations, and the event partition (shared prefix, parent-only tail,
fork-only tail). Semantic comparison is a behavior's job, not the
runtime's.
When the fork's results earn adoption, rt.promote(fork) applies
its net structural delta back to the parent — atomically,
fail-closed on conflicts with the parent's own post-fork work, and
fully audited through a promote.applied marker event. The
Fork, test, promote guide covers
the complete loop.
How the cache replays¶
For events before the fork point, the cache serves recorded responses by content hash:
- LLM call with the same prompt hash → cached response from
the parent run's
llm.respondedevent. - Tool call with the same args hash → cached response from
the parent run's
tool.respondedevent. - LLM or tool call whose hash drifted from the parent —
expected only after
--setchanged something upstream. Without--record, the fork's strict replay firesreplay-divergence-error; with--record, the fork accepts the new responses and records them as its own.
The cache is per-store, indexed by run id. A fork that needs to re-execute the same prompt the parent already ran in a different run can't reach across — caches don't cross runs. (Migration is the primitive for moving runs across stores; see Operating in production.)
When to fork vs when to use frames¶
Both let parallel computations proceed in isolation. The difference is durability and replay:
- Fork — a separate run with shared event-log lineage up to the fork point. Forks are durable, replayable, diffable. Use fork when the parallel branches might diverge permanently or need independent persistence.
- Frames (
frames.md) — sub-contexts within one run. The frame's events live in the same event log as everything else; replay is the whole run. Use frames when the parallel contexts are short-lived or semantically belong together.
The decision rule: if you'd want to inspect, diff, or migrate the two branches independently after the fact, use fork. If the branches converge back to a single output within the same run, use frames.
A common pattern is to start parallel work in frames, then fork only the branches worth keeping. Frames are the cheap parallel primitive; forks are the durable one.
What's related¶
graph— the world state the fork projects from its event log.events— the append-only history forks share up to the cutoff.replay— the operation that reconstructs the shared prefix in the fork.frames— the in-run parallel primitive that complements fork.patches— patches in a fork are independent of the parent's patches once the fork point passes.replay-divergence-error— the error case when strict-mode replay finds a divergent prompt hash or event stream.- CLI reference — the full surface for
activegraph forkand the surrounding commands.