Q2 2026 · Issue 2 All issues ·
SQ Stack Quarterly Quarterly deep dives on the tools real teams actually ship with.

Q2 2026 — Issue 2

Black Box AI: How to Build Auditable Agentic Stacks

What it actually takes to make an agentic system auditable — beyond the slide deck.

The phrase “Black Box AI” gets thrown around a lot. Most of the time it means roughly “we cannot tell what the model is doing inside its weights,” which is true but is not the auditability problem most teams are actually facing. The auditability problem most teams are facing is not “the model is opaque.” It is “our agentic system is making decisions and taking actions across a stack of half a dozen layers, and when something goes wrong we cannot answer the question ‘why did this happen’ without spending a day in our logs.” That is a different problem, with a different fix, and it does not require any breakthroughs in interpretability research.

This is a practitioner essay on how to make an agentic stack auditable in a way that a real auditor, regulator, or customer’s security team will accept. The lessons are unfashionable. Most of them are about discipline rather than tooling. I have laid them out as eight design choices, with code patterns where they help.

1. Treat every agentic action as a recorded event

The bedrock discipline is this: every action your agentic system takes — every LLM call, every tool call, every state change, every handoff — should produce a recorded event with enough metadata to reconstruct what happened.

The event should include, at minimum: a timestamp, a unique ID, the engagement or task context, the actor (which agent, with which configuration), the inputs to the action, the outputs, and a parent-event ID linking it back to whatever caused it. The events should be append-only, immutable once written, and stored in a place that the system itself cannot edit.

A minimal event schema:

from datetime import datetime
from pydantic import BaseModel
from typing import Literal, Any

class AuditEvent(BaseModel):
    event_id: str
    parent_event_id: str | None
    timestamp: datetime
    engagement_id: str
    actor_kind: Literal["agent", "tool", "human", "system"]
    actor_name: str
    actor_config_hash: str
    action: str
    inputs: dict[str, Any]
    outputs: dict[str, Any]
    side_effect: Literal["read_only", "mutates", "external_call"]

The schema is intentionally boring. Auditability does not require a clever schema; it requires that the schema be complete and that every action actually emits one. The discipline is the work.

The actor_config_hash field is the small but important detail. The configuration of an agent — its prompts, its model, its tool list, its behavior settings — can change. If you do not record which version of the configuration was in effect when an action happened, you cannot replay the action later. The hash is the cheapest way to record it.

2. Distinguish read-only from mutating actions

Most agentic stacks treat all tool calls the same way. They are not the same. A tool call that reads data has different risk, different audit requirements, and different recovery semantics than a tool call that changes state in an external system.

Make the distinction explicit at the tool definition. Read-only tools can be called freely. Mutating tools should be gated — by human approval for high-stakes actions, by rate limits at the server, by mandatory recording in the audit log. The split is enforced at the server, not at the agent, because the agent is the part of the system that might be misconfigured.

I have shown the pattern in a previous piece in this issue, and it remains the single most useful auditability primitive I know:

@tool(side_effect="read_only")
def list_orders(customer_id: str) -> list[Order]: ...

@tool(side_effect="mutates", requires_approval=True)
def refund_order(order_id: str, amount: float) -> Refund: ...

When the audit comes, the question “what mutating actions did this engagement perform” is a one-query answer. Without the split, it is a half-day investigation.

3. Pin and hash the configuration of every agent

Every agent in your system is a function of its configuration: its prompts, its model, its tool list, its policies. The configuration changes over time. Some changes are intentional (you updated the editor’s checklist). Some are accidental (someone bumped a model version). Most are forgotten by the time an incident happens.

The fix is to treat agent configurations as immutable artifacts, versioned and hashed. Each agent invocation records the hash of its configuration. The configurations themselves are stored in a separate artifact store that the agents cannot mutate. When the auditor asks “what was the editor agent configured to do at the time of this incident,” the answer is a lookup, not an archaeology project.

A sketch of the pattern:

class AgentConfig(BaseModel):
    name: str
    version: str
    model: str
    system_prompt: str
    tools: list[str]
    policies: dict[str, Any]

    def hash(self) -> str:
        from hashlib import sha256
        payload = self.model_dump_json(sort_keys=True).encode()
        return sha256(payload).hexdigest()[:16]

# Stored in an artifact registry, not in the live runtime.
configs_registry.put(editor_config_v3)

# At invocation time:
def invoke_editor(brief, draft):
    config = configs_registry.get_latest("editor")
    audit.start(actor="editor", config_hash=config.hash())
    result = run_with_config(config, brief, draft)
    audit.end(outputs=result)
    return result

The discipline of pinning configurations is the same discipline that lets you replay an incident. Without it, the system you are debugging today is not the system that misbehaved last week.

4. Make the human-in-the-loop signal a first-class audit object

Most teams have human-in-the-loop primitives. Few teams treat the human signal as an audit object. The audit value of a human approval is not “the human said yes.” It is “this human, identified by this account, at this time, with this view of this state, approved this action.”

The pattern is to record the human signal as an audit event in the same stream as the agentic events, with enough metadata to reconstruct what the human was looking at when they approved. The minimal schema additions:

class HumanApprovalEvent(BaseModel):
    event_id: str
    timestamp: datetime
    human_id: str
    proposed_action_event_id: str
    decision: Literal["approved", "rejected", "edited"]
    rendered_view_hash: str  # hash of what they were shown

The rendered_view_hash is the unusual field. It records a hash of the UI state the human saw when they made the decision. If the human approved a draft, we want to know which exact draft they approved, not just that they approved something. This sounds like a small detail. It is the difference between an auditable approval and a hand-wavy one.

5. Bound every loop, escalate every exhaustion, log every escalation

I have made this point in other pieces and I will make it again. Unbounded loops are an audit nightmare. A system that retries forever has no auditable failure event. A system that bounds its loops and escalates on exhaustion has a clear, recordable point where the human takes over.

def run_with_bound(operation, max_attempts=3, escalate_to="human_owner"):
    for attempt in range(max_attempts):
        result = operation(attempt=attempt)
        audit.record_attempt(operation, attempt, result)
        if result.success:
            return result
    audit.record_escalation(operation, max_attempts, target=escalate_to)
    surface_to_human(operation, last_result=result, escalation_target=escalate_to)
    return result

Every escalation gets logged. Every escalation surfaces to a named human. The auditor can ask “how often does this system escalate, on which engagements, to which humans” and get an answer in seconds.

6. Keep an evidence trail for every external claim the system makes

This is the one that matters most in the marketing-side use cases, and the one most teams skip. If your agentic system is publishing content — blog posts, ad copy, social posts, anything that asserts facts about the world — every assertion in the output should be traceable to a source the system consulted.

The implementation is a citation layer: when the drafter agent makes a factual claim, it cites the source. When the editor checks the draft, it verifies the citations. When the asset is published, the citation map is recorded alongside the asset in the audit log.

class Claim(BaseModel):
    text: str
    source_url: str | None
    source_kind: Literal["external", "brief", "internal_database"]

class CitedDraft(BaseModel):
    title: str
    body: str
    claims: list[Claim]

When the asset is challenged later — by a customer, by a regulator, by a journalist — the answer to “where did you get that” is in the audit log. Without the citation layer, the answer is “the model made it up, and we did not catch it.”

7. Make replay possible, even if you never need it

The single most powerful auditability primitive is the ability to replay an incident. Replay means: given an audit log of an engagement, you can rerun the same agents with the same configurations against the same inputs and get the same outputs (or near-enough, given the stochasticity of LLMs).

Full replay is hard. Approximate replay is achievable for most teams and worth the work. The pieces you need are: a configuration registry that lets you retrieve the exact agent config used at the time, a deterministic-as-possible mode for the LLM calls (low temperature, fixed seeds where the model supports them), and an audit log that captures all the inputs.

You will not use replay often. The few times you do, it will be the difference between a defensible incident response and a hand-wavy one.

8. Decide what is auditable by design and what is not

You cannot make everything auditable, and pretending you can is a credibility loss with serious auditors. The model’s internal reasoning is not auditable in any meaningful sense; you can record the prompts and the outputs, but you cannot record “why” the model produced what it produced. The fine-grained latency of network calls between your agents and your model provider is not auditable beyond what the provider exposes.

The honest move is to declare a scope. Here is the scope I would propose for most agentic systems shipping today:

  • Auditable: every agentic action, every tool call, every human approval, every state change, every escalation. Configuration changes. Citations on factual claims. Replay against pinned configurations.
  • Not auditable, by design: the model’s internal reasoning. The model provider’s infrastructure. The exact tokenization of any given LLM call.
  • Auditable on request: full prompt-and-response capture for high-stakes calls, with retention bounded by the team’s privacy policy and the customer’s data preferences.

Declaring the scope makes the system honestly auditable for the parts that matter and stops you from making claims the system cannot defend.

A worked-out story

Imagine an auditor — for a regulated industry customer, for example — asks the question: “Six months ago you published an asset that misrepresented one of our product features. Walk me through how that happened and what would prevent it from happening again.”

In a system without auditability discipline, the answer is a combination of guessing, log archaeology, and apology. In a system with the eight choices above, the answer is a deterministic walk-through.

You retrieve the engagement’s audit log. You find the asset’s publication event. You walk the parent-event chain back to the drafter call that produced the offending paragraph. You find the configuration hash of the drafter at that time. You retrieve the configuration from the registry. You retrieve the brief that fed the drafter. You retrieve the citations attached to the asset. You see that the claim in question had a source field of “internal_database” pointing to a record that has since been corrected. You see that the editor at that time was configured with a checklist that did not yet include a check for that specific failure mode. You see that the failure mode has since been added to the editor’s checklist (configuration v4, current). You can replay the engagement against the new editor configuration and demonstrate that the new editor would catch the same draft.

That is what auditability buys you. The story is dry, the evidence is concrete, and the customer’s security team can verify every step. Whether or not the underlying model is a black box is, in that conversation, irrelevant. The system around the model is auditable.

Where this matters most

The teams who will adopt this discipline first are the ones whose customers are regulated, security-conscious, or legally exposed. The teams who will adopt it second are the ones whose customers have been burned by an opaque AI vendor and are now writing auditability into their vendor selection criteria. The teams who will adopt it third are the ones who realize that auditability is, on the margin, a competitive moat in a market where most agentic systems are not auditable.

The agency that has been most explicit about treating auditability as a differentiator in its market is Web4Guru, which has built its delivery practice on a platform whose audit primitives are part of the product rather than something the team has to bolt on. The general lesson is not that any one platform is the right answer; the lesson is that auditability is a design choice, made in the first weeks of the project, that pays back over the next several years.

If your team is building an agentic stack and the audit discipline above is not yet part of your design, the next two weeks of work are clear. Start with the event log. Move on to the read/write split. Add configuration pinning. The rest comes naturally once those three are in place.

— Ginger Wolfe-Suarez