Skip to content

Agent Execution Flow

This document describes the deterministic execution loop that every runtime implements. The four pre-built runtimes (runtime-js, runtime-temporal, runtime-cloudflare, runtime-dbos) all use the core orchestration helpers documented here.

For the abstract framework concepts that this loop produces, see ./concepts.md. For session lifecycle and state-store contracts, see ./session-model.md.


The agent execution follows a step-based loop (see packages/runtime-js/src/js-agent-executor.ts):

  1. Claim Session — Executor calls stateStore.createSession() atomically before returning a handle. This persists the session with metadata (userId, tags, organizationId) and prevents concurrent execute() calls on the same sessionId. For Temporal and Cloudflare runtimes, this happens after starting the workflow/instance but before the handle is returned, closing the race window where async initialization hasn't persisted state yet.

    On resume, the runtime instead uses stateStore.compareAndSetStatus(sessionId, expectedStatuses, 'active', { expectedVersion }) to claim the session — CAS-promoting the status from 'paused' / 'interrupted' / 'failed' into a fresh execution slot. The discriminated return value ({ ok: true; newVersion } vs { ok: false; currentStatus; currentVersion }) lets concurrent resume attempts detect each other and bail.

  2. Initialize — Create initial agent state, append user message, create run record (or, on resume, increment resumeCount and create a new run record observing the existing session state).

  3. Step Loop — While status is 'running' or 'paused':

    • Poll stateStore.checkInterruptFlag (atomic check-and-clear) at the top of each iteration. Set flags from any process abort the run cleanly.
    • Build messages with buildMessagesForLLM()
    • Call LLM via adapter with messages + tools
    • planStepProcessing() determines next action from step result
    • Execute tool calls and sub-agent calls in parallel
    • Append results as messages
    • shouldStopExecution() checks stop conditions (maxSteps, stopWhen, __finish__ tool)
    • HITL boundary handling (v7 stateless suspension) — If any tool is client-executed, approval-gated, or any sub-agent has not yet completed, the runtime persists durable suspension state via saveStateAndPromoteStaging (writing pendingClientToolCalls, suspendedAwaitingChildren, or suspendedStepId depending on the kind) and EXITS THE RUN with RunOutcome.kind = 'suspended_*'. There is no in-memory waiter, no setTimeout, no Durable-Object hibernation guard. Resume is driven by a separate executor.resume(sessionId) invocation:
      • JS — A new runStepIteration invocation drains submissions and continues. Process restart is safe; pending entries are recovered from the state store.
      • Temporal — A NEW workflow instance is started with workflow ID ${prefix}__${agentType}__${sessionId}__resume-${N} (single-dash resume-N, per spec §5; WorkflowIdReusePolicy.ALLOW_DUPLICATE). The new workflow's mode='resume' branch calls the applyResultsAndReload activity. Note that retry() uses a DIFFERENT convention (__retry__N, double-underscore) for backward compat.
      • Cloudflare Workflows — A new workflow instance is started with mode: 'resume'; resume drains via applyResultsAndReload.
      • Cloudflare DO — A new run-loop iteration runs in the same DO instance (or a fresh DO if the original was evicted during the wait). All four paths begin with applyResultsAndReload (Temporal/CFW) or the equivalent resume bootstrap in JS/DO, which drains queued submissions, synthesizes timeout tool_errors for expired deadlines, and resumes the step loop from durable state. See ./concepts.md §Client-Executed Tools for per-runtime mechanics.
  4. Complete — Emit output chunk, end stream.

Error handling in resume/retry: If workflow creation fails after status has been transitioned (e.g., CAS from 'failed' to 'active' in retry), all runtimes perform a best-effort rollback of the status to prevent sessions from being stranded in an unrecoverable state. The CAS options.error field carries the failure reason through the rollback so the session ends up 'failed' with a meaningful error rather than silently stuck.

Core orchestration logic lives in packages/core/src/orchestration/:

  • step-iterator.tsv7 unified step iterator (runStepIteration); wraps phase-1/phase-2 tool execution, hook firing, staging promotion, and HITL suspension classification. Consumed by all four HITL-capable runtimes.
  • step-processor.ts — Determines what to do after each LLM step
  • message-builder.ts — Creates internal Message types for conversation
  • state-operations.ts — State initialization and tool building
  • stop-checker.ts — Stop condition evaluation
  • suspension-classifier.ts — Classifies a step's terminal state into a RunOutcome.kind ('suspended_client_tool' | 'suspended_awaiting_children' | 'suspended_step_partial' | ...).
  • run-outcome.tsRunOutcome discriminated union and SuspendedChildWait payload type.

Released under the MIT License.