acumen 0.0.1.dev0__py3-none-any.whl
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.
- acumen/__init__.py +160 -0
- acumen/_skills/__init__.py +0 -0
- acumen/_skills/data/SKILL.md +109 -0
- acumen/_skills/data/references/authoring.md +91 -0
- acumen/_skills/data/references/benchmark.md +100 -0
- acumen/_skills/data/references/python-api.md +106 -0
- acumen/_skills/data/references/setup.md +94 -0
- acumen/_skills/data/references/ship.md +53 -0
- acumen/_skills/install.py +185 -0
- acumen/assets/banner.svg +501 -0
- acumen/assets/logo.svg +47 -0
- acumen/bench.py +202 -0
- acumen/cli.py +626 -0
- acumen/config.py +204 -0
- acumen/draft.py +202 -0
- acumen/env.py +602 -0
- acumen/grade.py +104 -0
- acumen/improve.py +596 -0
- acumen/logs.py +255 -0
- acumen/paths.py +164 -0
- acumen/procs.py +195 -0
- acumen/prompts.py +942 -0
- acumen/report.py +1858 -0
- acumen/runner.py +343 -0
- acumen/sandbox.py +153 -0
- acumen/scaffold.py +118 -0
- acumen/ship.py +298 -0
- acumen/skills.py +298 -0
- acumen/taskgen.py +424 -0
- acumen/tasks.py +144 -0
- acumen/transcript.py +77 -0
- acumen-0.0.1.dev0.dist-info/METADATA +168 -0
- acumen-0.0.1.dev0.dist-info/RECORD +36 -0
- acumen-0.0.1.dev0.dist-info/WHEEL +4 -0
- acumen-0.0.1.dev0.dist-info/entry_points.txt +3 -0
- acumen-0.0.1.dev0.dist-info/licenses/LICENSE +21 -0
acumen/bench.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Benchmark orchestration: build the matrix, run it concurrently, resume what's done."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from acumen.config import Config
|
|
11
|
+
from acumen.env import AuthMode, Target
|
|
12
|
+
from acumen.paths import SPLITS, RunKey, Split, arm_name, is_complete, run_dir
|
|
13
|
+
from acumen.runner import RunOutcome, run_once
|
|
14
|
+
from acumen.skills import Skill
|
|
15
|
+
from acumen.tasks import Task
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class PlannedRun:
|
|
20
|
+
"""One cell of the benchmark matrix, with its caps already resolved."""
|
|
21
|
+
|
|
22
|
+
key: RunKey
|
|
23
|
+
task: Task
|
|
24
|
+
model: str
|
|
25
|
+
max_turns: int
|
|
26
|
+
max_usd: float
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def models_for(cfg: Config, task: Task) -> list[str]:
|
|
30
|
+
"""Return the models a task runs on — its own override, else the config's list."""
|
|
31
|
+
return [task.model] if task.model else list(cfg.models)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_matrix(
|
|
35
|
+
cfg: Config,
|
|
36
|
+
tasks: Sequence[Task],
|
|
37
|
+
*,
|
|
38
|
+
skill: str | None = None,
|
|
39
|
+
splits: Iterable[Split] = SPLITS,
|
|
40
|
+
task_ids: Sequence[str] | None = None,
|
|
41
|
+
) -> list[PlannedRun]:
|
|
42
|
+
"""Expand config and tasks into the full list of runs for one arm.
|
|
43
|
+
|
|
44
|
+
A pass is models x tasks x replicates x splits. Both splits always run; only
|
|
45
|
+
train results are ever shown to the improver, and that is enforced downstream.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
cfg
|
|
50
|
+
The pass config.
|
|
51
|
+
tasks
|
|
52
|
+
The tasks to run.
|
|
53
|
+
skill
|
|
54
|
+
Skill version for this arm, or ``None`` for the baseline.
|
|
55
|
+
splits
|
|
56
|
+
Which splits to run. Defaults to both.
|
|
57
|
+
task_ids
|
|
58
|
+
Restrict to these task ids; ``None`` runs all of them.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
The planned runs, in a stable order.
|
|
63
|
+
"""
|
|
64
|
+
splits = list(splits)
|
|
65
|
+
for split in splits:
|
|
66
|
+
if split not in SPLITS:
|
|
67
|
+
raise ValueError(f"split must be one of {SPLITS}, got {split!r}")
|
|
68
|
+
wanted = set(task_ids) if task_ids else None
|
|
69
|
+
if wanted:
|
|
70
|
+
known = {t.id for t in tasks}
|
|
71
|
+
missing = wanted - known
|
|
72
|
+
if missing:
|
|
73
|
+
raise ValueError(f"unknown task ids: {sorted(missing)} (known: {sorted(known)})")
|
|
74
|
+
|
|
75
|
+
arm = arm_name(skill)
|
|
76
|
+
planned: list[PlannedRun] = []
|
|
77
|
+
for task in tasks:
|
|
78
|
+
if wanted and task.id not in wanted:
|
|
79
|
+
continue
|
|
80
|
+
for split in splits:
|
|
81
|
+
for model in models_for(cfg, task):
|
|
82
|
+
for rep in range(1, cfg.n_replicates + 1):
|
|
83
|
+
planned.append(
|
|
84
|
+
PlannedRun(
|
|
85
|
+
key=RunKey(arm=arm, split=split, model=model, task_id=task.id, rep=rep),
|
|
86
|
+
task=task,
|
|
87
|
+
model=model,
|
|
88
|
+
max_turns=task.max_turns or cfg.max_turns,
|
|
89
|
+
max_usd=task.max_usd or cfg.max_usd,
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
return planned
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def pending(planned: Sequence[PlannedRun], runs_root: Path, *, resume: bool = True) -> list[PlannedRun]:
|
|
96
|
+
"""Drop runs that already have a complete ``result.json``."""
|
|
97
|
+
if not resume:
|
|
98
|
+
return list(planned)
|
|
99
|
+
return [p for p in planned if not is_complete(run_dir(runs_root, p.key))]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def run_matrix(
|
|
103
|
+
planned: Sequence[PlannedRun],
|
|
104
|
+
*,
|
|
105
|
+
target: Target,
|
|
106
|
+
runs_root: Path,
|
|
107
|
+
max_concurrency: int,
|
|
108
|
+
auth_mode: AuthMode = "api",
|
|
109
|
+
skill: Skill | None = None,
|
|
110
|
+
skill_name: str | None = None,
|
|
111
|
+
sandbox_base: Path | None = None,
|
|
112
|
+
keep_sandbox: bool = False,
|
|
113
|
+
stderr: Callable[[str], None] | None = None,
|
|
114
|
+
on_start: Callable[[PlannedRun], None] | None = None,
|
|
115
|
+
on_done: Callable[[RunOutcome], None] | None = None,
|
|
116
|
+
env_passthrough: Sequence[str] | None = None,
|
|
117
|
+
) -> list[RunOutcome]:
|
|
118
|
+
"""Run planned runs concurrently, bounded by ``max_concurrency``.
|
|
119
|
+
|
|
120
|
+
One failing run never takes down the pass — :func:`acumen.runner.run_once` records
|
|
121
|
+
agent errors as failed runs rather than raising.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
planned
|
|
126
|
+
The runs to execute; already filtered for resume by :func:`pending`.
|
|
127
|
+
target
|
|
128
|
+
The prepared target.
|
|
129
|
+
runs_root
|
|
130
|
+
The ``runs/`` root.
|
|
131
|
+
max_concurrency
|
|
132
|
+
Ceiling on simultaneous agents.
|
|
133
|
+
auth_mode
|
|
134
|
+
Which credential every run authenticates with; benchmark passes always use ``"api"``
|
|
135
|
+
so the recorded ``cost_usd`` reflects real metered spend.
|
|
136
|
+
skill
|
|
137
|
+
The skill every run in this matrix installs, or ``None`` for the baseline. One
|
|
138
|
+
matrix is one arm, so this is a property of the pass rather than of a run.
|
|
139
|
+
skill_name
|
|
140
|
+
Name of the skill under test (``config.skill_name``), used to recognise it in each
|
|
141
|
+
transcript. Pass it in the baseline arm too — nothing is installed there, but a
|
|
142
|
+
baseline run that loads it anyway is the contamination worth catching.
|
|
143
|
+
sandbox_base
|
|
144
|
+
Parent directory for sandboxes.
|
|
145
|
+
keep_sandbox
|
|
146
|
+
Leave sandboxes on disk for inspection.
|
|
147
|
+
stderr
|
|
148
|
+
Callback for each run's CLI subprocess stderr. Pass one shared
|
|
149
|
+
:class:`~acumen.runner.StderrFilter` so the per-spawn startup warnings are
|
|
150
|
+
printed once for the whole pass instead of once per run.
|
|
151
|
+
on_start
|
|
152
|
+
Optional callable invoked with each :class:`PlannedRun` as it is admitted through
|
|
153
|
+
the concurrency gate and begins, for progress output.
|
|
154
|
+
on_done
|
|
155
|
+
Optional callable invoked with each :class:`~acumen.runner.RunOutcome` as it
|
|
156
|
+
lands, for progress output.
|
|
157
|
+
env_passthrough
|
|
158
|
+
Extra environment variable names each run carries into its sandbox on top of the
|
|
159
|
+
built-in allowlist (the operator's ``config.env_passthrough``).
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
The outcomes, in completion order.
|
|
164
|
+
"""
|
|
165
|
+
semaphore = asyncio.Semaphore(max_concurrency)
|
|
166
|
+
outcomes: list[RunOutcome] = []
|
|
167
|
+
|
|
168
|
+
async def one(item: PlannedRun) -> RunOutcome:
|
|
169
|
+
async with semaphore:
|
|
170
|
+
if on_start is not None:
|
|
171
|
+
on_start(item)
|
|
172
|
+
outcome = await run_once(
|
|
173
|
+
key=item.key,
|
|
174
|
+
task=item.task,
|
|
175
|
+
target=target,
|
|
176
|
+
run_dir=run_dir(runs_root, item.key),
|
|
177
|
+
model=item.model,
|
|
178
|
+
max_turns=item.max_turns,
|
|
179
|
+
max_usd=item.max_usd,
|
|
180
|
+
auth_mode=auth_mode,
|
|
181
|
+
skill=skill,
|
|
182
|
+
skill_name=skill_name,
|
|
183
|
+
sandbox_base=sandbox_base,
|
|
184
|
+
keep_sandbox=keep_sandbox,
|
|
185
|
+
stderr=stderr,
|
|
186
|
+
env_passthrough=env_passthrough,
|
|
187
|
+
)
|
|
188
|
+
if on_done is not None:
|
|
189
|
+
on_done(outcome)
|
|
190
|
+
return outcome
|
|
191
|
+
|
|
192
|
+
for coro in asyncio.as_completed([one(item) for item in planned]):
|
|
193
|
+
outcomes.append(await coro)
|
|
194
|
+
return outcomes
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def summarize(outcomes: Sequence[RunOutcome]) -> dict[str, int]:
|
|
198
|
+
"""Count outcomes by reason, for a one-line pass summary."""
|
|
199
|
+
counts: dict[str, int] = {}
|
|
200
|
+
for outcome in outcomes:
|
|
201
|
+
counts[outcome.reason] = counts.get(outcome.reason, 0) + 1
|
|
202
|
+
return counts
|