Events and the audit

Copy Markdown

Every 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 data shaped 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.dropped event 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/digest

In 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, as MyApp.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 define cursor/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.