Skip to content

The agent() lifecycle

packages/core/src/runtime.ts

This is the heart of the engine. Every agent() call walks the same fixed 13-step sequence — and the order is load-bearing. The journal lookup happens before any model is spawned (that's resume), the budget and agent-cap gates fail fast as values, and the semaphore slot is always released in a finally. Click through each step; the panel shows the real code for that stage.

seq++ — the replay key

Every agent() call grabs a monotonic seq from the runtime. It's the primary key in the journal — the entire resume mechanism keys off it. Same script + same args ⇒ identical seq order, which is exactly why scripts must be deterministic.

this number is everything
runtime.ts · agent() → step 1/12
const agent = async (prompt, opts = {}) => {
  const mySeq = seq++;                       // monotonic, per-runtime
  const phase = opts.phase ?? currentPhase;
  const label = opts.label ?? `agent-${mySeq}`;
  const key = `${mySeq}:${phase}:${label}`;

Why the order matters

A few invariants fall straight out of this sequence:

  • Journal-before-spawn is the whole resume story. By the time control reaches the adapter, we've already proven this seq has no cached result. See Journal & resume.
  • Gates throw values, not exceptions. Budget and agent-cap produce a tagged WorkflowError wrapped in WorkflowThrow, so your script body can try/catch it like any error while library code keeps threading Result.
  • spawned++ is synchronous. The cap is claimed the instant the check passes, so a burst of concurrent launches can't all slip past maxAgents.
  • release() lives in finally. Success or throw, the semaphore slot goes back to the next waiter. Omitting it would deadlock the run.

The shape in one glance

Everything the runtime does is observable through events — there are no side channels.

Built from the source of @workflow/{schema,core,adapters,cli,ui}.