ADR-0022: Run Step Provenance — Model, Agent, and Provider Disclosure¶
Status: Proposed Date: 2026-05-21 Extends: ADR-0009 (SQLite state layer), ADR-0012 (execution lineage), ADR-0017 (session lifecycle)
Context¶
Individual run steps don't disclose what model, provider, or agent definition they used. The data is partially available but not persisted or surfaced:
What's missing¶
Sessions have agent_name (TEXT) — the agent profile name from -a reviewer. But:
- No
modelcolumn. The resolved model spec (claude/claude-sonnet-4-6,openai/gpt-4.1) is not stored. - No
providercolumn. Whether the run used Claude Code, Codex, OpenAI API, or Anthropic API is not recorded. - No
agent_definition_hash. The agent profile (~/.lionagi/agents/reviewer.md) can change between runs — there's no snapshot of which version was used. agent_nameis only set forli agent -aruns. Flow ops and play agents getNULL.
Branches have node_metadata JSON which can hold chat_model, but:
- The write path in
agent.pyonly copieschat_modelif it's inbranch_dict— and it's frequently missing (empty in 5/5 sampled rows). - Flow ops create multiple branches with different models — the per-branch model is the right place, but it's not reliably written.
- No
provideroreffortinnode_metadataeither.
The runs list shows agent name and status but not model. The run detail page shows messages but not which model produced them. For runs via pre-configured agents (-a reviewer, -a architect), the agent profile specifies the model — but the actual resolved model (after defaults, overrides, and fallbacks) is not captured.
Why this matters¶
- Debugging: "Why did this run produce bad output?" — was it using sonnet when it should have been opus? Can't tell from the DB.
- Cost tracking: Model choice drives cost. Without model info per session, cost attribution is impossible.
- Agent evolution: Agent profiles change. If
reviewer.mdswitches from sonnet to opus, historical runs should show what they actually used, not what the profile currently says. - Compliance: For future audit trails, knowing which model processed which data is a hard requirement.
Decision¶
Add first-class provenance columns to sessions¶
ALTER TABLE sessions ADD COLUMN model TEXT; -- resolved model spec: "claude/claude-sonnet-4-6"
ALTER TABLE sessions ADD COLUMN provider TEXT; -- provider name: "claude_code", "codex", "openai", "anthropic"
ALTER TABLE sessions ADD COLUMN effort TEXT; -- effort level: "low", "medium", "high", "xhigh"
ALTER TABLE sessions ADD COLUMN agent_hash TEXT; -- SHA-256 of agent definition file content at invocation time
These are resolved values — not what the config says, but what the runtime actually used after all defaults, overrides, and fallbacks.
Add first-class provenance to branches¶
ALTER TABLE branches ADD COLUMN model TEXT; -- branch-level model (may differ from session for multi-agent flows)
ALTER TABLE branches ADD COLUMN provider TEXT;
ALTER TABLE branches ADD COLUMN agent_name TEXT; -- the agent role within a flow (e.g., "explorer", "analyst")
Branch-level provenance matters for flows where different agents use different models. A flow session might use sonnet for the explorer and opus for the critic — the session-level model is the "default" or "primary" model; branch-level is the actual.
Write points¶
| Event | Who writes | What |
|---|---|---|
li agent start | agent.py | Session: model, provider, effort, agent_hash, agent_name |
li play start | flow.py | Session: model (default), provider, effort. Per-op branches: model, provider, agent_name |
li o flow start | flow.py | Same as play |
li o fanout start | fanout.py | Session: model, provider. Worker branches: model, agent_name |
| Branch creation (any) | _orchestration.py | Branch: model, provider, agent_name from the agent config |
Agent hash computation¶
import hashlib
from pathlib import Path
def agent_definition_hash(agent_name: str) -> str | None:
"""SHA-256 of the agent profile file content at invocation time."""
from lionagi.utils import LIONAGI_HOME
path = LIONAGI_HOME / "agents" / f"{agent_name}.md"
if not path.exists():
return None
return hashlib.sha256(path.read_bytes()).hexdigest()[:16]
16-char truncated SHA-256 is sufficient for "same or different" checks. Not a security hash — just a content fingerprint.
Model resolution disclosure¶
The model string stored must be the fully resolved spec, not the input. Resolution chain:
User input: "sonnet"
→ parse_model_spec(): ModelSpec(model="claude/claude-sonnet-4-6")
→ agent profile override: (none, use parsed)
→ effort override: (none)
→ fast mode: (none)
→ RESOLVED: "claude/claude-sonnet-4-6" ← this is what gets stored
If the user passes --model sonnet, the DB stores claude/claude-sonnet-4-6, not sonnet. This ensures the column is stable and comparable across runs.
Studio display¶
Runs list¶
Add a Model column to the runs list:
| Name | Agent | Model | Status | Duration |
|---|---|---|---|---|
| play:backend | architect | claude/claude-sonnet-4-6 | completed | 87m |
| agent | reviewer | openai/codex-mini-latest | completed | 15m |
| flow | — | claude/claude-sonnet-4-6 | running | 3h |
The model column shows the session-level model. For multi-model flows, a badge or tooltip shows "2 models" with the breakdown.
Run detail¶
Each step/branch shows its own model and agent:
Step: explorer (claude/claude-sonnet-4-6, effort=high)
[messages...]
Step: critic (claude/claude-opus-4-6, effort=high)
[messages...]
Agent reference¶
When agent_name is set and the agent definition file exists, the run detail page shows a link to the agent profile. The agent_hash enables a "definition changed since this run" indicator:
Agent: reviewer (definition changed since this run)
Backfill for existing sessions¶
Existing sessions have model = NULL. No automated backfill — the resolved model is lost for historical runs. The model column is nullable; Studio renders NULL as "—" or "unknown."
For imported filesystem runs (source_kind = 'imported_fs'), the import path can extract model from run.json → manifest.model_spec if present.
Consequences¶
Positive
- Every run discloses its model, provider, and effort level — no guessing.
- Multi-model flows show per-branch model, not just the session default.
- Agent definition snapshots via hash enable "drift since this run" detection.
- Cost attribution becomes possible (model → pricing table → cost per run).
- Fully resolved model specs are stable and comparable.
Negative
- Four new columns on sessions, three on branches. Reconciled via
_reconcile_columns()— no migration runner needed. - Write path changes in
agent.py,flow.py,fanout.py,_orchestration.py. Must ensure all paths write provenance. - Historical runs have NULL model — incomplete data for old runs.
agent_hashis a point-in-time snapshot; it doesn't store the actual content. If the agent file is deleted, the hash is unverifiable. Acceptable — the hash answers "same or different," not "what was it."
Alternatives Considered¶
| Alternative | Why Rejected |
|---|---|
Store model in node_metadata JSON only | Not queryable — "show all runs using opus" requires JSON parsing every row |
| Store full agent definition content | Too large for a column; agent files can be multi-KB. Hash is sufficient for drift detection |
Derive model from branch node_metadata | Already attempted — write path is unreliable, data is missing in practice |
| Store user-input model string (e.g., "sonnet") | Not stable — alias resolution changes over time. Resolved spec is canonical |
| Store model on sessions only (not branches) | Loses per-agent model info for multi-model flows |
| Automated backfill from filesystem runs | Model resolution is not deterministic from historical data — the agent profile and defaults may have changed since the run |
References¶
- ADR-0009 — SQLite state layer (sessions + branches schema)
- ADR-0012 — Execution lineage (provenance columns)
- ADR-0017 — Session lifecycle
lionagi/cli/agent.py— Session + branch creationlionagi/cli/orchestrate/flow.py— FlowOp agent model resolutionlionagi/cli/_providers.py—parse_model_spec()resolution chain
Prior art¶
- W3C PROV-DM (2013) — The Entity-Activity-Agent triple maps to lionagi's Session-Branch-Message model. Our provenance columns capture a simplified form of the PROV derivation relation.
- Buneman-Khanna-Tan 2001 ("Why and Where: A Characterization of Data Provenance", ICDT) — Distinguishes why-provenance (which inputs produced this output) from where-provenance (which source contributed).
agent_hashcaptures where-provenance; the branch message chain captures why-provenance.