Branching execution
chidori.branch(variants) lets an agent fork itself mid-run into N
branches. Each branch explores a strategy from the same anchored state, runs
its own editable source, can pause, and returns an outcome so the agent
(or a human) can compare and pick one. Because the shared prefix is
identical across branches, a branch is a controlled experiment: the only
variable is the branch's code/input — and each branch streams as its own
subtree in an OTLP trace viewer for side-by-side comparison.
This exists because iterating on agents is uniquely painful: a run is a long chain of steps that are expensive (LLM/tool calls cost money and seconds), stochastic (same prompt → different output), and stateful (step N depends on all prior steps). The default loop — change code, re-run the whole thing — re-pays for the entire prefix, and because the model is stochastic the prefix comes out different, so you cannot tell whether your change helped or the randomness moved. Branching turns durable execution into the workflow agents actually want: anchor at a decision point, reuse the prefix for free (its result is the shared starting state), vary one thing per branch, run each branch and compare.
A branch is a separate continuation source run once — not a re-run of the
parent. Re-running the parent's source would re-reach chidori.branch and
recurse, so the prefix is handed over as state (the parent's captured
workspace state plus an explicit input), not replayed. Branches act on the
result of the prefix; they don't re-derive it. Each branch is a new durable
sub-run seeded from the parent's anchor; the runtime runs each variant's
module, collects the outcomes, and returns them. The whole fan-out is one
recorded host call on the parent, so the parent's own replay
returns the outcomes from cache.
Signals compose with branching — a branch listening on a signal drains the parent run's shared mailbox; see Signals § Composition with branching.
The agent-facing API
type BranchVariant = {
/** Branch label (shown in outcomes + trace). Defaults to `branch-<k>`. */
label?: string;
/** Branch source module path, resolved like `callAgent` paths. Required. */
source: string;
/** State handed to the branch as its run input. Defaults to `{}`. */
input?: AgentJson;
};
type BranchOutcome = {
label: string;
branchId: string; // maps 1:1 to the branch's store path
status: "completed" | "paused" | "failed";
output?: AgentJson; // when completed
pendingPrompt?: string; // when paused (e.g. a chidori.input prompt)
error?: string; // when failed
};
// On the chidori object:
branch(variants: BranchVariant[], options?: {
concurrency?: number; // max branches running live at once (cost cap)
}): Promise<BranchOutcome[]>;sourceis required: a branch runs its own continuation module, never a copy of the parent (which would re-reachchidori.branchand recurse). Paths resolve likecallAgentpaths — relative to the working directory.- At most 16 variants per call: every branch makes live host calls past the fork (real LLM/tool spend), so an unbounded fan-out is a cost hazard before it is a correctness one.
- Every variant is validated (and its source read) before any branch runs,
so a missing
sourceor a typo'd path fails the whole call without spending anything — and without recording anything. - Returns all outcomes (compare, don't merge). The agent runs its own
selection:
const best = outcomes.reduce(pick); - A
pausedoutcome carries abranchIdyou can resume out-of-band withchidori branch-resume(see below), keeping the JS surface a single awaited Promise.
How it works
The branch call is a single recorded host call whose result is the outcomes
array — on parent replay it returns cached and the branches never re-run. For
each variant, the runtime:
- Anchors the branch on the parent's state at the fork: the parent's captured
workspace state plus the variant's explicit
input. - Runs the branch's source module as its own durable sub-run — live, through the same recorded-effect path as any run, under the same approval policy as the parent, journaling into the branch's own store.
- Settles the outcome —
completedwith the module's return value,pausedwith the pending prompt when the branch suspended on a host call, orfailedwith the error — and folds the branch's records into the parent's journal, so the full fan-out is one auditable history.
Variants run in waves of options.concurrency workers (default 1 —
sequential; clamped to the variant count). Each branch gets its own isolated
JS VM; outcomes are settled and persisted in variant order after each wave
finishes, so the journal and the outcomes array are deterministic regardless
of completion order.
Nested chidori.branch inside a branch is rejected; the rejection
surfaces as a failed outcome for that branch rather than failing the whole
call.
Tracing is free: branch records nest under the branch call, so with
OTEL_EXPORTER_OTLP_ENDPOINT set the operator sees the fork live as a
branch span with one child subtree per strategy, side by side.
The branch store
When the parent run persists (.chidori/runs/<run_id>/), every branch sub-run
is persisted under it:
<run dir>/branches/op-<fork point>/
anchor.json fork-time anchor: the parent's captured workspace state
branch-<k>/
source.ts the branch's own EDITABLE source copy
checkpoint.json the branch's journal artifact (a compacted journal —
not the parent's records.jsonl format)
branch.json metadata: label, id, status, pending input, input,
output/error
history/ the branch's git-like source history: a fork commit
of the variant's source, parented on the parent
run's head commit, plus one commit per accepted
edit ([Source History](./source-history.md))The anchor and the per-branch source copies are written before the fan-out
runs, so even a crash mid-fan-out leaves re-runnable branch stores behind. The
branchId in each outcome maps 1:1 to the branch's store path.
Resume and edit-and-rerun
The store makes a branch independently operable out-of-band, after the parent has moved on:
# List a run's persisted branches and their states:
chidori branches <run_id>
# A branch paused on chidori.input()? Answer it:
chidori branch-resume <run_id> <branch_id> --value "blue"
# Edit a strategy and re-run ONLY that branch from the same anchored state:
$EDITOR .chidori/runs/<run_id>/branches/op-*/branch-001/source.ts
chidori branch-rerun <run_id> <branch_id>Note that branch-resume's short flag -v means --value (the response to
deliver), not verbose. Both commands default their model to the one recorded
in the parent run's manifest (override with --model or CHIDORI_MODEL), and
accept --trusted/--untrusted for the branch's live gated effects — the
same posture flags as chidori run.
- Resume replays the branch's journal with your
--valueanswering the pendinginput()(the same mechanism the server's/resumeuses), then runs the branch'ssource.tslive to its next outcome. Resume answersinput()pauses; approval/signal pauses are reported but not resumable out-of-band. - Edit-and-rerun discards the branch's previous journal and re-runs the
branch fresh from the parent anchor with whatever
source.tsnow contains. The anchored state (fork-time workspace state + the variant'sinput) is identical to the original fork, so only the branch's code is the variable. Branch runs never go through the run manifest's source-identity gate — the anchor is the captured state, not a source check.
A resumed or re-run branch updates only its own store; the parent's recorded
branch outcome is immutable history (compare, don't merge). The branch's
code history is kept too: each edit that actually runs (branch-rerun, or
a resume whose source.ts changed) chains a commit onto the branch's
history/ store, so every strategy version that ever ran from the anchor
stays recoverable and diffable — chidori history <run_id> shows the chains
and --diff compares any two versions (see
Source History).
Correctness and determinism
- Parent determinism: the
branchcall is recorded with the outcomes as its result; parent replay short-circuits the fan-out like any cached host call. - Branch determinism: a persisted branch is replayable from its own stored journal; resume is that replay plus live continuation.
- One coherent history: each branch journals into its own store; settling folds its records into the parent's journal, so live runs and replays see the identical, collision-free record — a branch that violates this invariant fails the call rather than corrupting the journal.
- State-handover fidelity: branches inherit the captured workspace state
plus the explicit
input, not the parent's in-flight JS locals. The agent passes what a branch needs.
Cost, safety, concurrency
N branches make N sets of live host calls past the fork — real LLM/tool
spend. The controls: options.concurrency caps simultaneous live branches
(default 1), the fan-out is hard-capped at 16 variants, and each branch runs
under the same approval policy as the parent. Branches use separate, isolated
VMs; no shared mutable state.
Example
examples/branching/ is a runnable end-to-end
example: shared research once, a two-strategy fork, compare-and-pick, replay
via chidori resume, and the resume/edit-and-rerun workflows against the
branch store.