Skip to content

Determinism and resumption

The sandbox lets a script orchestrate many actions as one unit. Some of those actions take a long time, and some need to pause for a human. So the sandbox needs an answer to a hard question: how do you resume a script partway through, when the runtime that was executing it is long gone? Its answer is to record everything non-deterministic a run does, and replay that record to reconstruct the run’s exact state. This page explains the record — the call log — and why replaying it is safe.

A script is ordinary code with local variables, a call stack, and a position in its control flow. When an action inside it needs to wait — say, for a human to approve something — there is no practical way to freeze that live execution state and thaw it later, especially across a process restart or a different machine. Serialising a running JavaScript stack is not on the table.

The sandbox sidesteps the problem entirely. Instead of preserving a paused execution, it re-runs the script from the top on resume, and makes the re-run reach the same point without redoing the work. That only works if re-running is guaranteed to follow the same path — which is what determinism buys.

Every side-effecting or non-deterministic operation a run performs is appended, in order, to a call log. There are three kinds of recorded operation:

  • Action calls{ type: 'action', name, input, response }. When the script calls an action, the sandbox records which action, with what input, and what it returned.
  • Clock reads{ type: 'now', response }. The sandbox rewires Date.now() and new Date() (with no arguments) to a recorded source. Deterministic uses like new Date(0) are left alone.
  • Random reads{ type: 'random', response }. Math.random() is rewired the same way.

Everything else a script does — arithmetic, string handling, branching, writing to data — is pure with respect to these inputs. If the recorded operations return the same values, the script must take the same path and reach the same state. That is the whole premise.

When you pass a prior run’s callLog back to eval, the sandbox runs the script again from the start, but each recorded operation is served from the log instead of performed for real:

  • An action call whose slot is recorded returns the logged response immediately — the action’s execute is not invoked again. This matters: replaying a “send a message” action must not send it a second time.
  • A clock or random read returns its logged value, so time and randomness are frozen to what the original run saw.

The run advances deterministically until it reaches the position where the original run paused — the interrupted action’s slot, which was reserved but never recorded. There the script executes for real again, this time carrying the caller’s answer. See Resume an interrupted script for the API.

Because the script genuinely re-runs, its console output is regenerated in full on resume — the completed run carries the whole output, not just the tail after the interrupt.

A run pauses when an action throws ActionInterrupt. The sandbox stops the run and surfaces a resumable checkpoint. The interrupted call’s slot is deliberately left unrecorded so that a resumed run executes it fresh.

Concurrency complicates the picture slightly. Slots are reserved synchronously at call time, in call order — so ordering is stable even when a script fires several actions at once via Promise.all. If an interrupt fires while sibling calls are still in flight, those siblings may finish and record behind the interrupted slot, leaving a null hole where the interrupted call sits (and possibly at other reserved-but-uncompleted slots). A null slot is treated exactly like a missing entry: on replay it is executed fresh rather than served from the log. This is why the log’s type is (CallLogEntry | null)[].

Replay is only sound if the re-run produces the same sequence of operations. The sandbox verifies this as it goes. At each slot it compares what the script is doing now against what was recorded:

  • If the script reads the clock where an action was recorded, or calls an action where a clock read was recorded, the kinds disagree.
  • If the script calls a different action than the one recorded at that slot, the names disagree.

Either way the sandbox throws CallLogMismatchError, carrying the position that diverged. The message is unambiguous: the script is not deterministic and cannot be safely replayed — its control flow depended on something the log did not capture (ambient state, an un-rewired clock, external randomness). The guard fails the resume loudly rather than silently producing a run that never really happened.

Keeping scripts deterministic is therefore the one rule the model — or the author — must follow: branch only on data, on action results, and on the sandbox’s recorded clock and RNG. Do that, and every run is exactly reproducible, which is what makes a script a safe, resumable unit of work rather than a one-shot side effect.

Determinism and resumption are what let the sandbox sit alongside the agent as a first-class consumer of the same actions (see Architecture). An agent surfaces an interrupt as a paused run you resume or reject; the sandbox does the same for a script, and threads the two together when a sandbox is wrapped as an agent tool. Both rest on the shared interrupt mechanism from the core — the sandbox just adds the call log that makes a whole script resumable, not only a single action.