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.
Files changed (46) hide show
  1. agent_trajectory_diff-0.2.0/LICENSE +21 -0
  2. agent_trajectory_diff-0.2.0/PKG-INFO +204 -0
  3. agent_trajectory_diff-0.2.0/README.md +184 -0
  4. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/pyproject.toml +15 -3
  5. agent_trajectory_diff-0.2.0/src/agentdiff/__init__.py +93 -0
  6. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/adapters/__init__.py +4 -2
  7. agent_trajectory_diff-0.2.0/src/agentdiff/adapters/_iso.py +31 -0
  8. agent_trajectory_diff-0.2.0/src/agentdiff/adapters/base.py +42 -0
  9. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/adapters/langfuse.py +30 -19
  10. agent_trajectory_diff-0.2.0/src/agentdiff/adapters/langsmith.py +168 -0
  11. agent_trajectory_diff-0.2.0/src/agentdiff/adapters/openai_agents.py +260 -0
  12. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/adapters/openinference.py +29 -21
  13. agent_trajectory_diff-0.2.0/src/agentdiff/ci/baseline.py +59 -0
  14. agent_trajectory_diff-0.2.0/src/agentdiff/ci/github.py +66 -0
  15. agent_trajectory_diff-0.2.0/src/agentdiff/cli.py +276 -0
  16. agent_trajectory_diff-0.2.0/src/agentdiff/config.py +135 -0
  17. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/engine/aligner.py +19 -0
  18. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/engine/comparator.py +21 -1
  19. agent_trajectory_diff-0.2.0/src/agentdiff/engine/explanations.py +385 -0
  20. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/engine/loop_detector.py +5 -0
  21. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/engine/metrics.py +6 -0
  22. agent_trajectory_diff-0.2.0/src/agentdiff/engine/tree.py +101 -0
  23. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/loader.py +45 -13
  24. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/models/trace.py +4 -0
  25. agent_trajectory_diff-0.2.0/src/agentdiff/pytest_plugin.py +144 -0
  26. agent_trajectory_diff-0.2.0/src/agentdiff/reporters/pr.py +67 -0
  27. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/testing/assertions.py +10 -2
  28. agent_trajectory_diff-0.1.0/LICENSE +0 -622
  29. agent_trajectory_diff-0.1.0/PKG-INFO +0 -112
  30. agent_trajectory_diff-0.1.0/README.md +0 -92
  31. agent_trajectory_diff-0.1.0/src/agentdiff/__init__.py +0 -40
  32. agent_trajectory_diff-0.1.0/src/agentdiff/adapters/base.py +0 -20
  33. agent_trajectory_diff-0.1.0/src/agentdiff/adapters/deepeval.py +0 -134
  34. agent_trajectory_diff-0.1.0/src/agentdiff/cli.py +0 -120
  35. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/.gitignore +0 -0
  36. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/__main__.py +0 -0
  37. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/adapters/generic.py +0 -0
  38. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/engine/__init__.py +0 -0
  39. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/models/__init__.py +0 -0
  40. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/models/report.py +0 -0
  41. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/models/step.py +0 -0
  42. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/py.typed +0 -0
  43. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/reporters/__init__.py +0 -0
  44. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/reporters/markdown.py +0 -0
  45. {agent_trajectory_diff-0.1.0 → agent_trajectory_diff-0.2.0}/src/agentdiff/reporters/terminal.py +0 -0
  46. {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: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
24
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue.svg)](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: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue.svg)](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 = { text = "Apache-2.0 OR MIT" }
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
+ ]
@@ -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)