Data-driven execution

Copy Markdown View Source

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
end

The 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
end

decide 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.