Skip to content

The run ledger

Everything assurance concludes is computed from what actually happened — and the run ledger is where “what happened” lives. It answers: what did this system actually do, and what did each completed task cost?

Brutor names execution the way the wider ecosystem does, so a number here means the same thing as the number on your orchestrator’s dashboard (the full vocabulary, with the agent loop, is in the anatomy of a run):

Unit What it is Where it comes from
Run One task, start to terminal state Gateway-signed chain, or a client x-brutor-run-id
Step A logical phase of the plan — “research”, “settle” Client-asserted only (X-Brutor-Step-Id); never inferred
Turn One pass of the agent loop — a model call plus what it triggered Asserted (X-Brutor-Turn-Id) or inferred from model-call boundaries
Action One governed call — LLM, MCP tool, skill, or A2A Always observed; this is what the gateway sees

A run is one end-to-end task, not one HTTP request. An agent that classifies a question, looks a customer up, loads a policy skill, escalates to a peer agent and composes an answer has made six calls to do one job — and that is one run.

There are two ways actions become one run, and which one applies depends on who drives the loop:

  • Gateway-delegated actions ride the HMAC-signed call chain the gateway mints and forwards (LLM → MCP → A2A → skill). Nothing to instrument: the gateway signs the linkage, and those actions record trace_continuity: verified.

  • Self-driving agents — a LangGraph/CrewAI-style loop that calls the gateway action by action from its own process — have no chain to echo, because the gateway never sees the iteration happen. Such an agent sends one header on every call of a task:

    x-brutor-run-id: <any opaque id, unique per task>

    The gateway derives the real run root from that id, keyed by the tenant secret and bound to the calling credential — so an agent can group its own work and cannot write itself into another principal’s run, and the same id from two credentials lands in two different runs. These actions record trace_continuity: client_asserted, and the run’s integrity reads client_asserted rather than intact: the coverage is complete, and the linkage rests on the client’s word instead of a signature. The ledger keeps that distinction visible instead of rounding it up.

Optionally, the last call of a task can carry X-Brutor-Run-End with one of the seven terminal states (completed, completed_degraded, exhausted, errored, blocked_policy, abandoned, cancelled) so the run closes immediately with an explicit outcome instead of waiting for the idle sweeper — a run nobody closes is recorded abandoned, which is indistinguishable from a crash, and exhausted (the agent hit its iteration cap) is only ever set from this signal because the gateway cannot observe a framework’s cap from outside. X-Brutor-Run-End: true is ignored on proxy calls; only the body of POST /v1/runs/{root}/end accepts true, meaning “ended, infer the state”.

The same call can also carry what the task came to — X-Brutor-Run-Outcome: resolved (or escalated, handed_off, abandoned_by_user) — or the client can close the run with POST /v1/runs/{root}/end and {"state": "completed", "outcome": "resolved"}. A terminal state is what the gateway derived; a client outcome is what the client claims, and the ledger keeps them as two columns with two authors. Run stats report the outcome mix beside its own coverage (client_outcome_reported_pct), so a resolved rate over three reported runs is never mistaken for a rate over three hundred.

A third source of steps needs no headers at all: a system that does not route through the gateway can export its own OpenTelemetry GenAI spans to the inlet. Those steps record trace_continuity: external and source: otel — complete as reported, never as witnessed.

Each row in ai_system_runs records:

  • outcome — a terminal state (completed, errored, abandoned, exhausted, …)
  • shape — action count, tool calls, delegation depth, the set of tools used
  • cost — tokens and dollars, attributed to the completed task rather than smeared across requests
  • actor and subject — who acted (an agent identity, an end user, an API key) and who they acted for when the call was delegated (subject_id, subject_kind), so a standing check can be written about delegated authority
  • integrity — how the actions were linked: intact (gateway-signed), client_asserted (complete, grouped by the client’s run id), observed_external (complete as reported by an OpenTelemetry exporter — nothing signed, nothing credential-bound), or partial/broken (fragments — these are excluded from baselines), plus how many times the run was reopened by a late action
  • source — gateway, otel or mixed: whether the gateway witnessed the steps or was told about them (see Evidence sources)
  • instructions and corpus — a SHA-256 fingerprint of the system instructions the run was given (never the prompt itself) and the knowledge-base collections it retrieved from, so drift can name a prompt edit or a corpus change as the cause
  • outcome feedback — an optional human rating of the answer (see below); absent on most runs, and absence reads as unknown rather than as satisfactory

Find them under AI Estate → AI Systems → (a system), which opens on Lifecycle — a status strip (health with its computed-at age, liveness, open drift, coverage) stays pinned above seven tabs: Lifecycle (the governance stage rail, its gates and replay), Signals (one sub-tab per family: Liveness, Cost, Trajectory, Errors, Outcomes), Runs (rows expand into an action-by-action timeline; Details on a row opens every field the ledger holds, the governing contract included), Trajectory (see below), Drift, Contract (Active, Operating envelope, External evidence) and Assurance (Conclusion, Evidence, Exceptions & changes, Snapshots).

The Runs tab for one system: each run with its id, start and finish, actor, honest outcome, turn and action counts, tools, cost, what closed it and its chain integrity — one run expanded into per-model attribution and the ordered model and tool actions with their latencies, and a Details button per row.

The correlation runs both ways. Whatever the client sends, every LLM response carries the run it belongs to back out:

x-brutor-run-id: <the run's root id>

This is not an echo of what was sent. The gateway derives the run root from the asserted id (keyed by tenant secret and calling credential, as above), so the value that comes back is the derived root — the id the ledger is actually keyed on. Re-asserting a returned id on the next request would derive a second, different root; assert your own task id and use the returned one for lookups.

That is what lets a client attach something to the run it just saw — a rating, a support ticket, a trace link — without the server having to guess from timing, which is wrong the moment a user has two tabs open.

Everything above is behaviour: how many actions, which tools, what it cost, whether the calls succeeded. A run can be clean on every one of those and still have answered wrongly. Nothing observed from the outside can tell the difference.

So runs also carry an optional outcome signal — a thumbs rating from the person who read the answer:

Rate a run
curl -X POST "$GATEWAY/v1/portal/runs/$RUN_ID/feedback" \
-H "X-Gateway-Authorization: Bearer $PORTAL_JWT" \
-H "X-Tenant-ID: $TENANT" \
-H "Content-Type: application/json" \
-d '{ "score": 1, "comment": "optional — what was wrong with it" }'
Response 200
{ "run_id": "01JBRUTORRUNROOT0000000000", "score": 1 }

1 up, -1 down, 0 withdraws a rating (the response then carries "score": null). run_id is the value from the response header above. A rating is accepted only from a user who actually appears in that run, and only within their own tenant — anything else returns 404.

The Brutor portal sends this from the thumbs on each answer. Anything else driving the gateway can send it too — that is what makes the signal available to your own front end rather than only to ours.

Two questions the run list cannot reach, and the tab that answers them:

  • “All tool calls that originated from step X inside run Y — and their parent turns.” Pick a phase and the view regroups to just its turns, each with the actions and tools it contained and what it cost.
  • “Where did a check trip, and what ran shortly afterwards?” Bounded by a look-ahead you choose, in execution order.

There is also a fleet-wide find pane — by capability, action type, phase name, or blocked-only — for when you know the tool but not the run.

The Trajectory tab: find actions across runs by capability, type, phase and window on the left; inspect one run on the right — its actions, its inferred turns marked as such, and per-turn model, tool, action count and cost.

A run closes when the chain ends or when it has been idle past run_idle_timeout_seconds (default 120s, configurable per system — an integration may legitimately pause for minutes between actions where an application should not).

Runs that were only partly observed are marked and excluded from baselines. Folding a fragment into “normal” would lower the apparent action count and then flag every complete run as drift.