GNL
Docs menu
Core · Free@gnldev/durable

Time Travel & Run Forking

Reconstruct the state at any step of a run's journal (reconstructState), or fork a new branch from that point (forkRun) — for debugging and 'what-if' scenarios.

What it's for / when to use it#

An agent run is written to the journal step by step (as model and tool entries). Sometimes you need to answer questions like "what exactly did the model see at step 3?" or "what would have happened from this step onward with a different tool/model?"

reconstructState walks the journal entries PURELY (without side effects) and materializes the message list up to a given step, along with any pending (not-yet-resolved) tool calls — ideal for debugging/inspection (Studio inspector, log analysis).

forkRun goes one step further: it copies the source run's first N model steps + their associated tool records + the original input into a NEW runId. Everything after N is not copied — when continued via resumeRun, the prefix is replayed from the journal (side effects do NOT run again), while step N onward runs LIVE. This lets you try the "what would have happened from the same point with a different model/tool" scenario (what-if / A-B debugging) without disturbing the source run.

Setup / import#

Both functions are exported from @gnldev/durable's main entry point — no separate subpath is needed:

import
import { reconstructState, forkRun } from '@gnldev/durable';
import { runDurable, resumeRun } from '@gnldev/durable';

Step-by-step usage#

First, steps are written to the journal via a normal runDurable call (in the example below, the model calls a charge tool and then finishes with text):

1) normal run — the journal gets populated
await runDurable({
  runId: 'r',
  journal,
  model: srcModel(),
  tools: tools(),
  stopWhen: stepCountIs(6),
  prompt: 'x',
});

Fork a new branch from step 1 (after the first model step) — the source's model:0 entry and its associated tool record + the original :input are copied to the new fork1 runId; model:1 and everything after is NOT copied:

2) forkRun — a new branch from step 1
const fork = await forkRun(journal, 'r', 1, 'fork1');
// fork => { newRunId: 'fork1', copiedModel: 1, copiedTool: 1 }

Then continue the fork with resumeRun using DIFFERENT model/tool logic: step 0 (the charge tool call) is replayed from the journal — its side effect (e.g. charging again) does NOT run again — while step 1 onward truly runs live and can reach a different outcome than the source:

3) resumeRun(fork1, ...) — live from step 1
const r2 = await resumeRun('fork1', {
  journal,
  model: forkModel(),
  tools: tools(),
  stopWhen: stepCountIs(6),
});
// r2.text may differ (e.g. 'FORK-final'); the charge counter does NOT increase (replay).

To just inspect state (without opening a fork), reconstructState is enough — give it the journal entry array and the desired step:

reconstructState — materializing state
const entries = await journal.readRun('r');
const state = reconstructState(entries, 1);
// state => { step: 1, messages: [...], pending: [{ toolCallId: 'c1', toolName: 'pay' }] }

// Pass a seed to prepend the original user input as well:
const withSeed = reconstructState(entries, 3, { prompt: 'ode' });

API reference#

fnreconstructState

Walks the journal entry array purely and returns the messages up to uptoStep along with pending tool calls; an optional seed prepends the original input.

fnforkRun

Copies srcRunId's first `step` model steps + their related tool records + :input under newRunId; anything after is not copied — resumeRun continues live from step.

typeReconstructedState

reconstructState's return type: { step, messages, pending }.

typeReconstructSeed

Optional seed input for reconstructState: { system?, messages?, prompt? }.

typeForkResult

forkRun's return type: { newRunId, copiedModel, copiedTool }.

Warning
forkRun requires a journal that provides both write and read (Journal & JournalReader) — because the copied prefix is replayed from the journal, side effects before step N (e.g. charging) do NOT run again in the fork; only step N onward truly runs live.