Skip to content
allsrc.dev
Go back

Pattern 3: Decision Trace and Audit

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:

References



Previous Post
Pattern 2: HITL Approval Gate
Next Post
Pattern 4: Cost and Tool Budgeting