agent-trajectory-diff 0.1.0__tar.gz → 0.2.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.2.0/LICENSE +21 -0
- agent_trajectory_diff-0.2.0/PKG-INFO +204 -0
- agent_trajectory_diff-0.2.0/README.md +184 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/pyproject.toml +15 -3
- agent_trajectory_diff-0.2.0/src/agentdiff/__init__.py +93 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/adapters/__init__.py +4 -2
- agent_trajectory_diff-0.2.0/src/agentdiff/adapters/_iso.py +31 -0
- agent_trajectory_diff-0.2.0/src/agentdiff/adapters/base.py +42 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/adapters/langfuse.py +30 -19
- agent_trajectory_diff-0.2.0/src/agentdiff/adapters/langsmith.py +168 -0
- agent_trajectory_diff-0.2.0/src/agentdiff/adapters/openai_agents.py +260 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/adapters/openinference.py +29 -21
- agent_trajectory_diff-0.2.0/src/agentdiff/ci/baseline.py +59 -0
- agent_trajectory_diff-0.2.0/src/agentdiff/ci/github.py +66 -0
- agent_trajectory_diff-0.2.0/src/agentdiff/cli.py +276 -0
- agent_trajectory_diff-0.2.0/src/agentdiff/config.py +135 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/engine/aligner.py +19 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/engine/comparator.py +21 -1
- agent_trajectory_diff-0.2.0/src/agentdiff/engine/explanations.py +385 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/engine/loop_detector.py +5 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/engine/metrics.py +6 -0
- agent_trajectory_diff-0.2.0/src/agentdiff/engine/tree.py +101 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/loader.py +45 -13
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/models/trace.py +4 -0
- agent_trajectory_diff-0.2.0/src/agentdiff/pytest_plugin.py +144 -0
- agent_trajectory_diff-0.2.0/src/agentdiff/reporters/pr.py +67 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/testing/assertions.py +10 -2
- agent_trajectory_diff-0.1.0/LICENSE +0 -622
- agent_trajectory_diff-0.1.0/PKG-INFO +0 -112
- agent_trajectory_diff-0.1.0/README.md +0 -92
- agent_trajectory_diff-0.1.0/src/agentdiff/__init__.py +0 -40
- agent_trajectory_diff-0.1.0/src/agentdiff/adapters/base.py +0 -20
- agent_trajectory_diff-0.1.0/src/agentdiff/adapters/deepeval.py +0 -134
- agent_trajectory_diff-0.1.0/src/agentdiff/cli.py +0 -120
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/.gitignore +0 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/__main__.py +0 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/adapters/generic.py +0 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/engine/__init__.py +0 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/models/__init__.py +0 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/models/report.py +0 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/models/step.py +0 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/py.typed +0 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/reporters/__init__.py +0 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/reporters/markdown.py +0 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/reporters/terminal.py +0 -0
- {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/testing/__init__.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AgentDiff contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agent-trajectory-diff
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A developer-first package to evaluate and find regressions in agent trajectories
|
|
5
|
+
Project-URL: Homepage, https://github.com/lostmartian/agentdiff
|
|
6
|
+
Project-URL: Repository, https://github.com/lostmartian/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
|
+
# AgentDiff
|
|
22
|
+
|
|
23
|
+
[](LICENSE)
|
|
24
|
+
[](pyproject.toml)
|
|
25
|
+
|
|
26
|
+
**AgentDiff** is a developer-first Python library and CLI designed to solve the hardest problem in agent engineering: **regression testing multi-turn, tool-using AI agents by comparing execution paths (trajectories) head-to-head.**
|
|
27
|
+
|
|
28
|
+
## What AgentDiff Is
|
|
29
|
+
|
|
30
|
+
* **A Trajectory Diff Engine:** Compares Run A (Baseline) against Run B (Candidate) across their execution Directed Acyclic Graphs (DAGs).
|
|
31
|
+
* **A Local-First CI/CD Gate:** Runs locally in your terminal or inside `pytest` and GitHub Actions, raising errors or exit codes on regression violations.
|
|
32
|
+
* **A Universal Comparator:** Ingests telemetry run files from **OpenInference/OTel**, **Langfuse**, **LangSmith**, **OpenAI Agents SDK**, or raw/custom JSON.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Install the PyPI package:
|
|
37
|
+
```bash
|
|
38
|
+
pip install agent-trajectory-diff
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or using `uv`:
|
|
42
|
+
```bash
|
|
43
|
+
uv add agent-trajectory-diff
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quickstart
|
|
47
|
+
|
|
48
|
+
### 1. CLI Usage
|
|
49
|
+
|
|
50
|
+
Compare two trajectory JSON traces from your terminal:
|
|
51
|
+
```bash
|
|
52
|
+
agentdiff baseline_run.json candidate_run.json --fail-on-regression --max-divergence 0.25
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Options:
|
|
56
|
+
- `--adapter`: Telemetry parser to use (`auto`, `generic`, `openinference`, `langfuse`, `langsmith`, `openai_agents`).
|
|
57
|
+
- `--format`: Format for the output (`terminal`, `json`, `markdown`).
|
|
58
|
+
- `--fail-on-regression`: Return exit code `1` if thresholds are violated.
|
|
59
|
+
- `--max-loops`: Maximum loops allowed.
|
|
60
|
+
- `--max-divergence`: Maximum Trajectory Divergence Index (TDI) allowed.
|
|
61
|
+
- `--max-cost-delta`: Maximum cost increase percentage allowed.
|
|
62
|
+
- `--baseline, -b PATH`: Compare against a persistent baseline trace file (see [Baseline workflow](#baseline-workflow)).
|
|
63
|
+
- `--update-baseline`: Overwrite the persistent baseline with the candidate after a clean diff.
|
|
64
|
+
- `--config PATH`: Load defaults from an `agentdiff.toml` (auto-discovered if not given).
|
|
65
|
+
|
|
66
|
+
#### Config-as-code (`agentdiff.toml`)
|
|
67
|
+
|
|
68
|
+
Commit your thresholds, adapter, and baseline path next to your traces instead of repeating CLI flags. Explicit flags always win over config.
|
|
69
|
+
|
|
70
|
+
```toml
|
|
71
|
+
[compare]
|
|
72
|
+
detect_loops = true
|
|
73
|
+
strict_tool_signatures = false
|
|
74
|
+
|
|
75
|
+
[adapter]
|
|
76
|
+
name = "auto" # auto, generic, openinference, langfuse, langsmith, openai_agents
|
|
77
|
+
|
|
78
|
+
[cli]
|
|
79
|
+
format = "terminal" # terminal, json, markdown, pr
|
|
80
|
+
baseline = "baselines/current.json"
|
|
81
|
+
max_loops = 0
|
|
82
|
+
max_divergence = 0.3
|
|
83
|
+
max_cost_delta = 10.0
|
|
84
|
+
|
|
85
|
+
[assertions] # defaults used by assert_no_regressions / pytest plugin
|
|
86
|
+
max_divergence = 0.25
|
|
87
|
+
max_cost_increase_pct = 5.0
|
|
88
|
+
allow_loops = false
|
|
89
|
+
max_wasted_effort = 0.1
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
AgentDiff auto-discovers `agentdiff.toml` from the current directory upward, or you can point at it explicitly with `--config`.
|
|
93
|
+
|
|
94
|
+
## Baseline workflow
|
|
95
|
+
|
|
96
|
+
Keep a single `baseline.json` file committed to your repo instead of hand-managing two trace files. The first run establishes the baseline; later runs compare against it and advance it only on clean diffs.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# First run: stores candidate as the baseline, exits 0
|
|
100
|
+
agentdiff baseline.json today.json --baseline baseline.json --update-baseline
|
|
101
|
+
|
|
102
|
+
# Later runs: compare today's run against the stored baseline
|
|
103
|
+
agentdiff baseline.json today.json --baseline baseline.json --update-baseline --fail-on-regression
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- If `baseline.json` does not exist and `--update-baseline` is set, the candidate is copied in as the baseline and the command exits `0`.
|
|
107
|
+
- If it does not exist and `--update-baseline` is omitted, the command exits `2` with a helpful message.
|
|
108
|
+
- On a regression the baseline is **never** overwritten, and `--fail-on-regression` exits `1`.
|
|
109
|
+
|
|
110
|
+
### 2. Python SDK & Pytest Integration
|
|
111
|
+
|
|
112
|
+
Catch agent loop regressions or token cost spikes in your test suites:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import pytest
|
|
116
|
+
from agentdiff import load_trace, compare
|
|
117
|
+
from agentdiff.testing import assert_no_regressions
|
|
118
|
+
|
|
119
|
+
def test_agent_refactor_efficiency():
|
|
120
|
+
# Load traces from disk (auto-detects the telemetry format)
|
|
121
|
+
baseline = load_trace("tests/traces/baseline.json")
|
|
122
|
+
candidate = load_trace("tests/traces/candidate.json")
|
|
123
|
+
|
|
124
|
+
# Run the comparison
|
|
125
|
+
report = compare(baseline, candidate)
|
|
126
|
+
|
|
127
|
+
# Expressive assertion helper that raises detailed error messages on regression
|
|
128
|
+
assert_no_regressions(
|
|
129
|
+
report,
|
|
130
|
+
max_divergence=0.25, # TDI threshold [0.0 - 1.0]
|
|
131
|
+
max_cost_increase_pct=5.0, # Max cost increase allowed
|
|
132
|
+
allow_loops=False, # Reject if tool loops are detected
|
|
133
|
+
max_wasted_effort=0.10 # Max Wasted Effort Index (WEI) allowed
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 3. GitHub Action
|
|
138
|
+
|
|
139
|
+
Gate a PR on agent trajectory regressions with the reusable composite action.
|
|
140
|
+
Pin it to a release tag and point `package` at the published package (or a
|
|
141
|
+
`git+` path / local directory for pre-release testing):
|
|
142
|
+
|
|
143
|
+
```yaml
|
|
144
|
+
name: AgentDiff Gate
|
|
145
|
+
on:
|
|
146
|
+
pull_request:
|
|
147
|
+
|
|
148
|
+
jobs:
|
|
149
|
+
agentdiff:
|
|
150
|
+
runs-on: ubuntu-latest
|
|
151
|
+
steps:
|
|
152
|
+
- uses: actions/checkout@v4
|
|
153
|
+
- uses: actions/setup-python@v5
|
|
154
|
+
with:
|
|
155
|
+
python-version: "3.11"
|
|
156
|
+
- uses: lostmartian/agentdiff/.github/actions/agentdiff-check@v0.2.0
|
|
157
|
+
with:
|
|
158
|
+
baseline: traces/baseline.json # committed baseline trace
|
|
159
|
+
candidate: traces/candidate.json # generated by an earlier step
|
|
160
|
+
update-baseline: "false"
|
|
161
|
+
max-divergence: "0.3"
|
|
162
|
+
max-cost-delta: "10.0"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The action installs the package (default `agent-trajectory-diff` from PyPI),
|
|
166
|
+
runs `agentdiff --fail-on-regression`, and fails the job when divergence,
|
|
167
|
+
loops, or cost spikes exceed the thresholds.
|
|
168
|
+
|
|
169
|
+
**Available inputs:**
|
|
170
|
+
|
|
171
|
+
| Input | Default | Description |
|
|
172
|
+
| --- | --- | --- |
|
|
173
|
+
| `baseline` | *(required)* | Path to the stored baseline trace JSON. |
|
|
174
|
+
| `candidate` | *(required)* | Path to the candidate trace JSON. |
|
|
175
|
+
| `package` | `agent-trajectory-diff` | Python package spec to install (PyPI name, `git+https://…`, or a local path). |
|
|
176
|
+
| `adapter` | `auto` | Telemetry adapter: `auto`, `generic`, `openinference`, `langfuse`, `langsmith`, `openai_agents`. |
|
|
177
|
+
| `max-divergence` | `0.3` | Maximum Trajectory Divergence Index (TDI) before regression. |
|
|
178
|
+
| `max-loops` | `0` | Maximum loop count before regression. |
|
|
179
|
+
| `max-cost-delta` | `10.0` | Maximum cost increase percentage before regression. |
|
|
180
|
+
| `update-baseline` | `false` | Overwrite the stored baseline with the candidate when the run is clean. |
|
|
181
|
+
|
|
182
|
+
## Core Metrics
|
|
183
|
+
|
|
184
|
+
| Metric | Target / Range | Algorithmic Definition |
|
|
185
|
+
| --- | --- | --- |
|
|
186
|
+
| **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{}}$$ |
|
|
187
|
+
| **Wasted Effort Index (WEI)** | `0.0` (Optimal) to `1.0` (Total Waste) | $$\frac{\text{Count}(\text{Steps with status} \in \{\text{ERROR, RETRY, ABANDONED}\})}{\text{Total Execution Steps}}$$ |
|
|
188
|
+
| **Loop Buster Index (LBI)** | Integer ($\ge 0$) | Detects consecutive repeating sequences of tools with stagnant state changes. |
|
|
189
|
+
| **Resource Deltas ($\Delta\text{Res}$)** | Percentage ($\pm\%$) | Standard deltas for $\Delta\text{Tokens}$, $\Delta\text{Cost}$, and $\Delta\text{Latency}$. |
|
|
190
|
+
|
|
191
|
+
## Development & Operations
|
|
192
|
+
|
|
193
|
+
This project utilizes `uv` to manage environments and dependencies. Automation tasks are defined in the **[`Makefile`](file:///Users/lostmartian/Desktop/interview/agentdiff/Makefile)**:
|
|
194
|
+
|
|
195
|
+
- `make lint` / `make format`: Run Ruff linter checks and formatter.
|
|
196
|
+
- `make test`: Run pytest suite (including style & formatting assertions).
|
|
197
|
+
- `make build`: Package the library into source and wheel distributions in `dist/`.
|
|
198
|
+
- `make website-dev`: Start the Next.js landing and documentation site local server.
|
|
199
|
+
- `make website-build`: Build the Next.js static output in `website/out/`.
|
|
200
|
+
|
|
201
|
+
### Repository Layout
|
|
202
|
+
- `src/`: Python source code package modules.
|
|
203
|
+
- `tests/`: Quality assurance unit tests.
|
|
204
|
+
- `website/`: Next.js web application and documentation pages.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# AgentDiff
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](pyproject.toml)
|
|
5
|
+
|
|
6
|
+
**AgentDiff** is a developer-first Python library and CLI designed to solve the hardest problem in agent engineering: **regression testing multi-turn, tool-using AI agents by comparing execution paths (trajectories) head-to-head.**
|
|
7
|
+
|
|
8
|
+
## What AgentDiff Is
|
|
9
|
+
|
|
10
|
+
* **A Trajectory Diff Engine:** Compares Run A (Baseline) against Run B (Candidate) across their execution Directed Acyclic Graphs (DAGs).
|
|
11
|
+
* **A Local-First CI/CD Gate:** Runs locally in your terminal or inside `pytest` and GitHub Actions, raising errors or exit codes on regression violations.
|
|
12
|
+
* **A Universal Comparator:** Ingests telemetry run files from **OpenInference/OTel**, **Langfuse**, **LangSmith**, **OpenAI Agents SDK**, or raw/custom JSON.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Install the PyPI package:
|
|
17
|
+
```bash
|
|
18
|
+
pip install agent-trajectory-diff
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or using `uv`:
|
|
22
|
+
```bash
|
|
23
|
+
uv add agent-trajectory-diff
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
### 1. CLI Usage
|
|
29
|
+
|
|
30
|
+
Compare two trajectory JSON traces from your terminal:
|
|
31
|
+
```bash
|
|
32
|
+
agentdiff baseline_run.json candidate_run.json --fail-on-regression --max-divergence 0.25
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
- `--adapter`: Telemetry parser to use (`auto`, `generic`, `openinference`, `langfuse`, `langsmith`, `openai_agents`).
|
|
37
|
+
- `--format`: Format for the output (`terminal`, `json`, `markdown`).
|
|
38
|
+
- `--fail-on-regression`: Return exit code `1` if thresholds are violated.
|
|
39
|
+
- `--max-loops`: Maximum loops allowed.
|
|
40
|
+
- `--max-divergence`: Maximum Trajectory Divergence Index (TDI) allowed.
|
|
41
|
+
- `--max-cost-delta`: Maximum cost increase percentage allowed.
|
|
42
|
+
- `--baseline, -b PATH`: Compare against a persistent baseline trace file (see [Baseline workflow](#baseline-workflow)).
|
|
43
|
+
- `--update-baseline`: Overwrite the persistent baseline with the candidate after a clean diff.
|
|
44
|
+
- `--config PATH`: Load defaults from an `agentdiff.toml` (auto-discovered if not given).
|
|
45
|
+
|
|
46
|
+
#### Config-as-code (`agentdiff.toml`)
|
|
47
|
+
|
|
48
|
+
Commit your thresholds, adapter, and baseline path next to your traces instead of repeating CLI flags. Explicit flags always win over config.
|
|
49
|
+
|
|
50
|
+
```toml
|
|
51
|
+
[compare]
|
|
52
|
+
detect_loops = true
|
|
53
|
+
strict_tool_signatures = false
|
|
54
|
+
|
|
55
|
+
[adapter]
|
|
56
|
+
name = "auto" # auto, generic, openinference, langfuse, langsmith, openai_agents
|
|
57
|
+
|
|
58
|
+
[cli]
|
|
59
|
+
format = "terminal" # terminal, json, markdown, pr
|
|
60
|
+
baseline = "baselines/current.json"
|
|
61
|
+
max_loops = 0
|
|
62
|
+
max_divergence = 0.3
|
|
63
|
+
max_cost_delta = 10.0
|
|
64
|
+
|
|
65
|
+
[assertions] # defaults used by assert_no_regressions / pytest plugin
|
|
66
|
+
max_divergence = 0.25
|
|
67
|
+
max_cost_increase_pct = 5.0
|
|
68
|
+
allow_loops = false
|
|
69
|
+
max_wasted_effort = 0.1
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
AgentDiff auto-discovers `agentdiff.toml` from the current directory upward, or you can point at it explicitly with `--config`.
|
|
73
|
+
|
|
74
|
+
## Baseline workflow
|
|
75
|
+
|
|
76
|
+
Keep a single `baseline.json` file committed to your repo instead of hand-managing two trace files. The first run establishes the baseline; later runs compare against it and advance it only on clean diffs.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# First run: stores candidate as the baseline, exits 0
|
|
80
|
+
agentdiff baseline.json today.json --baseline baseline.json --update-baseline
|
|
81
|
+
|
|
82
|
+
# Later runs: compare today's run against the stored baseline
|
|
83
|
+
agentdiff baseline.json today.json --baseline baseline.json --update-baseline --fail-on-regression
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- If `baseline.json` does not exist and `--update-baseline` is set, the candidate is copied in as the baseline and the command exits `0`.
|
|
87
|
+
- If it does not exist and `--update-baseline` is omitted, the command exits `2` with a helpful message.
|
|
88
|
+
- On a regression the baseline is **never** overwritten, and `--fail-on-regression` exits `1`.
|
|
89
|
+
|
|
90
|
+
### 2. Python SDK & Pytest Integration
|
|
91
|
+
|
|
92
|
+
Catch agent loop regressions or token cost spikes in your test suites:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import pytest
|
|
96
|
+
from agentdiff import load_trace, compare
|
|
97
|
+
from agentdiff.testing import assert_no_regressions
|
|
98
|
+
|
|
99
|
+
def test_agent_refactor_efficiency():
|
|
100
|
+
# Load traces from disk (auto-detects the telemetry format)
|
|
101
|
+
baseline = load_trace("tests/traces/baseline.json")
|
|
102
|
+
candidate = load_trace("tests/traces/candidate.json")
|
|
103
|
+
|
|
104
|
+
# Run the comparison
|
|
105
|
+
report = compare(baseline, candidate)
|
|
106
|
+
|
|
107
|
+
# Expressive assertion helper that raises detailed error messages on regression
|
|
108
|
+
assert_no_regressions(
|
|
109
|
+
report,
|
|
110
|
+
max_divergence=0.25, # TDI threshold [0.0 - 1.0]
|
|
111
|
+
max_cost_increase_pct=5.0, # Max cost increase allowed
|
|
112
|
+
allow_loops=False, # Reject if tool loops are detected
|
|
113
|
+
max_wasted_effort=0.10 # Max Wasted Effort Index (WEI) allowed
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 3. GitHub Action
|
|
118
|
+
|
|
119
|
+
Gate a PR on agent trajectory regressions with the reusable composite action.
|
|
120
|
+
Pin it to a release tag and point `package` at the published package (or a
|
|
121
|
+
`git+` path / local directory for pre-release testing):
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
name: AgentDiff Gate
|
|
125
|
+
on:
|
|
126
|
+
pull_request:
|
|
127
|
+
|
|
128
|
+
jobs:
|
|
129
|
+
agentdiff:
|
|
130
|
+
runs-on: ubuntu-latest
|
|
131
|
+
steps:
|
|
132
|
+
- uses: actions/checkout@v4
|
|
133
|
+
- uses: actions/setup-python@v5
|
|
134
|
+
with:
|
|
135
|
+
python-version: "3.11"
|
|
136
|
+
- uses: lostmartian/agentdiff/.github/actions/agentdiff-check@v0.2.0
|
|
137
|
+
with:
|
|
138
|
+
baseline: traces/baseline.json # committed baseline trace
|
|
139
|
+
candidate: traces/candidate.json # generated by an earlier step
|
|
140
|
+
update-baseline: "false"
|
|
141
|
+
max-divergence: "0.3"
|
|
142
|
+
max-cost-delta: "10.0"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The action installs the package (default `agent-trajectory-diff` from PyPI),
|
|
146
|
+
runs `agentdiff --fail-on-regression`, and fails the job when divergence,
|
|
147
|
+
loops, or cost spikes exceed the thresholds.
|
|
148
|
+
|
|
149
|
+
**Available inputs:**
|
|
150
|
+
|
|
151
|
+
| Input | Default | Description |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| `baseline` | *(required)* | Path to the stored baseline trace JSON. |
|
|
154
|
+
| `candidate` | *(required)* | Path to the candidate trace JSON. |
|
|
155
|
+
| `package` | `agent-trajectory-diff` | Python package spec to install (PyPI name, `git+https://…`, or a local path). |
|
|
156
|
+
| `adapter` | `auto` | Telemetry adapter: `auto`, `generic`, `openinference`, `langfuse`, `langsmith`, `openai_agents`. |
|
|
157
|
+
| `max-divergence` | `0.3` | Maximum Trajectory Divergence Index (TDI) before regression. |
|
|
158
|
+
| `max-loops` | `0` | Maximum loop count before regression. |
|
|
159
|
+
| `max-cost-delta` | `10.0` | Maximum cost increase percentage before regression. |
|
|
160
|
+
| `update-baseline` | `false` | Overwrite the stored baseline with the candidate when the run is clean. |
|
|
161
|
+
|
|
162
|
+
## Core Metrics
|
|
163
|
+
|
|
164
|
+
| Metric | Target / Range | Algorithmic Definition |
|
|
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{}}$$ |
|
|
167
|
+
| **Wasted Effort Index (WEI)** | `0.0` (Optimal) to `1.0` (Total Waste) | $$\frac{\text{Count}(\text{Steps with status} \in \{\text{ERROR, RETRY, ABANDONED}\})}{\text{Total Execution Steps}}$$ |
|
|
168
|
+
| **Loop Buster Index (LBI)** | Integer ($\ge 0$) | Detects consecutive repeating sequences of tools with stagnant state changes. |
|
|
169
|
+
| **Resource Deltas ($\Delta\text{Res}$)** | Percentage ($\pm\%$) | Standard deltas for $\Delta\text{Tokens}$, $\Delta\text{Cost}$, and $\Delta\text{Latency}$. |
|
|
170
|
+
|
|
171
|
+
## Development & Operations
|
|
172
|
+
|
|
173
|
+
This project utilizes `uv` to manage environments and dependencies. Automation tasks are defined in the **[`Makefile`](file:///Users/lostmartian/Desktop/interview/agentdiff/Makefile)**:
|
|
174
|
+
|
|
175
|
+
- `make lint` / `make format`: Run Ruff linter checks and formatter.
|
|
176
|
+
- `make test`: Run pytest suite (including style & formatting assertions).
|
|
177
|
+
- `make build`: Package the library into source and wheel distributions in `dist/`.
|
|
178
|
+
- `make website-dev`: Start the Next.js landing and documentation site local server.
|
|
179
|
+
- `make website-build`: Build the Next.js static output in `website/out/`.
|
|
180
|
+
|
|
181
|
+
### Repository Layout
|
|
182
|
+
- `src/`: Python source code package modules.
|
|
183
|
+
- `tests/`: Quality assurance unit tests.
|
|
184
|
+
- `website/`: Next.js web application and documentation pages.
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agent-trajectory-diff"
|
|
3
|
-
version = "0.1.0"
|
|
4
3
|
description = "A developer-first package to evaluate and find regressions in agent trajectories"
|
|
5
4
|
readme = "README.md"
|
|
5
|
+
dynamic = ["version"]
|
|
6
6
|
authors = [
|
|
7
7
|
{ name = "Sahil Gangurde", email = "sahilgangurde08@gmail.com" }
|
|
8
8
|
]
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
|
-
license =
|
|
10
|
+
license = "MIT"
|
|
11
11
|
dependencies = [
|
|
12
12
|
"pydantic>=2.0",
|
|
13
13
|
"networkx>=3.0",
|
|
14
14
|
"typer>=0.9",
|
|
15
15
|
"rich>=13.0",
|
|
16
|
+
"tomli>=2.0; python_version < '3.11'",
|
|
16
17
|
]
|
|
17
18
|
classifiers = [
|
|
18
19
|
"Programming Language :: Python :: 3",
|
|
19
|
-
"License :: OSI Approved :: Apache Software License",
|
|
20
20
|
"License :: OSI Approved :: MIT License",
|
|
21
21
|
"Operating System :: OS Independent",
|
|
22
22
|
]
|
|
@@ -24,13 +24,18 @@ classifiers = [
|
|
|
24
24
|
[project.scripts]
|
|
25
25
|
agentdiff = "agentdiff.cli:main"
|
|
26
26
|
|
|
27
|
+
[project.entry-points.pytest11]
|
|
28
|
+
agentdiff = "agentdiff.pytest_plugin"
|
|
29
|
+
|
|
27
30
|
[project.urls]
|
|
28
31
|
Homepage = "https://github.com/lostmartian/agentdiff"
|
|
29
32
|
Repository = "https://github.com/lostmartian/agentdiff"
|
|
30
33
|
|
|
31
34
|
[dependency-groups]
|
|
32
35
|
dev = [
|
|
36
|
+
"hypothesis>=6.165.10",
|
|
33
37
|
"pytest>=8.0",
|
|
38
|
+
"pytest-cov>=7.1.0",
|
|
34
39
|
"ruff>=0.16.3",
|
|
35
40
|
]
|
|
36
41
|
|
|
@@ -38,6 +43,9 @@ dev = [
|
|
|
38
43
|
requires = ["hatchling"]
|
|
39
44
|
build-backend = "hatchling.build"
|
|
40
45
|
|
|
46
|
+
[tool.hatch.version]
|
|
47
|
+
path = "src/agentdiff/__init__.py"
|
|
48
|
+
|
|
41
49
|
[tool.hatch.build.targets.sdist]
|
|
42
50
|
include = [
|
|
43
51
|
"/src",
|
|
@@ -66,3 +74,7 @@ ignore = ["E501"]
|
|
|
66
74
|
[tool.ruff.format]
|
|
67
75
|
quote-style = "double"
|
|
68
76
|
indent-style = "space"
|
|
77
|
+
|
|
78
|
+
[tool.pytest.ini_options]
|
|
79
|
+
addopts = "-p pytester"
|
|
80
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""AgentDiff — regression testing for multi-turn, tool-using AI agents.
|
|
2
|
+
|
|
3
|
+
Compare two agent execution trajectories (the DAG of tool calls, LLM calls, and
|
|
4
|
+
decisions) head-to-head to catch regressions in CI.
|
|
5
|
+
|
|
6
|
+
Public API
|
|
7
|
+
----------
|
|
8
|
+
- ``compare(baseline, candidate)`` -> ``DiffReport``
|
|
9
|
+
Structural diff (alignment + TDI/WEI + resource deltas + loop detection).
|
|
10
|
+
- ``assert_no_regressions(report, ...)``
|
|
11
|
+
Threshold gates (divergence, cost, loops, wasted effort) for pytest.
|
|
12
|
+
- ``load_trace(path, adapter="auto")`` -> ``AgentTrace``
|
|
13
|
+
Load a trace from JSON, auto-detecting the telemetry format.
|
|
14
|
+
- ``parse_trace_data(data, adapter="auto")`` -> ``AgentTrace``
|
|
15
|
+
Parse an in-memory dict instead of a file.
|
|
16
|
+
- Adapters: ``GenericAdapter``, ``LangfuseAdapter``, ``LangSmithAdapter``,
|
|
17
|
+
``OpenInferenceAdapter``, ``OpenAIAgentsAdapter`` (all expose
|
|
18
|
+
``from_dict`` / ``from_file``).
|
|
19
|
+
- ``load_config(path=None)`` -> ``AgentDiffConfig``
|
|
20
|
+
Load defaults from ``agentdiff.toml`` (thresholds, adapter, baseline).
|
|
21
|
+
- Models: ``AgentTrace`` (canonical, ``schema_version``-ed), ``DiffReport``,
|
|
22
|
+
``StepDiff``/``StepDiffStatus``, ``TraceStep``/``StepStatus``/``StepType``.
|
|
23
|
+
|
|
24
|
+
Typical flow::
|
|
25
|
+
|
|
26
|
+
baseline = load_trace("baseline.json")
|
|
27
|
+
candidate = load_trace("candidate.json")
|
|
28
|
+
report = compare(baseline, candidate)
|
|
29
|
+
assert_no_regressions(report)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
__version__ = "0.2.0"
|
|
33
|
+
|
|
34
|
+
from agentdiff.adapters import (
|
|
35
|
+
BaseAdapter,
|
|
36
|
+
GenericAdapter,
|
|
37
|
+
LangfuseAdapter,
|
|
38
|
+
LangSmithAdapter,
|
|
39
|
+
OpenAIAgentsAdapter,
|
|
40
|
+
OpenInferenceAdapter,
|
|
41
|
+
)
|
|
42
|
+
from agentdiff.ci.baseline import decide_rotation
|
|
43
|
+
from agentdiff.config import AgentDiffConfig, load_config
|
|
44
|
+
from agentdiff.engine import compare
|
|
45
|
+
from agentdiff.engine.explanations import (
|
|
46
|
+
format_explanations,
|
|
47
|
+
generate_explanations,
|
|
48
|
+
locate_culprit,
|
|
49
|
+
)
|
|
50
|
+
from agentdiff.engine.tree import render_tree
|
|
51
|
+
from agentdiff.loader import load_trace, parse_trace_data
|
|
52
|
+
from agentdiff.models import (
|
|
53
|
+
AgentTrace,
|
|
54
|
+
DiffReport,
|
|
55
|
+
StepDiff,
|
|
56
|
+
StepDiffStatus,
|
|
57
|
+
StepStatus,
|
|
58
|
+
StepType,
|
|
59
|
+
TokenUsage,
|
|
60
|
+
TraceStep,
|
|
61
|
+
)
|
|
62
|
+
from agentdiff.reporters.pr import generate_pr_markdown
|
|
63
|
+
from agentdiff.testing import assert_no_regressions
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"AgentDiffConfig",
|
|
67
|
+
"AgentTrace",
|
|
68
|
+
"BaseAdapter",
|
|
69
|
+
"DiffReport",
|
|
70
|
+
"GenericAdapter",
|
|
71
|
+
"LangSmithAdapter",
|
|
72
|
+
"LangfuseAdapter",
|
|
73
|
+
"OpenAIAgentsAdapter",
|
|
74
|
+
"OpenInferenceAdapter",
|
|
75
|
+
"StepDiff",
|
|
76
|
+
"StepDiffStatus",
|
|
77
|
+
"StepStatus",
|
|
78
|
+
"StepType",
|
|
79
|
+
"TokenUsage",
|
|
80
|
+
"TraceStep",
|
|
81
|
+
"__version__",
|
|
82
|
+
"assert_no_regressions",
|
|
83
|
+
"compare",
|
|
84
|
+
"decide_rotation",
|
|
85
|
+
"format_explanations",
|
|
86
|
+
"generate_explanations",
|
|
87
|
+
"generate_pr_markdown",
|
|
88
|
+
"load_config",
|
|
89
|
+
"load_trace",
|
|
90
|
+
"locate_culprit",
|
|
91
|
+
"parse_trace_data",
|
|
92
|
+
"render_tree",
|
|
93
|
+
]
|
{agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/adapters/__init__.py
RENAMED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from agentdiff.adapters.base import BaseAdapter
|
|
2
|
-
from agentdiff.adapters.deepeval import DeepEvalAdapter
|
|
3
2
|
from agentdiff.adapters.generic import GenericAdapter
|
|
4
3
|
from agentdiff.adapters.langfuse import LangfuseAdapter
|
|
4
|
+
from agentdiff.adapters.langsmith import LangSmithAdapter
|
|
5
|
+
from agentdiff.adapters.openai_agents import OpenAIAgentsAdapter
|
|
5
6
|
from agentdiff.adapters.openinference import OpenInferenceAdapter
|
|
6
7
|
|
|
7
8
|
__all__ = [
|
|
8
9
|
"BaseAdapter",
|
|
9
|
-
"DeepEvalAdapter",
|
|
10
10
|
"GenericAdapter",
|
|
11
|
+
"LangSmithAdapter",
|
|
11
12
|
"LangfuseAdapter",
|
|
13
|
+
"OpenAIAgentsAdapter",
|
|
12
14
|
"OpenInferenceAdapter",
|
|
13
15
|
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Robust ISO-8601 timestamp parsing shared across telemetry adapters.
|
|
2
|
+
|
|
3
|
+
Python's ``datetime.fromisoformat`` is strict about fractional seconds on
|
|
4
|
+
Python 3.10 (it requires exactly 6 digits). Telemetry timestamps commonly use
|
|
5
|
+
a short fractional part (e.g. ``...00.5Z``) or ``Z``/``+00:00`` offsets. This
|
|
6
|
+
helper normalizes both so latency math is identical across Python 3.10+.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
_FRACTION = re.compile(r"\.(\d+)")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_iso_timestamp(value: Any) -> datetime | None:
|
|
19
|
+
"""Parse a telemetry timestamp to an aware ``datetime``.
|
|
20
|
+
|
|
21
|
+
Returns ``None`` (never raises) when the value cannot be parsed.
|
|
22
|
+
"""
|
|
23
|
+
if value is None:
|
|
24
|
+
return None
|
|
25
|
+
text = str(value).replace("Z", "+00:00")
|
|
26
|
+
if "." in text:
|
|
27
|
+
text = _FRACTION.sub(lambda m: "." + m.group(1).ljust(6, "0"), text, count=1)
|
|
28
|
+
try:
|
|
29
|
+
return datetime.fromisoformat(text)
|
|
30
|
+
except (ValueError, TypeError):
|
|
31
|
+
return None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from agentdiff.models.trace import AgentTrace
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseAdapter(ABC):
|
|
9
|
+
"""Converts a provider-specific trace format into a canonical :class:`AgentTrace`.
|
|
10
|
+
|
|
11
|
+
Subclasses implement :meth:`from_dict`, mapping a raw provider payload into
|
|
12
|
+
the normalized schema. :meth:`from_file` loads a JSON file and delegates.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def from_dict(cls, data: dict[str, Any]) -> AgentTrace:
|
|
18
|
+
"""Parses a raw dictionary input into a canonical AgentTrace."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def _as_float(cls, value: Any, default: float = 0.0) -> float:
|
|
23
|
+
"""Coerces an arbitrary value to float, falling back on malformed input."""
|
|
24
|
+
try:
|
|
25
|
+
return float(value)
|
|
26
|
+
except (TypeError, ValueError):
|
|
27
|
+
return default
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def _as_int(cls, value: Any, default: int = 0) -> int:
|
|
31
|
+
"""Coerces an arbitrary value to int, falling back on malformed input."""
|
|
32
|
+
try:
|
|
33
|
+
return int(value)
|
|
34
|
+
except (TypeError, ValueError):
|
|
35
|
+
return default
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_file(cls, filepath: str) -> AgentTrace:
|
|
39
|
+
"""Loads a JSON file and parses it into a canonical AgentTrace."""
|
|
40
|
+
with open(filepath, encoding="utf-8") as f:
|
|
41
|
+
data = json.load(f)
|
|
42
|
+
return cls.from_dict(data)
|