Skip to main content

Chain-of-Thought Ledger

The Chain-of-Thought (CoT) Ledger provides immutable, hash-chained reasoning traces for AI agents. Every reasoning step an agent takes is recorded in an append-only ledger with SHA-256 hash chaining, creating a tamper-evident audit trail that can be replayed, verified, and analyzed.

Why Chain-of-Thought Tracking?

When AI agents make decisions that affect business operations, you need to answer:

  • What did the agent reason about? -- Full trace of observations, hypotheses, tool calls, and conclusions
  • Can the trace be trusted? -- SHA-256 hash chains prove no steps were inserted, deleted, or modified
  • What went wrong? -- Replay a session to understand failures or unexpected decisions
  • Is the agent improving? -- Compare reasoning patterns across sessions to measure quality

The CoT Ledger answers all of these by recording every reasoning step in an immutable, verifiable format.

Architecture

Agent → log_reasoning_step → CotWriter
├── Append to _hatidata_cot table (DuckDB)
├── Update per-session hash chain
└── Conditionally dispatch for embedding

Analyst → replay_decision → CotReader
├── Load session trace from DuckDB
└── Verify hash chain integrity

Append-Only Enforcement

The CotAppendOnlyEnforcer intercepts DML statements targeting _hatidata_cot tables and blocks any operation that would modify existing records:

  • INSERT -- Allowed (append new steps)
  • UPDATE -- Blocked
  • DELETE -- Blocked
  • TRUNCATE -- Blocked
  • DROP -- Blocked

This enforcement happens at the proxy level, before DuckDB execution, so even direct SQL connections cannot tamper with the ledger.

Trace Schema

Each reasoning step is stored as an AgentTraceRow with 18 fields:

FieldTypeDescription
trace_idUUIDUnique identifier for this trace entry
org_idVARCHAROrganization identifier
agent_idVARCHARAgent that produced this step
session_idVARCHARGroups steps into a reasoning session
step_indexINTEGEROrdinal position within the session (0-based)
step_typeVARCHAROne of 12 step type variants (see below)
contentTEXTThe reasoning content (natural language or structured)
input_dataJSONInput to this step (tool arguments, observations)
output_dataJSONOutput from this step (tool results, decisions)
confidenceFLOATAgent's self-reported confidence (0.0 to 1.0)
duration_msBIGINTHow long this step took in milliseconds
token_countINTEGERToken usage for LLM-based steps
modelVARCHARModel identifier (e.g., gpt-4o, claude-3.5-sonnet)
metadataJSONArbitrary additional context
prev_hashVARCHARSHA-256 hash of the previous step (empty for step 0)
current_hashVARCHARSHA-256 hash of this step's content + prev_hash
has_embeddingBOOLEANWhether an embedding exists for this step
created_atTIMESTAMPWhen this step was recorded

Step Types

The step_type field uses one of 12 variants that cover the full agent reasoning lifecycle:

Step TypeDescriptionAlways Embedded?
ObservationAgent observes data or environment stateNo
HypothesisAgent forms a hypothesis about the dataNo
ToolCallAgent invokes an external toolNo
ToolResultResult returned from a tool callNo
ReasoningInternal reasoning or analysisNo
DecisionAgent makes a decisionYes
ActionAgent takes an action based on a decisionYes
ErrorAn error occurred during reasoningYes
CorrectionAgent corrects a previous stepYes
SummaryAgent summarizes findingsNo
PlanStepA step in a multi-step planNo
FinalAnswerThe final output of the reasoning sessionYes

Hash Chaining

Each step's current_hash is computed as:

current_hash = SHA-256(step_content + prev_hash)

Where prev_hash is the current_hash of the previous step in the session (empty string for the first step). This creates a linked chain where modifying any step invalidates all subsequent hashes.

Verification

The CotReader::verify_chain() method walks the entire session and recomputes every hash:

For each step i in session:
expected = SHA-256(step[i].content + step[i-1].current_hash)
if expected != step[i].current_hash:
return ChainBroken { step_index: i }

If all hashes match, the chain is verified as intact. If any hash mismatches, the exact step where tampering occurred is identified.

Core Components

CotWriter

The CotWriter manages the write path for reasoning traces:

  • Maintains a DashMap<String, String> mapping session_id to the latest hash in the chain
  • On each log_reasoning_step call, computes the new hash, appends the row to DuckDB, and updates the in-memory chain head
  • Handles concurrent sessions safely via DashMap's lock-free per-key operations

CotReader

The CotReader provides read and verification operations:

MethodDescription
replay_session(session_id)Load all steps for a session in order
get_trace(trace_id)Load a single trace entry by ID
list_traces(agent_id, limit)List recent sessions for an agent
verify_chain(session_id)Verify hash chain integrity for a session

Embedding Sampling

Not every reasoning step needs a vector embedding (which would be expensive at high throughput). The CoT system uses configurable embedding sampling:

  • Sampling rate -- Default 10% of steps are embedded (configurable via HATIDATA_COT_EMBEDDING_SAMPLE_RATE)
  • Critical step types -- Steps of type Decision, Action, Error, Correction, and FinalAnswer are always embedded regardless of sampling rate
  • Purpose -- Embedded steps can be found via semantic search (e.g., "find all decisions about pricing")

MCP Tools

log_reasoning_step

Record a single reasoning step in the ledger.

Input:

{
"session_id": "analysis-session-42",
"step_type": "Reasoning",
"content": "The revenue data shows a clear upward trend in Q4, driven primarily by the enterprise segment which grew 23% QoQ.",
"input_data": {
"query": "SELECT segment, SUM(revenue) FROM orders WHERE quarter = 'Q4' GROUP BY 1"
},
"output_data": {
"enterprise": 4500000,
"mid_market": 2100000,
"smb": 890000
},
"confidence": 0.92,
"model": "gpt-4o"
}

Output:

{
"trace_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"step_index": 3,
"current_hash": "a3f2b8c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1"
}

replay_decision

Replay a complete reasoning session to understand how a decision was made.

Input:

{
"session_id": "analysis-session-42",
"verify_chain": true
}

Output:

{
"session_id": "analysis-session-42",
"agent_id": "analyst-agent",
"step_count": 7,
"chain_valid": true,
"steps": [
{
"step_index": 0,
"step_type": "Observation",
"content": "User requested Q4 revenue analysis...",
"created_at": "2025-01-15T10:30:00Z"
},
{
"step_index": 1,
"step_type": "ToolCall",
"content": "Querying orders table for Q4 data...",
"input_data": {"sql": "SELECT ..."},
"created_at": "2025-01-15T10:30:01Z"
}
]
}

get_session_history

List recent reasoning sessions for an agent.

Input:

{
"agent_id": "analyst-agent",
"limit": 20
}

Output:

[
{
"session_id": "analysis-session-42",
"step_count": 7,
"first_step_at": "2025-01-15T10:30:00Z",
"last_step_at": "2025-01-15T10:30:15Z",
"chain_valid": true
}
]

Usage Example

from hatidata_agent import HatiDataAgent

agent = HatiDataAgent(
host="your-org.proxy.hatidata.com",
agent_id="analyst",
password="hd_live_your_api_key",
)

# Start a reasoning session
session_id = "revenue-analysis-q4"

# Log each step as the agent reasons
agent.log_reasoning_step(
session_id=session_id,
step_type="Observation",
content="User asked for Q4 revenue breakdown by segment",
)

agent.log_reasoning_step(
session_id=session_id,
step_type="ToolCall",
content="Querying orders table",
input_data={"sql": "SELECT segment, SUM(revenue) FROM orders WHERE quarter='Q4' GROUP BY 1"},
)

agent.log_reasoning_step(
session_id=session_id,
step_type="Reasoning",
content="Enterprise grew 23% QoQ while SMB declined 5%. The growth is concentrated in 3 accounts.",
confidence=0.88,
)

agent.log_reasoning_step(
session_id=session_id,
step_type="FinalAnswer",
content="Q4 revenue was $7.49M, up 15% QoQ. Enterprise drove the growth at +23%.",
confidence=0.95,
)

# Later: replay and verify
trace = agent.replay_decision(session_id, verify_chain=True)
assert trace["chain_valid"] is True

Configuration

VariableDefaultDescription
HATIDATA_COT_ENABLEDtrueEnable/disable CoT ledger
HATIDATA_COT_EMBEDDING_SAMPLE_RATE0.1Fraction of steps to embed (0.0 to 1.0)
HATIDATA_COT_MAX_CONTENT_LENGTH65536Maximum content length per step (bytes)
HATIDATA_COT_RETENTION_DAYS90How long to retain CoT records

Next Steps

Stay in the loop

Product updates, engineering deep-dives, and agent-native insights. No spam.