DEV Community

Cover image for Causal Lineage and Session Replay with ZizkaDB
Mir Arshad Ali Talpur
Mir Arshad Ali Talpur

Posted on

Causal Lineage and Session Replay with ZizkaDB

If you've shipped an LLM agent to production, you know the failure mode: a customer says the bot gave a wrong answer, you open your logs, and you see a wall of spans that tell you what happened but not why. The prompt changed three deploys ago. The agent skipped a tool call. A retrieval step pulled a stale document. Nothing in a flat trace tells you the causal chain that led to the bad output.

ZizkaDB is an open-source operational database built specifically for this problem. Instead of storing spans like a tracing tool, it stores agent decisions as a graph, where each event points to the event that caused it, plus session-level replay and drift detection against a baseline. This post walks through the two features that make it different from a generic tracing setup: causal lineage (why()) and session replay, with working code.

Why not just use a tracer?

Distributed tracing tools (Langfuse, LangSmith, Phoenix) give you a span tree: this call started, this call ended, here's the latency. That's useful for performance debugging. It's much weaker for behavioral debugging, where the question isn't how long did this take but what earlier decision caused this one. ZizkaDB models that explicitly by making every logged event optionally declare its parent_id, turning a session into a directed acyclic graph of decisions instead of a list of timestamps.

Setup

Self-hosting is one script:

bash
git clone https://github.com/Zizka-ai/ZizkaDB
cd ZizkaDB
bash scripts/quickstart.sh

This pulls the pre-built images, starts the API on localhost:8000, and opens a dashboard at localhost:3001 with no signup required for local dev. If you'd rather skip the clone entirely:

bash
curl -fsSL https://raw.githubusercontent.com/Zizka-ai/ZizkaDB/main/scripts/quickstart-remote.sh | bash

Install the Python SDK:

bash
pip install "zizkadb-sdk>=0.2.7"

The SDK is stateless by design: you pass agent, session_id, and event_id explicitly on every call rather than relying on hidden global state. That matters once you're running multiple agents or worker processes against the same store.

Logging events with parent links

Here's the core primitive. Every call to db.log() returns an event_id, and you pass that as parent_id on whatever event it caused:

python
import asyncio
from zizkadb import ZizkaDB

async def main():
async with ZizkaDB(host="http://localhost:8000") as db:
user_msg = await db.log(
agent="support-bot",
session_id="session-4821",
event="user_message",
data={"text": "How long do refunds take?"},
)

    retrieval = await db.log(
        agent="support-bot",
        session_id="session-4821",
        event="tool_call",
        data={"tool": "search_policy_docs", "query": "refund window"},
        parent_id=user_msg.event_id,
    )

    response = await db.log(
        agent="support-bot",
        session_id="session-4821",
        event="assistant_response",
        data={"text": "Refunds take 30 days."},
        parent_id=retrieval.event_id,
    )
Enter fullscreen mode Exit fullscreen mode

asyncio.run(main())

Three events, two causal edges: the tool call was caused by the user message, and the response was caused by the tool call. That chain is the whole point. It's what lets you ask why the agent said this and get an actual answer instead of a timestamp-sorted guess.

Causal lineage: why()

Given any event_id, why() walks the parent chain backward and returns the decision path that produced it:

python
result = await db.why(response.event_id)
result.print()
assistant_response "Refunds take 30 days."
↑ caused by
tool_call search_policy_docs("refund window") → outdated_faq_chunk.md
↑ caused by
user_message "How long do refunds take?"

This is the difference between a span tree and a lineage graph in practice: instead of scanning a trace for the surrounding calls and inferring causation yourself, you get the causal chain directly. In the incident this is modeled on, why() on the bad response is what surfaces that search_policy_docs returned an outdated FAQ chunk instead of the current policy doc: the actual root cause, not just a tool being called.

Session replay

why() traces one decision. Session replay reconstructs the entire session: every message, tool call, and response in order, with the state the agent had at each point.

python
session = await db.replay(agent="support-bot", session_id="session-4821")

for event in session.events:
print(f"{event.timestamp} {event.event} {event.data}")
14:01:58 session_start {}
14:02:09 user_message {"text": "How long do refunds take?"}
14:02:11 tool_call {"tool": "search_policy_docs", "result": "outdated_faq_chunk.md"}
14:02:12 assistant_response {"text": "Refunds take 30 days."}

The dashboard renders this same data as a timeline you can step through, which is where it's genuinely faster than grepping logs: you see exactly what the agent knew, including which documents it retrieved and which tool results it had, at the moment it generated the wrong answer. That's time travel over logged state rather than replaying UI interactions the way session-replay tools for web apps do; here the format is per-event input/output data.

Catching it before a customer does: drift baselines

Lineage and replay are for root-causing an incident you already know about. baseline() is for catching the regression before that. Once you have enough sessions logged, you snapshot known-good behavior and compare new sessions against it:

python
baseline = await db.baseline(agent="support-bot", label="pre-prompt-v2")

after deploying a new prompt version

drift = await db.check_drift(agent="support-bot", against="pre-prompt-v2")

if drift.flagged:
for change in drift.changes:
print(f"Drift on {change.topic}: {change.summary}")

In the ZizkaDB docs' worked example, this is exactly what flags the refund-policy regression: check_drift reports that refund answers changed shape after the prompt v2 deploy, pointing you at why() for the specific session before a customer files a ticket.

REST, if you're not in Python or TS

Everything above also has a plain REST API, useful if your agent runtime isn't Python or TypeScript:

bash
curl -s -H "Authorization: Bearer zizkadb_dev_local" \
-H "Content-Type: application/json" \
-d '{
"agent": "support-bot",
"session_id": "session-4821",
"event": "tool_call",
"data": {"tool": "search_policy_docs"},
"parent_id": "evt_9f2a"
}' \
http://localhost:8000/v1/events

Swagger docs are served at http://localhost:8000/swagger on self-hosted instances. There's also first-party support for LangChain (ZizkaDBCallbackHandler), CrewAI (ZizkaDBCrewLogger), and an MCP server for Cursor/Claude Desktop if you want lineage and replay available as tools inside your editor rather than only in the dashboard.

Where this fits

If you already have a tracer for latency and cost, you probably don't need to rip it out. What ZizkaDB is solving is a narrower, sharper problem: when an agent's behavior is wrong, not just slow, why() and session replay get you from customer complaint to root cause without reading logs. The parent_id graph is the whole mechanism, and it's simple enough to bolt onto an existing agent in an afternoon: three extra db.log() calls in the example above is most of the integration work.

Repo: ZizkaDB (AGPL, self-host free). Managed cloud with a hosted dashboard is at db.zizka.ai if you'd rather not run Docker.

Top comments (0)