agent-trajectory-diff 0.3.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/.gitignore +1 -0
- agent_trajectory_diff-0.5.0/PKG-INFO +228 -0
- agent_trajectory_diff-0.5.0/README.md +208 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/pyproject.toml +3 -2
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/__init__.py +1 -1
- agent_trajectory_diff-0.5.0/src/agentdiff/ci/approve.py +118 -0
- agent_trajectory_diff-0.5.0/src/agentdiff/cli.py +722 -0
- agent_trajectory_diff-0.5.0/src/agentdiff/config.py +246 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/engine/aligner.py +93 -0
- agent_trajectory_diff-0.5.0/src/agentdiff/engine/comparator.py +236 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/engine/explanations.py +13 -9
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/engine/loop_detector.py +50 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/engine/metrics.py +7 -1
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/engine/tree.py +16 -3
- agent_trajectory_diff-0.5.0/src/agentdiff/governance.py +135 -0
- agent_trajectory_diff-0.5.0/src/agentdiff/init_wizard.py +428 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/loader.py +45 -0
- agent_trajectory_diff-0.5.0/src/agentdiff/models/envelope.py +116 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/models/report.py +50 -0
- agent_trajectory_diff-0.5.0/src/agentdiff/recorder.py +158 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/reporters/markdown.py +23 -0
- agent_trajectory_diff-0.5.0/src/agentdiff/reporters/pr.py +156 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/reporters/terminal.py +34 -2
- agent_trajectory_diff-0.5.0/src/agentdiff/staleness.py +61 -0
- agent_trajectory_diff-0.5.0/src/agentdiff/testing/__init__.py +13 -0
- agent_trajectory_diff-0.5.0/src/agentdiff/testing/assertions.py +237 -0
- agent_trajectory_diff-0.3.0/PKG-INFO +0 -264
- agent_trajectory_diff-0.3.0/README.md +0 -244
- agent_trajectory_diff-0.3.0/src/agentdiff/cli.py +0 -289
- agent_trajectory_diff-0.3.0/src/agentdiff/config.py +0 -139
- agent_trajectory_diff-0.3.0/src/agentdiff/engine/comparator.py +0 -101
- agent_trajectory_diff-0.3.0/src/agentdiff/reporters/pr.py +0 -75
- agent_trajectory_diff-0.3.0/src/agentdiff/testing/__init__.py +0 -5
- agent_trajectory_diff-0.3.0/src/agentdiff/testing/assertions.py +0 -102
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/LICENSE +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/__main__.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/adapters/__init__.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/adapters/_iso.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/adapters/_messages.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/adapters/base.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/adapters/crewai.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/adapters/generic.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/adapters/langfuse.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/adapters/langgraph.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/adapters/langsmith.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/adapters/openai_agents.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/adapters/openinference.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/adapters/registry.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/ci/baseline.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/ci/github.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/engine/__init__.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/engine/benchmark.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/engine/suite.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/models/__init__.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/models/step.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/models/trace.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/py.typed +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/pytest_plugin.py +0 -0
- {agent_trajectory_diff-0.3.0 → agent_trajectory_diff-0.5.0}/src/agentdiff/reporters/__init__.py +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agent-trajectory-diff
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: A developer-first package to evaluate and find regressions in agent trajectories
|
|
5
|
+
Project-URL: Homepage, https://github.com/kerrshift/agentdiff
|
|
6
|
+
Project-URL: Repository, https://github.com/kerrshift/agentdiff
|
|
7
|
+
Author-email: Sahil Gangurde <sahilgangurde08@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: networkx>=3.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0
|
|
16
|
+
Requires-Dist: rich>=13.0
|
|
17
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
18
|
+
Requires-Dist: typer>=0.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
<div align="center">
|
|
22
|
+
|
|
23
|
+
<img src="docs/assets/logo.svg" alt="AgentDiff Logo" width="84" height="84" />
|
|
24
|
+
|
|
25
|
+
# AgentDiff
|
|
26
|
+
|
|
27
|
+
**Catch silent cost surges and broken agent loops before they ship.**
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<a href="https://github.com/kerrshift/agentdiff/actions/workflows/ci.yml"><img src="https://github.com/kerrshift/agentdiff/actions/workflows/ci.yml/badge.svg" alt="CI Build" /></a>
|
|
31
|
+
<a href="https://pypi.org/project/agent-trajectory-diff/"><img src="https://img.shields.io/pypi/v/agent-trajectory-diff.svg?color=10b981" alt="PyPI version" /></a>
|
|
32
|
+
<a href="https://pypi.org/project/agent-trajectory-diff/"><img src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg" alt="Python Versions" /></a>
|
|
33
|
+
<a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/badge/code%20style-ruff-000000.svg" alt="Code Style: Ruff" /></a>
|
|
34
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
|
|
35
|
+
<a href="https://agentdiff.app"><img src="https://img.shields.io/badge/docs-agentdiff.app-emerald.svg" alt="Website" /></a>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install agent-trajectory-diff
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
[Website & Interactive Docs](https://agentdiff.app) · [Cookbooks](cookbooks/) · [Live Demo Repo](https://github.com/lostmartian/agentdiff-demo) · [Changelog](CHANGELOG.md)
|
|
43
|
+
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
**AgentDiff** is a developer-first Python library, pytest plugin, and CLI for **regression testing multi-turn, tool-using AI agents by comparing execution paths (trajectories) head-to-head.**
|
|
47
|
+
|
|
48
|
+
When you change a prompt, tweak a system instruction, or upgrade an LLM, traditional assertions only verify that the final string matches. They miss the silent failures: **the agent took 5 extra tool calls, burned 3× the tokens, entered an infinite retry loop, or drifted from the verified execution path.**
|
|
49
|
+
|
|
50
|
+
AgentDiff aligns candidate execution DAGs against committed golden baselines in `<10ms` without calling any paid LLM judges.
|
|
51
|
+
|
|
52
|
+
## Highlights
|
|
53
|
+
|
|
54
|
+
- **Deterministic Graph Diffing:** Topological DAG alignment and Longest Common Subsequence (LCS) step comparison in `<10ms` with zero paid LLM-judge calls.
|
|
55
|
+
- **Statistical Baselines & Variance Bands:** Capture N-run envelopes (`record --runs 3`) so non-deterministic agents don't flake CI on harmless jitter.
|
|
56
|
+
- **Zero-Config Setup (`agentdiff init`):** Auto-detects LangGraph, CrewAI, OpenAI Agents SDK, or OpenTelemetry and writes `agentdiff.toml` + CI workflow in seconds.
|
|
57
|
+
- **In-PR Interactive Blessings (`/agentdiff approve`):** Reviewers bless intended trajectory improvements from PR comments as `agentdiff-ci[bot]`.
|
|
58
|
+
- **100% Local & Air-Gapped:** Zero telemetry, no cloud accounts, no outbound network calls during diffs. Raw prompts and tool outputs stay local.
|
|
59
|
+
- **Drop-in CI Merge Gate:** Native exit codes (`0` pass / `1` regression fail) and automated GitHub Action PR comments with human-first verdicts.
|
|
60
|
+
- **Universal Telemetry Adapters:** Seamlessly diff traces from **LangGraph**, **CrewAI**, **OpenAI Agents SDK**, **Langfuse**, **LangSmith**, **OpenInference / OpenTelemetry**, or generic JSON.
|
|
61
|
+
|
|
62
|
+
## Quickstart
|
|
63
|
+
|
|
64
|
+
### 1. Initialize with `agentdiff init`
|
|
65
|
+
Auto-detect your agent framework and generate your configuration + CI workflow:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
agentdiff init --scenario customer_support --runs 3 --with-approve
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. Record a Statistical Baseline Envelope
|
|
72
|
+
Record an N-run baseline envelope from any agent function without writing boilerplate telemetry:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
agentdiff record my_agent:run \
|
|
76
|
+
--input '{"query": "summarize repo"}' \
|
|
77
|
+
--runs 3 \
|
|
78
|
+
--out baselines/customer_support.envelope.json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 3. Compare Traces in CLI
|
|
82
|
+
Compare candidate runs against your baseline envelope:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
agentdiff diff baselines/customer_support.envelope.json traces/candidate.json --fail-on-regression
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 4. Pytest Regression Testing
|
|
89
|
+
Enforce trajectory parity directly in your test suite:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import pytest
|
|
93
|
+
from agentdiff import load_trace, compare
|
|
94
|
+
from agentdiff.testing import assert_no_regressions
|
|
95
|
+
|
|
96
|
+
def test_agent_refactor_efficiency():
|
|
97
|
+
# Load traces (auto-detects telemetry source format)
|
|
98
|
+
baseline = load_trace("tests/baselines/golden.json")
|
|
99
|
+
candidate = load_trace("tests/traces/candidate.json")
|
|
100
|
+
|
|
101
|
+
# Run sub-10ms deterministic comparison
|
|
102
|
+
report = compare(baseline, candidate)
|
|
103
|
+
|
|
104
|
+
# Assert no structural drift, cost surges, or tool loops
|
|
105
|
+
assert_no_regressions(
|
|
106
|
+
report,
|
|
107
|
+
max_divergence=0.25, # Max Trajectory Divergence Index [0.0 - 1.0]
|
|
108
|
+
max_cost_increase_pct=5.0, # Max 5% token cost increase
|
|
109
|
+
allow_loops=False, # Reject repetitive tool call cycles
|
|
110
|
+
max_wasted_effort=0.10, # Max 10% error/retry/abandoned steps
|
|
111
|
+
max_recovery_step_ratio=1.5 # Max recovery steps relative to baseline
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## GitHub Actions CI Gate
|
|
116
|
+
|
|
117
|
+
Block broken agent PRs before they land in production using the official composite action:
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
name: AgentDiff Regression Gate
|
|
121
|
+
|
|
122
|
+
on:
|
|
123
|
+
pull_request:
|
|
124
|
+
|
|
125
|
+
permissions:
|
|
126
|
+
contents: read
|
|
127
|
+
pull-requests: write # Allows posting automated root-cause PR comments
|
|
128
|
+
|
|
129
|
+
jobs:
|
|
130
|
+
agent-regression-gate:
|
|
131
|
+
runs-on: ubuntu-latest
|
|
132
|
+
steps:
|
|
133
|
+
- uses: actions/checkout@v4
|
|
134
|
+
|
|
135
|
+
- uses: actions/setup-python@v5
|
|
136
|
+
with:
|
|
137
|
+
python-version: "3.11"
|
|
138
|
+
|
|
139
|
+
- uses: kerrshift/agentdiff/.github/actions/agentdiff-check@v0.5.0
|
|
140
|
+
with:
|
|
141
|
+
baseline: baselines/customer_support.envelope.json
|
|
142
|
+
candidate: traces/pr_candidate.json
|
|
143
|
+
pr: ${{ github.event.pull_request.number }}
|
|
144
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
When a regression occurs, the gate fails with exit code `1` and comments on the PR with culprit identification and a collapsed divergence tree:
|
|
148
|
+
|
|
149
|
+
```markdown
|
|
150
|
+
### AgentDiff Gate: REGRESSION DETECTED
|
|
151
|
+
|
|
152
|
+
| Metric | Baseline | Candidate | Threshold | Status |
|
|
153
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
154
|
+
| **Divergence (TDI)** | 0.00 | 0.42 | ≤ 0.25 | FAIL |
|
|
155
|
+
| **Cost Surge** | $0.0042 | $0.0138 (+228%) | ≤ +5.0% | FAIL |
|
|
156
|
+
| **Loops (LBI)** | 0 | 3 loops | 0 | FAIL |
|
|
157
|
+
| **Wasted Effort (WEI)**| 0.00 | 0.38 | ≤ 0.10 | FAIL |
|
|
158
|
+
|
|
159
|
+
**Culprit Step:** Step 4 `execute_sql` entered a 3× retry loop after schema refactor.
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Core Metric Mathematics
|
|
163
|
+
|
|
164
|
+
| Metric | Target / Range | Algorithmic Definition | Description |
|
|
165
|
+
| :--- | :--- | :--- | :--- |
|
|
166
|
+
| **Trajectory Divergence Index (TDI)** | `0.0` (Identical) to `1.0` (Divergent) | $$1.0 - \frac{2 \times \vert{}\text{LCS}(\text{Steps}_A, \text{Steps}_B)\vert{}}{\vert{}\text{Steps}_A\vert{} + \vert{}\text{Steps}_B\vert{}}$$ | Structural distance between baseline and candidate execution DAGs using Longest Common Subsequence. |
|
|
167
|
+
| **Wasted Effort Index (WEI)** | `0.0` (Optimal) to `1.0` (Total Waste) | $$\frac{\text{Count}(\text{Steps} \in \{\text{ERROR, RETRY, ABANDONED}\})}{\text{Total Steps}}$$ | Fraction of execution steps spent in failed, retried, or aborted tool operations. |
|
|
168
|
+
| **Loop Buster Index (LBI)** | Integer ($\ge 0$) | Stagnant State Cycle Detection | Counts repeating consecutive tool call patterns where inputs/outputs show no state progression. |
|
|
169
|
+
| **Recovery Step Ratio (RSR)** | `1.0` = Parity; $> 1.0$ = Slower Recovery | $$\text{RSR} = \frac{\text{Recovery Steps}_{\text{candidate}}}{\text{Recovery Steps}_{\text{baseline}}}$$ | Measures the number of steps required to return to the verified golden trajectory path after encountering an error. |
|
|
170
|
+
| **Resource Deltas ($\Delta\text{Res}$)** | Percentage ($\pm\%$) | $\frac{\text{Val}_{\text{candidate}} - \text{Val}_{\text{baseline}}}{\text{Val}_{\text{baseline}}} \times 100$ | Exact percentage deltas for $\Delta\text{Tokens}$, $\Delta\text{Cost}$, and $\Delta\text{Latency}$. |
|
|
171
|
+
|
|
172
|
+
## Supported Telemetry Formats
|
|
173
|
+
|
|
174
|
+
| Telemetry Framework / Format | Adapter Spec | Ingestion Guide |
|
|
175
|
+
| :--- | :--- | :--- |
|
|
176
|
+
| **LangGraph / LangChain** | `--adapter langgraph` | [`cookbooks/langgraph`](cookbooks/) |
|
|
177
|
+
| **CrewAI** | `--adapter crewai` | [`cookbooks/crewai`](cookbooks/) |
|
|
178
|
+
| **OpenAI Agents SDK** | `--adapter openai_agents` | [`cookbooks/openai_agents`](cookbooks/) |
|
|
179
|
+
| **Langfuse** | `--adapter langfuse` | [`cookbooks/langfuse`](cookbooks/) |
|
|
180
|
+
| **LangSmith** | `--adapter langsmith` | [`cookbooks/langsmith`](cookbooks/) |
|
|
181
|
+
| **OpenInference / OpenTelemetry** | `--adapter openinference` | [`cookbooks/openinference`](cookbooks/) |
|
|
182
|
+
| **Generic JSON Schema** | `--adapter generic` | [`schema/v0.1.0/trace.json`](schema/v0.1.0/trace.json) |
|
|
183
|
+
|
|
184
|
+
## Local-First Privacy Guarantee
|
|
185
|
+
|
|
186
|
+
Agent trajectories often contain proprietary prompts, sensitive tool payloads, and customer data. AgentDiff is engineered with strict local-first principles:
|
|
187
|
+
|
|
188
|
+
- **Zero Outbound Network Traffic:** Parsing, DAG diffing, metric calculations, and reporting run 100% locally.
|
|
189
|
+
- **Air-Gapped & Firewall Friendly:** Run tests on laptops, in air-gapped VPCs, or under strict enterprise egress policies.
|
|
190
|
+
- **Repo-Committed Baselines:** Your golden trajectories live in Git next to the code they protect.
|
|
191
|
+
- **No Third-Party APM Lock-In:** Switch tracing providers at any time; AgentDiff normalizes all schemas to a unified specification.
|
|
192
|
+
|
|
193
|
+
## Documentation & Cookbooks
|
|
194
|
+
|
|
195
|
+
- **Official Documentation:** [https://agentdiff.app](https://agentdiff.app)
|
|
196
|
+
- **Live Demo Repository:** [github.com/lostmartian/agentdiff-demo](https://github.com/lostmartian/agentdiff-demo)
|
|
197
|
+
- **Engine Specification:** [Under the Hood](https://agentdiff.app/features)
|
|
198
|
+
- **Interactive Visualizer:** [Diff Playground](https://agentdiff.app/compare)
|
|
199
|
+
- **Adapters Guide:** [Framework Integration](https://agentdiff.app/adapters)
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
This repository uses [`uv`](https://docs.astral.sh/uv/) for lightning-fast environment and dependency management.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Clone the repository
|
|
207
|
+
git clone https://github.com/kerrshift/agentdiff.git
|
|
208
|
+
cd agentdiff
|
|
209
|
+
|
|
210
|
+
# Install dependencies and sync virtualenv
|
|
211
|
+
uv sync
|
|
212
|
+
|
|
213
|
+
# Run linting and code formatting checks
|
|
214
|
+
make lint
|
|
215
|
+
|
|
216
|
+
# Run the test suite
|
|
217
|
+
make test
|
|
218
|
+
|
|
219
|
+
# Build package distributions
|
|
220
|
+
make build
|
|
221
|
+
|
|
222
|
+
# Launch the website & docs locally
|
|
223
|
+
make website-dev
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
Distributed under the **MIT License**. See [`LICENSE`](LICENSE) for more information.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="docs/assets/logo.svg" alt="AgentDiff Logo" width="84" height="84" />
|
|
4
|
+
|
|
5
|
+
# AgentDiff
|
|
6
|
+
|
|
7
|
+
**Catch silent cost surges and broken agent loops before they ship.**
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://github.com/kerrshift/agentdiff/actions/workflows/ci.yml"><img src="https://github.com/kerrshift/agentdiff/actions/workflows/ci.yml/badge.svg" alt="CI Build" /></a>
|
|
11
|
+
<a href="https://pypi.org/project/agent-trajectory-diff/"><img src="https://img.shields.io/pypi/v/agent-trajectory-diff.svg?color=10b981" alt="PyPI version" /></a>
|
|
12
|
+
<a href="https://pypi.org/project/agent-trajectory-diff/"><img src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg" alt="Python Versions" /></a>
|
|
13
|
+
<a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/badge/code%20style-ruff-000000.svg" alt="Code Style: Ruff" /></a>
|
|
14
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
|
|
15
|
+
<a href="https://agentdiff.app"><img src="https://img.shields.io/badge/docs-agentdiff.app-emerald.svg" alt="Website" /></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install agent-trajectory-diff
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
[Website & Interactive Docs](https://agentdiff.app) · [Cookbooks](cookbooks/) · [Live Demo Repo](https://github.com/lostmartian/agentdiff-demo) · [Changelog](CHANGELOG.md)
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
**AgentDiff** is a developer-first Python library, pytest plugin, and CLI for **regression testing multi-turn, tool-using AI agents by comparing execution paths (trajectories) head-to-head.**
|
|
27
|
+
|
|
28
|
+
When you change a prompt, tweak a system instruction, or upgrade an LLM, traditional assertions only verify that the final string matches. They miss the silent failures: **the agent took 5 extra tool calls, burned 3× the tokens, entered an infinite retry loop, or drifted from the verified execution path.**
|
|
29
|
+
|
|
30
|
+
AgentDiff aligns candidate execution DAGs against committed golden baselines in `<10ms` without calling any paid LLM judges.
|
|
31
|
+
|
|
32
|
+
## Highlights
|
|
33
|
+
|
|
34
|
+
- **Deterministic Graph Diffing:** Topological DAG alignment and Longest Common Subsequence (LCS) step comparison in `<10ms` with zero paid LLM-judge calls.
|
|
35
|
+
- **Statistical Baselines & Variance Bands:** Capture N-run envelopes (`record --runs 3`) so non-deterministic agents don't flake CI on harmless jitter.
|
|
36
|
+
- **Zero-Config Setup (`agentdiff init`):** Auto-detects LangGraph, CrewAI, OpenAI Agents SDK, or OpenTelemetry and writes `agentdiff.toml` + CI workflow in seconds.
|
|
37
|
+
- **In-PR Interactive Blessings (`/agentdiff approve`):** Reviewers bless intended trajectory improvements from PR comments as `agentdiff-ci[bot]`.
|
|
38
|
+
- **100% Local & Air-Gapped:** Zero telemetry, no cloud accounts, no outbound network calls during diffs. Raw prompts and tool outputs stay local.
|
|
39
|
+
- **Drop-in CI Merge Gate:** Native exit codes (`0` pass / `1` regression fail) and automated GitHub Action PR comments with human-first verdicts.
|
|
40
|
+
- **Universal Telemetry Adapters:** Seamlessly diff traces from **LangGraph**, **CrewAI**, **OpenAI Agents SDK**, **Langfuse**, **LangSmith**, **OpenInference / OpenTelemetry**, or generic JSON.
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
### 1. Initialize with `agentdiff init`
|
|
45
|
+
Auto-detect your agent framework and generate your configuration + CI workflow:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
agentdiff init --scenario customer_support --runs 3 --with-approve
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Record a Statistical Baseline Envelope
|
|
52
|
+
Record an N-run baseline envelope from any agent function without writing boilerplate telemetry:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
agentdiff record my_agent:run \
|
|
56
|
+
--input '{"query": "summarize repo"}' \
|
|
57
|
+
--runs 3 \
|
|
58
|
+
--out baselines/customer_support.envelope.json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 3. Compare Traces in CLI
|
|
62
|
+
Compare candidate runs against your baseline envelope:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
agentdiff diff baselines/customer_support.envelope.json traces/candidate.json --fail-on-regression
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 4. Pytest Regression Testing
|
|
69
|
+
Enforce trajectory parity directly in your test suite:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import pytest
|
|
73
|
+
from agentdiff import load_trace, compare
|
|
74
|
+
from agentdiff.testing import assert_no_regressions
|
|
75
|
+
|
|
76
|
+
def test_agent_refactor_efficiency():
|
|
77
|
+
# Load traces (auto-detects telemetry source format)
|
|
78
|
+
baseline = load_trace("tests/baselines/golden.json")
|
|
79
|
+
candidate = load_trace("tests/traces/candidate.json")
|
|
80
|
+
|
|
81
|
+
# Run sub-10ms deterministic comparison
|
|
82
|
+
report = compare(baseline, candidate)
|
|
83
|
+
|
|
84
|
+
# Assert no structural drift, cost surges, or tool loops
|
|
85
|
+
assert_no_regressions(
|
|
86
|
+
report,
|
|
87
|
+
max_divergence=0.25, # Max Trajectory Divergence Index [0.0 - 1.0]
|
|
88
|
+
max_cost_increase_pct=5.0, # Max 5% token cost increase
|
|
89
|
+
allow_loops=False, # Reject repetitive tool call cycles
|
|
90
|
+
max_wasted_effort=0.10, # Max 10% error/retry/abandoned steps
|
|
91
|
+
max_recovery_step_ratio=1.5 # Max recovery steps relative to baseline
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## GitHub Actions CI Gate
|
|
96
|
+
|
|
97
|
+
Block broken agent PRs before they land in production using the official composite action:
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
name: AgentDiff Regression Gate
|
|
101
|
+
|
|
102
|
+
on:
|
|
103
|
+
pull_request:
|
|
104
|
+
|
|
105
|
+
permissions:
|
|
106
|
+
contents: read
|
|
107
|
+
pull-requests: write # Allows posting automated root-cause PR comments
|
|
108
|
+
|
|
109
|
+
jobs:
|
|
110
|
+
agent-regression-gate:
|
|
111
|
+
runs-on: ubuntu-latest
|
|
112
|
+
steps:
|
|
113
|
+
- uses: actions/checkout@v4
|
|
114
|
+
|
|
115
|
+
- uses: actions/setup-python@v5
|
|
116
|
+
with:
|
|
117
|
+
python-version: "3.11"
|
|
118
|
+
|
|
119
|
+
- uses: kerrshift/agentdiff/.github/actions/agentdiff-check@v0.5.0
|
|
120
|
+
with:
|
|
121
|
+
baseline: baselines/customer_support.envelope.json
|
|
122
|
+
candidate: traces/pr_candidate.json
|
|
123
|
+
pr: ${{ github.event.pull_request.number }}
|
|
124
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
When a regression occurs, the gate fails with exit code `1` and comments on the PR with culprit identification and a collapsed divergence tree:
|
|
128
|
+
|
|
129
|
+
```markdown
|
|
130
|
+
### AgentDiff Gate: REGRESSION DETECTED
|
|
131
|
+
|
|
132
|
+
| Metric | Baseline | Candidate | Threshold | Status |
|
|
133
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
134
|
+
| **Divergence (TDI)** | 0.00 | 0.42 | ≤ 0.25 | FAIL |
|
|
135
|
+
| **Cost Surge** | $0.0042 | $0.0138 (+228%) | ≤ +5.0% | FAIL |
|
|
136
|
+
| **Loops (LBI)** | 0 | 3 loops | 0 | FAIL |
|
|
137
|
+
| **Wasted Effort (WEI)**| 0.00 | 0.38 | ≤ 0.10 | FAIL |
|
|
138
|
+
|
|
139
|
+
**Culprit Step:** Step 4 `execute_sql` entered a 3× retry loop after schema refactor.
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Core Metric Mathematics
|
|
143
|
+
|
|
144
|
+
| Metric | Target / Range | Algorithmic Definition | Description |
|
|
145
|
+
| :--- | :--- | :--- | :--- |
|
|
146
|
+
| **Trajectory Divergence Index (TDI)** | `0.0` (Identical) to `1.0` (Divergent) | $$1.0 - \frac{2 \times \vert{}\text{LCS}(\text{Steps}_A, \text{Steps}_B)\vert{}}{\vert{}\text{Steps}_A\vert{} + \vert{}\text{Steps}_B\vert{}}$$ | Structural distance between baseline and candidate execution DAGs using Longest Common Subsequence. |
|
|
147
|
+
| **Wasted Effort Index (WEI)** | `0.0` (Optimal) to `1.0` (Total Waste) | $$\frac{\text{Count}(\text{Steps} \in \{\text{ERROR, RETRY, ABANDONED}\})}{\text{Total Steps}}$$ | Fraction of execution steps spent in failed, retried, or aborted tool operations. |
|
|
148
|
+
| **Loop Buster Index (LBI)** | Integer ($\ge 0$) | Stagnant State Cycle Detection | Counts repeating consecutive tool call patterns where inputs/outputs show no state progression. |
|
|
149
|
+
| **Recovery Step Ratio (RSR)** | `1.0` = Parity; $> 1.0$ = Slower Recovery | $$\text{RSR} = \frac{\text{Recovery Steps}_{\text{candidate}}}{\text{Recovery Steps}_{\text{baseline}}}$$ | Measures the number of steps required to return to the verified golden trajectory path after encountering an error. |
|
|
150
|
+
| **Resource Deltas ($\Delta\text{Res}$)** | Percentage ($\pm\%$) | $\frac{\text{Val}_{\text{candidate}} - \text{Val}_{\text{baseline}}}{\text{Val}_{\text{baseline}}} \times 100$ | Exact percentage deltas for $\Delta\text{Tokens}$, $\Delta\text{Cost}$, and $\Delta\text{Latency}$. |
|
|
151
|
+
|
|
152
|
+
## Supported Telemetry Formats
|
|
153
|
+
|
|
154
|
+
| Telemetry Framework / Format | Adapter Spec | Ingestion Guide |
|
|
155
|
+
| :--- | :--- | :--- |
|
|
156
|
+
| **LangGraph / LangChain** | `--adapter langgraph` | [`cookbooks/langgraph`](cookbooks/) |
|
|
157
|
+
| **CrewAI** | `--adapter crewai` | [`cookbooks/crewai`](cookbooks/) |
|
|
158
|
+
| **OpenAI Agents SDK** | `--adapter openai_agents` | [`cookbooks/openai_agents`](cookbooks/) |
|
|
159
|
+
| **Langfuse** | `--adapter langfuse` | [`cookbooks/langfuse`](cookbooks/) |
|
|
160
|
+
| **LangSmith** | `--adapter langsmith` | [`cookbooks/langsmith`](cookbooks/) |
|
|
161
|
+
| **OpenInference / OpenTelemetry** | `--adapter openinference` | [`cookbooks/openinference`](cookbooks/) |
|
|
162
|
+
| **Generic JSON Schema** | `--adapter generic` | [`schema/v0.1.0/trace.json`](schema/v0.1.0/trace.json) |
|
|
163
|
+
|
|
164
|
+
## Local-First Privacy Guarantee
|
|
165
|
+
|
|
166
|
+
Agent trajectories often contain proprietary prompts, sensitive tool payloads, and customer data. AgentDiff is engineered with strict local-first principles:
|
|
167
|
+
|
|
168
|
+
- **Zero Outbound Network Traffic:** Parsing, DAG diffing, metric calculations, and reporting run 100% locally.
|
|
169
|
+
- **Air-Gapped & Firewall Friendly:** Run tests on laptops, in air-gapped VPCs, or under strict enterprise egress policies.
|
|
170
|
+
- **Repo-Committed Baselines:** Your golden trajectories live in Git next to the code they protect.
|
|
171
|
+
- **No Third-Party APM Lock-In:** Switch tracing providers at any time; AgentDiff normalizes all schemas to a unified specification.
|
|
172
|
+
|
|
173
|
+
## Documentation & Cookbooks
|
|
174
|
+
|
|
175
|
+
- **Official Documentation:** [https://agentdiff.app](https://agentdiff.app)
|
|
176
|
+
- **Live Demo Repository:** [github.com/lostmartian/agentdiff-demo](https://github.com/lostmartian/agentdiff-demo)
|
|
177
|
+
- **Engine Specification:** [Under the Hood](https://agentdiff.app/features)
|
|
178
|
+
- **Interactive Visualizer:** [Diff Playground](https://agentdiff.app/compare)
|
|
179
|
+
- **Adapters Guide:** [Framework Integration](https://agentdiff.app/adapters)
|
|
180
|
+
|
|
181
|
+
## Development
|
|
182
|
+
|
|
183
|
+
This repository uses [`uv`](https://docs.astral.sh/uv/) for lightning-fast environment and dependency management.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Clone the repository
|
|
187
|
+
git clone https://github.com/kerrshift/agentdiff.git
|
|
188
|
+
cd agentdiff
|
|
189
|
+
|
|
190
|
+
# Install dependencies and sync virtualenv
|
|
191
|
+
uv sync
|
|
192
|
+
|
|
193
|
+
# Run linting and code formatting checks
|
|
194
|
+
make lint
|
|
195
|
+
|
|
196
|
+
# Run the test suite
|
|
197
|
+
make test
|
|
198
|
+
|
|
199
|
+
# Build package distributions
|
|
200
|
+
make build
|
|
201
|
+
|
|
202
|
+
# Launch the website & docs locally
|
|
203
|
+
make website-dev
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
Distributed under the **MIT License**. See [`LICENSE`](LICENSE) for more information.
|
|
@@ -28,8 +28,8 @@ agentdiff = "agentdiff.cli:main"
|
|
|
28
28
|
agentdiff = "agentdiff.pytest_plugin"
|
|
29
29
|
|
|
30
30
|
[project.urls]
|
|
31
|
-
Homepage = "https://github.com/
|
|
32
|
-
Repository = "https://github.com/
|
|
31
|
+
Homepage = "https://github.com/kerrshift/agentdiff"
|
|
32
|
+
Repository = "https://github.com/kerrshift/agentdiff"
|
|
33
33
|
|
|
34
34
|
[dependency-groups]
|
|
35
35
|
dev = [
|
|
@@ -37,6 +37,7 @@ dev = [
|
|
|
37
37
|
"pytest>=8.0",
|
|
38
38
|
"pytest-benchmark>=4.0",
|
|
39
39
|
"pytest-cov>=7.1.0",
|
|
40
|
+
"pyyaml>=6.0.3",
|
|
40
41
|
"ruff>=0.16.3",
|
|
41
42
|
]
|
|
42
43
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Pillar 3 — in-PR baseline approval (``/agentdiff approve``).
|
|
2
|
+
|
|
3
|
+
Closes the baseline-maintenance loop entirely inside GitHub: a reviewer
|
|
4
|
+
comments ``/agentdiff approve`` on a flagged PR, the approve workflow calls
|
|
5
|
+
:func:`approve_candidate`, and the candidate trace becomes (or joins) the
|
|
6
|
+
golden baseline. No local checkout, no manual JSON.
|
|
7
|
+
|
|
8
|
+
Policy (decision D3, SPEC-0.5.0 §10): a human may bless **path drift** and
|
|
9
|
+
**cost spikes** — those are product judgments. **Loops are never blessable**:
|
|
10
|
+
a cyclical tool loop or a tool-repeat-cap breach is non-termination, and
|
|
11
|
+
re-baselining it would teach the gate that broken is the new normal.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from agentdiff.engine.comparator import compare, compare_envelope
|
|
19
|
+
from agentdiff.loader import load_baseline, load_trace
|
|
20
|
+
from agentdiff.models.envelope import BaselineEnvelope
|
|
21
|
+
from agentdiff.models.report import DiffReport, GateFinding
|
|
22
|
+
from agentdiff.models.trace import AgentTrace
|
|
23
|
+
from agentdiff.testing.assertions import GateResult, evaluate_gate
|
|
24
|
+
|
|
25
|
+
# Loop-family codes: hard invariants that approve must never bless (D3).
|
|
26
|
+
_UNBLESSABLE_CODES = frozenset({"tool_loop", "tool_repeats", "loops"})
|
|
27
|
+
|
|
28
|
+
DEFAULT_SAMPLE_RUNS = 3
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class ApproveDecision:
|
|
33
|
+
"""Outcome of an ``/agentdiff approve`` invocation."""
|
|
34
|
+
|
|
35
|
+
approved: bool
|
|
36
|
+
reason: str
|
|
37
|
+
report: DiffReport | None
|
|
38
|
+
unblessable_findings: tuple[GateFinding, ...] = ()
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def refusals(self) -> tuple[str, ...]:
|
|
42
|
+
return tuple(f.message for f in self.unblessable_findings)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _gate_for(
|
|
46
|
+
envelope: BaselineEnvelope, candidate: AgentTrace, scenario_cfg
|
|
47
|
+
) -> tuple[DiffReport, GateResult]:
|
|
48
|
+
if envelope.mode == "statistical" and envelope.n_runs >= 2:
|
|
49
|
+
tol = getattr(scenario_cfg, "tolerances", None)
|
|
50
|
+
return compare_envelope(
|
|
51
|
+
envelope,
|
|
52
|
+
candidate,
|
|
53
|
+
max_divergence=tol.divergence_ceiling if tol else 0.35,
|
|
54
|
+
max_cost_increase_pct=(
|
|
55
|
+
getattr(scenario_cfg, "max_cost_increase_pct", 20.0)
|
|
56
|
+
if scenario_cfg
|
|
57
|
+
else 20.0
|
|
58
|
+
),
|
|
59
|
+
step_count_std_dev=tol.step_count_std_dev if tol else 2.0,
|
|
60
|
+
)
|
|
61
|
+
report = compare(envelope.runs[0], candidate)
|
|
62
|
+
return report, evaluate_gate(report)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def approve_candidate(
|
|
66
|
+
baseline_path: str,
|
|
67
|
+
candidate_path: str,
|
|
68
|
+
scenario_cfg=None,
|
|
69
|
+
adapter: str = "auto",
|
|
70
|
+
sample_runs: int = DEFAULT_SAMPLE_RUNS,
|
|
71
|
+
) -> ApproveDecision:
|
|
72
|
+
"""Blesses a candidate run as the new golden baseline.
|
|
73
|
+
|
|
74
|
+
Refuses when a loop-family hard invariant fires (D3). Otherwise rotates
|
|
75
|
+
the baseline — envelopes append the candidate into their rolling window
|
|
76
|
+
and recompute bands; strict baselines are replaced.
|
|
77
|
+
|
|
78
|
+
Raises the same load errors as ``load_baseline``/``load_trace``.
|
|
79
|
+
"""
|
|
80
|
+
envelope = load_baseline(baseline_path, adapter)
|
|
81
|
+
candidate = load_trace(candidate_path, adapter)
|
|
82
|
+
|
|
83
|
+
report, gate = _gate_for(envelope, candidate, scenario_cfg)
|
|
84
|
+
|
|
85
|
+
loop_findings = tuple(f for f in gate.violations if f.code in _UNBLESSABLE_CODES)
|
|
86
|
+
if loop_findings:
|
|
87
|
+
return ApproveDecision(
|
|
88
|
+
approved=False,
|
|
89
|
+
reason=(
|
|
90
|
+
"refused: loop violations are never blessable — fix the loop, "
|
|
91
|
+
"then approve (policy: non-termination is not a baseline)"
|
|
92
|
+
),
|
|
93
|
+
report=report,
|
|
94
|
+
unblessable_findings=loop_findings,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if envelope.mode == "statistical" and envelope.n_runs >= 2:
|
|
98
|
+
envelope.runs.append(candidate)
|
|
99
|
+
keep = max(2, sample_runs)
|
|
100
|
+
envelope.runs = envelope.runs[-keep:]
|
|
101
|
+
envelope.refresh()
|
|
102
|
+
else:
|
|
103
|
+
# strict baseline: candidate replaces the run set; mode is preserved
|
|
104
|
+
envelope.runs = [candidate]
|
|
105
|
+
envelope.refresh()
|
|
106
|
+
|
|
107
|
+
with open(baseline_path, "w", encoding="utf-8") as fh:
|
|
108
|
+
fh.write(envelope.model_dump_json())
|
|
109
|
+
|
|
110
|
+
blessed = []
|
|
111
|
+
if gate.violations:
|
|
112
|
+
blessed = [f.code for f in gate.violations]
|
|
113
|
+
reason = (
|
|
114
|
+
"approved with human blessing of: " + ", ".join(blessed)
|
|
115
|
+
if blessed
|
|
116
|
+
else "approved: candidate is clean against its own new baseline"
|
|
117
|
+
)
|
|
118
|
+
return ApproveDecision(approved=True, reason=reason, report=report)
|