Skip to content

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:

  1. Capture — log every incoming message with a user identity and timestamp, and every agent action taken.
  2. 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.
  3. 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.