Spectre.Stack compiles the packages and services available to an Agent into one immutable environment. It replaces implicit global extension discovery with explicit installation and logical references.

For release and umbrella integration tests, pass the complete package set to Spectre.Stack.Conformance.run/2. It invokes the same compiler invariants as an Agent Stack and catches compatibility, dependency, conflict, ordering, and ownership failures that isolated manifest checks cannot see. See Foundation Conformance.

Define a Stack

defmodule MyApp.AI do
  use Spectre.Stack

  install MyApp.Inference do
    provider :openrouter, MyApp.OpenRouter
    model :fast, id: "small-model"
  end

  install MyApp.JobActions
end

Package blocks are kept as raw AST until the package compiler receives them. provider and model in this example are local forms understood by MyApp.Inference; Stack does not import those names as macros.

Bind the resolved Stack to an Agent:

defmodule MyApp.Agent do
  use Spectre.Agent, stack: MyApp.AI
end

The compiled Agent stores the Stack module and immutable, digest-fenced installation references. For each package that declares agent_extensions, the Agent automatically registers the corresponding adapter with that package's compiled Stack configuration. A package can therefore activate its selector, memory adapter, turn handler, action provider, flow handler, or effect executor without asking the application to repeat use Package.

It does not copy runtime clients, credentials, processes, or connections into the Agent definition.

Installed Does Not Mean Authorized

Installation answers which capabilities exist in the environment. It does not make every Operation or Action visible to a model, Flow, Work, or Skill. Binding and policy remain separate:

  • package manifests declare capabilities;
  • Stack validates ownership and returns closed Spectre.Stack.Ref values;
  • package-declared Agent extensions activate the installed implementation;
  • a later Flow, Work, Skill, or policy binding selects which Refs are usable;
  • policy and the host still decide whether a staged effect may execute.

An installed package may intentionally contribute an Action provider, but that does not make every operation planner-visible or bypass protection and host authorization.

Installable Contract V1

A package implements Spectre.Stack.Installable:

defmodule MyApp.Inference do
  use Spectre.Stack.Installable,
    id: :inference,
    version: "0.3.0",
    contract: 1,
    spectre: "~> 0.3.0",
    provides: [{:service, :inference}],
    operations: [{:inference, :complete}],
    resources: [:client],
    agent_extensions: [MyApp.Inference.Extension],
    dsl: __MODULE__

  alias Spectre.Stack.DSL

  @impl Spectre.Stack.Installable
  def compile(opts, block, caller) do
    declarations = DSL.compile!(block, caller, provider: 2, model: 2)
    {:ok, %{options: opts, declarations: declarations}}
  end
end

The manifest fields are:

  • id and semantic version;
  • Stack contract version and compatible Spectre requirement;
  • provided contracts or uniquely owned services;
  • required packages, contracts, or services;
  • conflicts;
  • namespaced Operation and canonical {provider, action} refs;
  • runtime resource identifiers;
  • Agent extensions that adapt the installation into executable Agent behavior;
  • the optional package-local DSL module and portable metadata.

compile/3 is optional. Its result must be a map or keyword list containing only immutable data. PID, ports, references, and functions are rejected. Package manifests and compiled installations receive deterministic SHA-256 digests.

Satellite package tests can execute the versioned contract directly:

assert {:ok, package} =
         Spectre.Stack.Contract.V1.verify_installable(MyApp.Inference)

Dependencies And Resolution

Package dependencies use closed descriptors:

requires: [
  {:package, :inference, "~> 0.1.5"},
  {:service, :continuity},
  {:contract, :memory_store}
]

Stack validates missing and incompatible dependencies, cycles, conflicts, duplicate packages, and duplicate ownership. Declaration order does not need to be dependency order.

Resolve capabilities without obtaining their runtime handle:

{:ok, ref} =
  Spectre.Stack.resolve(MyApp.AI, :operation, {:inference, :complete})

ref.package
ref.version
ref.stack_digest

Supported Ref kinds are :package, :contract, :service, :operation, :action, and :resource.

Caller-Owned Runtime

An installable may optionally return supervised resources:

@impl Spectre.Stack.Installable
def child_specs(_installation, runtime_opts) do
  client_opts = Keyword.fetch!(runtime_opts, :client)
  [{:client, {MyApp.Client, client_opts}}]
end

Start a runtime explicitly and pass secrets only at that boundary:

{:ok, runtime} =
  Spectre.Stack.start_link(MyApp.AI,
    packages: [inference: [client: runtime_client_options()]]
  )

{:ok, ref} = Spectre.Stack.resolve(MyApp.AI, :resource, :client)
{:ok, client_pid} = Spectre.Stack.Runtime.resolve(runtime, ref)

The runtime has no default registered name. Child identifiers are fenced by the Stack, Installation, package version, and digests, so stale Refs do not resolve. Existing core application children are not yet claimed to be per-Stack; their migration belongs to the Agent Instance and runtime phases.

Journal identity

When a Journal recorder is configured, routing, runtime, persistence, and extension records include the authoritative Stack id, owner, digest, and the id/version/digest of every installation. Package configuration, credentials, PIDs, connections, and runtime handles are never copied into Journal metadata.

Legacy Adapters

Spectre.Stack.Installation.from_extension_mount/1 and from_provider_mount/1 materialize legacy mounts as explicit immutable installations. This keeps existing Extension, provider, and Kinetic integrations source-compatible while packages adopt the V1 manifest.