pasr-bench 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.
- pasr_bench-0.2.0/.gitignore +226 -0
- pasr_bench-0.2.0/PKG-INFO +116 -0
- pasr_bench-0.2.0/README.md +91 -0
- pasr_bench-0.2.0/RESULTS.md +151 -0
- pasr_bench-0.2.0/pasr_eval/__init__.py +34 -0
- pasr_bench-0.2.0/pasr_eval/__main__.py +47 -0
- pasr_bench-0.2.0/pasr_eval/agents.py +28 -0
- pasr_bench-0.2.0/pasr_eval/arms.py +195 -0
- pasr_bench-0.2.0/pasr_eval/bakeoff.py +332 -0
- pasr_bench-0.2.0/pasr_eval/llm_agent.py +77 -0
- pasr_bench-0.2.0/pasr_eval/metrics.py +103 -0
- pasr_bench-0.2.0/pasr_eval/plans/pilot.json +71 -0
- pasr_bench-0.2.0/pasr_eval/run.py +202 -0
- pasr_bench-0.2.0/pasr_eval/runner.py +96 -0
- pasr_bench-0.2.0/pasr_eval/spec.py +125 -0
- pasr_bench-0.2.0/pasr_eval/validate.py +77 -0
- pasr_bench-0.2.0/pyproject.toml +45 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
219
|
+
|
|
220
|
+
# PASR local artifacts — receipts are ignored, Context Packs are committable
|
|
221
|
+
.pasr/*
|
|
222
|
+
!.pasr/packs/
|
|
223
|
+
eval/runs/
|
|
224
|
+
eval/pasr_eval/runs/
|
|
225
|
+
pasr-bench-runs/
|
|
226
|
+
.eval-checkouts/
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pasr-bench
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: PASR-Bench: a pre-registered protocol for measuring a code-context retriever against grep, a repo-map, semantic search, and a whole-repo dump.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Apheironn/pasr
|
|
6
|
+
Project-URL: Source, https://github.com/Apheironn/pasr/tree/main/eval
|
|
7
|
+
Author: Apheironn
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: benchmark,coding-agent,context,evaluation,non-inferiority,retrieval
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Testing
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: pasr-mcp>=0.2
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest<9,>=8; extra == 'dev'
|
|
19
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
20
|
+
Provides-Extra: llm
|
|
21
|
+
Requires-Dist: anthropic>=0.40; extra == 'llm'
|
|
22
|
+
Provides-Extra: plots
|
|
23
|
+
Requires-Dist: matplotlib>=3.7; extra == 'plots'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# PASR-Bench
|
|
27
|
+
|
|
28
|
+
A small, **pre-registered** protocol for measuring a code-context retriever, and the
|
|
29
|
+
harness that runs it. Packaged separately from `pasr-mcp` (`pip install
|
|
30
|
+
./eval`, distribution name `pasr-bench`). Results for PASR itself:
|
|
31
|
+
[`RESULTS.md`](RESULTS.md) and [`../docs/competitors-benchmark.md`](../docs/competitors-benchmark.md).
|
|
32
|
+
|
|
33
|
+
## What it measures
|
|
34
|
+
|
|
35
|
+
**Real-agent run** — four arms per task, each producing the same `ArmResult`:
|
|
36
|
+
|
|
37
|
+
1. **native_search** — deterministic grep + read-files (stand-in for an agent's own tools)
|
|
38
|
+
2. **broad** — the whole repo, source-first, truncated to a cap
|
|
39
|
+
3. **pasr** — `select_context` at a fixed budget
|
|
40
|
+
4. **pasr_fallback** — `pasr`, then one budget widening if confidence is low
|
|
41
|
+
|
|
42
|
+
A model answers each task from **only** that arm's context (`UNKNOWN` if absent); a
|
|
43
|
+
second model judges the answer against the task's expected identifiers;
|
|
44
|
+
`critical_source_hit` is still required. Metrics: task success, model input tokens
|
|
45
|
+
(incl. a flat per-tool-call overhead), round trips, critical-source miss rate, fallback
|
|
46
|
+
rate. Then a **paired non-inferiority** test of `pasr` / `pasr_fallback` vs the
|
|
47
|
+
`baseline_arm` at the plan's `margin_task_success` (point estimate + 10k-resample
|
|
48
|
+
bootstrap CI).
|
|
49
|
+
|
|
50
|
+
**Bake-off** — an offline, no-API retrieval comparison at a shared budget: `grep`,
|
|
51
|
+
`repomap` (aider-style signatures), `embed_lex` (a no-setup semantic floor), `pasr`,
|
|
52
|
+
`pasr_hash`, `pasr_map`. Scored on critical-file hit ∧ keyword coverage, split by task
|
|
53
|
+
kind.
|
|
54
|
+
|
|
55
|
+
**No GPU.** Arms run on CPU; the only model use is one answer + one judge call per
|
|
56
|
+
(task, arm) — ~400 Anthropic calls for the 50-task plan. Full Sonnet ≈ $12–15; a cheap
|
|
57
|
+
`--model` with a strong `--judge-model` ≈ $4–6.
|
|
58
|
+
|
|
59
|
+
## Install & run
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install ./eval # the pasr-bench distribution (deps: pasr-mcp)
|
|
63
|
+
pip install "./eval[llm,plots]" # + anthropic (real agent) + matplotlib (report.png)
|
|
64
|
+
|
|
65
|
+
pasr-bench plans # the packaged plan(s)
|
|
66
|
+
pasr-bench run --agent keyword # offline smoke: full matrix + report + validation
|
|
67
|
+
pasr-bench bakeoff --budget 6000 # offline retrieval bake-off
|
|
68
|
+
|
|
69
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
70
|
+
pasr-bench run --agent claude --max-tasks 4 # cheap trial (~$0.4)
|
|
71
|
+
pasr-bench run --agent claude \
|
|
72
|
+
--model claude-haiku-4-5 --judge-model claude-sonnet-5 \
|
|
73
|
+
--checkout-dir .eval-checkouts # budget-safe full run (~$4–6)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Each finished `(task, arm)` row is appended to `matrix.jsonl` and flushed, so a crash
|
|
77
|
+
keeps every completed row; `--resume <run_dir>` reloads the partial matrix and finishes
|
|
78
|
+
into the same delivery. `--checkout-dir DIR` reuses clones. `PASR_EVAL_BROAD_CAP=30000`
|
|
79
|
+
shrinks the `broad` arm. A run writes `<out>/<plan>_<utc>/` with `matrix.jsonl`,
|
|
80
|
+
`report.{json,md,png}`, `validation.json`, `resolved_commits.json`.
|
|
81
|
+
|
|
82
|
+
`python eval/run_eval.py …` and `python eval/bakeoff.py …` still work as thin shims for
|
|
83
|
+
the two sub-commands.
|
|
84
|
+
|
|
85
|
+
## Bring your own retriever
|
|
86
|
+
|
|
87
|
+
The protocol is retriever-agnostic. To measure a different context tool against the
|
|
88
|
+
same 50 tasks and the same baselines:
|
|
89
|
+
|
|
90
|
+
1. Add an arm in [`pasr_eval/arms.py`](pasr_eval/arms.py): extend `ARMS` and add a
|
|
91
|
+
branch in `_build(...)` that returns `(context, sources, tool_calls, round_trips,
|
|
92
|
+
fallback)` for your retriever. Everything downstream — grading, metrics,
|
|
93
|
+
non-inferiority, the validator — is arm-agnostic.
|
|
94
|
+
2. For a bake-off arm, add a function in [`pasr_eval/bakeoff.py`](pasr_eval/bakeoff.py)
|
|
95
|
+
and list it in that file's `ARMS`.
|
|
96
|
+
3. Keep the plan (`pasr_eval/plans/pilot.json`) fixed so numbers stay comparable, or
|
|
97
|
+
register a new plan and cite it.
|
|
98
|
+
|
|
99
|
+
The validator (`validate_matrix`) rejects synthetic rows, query→answer leaks, and an
|
|
100
|
+
unmatched task×arm matrix, so a submitted result is checkable.
|
|
101
|
+
|
|
102
|
+
## Files
|
|
103
|
+
|
|
104
|
+
| Path | What |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `pasr_eval/spec.py` | `RepoSpec` / `TaskSpec` / `EvalPlan`, `load_plan`; the leak + kind guards |
|
|
107
|
+
| `pasr_eval/arms.py` | the four real-agent arms → `ArmResult` |
|
|
108
|
+
| `pasr_eval/bakeoff.py` | the six offline bake-off arms |
|
|
109
|
+
| `pasr_eval/agents.py` | `AgentRunner` protocol, `KeywordAgent` (offline proxy) |
|
|
110
|
+
| `pasr_eval/llm_agent.py` | `LlmAgent` — answer + judge via the Anthropic API (`[llm]` extra) |
|
|
111
|
+
| `pasr_eval/metrics.py` | grade, aggregate, paired bootstrap CI, non-inferiority, `full_report` |
|
|
112
|
+
| `pasr_eval/runner.py` | `resolve_repos`, `run_plan` (`skip=` / `on_row=`), `write_matrix` |
|
|
113
|
+
| `pasr_eval/validate.py` | `validate_matrix` — no synthetic rows, no leaks, matched matrix |
|
|
114
|
+
| `pasr_eval/run.py` | end-to-end orchestrator: clone → streamed matrix → report → validate |
|
|
115
|
+
| `pasr_eval/plans/pilot.json` | the registered plan — 10 pinned repos, 50 tasks |
|
|
116
|
+
| `RESULTS.md` | pre-registration + n=15 pilot + keyword-50 + the n=50 real-agent headline |
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# PASR-Bench
|
|
2
|
+
|
|
3
|
+
A small, **pre-registered** protocol for measuring a code-context retriever, and the
|
|
4
|
+
harness that runs it. Packaged separately from `pasr-mcp` (`pip install
|
|
5
|
+
./eval`, distribution name `pasr-bench`). Results for PASR itself:
|
|
6
|
+
[`RESULTS.md`](RESULTS.md) and [`../docs/competitors-benchmark.md`](../docs/competitors-benchmark.md).
|
|
7
|
+
|
|
8
|
+
## What it measures
|
|
9
|
+
|
|
10
|
+
**Real-agent run** — four arms per task, each producing the same `ArmResult`:
|
|
11
|
+
|
|
12
|
+
1. **native_search** — deterministic grep + read-files (stand-in for an agent's own tools)
|
|
13
|
+
2. **broad** — the whole repo, source-first, truncated to a cap
|
|
14
|
+
3. **pasr** — `select_context` at a fixed budget
|
|
15
|
+
4. **pasr_fallback** — `pasr`, then one budget widening if confidence is low
|
|
16
|
+
|
|
17
|
+
A model answers each task from **only** that arm's context (`UNKNOWN` if absent); a
|
|
18
|
+
second model judges the answer against the task's expected identifiers;
|
|
19
|
+
`critical_source_hit` is still required. Metrics: task success, model input tokens
|
|
20
|
+
(incl. a flat per-tool-call overhead), round trips, critical-source miss rate, fallback
|
|
21
|
+
rate. Then a **paired non-inferiority** test of `pasr` / `pasr_fallback` vs the
|
|
22
|
+
`baseline_arm` at the plan's `margin_task_success` (point estimate + 10k-resample
|
|
23
|
+
bootstrap CI).
|
|
24
|
+
|
|
25
|
+
**Bake-off** — an offline, no-API retrieval comparison at a shared budget: `grep`,
|
|
26
|
+
`repomap` (aider-style signatures), `embed_lex` (a no-setup semantic floor), `pasr`,
|
|
27
|
+
`pasr_hash`, `pasr_map`. Scored on critical-file hit ∧ keyword coverage, split by task
|
|
28
|
+
kind.
|
|
29
|
+
|
|
30
|
+
**No GPU.** Arms run on CPU; the only model use is one answer + one judge call per
|
|
31
|
+
(task, arm) — ~400 Anthropic calls for the 50-task plan. Full Sonnet ≈ $12–15; a cheap
|
|
32
|
+
`--model` with a strong `--judge-model` ≈ $4–6.
|
|
33
|
+
|
|
34
|
+
## Install & run
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install ./eval # the pasr-bench distribution (deps: pasr-mcp)
|
|
38
|
+
pip install "./eval[llm,plots]" # + anthropic (real agent) + matplotlib (report.png)
|
|
39
|
+
|
|
40
|
+
pasr-bench plans # the packaged plan(s)
|
|
41
|
+
pasr-bench run --agent keyword # offline smoke: full matrix + report + validation
|
|
42
|
+
pasr-bench bakeoff --budget 6000 # offline retrieval bake-off
|
|
43
|
+
|
|
44
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
45
|
+
pasr-bench run --agent claude --max-tasks 4 # cheap trial (~$0.4)
|
|
46
|
+
pasr-bench run --agent claude \
|
|
47
|
+
--model claude-haiku-4-5 --judge-model claude-sonnet-5 \
|
|
48
|
+
--checkout-dir .eval-checkouts # budget-safe full run (~$4–6)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Each finished `(task, arm)` row is appended to `matrix.jsonl` and flushed, so a crash
|
|
52
|
+
keeps every completed row; `--resume <run_dir>` reloads the partial matrix and finishes
|
|
53
|
+
into the same delivery. `--checkout-dir DIR` reuses clones. `PASR_EVAL_BROAD_CAP=30000`
|
|
54
|
+
shrinks the `broad` arm. A run writes `<out>/<plan>_<utc>/` with `matrix.jsonl`,
|
|
55
|
+
`report.{json,md,png}`, `validation.json`, `resolved_commits.json`.
|
|
56
|
+
|
|
57
|
+
`python eval/run_eval.py …` and `python eval/bakeoff.py …` still work as thin shims for
|
|
58
|
+
the two sub-commands.
|
|
59
|
+
|
|
60
|
+
## Bring your own retriever
|
|
61
|
+
|
|
62
|
+
The protocol is retriever-agnostic. To measure a different context tool against the
|
|
63
|
+
same 50 tasks and the same baselines:
|
|
64
|
+
|
|
65
|
+
1. Add an arm in [`pasr_eval/arms.py`](pasr_eval/arms.py): extend `ARMS` and add a
|
|
66
|
+
branch in `_build(...)` that returns `(context, sources, tool_calls, round_trips,
|
|
67
|
+
fallback)` for your retriever. Everything downstream — grading, metrics,
|
|
68
|
+
non-inferiority, the validator — is arm-agnostic.
|
|
69
|
+
2. For a bake-off arm, add a function in [`pasr_eval/bakeoff.py`](pasr_eval/bakeoff.py)
|
|
70
|
+
and list it in that file's `ARMS`.
|
|
71
|
+
3. Keep the plan (`pasr_eval/plans/pilot.json`) fixed so numbers stay comparable, or
|
|
72
|
+
register a new plan and cite it.
|
|
73
|
+
|
|
74
|
+
The validator (`validate_matrix`) rejects synthetic rows, query→answer leaks, and an
|
|
75
|
+
unmatched task×arm matrix, so a submitted result is checkable.
|
|
76
|
+
|
|
77
|
+
## Files
|
|
78
|
+
|
|
79
|
+
| Path | What |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `pasr_eval/spec.py` | `RepoSpec` / `TaskSpec` / `EvalPlan`, `load_plan`; the leak + kind guards |
|
|
82
|
+
| `pasr_eval/arms.py` | the four real-agent arms → `ArmResult` |
|
|
83
|
+
| `pasr_eval/bakeoff.py` | the six offline bake-off arms |
|
|
84
|
+
| `pasr_eval/agents.py` | `AgentRunner` protocol, `KeywordAgent` (offline proxy) |
|
|
85
|
+
| `pasr_eval/llm_agent.py` | `LlmAgent` — answer + judge via the Anthropic API (`[llm]` extra) |
|
|
86
|
+
| `pasr_eval/metrics.py` | grade, aggregate, paired bootstrap CI, non-inferiority, `full_report` |
|
|
87
|
+
| `pasr_eval/runner.py` | `resolve_repos`, `run_plan` (`skip=` / `on_row=`), `write_matrix` |
|
|
88
|
+
| `pasr_eval/validate.py` | `validate_matrix` — no synthetic rows, no leaks, matched matrix |
|
|
89
|
+
| `pasr_eval/run.py` | end-to-end orchestrator: clone → streamed matrix → report → validate |
|
|
90
|
+
| `pasr_eval/plans/pilot.json` | the registered plan — 10 pinned repos, 50 tasks |
|
|
91
|
+
| `RESULTS.md` | pre-registration + n=15 pilot + keyword-50 + the n=50 real-agent headline |
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# PASR real-agent evaluation — results
|
|
2
|
+
|
|
3
|
+
> **50-task real-agent run done** (`eval/deliveries/realagent50_20260902T221528Z/`,
|
|
4
|
+
> 10 pinned repos, Claude answer + judge). PASR gives the model **5.8k targeted
|
|
5
|
+
> tokens in one tool call** and it answers **48%** of the tasks; the **59k-token
|
|
6
|
+
> source-first repo dump answers 38%** — PASR is **+0.10** on paired task success
|
|
7
|
+
> (`pasr_fallback` +0.12), and the **point estimate clears the -0.05
|
|
8
|
+
> non-inferiority margin** (95% CI still crosses it: `pasr` [-0.08, +0.30]).
|
|
9
|
+
> PASR puts the critical file in context **46/50** times vs the dump's **33/50**.
|
|
10
|
+
>
|
|
11
|
+
> Reading: on localized code questions PASR matches — slightly beats — a 10×-larger
|
|
12
|
+
> whole-repo dump, at **~90% fewer input tokens and one round trip**. `native_search`
|
|
13
|
+
> (grep + read 6 files, 23k tokens, 6 round trips) is the raw-success leader at 0.52
|
|
14
|
+
> but misses the critical file 30% of the time. This is a **bounded efficiency
|
|
15
|
+
> result**, not a superiority claim — one more batch of 50 would settle the interval.
|
|
16
|
+
|
|
17
|
+
## Pre-registration
|
|
18
|
+
|
|
19
|
+
- **Plan:** `pasr_eval/plans/pilot.json` — 10 pinned public Python repos, 50 source-grounded
|
|
20
|
+
`locate` / `trace` / `explain` tasks (5/repo). `pasr-bench run` records each repo's
|
|
21
|
+
resolved commit SHA and the answer/judge model IDs into `matrix.jsonl`'s `_meta` line.
|
|
22
|
+
- **Arms:** `native_search`, `broad`, `pasr`, `pasr_fallback`.
|
|
23
|
+
- **Baseline:** `broad`.
|
|
24
|
+
- **Primary claim (non-inferiority):** on paired tasks, `pasr_fallback` task success
|
|
25
|
+
is non-inferior to `broad` within a **-0.05** margin (both the point estimate and
|
|
26
|
+
the lower bound of a 10k-resample paired bootstrap 95% CI ≥ margin).
|
|
27
|
+
- **Secondary:** `pasr` / `pasr_fallback` reduce mean `tokens_in` and `round_trips`
|
|
28
|
+
vs `broad`; `critical_source_miss_rate` reported per arm; `fallback_rate` reported.
|
|
29
|
+
- **No synthetic rows, no reference leak:** enforced by `validate_matrix` (every row
|
|
30
|
+
traces to a registered task + repo; the query never contains the full answer;
|
|
31
|
+
exactly one row per task×arm).
|
|
32
|
+
|
|
33
|
+
## The two graders
|
|
34
|
+
|
|
35
|
+
- **Keyword proxy** (`--agent keyword`, offline, no API): task success = every expected
|
|
36
|
+
identifier appears verbatim in the arm's context **and** the critical file is present.
|
|
37
|
+
A harsh literal proxy for retrieval quality — machinery + token-story check only.
|
|
38
|
+
- **Real agent** (`--agent claude`): a model answers from **only** the arm's context
|
|
39
|
+
(`UNKNOWN` if not present), then a second model judges the answer against the task's
|
|
40
|
+
expected identifiers; `critical_source_hit` is still required for a pass.
|
|
41
|
+
|
|
42
|
+
The n=15 pilot and the 50-task keyword re-run below are the supporting runs; the
|
|
43
|
+
[50-task real-agent run](#50-task-real-agent-run--claude-answer--judge-10-repos---headline)
|
|
44
|
+
is the headline.
|
|
45
|
+
|
|
46
|
+
## Pilot run — `claude-sonnet-5`, 15 tasks, 5 repos
|
|
47
|
+
|
|
48
|
+
| arm | task success | context tokens | round trips | crit. miss | fallback rate |
|
|
49
|
+
|---|---:|---:|---:|---:|---:|
|
|
50
|
+
| broad (58k source-first repo dump) | 0.467 | 58 146 | 1 | 0.067 | – |
|
|
51
|
+
| native_search (grep + 6 files, 4k each) | 0.400 | 22 771 | 6 | 0.267 | – |
|
|
52
|
+
| **pasr** | **0.533** | **5 764** | **1** | 0.133 | – |
|
|
53
|
+
| pasr_fallback | 0.600 | 5 764 | 1 | 0.133 | 0.000 |
|
|
54
|
+
|
|
55
|
+
- **Token savings vs `broad`: pasr / pasr_fallback +90.0%**; native_search +60.4%.
|
|
56
|
+
- **Non-inferiority (`pasr_fallback` vs `broad`, task success, margin -0.05):**
|
|
57
|
+
delta **+0.133**, 95% CI **[-0.20, +0.47]** — **point estimate PASSES**, the CI
|
|
58
|
+
lower bound (-0.20) does **not** clear the margin. `pasr` vs `broad`: delta +0.067,
|
|
59
|
+
CI [-0.33, +0.47], same picture.
|
|
60
|
+
- `pasr` critical-source miss: **2/15** (`req-02`, `star-03`; `star-03` was also
|
|
61
|
+
missed by `broad` → likely a task-spec problem, not retrieval).
|
|
62
|
+
- The `pasr_fallback` threshold (`confidence < 0.5`) **never triggered** — all 15
|
|
63
|
+
PASR selections were confident. `pasr_fallback` ran on the same contexts as `pasr`;
|
|
64
|
+
its +0.067 over `pasr` is answer/judge sampling noise, not a fallback effect.
|
|
65
|
+
|
|
66
|
+
### Reading it
|
|
67
|
+
|
|
68
|
+
- `broad` puts the critical file in context 14/15 times (58k tokens) yet the model
|
|
69
|
+
answers only 7/15 — a large-haystack utilisation effect. `pasr` gives the model
|
|
70
|
+
5.8k targeted tokens and it answers 8/15. **Same answer quality band, one order of
|
|
71
|
+
magnitude fewer tokens, one tool call instead of the model chewing 58k.**
|
|
72
|
+
- `native_search` is worst: brittle grep (27% critical miss) plus six partial files.
|
|
73
|
+
- **What this does not show:** inferential non-inferiority (n=15 CI is ±0.3). The
|
|
74
|
+
bounded positive claim is a cross-repo efficiency direction, mirroring `researchv2`'s
|
|
75
|
+
LongBench Pro finding.
|
|
76
|
+
|
|
77
|
+
Delivery: `eval/deliveries/pilot_20260902T212108Z/`.
|
|
78
|
+
|
|
79
|
+
## 50-task keyword-proxy re-run (no API — `pasr-bench run --agent keyword`)
|
|
80
|
+
|
|
81
|
+
`eval/deliveries/keyword50_20260902T213744Z/`. 10 repos, 50 source-grounded tasks;
|
|
82
|
+
`_FALLBACK_CONFIDENCE` raised to 0.65.
|
|
83
|
+
|
|
84
|
+
| arm | task success | context tokens | round trips | crit. miss | fallback rate |
|
|
85
|
+
|---|---:|---:|---:|---:|---:|
|
|
86
|
+
| broad (58k source-first) | 0.660 | 59 075 | 1 | **0.340** | – |
|
|
87
|
+
| native_search (6 files, 4k) | 0.640 | 22 901 | 6 | 0.300 | – |
|
|
88
|
+
| **pasr** | **0.700** | **5 777** | 1 | **0.080** | – |
|
|
89
|
+
| pasr_fallback | 0.700 | 5 854 | 1.02 | 0.060 | 0.020 |
|
|
90
|
+
|
|
91
|
+
- **PASR beats both baselines on the literal grader** (0.70 vs 0.66 / 0.64) at **+90%
|
|
92
|
+
tokens**, and its critical-source miss (0.08 = 4/50) is **4× lower than broad's
|
|
93
|
+
0.34** — a 60k source-first dump still fails to include the right file for 17/50
|
|
94
|
+
real-repo tasks; PASR's targeted retrieval gets it in 92% of the time.
|
|
95
|
+
- Non-inferiority (`pasr` vs `broad`, keyword grader): delta **+0.04**, 95% CI
|
|
96
|
+
**[-0.14, +0.22]** — point PASSES, CI is now ±0.18 (was ±0.30 at n=15) and just
|
|
97
|
+
misses clearing the -0.05 margin.
|
|
98
|
+
- Fallback engaged on 1/50 (crit-miss 0.08 → 0.06).
|
|
99
|
+
|
|
100
|
+
## 50-task real-agent run — Claude answer + judge, 10 repos ★ headline
|
|
101
|
+
|
|
102
|
+
`eval/deliveries/realagent50_20260902T221528Z/`. Models recorded in `matrix.jsonl`'s
|
|
103
|
+
`_meta`. `validate_matrix` clean (no synthetic rows, no leak, matched 4×50 matrix).
|
|
104
|
+
|
|
105
|
+
| arm | task success | context tokens | tokens_in | round trips | crit. miss | fallback |
|
|
106
|
+
|---|---:|---:|---:|---:|---:|---:|
|
|
107
|
+
| broad (59k source-first repo dump) | 0.380 | 59 075 | 59 115 | 1 | **0.340** | – |
|
|
108
|
+
| native_search (grep + 6 files, 4k each) | **0.520** | 22 901 | 23 181 | 6 | 0.300 | – |
|
|
109
|
+
| **pasr** | 0.480 | **5 777** | **5 817** | **1** | **0.080** | – |
|
|
110
|
+
| pasr_fallback | 0.500 | 5 854 | 5 895 | 1.02 | 0.060 | 0.020 |
|
|
111
|
+
|
|
112
|
+
- **Token savings vs `broad`: pasr +90.2%, pasr_fallback +90.0%**; native_search +60.8%.
|
|
113
|
+
- **Non-inferiority (task success, margin -0.05):** `pasr` delta **+0.100**, 95% CI
|
|
114
|
+
**[-0.08, +0.30]**; `pasr_fallback` delta **+0.120**, CI **[-0.06, +0.30]**. Both
|
|
115
|
+
**point estimates PASS**; both CI lower bounds miss the margin by ≈0.03 (half-width
|
|
116
|
+
±0.18–0.19 at n=50, down from ±0.30 at n=15).
|
|
117
|
+
- **Critical-source hit: `pasr` 46/50 (92%), `pasr_fallback` 47/50** vs **`broad`
|
|
118
|
+
33/50 (66%)**. A 59k source-first dump *still* omits the answer's file for 17/50
|
|
119
|
+
real-repo tasks — concentrated in the large repos (`typer` 5/5 missed, `jinja` 4/5,
|
|
120
|
+
`packaging` 4/5, `anyio` 3/5); those are exactly the repos where `broad` scores
|
|
121
|
+
0–1/5. Where the file *does* fit (`requests`, `pluggy`, `httpx`) `broad` reaches
|
|
122
|
+
4/5. **`broad`'s 0.38 is a truncation + large-haystack failure, not a grading one.**
|
|
123
|
+
- Head-to-head: `pasr` wins **15** tasks `broad` loses, loses **10** `broad` wins
|
|
124
|
+
(net +5 of 50 = the +0.10). `pasr_fallback` widened once (`pkg`-family), turning one
|
|
125
|
+
loss into a win.
|
|
126
|
+
- **PASR's weak spot — `typer` (0/5, both PASR arms).** The slice reaches the right
|
|
127
|
+
file 3/5 but the answering lines aren't in the selected window: `typer` leans on
|
|
128
|
+
re-exports and decorator plumbing that the current chunker + 6k budget don't
|
|
129
|
+
resolve. Drop `typer` and `pasr` is 24/45 = **0.53**. Logged for a chunker follow-up.
|
|
130
|
+
- `native_search` is the raw-success leader (0.52) but pays **4× the tokens, 6 round
|
|
131
|
+
trips**, and a **30% critical-source miss** — brittle when the query terms don't
|
|
132
|
+
literally appear near the answer.
|
|
133
|
+
|
|
134
|
+
### Reading it
|
|
135
|
+
|
|
136
|
+
The claim PASR makes is a **profile trade**, and the run supports it: at parity-ish
|
|
137
|
+
answer quality with a whole-repo dump (−0 to +0.12 depending on arm/margin), PASR
|
|
138
|
+
costs **one order of magnitude fewer input tokens, one tool call instead of the model
|
|
139
|
+
chewing 59k, and a 92% critical-file hit rate with per-line provenance**. It is *not*
|
|
140
|
+
an inferential non-inferiority pass (CI lower bound −0.08) and *not* a raw-accuracy
|
|
141
|
+
win over an agent's own grep. It mirrors `researchv2`'s LongBench Pro finding: a
|
|
142
|
+
bounded cross-repo efficiency direction.
|
|
143
|
+
|
|
144
|
+
### Next
|
|
145
|
+
|
|
146
|
+
1. **Second batch of 50 tasks** to close the CI (projected half-width ≈±0.13 at
|
|
147
|
+
n=100 would clear the −0.05 margin if the point estimate holds).
|
|
148
|
+
2. **Chunker follow-up for re-export/decorator-heavy repos** (`typer`): the pass that
|
|
149
|
+
turns a critical-file hit into an answerable window.
|
|
150
|
+
3. Consider a relevance-ranked `broad` truncation so it isn't a strawman past ~40k —
|
|
151
|
+
though the point of this arm is precisely "what a naive big-context dump gets you".
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Real-agent evaluation harness for the PASR MCP (roadmap M11 / research Track 2).
|
|
2
|
+
|
|
3
|
+
Not shipped with ``pasr-mcp``. Compares four arms per task — the agent's native
|
|
4
|
+
search, full/broad context, PASR selected context, PASR + one controlled fallback —
|
|
5
|
+
and reports paired metrics with a pre-registered non-inferiority margin.
|
|
6
|
+
|
|
7
|
+
The dry-run path (``KeywordAgent`` + local-mode plan) exercises the whole matrix
|
|
8
|
+
offline; the real run swaps in ``LlmAgent`` (answer + judge over the Anthropic API).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pasr_eval.arms import ARMS, ArmResult, run_arm
|
|
12
|
+
from pasr_eval.metrics import aggregate, bootstrap_ci, grade, non_inferiority, paired_delta
|
|
13
|
+
from pasr_eval.runner import run_plan, write_matrix
|
|
14
|
+
from pasr_eval.spec import EvalPlan, RepoSpec, TaskSpec, load_plan, plan_from_dict
|
|
15
|
+
from pasr_eval.validate import validate_matrix
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ARMS",
|
|
19
|
+
"ArmResult",
|
|
20
|
+
"run_arm",
|
|
21
|
+
"run_plan",
|
|
22
|
+
"write_matrix",
|
|
23
|
+
"EvalPlan",
|
|
24
|
+
"RepoSpec",
|
|
25
|
+
"TaskSpec",
|
|
26
|
+
"load_plan",
|
|
27
|
+
"plan_from_dict",
|
|
28
|
+
"aggregate",
|
|
29
|
+
"paired_delta",
|
|
30
|
+
"bootstrap_ci",
|
|
31
|
+
"non_inferiority",
|
|
32
|
+
"grade",
|
|
33
|
+
"validate_matrix",
|
|
34
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""``pasr-bench`` console entry: a thin dispatcher over the two orchestrators.
|
|
2
|
+
|
|
3
|
+
pasr-bench run [args...] # the 4-arm real-agent / keyword evaluation
|
|
4
|
+
pasr-bench bakeoff [args...] # the offline retrieval bake-off
|
|
5
|
+
pasr-bench plans # list the packaged plans
|
|
6
|
+
|
|
7
|
+
Everything after the sub-command is forwarded verbatim, so
|
|
8
|
+
``pasr-bench run --agent claude --resume ...`` works exactly like the old
|
|
9
|
+
``python eval/run_eval.py``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
_USAGE = "usage: pasr-bench {run|bakeoff|plans} [args...]"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main(argv: list[str] | None = None) -> int:
|
|
21
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
22
|
+
if not args or args[0] in ("-h", "--help"):
|
|
23
|
+
print(_USAGE)
|
|
24
|
+
print("\n run end-to-end evaluation -> a timestamped delivery")
|
|
25
|
+
print(" bakeoff offline retrieval bake-off (grep / repo-map / semantic / PASR)")
|
|
26
|
+
print(" plans list the plan files bundled with this package")
|
|
27
|
+
return 0 if args else 2
|
|
28
|
+
|
|
29
|
+
cmd, rest = args[0], args[1:]
|
|
30
|
+
if cmd == "run":
|
|
31
|
+
from pasr_eval.run import main as run_main
|
|
32
|
+
|
|
33
|
+
return run_main(rest)
|
|
34
|
+
if cmd == "bakeoff":
|
|
35
|
+
from pasr_eval.bakeoff import main as bakeoff_main
|
|
36
|
+
|
|
37
|
+
return bakeoff_main(rest)
|
|
38
|
+
if cmd == "plans":
|
|
39
|
+
for path in sorted((Path(__file__).parent / "plans").glob("*.json")):
|
|
40
|
+
print(path)
|
|
41
|
+
return 0
|
|
42
|
+
print(f"unknown sub-command {cmd!r}\n{_USAGE}", file=sys.stderr)
|
|
43
|
+
return 2
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
raise SystemExit(main())
|