Middleware
Copy MarkdownSetup
Paste this into a fresh IEx session. The model calls need an Anthropic key. The agent gets the order tool from Tools:
Mix.install([{:wardrik_core, "~> 0.1.0-rc.1"}])
System.put_env("ANTHROPIC_API_KEY", "sk-ant-...")
children = [
{WardrikCore.Backends.MemoryStore, name: :agents},
{WardrikCore, backend: {WardrikCore.Backends.MemoryStore, name: :agents}}
]
{:ok, _} = Supervisor.start_link(children, strategy: :rest_for_one)
defmodule MyApp.Orders do
@behaviour WardrikCore.Capability
@impl true
def tools(_context, orders) do
[
WardrikCore.Tool.new!(
name: "order_status",
description: "Say where an order is.",
schema: [order_id: [type: :string, required: true]],
run: fn %{order_id: id}, _context -> {:ok, Map.get(orders, id, "unknown")} end
)
]
end
endReplace "sk-ant-..." with your key.
Wrapping every call
Middleware wraps each turn, model call and tool call. It reads the step from context.step, then
calls next to carry on or answers the step itself. This one refuses the shell:
defmodule MyApp.NoShell do
@behaviour WardrikCore.Middleware
@impl true
def call(%{step: {:tool_call, %{name: "shell"}}} = context, _next, _opts),
do: {:ok, %{context | response: {:error, "No shell."}}}
def call(context, next, _opts), do: next.(context)
endThe refusal becomes the call's answer. The model sees it as an error, and carries on.
Use middleware when every call needs the same treatment: policy, tracing, retries, a system prompt.
Like capabilities, it takes options as {MyApp.NoShell, opts}.
Waiting for a person
Middleware can park a call until a person answers. This one asks before any tool runs:
defmodule MyApp.Approve do
@behaviour WardrikCore.Middleware
@impl true
def call(%{step: {:tool_call, call}} = context, next, _opts) do
case answer(context, call) do
:approve -> next.(context)
{:deny, reason} -> {:ok, %{context | response: {:error, reason}}}
nil -> {:park, :approval, context}
end
end
def call(context, next, _opts), do: next.(context)
defp answer(%{resume: %{input: answers}}, call), do: Map.get(answers, call.id)
defp answer(_context, _call), do: nil
end
{:ok, agent} =
WardrikCore.start_agent(
agent_id: "support",
model: "anthropic:claude-haiku-4-5",
capabilities: [{MyApp.Orders, %{"1001" => "shipped"}}],
middleware: [MyApp.NoShell, MyApp.Approve]
)
:ok = WardrikCore.ask(agent, "Where is order 1001?")The first middleware in the list runs outermost. The turn waits until you resume it with an answer for each call:
{:tool_calls, [{call, {:park, :approval}}]} = WardrikCore.parked(agent)
#=> {:tool_calls, [{%WardrikCore.ToolCall{name: "order_status", ...}, {:park, :approval}}]}
:ok = WardrikCore.resume(agent, %{call.id => :approve}, %{by: "user:maria"})The metadata, here who answered, appears on the resumed turn's
[:wardrik_core, :turn, :start] event.
Private state
Middleware can keep state with the agent, such as a count or what it spent. Store it with
WardrikCore.Context.put_private/3, under your own module as the key. It's saved with the agent,
so it survives restarts.
Asking a model
When middleware needs a model's judgement on a call, WardrikCore.evaluate/4 asks an evaluation
model. WardrikCore.generate/1 calls the agent's own model.