Skip to content

Architecture

agentdelta is a pure-Python library with five independent modules and a thin CLI layer.

Module map

src/agentdelta/
├── trace.py        # Data model: TraceNode, TraceEdge, AgentTrace
├── embed.py        # Embedding + sliding-window alignment
├── diff.py         # Fork detection algorithm → DiffResult
├── instrument.py   # LangChain callback + record() context manager
├── report.py       # Rich / JSON / Markdown output formatters
├── cli.py          # Click CLI (thin wrapper over diff + report)
├── api.py          # FastAPI REST wrapper (pip install agentdelta[api])
└── mcp_server.py   # MCP server (pip install agentdelta[mcp])

Data flow

flowchart TD
    A[Agent run\nLangChain / custom] -->|on_llm_end\non_tool_start\non_tool_end| B[AgentdeltaCallback\ninstrument.py]
    B --> C[AgentTrace\ntrace.py\nlist of TraceNode + TraceEdge\nsaved as JSONL]
    C --> D[embed_trace\nembed.py\nall-MiniLM-L6-v2\n384-dim vectors]
    D --> E[align_traces\nembed.py\nsliding-window cosine\ngreedy 1:1 match]
    E --> F[diff_traces\ndiff.py\nper-pair status\nfork detection]
    F --> G1[print_diff\nRich terminal]
    F --> G2[to_json\nCI/CD JSON]
    F --> G3[to_markdown\nGitHub PR comment]

Trace format (JSONL)

Each trace is a .jsonl file — one JSON object per line.

{"type": "trace_meta", "run_id": "v1.0"}
{"type": "node", "id": "a3f8...", "step": 1, "node_type": "start",      "content": "What is the weather in Tokyo?"}
{"type": "node", "id": "b9c1...", "step": 2, "node_type": "llm",        "content": "I should look up the current weather."}
{"type": "node", "id": "d2e4...", "step": 3, "node_type": "tool_call",  "content": "get_weather(location='Tokyo')"}
{"type": "node", "id": "f5a7...", "step": 4, "node_type": "tool_return","content": "{\"temp\": 22, \"condition\": \"sunny\"}"}
{"type": "node", "id": "c8b2...", "step": 5, "node_type": "end",        "content": "Tokyo: 22°C, sunny."}
{"type": "edge", "source_step": 1, "target_step": 2, "edge_type": "sequence",     "label": ""}
{"type": "edge", "source_step": 2, "target_step": 3, "edge_type": "llm_decision", "label": ""}
{"type": "edge", "source_step": 3, "target_step": 4, "edge_type": "tool_call",    "label": "get_weather"}

Node IDs are content-addressed: SHA-256[:16] of "{node_type}:{content}". The same reasoning step always produces the same ID regardless of which run it appears in.

Alignment algorithm

The sliding-window alignment in embed.py:align_traces() is a greedy 1:1 matcher:

  1. For each node na in trace A at index i, search trace B nodes in the window [i-window, i+window]
  2. Find the candidate nb maximising cosine_similarity(na.embedding, nb.embedding)
  3. If score >= threshold: match (na, nb, score) and mark nb as used
  4. Else: emit (na, None, 0.0) — node removed in B
  5. Append any unmatched trace B nodes as (None, nb, 0.0) — nodes added in B

Complexity: O(n·window) per trace pair — fast for typical agent traces (< 100 steps).

Fork detection

diff_traces() labels each aligned pair:

Condition Status
score >= match_threshold (default 0.85) match
fork_threshold ≤ score < match_threshold changed
na is None added
nb is None removed

The first changed pair becomes the ForkPoint. has_regression is True iff a ForkPoint exists.

Embedding model

all-MiniLM-L6-v2 is chosen for:

  • Offline — no API key, no network call after first download
  • Fast — ~5ms per step on CPU, 384-dim output
  • Accurate — strong semantic clustering of short instruction text
  • MIT license — compatible with any downstream use