# `Raxol.Agent.Contract`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.8.0/lib/raxol/agent/contract.ex#L1)

The typed event contract between the agent core and any surface (CLI, TUI,
LiveView, remote): the event envelope (`Raxol.Agent.Contract.Event`), its
JSON line encoder, and the decoder for the evidence marker a closing
`turn_completed` carries.

Every observable step of an agent run is one `Event`. This module defines
the shape and encodes it; it neither produces nor publishes events. The
package README ("The event contract") describes the shape.

## v0 vocabulary (family `:loop` only)

  * `:turn_started`   — a prompt was accepted; payload `%{prompt}`
  * `:item_started`   — an item opened; payload `%{item_id, item_type}`
  * `:item_delta`     — streaming text chunk; payload `%{item_id, chunk}`;
    the one **ephemeral** event (live render only, never for replay)
  * `:item_completed` — a finished item; payload
    `%{item_id, item_type: :message | :reasoning | :tool_use | :tool_result, ...}`.
    `item_id`s are turn-scoped (`"i1"`, `"i2"`, …) and distinct per item.
  * `:turn_completed` — a turn boundary; payload
    `%{usage, iteration, final}` (`final: true` closes the run)
  * `:error`          — fault; payload `%{reason}`

The contract only grows: a field or a type is added, never removed, renamed
or made required.

## Evidence tri-state wire marker

The payload of a `turn_completed{final: true}` may carry one optional
discriminator plus one optional detail:

  * `evidence: :accepted | :rejected | :absent` — grow-only enum (atom
    in-memory, JSON string on the wire, same convention as every other
    enum on this surface).
    - `:accepted` — stamped alongside `refs`, the accepted-refs carrier.
    - `:absent` — the turn offered no refs at all.
    - `:rejected` — refs were offered and the producer's done gate refused
      them.
  * `evidence_rejected` (present only when `evidence: :rejected`) —
    `%{"refs" => offered_refs, "reason" => reason_name, "ref" =>
    offset_or_nil}`. `reason_name` is one of `"missing_ref"`,
    `"not_evidence"`, `"foreign_turn"`, `"stale_evidence"`,
    `"mutation_echo"`.

A record without the marker cannot tell a rejected done from a never-offered
one: both are `%{usage, final: true}` with no `refs`. `evidence_status/1`
therefore decodes the field with a grandfather rule: an `evidence` key
present is authoritative; a **legacy** record (key absent) with `refs`
present grandfathers to `:accepted` (the `refs` carrier never lied); a
legacy record with neither key present is genuinely `:unknown` — it must
NEVER be guessed as `:absent`, which would launder a historical rejection
into a false "never offered" claim.

The marker is in journals written at `schema_version` 1.1.0 and later
(`Raxol.Agent.Journal.FileStore.Writer` stamps the version). The `v1.0.0`
golden corpus has none: an old journal's records are never rewritten in
place, only decoded through the grandfather rule above.

## Trust boundary — producer-stamped, journal-trusted

`evidence_status/1` is a **decode**, not a **re-derivation**. It reports the
enum the producer stamped; it does NOT re-run the producer's done gate over
the journal at read time. The soundness of the marker therefore rests on two
explicit assumptions, stated here so a reader wiring it to a surface knows
exactly what it does and does not defend against:

  1. **Producer honesty is stamped, not asserted.** The producer writes the
     marker from the same gate verdict that decides the done, so producer
     and decider cannot disagree at write time. This module does not check
     that.

  2. **The reader trusts the journal.** A hand-forged or replayed line that
     stamps `evidence: :accepted` — or a legacy-shaped forged line that omits
     the marker but carries a `refs` list, which the grandfather arm blesses
     `:accepted` — will decode as proven-done. This is the **identical**
     threat model as `refs` themselves and every other durable field: a
     journal that can be forged can fabricate tool results, refs, and verdicts
     directly. The marker adds no trust surface the journal did not already
     assume (append-only, single-writer integrity). It is sound only on a
     trusted, append-only journal.

What the decoder *does* defend: an `evidence` value it cannot understand —
a forged string, a future enum value, a garbage type — fails safe to
`:unknown`, never `:accepted` (see `normalize_evidence/1`). A forged
non-legacy line thus cannot launder a bogus marker into acceptance.

**The decoder does not re-derive the verdict.** Re-running the gate at
decode time would (a) require the whole journal + turn + refs, coupling a
pure single-record decode to journal state it does not carry, and (b)
re-evaluate at the display surface the *open* gate predicates (staleness
relative to *later* mutations), which must NOT be frozen or recomputed
there. A hostile-journal re-check is out of scope for a producer-stamped
durable marker.

# `encode_line`

```elixir
@spec encode_line(Raxol.Agent.Contract.Event.t()) :: iodata()
```

Encode an event as a JSON line (newline-terminated). There is no decoder:
v0 is emit-only.

# `evidence_status`

```elixir
@spec evidence_status(map()) :: :accepted | :rejected | :absent | :unknown
```

Decode a `turn_completed{final: true}` payload's evidence status.

Tolerant of both atom- and string-keyed payloads and both atom- and
string-valued enums (live in-memory events vs. journal-replayed JSON).

Implements the grandfather rule for records written before this marker
existed (see moduledoc): an `evidence` key present is authoritative and
wins outright. A legacy record (key absent) with `refs` present
grandfathers to `:accepted` — the `refs` carrier never lied about
acceptance. A legacy record with **neither** key present is genuinely
`:unknown`: without the marker, a rejected done and a never-offered done
are wire-identical, so there is no way to tell them apart. This case must
NEVER decode as `:absent` — that would launder a
historical rejection into a false "never offered" claim.

Totality: the decoder never crashes. An `evidence` value it does not
recognize — a future grow-only enum value, a forged string, a garbage type —
degrades to `:unknown` (fail-safe, never `:accepted`); see
`normalize_evidence/1`. The decode is quaternary — `:accepted | :rejected |
:absent | :unknown` — even though the wire enum a producer stamps is
tri-state; `:unknown` is the decode-only bucket for legacy/unrecognized
records, so a consumer's `case` over this must handle four outcomes.

Trust: this reports the stamped enum, it does not re-derive the verdict from
the journal — see the "Trust boundary" section in the moduledoc for what that
does and does not defend.

# `sanitize_payload`

```elixir
@spec sanitize_payload(map()) :: map()
```

Coerce a payload map into a JSON-encodable one.

Same boundary sanitization `encode_line/1` applies, exposed for the durable
journal sink: an event whose payload carries non-encodable terms (message
tuples, structs, pids) must not crash `Jason.encode!` when appended. Anything
Jason can't take becomes `inspect/1` text.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
