This document describes the processor and runtime path that turns authored SDL into executable backend operations. It is an SDL-native architecture built for the SDL itself and its backend contracts. See ADR-004 and ADR-036 for the decision records.
In the broader ecosystem architecture, this document focuses on the processor-runtime-backend path that is currently implemented in code. It does not attempt to fully specify every other apparatus surface. In particular, the ecosystem distinguishes:
- authored scenario meaning in SDL
- the processor that instantiates, compiles, plans, and determines support
- the runtime that coordinates live execution against a target
- the backend that realizes deployable or simulated target operations
- optional participant implementations that consume participant contracts
- live runtime state
- archival run, evidence, and provenance records
The compile -> plan -> execute stack described here is therefore one important subset of the overall architecture rather than the whole experiment apparatus.
Time is one of the clearest examples of why those broader apparatus boundaries matter. The authored SDL may express timelines, deadlines, timeouts, or episode-local budgets, but the realized execution still depends on apparatus choices such as clock authority, time domain, pacing policy, synchronization strategy, and the ordering guarantees that the runtime/backend can actually honor. Those choices are not just incidental implementation details: they shape experiment validity and must be declared and preserved as part of run provenance when different realizations are compared.
Under the repository's coding standards,
this layer is where FM2 and FM3 work becomes most relevant. The
formalization target here is not raw YAML, but the typed runtime model,
processor plans, and execution contracts that preserve semantic meaning across
validation, compilation, planning, and backend execution.
This layer also draws from a different precedent set than the author-facing SDL surface. OCR and CACAO still matter, but the strongest implementation models here come from mature workflow and distributed-runtime systems:
- AWS Step Functions, Argo Workflows, and W3C SCXML for explicit control-flow semantics
- Kubernetes, Temporal, and OpenC2 for portable execution-state and language-neutral contract boundaries
aces_sdl -> parse + instantiate + SDL-language semantics
aces_processor -> compile + plan + support/contract semantics
aces_runtime -> live control + manager + control-plane APIs
aces_backend_protocols -> backend capability/protocol declarations
aces_backend_stubs -> non-normative in-memory target implementations
aces.* -> legacy compatibility wrappers
ADR-036 backs this package boundary with tools/check_repo_policy.py: the
full policy gate scans every Python file under each configured package root,
and the pre-commit gate runs the same architecture-as-code check against
staged changes.
instantiate_scenario(raw_scenario, parameters=None, profile=None) is the
repo-owned concretization pass that runs before compilation.
It:
- validates the normalized/expanded authoring input
- applies explicit parameter values
- applies SDL variable defaults
- rejects unresolved
${var}placeholders - rebuilds a closed
InstantiatedScenariowith portable binding/import, capability, and explicitness evidence - removes
variables,imports, andmodulefrom the concrete shape - reruns semantic validation after substitution
compile_runtime_model(scenario) then lowers the admitted instantiated scenario
into runtime objects. A direct or JSON-deserialized InstantiatedScenario
passes admit_instantiated_scenario() first; compiler callers cannot disable
that semantic gate.
It separates reusable definitions from bound runtime instances:
features-> feature templatesnode.features-> node-scoped feature bindingsconditions-> condition templatesnode.conditions-> node-scoped condition bindingsinjects-> first-class orchestration inject resourcesnode.injects-> optional node-scoped inject bindings layered on top of top-level inject resourcesnodes+infrastructure-> deployable network/node resources- orchestration and objective sections -> resolved runtime programs and graph nodes
The output is a RuntimeModel with canonical addresses for every runtime-owned
object.
Bound condition refs fail closed. An unqualified condition reference must resolve to exactly one bound runtime instance; zero matches and multiple matches are both compile-time diagnostics. Event inject refs resolve directly to top-level inject resources and fail if the named inject does not exist. Bound feature dependencies also fail closed: if a node-scoped feature binding declares a dependency on another feature that is not bound on the same node, the compiler emits a diagnostic instead of silently dropping that dependency.
Compiled workflows are not just flattened successor maps. WorkflowRuntime
preserves:
start_step- optional workflow timeout policy in the compiled execution contract
- per-step structured semantics (
objective,decision,switch,retry,call,parallel,join,end) - explicit call targets and ordered switch-case predicates
- explicit control edges
- external predicate dependencies
- prior-step state dependencies
- declared workflow feature usage
- a compiled
result_contractfor step-visible state - a compiled
execution_contractfor workflow-level state/history validation
Workflow control is the flagship current FM3 surface:
- it has explicit branching and re-entry behavior
- it defines portable execution-visible state
- it relies on reachability and visibility guarantees across multiple layers
That means workflow changes should be treated as state-machine work on the runtime model and contracts, not merely as YAML authoring changes.
The intent is not to clone any one system. The SDL keeps its own objective-centric workflow surface, but its semantics deliberately follow the best-practice pattern from mature systems: explicit branching semantics, explicit convergence rules, typed observable state, and a runtime contract that is portable across backend implementations.
plan(runtime_model, manifest, snapshot, target_name=None) is also pure.
It validates semantic backend requirements and reconciles desired runtime
objects against the current RuntimeSnapshot.
ExecutionPlan is composite:
ProvisioningPlanfor deployable resources and bindingsOrchestrationPlanfor events, scripts, stories, workflows, and inject stateEvaluationPlanfor condition bindings and objectives
Each plan is provenance-bound to:
- an optional target name
- the backend manifest used for validation
- the base snapshot it was reconciled against
Direct planner output is unbound by default. Only RuntimeManager.plan() or an
explicit target_name= bind a plan to a concrete runtime target for apply.
Reconciliation actions are explicit:
CREATEUPDATEDELETEUNCHANGED
Runtime resources carry two dependency sets:
ordering_dependencies: same-domain edges used for create/start ordering and reverse delete orderingrefresh_dependencies: edges whose changes force downstreamUPDATE
Cross-domain refs participate in refresh propagation, but not startup ordering. This keeps the fixed phase order intact while still making downstream plans honestly react to upstream changes. Ordering graphs must remain acyclic within each domain; the planner emits error diagnostics and invalidates the plan if a cycle survives into runtime planning.
aces_processor.reference.run_reference_processor(scenario, backend_manifest)
(and the ReferenceProcessor class) is the repository-owned reference
implementation of the processing model. It assembles the stages above into one
call — accepting SDL text, a file path, or an already-parsed scenario — and
returns a ReferenceProcessorResult carrying the compiled RuntimeModel, the
ExecutionPlan, and the combined compilation + planning diagnostics
(is_valid is false when any are errors). ReferenceProcessor.manifest_payload()
exposes the published processor manifest through the canonical renderer.
Per ADR-008 the processor is the semantics-bearing layer between SDL authoring
and backend realization, so the reference processor's responsibility ends at the
ExecutionPlan: it imports only the SDL/processor/contract layers and never
aces_runtime (the one-directional boundary enforced by
tools/policy/adr_policy.yaml). End-to-end execution is realized by composing
its plan with the reference runtime (RuntimeManager / RuntimeControlPlane);
the backend-conformance live probe drives exactly this composition, and the
RUN-313 tests use it to prove every contract version the processor manifest
declares is exercised end to end.
RuntimeSnapshot is the typed state model used by the planner and manager. Each
entry records:
- canonical address
- domain
- resource type
- resolved payload
- ordering dependencies
- refresh dependencies
- current status
This gives the planner and manager a typed state model instead of an untyped
resources/status map.
The broader time model is only partly materialized in current contracts. The implemented snapshot and control-plane schemas carry timestamps and timeout-related state, but they do not fully represent clock authority, time domain, synchronization policy, pacing mode, or realized temporal guarantees. Those concerns belong to the declared apparatus surface and to archival provenance when implementations compare different realizations.
Backends and processors publish a shared apparatus-manifest envelope with:
identitysupported_contract_versionscompatibilityrealization_supportconstraintscapabilities
Backend manifests preserve nested concern blocks inside capabilities:
provisionerorchestrator- zero or one
evaluator
Processor manifests preserve processor-specific capability blocks inside capabilities:
supported_sdl_versionssupported_features
At the ecosystem level, backend manifests are only one declaration surface.
The reference processor publishes the same shared envelope with processor-specific capabilities.
Participant-implementation manifests are a distinct apparatus surface. They
publish participant-implementation-manifest-v1, which declares implementation
identity, implementation kind, supported participant contracts, supported
decision-surface modes, tool and affordance expectations, compatibility,
concept bindings, and constraints.
Validation is semantic, not section-only. Current checks include:
- node types
- OS families
- total deployable node count
- ACL usage
- content types
- account features
- orchestration/workflow usage
- fine-grained workflow feature usage (
decision,retry,parallelbarriers, failure transitions) - workflow predicate assertion refs
- workflow predicate prior-step state refs and state-predicate subfeatures (
outcome-matching,attempt-counts) - objective usage
OrchestratorCapabilities expose both coarse workflow support and fine-grained workflow semantics:
supports_workflowssupports_assertion_refssupported_workflow_featuressupported_workflow_state_predicates
Both the backend and processor manifest surfaces also carry explicit
realization_support declarations. These make constrained realization and its
required disclosures visible in the machine-readable apparatus boundary rather
than leaving them implied by docs or implementation code.
The shared manifest envelope is enforced as a concrete declaration surface, not just a shape:
compatibilitymust name at least one compatible processor, backend, or participant implementation- every
realization_supportdeclaration must name non-empty disclosure kinds and at least one supported exact or constraint kind - processor capability blocks must declare non-empty SDL and feature support
- backend capability blocks must declare concrete provisioning and orchestration support, not empty concern shells
The reference backend, reference processor, and backend conformance profiles use
the shared v2 apparatus manifests. Legacy v1 manifest schemas remain in the
repo as deprecated reference artifacts, not as the current conformance target.
Participant implementations use participant-implementation-manifest-v1
alongside the backend and processor surfaces rather than nesting inside either
one.
Capability validation operates on admitted concrete values. Where
nodes.os or infrastructure.count was selected from an authored finite
domain, portable SDL provenance carries that domain and the compiler lowers it
to one typed CompiledCapabilityConstraint; the planner validates the whole
declared domain against backend limits. It does not reconstruct variable
definitions or pair private variable-spec/ref maps.
Targets must provide an explicit manifest. The registry separates capability
inspection from instantiation, and create() uses the manifest returned by
manifest() as its single source of truth:
registry.manifest(name, **config)registry.create(name, **config)
RuntimeManager drives lifecycle in this order:
- compile
- plan
- validate provisioning apply
- apply provisioning plan
- start evaluator only when the evaluation plan has actionable operations
- start orchestrator only when the orchestration plan has actionable operations
- on failed runtime-service startup, roll back started services while keeping provisioning state
- stop orchestrator -> stop evaluator -> delete provisioning resources
Participant implementations do not collapse into the backend boundary. The
backend remains responsible for world realization and execution services;
participant implementations are a separate apparatus concern whose identity,
configuration, and participant-visible decision surface belong in
participant-implementation-provenance-v1 rather than being inferred from
backend state.
The orchestration runtime contract includes:
- a plain-data workflow execution-state envelope
- a plain-data workflow history stream
- a compiled
result_contractfor step-visible state - a compiled
execution_contractfor workflow-level legality/history validation - control-plane operations for canceling running workflows and reconciling timeout expiry
- explicit compensation status/history when a workflow declares rollback behavior
Backends report portable execution envelopes rather than backend-native object
identity. The manager validates raw backend payloads against the compiled
contracts, not against incidental planner payload structure. Compiled workflow
predicates are fully typed runtime data; orchestrators should not rely on raw
SDL spec to execute workflow semantics.
Python typed workflow result models remain useful internally, but only as normalization helpers after boundary validation. They are not the backend protocol.
This is also why semantic modeling is split across processor and runtime contracts rather than parser code. The processor compiles backend-agnostic guarantees such as allowed transitions, result visibility, and portability of workflow state; the runtime validates live backend reports against those compiled contracts.
This mirrors the contract style used by mature multi-runtime systems:
- a portable wire/data contract at the boundary
- a compiled semantic contract between definition and execution
- published machine-readable JSON Schemas under
contracts/schemas/ - an async-style control-plane surface (
RuntimeControlPlane) that can be adapted to HTTPS/JSON without changing backend semantics - typed in-process adapters behind that boundary
The stack currently applies that pattern first to workflow results because workflow control is the sharpest semantic surface in the SDL/runtime stack.
The evaluator side follows the same contract discipline: compiled evaluation result/execution contracts are attached to observable evaluation resources, backends report plain-data evaluator result envelopes and history streams, and the manager validates those payloads against compiled contracts instead of accepting ad hoc evaluation dictionaries.
Objective window refs remain declarative scope/refresh inputs. They can force
objective refresh when referenced orchestration state changes, but they do not
create executor ordering edges across domains.
Objective windows compile through one shared normalized semantic form. The compiler preserves explicit resolved window references alongside the existing address sets so planner/runtime code can reason from canonical reference identities instead of reparsing raw SDL strings.
Planner FM2 semantics are explicit rather than incidental:
orderingedges define create/start and delete/teardown orderrefreshedges define recomputation/update propagation- refresh propagation is transitive over the refresh graph
- cross-domain refresh does not create startup ordering
Those rules are owned by aces_processor.semantics.planner, not by local planner
algorithm shape.
This phase is also intentionally composition-ready. Module/import expansion happens before semantic validation and compile, so the runtime layer operates only on canonical resolved identities rather than on source-file layout. That same foundation is what makes namespaced reusable workflow calls portable.
Composition is registry-ready as well:
- local imports remain supported through
path:andsource: local:... - reusable remote modules use
source: oci:... - concrete resolved imports may be pinned via
source: locked:... aces sdl resolvewritesaces.lock.jsonaces sdl verify-importsverifies lockfile, trust, digests, and signaturesaces sdl publishpackages a publishable SDL module as an OCI image layout
Resolution and trust happen before instantiation and semantic validation. Planner/runtime semantics see one admitted concrete scenario; replay-relevant resolution and binding facts remain under its typed provenance rather than in live imports, variables, or Python-private side channels.
RuntimeManager.apply() requires the plan provenance to match the manager:
- plan must be target-bound
- same target name
- same manifest
- same base snapshot
This prevents applying a plan against a different runtime target or a stale snapshot than the one it was reconciled against.
RuntimeTarget is self-validating at construction time:
- manifest presence and component shape must match
- required protocol methods must exist
- those methods must be invokable with the runtime's actual call shapes, not just be present by name
RuntimeManager also hardens the execution boundary at call time. Backend
exceptions and invalid lifecycle return payloads are converted into structured
runtime diagnostics instead of surfacing as unhandled crashes.
The HTTP adapter defaults to a fail-closed security configuration: no trusted
header identities and no bearer tokens are built in. Deployments that use
header identity must pass an explicit ControlPlaneSecurityConfig, set
trust_proxy_identity_headers=True, and only trust those headers behind an
authenticated proxy that strips caller-supplied identity headers.
The current runtime scope includes:
- compiler
- planner
- runtime manager
- registry
- control plane, HTTP adapter, operation store, security, and audit
- honest in-memory stubs
- tests and docs
Real Docker/cloud/simulation backends are outside this repository's current implementation surface. Such backends would have to consume and satisfy these contracts.