ADR-017: SDL Runtime Layer¶
Status: Proposed Date: 2026-03-30 Deciders: Brad Edwards
Historical note: ADR-035 supersedes this proposed runtime direction. The local parser, compiler, planner, and target registry described here were removed after the RAES SDL cutover. The current runtime handoff uses RAES SDL and RAES runtime planning.
Context¶
The SDL is now the authoritative scenario specification surface. The next layer must execute that model directly rather than adapting legacy scenario/runtime code that predates the SDL.
The first runtime attempt introduced the right package boundary but the wrong core abstraction: it treated raw SDL section entries as generic executable steps. That collapsed reusable definitions and bound runtime instances into the same concept, made reconciliation incomplete, and pushed too much meaning into a single cross-domain interface.
The historical local runtime was planned inside this package boundary:
local SDL parser -> specification and validation
local runtime package -> compile, plan, execute contracts
aptl.backends.* -> target-specific implementations
Decision¶
Adopt a three-stage SDL-native runtime architecture:
- Compile
Scenario -> RuntimeModel - Plan
RuntimeModel + BackendManifest + RuntimeSnapshot -> ExecutionPlan - Execute domain plans through explicit runtime target protocols
Compiler¶
compile_runtime_model() is a pure normalization pass that separates reusable
SDL definitions from bound runtime instances.
Examples:
- top-level
featuresremain templates, whilenode.featuresbecome node-scopedFeatureBindings - top-level
conditionsremain templates, whilenode.conditionsbecome node-scopedConditionBindings - top-level
injectsbecome first-class orchestration resources, whilenode.injectsbecome optional node-scopedInjectBindings that target those resources nodes+infrastructurebecome deployable network/node resources- events, scripts, stories, workflows, metrics, evaluations, TLOs, goals, and objectives become resolved runtime graph nodes with canonical addresses
Bound condition refs fail closed. Unqualified condition refs must resolve to exactly one binding; zero or multiple matches produce diagnostics instead of implicit fan-out. Event inject refs resolve directly to top-level inject resources and produce diagnostics when the named inject is missing.
Planner¶
The planner no longer emits a flat step DAG. ExecutionPlan is composite:
ProvisioningPlanOrchestrationPlanEvaluationPlan
Each plan operates on canonical runtime resources inside its own temporal model. Reconciliation is explicit and complete:
- desired-only ->
CREATE - changed ->
UPDATE - snapshot-only ->
DELETE - identical ->
UNCHANGED
Plans are provenance-bound to an optional target name, backend manifest, and
base runtime snapshot they were reconciled against. Direct planner output is
unbound by default; only manager-generated plans or plans with an explicit
target_name are applyable.
Runtime resources carry two dependency sets:
ordering_dependenciesfor same-domain create/start ordering and reverse delete orderingrefresh_dependenciesfor downstream refresh propagation when upstream state changes
Cross-domain refs participate only in refresh propagation. Fixed phase order
remains provisioning -> evaluation -> orchestration.
If any same-domain ordering graph is cyclic, planning fails closed with runtime
diagnostics rather than guessing an execution order.
Capability Model¶
Capabilities are domain-specific rather than a single overloaded bag:
ProvisionerCapabilitiesOrchestratorCapabilitiesEvaluatorCapabilities
The planner validates semantic requirements from the compiled model, including node types, OS families, scaling limits, ACL usage, content types, account features, orchestration usage, workflows, workflow predicate condition refs, scoring, and objectives. Capability-relevant variable refs consumed by the planner must still resolve soundly even when SDL semantic cross-reference validation was skipped earlier:
- undeclared capability-relevant
${var}refs are planner errors - finite
allowed_valuesdomains are first revalidated against the SDL field being parameterized before backend capability checks run - only field-valid finite domains are checked against backend capabilities
- declared variables without a finite field-valid pre-instantiation domain produce warning diagnostics and defer exact capability validation until instantiation rather than guessing from defaults
Runtime Target And Registry¶
Backends must provide an explicit BackendManifest. Runtime targets are created
through a registry that separates:
manifest()for capability introspectioncreate()for backend instantiation
There is no fallback capability inference from a provisioner instance, and
create() must instantiate components against the same manifest returned during
introspection. Phase 1 intentionally supports at most one evaluator per target;
explicit evaluator partitioning is deferred until the runtime has a real routing
model.
RuntimeTarget is self-validating: manifest presence, component shape, and the
required invokable protocol surface must all match both for registry-created
targets and direct construction. Validation is signature-aware: methods must be
callable with the runtime's actual lifecycle call shapes, not merely present by
name.
At execution time, backend exceptions and invalid lifecycle return payloads are
treated as structured runtime failures rather than bubbling up as uncaught
manager crashes.
Protocols¶
Protocols consume domain plans, not generic steps:
Provisioner.apply(provisioning_plan, snapshot)Orchestrator.start(orchestration_plan, snapshot)Evaluator.start(evaluation_plan, snapshot)
Orchestrators and the phase 1 evaluator are lifecycle services with status()
and stop(). Failed runtime-service startup triggers best-effort rollback of
started services while preserving any provisioning state already applied.
Services are only started when their domain plan has actionable operations, but
delete-only reconciliation still runs through the same lifecycle entrypoint.
Objective window refs remain declarative scope/refresh inputs. They do not
create cross-domain executor ordering semantics. depends_on remains the only
objective ordering relation.
Consequences¶
Positive¶
- The runtime now matches SDL semantics instead of forcing them through a flat step abstraction.
- Reconciliation is honest and supports deletes as well as updates.
- Capability validation can fail fast on real backend mismatches.
- Ambiguous or unbound runtime refs are rejected instead of being guessed.
- Phase 1 stubs exercise the correct contracts for future real backends.
Negative¶
- The just-added runtime API is intentionally broken and replaced.
- The compiler/planner split adds more explicit types and indirection.
- Future backends must implement manifests and domain protocols from day one.
- Real scenarios must bind orchestration/evaluation refs unambiguously.
Scope Boundaries¶
- This decision does not preserve or adapt legacy backend/runtime code.
- Phase 1 ends at compiler, planner, manager, stubs, and tests/docs.
- Real target implementations follow later on top of these contracts.