Spectre.Agent (Spectre v0.3.0)

Copy Markdown View Source

Declarative DSL for building Spectre agents.

The DSL is intentionally a control plane, not a place to hide application logic. It describes the agent boundary: how input is normalized, how routes are selected, which prompts/actions run, and which actions must pass a policy gate before side effects can execute. The actual domain work should stay in ordinary Elixir modules and be called through run/2, action/2, adapters, or lifecycle hooks.

This split keeps complex agents understandable:

  • flow/2 declares conversation intents and handlers.

  • router/1 declares which evidence providers produce candidates.

  • policy/2 declares approval/rejection gates for dangerous actions.

  • protect/2 attaches an action name to a policy independently from the prompt or handler that produced the action.

  • actions/2 and action_provider/3 keep side effects behind registered providers.

  • action_planner/2 accepts provider-neutral plans without coupling the runtime to a planning library.

  • state/1 and memory/1 keep persistence at explicit runtime boundaries.

    defmodule MyApp.ProjectAgent do use Spectre.Agent, prompt_root: "priv/agents/project/prompts"

    model MyApp.LLM actions MyApp.ProjectActions state MyApp.AgentStateStore memory MyApp.AgentMemory input_pipeline do

    plug Spectre.Input.Plugs.NormalizeText, case: :downcase

    end shutdown 600_000 fail :agent_failure_reply

    router via: [:regex, :semantic_cache, :classifier, :llm_classifier]

    protect :create_project, with: :terms

    policy :terms do

    request :accept_terms
    accept :accepted_terms, regex: ~r/^accetto$/i
    reject :rejected_terms, regex: ~r/^no$/i
    otherwise ask: :accept_terms_retry
    attempts 3, then: :cancel_pending

    end

    flow :project_create do

    on :wants_project_create, regex: ~r/crea.*progetto/i do
      ask :project_create
    end

    end end

In a larger agent, keep this module as the readable map of the system and move business decisions into named functions or modules:

defmodule MyApp.BillingAgent do
  use Spectre.Agent

  actions MyApp.BillingActions
  router via: [:regex, :classifier, :embedding, :llm_classifier]

  protect :issue_refund, with: :refund_confirmation

  flow :billing do
    on :refund_request do
      run :prepare_refund_case
    end

    on :confirm_refund, regex: ~r/^refund now$/i do
      action :issue_refund
    end
  end

  def prepare_refund_case(input, ctx) do
    MyApp.Billing.PrepareRefund.call(input, ctx)
  end
end

Summary

Functions

Imports the DSL and initializes compile-time metadata for an agent module.

Calls the configured model and permits the closed action planner.

Creates a deterministic action handler.

Configures the provider-neutral action planner port.

Registers an action provider under a stable identifier.

Configures the action adapter and optional action-level protections/hooks.

Block-form variant for actions/2.

Registers an action lifecycle hook.

Replaces the default evidence arbitrator.

Creates a handler that renders a prompt, calls the LLM, and lets the configured planner stage provider-neutral actions.

Registers a pre-execution guard for an action effect.

Creates a declarative handler that requests a registered Agent operation.

Configures the canonical checkpoint adapter used by Agent Instances.

Configures classifier adapters.

Configures the embedding adapter used by embedding-based router strategies.

Configures the prompt used by Spectre.Monitor failure fallback text.

Declares route rules that belong to a conversation flow.

Declares a flow with extension-owned namespaced options.

Configures how many completed turns are stored in chat history.

Configures idle timeout for a supervised session.

Adds a typed prompt fragment to the current Agent or Skill scope.

Declares an input normalization pipeline using plug syntax.

Declares a global route using the compact keyword do: form.

Declares a global route that is checked before normal flow rules.

Configures a structured agent journal store.

Configures a memory adapter used to recall and remember conversation context.

Configures the LLM adapter used by ask/2.

Registers an application operation for Work, Vigil and external controllers.

Declares a policy gate for a pending action effect.

Attaches an action to a policy gate.

Calls the configured model without permitting action planning.

Creates a deterministic reply handler without calling the LLM.

Declares a logical action that must be bound when a reusable Skill is mounted.

Declares an immutable Agent operation required by a reusable Skill.

Alias for requires_action/2 using tool-oriented terminology.

Configures which committed operational events re-enter the normal Flow router.

Configures router behavior for the agent.

Creates a handler that calls an agent-local function.

Configures the maximum lifetime for a supervised session.

Mounts a reusable Spectre.Skill inside an Agent.

Configures a state adapter used to load and persist conversation state.

Appends an optional handler to the pre-route turn pipeline.

Replaces the complete turn-handler pipeline or disables it with false.

Starts a separate precise Work owned by the current Agent Instance.

Functions

__using__(opts)

(macro)

Imports the DSL and initializes compile-time metadata for an agent module.

Options are stored as runtime configuration and are later exposed through generated __spectre_*__ functions. This is why the DSL can stay declarative: all route and policy data is compiled once, then interpreted by the runtime.

defmodule MyApp.SupportAgent do
  use Spectre.Agent,
    prompt_root: "priv/agents/support/prompts",
    history: 20
end

act(prompt, opts \\ [])

(macro)

Calls the configured model and permits the closed action planner.

action(action, opts \\ [])

(macro)

Creates a deterministic action handler.

If the action is protected, Spectre stores it as pending and asks the policy prompt. If it is not protected, the action is staged for execution by the host boundary.

on :delete_account, regex: ~r/^delete my account$/i do
  action :delete_account
end

action_planner(module, opts \\ [])

(macro)

Configures the provider-neutral action planner port.

Optional libraries normally mount this through their own DSL, for example use Spectre.Kinetic. The explicit form is useful for application-specific planners.

action_provider(id, module, opts \\ [])

(macro)

Registers an action provider under a stable identifier.

This is the low-level port used by optional Spectre libraries. Applications using an ordinary Elixir module can keep the shorter actions/2 DSL.

action_provider :browser, MyApp.BrowserProvider
action_provider {:mcp, :github}, MyApp.GitHubProvider

actions(module, opts \\ [])

(macro)

Configures the action adapter and optional action-level protections/hooks.

Use the block form when the action module should be declared next to its lifecycle policy. This keeps side-effect boundaries visible in the agent file while the actual implementation remains in the action module.

actions MyApp.ProjectActions do
  protect :delete_project, with: :confirm_delete
  after_action :delete_project, on: :delivered, run: :audit_delete
end

actions(module, opts, list)

(macro)

Block-form variant for actions/2.

after_action(action, opts)

(macro)

Registers an action lifecycle hook.

Hooks run after the action execution result is available, which makes them a good place for audit trails, delivery acknowledgements, and integration events that should not affect route selection.

after_action :delete_account, on: :delivered, run: :audit_delete_account

arbitrator(module, opts \\ [])

(macro)

Replaces the default evidence arbitrator.

The arbitrator receives all candidate routes from the pipeline and decides whether to accept one, ask the LLM classifier, clarify, or fail.

arbitrator MyApp.Router.Arbitrator, conflict: :llm

ask(prompt, opts \\ [])

(macro)

Creates a handler that renders a prompt, calls the LLM, and lets the configured planner stage provider-neutral actions.

on :support_question do
  ask :support_answer
end

before_action(action, opts)

(macro)

Registers a pre-execution guard for an action effect.

Guards run right before the capability is invoked, after routing, planning, and any policy approval. A guard returning :allow lets execution proceed; {:suppress, reply_text} cancels the pending effect without invoking the capability and returns a normal reply result carrying that text. Use guards for host-state vetoes that no route or policy can see, such as "this user already has an open draft".

before_action :create_project, run: {MyApp.Guards, :no_duplicate_draft}

The guard receives (action, ctx) — also accepted as arity 1 (action) or a local agent function via an atom. :all matches every action.

call_operation(operation, opts \\ [])

(macro)

Creates a declarative handler that requests a registered Agent operation.

The handler records only the operation identifier and portable input policy; execution remains an explicit host boundary.

on :lookup, check: {:text, "lookup"} do
  call_operation :lookup, input: :text
end

checkpoint_store(module, opts \\ [])

(macro)

Configures the canonical checkpoint adapter used by Agent Instances.

classifier(adapter, opts \\ [])

(macro)

Configures classifier adapters.

The first argument is the LLM adapter used only by :llm_classifier arbitration. local: configures the local classifier adapter used by the :classifier router strategy.

classifier MyApp.SmallLLM,
  model: "small",
  prompt: &MyApp.ClassifierPrompt.build/1,
  llm_opts: [temperature: 0.0, max_tokens: 8],
  local: MyApp.LocalClassifier,
  artifact_dir: "priv/spectre/support"

embedding(module, opts \\ [])

(macro)

Configures the embedding adapter used by embedding-based router strategies.

embedding MyApp.Embeddings, model: "text-embedding-3-small"

fail(prompt, opts \\ [])

(macro)

Configures the prompt used by Spectre.Monitor failure fallback text.

fail :agent_failure_reply

flow(name, list)

(macro)

Declares route rules that belong to a conversation flow.

Flow names are stored on routes and can be used by stateful applications to prioritize current-flow rules before general fallback rules.

flow :project_create do
  on :wants_project_create,
    regex: ~r/create.*project/i do
    ask :project_create
  end
end

Flows nest. A nested flow is a taxonomy grouping: each rule keeps the full path in flow_path while flow stays the innermost name. inject declarations and flow options are inherited by nested flows.

flow :checkout do
  on :PAY_CARD, embedding: ["pay by card"] do
    act :pay_card
  end

  flow :shipping do
    on :TRACK_PARCEL, embedding: ["where is my parcel?"] do
      reason :track_parcel
    end
  end
end

flow(name, opts, list)

(macro)

Declares a flow with extension-owned namespaced options.

Mounted extensions consume their own options during the Agent's single compile pass. Unknown or unconsumed options fail compilation. Nested flows inherit the options of their ancestors; their own options win on conflict.

history(limit, opts \\ [])

(macro)

Configures how many completed turns are stored in chat history.

history 50

With summary:, turns evicted from the window are folded into a rolling summary kept under state.data.chat_summary instead of being dropped. The summarizer receives (current_summary_or_nil, evicted_entries) and returns the new summary string; on error the previous summary is kept and the entries are dropped as before.

history 50, summary: {MyApp.Chat, :compact}

idle(timeout)

(macro)

Configures idle timeout for a supervised session.

idle :timer.minutes(5)

inject(id, opts \\ [])

(macro)

Adds a typed prompt fragment to the current Agent or Skill scope.

inject :company_identity, into: :instructions, position: :start

input_pipeline(specs)

(macro)

Declares an input normalization pipeline using plug syntax.

Input plugs run before state, routing, and policy handling. This makes downstream decisions work with one normalized internal shape rather than each router plug parsing raw host input differently.

input_pipeline do
  plug Spectre.Input.Plugs.NormalizeText, trim?: true, case: :downcase
end

You can also pass an already-built plug spec list:

input_pipeline [
  {Spectre.Input.Plugs.NormalizeText, trim?: true}
]

interrupt(label, opts)

(macro)

Declares a global route using the compact keyword do: form.

interrupt(label, opts, list)

(macro)

Declares a global route that is checked before normal flow rules.

Interrupts are useful for cancel, help, handoff, and other commands that should work regardless of the current flow.

interrupt :cancel, regex: ~r/^cancel$/i do
  run :cancel_current
end

journal(store, opts \\ [])

(macro)

Configures a structured agent journal store.

Journaling is opt-in and excludes input/reply content by default. The monitoring default is asynchronous warning mode; use synchronous error mode only when a failed append must fail the turn.

journal MyApp.SpectreJournal,
  events: [:routing, :arbitration],
  mode: :async,
  on_error: :warn,
  include_input: false

journal(false) explicitly disables an application-level default for this agent.

memory(module)

(macro)

Configures a memory adapter used to recall and remember conversation context.

Memory is intentionally separate from state: state is the authoritative machine state for routing and policies, while memory is contextual material that prompts or adapters may use.

memory MyApp.AgentMemory

model(adapter, opts \\ [])

(macro)

Configures the LLM adapter used by ask/2.

By default Spectre calls complete(prompt, opts) on the adapter. Use with: or function: when the adapter exposes a different function name.

model MyApp.OpenAIAdapter, with: :complete_chat, model: "gpt-4.1-mini"

operation(id, executor, opts \\ [])

(macro)

Registers an application operation for Work, Vigil and external controllers.

The executor is a stable module or {module, function} reference. Runtime inputs and outputs are validated against this immutable registry entry; models and planners cannot inject executable modules or arbitrary MFAs.

operation :read_logs, {MyApp.Operations, :read_logs},
  input: :map,
  output: :map,
  side_effect: :none,
  timeout: 15_000

policy(name, list)

(macro)

Declares a policy gate for a pending action effect.

A policy is a small deterministic router used only while an action effect is waiting for approval. It bypasses normal routing so a confirmation such as "yes" is interpreted as a policy response instead of a generic user intent.

policy :delete_account_confirmation do
  request :confirm_delete_account
  accept :delete_confirmed, regex: ~r/^yes, delete$/i
  reject :delete_rejected, regex: ~r/^no$/i
  otherwise ask: :confirm_delete_account_retry
  attempts 3, then: :cancel_pending
end

protect(action_or_opts, opts \\ [])

(macro)

Attaches an action to a policy gate.

Protection is action-centric rather than prompt-centric on purpose: the same dangerous action can be produced by DSL handlers or by an optional planner inspecting an LLM reply, and it must still pass the same policy.

protect :delete_account, with: :delete_account_confirmation

reason(prompt, opts \\ [])

(macro)

Calls the configured model without permitting action planning.

reply(prompt, opts \\ [])

(macro)

Creates a deterministic reply handler without calling the LLM.

on :healthcheck, regex: ~r/^ping$/i do
  reply :pong
end

requires_action(name, opts \\ [])

(macro)

Declares a logical action that must be bound when a reusable Skill is mounted.

requires_action :search, mode: :read

requires_operation(name, opts \\ [])

(macro)

Declares an immutable Agent operation required by a reusable Skill.

Operations are resolved from the host Agent registry. A Skill can reference their stable identifiers, but cannot register executors or inject executable callbacks.

requires_operation :read_logs

requires_tool(name, opts \\ [])

(macro)

Alias for requires_action/2 using tool-oriented terminology.

route_operation_events(types \\ :all)

(macro)

Configures which committed operational events re-enter the normal Flow router.

Use :all, a list of event types, or false. Events are converted to Spectre.Input values and still need to match ordinary on rules; this does not install a second event matcher.

route_operation_events [:completed, :blocked, :observation_significant]

router(opts)

(macro)

Configures router behavior for the agent.

via: is the common path: it expands into router plugs and then appends the arbitration and terminalization steps. Use pipeline: only when the agent needs a fully custom router pipeline.

router via: [:regex, :semantic_cache, :classifier, :embedding, :llm_classifier]

run(function, opts \\ [])

(macro)

Creates a handler that calls an agent-local function.

Use run/2 when the next step is normal Elixir orchestration rather than an LLM prompt or a protected action boundary.

on :refund_request do
  run :prepare_refund_case
end

shutdown(timeout)

(macro)

Configures the maximum lifetime for a supervised session.

shutdown :timer.minutes(30)

skill(module, opts \\ [])

(macro)

Mounts a reusable Spectre.Skill inside an Agent.

as: assigns the local scope identifier. bind: maps logical Skill action requirements to concrete actions owned by the Agent.

skill MyApp.Skills.Research,
  as: :research,
  bind: [search: :web_search, publish: :publish_report]

state(module)

(macro)

Configures a state adapter used to load and persist conversation state.

State adapters keep storage outside the domain runtime. They may implement load/3 and persist/4 for agent-aware calls, or the smaller load/2 and persist/2 callbacks for simpler applications.

state MyApp.AgentStateStore

turn_handler(handler, opts \\ [])

(macro)

Appends an optional handler to the pre-route turn pipeline.

Handlers run in declaration order after an already-open Spectre policy and before ordinary routing. They return :cont to preserve the pipeline or a typed reply to own the turn. This is a dependency-free integration point for external runtimes; memory, actions, prompts, input, and telemetry retain their narrower dedicated boundaries.

turn_handler MyApp.ActiveWorkflow, namespace: :support

turn_handlers(handlers)

(macro)

Replaces the complete turn-handler pipeline or disables it with false.

turn_handlers [
  MyApp.FirstIntegration,
  {MyApp.SecondIntegration, namespace: :support}
]

turn_handlers false

work(controller, opts \\ [])

(macro)

Starts a separate precise Work owned by the current Agent Instance.