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:
| Field | Type | Description |
|---|---|---|
trace_id | UUID | Unique identifier for this trace entry |
org_id | VARCHAR | Organization identifier |
agent_id | VARCHAR | Agent that produced this step |
session_id | VARCHAR | Groups steps into a reasoning session |
step_index | INTEGER | Ordinal position within the session (0-based) |
step_type | VARCHAR | One of 12 step type variants (see below) |
content | TEXT | The reasoning content (natural language or structured) |
input_data | JSON | Input to this step (tool arguments, observations) |
output_data | JSON | Output from this step (tool results, decisions) |
confidence | FLOAT | Agent's self-reported confidence (0.0 to 1.0) |
duration_ms | BIGINT | How long this step took in milliseconds |
token_count | INTEGER | Token usage for LLM-based steps |
model | VARCHAR | Model identifier (e.g., gpt-4o, claude-3.5-sonnet) |
metadata | JSON | Arbitrary additional context |
prev_hash | VARCHAR | SHA-256 hash of the previous step (empty for step 0) |
current_hash | VARCHAR | SHA-256 hash of this step's content + prev_hash |
has_embedding | BOOLEAN | Whether an embedding exists for this step |
created_at | TIMESTAMP | When this step was recorded |
Step Types
The step_type field uses one of 12 variants that cover the full agent reasoning lifecycle:
| Step Type | Description | Always Embedded? |
|---|---|---|
Observation | Agent observes data or environment state | No |
Hypothesis | Agent forms a hypothesis about the data | No |
ToolCall | Agent invokes an external tool | No |
ToolResult | Result returned from a tool call | No |
Reasoning | Internal reasoning or analysis | No |
Decision | Agent makes a decision | Yes |
Action | Agent takes an action based on a decision | Yes |
Error | An error occurred during reasoning | Yes |
Correction | Agent corrects a previous step | Yes |
Summary | Agent summarizes findings | No |
PlanStep | A step in a multi-step plan | No |
FinalAnswer | The final output of the reasoning session | Yes |
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>mappingsession_idto the latest hash in the chain - On each
log_reasoning_stepcall, 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:
| Method | Description |
|---|---|
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, andFinalAnswerare 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
| Variable | Default | Description |
|---|---|---|
HATIDATA_COT_ENABLED | true | Enable/disable CoT ledger |
HATIDATA_COT_EMBEDDING_SAMPLE_RATE | 0.1 | Fraction of steps to embed (0.0 to 1.0) |
HATIDATA_COT_MAX_CONTENT_LENGTH | 65536 | Maximum content length per step (bytes) |
HATIDATA_COT_RETENTION_DAYS | 90 | How long to retain CoT records |
Next Steps
- Agent Memory -- Persistent memory for agents
- Semantic Triggers -- Trigger actions based on reasoning patterns
- State Branching -- Explore alternative reasoning paths