keel
Harness

Tracing and inspection

Explain a turn using ordered events, child records, and its final outcome.

01gateadmit → pass
02toollookup → refused
03defecttool-input-invalid
04toolcorrected lookup → result
05outcomecompleted
Read events in sequence, then confirm the final outcome. A recorded defect may have been recovered.

The final answer rarely explains why a tool was refused or a fallback ran. Keel records those decisions as events, with a turnId that ties them to the turn that made them. Start from the outcome, then trace the decisions that produced it.

Follow the evidence

Event kindWhat it records
gateA gate's verdict and supporting details.
assemble, renderContext sources, reader output, estimates, and clipping.
reduceA state update landing on a block — event, version, and the value after.
toolCalls, results, progress, refused input, filtering, caching, and execution errors.
chainModel attempts, retries, fallback, and exhaustion.
defect, warnA failure condition or warning encountered during execution.
sayA part emitted on that turn's wire.
outcomeThe final outcome and completed turn record.

Events have an ordered seq, a timestamp at from the runtime's injectable clock, and a wall-clock timestamp wall. The bus retains the last busLimit events (default 10 000, set in RuntimeConfig); older events roll off, so a long session's export is a window, not an archive.

Inspect events in code

const unsubscribe = runtime.bus.subscribe((event) => {
  if (event.kind === "defect") {
    console.log(event.turnId, event.defect.code);
  }
});

try {
  await runtime.runTurn({
    agent: assistant,
    ambient,
    envelope,
    budget: { steps: 8 },
  });
} finally {
  unsubscribe();
}

const records = await runtime.store.records();
const traceJson = runtime.bus.export();

Store reads are async — records() and listMessages() return promises, so await them. Keep subscribers lightweight: a bus subscriber that throws is contained (a console.warn, and the turn continues), because observability must never take the turn down. The onEvent hook is held to a higher standard — a throwing hook surfaces as a hook-failure defect on the bus, though the turn still continues. The bus export contains the session's retained events, not just the latest turn; filter runtime.events by turnId when inspecting a single invocation.

Read recovered failures correctly

A tool-input-invalid defect followed by a corrected call can belong to a completed turn. Conversely, text emitted before a timeout can belong to a cut turn. Even a gate can speak twice on one turn: the close gate logs refuse when a required deliverable is missing, then pass after a nudge round recovers it. Neither a defect alone nor visible output alone determines the outcome.

Read the final record's outcome and outcomeDetail, then inspect the earlier events for the cause. Billing fields record Keel's decision; they do not execute a refund through an external payment provider.

Follow delegation without mixing wires

Each child has its own turn ID and a record whose parentTurnId points to its caller. Follow that relationship to reconstruct nested work. Child events remain observable even when forwarding is disabled.

The user's output stream comes from the root's wire hook. A session-wide runtime.events list contains child events too; do not treat that entire list as output the root user received. Model usage is recorded per turn; children have separate records rather than being included automatically in the parent's token total.

Inspect in Studio

Select the failing turn in TURNS first. Use CONTEXT for input size and tool visibility, MODEL for calls and fallback, STATE for reads and updates, and WIRE for that turn's emitted parts.

For an unexpected result, reproduce the run with a fake tier behind the public ModelAdapter interface, find the first relevant refusal or defect, and verify the final outcome. Export the session when sharing the reproduction, and keep the reproduction as a behavior test so the fix stays fixed.

Next: Studio and Testing.