Skip to content

Trace & Lineage

Every event records its causal ancestors in the cause field. The trace module reconstructs the full causal chain from any event back to the root goal.

Functions

kando.trace.lineage.build_lineage_index(events)

Map each event ID to its causal ancestors (parent event IDs).

Source code in kando/trace/lineage.py
5
6
7
def build_lineage_index(events: list[KandoEvent]) -> dict[str, list[str]]:
    """Map each event ID to its causal ancestors (parent event IDs)."""
    return {e.id: e.cause for e in events}

kando.trace.lineage.explain(event_id, events)

Return the full causal chain as actual event objects, in BFS order from event_id to root.

Source code in kando/trace/lineage.py
def explain(event_id: str, events: list[KandoEvent]) -> list[KandoEvent]:
    """Return the full causal chain as actual event objects, in BFS order from event_id to root."""
    index = build_lineage_index(events)
    event_map: dict[str, KandoEvent] = {e.id: e for e in events}
    chain_ids = trace(event_id, index)
    return [event_map[eid] for eid in chain_ids if eid in event_map]

kando.trace.lineage.trace(event_id, index)

Return the full causal chain from event_id back to root, in BFS order.

Source code in kando/trace/lineage.py
def trace(event_id: str, index: dict[str, list[str]]) -> list[str]:
    """Return the full causal chain from event_id back to root, in BFS order."""
    chain: list[str] = []
    seen: set[str] = set()
    queue = [event_id]
    while queue:
        eid = queue.pop(0)
        if eid in seen:
            continue
        seen.add(eid)
        chain.append(eid)
        queue.extend(index.get(eid, []))
    return chain

Usage

from kando.trace.lineage import build_lineage_index, explain

all_events = list(store.read_all())

# Full causal chain as event objects, from target back to root
chain = explain("claim-1", all_events)
for evt in chain:
    cause_str = f"← {evt.cause}" if evt.cause else "(root)"
    print(f"  {evt.id} [{evt.type}] actor={evt.actor} {cause_str}")

Output:

  claim-1 [object.created] actor=diligence.on_company_created ← ['company.created-abc123']
  company.created-abc123 [object.created] actor=cli (root)

Reading from the CLI

kando trace claim-1 --run abc123def456

Via MCP

{
  "tool": "explain_trace",
  "arguments": {"event_id": "claim-1", "run_id": "abc123def456"}
}

Design note

Traces are not observability bolted on after the fact — they are a structural output of every run. The cause list on every event is written at emission time by each responder, so the lineage graph is always complete and queryable without additional instrumentation.