
Part 2 applies zero-trust AI architecture and identity propagation to agent audit trails, showing how JSON Schema validation for LLMs turns due diligence reasoning into structured, governable, and partner-verifiable evidence.
Part 1 argued that we must move from generic API logs to identity-bound, tamper-evident, reasoning-aware audit trails if we want AI-driven due diligence to be defensible.
Now we’ll shape that into a concrete architecture, focusing on identity attribution, layered logging, and structured reasoning artifacts.
Traditional observability metrics, traces, logs helps SREs debug latency issues but fails to answer “Which associate or agent decided this clause was high risk for case DD‑2026‑042?”
An audit-trail-first architecture starts from human and agent identity and works outward to policy and reasoning.
Regulators and auditors want to know who did what, not which Kubernetes pod handled the request.
Identity-bound logging binds every agent action to a human principal, a concrete agent identity, and a case or tenant context.
A minimal identity envelope for agent actions might look like this:
{
"actor": {
"user_id": "usr_7f1c...",
"role": "PARTNER",
"idp": "azure-ad",
"session_id": "sess_f92a..."
},
"agent": {
"agent_id": "ag_contract_risk_v3",
"version": "3.2.1",
"policy_bundle_id": "pol_bundle_2026_01"
},
"context": {
"case_id": "DD-2026-042",
"client_id": "cli_1234",
"jurisdiction": ["EU", "US-DE"],
"regimes": ["EU_AI_ACT", "SOX", "FATF"]
}
}
Identity-focused guides emphasise that audit events should capture human, agent, and context identifiers on every entry to support investigations and compliance audits.
Several recent design guides propose multilayer logging for AI agents, capturing not just technical events but decisions, policies, and inter-agent communication.
But layering isn't just a categorisation exercise:each layer has a different destination system, because observability data and compliance evidence have fundamentally different integrity requirements.
Industry guidance is blunt about this distinction: tools like LangSmith, Logfire, and OpenTelemetry produce mutable debugging telemetry: spans and runs you're expected to sample, redact, or delete on a retention policy: while an audit trail must answer a narrower, adversarial question: for this sensitive action, which principal performed it, and can you prove the record wasn't edited afterward? A SOC 2 badge on your observability vendor attests to their controls, not to the integrity of the compliance record your own agent produces.
For due diligence, you can think in five layers, split across two storage destinations:

Route to standard observability stacks (Elasticsearch, Datadog, or equivalent APM/log aggregation):
1. Application layer – HTTP requests, status codes, latencies. High-volume, useful for performance debugging, safe to sample or expire on a short retention window.
2. Tool layer – Every vector search, DB query, or external API call with parameters and redacted results. Valuable for prompt/tool debugging, but not itself legal evidence of what the agent decided.
Route to the tamper-evident Audit Ledger (hash-chained, append-only, anchored to WORM storage):
3. Decision layer – Which tools were selected, intermediate conclusions, risk score transitions, and branch choices. This is compliance-critical: a decision log must record why a policy engine or agent chose a path, not just that a call occurred, because that "why" is what auditors and regulators actually request during an investigation.
4. Policy layer – Policy evaluations, allow/deny decisions, escalations, and justifications, including the specific policy version and rule that fired. This carries direct legal weight: authorization decision logs are frequently the evidentiary artifact used to prove that only authorised actions occurred within compliance requirements.
5. Reasoning layer – Structured reasoning artifacts mapping evidence to scores and domain concepts. This is the AI's "legal opinion" in machine-readable form, and it must be as tamper-resistant as the decision and policy layers it depends on.
The reason for this split isn't arbitrary convenience it's a durability and evidentiary distinction. Application and tool logs are telemetry: mutable, sampled, and expected to be edited or dropped as part of normal operations debugging. Decision, policy, and reasoning logs are evidence: each entry should be independently attributable to a principal, chained so that edits are cryptographically detectable, and retained on a compliance schedule rather than an operational one.
A practical architectural rule some teams enforce: the compliance log must be written by a system separate from the one making the AI call, so it survives selective logging, agent misbehavior, or a crash that wipes application-layer telemetry. If your Decision, Policy, and Reasoning layers only exist inside the same mutable database your application writes to, a sufficiently privileged insider:or a compromised agent can quietly rewrite the story after the fact; routing them into the hash-chained ledger (and anchoring that ledger's tip externally, as covered later) closes that gap.
An IETF draft for autonomous agent audit trails shows how a single session’s log can include all of these, including every tool call, policy evaluation, and final action.
Security and compliance blogs extend this with hash-chained storage and external anchoring for enterprise-grade tamper evidence.
Concept bottleneck work in legal AI provides a practical blueprint: force the model to emit scores for a fixed set of domain concepts in a strict schema.
Instead of free-form reasoning, you get a stable vector you can compare, monitor, and log.
For a contract risk agent, a reasoning schema might look like:
{
"$id": "https://example.com/schemas/contractRisk.v1.json",
"type": "object",
"required": ["clause_id", "risk_scores", "evidence_refs", "schema_version", "policy_version"],
"properties": {
"schema_version": { "type": "string", "const": "contractRisk.v1" },
"policy_version":
{ "type": "string",
"description": "ID of the risk playbook/policy bundle this scoring was evaluated against, e.g. MNA_EU_US_2026_01.",
"pattern": "^[A-Za-z0-9_.-]+$"
},
"clause_id": { "type": "string" },
"risk_scores": {
"type": "object",
"required": ["counterparty", "jurisdiction", "operational", "compliance", "financial"],
"properties": {
"counterparty": { "type": "number", "minimum": 0, "maximum": 1 },
"jurisdiction": { "type": "number", "minimum": 0, "maximum": 1 },
"operational": { "type": "number", "minimum": 0, "maximum": 1 },
"compliance": { "type": "number", "minimum": 0, "maximum": 1 },
"financial": { "type": "number", "minimum": 0, "maximum": 1 }
}
},
"evidence_refs": {
"type": "array",
"items": {
"type": "object",
"required": ["doc_id", "page", "offset"],
"properties": {
"doc_id": { "type": "string" },
"page": { "type": "integer", "minimum": 1 },
"offset": { "type": "integer", "minimum": 0 }
}
}
}
}
}
This style of JSON schema is directly compatible with validation approaches described in structured logging and AI governance tutorials, which stress schema versioning for retroactive auditability.
The jsonschema library above is correct but re-parses and re-walks the schema on every call, which becomes a measurable bottleneck once you're validating thousands of agent reasoning artifacts per hour across a due diligence portfolio. In production, most teams instead use a compiled type-validation framework: Pydantic v2 in Python, or Zod/TypeBox in TypeScript.
Pydantic v2's validation core (pydantic-core) is written in Rust and pre-compiles validators ahead of time, delivering roughly 5–50x the throughput of pure-Python JSON Schema validation and handling around 20,000 complex model validations per second per CPU thread. The same principle applies on the TypeScript side: Zod and TypeBox generate optimised validators at schema-definition time rather than interpreting a schema object on every request.
from pydantic import BaseModel, Field, ValidationError, field_validator
from typing import Literal
class EvidenceRef(BaseModel):
doc_id: str
page: int = Field(ge=1)
offset: int = Field(ge=0)
class RiskScores(BaseModel):
counterparty: float = Field(ge=0, le=1)
jurisdiction: float = Field(ge=0, le=1)
operational: float = Field(ge=0, le=1)
compliance: float = Field(ge=0, le=1)
financial: float = Field(ge=0, le=1)
class ContractRiskArtifact(BaseModel):
schema_version: Literal["contractRisk.v1"]
policy_version: str = Field(pattern=r"^[A-Za-z0-9_.-]+$")
clause_id: str
risk_scores: RiskScores
evidence_refs: list[EvidenceRef]
Critically, when validation fails, the system should not simply raise an exception and drop the reasoning artifact into a dead-letter queue. Doing so throws away a recoverable model output and logs a hard failure for what is often a trivial formatting slip. Instead, route the failure into a Structured Repair Loop: feed the exact schema error diff back to the LLM as a targeted correction instruction, let it re-emit a corrected payload, re-validate, and only escalate to a logged failure if the loop is exhausted.
import json
from pydantic import ValidationError
MAX_REPAIR_ATTEMPTS = 2
def validate_with_repair_loop(
raw_output: dict,
llm_repair_fn, # callable: (bad_payload, error_diff) -> dict
audit: "AuditLog",
case_id: str,
agent_id: str,
) -> ContractRiskArtifact:
attempt = 0
payload = raw_output
while True:
try:
return ContractRiskArtifact.model_validate(payload)
except ValidationError as e:
attempt += 1
error_diff = e.errors() # structured: [{loc, msg, type}, ...]
audit.append(
case_id=case_id,
agent_id=agent_id,
actor_id="sys_validator",
event_type="REASONING_VALIDATION_RETRY" if attempt <= MAX_REPAIR_ATTEMPTS else "REASONING_VALIDATION_FAILED",
payload={"attempt": attempt, "error_diff": error_diff},
)
if attempt > MAX_REPAIR_ATTEMPTS:
raise ValueError(
f"Reasoning artifact failed validation after {MAX_REPAIR_ATTEMPTS} repair attempts: {error_diff}"
) from e
# Feed the exact schema diff back to the LLM, not a generic retry
payload = llm_repair_fn(payload, error_diff)
This mirrors the "validate, repair, retry" pattern documented across LLM structured-output tooling: send the model the specific field paths and reasons it failed on, cap retries at two or three attempts since success probability drops sharply after that, and only log a hard failure once the budget is exhausted. Every retry attempt : not just the final failure ; is itself written to the audit ledger as a REASONING_VALIDATION_RETRY event, so a partner reviewing the case later can see that the agent's output required correction and exactly what was wrong, rather than seeing only a clean final artifact with no record of the struggle behind it.
This ensures your audit trail only includes well-typed, versioned, policy-bound reasoning artifacts ;validated at compiled speed, self-corrected where possible, and only escalated to a genuine failure when the LLM cannot converge on a schema-valid payload
Structured JSON is great for machines; partners still want readable narratives. Compliance and AML tooling often solves this by rendering side-by-side views: structured scores on one side, narrative explanation referencing those scores and evidence on the other.
A simple pattern is to generate the narrative from the structured reasoning, not from raw text, so that the explanation is constrained to what is logged:
def render_partner_view(reasoning: dict) -> str:
scores = reasoning["risk_scores"]
ev = reasoning["evidence_refs"]
ev_summary = ", ".join(f"{e['doc_id']}@p{e['page']}" for e in ev[:3])
return (
f"Counterparty risk: {scores['counterparty']:.2f}. "
f"Jurisdiction risk: {scores['jurisdiction']:.2f}. "
f"Operational risk: {scores['operational']:.2f}. "
f"Evidence sampled from {ev_summary}."
)
This mirrors patterns in transaction monitoring tools where investigatory narratives reference structured fields and evidence identifiers recorded in the audit trail.
For a TypeScript-based agent platform, you can implement a logging wrapper that decorates each agent step with identity, context, and reasoning artifacts:
// types.ts
export interface AgentContext {
caseId: string;
userId: string;
userRole: string;
agentId: string;
agentVersion: string;
policyBundleId: string;
}
export interface ReasoningArtifact {
schemaVersion: string;
riskScores: Record<string, number>;
evidenceRefs: Array<{ docId: string; page: number; offset: number }>;
}
export interface AuditRecord {
eventId: string;
occurredAt: string;
context: AgentContext;
eventType: string;
inputSummary: string;
outputSummary: string;
reasoning?: ReasoningArtifact;
}
// auditLogger.ts
import { randomUUID } from "crypto";
import { AgentContext, AuditRecord, ReasoningArtifact } from "./types";
export interface AuditSink {
append(record: AuditRecord): Promise<void>;
}
export function withAuditTrail<TArgs extends any[], TResult>(
ctx: AgentContext,
sink: AuditSink,
eventType: string,
fn: (...args: TArgs) => Promise<{ result: TResult; reasoning?: ReasoningArtifact }>
) {
return async (...args: TArgs): Promise<TResult> => {
const started = new Date().toISOString();
const { result, reasoning } = await fn(...args);
const record: AuditRecord = {
eventId: randomUUID(),
occurredAt: started,
context: ctx,
eventType,
inputSummary: JSON.stringify(args).slice(0, 512),
outputSummary: JSON.stringify(result).slice(0, 512),
reasoning,
};
await sink.append(record);
return result;
};
}
This pattern aligns with modern GenAI logging patterns that recommend capturing model calls, tool calls, and human review decisions as structured records.
Governance frameworks for agentic workflows emphasise explicit approval gates for high-risk or ambiguous decisions, mapped to roles and escalation paths.
In a due diligence platform, these gates become part of the orchestration topology, not a bolt-on review step.
A simple gate record could look like this:
{
"gate_id": "gate_material_risk_review",
"case_id": "DD-2026-042",
"agent_event_id": "evt_a1b2",
"required_role": "PARTNER",
"reviewer_id": "usr_partner_01",
"decision": "APPROVED",
"reviewed_at": "2026-06-25T11:20:00Z",
"review_rationale": "Classification aligns with internal playbook; evidence sufficient."
}
Gate outcomes should be written into the same audit ledger as agent decisions, producing a unified view of human and machine behavior over the case lifecycle.
At this point, the reference architecture for a controlled agentic due diligence platform looks like:
In Part 3, we’ll apply these patterns to concrete workflows in M&A, contract intelligence, SoW/KYC, and audit investigations, and show how to instrument them end to end.