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.
Files changed (44) hide show
  1. agentstress-0.1.0/LICENSE +21 -0
  2. agentstress-0.1.0/PKG-INFO +185 -0
  3. agentstress-0.1.0/README.md +149 -0
  4. agentstress-0.1.0/agentstress/__init__.py +0 -0
  5. agentstress-0.1.0/agentstress/cli.py +313 -0
  6. agentstress-0.1.0/agentstress/correctness.py +460 -0
  7. agentstress-0.1.0/agentstress/grader.py +300 -0
  8. agentstress-0.1.0/agentstress/grading/__init__.py +0 -0
  9. agentstress-0.1.0/agentstress/grading/batch_judge.py +211 -0
  10. agentstress-0.1.0/agentstress/grading/claude_judge.py +348 -0
  11. agentstress-0.1.0/agentstress/grading/faq_rubric.md +175 -0
  12. agentstress-0.1.0/agentstress/grading/inv_rubric.md +200 -0
  13. agentstress-0.1.0/agentstress/grading/ram_rubric.md +143 -0
  14. agentstress-0.1.0/agentstress/grading/ut_rubric.md +172 -0
  15. agentstress-0.1.0/agentstress/harnesses/__init__.py +0 -0
  16. agentstress-0.1.0/agentstress/harnesses/crewai_runner.py +87 -0
  17. agentstress-0.1.0/agentstress/harnesses/crewai_tools_impl.py +77 -0
  18. agentstress-0.1.0/agentstress/harnesses/langgraph_runner.py +54 -0
  19. agentstress-0.1.0/agentstress/harnesses/langgraph_tools.py +77 -0
  20. agentstress-0.1.0/agentstress/harnesses/models.py +52 -0
  21. agentstress-0.1.0/agentstress/harnesses/openai_runner.py +73 -0
  22. agentstress-0.1.0/agentstress/harnesses/openai_tools_impl.py +82 -0
  23. agentstress-0.1.0/agentstress/proxy.py +60 -0
  24. agentstress-0.1.0/agentstress/run_config.py +49 -0
  25. agentstress-0.1.0/agentstress/scenario_model.py +44 -0
  26. agentstress-0.1.0/agentstress/scenarios.py +399 -0
  27. agentstress-0.1.0/agentstress/scenarios_expansion.py +1033 -0
  28. agentstress-0.1.0/agentstress/scenarios_phase1.py +1082 -0
  29. agentstress-0.1.0/agentstress/tools.py +262 -0
  30. agentstress-0.1.0/agentstress/trace.py +124 -0
  31. agentstress-0.1.0/agentstress.egg-info/PKG-INFO +185 -0
  32. agentstress-0.1.0/agentstress.egg-info/SOURCES.txt +42 -0
  33. agentstress-0.1.0/agentstress.egg-info/dependency_links.txt +1 -0
  34. agentstress-0.1.0/agentstress.egg-info/entry_points.txt +2 -0
  35. agentstress-0.1.0/agentstress.egg-info/requires.txt +22 -0
  36. agentstress-0.1.0/agentstress.egg-info/top_level.txt +1 -0
  37. agentstress-0.1.0/pyproject.toml +45 -0
  38. agentstress-0.1.0/setup.cfg +4 -0
  39. agentstress-0.1.0/tests/test_correctness.py +251 -0
  40. agentstress-0.1.0/tests/test_grader.py +202 -0
  41. agentstress-0.1.0/tests/test_judge_context.py +41 -0
  42. agentstress-0.1.0/tests/test_judge_parse.py +49 -0
  43. agentstress-0.1.0/tests/test_prompt_hygiene.py +69 -0
  44. 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())