Short answer
An AI audit trail is a tamper-evident, append-only record of every run: who asked, which model and prompt version answered, what was retrieved, which tools ran with what arguments, who approved what, and what was returned. Structure it as linked trace events, follow OpenTelemetry’s GenAI conventions for field names, keep sensitive content in a separate controlled store, and test the trail by reconstructing a past run end to end.
- Log runs as linked events: model calls, retrievals, tool calls, approvals and outputs.
- Record versions of prompts, models, tools and indexes, or you cannot reproduce anything.
- Keep message content in a separate store with its own access and retention rules.
- Never sample audit events. Sample debugging traces if you must.
- Chain event hashes so tampering or gaps are detectable.
What should an AI audit trail let you answer?
An AI audit trail should let someone who was not there reconstruct any run, weeks later, and answer five questions: what the system did, what it saw when it decided, who authorised it, what changed in other systems as a result, and which versions of prompts, models, tools and data were live at the time. If any answer needs an engineer’s memory, the trail is incomplete.
That makes an audit log different from ordinary application logging. Debug logs help engineers fix code and can be sampled, truncated or rotated quickly. Audit events are complete, protected against tampering, linked across systems and kept on a retention schedule the organisation chooses. The NIST AI RMF core asks for both sides: transparency and accountability risks examined and documented (Measure 2.8), and post-deployment monitoring that covers incident response, recovery and change management (Manage 4.1).
How should the trace be structured?
Structure each run as a trace of linked events, one per meaningful step, all carrying the same run identifier and a sequence number. The event types below cover most systems, including agents.
| Event | Key fields | Content handling |
|---|---|---|
run.start |
run ID, tenant, pseudonymous user ID, channel, app version, prompt version | Request text stored by reference |
model.call |
operation, provider, requested and returned model, token counts, finish reason, latency | Input and output stored by reference, with hashes |
retrieval |
index version, document IDs and versions, scores, filters applied | Passage text by reference |
tool.call |
tool name and version, arguments, idempotency key, status, external record ID | Secrets redacted before logging |
approval |
approver, decision, time, hash of exactly what was shown | Rendered action stored by reference |
output |
recipient, delivery channel, external message ID | Delivered content stored by reference |
run.end |
status, total cost, step count, escalation reason | None |
Use the OpenTelemetry GenAI semantic conventions for field names where they fit. As listed on 19 October 2026 they define gen_ai.operation.name values including chat, embeddings, retrieval, execute_tool and invoke_agent, plus attributes such as gen_ai.request.model and gen_ai.usage.input_tokens. They are marked as in development, and content attributes such as gen_ai.input.messages are opt-in because they may contain personal information. If your agents call MCP servers, the 2026-07-28 MCP revision documents how to pass trace context in request metadata, so server-side tool events can join the same trace.
A single tool event might look like this:
{
"event": "tool.call",
"run_id": "run_7f3c2a",
"seq": 6,
"ts": "2026-10-14T21:42:07.118Z",
"tenant": "acme-accounting",
"actor": {"type": "agent", "id": "gst-assistant", "prompt_version": "v14", "model": "claude-sonnet-5-5"},
"on_behalf_of": "user_5521",
"gen_ai.operation.name": "execute_tool",
"tool": {"name": "send_client_email", "version": "3.2.0"},
"args": {"client_id": "C-1093", "template": "gst_draft_ready", "attachment_doc_id": "doc_88a1"},
"idempotency_key": "run_7f3c2a:6",
"approval_ref": "appr_219",
"result": {"status": "sent", "external_id": "msg_4b9e0d"},
"content_ref": "cs://runs/run_7f3c2a/6",
"prev_hash": "9c1e...e4",
"hash": "51ab...07"
}
What should you record, hash or leave out?
Record identifiers and versions in full, keep content in a separate store, and never log secrets. The split keeps the audit log small, searchable and safe to share with investigators, while the content store holds the sensitive material under tighter control.
| Data | Audit log | Content store | Never logged |
|---|---|---|---|
| Prompt template, model and tool versions | Full | Not needed | |
| User messages and model outputs | Reference and hash | Full, access-controlled | |
| Retrieved passages | Document IDs, versions, scores | Text if documents may change later | |
| Tool arguments | Business identifiers in full | Free-text arguments | API keys, tokens, card numbers |
| What an approver saw | Hash and reference | The rendered action | |
| Provider reasoning output | Usually omitted | Only if your provider returns it and you need it |
Storing a hash of what an approver was shown matters more than it looks. When a reviewer later says the screen did not show the recipient, you can prove what it did show.
How do you make the log tamper-evident?
Make the log append-only and chain each event to the previous one with a cryptographic hash, so any edit, deletion or insertion breaks verification from that point on.
import hashlib
import json
GENESIS = "0" * 64
def _digest(prev_hash: str, event: dict) -> str:
body = json.dumps(event, sort_keys=True, separators=(",", ":"))
return hashlib.sha256((prev_hash + body).encode()).hexdigest()
def append_event(log: list, event: dict) -> None:
prev = log[-1]["hash"] if log else GENESIS
log.append({**event, "prev_hash": prev, "hash": _digest(prev, event)})
def verify(log: list) -> int:
"""Return the index of the first bad event, or -1 if the chain is intact."""
prev = GENESIS
for i, entry in enumerate(log):
event = {k: v for k, v in entry.items() if k not in ("prev_hash", "hash")}
if entry["prev_hash"] != prev or entry["hash"] != _digest(prev, event):
return i
prev = entry["hash"]
return -1
Hash chaining detects tampering; it does not prevent it. Pair it with write-once storage in a separate account that application credentials cannot delete from, and record the latest chain hash somewhere independent each day.
What do New Zealand privacy rules mean for AI logs?
AI logs that contain personal information are subject to the Privacy Act 2020 like any other record. Three information privacy principles in the Act shape the design:
- IPP 5, storage and security. Personal information must be protected by reasonable safeguards against loss and unauthorised access, use, modification or disclosure. Logs copied into every observability tool fail this quickly.
- IPP 6, access. People can ask for personal information you hold about them, which includes what your AI system recorded. Index the content store by person so requests are answerable.
- IPP 9, retention. Personal information must not be kept longer than required for the purposes it may lawfully be used for. A common pattern is longer retention for event metadata and hashes, shorter for message content.
Where the logs are stored is a separate decision from where the model runs; see data residency for AI systems.
How does an incident reconstruction work in practice?
Illustrative example: an accounting practice’s assistant emails a client a draft GST return that belongs to a different client. The audit trail turns a vague complaint into a sequence of facts.
- Find the run. The client forwards the email; its message ID matches
external_idon atool.callevent, which gives the run ID. - Replay the sequence. Ordering events by
seqshows the request, two retrievals, a model call, an approval and the send. - Inspect the retrieval. The second retrieval returned documents for two clients with near-identical trading names, and the model attached the wrong one.
- Inspect the approval. The approval hash matches a rendered summary that showed the recipient but not the attachment’s client name. The reviewer approved what they were shown.
- Check versions. Prompt version v14, deployed four days earlier, removed an instruction to confirm the client ID against the attachment.
- Assess and act. Personal information was disclosed, so the practice assesses serious harm and, if it applies, notifies the Privacy Commissioner. Fixes: a code check that attachment and recipient client IDs match, and an approval screen that shows both.
Without the trail, steps 3 to 5 are guesswork. Our AI incident response guide covers the wider process, and the AI incident log template is a place to record the outcome.
Where do AI audit trails fail?
They usually fail by omission, discovered during the first serious incident.
- Missing versions. Without prompt, model, tool and index versions, you cannot say what code made the decision.
- Sampled audit events. Sampling suits performance traces, not audit. Every run must be complete.
- Approvals without context. Logging “approved” without what was shown makes the approval worthless as evidence.
- Content everywhere. Full prompts in third-party tracing tools spread personal information beyond your controls.
- Lost on failure. Events buffered in memory vanish when a worker crashes. Write before acting on irreversible steps.
- Unlinked systems. Tool events that do not record the external record ID cannot be matched to what happened in the CRM or ledger.
Audit design is part of our AI governance advisory, and running the trail day to day, with monitoring of AI quality on top, is part of managed AI operations. Approval records connect directly to the controls in agent permissions and approvals.
- Every run has a run ID, sequence numbers and linked events for each step
- Prompt, model, tool and index versions are on every relevant event
- Tool events record arguments, idempotency keys and external record IDs
- Approval events store a hash of exactly what the approver saw
- Message content lives in a separate store with its own access and retention
- Secrets and payment data are redacted before any event is written
- Audit events are never sampled
- Events are hash-chained, stored write-once and verified on a schedule
- Content can be found by person to answer IPP 6 requests
- A past run has been reconstructed end to end as a drill
Sources
- OpenTelemetry GenAI semantic conventions, OpenTelemetry (GitHub), accessed 19 October 2026
- Model Context Protocol: Key Changes in 2026-07-28, Model Context Protocol, 28 July 2026
- Privacy Act 2020, New Zealand Legislation, 2020
- AI RMF Core, National Institute of Standards and Technology, 2023
- NotifyUs of a serious privacy breach, Office of the Privacy Commissioner, accessed 19 October 2026
Written by the Levy Street team in Wellington. Last updated 19 October 2026.