# Persistent run evidence

BeamAgent can retain **sanitized** completed-run summaries and trace steps in
PostgreSQL. Storage is disabled by default. The returned `%BeamAgent.Run{}`
still contains its original in-memory trace, including execution content;
persisted records have a separate, fixed field contract and cannot resume a
run.

Add `:postgrex` to the consuming application's dependencies and supervise a
`Postgrex` connection or pool named `MyApp.BeamAgentPostgres`. Apply the SQL in
`priv/sql/001_run_evidence.sql` and `priv/sql/002_model_usage.sql` in order
to its database, then configure:

```elixir
# In the consuming application's supervisor children:
{Postgrex,
 name: MyApp.BeamAgentPostgres,
 hostname: "localhost",
 database: "my_app",
 username: "my_app"}
```

```elixir
config :beam_agent, :trace_store,
  adapter: BeamAgent.Trace.Store.Postgres,
  retention_ms: :timer.hours(24),
  options: [connection: MyApp.BeamAgentPostgres]
```

`retention_ms` is required and positive; BeamAgent has no retention default.
The application owns the database connection, credentials, access controls,
backups, and migration rollout. The adapter can use a named Postgrex process
or pool. If the store is unavailable after configuration, a run continues and
reports evidence gaps. A database connection that fails to start is governed
by the consuming application's supervision policy.

Each returned Run has `trace_storage: %{status: :complete | :incomplete,
gap_count: non_neg_integer()}` when storage is enabled, or `nil` when disabled.
An append failure increments the gap count without changing `{:ok, run}` or
`{:error, run}`. A supervised per-run writer performs database I/O outside
the runner. The API waits up to five seconds for that writer to flush at
completion, including after a runner crash or hard timeout. A stalled flush
or final-summary write failure makes the returned storage status incomplete.
If the database remains unavailable, lookup returns
`{:error, :unavailable}` rather than claiming a complete trace.

```elixir
{:ok, run} = BeamAgent.API.run("...", llm: {MyApp.LLM, []})
{:ok, %{summary: summary, trace: steps}} =
  BeamAgent.API.lookup_completed(run.run_key)

{:ok, recent_summaries} = BeamAgent.API.list_completed(50)
```

`lookup_completed/1` returns `:not_found` for an invalid, absent, uncompleted,
or expired key. `list_completed/1` returns at most 200 summaries, newest first,
and returns `{:error, :unavailable}` when storage is disabled or unreachable.
These are library APIs, not authorization checks; applications must control
who can query completed history.

The stored summary contains only `run_key`, `outcome`, `execution_status`,
`verification_status`, bounded `error_type`, `iterations`, `tool_calls`,
`finished_at_ms`, `trace_status`, `gap_count`, `input_tokens`,
`output_tokens`, `reported_model_calls`, `unreported_model_calls`,
`usage_status`, `cost_micro_usd`, `cost_status`, and `price_version`. Each
stored trace step contains only `sequence`, finite `type`, `iteration`, and
`at_ms`. Stored step
types are `model_call`, `model_finished`, `tool_requested`, `tool_started`,
`tool_completed`, `tool_failed`, `context_compressed`, `finished`, and `failed`.
No prompt, goal, answer, model response, tool name/ID/arguments/result,
verifier reason, raw exception, stacktrace, State, Run, or original trace
payload is stored. The ephemeral telemetry `run_id` and
`telemetry_span_context` references are not stored.

Records expire at their start time plus `retention_ms`. Expired records are
hidden immediately from lookup and listing. To remove them physically, schedule
`BeamAgent.Trace.Store.prune_expired/0` from the consuming application; it
deletes expired summaries and their steps and returns `{:ok, deleted_count}`.
There is no background deletion process in BeamAgent. The PostgreSQL schema
uses a unique `(run_key, sequence)` key; repeating the same append is
idempotent, and a conflicting append fails. Normal API execution never retries
storage writes. A process or node dying before a final summary can leave an
incomplete, hidden record until retention cleanup.

Model clients may return `{:reply, text, usage}` or
`{:tool_call, tool_call, usage}` alongside the existing two-element forms.
`usage` must be exactly `%{input_tokens: non_neg_integer(),
output_tokens: non_neg_integer()}` as reported by the provider. BeamAgent does
not estimate missing tokens. A malformed usage report fails the run with
`:invalid_model_usage` rather than becoming a false zero.

To calculate cost, configure a versioned price schedule in the consuming
application. The key must match the `:model` string supplied in LLM options:

```elixir
config :beam_agent, :model_pricing,
  version: 1,
  prices: %{
    "provider/model" => %{
      input_micro_usd_per_million: 500_000,
      output_micro_usd_per_million: 2_000_000
    }
  }
```

For example, `llm: {MyApp.LLM, model: "provider/model"}` selects that
price. Rates are non-negative integer **micro-USD per million tokens**.
BeamAgent sums token-rate products for the run and rounds once, half up, to
integer micro-USD. One micro-USD is $0.000001. The price schedule is read at
runner startup and kept fixed for that run; `price_version` identifies the
configured version, not a provider price catalog. Model keys and raw price
settings are not stored.

`run.model_usage` and the stored summary report input/output token totals,
reported/unreported model-call counts, `usage_status`, `cost_micro_usd`,
`cost_status`, and `price_version`. `usage_status` is `:reported` when every
model call supplied usage, `:partial` when only some did, and `:unavailable`
when none did. `cost_status` is `:known` only when all calls reported usage
and a matching price existed; otherwise it is `:unknown_usage` or
`:unknown_price`, with `cost_micro_usd: nil`. A known zero is different from
an unknown cost. Runner crashes and hard timeouts cannot recover usage held
only in that runner and therefore report usage as unavailable in their Run.
Configured retries count each returned provider attempt, including failed
attempts that report usage. Missing usage on any attempt makes total cost unknown.
