Spectre 0.2.8 adds precise Work programs authored as portable data. Compiled
and runtime-authored programs normalize into the same
Spectre.Execution.Program IR and execute through the existing
Spectre.Operation.Runtime. They therefore share one lifecycle, runner,
checkpoint, fencing, retry, control, query, and recovery implementation.
Authored data describes a finite graph. It never supplies a module, callback, MFA, evaluator, executor, authority grant, or lifecycle policy. Every step, predicate, inference, and migration names an operation that the host Agent already registered.
One IR from compiled or runtime declarations
A compiled Work uses the closed DSL:
defmodule MyApp.ResolveTicket do
use Spectre.Execution.Work,
id: :resolve_ticket,
version: 1,
entry: :lookup,
input: :map,
state: :map,
initial: :input,
budget: %{steps: 4, attempts: 4, duration_ms: 10_000, cost: 5}
step :lookup,
operation: :lookup_ticket,
input: :state,
save_as: ["ticket"],
next: :decide
decide :decide,
predicate: :resolved?,
input: :state,
on_true: :done,
on_false: :failed
finish :done, output: :state
fail :failed, :ticket_not_resolved
endThe runtime equivalent is ordinary JSON-shaped data:
{:ok, program} =
Spectre.Execution.Program.new(%{
"id" => "resolve_ticket",
"version" => 1,
"entry" => "lookup",
"input" => "map",
"state" => "map",
"initial" => "input",
"budget" => %{
"steps" => 4,
"attempts" => 4,
"duration_ms" => 10_000,
"cost" => 5
},
"nodes" => [
%{
"id" => "lookup",
"kind" => "step",
"operation_ref" => "lookup_ticket",
"input" => "state",
"save_as" => ["ticket"],
"next" => "decide"
},
%{
"id" => "decide",
"kind" => "decide",
"predicate_ref" => "resolved?",
"input" => "state",
"on_true" => "done",
"on_false" => "failed"
},
%{"id" => "done", "kind" => "complete", "output" => "state"},
%{"id" => "failed", "kind" => "fail", "reason" => "ticket_not_resolved"}
]
})Program.from_compiled/1 and Program.new/1 produce byte-identical
Program.to_data/1 values and equal digests for equivalent declarations.
The graph must be reachable and terminating. Cycles are rejected unless they
cross an explicit bounded repeat node, and every program has positive step
and attempt limits.
Closed expressions and registered operations
Spectre.Execution.Expression can read only the immutable input, current
state, or last operation result; embed a portable fixed value; or assemble a
map/list from those values. State writes use declared paths. Negative indices,
duplicate atom/string key forms, modules, MFAs, PIDs, functions, references,
and non-portable values fail closed. Program-authored literals and metadata
normalize recursively to JSON-stable values (non-contract atoms become their
string names), and expression nesting is bounded before identity is digested.
The host owns operation code:
defmodule MyApp.Agent do
use Spectre.Agent
operation :lookup_ticket, {MyApp.Tickets, :lookup},
input: :map,
output: :map,
side_effect: :none
operation :resolved?, {MyApp.Tickets, :resolved?},
input: :map,
output: :boolean,
domain: [true, false],
side_effect: :none
enddecide predicates and state migrations must resolve to pure
side_effect: :none operations. infer nodes resolve only to registered
cognitive operations and carry typed inference constraints. JSON operation
names are bridged only to an identically named existing registry entry; no
atom or executable code is created from authored data. Known inference enum
strings normalize to the existing contract atoms, while unknown enum values
are rejected instead of depending on the VM atom table.
Materialization and execution
A runtime Skill embeds one or more Programs and routes to a Work handler. Mount validates every operation binding and checks Work cost and duration against the effective Authority Envelope. Selection then produces one sealed materialization:
{:ok, materialization, skill_runtime} =
Spectre.Execution.Materializer.materialize(
skill_runtime,
%{text: "resolve ticket 42", meta: %{}},
%{scope: :support},
expected_revision: 1
)
:ok = Spectre.Execution.Materialization.verify(materialization)
{:ok, loop_ref, view} =
Spectre.start_execution(instance, materialization, id: "ticket-42")Routing, Work input resolution, prompt rendering, Definition pinning, prompt
receipts, and Execution projection happen once. Starting execution revalidates
the materialization and feeds the exact Program to the shared operational
runtime. The Instance APIs loop/2, loops/1, pause_loop/2,
update_and_resume_loop/3, resume_loop/2, and stop_loop/2 retain
their normal semantics. Materialization.verify/1 repeats the construction
binding for mount, route, continuation, input, plans, receipts, and mandatory
projection evidence; a self-consistent materialization digest cannot hide
drift between those fields and its projection.
An amendment can change only state paths listed in mutable_paths. It cannot
replace the Program, input, history, receipts, prompt plans, Definition Ref, or
materialization lineage. Recovery reloads the exact embedded Program and
continues under a fresh epoch and snapshot fence.
Prompt evidence and model constraints
An infer node refers to a governed prompt fragment in the owning Skill.
Spectre.Prompt.Materializer renders only declared scalar placeholders and
returns both a typed Spectre.Prompt.Plan and
Spectre.Prompt.Receipt. Replacement is a single pass over the original
template, so placeholder-like text inside a resolved value is never expanded
again. The materializer revalidates the canonical fragment digest, accepts
only static fragments, and owns the input namespace; context keys cannot
replace the actual normalized input used by input.* placeholders. The
receipt binds:
- the exact Definition Ref and fragment digest;
- rendered bytes and their digest, without copying prompt text;
- input evidence digest and provenance digest;
- effective placement, trust, visibility, priority, budget class, and cap; and
- generator id/version.
Spectre.Projection.Execution contains only stable program identity,
lineage, plan digests, receipt summaries, route evidence, and budgets. Input
and prompt content do not enter the projection. Prism or another selector may
choose a compatible model profile from the typed constraints, but that choice
does not grant operation or lifecycle authority.
Typed handoffs
Spectre.Execution.Handoff represents Flow → Work, Work → Work, and Work →
Flow exchanges as portable data. A handoff binds an exact Definition Ref,
stable source and target ids, closed input, parent/correlation lineage,
provenance, and a content digest.
Handoff.validate_target/2 requires the exact materialization Definition,
Work id, and input. Handoff.event/1 emits a Flow-target event; it does not
call a Flow or start a Work by itself. Admission remains a host/runtime action.
Registered state migrations
Spectre.Execution.Migration.prepare/4 resolves a migration declared by the
Program and returns a normal registered operation request. The host executes
that request through its existing boundary. Migration.commit/3 then
revalidates the operation contract, input, output schema, request lineage, and
digests before returning the migrated state and an integrity receipt. A
portable structured operation receipt is deterministically encoded before it
is digested; it cannot crash migration commit merely because it is not a
canonical plain map.
There is no callback selected by state data, and migration preparation never executes the operation.
Rehearsal without Effects
Spectre.Execution.Rehearsal.run/4 drives the real transition engine using
exact recorded operation boundaries. It never invokes the registered
executor, including for operations marked idempotent or non-idempotent.
Recordings bind the operation ref and exact input digest; missing, extra,
ambiguous, or malformed recordings fail closed.
The returned Spectre.Execution.Rehearsal.Report records privacy-safe trace,
retry/reconcile status, final-state and outcome digests, consumed recording
count, and effect_dispatches: 0. Running the same Program, input, and
recordings produces the same report digest. Canonical inputs use the same
evidence-digest domain as Execution projections and materializations;
operational structs retain a deterministic tagged fallback.
The permanent fixture at
test/fixtures/compatibility/0.2.8/data-driven-execution-v1.json pins a
portable Program and its no-Effect rehearsal identities.
Security and scope
- Runtime data cannot register code or select modules/MFAs.
- Security, executor, priority grants, authority, and lifecycle policy remain host-owned.
- Program and receipt digests are recomputed on every load boundary.
- Cost, duration, attempts, steps, retries, and repeat counts are explicit and bounded.
- Prompt and input content stay outside projections and receipts.
- Publication, Skill lifecycle, Candidate creation, and Activation remain explicit trusted-host actions.
This gate does not add generated callbacks, goal/mission hierarchies, autonomous Forge behavior, governance, or model self-activation.