AI Agent Audit Trail: Recording What the Agent Saw
What an AI agent audit trail must record to be useful: not just the action, but the context the agent read, when it was true, and the policy it acted under.
What an AI agent audit trail must record to be useful: not just the action, but the context the agent read, when it was true, and the policy it acted under.
TL;DR: An AI agent audit trail is useful only if it lets you answer two questions about any decision: was the decision reasonable given what the agent saw, and was what it saw true at the time? Most agent logs answer neither. They record the action and the prompt, not the values the agent’s tools returned or the moment those values were true. Record each decision as a row: the inputs, each input’s as-of time, the policy version, the action and its external reference.
A customer disputes a decision your agent made last Tuesday. Risk wants to know why the agent approved it. Legal wants to know whether the agent followed policy. Engineering pulls the trace.
The trace shows the prompt, the model’s reasoning, four tool calls by name, and the final approve_refund call. What it doesn’t show is what get_chargebacks returned, or whether that value was current. The reasoning says “no prior chargebacks.” Was that true? Was it true at 10:02 on Tuesday, when the agent read it? Nobody can say, because nobody recorded it.
That ’s the gap between logging and an audit trail. Logs tell you what the agent did. An audit trail has to tell you what it knew.
An AI agent audit trail is a durable, tamper-evident record of each consequential decision an agent made: what it did, under which model, prompt and policy version, on whose authority, and the context it read to decide, including when each piece of that context was true. It lets a reviewer reconstruct why the agent acted as it did.
The last part is what separates an audit trail from activity logging. An action without its inputs is a fact you can’t evaluate. Inputs without their timing can’t tell you whether the agent was wrong or the data was.
The common source of agent telemetry is tracing: a span per model call, a span per tool call. OpenTelemetry’s semantic conventions for generative AI spans, now maintained in a dedicated repository, define operation names including execute_tool, along with attributes for the tool’s name and call.
Two details matter for audit. The conventions are marked with status “Development,” so names and requirements can still change. And the attributes that hold the tool’s input and output, gen_ai.tool.call.arguments (“Parameters passed to the tool call”) and gen_ai.tool.call.result (“The result returned by the tool call”), are Opt-In. A default setup can produce a perfect trace of which tools ran without recording what any of them returned.
Even with those attributes turned on, a tool result is a value with no time attached. {“chargebacks_90d”: 0} is a different fact if it was read from the primary database than from a replica running forty seconds behind, or from a feature table refreshed hourly.
Traces are built for debugging latency and errors. An audit trail is built for answering questions months later. They overlap, but the second needs things the first leaves out.
Every review of an agent decision comes down to two questions, and they have different owners.
Was the decision reasonable given its inputs? This is about the model, the prompt and the policy. If the agent saw “no prior chargebacks, customer tier gold, amount under limit” and approved, that’s defensible. If it saw “three chargebacks” and approved, the prompt or policy is wrong.
Were the inputs true when the agent read them? This is about data. If the agent saw “no prior chargebacks” but one was filed twenty seconds earlier and hadn’t reached the system the tool read, the agent behaved correctly and the context was wrong. No prompt change will fix that.
Without inputs, you can’t answer the first. Without the as-of time of each input, you can’t answer the second, and teams end up “fixing” prompts for what were data-freshness failures.
Record each consequential decision as a structured row, not a log line. Start with the decisions that move money, change access or reach a customer; those are the ones someone will ask about. A minimal set of fields:
CREATE TABLE agent_decisions (
decision_id uuid PRIMARY KEY,
actor text NOT NULL,
policy_version text NOT NULL,
decided_at timestamptz NOT NULL,
action text NOT NULL,
action_params jsonb NOT NULL,
external_ref text,
approver text,
inputs jsonb NOT NULL
-- inputs example:
-- {"chargebacks_90d": {"value": 0, "source": "risk_replica",
-- "as_of": "2026-10-13T10:01:31Z"}, ...}
);
Make the table append-only. Corrections are new rows that reference the old one. The decision ledger entry covers this design in more depth.
| Field | What it holds | Why an auditor needs it |
|---|---|---|
decision_id | Unique ID, also used as the idempotency key for the action | Ties the record to the effect |
actor | Agent ID and the user or service it acted for | Authority |
policy_version | Model, prompt, tool set and rules version | Which logic decided |
inputs | The values each tool returned, or references to them | What the agent saw |
inputs_as_of | Per input: source system and the moment the value was true | Whether what it saw was current |
action | What it did, with parameters | What happened |
external_ref | Refund ID, message ID, order ID in the downstream system | Where to find the effect |
approver | Human approver, if any, and when | Oversight |
decided_at | When the agent committed to the action | Ordering against other events |
You can record the input values themselves or a reference that lets you look them up later. References are smaller, but they only work if the source can answer “what was this value at that moment?” months later. Most operational systems can’t: an account row that was updated has lost its old value unless something kept history. A feature table recomputed in place each night has overwritten yesterday’s number.
So record values for anything the decision turned on: the fields the model cited, the thresholds the policy compared, the scores. Use references with a content hash for bulky inputs such as retrieved documents. If you store references, check that the source keeps versions for at least as long as your retention period.
“Append-only” is a convention until something enforces it. Three layers, from cheapest to strongest:
Privileges. Give the role that writes decisions INSERT and SELECT on the table, not UPDATE or DELETE, and don’t make it the table’s owner or a member of the owning role. PostgreSQL’s privilege system grants each of these separately, so the agent’s credential can add rows but not rewrite them.
Hash chaining. Store, on each row, a hash of its contents plus the previous row’s hash. Editing or deleting a row breaks every hash after it, and a verifier can recompute the chain. Keep the latest hash somewhere the writer can’t change, such as the write-once export below; otherwise whoever edits a row can recompute the rest. Crosby and Wallach’s Efficient Data Structures for Tamper-Evident Logging (USENIX Security 2009) states the goal: “The logger must be able to prove that individual logged events are still present, and that the log, as seen now, is consistent with how it was seen in the past.” Their tree-based design produces much smaller proofs than a classic hash chain, but a plain chain is the simplest place to start.
Write-once exports. Copy closed periods to write-once storage. In S3 Object Lock compliance mode, “a protected object version can’t be overwritten or deleted by any user, including the root user in your AWS account.”
The actor field is easy to get wrong. An agent usually calls tools with its own credential while acting for a user or another service, and the audit needs both. OAuth has a standard shape for this: RFC 8693, OAuth 2.0 Token Exchange, defines an act claim that “provides a means within a JWT to express that delegation has occurred and identify the acting party to whom authority has been delegated,” and “a chain of delegation can be expressed by nesting one act claim within another.”
If your agents receive delegated tokens, record the subject and every actor from the token that authorized the call, not a name taken from the prompt. In a multi-agent run, that chain shows which orchestrator handed the task to which sub-agent.
The as_of for an input is the moment the value reflected the source, not the moment the tool returned it. They differ whenever the tool reads a copy. Here is what you can record from common sources:
An operational database. The time of the read, or a position in the write-ahead log. On a PostgreSQL primary, pg_current_wal_lsn() “Returns the current write-ahead log write location” and pg_current_snapshot() returns “a data structure showing which transaction IDs are now in-progress,” per the administration functions and system information docs.
A replica or CDC-fed copy. The source position the copy had applied when the tool read it, not the replica’s wall clock. On a PostgreSQL standby, pg_current_wal_lsn() can’t run during recovery; pg_last_wal_replay_lsn() “Returns the last write-ahead log location that has been replayed during recovery,” per the recovery control functions docs.
A precomputed feature or materialized view. The time of its last refresh.
A vector index or search index. The time of its last sync from the source documents.
If a decision reads four tools and records four different as_of times spread over minutes, that’s worth knowing on its own: the agent combined values that were never true at the same moment. The agent loop post walks through how that happens.
Here is an illustrative scenario. The numbers are constructed, not measured.
10:01:31 Chargeback filed on customer 5507 (primary payments database)
10:02:04 Agent reads chargebacks_90d = 0 from risk replica (as_of = 10:01:10)
10:02:04 Agent reads tier = gold from CRM (as_of = 10:02:04)
10:02:09 Agent approves refund 140; decision row written with inputs and as_of
10:02:40 Replica catches up; chargebacks_90d = 1
With a full record, the review takes five minutes. The decision was reasonable for its inputs: a gold customer with no chargebacks, under the limit. The input was stale: the replica’s as_of of 10:01:10 was before the 10:01:31 chargeback. The fix belongs in the data path, not the prompt. Without as_of, the same review concludes “the agent ignored a chargeback,” and someone rewrites a prompt that wasn’t broken.
If you deploy agents in a regulated setting, record-keeping may be a legal requirement, not only good practice. The EU’s Artificial Intelligence Act (Regulation (EU) 2024/1689) is one explicit example, and its logging rules apply to high-risk AI systems as the Act defines them.
Article 12(1) says: “High-risk AI systems shall technically allow for the automatic recording of events (logs) over the lifetime of the system.” Article 12(2) says those logging capabilities “shall enable the recording of events relevant for” identifying situations that may result in the system presenting a risk or in a substantial modification, facilitating post-market monitoring, and “monitoring the operation of high-risk AI systems referred to in Article 26(5).”
On retention, Article 26(6) says deployers “shall keep the logs automatically generated by that high-risk AI system to the extent such logs are under their control, for a period appropriate to the intended purpose of the high-risk AI system, of at least six months, unless provided otherwise in applicable Union or national law, in particular in Union law on the protection of personal data.” Article 19(1) sets the same six-month minimum for providers.
Apart from a minimum list for remote biometric identification systems in Article 12(3), the Act doesn’t prescribe what a log must contain, and many agent deployments won’t be high-risk systems under it. But a log that can’t show what a system relied on to decide is hard to use for monitoring of any kind. This section summarizes the text; it isn’t legal advice. Ask your counsel which obligations apply to you.
An audit trail has two hard parts: getting a consistent record of what the agent read, and writing the decision record so it can’t disagree with the state the decision changed.
The Tacnode Context Lake™ holds the decision context for agents: everything a decision reads and everything it decided. Decision coherence describes the architecture.
Decision records as state Tacnode holds. The decision row, the hold or limit it changed, and the agent’s action record can be written in one ACID transaction. There’s never a hold or limit change in the store without its record, or a record for a change that didn’t commit. Later decisions read those records as context, and auditors query the same table with SQL.
Inputs read from one moment. When the agent’s context comes from Tacnode, the raw rows, aggregates computed on demand and vector similarity results it reads in a single read resolve against the same snapshot. For data that arrives from your own systems by change data capture, that snapshot is internally coherent, against the same set of ingested events, so the inputs in the record describe one set of ingested data instead of four systems at four as_of times. Payments and orders stay in the systems you run; Tacnode doesn’t enforce anything inside a ledger it doesn’t hold.
If your agent only answers questions from a document set that rarely changes, an audit trail needs the documents’ versions, and a vector index on pgvector is enough.

Former Couchbase and IBM. Two decades helping enterprises adopt new database paradigms.
Fresh Context Newsletter
The argument behind the posts — what breaks when AI acts on stale data, from the team building the Context Lake. About twice a month.
Book a demo and discover how Tacnode can power your AI-native applications.
Book a Demo