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:
- For each node
nain trace A at indexi, search trace B nodes in the window[i-window, i+window] - Find the candidate
nbmaximisingcosine_similarity(na.embedding, nb.embedding) - If
score >= threshold: match(na, nb, score)and marknbas used - Else: emit
(na, None, 0.0)— node removed in B - 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