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/2declares conversation intents and handlers.router/1declares which evidence providers produce candidates.policy/2declares approval/rejection gates for dangerous actions.protect/2attaches an action name to a policy independently from the prompt or handler that produced the action.actions/2andaction_provider/3keep side effects behind registered providers.action_planner/2accepts provider-neutral plans without coupling the runtime to a planning library.state/1andmemory/1keep 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: :downcaseend 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_pendingend
flow :project_create do
on :wants_project_create, regex: ~r/crea.*progetto/i do ask :project_create endend 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
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
Calls the configured model and permits the closed action planner.
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
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.
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
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
Block-form variant for actions/2.
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
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
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
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.
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
Configures the canonical checkpoint adapter used by Agent Instances.
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"
Configures the embedding adapter used by embedding-based router strategies.
embedding MyApp.Embeddings, model: "text-embedding-3-small"
Configures the prompt used by Spectre.Monitor failure fallback text.
fail :agent_failure_reply
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
endFlows 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
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.
Configures how many completed turns are stored in chat history.
history 50With 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}
Configures idle timeout for a supervised session.
idle :timer.minutes(5)
Adds a typed prompt fragment to the current Agent or Skill scope.
inject :company_identity, into: :instructions, position: :start
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
endYou can also pass an already-built plug spec list:
input_pipeline [
{Spectre.Input.Plugs.NormalizeText, trim?: true}
]
Declares a global route using the compact keyword do: form.
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
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: falsejournal(false) explicitly disables an application-level default for this
agent.
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
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"
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
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
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
Calls the configured model without permitting action planning.
Creates a deterministic reply handler without calling the LLM.
on :healthcheck, regex: ~r/^ping$/i do
reply :pong
end
Declares a logical action that must be bound when a reusable Skill is mounted.
requires_action :search, mode: :read
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
Alias for requires_action/2 using tool-oriented terminology.
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]
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]
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
Configures the maximum lifetime for a supervised session.
shutdown :timer.minutes(30)
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]
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
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
Replaces the complete turn-handler pipeline or disables it with false.
turn_handlers [
MyApp.FirstIntegration,
{MyApp.SecondIntegration, namespace: :support}
]
turn_handlers false
Starts a separate precise Work owned by the current Agent Instance.