Execution Hierarchy¶
Document type: Normative.
Scope: archetype.world.simulation and exact dispatcher operations.
1. The four execution levels¶
Rollout = N episodes, each in a fresh lifecycle-owned fork.
Episode = step until termination or a cap on the supplied world.
Run = N steps with one RunConfig; no termination check or fork.
Step = one committed managed tick.
Episodes do not fork. Rollouts do. A caller may run an episode on a base world or on a caller-created fork; the episode preserves that choice.
The supported runtime usually exposes these operations through a
RuntimeWorld handle. Runtime constructs the exact execution model and enters
CommandDispatcher.apply; authenticated API routes construct the same model
and enter CommandDispatcher.apply_as. The registered handler then calls the
module functions in archetype.world.simulation.
2. Step and run¶
step(registry, world_id, run_config, **inputs) acquires the registry's exact
world lease and advances one managed tick. Before admitting a new tick it
retries any retained required-projector receipt. The core tick then
materializes due commands, computes and publishes state, and returns a stable
CommittedTickReceipt. If a required projector is bound, managed simulation
retains, projects, and acknowledges that receipt before returning.
run(...) acquires the lease once and invokes the lock-held step path
run_config.num_steps times. Repeated runs preserve the immutable run identity
owned by the world:
await world.run(steps=100) # tick 0..100
await world.run(steps=100) # tick 100..200, same world and run_id
A pre-publication failure leaves the tick retryable. A required-projector failure is post-commit: it raises with the exact receipt, retains that receipt, and never replays the tick.
3. Episode¶
An episode runs one supplied world until a termination condition fires or
max_steps is reached.
class EpisodeConfig(BaseModel):
episode_id: UUID = Field(default_factory=uuid7)
run_config: RunConfig = Field(default_factory=RunConfig)
max_steps: int = 1000
terminal_component: type[Component] | None = None
terminal_field: str | None = None
terminal_all: bool = True
termination: Callable[[AsyncWorld], bool] | None = None
Termination has three forms:
- With only
terminal_component, the episode stops when that component appears in an active signature. - With
terminal_componentplusterminal_field, persisted values are reduced throughiStorageService.terminal_allselects all-versus-any. terminationis a trusted in-process predicate over the live world.
Every condition is bounded by max_steps. The result identifies the exact
episode, world, immutable run, starting tick, final tick, termination outcome,
and duration.
class EpisodeResult(BaseModel):
episode_id: UUID
world_id: UUID
run_id: UUID
start_tick: int
final_tick: int
terminated: bool
duration_steps: int
run_episode(...) acquires one exact-world lease for the whole bounded
attempt. It does not implicitly fork.
4. Rollout¶
A rollout repeats one episode template on independent forks:
class RolloutConfig(BaseModel):
rollout_id: UUID = Field(default_factory=uuid7)
episode_config: EpisodeConfig = Field(default_factory=EpisodeConfig)
num_episodes: int = 1
parallel: bool = False
name_prefix: str = "ep"
destroy_forks_on_complete: bool = False
The world lifecycle creates each fork. Sequential rollouts await episodes in index order. Parallel rollouts keep each fork under its own exact-world lock and preserve result order, but the parent structurally drains every started episode before it returns or raises. One episode failure does not cancel siblings: bounded siblings finish naturally so cancellation cannot interrupt the transfer of a newly registered fork into rollout-owned teardown. The first observed child failure establishes its causal chain's precedence after the drain. If that child's teardown also fails, the exact teardown failure replaces the earlier child failure as primary for the chain, inherits its precedence, and retains the earlier failure as its cause. Additional failures are recorded as deterministic child/phase/type/fork notes so that causal chain is never replaced.
For either execution mode, caller cancellation follows the same boundary. The
rollout cancels each started child once so episode work stops promptly, while
shielding fork acquisition until the child owns the returned ID and shielding
that fork's teardown to completion. Repeated cancellation cannot interrupt
cleanup. Cancellation propagates only after every started child reaches its
finally teardown, and a cancelled sequential rollout does not start its next
episode. A substantive child failure observed before caller cancellation
remains chained beneath the caller's original cancellation. A teardown failure
first observed after caller cancellation instead becomes primary after cleanup
completes, with the initiating cancellation retained as its cause or note.
If destroy_forks_on_complete is true, application teardown runs in finally
for each fork. It reconciles committed work, cancels unsettled durable commands,
then delegates lifecycle close. Closing removes live ownership but never
deletes persisted world, command, or audit history.
class RolloutResult(BaseModel):
rollout_id: UUID
base_world_id: UUID
episodes: tuple[EpisodeResult, ...]
num_episodes: int
total_duration_steps: int
Forks inside a rollout are family implementation details. The external audit unit is the one gated rollout call, not one authorization event per internal fork.
5. Gate proxies and permissions¶
Authenticated API routes expose authorized step, run, run_episode, and
run_rollout operations. Each constructs one exact model and enters the
actor-aware dispatcher once. Commands-owned policy authorizes it, the
registered world handler executes it, and bounded advisory access evidence
records the result.
| Method | viewer | player | operator | admin |
|---|---|---|---|---|
step / run |
— | — | ✓ | ✓ |
run_episode |
— | — | ✓ | ✓ |
run_rollout |
— | — | ✓ | ✓ |
6. Executable contracts¶
Focused world execution behavior lives under tests/world/. Dispatcher
authorization and API safe-result behavior remain under tests/commands/ and
tests/api/.