Scripts and the JournalDeterministic QuickJS orchestration with a content-keyed journal — replay without re-execution.

A script child is JavaScript the agent wrote, running in a deterministic QuickJS realm whose every effect is journaled. It is the orchestration engine: scripts orchestrate, microVM code computes. A script is spawned by passing script to delegate; it becomes a run of kind: 'workflow' with no session of its own.

Implementation: agents/src/workflow-host.ts.

Determinism is enforced, not requested

The realm removes the two things that would break replay (workflow-host.ts:332-333):

Math.random = function () { throw new Error('Math.random is not available in workflows; take randomness as input') } Date = function () { throw new Error('Date is not available in workflows; use await ctx.now()') }

There is no fetch, no setTimeout, no require/import, no process, no globalThis. lintWorkflowSource (:129-158) rejects those patterns at submission time, so the failure is a clear message at authoring rather than a mystery at runtime. TextEncoder/TextDecoder are provided (:268-331) as hand-rolled UTF-8 implementations — pure functions, safe to replay — because a real agent workflow once died on a byte-size check without them.

Resource bounds: source ≤ 256 KiB (WORKFLOW_SOURCE_MAX_BYTES), memory 64 MiB, stack 1 MiB, and a 2-second fuel slice (DEFAULT_FUEL_MS) between awaits — exceed it and the run fails fuel-exhausted. Stack frames read as workflow.js:LINE because each eval unit is given that filename (:451), which is what makes the error inspector able to excerpt the offending source line.

The ctx API

Every method is defined in the prelude (workflow-host.ts:205-267); each one that matters is journaled.

Method

What it does

ctx.input, ctx.runId

This run's input and id

ctx.call(tool, input, {description})

Invoke a callable; description is narration

ctx.delegate(spec) / ctx.agent(spec)

Spawn a child; unwraps its output, throws ActionError on failure

ctx.parallel(thunks) / ctx.parallelSettled(thunks)

Concurrent fan-out

ctx.step(label, fn)

A named unit of work; journals start and end phases

ctx.plan(plan)

Publish the checklist

ctx.sleep(ms), ctx.minutes(n), ctx.hours(n)

Park on a timer

ctx.waitForEvent(match, {timeout})

Park until an event or signal arrives

ctx.continueAsNew(state)

Hand off to a successor; never resolves

ctx.now()

The clock, as a journaled effect

ctx.log(level, message, data)

A journaled log line

ctx.progress(patch)

Fire-and-forget UI progress — deliberately not journaled

The journal and content-keyed replay

Every effect writes to run_journal ((run_id, seq) primary key, CBOR entries). Entry kinds: call, result, timer, fired, wait, event, now, log, step, plan.

Resuming a script means running the source again from the top. What makes that safe is that each effect computes a deterministic content keyeffectKey(op, args) (:411-434), e.g. tool|${tool}|${JSON.stringify(input)} or sleep|${ms} — and journal entries are filed under that key rather than under arrival order. On replay, a matching group is consumed FIFO per key and its recorded result returned without re-executing.

Keying by content rather than sequence is not incidental: ctx.parallel continuations can complete in a different order live than on replay, and an order-indexed journal would mismatch. There is a test for exactly this (workflow-host.test.ts:353-390).

Narration rides outside the key. The {description} on ctx.call is attached to the journal entry but excluded from effectKey (:568-587), so relabeling a call for readability never invalidates a replay. That is what the run card's Activity drawer renders.

Bounds: 5,000 entries or 8 MiB per run (WORKFLOW_JOURNAL_MAX_ENTRIES, WORKFLOW_JOURNAL_MAX_BYTES), after which the run fails journal-cap — the signal to use continueAsNew.

One honest caveat

A ctx.call interrupted mid-flight — the process dies after the tool ran but before its result was journaled — re-executes on resume. Effects are at-least-once, not exactly-once, and agents/docs/security.md records this as a known gap rather than a solved problem. Signals and event deliveries, by contrast, are exactly-once, because they commit in a single transaction (SignalRun).

Related

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime