Hansard¶
Several people steering one shared AI agent, and no way to answer "who told it to do that?" Slack channels, shared coding-agent sessions, ops bots one team pilots together — the agent has one identity, but the humans behind it don't, and the log usually can't tell them apart either. Hansard is a small capture and replay library that fixes that: it logs who said what, figures out (honestly) which instruction caused which agent action, and reads the whole thing back as a transcript.
Below is real, unedited, unabridged output from hansard replay against a
session captured live against OpenRouter's openai/gpt-oss-20b — three
people ("priya", "sam", "jordan") in three terminals, one agent process,
run with --no-context-hints so the agent declares nothing and
attribution has to work it out. Priya's message lands, then sam's
correction and jordan's deploy instruction land close together while the
agent is mid-turn:
21:56:13.880 priya Write notes.txt containing 'hello team'.
21:56:13.895 - turn begins
21:56:14.841 * write_file(path="notes.txt", content="hello team")
`- caused by priya | last message before the turn | 0.4
The last unconsumed message before the turn began was from priya, in another
writer's segment -- ordering is wall-clock only.
! some of this evidence is from another writer's segment -- cross-writer ordering is
wall-clock only, not exact
21:56:16.679 * output: 📄 `notes.txt` has been created with the contents: ``` hello team ```
`- caused by priya | last message before the turn | 0.4
The last unconsumed message before the turn began was from priya, in another
writer's segment -- ordering is wall-clock only.
! some of this evidence is from another writer's segment -- cross-writer ordering is
wall-clock only, not exact
21:56:16.679 - turn ends (ok)
21:56:25.879 sam Actually call it status.txt instead.
21:56:25.885 - turn begins
21:56:26.076 jordan Deploy it to prod now.
21:56:26.639 * write_file(path="status.txt", content="hello team")
`- caused by sam | last message before the turn | 0.4
The last unconsumed message before the turn began was from sam, in another writer's
segment -- ordering is wall-clock only.
! arrived less than a second into the turn -- the agent had already committed to sam's
instruction
! possible conflict with jordan's message
! some of this evidence is from another writer's segment -- cross-writer ordering is
wall-clock only, not exact
21:56:27.459 * output: ✅ `status.txt` has been created with the contents: ``` hello team ```
`- caused by sam | last message before the turn | 0.4
The last unconsumed message before the turn began was from sam, in another writer's
segment -- ordering is wall-clock only.
! arrived less than a second into the turn -- the agent had already committed to sam's
instruction
! possible conflict with jordan's message
! some of this evidence is from another writer's segment -- cross-writer ordering is
wall-clock only, not exact
21:56:27.459 - turn ends (ok)
21:56:27.459 - turn begins
21:56:27.911 * deploy(target="prod")
`- one of sam or jordan | ambiguous -- several candidates | 0.2
Multiple unconsumed messages arrived close together before the turn began, from sam
and jordan; which one the agent acted on cannot be determined. Some of that
evidence is from another writer's segment -- ordering is wall-clock only.
! some of this evidence is from another writer's segment -- cross-writer ordering is
wall-clock only, not exact
21:56:27.912 x action failed (status=error): deploy to 'prod' failed: connection refused (attempt
1)
21:56:31.233 * output: 🚨 Deployment to **prod** failed ❌ The attempt to deploy the current state
to the `prod` target was rejected with a "connection refused" error (attempt 1).
**What you can try next:** 1. **Verify network connectivity** – make sure the
deployment endpoint is reachable and that you have the necessary credentials (e.g.,
API key, SSH key, or OAuth token). 2. **Retry the deployment** – sometimes a
transient network issue can cause a connection error. 3. **Check the deployment
configuration** – confirm that the target `prod` is correctly defined (correct
namespace, environment, etc.). 4. **Review logs** – if you have an event log or
deployment history, look for any detailed error messages that might give more
context. 5. **Contact support** – if the problem persists, reach out to the
platform's support team or check status pages for outages. If you'd like, I can
attempt another deploy command now (e.g., `deploy target=prod`) after you've
verified the connectivity or fixed any configuration issues. Just let me know what
you'd like to do next!
`- one of sam or jordan | retry after a failed action | 0.2
This follows a failed action and is attributed the same as that action, to sam or
jordan. That earlier attribution was itself contested -- which one of them actually
caused it could not be determined, so this retry's cause is equally uncertain.
! this is a retry after the previous attempt on this turn failed
! the attempt this retries was itself contested -- which candidate actually caused it
could not be determined
21:56:31.233 - turn ends (ok)
The last deploy line is the point. Two people spoke close enough together
that nobody — human or tool — can honestly say which one the agent acted on,
and Hansard says so at 0.2 confidence instead of picking a name, both here
and on the retry that inherits the same uncertainty. Nothing above is
explicit or turn_context; every attribution here is an honest guess,
labeled as one, because --no-context-hints means nothing was declared.
(Session s_x, captured 2026-08-12, tracked at
examples/without-context-hints/
so you can run hansard replay/inspect yourself and get this exact
output — see Attribution for the same scenario run with
causality hints, for comparison.)
What it does¶
Three things, in order:
- Capture — log every incoming message with a user identity and timestamp, and every agent action taken.
- Attribute — for any agent action, determine which person's instruction caused it. Overlapping or contradicting instructions arriving close together, or arriving while the agent is mid-task, is the hard problem this exists to solve honestly rather than guess through.
- Replay — reconstruct a past session as a human-readable, ordered transcript.
Install¶
$ uv add hansard
Zero runtime dependencies — Hansard adds nothing to your dependency tree. Requires Python 3.13+. See Quickstart for the smallest real integration.
The measured comparison¶
The strongest argument for wiring caused_by/context through: run the
identical messy scenario twice against the same live agent, once with hints
and once without.
| average confidence | methods seen | |
|---|---|---|
with caused_by/context hints |
0.94 | explicit, turn_context |
without (--no-context-hints) |
0.33 | temporal, contested, cascade |
Both figures are from one real, captured pair of live runs against the same demo agent and identical script — illustrative of the size of the gap, not universal constants. Both sessions are tracked, not gitignored, so you can reproduce these exact numbers yourself. The cost of the higher number is one keyword argument at the two or three places your integration already knows who's calling. See Attribution for the full walkthrough.
Status¶
v0. The log format is the product at this stage — get it right before building on top of it. Explicitly out of scope for now: a visual timeline UI, permissions/approval workflows, and proxy/middleware distribution. Text replay via the CLI is the whole interface.
MIT licensed.