Events and the audit
Copy MarkdownEvery agent's events are kept, to read back, follow, or carry into your app. Everything people did through Wardrik is in the audit.
See it in action: See everything an agent did
Setup
In a fresh IEx session:
Mix.install([{:wardrik, "~> 0.1.0-rc.1"}])
{:ok, _} = Wardrik.start_link(name: MyApp.AgentRuntime)
scope = Wardrik.as(MyApp.AgentRuntime, "acme/@maria", roles: [:owner], resources: ["acme/@maria/*"])Nothing on this page calls a model, so no provider key is needed.
Read and follow
Take the cursor where the log stands, define an agent, and read what was written since:
{:ok, cursor} = Wardrik.Events.head(scope)
{:ok, _agent} =
Wardrik.Agent.define(scope, "acme/@maria/assistant",
model: "anthropic:claude-haiku-4-5",
instructions: "You keep Maria's notes."
)
Wardrik.Events.list(scope, after: cursor, limit: 2)
#=> {:ok,
#=> %Wardrik.Page{
#=> events: [
#=> %Wardrik.Event{
#=> type: "wardrik.agent.started",
#=> subject: "agents/acme/@maria/assistant",
#=> time: ~U[2026-10-23 09:00:01Z],
#=> data: %{"agent" => %{"id" => "acme/@maria/assistant", ...}, ...},
#=> ...
#=> }
#=> ],
#=> next: "eyJhY21lL0BtYXJpYS9hc3Npc3RhbnQiOjF9"
#=> }}Pass next as after: to read on. list/2 reads from the start without after:.
stream_list/2 reads after the cursor, then keeps following. Without after:, it starts with
what's written next:
{:ok, stream} = Wardrik.Events.stream_list(scope, after: cursor, resources: ["acme/@maria/*"])
Enum.take(stream, 1)
#=> [{%Wardrik.Event{type: "wardrik.agent.started", ...}, "eyJhY21lL0BtYXJpYS9hc3Npc3RhbnQiOjF9"}]The stream doesn't end. Each item is an event and the cursor past it.
- Each event is a CloudEvent, with
datashaped as the protocol describes its type. - A scope only sees the events of agents it can see. A cursor only names those agents.
- Where a log was trimmed past the cursor, a
wardrik.events.droppedevent says so. Reading goes on from the oldest event kept.
Follow them into your app
A listener hands each event to your module with the cursor past it. Save the cursor, and the listener carries on from it after a restart:
defmodule MyApp.Started do
use Wardrik.Listener, wardrik: MyApp.AgentRuntime, types: ["wardrik.agent.started"]
@impl true
def handle(%Wardrik.Event{} = event, listener) do
IO.puts("Started #{event.subject}")
:persistent_term.put(__MODULE__, listener.cursor)
:ok
end
@impl true
def cursor(_listener), do: :persistent_term.get(__MODULE__, nil)
end
{:ok, _} = Supervisor.start_link([MyApp.Started], strategy: :one_for_one)
Wardrik.Agent.define(scope, "acme/@maria/digest",
model: "anthropic:claude-haiku-4-5",
instructions: "You write Maria's daily digest."
)
# Started agents/acme/@maria/digestIn your app, save the cursor where you save the work, in the same transaction, as with
MyApp.Repo.transaction/1. Have Wardrik start the listener, so it starts after what it follows:
config :my_app, MyApp.AgentRuntime, listeners: [MyApp.Started]Or add it to your own supervision tree, after MyApp.AgentRuntime.
Each event is handed over at least once. Drop repeats by the event's id. If handle/2 raises or
returns anything but :ok, the listener restarts from cursor/1.
Options
:wardrik- required. The module that uses Wardrik, asMyApp.AgentRuntime.:resources- agents to follow. Every agent unless set.:types- event types to follow. Every type unless set.:after- the cursor to start after, unless you definecursor/1. Without one, it reads what's written once it starts.
The audit
Everything people did through Wardrik, for the whole deployment. Reading it needs a grant on
deployment:
operator = Wardrik.grant(scope, roles: [:owner], resources: ["deployment"])
Wardrik.Audit.list(operator, limit: 50)
#=> {:ok,
#=> %Wardrik.Page{
#=> events: [
#=> %Wardrik.Event{type: "wardrik.agent.defined", data: %{"actor" => %{"id" => "acme/@maria"}, ...}, ...},
#=> ...
#=> ],
#=> next: 2
#=> }}Pass next as starting_after: to read the following page.