A Decision Trace and Audit pattern is an observability mechanism that systematically records structured, redacted events of an agent’s observable actions, policy evaluations, and tool calls for post-execution analysis and compliance. Record the observable evidence around every agent decision, so a run can be reconstructed after the fact without re-running it.
The Need for Agent Decision Auditing
Your agent made a decision. Somewhere, at some point, someone will ask why.
It might be a colleague debugging an unexpected output. A security reviewer questioning whether the agent acted within its defined scope. A compliance team asking whether a sensitive record was accessed legitimately. Or just you, six weeks from now, trying to understand why the agent chose path A over path B when both looked valid.
If your answer in that moment is “there is no record, we would have to re-run it and see,” you do not have an observability problem. You have an architecture problem. Logs tell you what a process emitted, not what the agent saw, what tools it tried to use, which policies fired, which approvals happened, and what outcome was produced.
How Decision Trace and Audit Works
Decision Trace and Audit stores an ordered, structured trace for each agent run. It records observable events and decisions, not private chain-of-thought.
flowchart TD A[User request] --> B[Start trace] B --> C[Record selected agent] C --> D[Record retrieved source metadata] D --> E[Record redacted tool call] E --> F[Record policy decision] F --> G[Record final outcome and usage] G --> H[Trace available for debug, eval, audit]
Events are appended with a name, timestamp, and redacted payload:
trace.add("source.retrieved", {
"source_id": "runbook-orders-lag",
"lifecycle": "active",
})
trace.add("policy.decided", {
"decision": "allow",
"risk_level": "low",
})
In LangGraph, the trace store is not a node — it is a side effect every node calls. Shown across a realistic intake → retrieve → synthesize pipeline, each step appends one observable event, so the trace reconstructs the whole run without any node being dedicated to auditing:
store = AuditStore()
def intake(state: AgentState) -> dict:
store.record("request.received", "user", {"request": state["messages"][-1].content})
return {}
def retrieve(state: AgentState) -> dict:
store.record("source.retrieved", "retriever",
{"source_id": "runbook-orders-lag", "lifecycle": "active"})
return {"evidence": "runbook-orders-lag"}
def synthesize(state: AgentState) -> dict:
response = llm.invoke(state["messages"])
store.record("outcome.final", "agent",
{"content": response.content, "cited": state["evidence"]})
return {"messages": [response]}
builder.add_edge(START, "intake")
builder.add_edge("intake", "retrieve")
builder.add_edge("retrieve", "synthesize")
builder.add_edge("synthesize", END)
Problems and Considerations
Consider the following factors when you implement this pattern:
- What gets captured. Request receipt and routing, selected agent, retrieved source identifiers and lifecycle state, redacted tool parameters, policy decisions, human approval events, validation results, and final answer with usage metrics.
- Decision vs. observation. Not every trace event is the same kind of thing. A model’s suggestion to restart a service is an untrusted proposal. A broker’s decision to allow or deny that restart is a deterministic policy decision. A human’s approval is an accountable reviewer decision.
- Storage maturity. A local prototype can keep JSON-like traces in memory or a file, while a production system may use append-only logs, hash-chained events, or export to a SIEM.
- Emit structured events. A textual log is insufficient for automation. Use structured events that SIEM or Data Security Posture Management (DSPM) systems can parse, alert on, and retain.
- Continuous monitoring. The audit log should be fed into continuous monitoring systems to identify anomalous behavior or model drift across many invocations.