The support agent in the README is deliberately minimal. These two compositions show what a useful agent looks like with one satellite package each.
Answer questions from your database (core + Kinetic)
Each query is an ordinary function. The @al attribute, doc, and typespec are
all Kinetic needs to build its planning registry — no JSON schemas, no tool
catalog in the prompt:
defmodule MyApp.Warehouse do
use SpectreKinetic
@al ~s(COUNT ORDERS WITH: ACCOUNT="acme" PERIOD="last_month")
@doc "Counts orders placed by an account in a period."
@spec count_orders(String.t(), String.t()) ::
{:ok, non_neg_integer()} | {:error, term()}
def count_orders(account, period),
do: MyApp.Reports.count_orders(account, period)
@al ~s(TOP CUSTOMERS WITH: LIMIT="5")
@doc "Lists the accounts with the most orders."
@spec top_customers(String.t()) :: {:ok, [map()]} | {:error, term()}
def top_customers(limit), do: MyApp.Reports.top_customers(limit)
end
defmodule MyApp.AnalyticsAgent do
use Spectre.Agent
use Spectre.Kinetic,
actions: MyApp.Warehouse,
modes: [count_orders: :read, top_customers: :read]
model(MyApp.LLM, purpose: :smart)
router(via: [:regex, :llm_classifier])
flow :analytics do
on :DATA_QUESTION,
regex: ~r/\b(how many|orders|customers|revenue)\b/i do
act(:data_question)
end
end
endAsk a question:
{:ok, turn} =
Spectre.turn(MyApp.AnalyticsAgent,
"how many orders did acme place last month?"
)act renders the prompt and calls the model; the model answers in compact
Action Language (COUNT ORDERS WITH: ACCOUNT="acme" PERIOD="last_month");
Kinetic maps it onto MyApp.Warehouse.count_orders/2 with validated, canonical
argument names. The planner never touches the database: the selected Action is
staged as a :read Effect, and the query runs only when the host executes the
{:needs, effect, result} decision. Add protect(...) to any function that
writes, and the same policy machinery from the support example guards it.
Read a page and answer (core + Lens)
Lens gives the agent browser perception through a Stack, with browser processes kept in an explicitly started runtime:
defmodule MyApp.AI do
use Spectre.Stack
install Spectre.Lens, planner_exposure: [:look, :discover] do
backend(SpectreLens.Browsers.Lightpanda,
instances: 1,
protocol: SpectreLens.Protocol.Lightpanda
)
end
end
defmodule MyApp.ResearchAgent do
use Spectre.Agent, stack: MyApp.AI
model(MyApp.LLM, purpose: :smart)
router(via: [:regex])
flow :research do
on :CHECK_RELEASES, regex: ~r/\brelease page\b/i do
action({:lens, :look},
args: %{
url: "https://github.com/elchemista/spectre/releases",
opts: [include: [:markdown, :links]]
},
mode: :read
)
end
end
endRun it with the browser runtime supplied at the boundary, never stored in the definition:
{:ok, _stack_runtime} =
Spectre.Stack.start_link(MyApp.AI,
name: MyApp.AIRuntime,
packages: [lens: [binary: "/opt/lightpanda"]]
)
{:ok, turn} =
Spectre.turn(MyApp.ResearchAgent, "check the release page",
stack_runtime: MyApp.AIRuntime
)The deterministic route stages the :look Action; executing it returns the
page as structured data marked trust: :untrusted, which the host converts
with agent_context/2 before any model sees it. Because the Stack declares
planner_exposure: [:look, :discover], an act route may also let the model
plan browsing — but only over those two operations, and every browser step
still passes through the same staged-Effect lifecycle.
Route through a vector index (native Router Adapter)
The index stores only stable {scope, label} refs and scores. Spectre resolves
the ref back to the compiled rule and remains the only component that can build
an authoritative Candidate:
defmodule MyApp.TicketVectorRouter do
use Spectre.Router.Adapter,
id: :ticket_vectors,
accept: 0.87,
margin: 0.04,
strength: :medium
@impl Spectre.Router.Adapter
def evaluate(%Spectre.Router.Adapter.Request{text: text, rules: rules}) do
allowed = Map.new(rules, &{&1.ref, &1})
with {:ok, hits} <- MyApp.TicketIndex.search(text, limit: 32) do
{:ok,
Enum.flat_map(hits, fn hit ->
case Map.fetch(allowed, hit.rule_ref) do
{:ok, rule} ->
[result(rule, hit.score, margin: hit.margin, matched: hit.example_id)]
:error ->
[]
end
end)}
end
end
end
defmodule MyApp.TicketAgent do
use Spectre.Agent
router(via: [:regex, MyApp.TicketVectorRouter, :llm_classifier])
flow :tickets do
on :DUPLICATE_TICKET,
ticket_vectors: [examples: ["disk full", "no space left"]],
via: [:ticket_vectors, :llm_classifier] do
reply(:duplicate_ticket)
end
end
endThe Adapter may return several distinct refs; Spectre keeps each structurally
valid result so a secondary vector hit can agree with another provider. Run
Spectre.Router.Adapter.Conformance against an isolated index fixture in CI,
and reserve live index evaluation for an opt-in corpus.