# 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](https://docs.activegraph.ai/concepts/replay/index.md) 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.

```text
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](https://docs.activegraph.ai/concepts/replay/index.md) 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

```bash
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.9` is in scope; `runtime.budget.max_cost_usd=10` is out of scope). Multiple `--set` flags 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](https://docs.activegraph.ai/concepts/reference/cli/index.md).

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

```python
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`](https://docs.activegraph.ai/reference/errors/incompatible-runtime-state/index.md) 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`](https://docs.activegraph.ai/reference/errors/event-not-found-error/index.md) 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](https://docs.activegraph.ai/guides/fork-test-promote/index.md) 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.responded` event.
- **Tool call with the same args hash** → cached response from the parent run's `tool.responded` event.
- **LLM or tool call whose hash drifted from the parent** — expected only after `--set` changed something upstream. Without `--record`, the fork's strict replay fires [`replay-divergence-error`](https://docs.activegraph.ai/reference/errors/replay-divergence-error/index.md); 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](https://docs.activegraph.ai/guides/operating-in-production/index.md).)

## 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`](https://docs.activegraph.ai/concepts/frames/index.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`](https://docs.activegraph.ai/concepts/graph/index.md) — the world state the fork projects from its event log.
- [`events`](https://docs.activegraph.ai/concepts/events/index.md) — the append-only history forks share up to the cutoff.
- [`replay`](https://docs.activegraph.ai/concepts/replay/index.md) — the operation that reconstructs the shared prefix in the fork.
- [`frames`](https://docs.activegraph.ai/concepts/frames/index.md) — the in-run parallel primitive that complements fork.
- [`patches`](https://docs.activegraph.ai/concepts/patches/index.md) — patches in a fork are independent of the parent's patches once the fork point passes.
- [`replay-divergence-error`](https://docs.activegraph.ai/reference/errors/replay-divergence-error/index.md) — the error case when strict-mode replay finds a divergent prompt hash or event stream.
- [CLI reference](https://docs.activegraph.ai/concepts/reference/cli/index.md) — the full surface for `activegraph fork` and the surrounding commands.
