Runtime¶
Document type: Normative.
Scope: packages/archetype-ecs/src/archetype/runtime/ — the trusted Python
scripting boundary, plus typed adapters supplied by installed world libraries.
Ordinary runtime/world operations construct exact family operation models and
enter the process-owned CommandDispatcher through apply() or defer().
They never import authentication or actor models. World-library adapters follow
the same boundary: they dispatch exact family operations, while each library's
private extension composes its internal workflow only inside process ownership
provided by the framework.
1. Purpose¶
ArchetypeRuntime is the primary supported Python API. It:
- owns one explicit
RuntimeResourcesprocess graph; - creates lazy, strongly registered world and workflow handles;
- provides ergonomic async and sync scripting semantics; and
- sends canonical exact operations through the shared
CommandDispatcher.
The runtime is a trusted in-process boundary. Possession of the runtime grants the host the capabilities it was constructed with; it does not fabricate a default administrator or simulate RBAC. Untrusted callers authenticate at an ingress adapter, which enters the same dispatcher through actor-aware methods.
2. Hard requirements¶
R1 — Exact-operation dispatcher execution¶
Ordinary runtime modules may import only:
- the commands dispatcher contract and exact family operation models;
- supported cross-boundary component/configuration/result types; and
RuntimeResourceslifetime state.
They may not import API authentication, concrete family services, command
schedulers, ledgers, backend clients, or API modules. Concrete construction is
late-bound through archetype.wiring.
R2 — Handles hold identity, never live capabilities¶
RuntimeWorld holds its runtime/resources reference, configuration, local
lifecycle state, and a world_id after activation. It never holds an
AsyncWorld, concrete app service, backend client, or wiring graph.
R3 — Runtime is actor-free¶
ArchetypeRuntime, RuntimeWorld, and their sync variants do not accept or
retain ActorCtx. The supported runtime surface has no as_actor() operation.
Role testing and multi-tenant embedding exercise
CommandDispatcher.apply_as()/defer_as() through focused security fixtures
or an authenticated host adapter.
Trusted deferred submissions persist an explicit local origin. They do not record a fictional authorization decision.
R4 — Async context manager is canonical¶
Shutdown must:
- stop admitting new runtime and handle operations;
- wait for every already-admitted operation;
- cancel every supervised task across all owners, then await their completion;
- close workflow and world handles without destroying durable attached state;
- attempt every independent cleanup step in the current phase and aggregate failures; and
- be idempotent.
RuntimeResources executes the exact phase order admission,
supervised-tasks, workflow-handles, world-handles, audit, storage.
Every complete runtime or handle call is admitted by exact task into both the
process-operation and dispatcher gates. Shutdown synchronously publishes stop
intent to both gates before waiting on either lock, then finishes the
process-operation stop and drain before the dispatcher stop and drain. An
already-admitted exact task may cross its first dispatcher boundary and finish
same-task nested operations after stop intent; fresh direct/API work and a new
task, including a child task, have no inherited admission.
At the same close-start boundary, every extant owner gate enters process-stop:
raw owner-only work rejects, while an exact task already admitted by the
process or dispatcher may still cross its first owner boundary. An owner
created by such a continuation is born process-stopped. After both global
gates drain, shutdown re-snapshots the owner inventory, tightens every owner
gate to strict stop, and drains them all before supervised cancellation or
resource cleanup.
Supervised cancellation is broadcast before any owner is awaited, so one
cancellation-resistant task cannot delay cancellation of its peers. Closing
from the current admitted or supervised task rejects deterministically instead
of waiting on itself.
If a phase fails, the runtime rejects public work and retains only the failed
owners plus their dependencies. A later serialized shutdown() retries that
phase before advancing. RuntimeShutdownError reports the phase and ordered
owner-labelled original causes. Calls after successful finalization are
no-ops. Only terminal success detaches handler/dependency graphs; retryable
failure retains them intact.
R5 — Sync parity¶
SyncArchetypeRuntime and SyncRuntimeWorld expose the same product semantics
as the async classes without await. The sync facade owns an asyncio.Runner
and does not reuse an outer event loop. A retryable teardown failure retains
that runner and the supported shutdown() method retries the same process
owner; the runner is released only after successful finalization.
This requirement covers the generic framework surface. A separately installed world library may publish an async-only typed adapter and must document that gap explicitly. Installing the library does not add a synchronous domain method to the framework contract.
R6 — World handles are declarative and lazy¶
world = runtime.world(
"demo",
storage="./data",
cache=CacheConfig(...),
processors=[Movement()],
resources=[shared_state],
hooks=[(PreTick, on_tick)],
)
The call captures configuration and synchronously reserves a world-handle owner
but performs no backend I/O. The first operation single-flights activation
through the dispatcher. Processors, resources, and hooks are installed in
declared order. A failed activation must clean up any partially created live
world and remain retryable when the failure is transient. Activation and its
compensation are one admitted handle operation, so shutdown cannot close
dispatcher admission between CreateWorld and later installation or rollback.
R7 — Per-world operation serialization¶
All operations that target the same live world are serialized by
WorldRegistry, regardless of which runtime handle or API request originated
them. A handle-local lock may improve ergonomics but is not the concurrency
authority. Different worlds may proceed concurrently.
R8 — Runtime and world lifetimes are distinct¶
One runtime may own many handles. Closing a handle waits for work admitted through that handle and invalidates the local view; it does not tear down shared process services. Local admission closes before that wait, so late work is rejected even when cleanup remains retryable. Runtime shutdown owns shared services.
runtime.attach(world_id) returns a non-owning handle. Closing it never
destroys the world. Destruction is an explicit application operation and does
not delete append-only durable rows. Destroy first rejects late handle calls
and drains calls admitted earlier, then performs the durable destroy effect.
A failed destroy reopens the handle for work and retry; a successful effect
closes and releases the handle.
R9 — Boundary-safe results¶
The runtime receives immutable information snapshots, identifiers, typed receipts, supported result/configuration models, and explicitly specified DataFrames. It never receives a live world, service, registry, wiring graph, credential, or backend client.
R10 — Storage and cache coercion¶
storage: str | Path | StorageConfig | None and cache: CacheConfig | None are
accepted at the scripting boundary. A string or path becomes
StorageConfig(uri=str(value)); richer backend policy remains below runtime.
R11 — Evaluation and world-library adapters¶
world.grade(...) queries the handle's append-only history and dispatches
RunGraders over that lazy frame; its outputs are ephemeral.
world.evaluate(...) instead dispatches an exact Evaluate operation. The
registered family handler owns snapshot pinning, grader execution, outcome
validation, and durable receipt persistence. The runtime supplies the handle's
explicit storage coordinates but does not pin or persist that evaluation
itself.
Durable world.evaluate(...) receipts require the world handle to use an
explicit StorageConfig(..., backend=StorageBackend.ICEBERG). Omitted, string,
and path storage forms select LanceDB and cannot persist evaluation receipts;
use world.grade(...) when persistence is not required.
Research(world).autoresearch(...) dispatches the exact direct-only AutoResearch
operation to the research-family handler. Trusted and actor-aware immediate
entry share that handler; actor-aware use requires operator, resolves a
live_world quota coordinate, and charges
200 * max(max_iterations, 1). Deferred entry rejects before catalog effects
because evaluator, preparer, and iteration callbacks are live capabilities.
The typed Research adapter is async. Its installed manifest registers only the
exact operation and typed adapter; it does not add AutoResearch methods to
generic async or sync world handles.
The dispatcher synchronously awaits the entire outer workflow inside one process admission. Callback execution does not hold a runtime handle or named world lock, so ordinary inner world and storage operations remain available without recursive dispatch. A ledgered workflow does retain its experiment-key admission: direct same-task reentry for that experiment fails fast, while a separately scheduled same-key call remains a normal waiter and must not be awaited before the callback returns. Sync callbacks must likewise resume the same experiment only after the outer call returns. Runtime shutdown therefore joins an admitted AutoResearch call before closing shared dependencies, without a research-owned task, owner reservation, or finalizer.
Physical evaluation enters through
PhysicalAI(world).run_hosted_episode(...), which
dispatches the exact RunHostedEpisode operation to the registered
physical-AI handler. The world handle must retain explicit storage coordinates. The
handler owns hosted Activity admission, remote Modal execution or
reconciliation by stable operation identity, and durable result publication;
the runtime does not run episodes or collect terminal rows itself. The typed
world-library adapter is async, and the framework does not import Physical AI
or add the operation to generic world handles. See
Physical AI and World Libraries.
R12 — Typed artifacts and transcript evidence¶
world.ingest_artifacts(*sources) is the supported file-ingestion boundary.
Each ArtifactSource names one exact file or Daft-readable glob and may supply
an explicit portable logical path. Recursive discovery is expressed by the
glob itself. The artifacts-family handler receives the runtime's exact effective
storage configuration, resolves the durable run and published tick head, scans
metadata, computes SHA-256 and XXH3-64 in one pass, writes an immutable
content-addressed object, publishes any media-specific index, and then
publishes the common artifact_files row.
It returns one ArtifactRef per occurrence. A repeated submission is a new
UUIDv7 occurrence that may point to the same content object.
world.artifacts() returns the current world's current-run common file index
as a Daft DataFrame. Table registration, world/run envelope columns, schema
checking, and Iceberg append semantics remain internal to the storage
substrate reached through the artifacts-family view. The runtime neither
inspects daft.Catalog nor exposes the storage service, family handlers, or
process wiring.
MissionWorld(world).ingest_claude_transcript(source) is the recommended coding-agent
transcript boundary. ClaudeTranscriptSource carries local input configuration
and stable project/session identity. The application workflow snapshots and
redacts the file, parses only the sanitized copy, ingests that copy as an
artifact, and appends normalized rows to the Iceberg transcript table. The
returned TranscriptIngestionResult identifies the sanitized ArtifactRef,
row count, trajectory linkage, and redaction outcome. The runtime does not open
the source file, write
narrative Components, or coordinate those steps itself. The Missions adapter
is async. MissionWorld(world).transcript_rows() returns the normalized
session and turn rows for the current run. Installing Missions does not add
these methods to generic world handles.
Artifact and transcript capabilities require the handle to retain explicit
storage coordinates. A handle created with runtime.attach(world_id) without
storage=... may still use live-world capabilities, but these storage-addressed
methods reject before dispatch instead of recovering coordinates from
process-local world state.
ArtifactSource, ArtifactRef, and ArtifactStoreConfig are the supported
top-level file contracts. Family-owned handlers and views, the storage port,
and process wiring remain internal implementation surfaces.
R13 — Observability is host-configured and quiet by default¶
Scripts own stdout. ARCHETYPE_LOG=debug|info|warning|error or
ArchetypeRuntime(log=...) configures the archetype logger at the runtime
boundary. Core and app layers emit records but configure no handlers. Imports
remain silent. At construction an otherwise unconfigured runtime owns at most
one package handler: a null handler by default, or a stderr handler when
logging is enabled. A later default runtime preserves an already enabled
handler. The adapter does not alter root logging, the global LogRecordFactory,
or foreign handlers and filters, so machine-readable stdout remains
deterministic.
When logging is enabled, the owned handler's fail-open filter replaces reserved
correlation fields with the active lowercase trace/span IDs and the validated
safe signal context. Callers cannot forge those fields through extra; with no
active context the fields are absent. Its formatter omits exception and stack
metadata from its own stderr rendering and substitutes placeholders for
non-primitive arguments, then restores those producer fields for later
host-owned handlers. Correlation fields remain enriched. Producer-side policy
remains responsible for sensitive text already supplied as a primitive string.
Tracing uses the OpenTelemetry API. A host-registered provider is respected; optional Logfire or OTLP backends are selected only at the host/runtime boundary. With no configured backend the API remains a no-op and does not prevent a later host from registering one. Signal names, safe attributes, failure handling, and metric cardinality follow the normative Observability contract; telemetry never changes a runtime result or exception.
R14 — Public callables do not accept raw services¶
Supported callables may accept ArchetypeRuntime, handles, configuration,
components, callbacks, and safe models. They may not require callers to pass a
concrete service or RuntimeResources. Repository checks enforce this rule.
R15 — Multiple runtimes¶
A process may hold multiple runtimes. Each owns its RuntimeResources unless
an explicit internal host composition injects one. Cross-runtime live-handle
transfer is out of scope; durable identity and storage coordinates are the
interchange boundary.
R16 — Agent Missions V1¶
Missions(runtime, name, config=..., storage=...) returns an async Missions
handle. It configures one mission-capable world with the
built-in Components, graph view, transition processors, durable
author-and-critic Activity binding, and injected Sandbox Backend plus
coding-agent and critic drivers. The V1 workflow admits only the Modal sandbox backend
for end-to-end missions; submission rejects any other configured backend
deterministically before admission. The family-owned
Sandbox Service retains the author Session and owns fresh candidate-scoped
critic Sessions. Authors submit typed tasks and critic policies; they never
wire that bundle themselves. A custom critic driver declares driver_id, and
every submitted task policy must name that configured identity.
Passing validators and publishing the exact head moves a task to candidate.
The runtime returns terminal success only after a separate critic sandbox has
verified the exact base/head/diff and a processor has accepted its
identity-bound receipt. Blocking findings become the next author dispatch's
durable repair input. Reviewer outages do not consume author dispatches;
exhausted review budget raises while leaving the task pending review.
The handle owns a strongly registered workflow reservation. Its first submit
or run constructs and binds the internal mission service exactly once; later
operations resolve that same owner without a parallel service registry.
Missions.accept() records a durable MissionRun and returns its run_id
before SubmitMission or RunMission complete. Closing the initiating coroutine
does not cancel that run. SubmittedMission carries the exact durable World
identity. Therefore a
replacement process can recreate the handle with the same storage coordinates
and call run(submitted) or get_run(run_id) directly: wiring binds the Activity projector before
mutable World reconstruction, reinstalls process-local processors, resources,
and hooks, and reconciles provider-bound work instead of replaying it. Closing the handle
strictly stops and drains the reservation's exact-task admission before it
joins supervised critic work and closes sandbox resources plus its exact
mission-world cleanup without closing the parent runtime. Facade calls and the
registered direct SubmitMission, RunMission, RestoreMissionSandbox,
AcceptMissionRun, GetMissionRun, and CancelMissionRun handlers share that
owner gate, beginning before first
service construction or lookup. Therefore close drains work admitted through
either ingress, while late direct work rejects before construction or provider
effect. A failed cleanup
retains the facade, service, world, and dependencies for retry. Workflow
handles close before ordinary world handles during runtime teardown. Once
exact-world cleanup finishes, a later mission-world close failure retries only
the world-close stage rather than reusing the consumed cleanup lease.
Missions imports no concrete application service. It dispatches
SubmitMission, RunMission, RestoreMissionSandbox, AcceptMissionRun,
GetMissionRun, and CancelMissionRun; wiring constructs
the handler-side service with the same reservation. V1 is async-only, so sync
parity is outside the 0.6 world-library contract permitted by R5.
3. Canonical surface¶
Generic world operations below have sync parity. The installed Missions, Physical AI, and Research adapters shown here are async-only:
from archetype.missions import AgentMissionConfig, AgentTask, MissionWorld, Missions
from archetype.missions.trajectories import TrajectoryTurn
from archetype.physical_ai import PhysicalAI
from archetype.research import AutoResearchConfig, Research
world = runtime.world(
name,
storage=...,
cache=...,
processors=...,
resources=...,
hooks=...,
)
world = runtime.attach(world_id, storage=...)
missions = Missions(
runtime,
"software-factory",
config=AgentMissionConfig(
sandbox_backend=my_backend,
sandbox_environment="provider-image@sha256:digest",
),
storage=...,
)
submitted = await missions.submit(
repository="owner/repository",
branch="agent/change",
tasks=(AgentTask(...),),
)
mission_result = await missions.run(submitted)
# Checkpoint references are evidence only. Workflow restore fails
# explicitly until a checkpoint is bound into immutable Activity admission.
eid = await world.spawn(Position(x=0), Velocity(dx=1))
ids = await world.spawn_batch(Position(x=0), count=10_000)
ids = await world.spawn_many([[Position(x=float(i))] for i in range(100)])
await world.despawn(eid)
await world.update(eid, Position(x=10))
await world.add_components(eid, Health(hp=100))
await world.remove_components(eid, Velocity)
refs = await world.ingest_artifacts(ArtifactSource(...))
mission_rows = await MissionWorld(world).query_trajectory(TrajectoryTurn)
hosted = await PhysicalAI(world).run_hosted_episode([...], provider=...)
await world.add_processor(MyProcessor())
await world.remove_processor(MyProcessor)
await world.step()
result = await world.run(steps=10)
episode = await world.run_episode(EpisodeConfig(...))
rollout = await world.run_rollout(RolloutConfig(...))
research = await Research(world).autoresearch(AutoResearchConfig(...), evaluator)
branch = await world.fork(name="branch-a")
await world.destroy()
df = await world.query(Position, Velocity)
outcomes = await world.grade(Position, graders=[grader])
info = await world.info()
history = await world.history(limit=100)
processors = await world.list_processors()
hooks = await world.list_hooks()
resources = await world.list_resources()
hook = await world.add_hook(PreTick, on_tick)
await world.remove_hook(hook)
await world.shutdown()
Component instances and types remain variadic at the ergonomic boundary.
update overlays existing component types; add_components changes the
entity's archetype. These intents remain distinct.
4. Out of scope¶
- HTTP/FastAPI and authentication;
- authorization or role simulation;
- direct command-ledger, scheduler, audit-store, or backend access;
- cross-runtime live-object transfer;
- distributed process coordination inside the runtime package; and
- schema migration policy.
5. Module layout¶
packages/archetype-ecs/src/archetype/runtime/
__init__.py
runtime.py ArchetypeRuntime and SyncArchetypeRuntime
world.py RuntimeWorld and SyncRuntimeWorld
entrypoint.py managed script decorator
_config.py scripting-boundary coercion
packages/archetype-missions/src/archetype/missions/runtime.py
Missions and MissionWorld typed adapters
packages/archetype-physical-ai/src/archetype/physical_ai/runtime.py
PhysicalAI typed adapter
packages/archetype-research/src/archetype/research/runtime.py
Research typed adapter
Storage session construction lives below runtime. World-library adapters live with the behavior they expose, and the framework never imports those packages by name.
6. Canonical example¶
from archetype import ArchetypeRuntime
with ArchetypeRuntime.sync() as runtime:
world = runtime.world("demo", processors=[Movement()])
entity_id = world.spawn(Position(), Velocity(dx=1, dy=2))
world.run(steps=3)
print(world.query(Position).collect().to_pylist())