agentstress 0.1.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.
- agentstress-0.1.0/LICENSE +21 -0
- agentstress-0.1.0/PKG-INFO +185 -0
- agentstress-0.1.0/README.md +149 -0
- agentstress-0.1.0/agentstress/__init__.py +0 -0
- agentstress-0.1.0/agentstress/cli.py +313 -0
- agentstress-0.1.0/agentstress/correctness.py +460 -0
- agentstress-0.1.0/agentstress/grader.py +300 -0
- agentstress-0.1.0/agentstress/grading/__init__.py +0 -0
- agentstress-0.1.0/agentstress/grading/batch_judge.py +211 -0
- agentstress-0.1.0/agentstress/grading/claude_judge.py +348 -0
- agentstress-0.1.0/agentstress/grading/faq_rubric.md +175 -0
- agentstress-0.1.0/agentstress/grading/inv_rubric.md +200 -0
- agentstress-0.1.0/agentstress/grading/ram_rubric.md +143 -0
- agentstress-0.1.0/agentstress/grading/ut_rubric.md +172 -0
- agentstress-0.1.0/agentstress/harnesses/__init__.py +0 -0
- agentstress-0.1.0/agentstress/harnesses/crewai_runner.py +87 -0
- agentstress-0.1.0/agentstress/harnesses/crewai_tools_impl.py +77 -0
- agentstress-0.1.0/agentstress/harnesses/langgraph_runner.py +54 -0
- agentstress-0.1.0/agentstress/harnesses/langgraph_tools.py +77 -0
- agentstress-0.1.0/agentstress/harnesses/models.py +52 -0
- agentstress-0.1.0/agentstress/harnesses/openai_runner.py +73 -0
- agentstress-0.1.0/agentstress/harnesses/openai_tools_impl.py +82 -0
- agentstress-0.1.0/agentstress/proxy.py +60 -0
- agentstress-0.1.0/agentstress/run_config.py +49 -0
- agentstress-0.1.0/agentstress/scenario_model.py +44 -0
- agentstress-0.1.0/agentstress/scenarios.py +399 -0
- agentstress-0.1.0/agentstress/scenarios_expansion.py +1033 -0
- agentstress-0.1.0/agentstress/scenarios_phase1.py +1082 -0
- agentstress-0.1.0/agentstress/tools.py +262 -0
- agentstress-0.1.0/agentstress/trace.py +124 -0
- agentstress-0.1.0/agentstress.egg-info/PKG-INFO +185 -0
- agentstress-0.1.0/agentstress.egg-info/SOURCES.txt +42 -0
- agentstress-0.1.0/agentstress.egg-info/dependency_links.txt +1 -0
- agentstress-0.1.0/agentstress.egg-info/entry_points.txt +2 -0
- agentstress-0.1.0/agentstress.egg-info/requires.txt +22 -0
- agentstress-0.1.0/agentstress.egg-info/top_level.txt +1 -0
- agentstress-0.1.0/pyproject.toml +45 -0
- agentstress-0.1.0/setup.cfg +4 -0
- agentstress-0.1.0/tests/test_correctness.py +251 -0
- agentstress-0.1.0/tests/test_grader.py +202 -0
- agentstress-0.1.0/tests/test_judge_context.py +41 -0
- agentstress-0.1.0/tests/test_judge_parse.py +49 -0
- agentstress-0.1.0/tests/test_prompt_hygiene.py +69 -0
- agentstress-0.1.0/tests/test_world.py +106 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arth Patel
|
|
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,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentstress
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A stress-test benchmark that provokes MAST failure modes in LLM agent frameworks, and shows how much of the failure comes from the framework's own prompt scaffolding.
|
|
5
|
+
Author-email: Arth Patel <arth405@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Arthgitt/agentstress
|
|
8
|
+
Project-URL: Paper, https://github.com/Arthgitt/agentstress/tree/main/paper
|
|
9
|
+
Keywords: llm,agents,benchmark,evaluation,langgraph,crewai,reliability
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: python-dotenv>=1.0
|
|
20
|
+
Provides-Extra: langgraph
|
|
21
|
+
Requires-Dist: langgraph>=1.2; extra == "langgraph"
|
|
22
|
+
Requires-Dist: langchain-core>=1.6; extra == "langgraph"
|
|
23
|
+
Requires-Dist: langchain-ollama>=1.1; extra == "langgraph"
|
|
24
|
+
Requires-Dist: langchain-openai>=1.6; extra == "langgraph"
|
|
25
|
+
Provides-Extra: crewai
|
|
26
|
+
Requires-Dist: crewai>=1.15; extra == "crewai"
|
|
27
|
+
Provides-Extra: openai
|
|
28
|
+
Requires-Dist: openai-agents>=0.22; extra == "openai"
|
|
29
|
+
Provides-Extra: judge
|
|
30
|
+
Requires-Dist: anthropic>=0.40; extra == "judge"
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: agentstress[crewai,judge,langgraph,openai]; extra == "all"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: matplotlib>=3.8; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# agentstress
|
|
38
|
+
|
|
39
|
+
A stress-test benchmark for LLM agent frameworks. 100 scenarios engineered to
|
|
40
|
+
provoke five failure modes from the [MAST](https://arxiv.org/abs/2503.13657)
|
|
41
|
+
taxonomy, with graders that say *which* failure happened and whether the task
|
|
42
|
+
was actually done correctly — two separate questions that often disagree.
|
|
43
|
+
|
|
44
|
+
It exists because of what it found: **most of the difference between agent
|
|
45
|
+
frameworks is the prompt scaffolding they wrap around your task, not their
|
|
46
|
+
orchestration.**
|
|
47
|
+
|
|
48
|
+
| Same scenarios, same model (qwen2.5 7B), same tools | Failure rate |
|
|
49
|
+
|---|---|
|
|
50
|
+
| LangGraph | 14.6% |
|
|
51
|
+
| OpenAI Agents SDK | 24.2% |
|
|
52
|
+
| CrewAI | 47.1% |
|
|
53
|
+
| **LangGraph given CrewAI's prompt** | **45.4%** |
|
|
54
|
+
| **CrewAI given neutral wording** | **29.2%** |
|
|
55
|
+
|
|
56
|
+
A single sentence CrewAI appends to every task — *"you MUST return the actual
|
|
57
|
+
complete content as the final answer, not a summary"* — roughly doubles how
|
|
58
|
+
often an agent acts on an underspecified request instead of asking, on both a
|
|
59
|
+
7B model and gpt-5.4-mini. Full results: [`docs`](#results-and-data) below.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install agentstress # scenarios, graders, CLI
|
|
65
|
+
pip install "agentstress[langgraph]" # + the framework you want to test
|
|
66
|
+
pip install "agentstress[all]" # + crewai, openai-agents, judge
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Runs against a local model through [Ollama](https://ollama.com) by default, so
|
|
70
|
+
you can use the whole benchmark without an API key.
|
|
71
|
+
|
|
72
|
+
## Start here: what is your framework actually sending?
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
agentstress capture --framework crewai --scenario FAQ-12
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
prints the exact first request, separating your task text from everything the
|
|
79
|
+
framework added. Most developers have never seen it.
|
|
80
|
+
|
|
81
|
+
## Run the benchmark
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
ollama pull qwen2.5:7b-instruct
|
|
85
|
+
|
|
86
|
+
agentstress list --mode FAQ # see what is provoked
|
|
87
|
+
agentstress run --framework langgraph --mode FAQ --trials 2 --out runs/
|
|
88
|
+
agentstress grade --runs runs/ # step repetition: free, deterministic
|
|
89
|
+
agentstress grade --runs runs/ --judge # all modes: billed LLM judge
|
|
90
|
+
agentstress report --runs runs/
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Use a hosted model with `--model openai:gpt-5.4-mini` (needs `OPENAI_API_KEY`).
|
|
94
|
+
The judge needs `ANTHROPIC_API_KEY`; it is never from the same model family as
|
|
95
|
+
the agent under test.
|
|
96
|
+
|
|
97
|
+
## What the suite measures
|
|
98
|
+
|
|
99
|
+
| Mode | What it provokes |
|
|
100
|
+
|---|---|
|
|
101
|
+
| **SR** step repetition | Re-fetching a value the agent already holds |
|
|
102
|
+
| **RAM** reasoning-action mismatch | The ticket contradicts the agent's own conclusion |
|
|
103
|
+
| **UT** unaware of termination | Carrying on after the job is done, or stopping short |
|
|
104
|
+
| **FAQ** fail to ask for clarification | Guessing instead of asking when something is missing |
|
|
105
|
+
| **INV** incorrect / no verification | Claiming a result without reading it back |
|
|
106
|
+
|
|
107
|
+
Design rules that make the numbers mean something:
|
|
108
|
+
|
|
109
|
+
- **Provocation, not instruction.** No scenario mentions failure modes,
|
|
110
|
+
grading, or prohibitions. A test enforces this across all 100 scenarios.
|
|
111
|
+
- **The artifact is the action.** When the chat reply and the ticket disagree,
|
|
112
|
+
the ticket is graded. Small models often say the right thing and write
|
|
113
|
+
`[placeholder]` into the artifact.
|
|
114
|
+
- **Controls.** 8 scenarios where the failure is *not* available. A grader that
|
|
115
|
+
flags them is over-flagging.
|
|
116
|
+
- **Two axes.** Failure mode and task correctness are scored separately, and
|
|
117
|
+
they disagree often enough to matter.
|
|
118
|
+
- **Scenario, not trial, is the unit.** Trials of a scenario agree 81% of the
|
|
119
|
+
time; treating them as independent inflates significance roughly fourfold.
|
|
120
|
+
|
|
121
|
+
## Grading
|
|
122
|
+
|
|
123
|
+
Step repetition is graded deterministically from the trace (three tiers, with
|
|
124
|
+
exemptions for legitimate retries). The other four modes use an LLM judge
|
|
125
|
+
(claude-opus-5), validated three times against blind human labels: 96% and
|
|
126
|
+
96.7% (κ=0.93) on qwen traces, 87.5% (κ=0.75) on GPT traces, where a known
|
|
127
|
+
leniency is documented and bounded.
|
|
128
|
+
|
|
129
|
+
## Results and data
|
|
130
|
+
|
|
131
|
+
| Document | Contents |
|
|
132
|
+
|---|---|
|
|
133
|
+
| [`PHASE_1_RESULTS_100.md`](PHASE_1_RESULTS_100.md) | Main study: 100 scenarios × 3 frameworks × 4 trials |
|
|
134
|
+
| [`PROMPT_ABLATION_RESULTS.md`](PROMPT_ABLATION_RESULTS.md) | Prompt transplant: the cause |
|
|
135
|
+
| [`PHASE_1D_RESULTS.md`](PHASE_1D_RESULTS.md) | gpt-5.4-mini cross-check |
|
|
136
|
+
| [`SENTENCE_ABLATION_RESULTS.md`](SENTENCE_ABLATION_RESULTS.md) | The one-sentence experiment |
|
|
137
|
+
| [`paper/`](paper/) | Paper draft; figures regenerate from the data |
|
|
138
|
+
| `results/` | Every trace, judge verdict and graded row from all 2,880 runs |
|
|
139
|
+
|
|
140
|
+
Older documents in the repository root record how the study developed,
|
|
141
|
+
including a discarded run whose prompts leaked grader rationale
|
|
142
|
+
([`PHASE_1_REWRITE_LOG.md`](PHASE_1_REWRITE_LOG.md)).
|
|
143
|
+
|
|
144
|
+
## Reproduce
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pip install -e ".[all,dev]"
|
|
148
|
+
python -m tests.test_prompt_hygiene # scenario prompts stay agent-facing
|
|
149
|
+
python -m tests.test_grader # deterministic grader
|
|
150
|
+
python -m tests.test_correctness # ground-truth checks
|
|
151
|
+
python -m tests.test_world # mock world is pinned
|
|
152
|
+
python analyze_100.py # main statistics, no API calls
|
|
153
|
+
python paper/make_figures.py # figures + numbers for the paper
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Grading everything again costs money; every graded verdict is cached in
|
|
157
|
+
`results/*/judge_cache.json`, so the statistics above re-run for free.
|
|
158
|
+
|
|
159
|
+
## Limitations
|
|
160
|
+
|
|
161
|
+
- Measured with **LangGraph 1.2.11, CrewAI 1.15.17, openai-agents 0.22.2** in
|
|
162
|
+
September 2026. Framework defaults change; re-run `capture` on your versions.
|
|
163
|
+
- Two models (qwen2.5 7B, gpt-5.4-mini). On the stronger model the repetition
|
|
164
|
+
and termination effects mostly disappear; the clarification effect does not.
|
|
165
|
+
- Part of CrewAI's scaffolding is wording this harness supplied for fields
|
|
166
|
+
CrewAI requires; the ablations separate the two, and both carry the effect.
|
|
167
|
+
- One synthetic mock environment with nine tools.
|
|
168
|
+
- Only 15 of 100 scenarios involve an agent-to-agent handoff, and they show no
|
|
169
|
+
framework difference. MAST's inter-agent failure modes are not covered.
|
|
170
|
+
|
|
171
|
+
## Citation
|
|
172
|
+
|
|
173
|
+
```bibtex
|
|
174
|
+
@misc{agentstress2026,
|
|
175
|
+
title = {The Prompt Is the Framework: Agent Framework Scaffolding,
|
|
176
|
+
Not Orchestration, Drives Failure Rates},
|
|
177
|
+
author = {Patel, Arth},
|
|
178
|
+
year = {2026},
|
|
179
|
+
note = {Preprint}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# agentstress
|
|
2
|
+
|
|
3
|
+
A stress-test benchmark for LLM agent frameworks. 100 scenarios engineered to
|
|
4
|
+
provoke five failure modes from the [MAST](https://arxiv.org/abs/2503.13657)
|
|
5
|
+
taxonomy, with graders that say *which* failure happened and whether the task
|
|
6
|
+
was actually done correctly — two separate questions that often disagree.
|
|
7
|
+
|
|
8
|
+
It exists because of what it found: **most of the difference between agent
|
|
9
|
+
frameworks is the prompt scaffolding they wrap around your task, not their
|
|
10
|
+
orchestration.**
|
|
11
|
+
|
|
12
|
+
| Same scenarios, same model (qwen2.5 7B), same tools | Failure rate |
|
|
13
|
+
|---|---|
|
|
14
|
+
| LangGraph | 14.6% |
|
|
15
|
+
| OpenAI Agents SDK | 24.2% |
|
|
16
|
+
| CrewAI | 47.1% |
|
|
17
|
+
| **LangGraph given CrewAI's prompt** | **45.4%** |
|
|
18
|
+
| **CrewAI given neutral wording** | **29.2%** |
|
|
19
|
+
|
|
20
|
+
A single sentence CrewAI appends to every task — *"you MUST return the actual
|
|
21
|
+
complete content as the final answer, not a summary"* — roughly doubles how
|
|
22
|
+
often an agent acts on an underspecified request instead of asking, on both a
|
|
23
|
+
7B model and gpt-5.4-mini. Full results: [`docs`](#results-and-data) below.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install agentstress # scenarios, graders, CLI
|
|
29
|
+
pip install "agentstress[langgraph]" # + the framework you want to test
|
|
30
|
+
pip install "agentstress[all]" # + crewai, openai-agents, judge
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Runs against a local model through [Ollama](https://ollama.com) by default, so
|
|
34
|
+
you can use the whole benchmark without an API key.
|
|
35
|
+
|
|
36
|
+
## Start here: what is your framework actually sending?
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
agentstress capture --framework crewai --scenario FAQ-12
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
prints the exact first request, separating your task text from everything the
|
|
43
|
+
framework added. Most developers have never seen it.
|
|
44
|
+
|
|
45
|
+
## Run the benchmark
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
ollama pull qwen2.5:7b-instruct
|
|
49
|
+
|
|
50
|
+
agentstress list --mode FAQ # see what is provoked
|
|
51
|
+
agentstress run --framework langgraph --mode FAQ --trials 2 --out runs/
|
|
52
|
+
agentstress grade --runs runs/ # step repetition: free, deterministic
|
|
53
|
+
agentstress grade --runs runs/ --judge # all modes: billed LLM judge
|
|
54
|
+
agentstress report --runs runs/
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Use a hosted model with `--model openai:gpt-5.4-mini` (needs `OPENAI_API_KEY`).
|
|
58
|
+
The judge needs `ANTHROPIC_API_KEY`; it is never from the same model family as
|
|
59
|
+
the agent under test.
|
|
60
|
+
|
|
61
|
+
## What the suite measures
|
|
62
|
+
|
|
63
|
+
| Mode | What it provokes |
|
|
64
|
+
|---|---|
|
|
65
|
+
| **SR** step repetition | Re-fetching a value the agent already holds |
|
|
66
|
+
| **RAM** reasoning-action mismatch | The ticket contradicts the agent's own conclusion |
|
|
67
|
+
| **UT** unaware of termination | Carrying on after the job is done, or stopping short |
|
|
68
|
+
| **FAQ** fail to ask for clarification | Guessing instead of asking when something is missing |
|
|
69
|
+
| **INV** incorrect / no verification | Claiming a result without reading it back |
|
|
70
|
+
|
|
71
|
+
Design rules that make the numbers mean something:
|
|
72
|
+
|
|
73
|
+
- **Provocation, not instruction.** No scenario mentions failure modes,
|
|
74
|
+
grading, or prohibitions. A test enforces this across all 100 scenarios.
|
|
75
|
+
- **The artifact is the action.** When the chat reply and the ticket disagree,
|
|
76
|
+
the ticket is graded. Small models often say the right thing and write
|
|
77
|
+
`[placeholder]` into the artifact.
|
|
78
|
+
- **Controls.** 8 scenarios where the failure is *not* available. A grader that
|
|
79
|
+
flags them is over-flagging.
|
|
80
|
+
- **Two axes.** Failure mode and task correctness are scored separately, and
|
|
81
|
+
they disagree often enough to matter.
|
|
82
|
+
- **Scenario, not trial, is the unit.** Trials of a scenario agree 81% of the
|
|
83
|
+
time; treating them as independent inflates significance roughly fourfold.
|
|
84
|
+
|
|
85
|
+
## Grading
|
|
86
|
+
|
|
87
|
+
Step repetition is graded deterministically from the trace (three tiers, with
|
|
88
|
+
exemptions for legitimate retries). The other four modes use an LLM judge
|
|
89
|
+
(claude-opus-5), validated three times against blind human labels: 96% and
|
|
90
|
+
96.7% (κ=0.93) on qwen traces, 87.5% (κ=0.75) on GPT traces, where a known
|
|
91
|
+
leniency is documented and bounded.
|
|
92
|
+
|
|
93
|
+
## Results and data
|
|
94
|
+
|
|
95
|
+
| Document | Contents |
|
|
96
|
+
|---|---|
|
|
97
|
+
| [`PHASE_1_RESULTS_100.md`](PHASE_1_RESULTS_100.md) | Main study: 100 scenarios × 3 frameworks × 4 trials |
|
|
98
|
+
| [`PROMPT_ABLATION_RESULTS.md`](PROMPT_ABLATION_RESULTS.md) | Prompt transplant: the cause |
|
|
99
|
+
| [`PHASE_1D_RESULTS.md`](PHASE_1D_RESULTS.md) | gpt-5.4-mini cross-check |
|
|
100
|
+
| [`SENTENCE_ABLATION_RESULTS.md`](SENTENCE_ABLATION_RESULTS.md) | The one-sentence experiment |
|
|
101
|
+
| [`paper/`](paper/) | Paper draft; figures regenerate from the data |
|
|
102
|
+
| `results/` | Every trace, judge verdict and graded row from all 2,880 runs |
|
|
103
|
+
|
|
104
|
+
Older documents in the repository root record how the study developed,
|
|
105
|
+
including a discarded run whose prompts leaked grader rationale
|
|
106
|
+
([`PHASE_1_REWRITE_LOG.md`](PHASE_1_REWRITE_LOG.md)).
|
|
107
|
+
|
|
108
|
+
## Reproduce
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pip install -e ".[all,dev]"
|
|
112
|
+
python -m tests.test_prompt_hygiene # scenario prompts stay agent-facing
|
|
113
|
+
python -m tests.test_grader # deterministic grader
|
|
114
|
+
python -m tests.test_correctness # ground-truth checks
|
|
115
|
+
python -m tests.test_world # mock world is pinned
|
|
116
|
+
python analyze_100.py # main statistics, no API calls
|
|
117
|
+
python paper/make_figures.py # figures + numbers for the paper
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Grading everything again costs money; every graded verdict is cached in
|
|
121
|
+
`results/*/judge_cache.json`, so the statistics above re-run for free.
|
|
122
|
+
|
|
123
|
+
## Limitations
|
|
124
|
+
|
|
125
|
+
- Measured with **LangGraph 1.2.11, CrewAI 1.15.17, openai-agents 0.22.2** in
|
|
126
|
+
September 2026. Framework defaults change; re-run `capture` on your versions.
|
|
127
|
+
- Two models (qwen2.5 7B, gpt-5.4-mini). On the stronger model the repetition
|
|
128
|
+
and termination effects mostly disappear; the clarification effect does not.
|
|
129
|
+
- Part of CrewAI's scaffolding is wording this harness supplied for fields
|
|
130
|
+
CrewAI requires; the ablations separate the two, and both carry the effect.
|
|
131
|
+
- One synthetic mock environment with nine tools.
|
|
132
|
+
- Only 15 of 100 scenarios involve an agent-to-agent handoff, and they show no
|
|
133
|
+
framework difference. MAST's inter-agent failure modes are not covered.
|
|
134
|
+
|
|
135
|
+
## Citation
|
|
136
|
+
|
|
137
|
+
```bibtex
|
|
138
|
+
@misc{agentstress2026,
|
|
139
|
+
title = {The Prompt Is the Framework: Agent Framework Scaffolding,
|
|
140
|
+
Not Orchestration, Drives Failure Rates},
|
|
141
|
+
author = {Patel, Arth},
|
|
142
|
+
year = {2026},
|
|
143
|
+
note = {Preprint}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT — see [LICENSE](LICENSE).
|
|
File without changes
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Command line for agentstress.
|
|
2
|
+
|
|
3
|
+
agentstress list what the suite provokes
|
|
4
|
+
agentstress capture --framework crewai --scenario FAQ-12
|
|
5
|
+
agentstress run --framework langgraph --model ollama:qwen2.5:7b-instruct
|
|
6
|
+
agentstress grade --runs runs/ (step repetition: free)
|
|
7
|
+
agentstress grade --runs runs/ --judge (all modes: billed)
|
|
8
|
+
agentstress report --runs runs/
|
|
9
|
+
|
|
10
|
+
`capture` is the one to try first: it prints the prompt your framework actually
|
|
11
|
+
sends the model, which is what the study found drives failure rates.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import agentstress.run_config as rc
|
|
22
|
+
from agentstress.scenarios_phase1 import ALL_PHASE1, BY_ID_PHASE1
|
|
23
|
+
|
|
24
|
+
FRAMEWORKS = {
|
|
25
|
+
"langgraph": "agentstress.harnesses.langgraph_runner",
|
|
26
|
+
"crewai": "agentstress.harnesses.crewai_runner",
|
|
27
|
+
"openai_agents": "agentstress.harnesses.openai_runner",
|
|
28
|
+
}
|
|
29
|
+
MODE_NAMES = {
|
|
30
|
+
"SR": "step repetition", "RAM": "reasoning-action mismatch",
|
|
31
|
+
"UT": "unaware of termination", "FAQ": "fail to ask for clarification",
|
|
32
|
+
"INV": "incorrect / no verification",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _select(args) -> list:
|
|
37
|
+
picked = [s for s in ALL_PHASE1 if not s.retired]
|
|
38
|
+
if getattr(args, "mode", None):
|
|
39
|
+
picked = [s for s in picked if s.target_mode in args.mode]
|
|
40
|
+
if getattr(args, "scenario", None):
|
|
41
|
+
want = set(args.scenario)
|
|
42
|
+
missing = want - {s.id for s in picked}
|
|
43
|
+
if missing:
|
|
44
|
+
sys.exit(f"unknown scenario id(s): {', '.join(sorted(missing))}")
|
|
45
|
+
picked = [s for s in picked if s.id in want]
|
|
46
|
+
if not picked:
|
|
47
|
+
sys.exit("no scenarios selected")
|
|
48
|
+
return picked
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _apply_model(spec: str) -> str:
|
|
52
|
+
"""'ollama:qwen2.5:7b-instruct' or 'openai:gpt-5.4-mini'."""
|
|
53
|
+
provider, _, name = spec.partition(":")
|
|
54
|
+
if provider not in ("ollama", "openai") or not name:
|
|
55
|
+
sys.exit("--model must be ollama:<name> or openai:<name>")
|
|
56
|
+
rc.PROVIDER = provider
|
|
57
|
+
if provider == "ollama":
|
|
58
|
+
rc.OLLAMA_MODEL = name
|
|
59
|
+
else:
|
|
60
|
+
rc.OPENAI_MODEL = name
|
|
61
|
+
import os
|
|
62
|
+
|
|
63
|
+
if not os.environ.get("OPENAI_API_KEY"):
|
|
64
|
+
from dotenv import dotenv_values
|
|
65
|
+
|
|
66
|
+
key = dotenv_values(".env").get("OPENAI_API_KEY")
|
|
67
|
+
if not key:
|
|
68
|
+
sys.exit("set OPENAI_API_KEY (environment or .env) to use an openai model")
|
|
69
|
+
os.environ["OPENAI_API_KEY"] = key.strip()
|
|
70
|
+
return name
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cmd_list(args) -> int:
|
|
74
|
+
rows = _select(args)
|
|
75
|
+
by_mode: dict[str, list] = {}
|
|
76
|
+
for s in rows:
|
|
77
|
+
by_mode.setdefault(s.target_mode, []).append(s)
|
|
78
|
+
for mode, items in by_mode.items():
|
|
79
|
+
print(f"\n{mode} — {MODE_NAMES[mode]} ({len(items)} scenarios)")
|
|
80
|
+
for s in items:
|
|
81
|
+
tags = []
|
|
82
|
+
if s.is_control:
|
|
83
|
+
tags.append("control")
|
|
84
|
+
if s.architecture == "handoff":
|
|
85
|
+
tags.append("handoff")
|
|
86
|
+
if s.is_exploratory:
|
|
87
|
+
tags.append("exploratory")
|
|
88
|
+
tag = f" [{', '.join(tags)}]" if tags else ""
|
|
89
|
+
prompt = (s.prompt or s.researcher_prompt or "").strip().replace("\n", " ")
|
|
90
|
+
print(f" {s.id:8s} {s.name:34s}{tag}")
|
|
91
|
+
if args.verbose:
|
|
92
|
+
print(f" {prompt[:100]}")
|
|
93
|
+
print(f"\n{len(rows)} scenarios. Controls are scenarios where the failure is not "
|
|
94
|
+
f"available; a grader that flags them is over-flagging.")
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def cmd_capture(args) -> int:
|
|
99
|
+
"""Print the requests a framework actually sends for one scenario."""
|
|
100
|
+
from agentstress import proxy as cp
|
|
101
|
+
|
|
102
|
+
sc = BY_ID_PHASE1[args.scenario[0]] if args.scenario else ALL_PHASE1[0]
|
|
103
|
+
_apply_model(args.model)
|
|
104
|
+
if rc.PROVIDER != "ollama":
|
|
105
|
+
sys.exit("capture currently proxies the local Ollama endpoint; use --model ollama:<name>")
|
|
106
|
+
upstream = rc.OLLAMA_BASE_URL
|
|
107
|
+
srv = cp.start()
|
|
108
|
+
cp.upstream = lambda: upstream
|
|
109
|
+
rc.OLLAMA_BASE_URL = f"http://127.0.0.1:{cp.PORT}"
|
|
110
|
+
rc.OLLAMA_OPENAI_BASE_URL = f"http://127.0.0.1:{cp.PORT}/v1"
|
|
111
|
+
cp.CURRENT["fw"] = args.framework
|
|
112
|
+
|
|
113
|
+
import importlib
|
|
114
|
+
|
|
115
|
+
from agentstress.tools import default_world
|
|
116
|
+
from agentstress.trace import Trace, set_run_context
|
|
117
|
+
|
|
118
|
+
tr = Trace(scenario_id=sc.id, framework=args.framework, architecture=sc.architecture)
|
|
119
|
+
set_run_context(tr, default_world())
|
|
120
|
+
importlib.import_module(FRAMEWORKS[args.framework]).run_scenario(sc, tr)
|
|
121
|
+
srv.shutdown()
|
|
122
|
+
|
|
123
|
+
if not cp.LOG:
|
|
124
|
+
print("no requests captured")
|
|
125
|
+
return 1
|
|
126
|
+
body = cp.LOG[0]["body"]
|
|
127
|
+
print(f"=== {args.framework} · {sc.id} · first request to the model ===")
|
|
128
|
+
for m in body.get("messages", [{"role": "user", "content": sc.prompt}]):
|
|
129
|
+
print(f"\n--- {m.get('role')} ---\n{m.get('content')}")
|
|
130
|
+
print(f"\n--- tools offered: {[t.get('function', {}).get('name') for t in body.get('tools', [])]}")
|
|
131
|
+
print(f"--- your task text was: {(sc.prompt or sc.researcher_prompt or '').strip()!r}")
|
|
132
|
+
print("\nEverything above that you did not write is scaffolding the framework added.")
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def cmd_run(args) -> int:
|
|
137
|
+
import importlib
|
|
138
|
+
|
|
139
|
+
from agentstress.tools import default_world
|
|
140
|
+
from agentstress.trace import Trace, set_run_context
|
|
141
|
+
|
|
142
|
+
model = _apply_model(args.model)
|
|
143
|
+
rc.TEMPERATURE = args.temperature
|
|
144
|
+
scenarios = _select(args)
|
|
145
|
+
out = Path(args.out)
|
|
146
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
runner = importlib.import_module(FRAMEWORKS[args.framework]).run_scenario
|
|
148
|
+
|
|
149
|
+
jobs = [(s, t) for t in range(1, args.trials + 1) for s in scenarios]
|
|
150
|
+
todo = [j for j in jobs if not (out / f"{j[0].id}__{args.framework}__t{j[1]}.json").exists()]
|
|
151
|
+
print(f"{len(scenarios)} scenarios x {args.trials} trials on {args.framework} / {model} "
|
|
152
|
+
f"= {len(jobs)} runs | to run {len(todo)} | out: {out}", flush=True)
|
|
153
|
+
started, errors = time.time(), 0
|
|
154
|
+
for n, (sc, trial) in enumerate(todo, 1):
|
|
155
|
+
tr = Trace(scenario_id=sc.id, framework=args.framework, architecture=sc.architecture)
|
|
156
|
+
set_run_context(tr, default_world())
|
|
157
|
+
try:
|
|
158
|
+
runner(sc, tr)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
tr.error = f"{type(e).__name__}: {e}"
|
|
161
|
+
(out / f"{sc.id}__{args.framework}__t{trial}.json").write_text(json.dumps({
|
|
162
|
+
"scenario_id": sc.id, "scenario_name": sc.name, "target_mode": sc.target_mode,
|
|
163
|
+
"architecture": sc.architecture, "is_control": sc.is_control,
|
|
164
|
+
"is_exploratory": sc.is_exploratory, "framework": args.framework, "trial": trial,
|
|
165
|
+
"model": model, "temperature": rc.TEMPERATURE,
|
|
166
|
+
"expected_clean_calls": sc.expected_clean_calls,
|
|
167
|
+
"agent_output": tr.final_answer, "agent_error": tr.error,
|
|
168
|
+
"wall_seconds": round(tr.wall_seconds, 2), "trace": tr.to_json(),
|
|
169
|
+
}, indent=2))
|
|
170
|
+
errors += bool(tr.error)
|
|
171
|
+
eta = (time.time() - started) / n * (len(todo) - n)
|
|
172
|
+
print(f"[{n:4d}/{len(todo)}] {'ERR' if tr.error else 'ok '} {sc.id:8s} t{trial} "
|
|
173
|
+
f"{len(tr.calls):2d} calls {tr.wall_seconds:6.1f}s eta {eta/60:5.1f}m", flush=True)
|
|
174
|
+
print(f"done — {len(todo)} runs, {errors} harness error(s). Next: agentstress grade --runs {out}")
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _grade_dir(runs: Path, use_judge: bool):
|
|
179
|
+
"""-> rows [{scenario_id, framework, trial, mode, outcome, detail}]"""
|
|
180
|
+
from agentstress.correctness import check
|
|
181
|
+
from agentstress.grader import grade_trace
|
|
182
|
+
|
|
183
|
+
judge = None
|
|
184
|
+
if use_judge:
|
|
185
|
+
from agentstress.grading.claude_judge import judge_scenario
|
|
186
|
+
|
|
187
|
+
judge = judge_scenario
|
|
188
|
+
rows = []
|
|
189
|
+
for f in sorted(runs.glob("*.json")):
|
|
190
|
+
d = json.loads(f.read_text())
|
|
191
|
+
sc = BY_ID_PHASE1.get(d["scenario_id"])
|
|
192
|
+
if sc is None:
|
|
193
|
+
continue
|
|
194
|
+
if d.get("agent_error"):
|
|
195
|
+
outcome, detail = "ERROR", d["agent_error"][:80]
|
|
196
|
+
elif sc.target_mode == "SR":
|
|
197
|
+
g = grade_trace(d["trace"], sc.repeated_mutations_expected)
|
|
198
|
+
outcome = "FAIL" if g["extended_verdict"] == "FAIL" else "PASS"
|
|
199
|
+
detail = g["detail"] if isinstance(g.get("detail"), str) else ""
|
|
200
|
+
elif judge is None:
|
|
201
|
+
outcome, detail = "UNGRADED", "needs --judge"
|
|
202
|
+
else:
|
|
203
|
+
from agentstress.grading.claude_judge import agent_prompt_shown_to_judge
|
|
204
|
+
|
|
205
|
+
v = judge(sc.id, sc.target_mode, d["framework"], d.get("agent_output") or "",
|
|
206
|
+
agent_prompt_shown_to_judge(sc),
|
|
207
|
+
structural_reason=sc.structural_reason,
|
|
208
|
+
tool_calls=d["trace"]["calls"],
|
|
209
|
+
expected_clean_calls=sc.expected_clean_calls)
|
|
210
|
+
outcome, detail = v["verdict"], f"score {v['score']}"
|
|
211
|
+
corr = check(sc.id, d["trace"], d.get("agent_output") or "")
|
|
212
|
+
rows.append({"scenario_id": sc.id, "framework": d["framework"], "trial": d["trial"],
|
|
213
|
+
"mode": sc.target_mode, "outcome": outcome, "detail": detail,
|
|
214
|
+
"correct": None if corr is None else corr["correct"]})
|
|
215
|
+
return rows
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def cmd_grade(args) -> int:
|
|
219
|
+
runs = Path(args.runs)
|
|
220
|
+
if not runs.exists():
|
|
221
|
+
sys.exit(f"no such directory: {runs}")
|
|
222
|
+
modes = [BY_ID_PHASE1[d["scenario_id"]].target_mode
|
|
223
|
+
for d in (json.loads(f.read_text()) for f in runs.glob("*.json"))
|
|
224
|
+
if d["scenario_id"] in BY_ID_PHASE1]
|
|
225
|
+
n_judge = sum(m != "SR" for m in modes)
|
|
226
|
+
if args.judge:
|
|
227
|
+
print(f"judging {n_judge} runs with {rc_judge_model()} — this is billed", flush=True)
|
|
228
|
+
elif n_judge:
|
|
229
|
+
print(f"note: {n_judge} runs need the LLM judge (--judge); grading step repetition only")
|
|
230
|
+
rows = _grade_dir(runs, args.judge)
|
|
231
|
+
(runs / "graded.json").write_text(json.dumps(rows, indent=2))
|
|
232
|
+
print(f"wrote {runs / 'graded.json'} ({len(rows)} rows)")
|
|
233
|
+
return cmd_report(args)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def rc_judge_model() -> str:
|
|
237
|
+
from agentstress.grading.claude_judge import JUDGE_MODEL
|
|
238
|
+
|
|
239
|
+
return JUDGE_MODEL
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def cmd_report(args) -> int:
|
|
243
|
+
runs = Path(args.runs)
|
|
244
|
+
path = runs / "graded.json"
|
|
245
|
+
if not path.exists():
|
|
246
|
+
sys.exit(f"no graded.json in {runs} — run: agentstress grade --runs {runs}")
|
|
247
|
+
rows = json.loads(path.read_text())
|
|
248
|
+
by: dict[tuple, list] = {}
|
|
249
|
+
for r in rows:
|
|
250
|
+
by.setdefault((r["mode"], r["framework"]), []).append(r)
|
|
251
|
+
print(f"\n{'mode':6s} {'framework':16s} {'runs':>5} {'failure rate':>13} {'correct':>9}")
|
|
252
|
+
for (mode, fw), items in sorted(by.items()):
|
|
253
|
+
bad = sum(i["outcome"] in ("FAIL", "ERROR") for i in items)
|
|
254
|
+
graded = [i for i in items if i["outcome"] != "UNGRADED"]
|
|
255
|
+
checked = [i for i in items if i["correct"] is not None]
|
|
256
|
+
rate = f"{100 * bad / len(graded):.0f}%" if graded else "ungraded"
|
|
257
|
+
corr = f"{100 * sum(bool(i['correct']) for i in checked) / len(checked):.0f}%" if checked else "-"
|
|
258
|
+
print(f"{mode:6s} {fw:16s} {len(items):5d} {rate:>13} {corr:>9}")
|
|
259
|
+
worst = sorted({r["scenario_id"] for r in rows if r["outcome"] in ("FAIL", "ERROR")})
|
|
260
|
+
if worst:
|
|
261
|
+
print(f"\nscenarios with at least one failure ({len(worst)}): {', '.join(worst[:24])}"
|
|
262
|
+
+ (" ..." if len(worst) > 24 else ""))
|
|
263
|
+
print("\nFailure rate and correctness are separate axes: a run can avoid the failure "
|
|
264
|
+
"mode and still answer wrongly.")
|
|
265
|
+
return 0
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def main(argv=None) -> int:
|
|
269
|
+
ap = argparse.ArgumentParser(prog="agentstress", description=__doc__,
|
|
270
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
271
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
272
|
+
|
|
273
|
+
def add_select(p):
|
|
274
|
+
p.add_argument("--mode", nargs="+", choices=list(MODE_NAMES), help="limit to these failure modes")
|
|
275
|
+
p.add_argument("--scenario", nargs="+", help="scenario ids, e.g. FAQ-12 SR-02")
|
|
276
|
+
|
|
277
|
+
p = sub.add_parser("list", help="list scenarios")
|
|
278
|
+
add_select(p)
|
|
279
|
+
p.add_argument("--verbose", action="store_true", help="show the task text")
|
|
280
|
+
p.set_defaults(func=cmd_list)
|
|
281
|
+
|
|
282
|
+
p = sub.add_parser("capture", help="print what your framework sends the model")
|
|
283
|
+
p.add_argument("--framework", choices=list(FRAMEWORKS), required=True)
|
|
284
|
+
p.add_argument("--scenario", nargs=1, required=True)
|
|
285
|
+
p.add_argument("--model", default="ollama:qwen2.5:7b-instruct")
|
|
286
|
+
p.set_defaults(func=cmd_capture)
|
|
287
|
+
|
|
288
|
+
p = sub.add_parser("run", help="run scenarios against a framework")
|
|
289
|
+
p.add_argument("--framework", choices=list(FRAMEWORKS), required=True)
|
|
290
|
+
p.add_argument("--model", default="ollama:qwen2.5:7b-instruct",
|
|
291
|
+
help="ollama:<name> or openai:<name>")
|
|
292
|
+
p.add_argument("--trials", type=int, default=2)
|
|
293
|
+
p.add_argument("--temperature", type=float, default=rc.TEMPERATURE)
|
|
294
|
+
p.add_argument("--out", default="runs")
|
|
295
|
+
add_select(p)
|
|
296
|
+
p.set_defaults(func=cmd_run)
|
|
297
|
+
|
|
298
|
+
p = sub.add_parser("grade", help="grade a runs directory")
|
|
299
|
+
p.add_argument("--runs", default="runs")
|
|
300
|
+
p.add_argument("--judge", action="store_true",
|
|
301
|
+
help="use the LLM judge for RAM/UT/FAQ/INV (billed; needs ANTHROPIC_API_KEY)")
|
|
302
|
+
p.set_defaults(func=cmd_grade)
|
|
303
|
+
|
|
304
|
+
p = sub.add_parser("report", help="summarise graded runs")
|
|
305
|
+
p.add_argument("--runs", default="runs")
|
|
306
|
+
p.set_defaults(func=cmd_report)
|
|
307
|
+
|
|
308
|
+
args = ap.parse_args(argv)
|
|
309
|
+
return args.func(args)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
if __name__ == "__main__":
|
|
313
|
+
sys.exit(main())
|