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/__init__.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Build, benchmark, and optimize Claude skills for Python packages."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from acumen.bench import PlannedRun, build_matrix, pending, run_matrix, summarize
|
|
6
|
+
from acumen.config import Config, ConfigError, load_config, parse_config
|
|
7
|
+
from acumen.draft import DraftError, DraftResult, draft_skill
|
|
8
|
+
from acumen.env import (
|
|
9
|
+
AuthMode,
|
|
10
|
+
EnvError,
|
|
11
|
+
Target,
|
|
12
|
+
api_auth_available,
|
|
13
|
+
auth_available,
|
|
14
|
+
build_agent_env,
|
|
15
|
+
check_auth,
|
|
16
|
+
prepare_target,
|
|
17
|
+
resolve_auth_mode,
|
|
18
|
+
scrubbed_env,
|
|
19
|
+
session_auth_available,
|
|
20
|
+
)
|
|
21
|
+
from acumen.grade import Grade, Reason, grade_answer, grade_run
|
|
22
|
+
from acumen.improve import (
|
|
23
|
+
ImproveError,
|
|
24
|
+
ImproveResult,
|
|
25
|
+
TrainRun,
|
|
26
|
+
collect_train_runs,
|
|
27
|
+
find_test_access,
|
|
28
|
+
improve_skill,
|
|
29
|
+
make_test_guard,
|
|
30
|
+
)
|
|
31
|
+
from acumen.logs import LiveLog
|
|
32
|
+
from acumen.paths import RunKey, Split, arm_name, is_complete, parse_run_dir, run_dir, skill_from_arm
|
|
33
|
+
from acumen.report import (
|
|
34
|
+
Report,
|
|
35
|
+
ReportError,
|
|
36
|
+
SkillTests,
|
|
37
|
+
arm_metrics,
|
|
38
|
+
build_report,
|
|
39
|
+
load_results,
|
|
40
|
+
resolve_palette,
|
|
41
|
+
skill_tests,
|
|
42
|
+
tradeoff_figure,
|
|
43
|
+
)
|
|
44
|
+
from acumen.runner import RunOutcome, run_once
|
|
45
|
+
from acumen.sandbox import Sandbox, install_skill, sandbox
|
|
46
|
+
from acumen.scaffold import InitError, scaffold
|
|
47
|
+
from acumen.ship import ShipError, ShipResult, installer_exists, ship_skill
|
|
48
|
+
from acumen.skills import (
|
|
49
|
+
Skill,
|
|
50
|
+
SkillError,
|
|
51
|
+
SkillMeta,
|
|
52
|
+
available_versions,
|
|
53
|
+
latest_version,
|
|
54
|
+
load_skill,
|
|
55
|
+
next_version,
|
|
56
|
+
skill_content,
|
|
57
|
+
skill_hash,
|
|
58
|
+
)
|
|
59
|
+
from acumen.taskgen import (
|
|
60
|
+
TaskGenError,
|
|
61
|
+
TaskGenResult,
|
|
62
|
+
build_filtered_source,
|
|
63
|
+
dump_tasks,
|
|
64
|
+
find_skill_access,
|
|
65
|
+
generate_tasks,
|
|
66
|
+
make_skill_guard,
|
|
67
|
+
)
|
|
68
|
+
from acumen.tasks import Task, TaskError, TaskSplit, load_tasks, parse_tasks
|
|
69
|
+
from acumen.transcript import locate_transcript, render_transcript
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"AuthMode",
|
|
73
|
+
"Config",
|
|
74
|
+
"ConfigError",
|
|
75
|
+
"DraftError",
|
|
76
|
+
"DraftResult",
|
|
77
|
+
"EnvError",
|
|
78
|
+
"Grade",
|
|
79
|
+
"ImproveError",
|
|
80
|
+
"ImproveResult",
|
|
81
|
+
"InitError",
|
|
82
|
+
"LiveLog",
|
|
83
|
+
"PlannedRun",
|
|
84
|
+
"Reason",
|
|
85
|
+
"Report",
|
|
86
|
+
"ReportError",
|
|
87
|
+
"RunKey",
|
|
88
|
+
"RunOutcome",
|
|
89
|
+
"Sandbox",
|
|
90
|
+
"ShipError",
|
|
91
|
+
"ShipResult",
|
|
92
|
+
"Skill",
|
|
93
|
+
"SkillError",
|
|
94
|
+
"SkillMeta",
|
|
95
|
+
"Split",
|
|
96
|
+
"Target",
|
|
97
|
+
"Task",
|
|
98
|
+
"TaskError",
|
|
99
|
+
"TaskGenError",
|
|
100
|
+
"TaskGenResult",
|
|
101
|
+
"TaskSplit",
|
|
102
|
+
"TrainRun",
|
|
103
|
+
"__version__",
|
|
104
|
+
"api_auth_available",
|
|
105
|
+
"arm_metrics",
|
|
106
|
+
"arm_name",
|
|
107
|
+
"auth_available",
|
|
108
|
+
"available_versions",
|
|
109
|
+
"check_auth",
|
|
110
|
+
"build_agent_env",
|
|
111
|
+
"build_filtered_source",
|
|
112
|
+
"build_matrix",
|
|
113
|
+
"build_report",
|
|
114
|
+
"collect_train_runs",
|
|
115
|
+
"draft_skill",
|
|
116
|
+
"dump_tasks",
|
|
117
|
+
"find_skill_access",
|
|
118
|
+
"find_test_access",
|
|
119
|
+
"generate_tasks",
|
|
120
|
+
"grade_answer",
|
|
121
|
+
"grade_run",
|
|
122
|
+
"improve_skill",
|
|
123
|
+
"install_skill",
|
|
124
|
+
"installer_exists",
|
|
125
|
+
"is_complete",
|
|
126
|
+
"make_skill_guard",
|
|
127
|
+
"make_test_guard",
|
|
128
|
+
"latest_version",
|
|
129
|
+
"load_config",
|
|
130
|
+
"locate_transcript",
|
|
131
|
+
"load_results",
|
|
132
|
+
"load_skill",
|
|
133
|
+
"load_tasks",
|
|
134
|
+
"next_version",
|
|
135
|
+
"parse_config",
|
|
136
|
+
"parse_run_dir",
|
|
137
|
+
"parse_tasks",
|
|
138
|
+
"pending",
|
|
139
|
+
"prepare_target",
|
|
140
|
+
"render_transcript",
|
|
141
|
+
"resolve_auth_mode",
|
|
142
|
+
"resolve_palette",
|
|
143
|
+
"run_dir",
|
|
144
|
+
"run_matrix",
|
|
145
|
+
"run_once",
|
|
146
|
+
"sandbox",
|
|
147
|
+
"scaffold",
|
|
148
|
+
"scrubbed_env",
|
|
149
|
+
"session_auth_available",
|
|
150
|
+
"ship_skill",
|
|
151
|
+
"SkillTests",
|
|
152
|
+
"skill_content",
|
|
153
|
+
"skill_from_arm",
|
|
154
|
+
"skill_hash",
|
|
155
|
+
"skill_tests",
|
|
156
|
+
"summarize",
|
|
157
|
+
"tradeoff_figure",
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
__version__ = version("acumen")
|
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: acumen
|
|
3
|
+
description: Use for any question or task involving the Python package `acumen` (its CLI or its API) — setting up a benchmark project against a target package, writing or generating benchmark tasks, drafting/improving/hand-editing an agent Skill, running and interpreting skill-vs-baseline benchmark passes, shipping a skill into the target package, diagnosing a run, and choosing what to do next in that loop — open it before answering or running anything, because acumen's defaults, guardrails and correct next step are not guessable from the command names.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# acumen
|
|
7
|
+
|
|
8
|
+
One project directory targets one Python package. Everything is driven by the `acumen`
|
|
9
|
+
CLI (a thin shell over an importable API — see `references/python-api.md`). Commands are
|
|
10
|
+
run from the project dir and default to `config.yaml`, `tasks.yaml`, `skills/`, `runs/`,
|
|
11
|
+
`logs/`.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
acumen init # scaffold config.yaml + tasks.yaml (placeholders the user must fill)
|
|
15
|
+
acumen tasks # optional: mine the package for tasks, run them for ground truth
|
|
16
|
+
acumen bench --no-skill # baseline arm (this is also the default arm)
|
|
17
|
+
acumen draft # agent reads the package source -> skills/v1/
|
|
18
|
+
acumen bench --skill v1
|
|
19
|
+
acumen improve # agent reads v1's TRAIN runs -> skills/v2/
|
|
20
|
+
acumen bench --skill v2
|
|
21
|
+
acumen report # report.html + report.csv over every arm on disk
|
|
22
|
+
acumen ship --skill v2 # wire a `<dist>-install-skills` script into the target package
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That is the shape of the loop, not a script to execute unattended, and not a fixed order:
|
|
26
|
+
several stages have more than one correct next move, and some stages call for talking to
|
|
27
|
+
the user rather than running the next command at all.
|
|
28
|
+
|
|
29
|
+
## The loop is collaborative — propose, discuss, then run
|
|
30
|
+
|
|
31
|
+
`bench` bills the Anthropic API for every cell and the other commands drive long
|
|
32
|
+
autonomous agents, so the user decides each step and when to stop. At every stage: state
|
|
33
|
+
what you would do next and why, and let the user choose. Never chain commands
|
|
34
|
+
unattended, and never decide on your own that a skill is good enough.
|
|
35
|
+
|
|
36
|
+
**Before running anything, get from the user what acumen cannot infer** — the target, the
|
|
37
|
+
models, the budgets, whether tasks and results look right, whether to ship. Guessing those
|
|
38
|
+
is the most common way to waste a pass. The step after a command is often a question, not
|
|
39
|
+
a command.
|
|
40
|
+
|
|
41
|
+
| Just happened | What to propose next |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `acumen init` | Both files are placeholders (`repo: OWNER/REPO`, `REPLACE_ME` answers). Ask the user for what only they can supply — the target repo/path, ref, models, budgets — and fill in `config.yaml` with their answers. Do not guess a target or run the next command. |
|
|
44
|
+
| `config.yaml` filled | Offer both ways to get tasks: `acumen tasks` to generate them, or writing `tasks.yaml` by hand. Review generated tasks with the user before benching. |
|
|
45
|
+
| config + tasks ready | `acumen bench --no-skill` and `acumen draft` are **both** correct next steps — they are independent, and you need the baseline arm and a skill arm before any comparison means anything. Offer both. |
|
|
46
|
+
| a skill version exists | `acumen bench --skill vN`. |
|
|
47
|
+
| any `bench` finished | `acumen report` — the per-arm, per-split numbers come from the report; do not judge a version by eyeballing runs or by one arm's pass count. |
|
|
48
|
+
| `report` written | Discuss the results with the user and propose next steps: `improve` + re-bench if it has not beaten the baseline or the previous version, `ship` once a version has proven out, or stop. The user decides. |
|
|
49
|
+
| user has a skill dir from elsewhere | Copy it into `skills/v1/` (next unused version) inside the acumen project — versions are only ever read from `skills/`; there is no import command and no external path flag. |
|
|
50
|
+
|
|
51
|
+
## Route by goal
|
|
52
|
+
|
|
53
|
+
| Goal | Command | Depth |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| Start a project for package X | `acumen init`, then fill `config.yaml` with the user | `references/setup.md` |
|
|
56
|
+
| Get benchmark tasks without writing them | `acumen tasks [--force] [--feedback "…"]` | `references/setup.md` |
|
|
57
|
+
| Write tasks by hand | edit `tasks.yaml` | `references/setup.md` |
|
|
58
|
+
| Get a first skill | `acumen draft [--feedback "…"]` | `references/authoring.md` |
|
|
59
|
+
| Measure whether the skill helps | `acumen bench --no-skill` and `acumen bench --skill vN` | `references/benchmark.md` |
|
|
60
|
+
| Make the skill better | `acumen improve [--from vN]` then bench the new version | `references/authoring.md` |
|
|
61
|
+
| See results / decide when to stop | `acumen report` | `references/benchmark.md` |
|
|
62
|
+
| Hand-edit a skill version | copy `skills/vN` → `skills/v(N+1)`, edit, bench it | `references/authoring.md` |
|
|
63
|
+
| Give package users the skill | `acumen ship --skill vN` | `references/ship.md` |
|
|
64
|
+
| Script any of this in Python | `from acumen import …` | `references/python-api.md` |
|
|
65
|
+
| A run failed / the skill did nothing | inspect `runs/…/result.json`, `logs/*.jsonl` | `references/benchmark.md` |
|
|
66
|
+
|
|
67
|
+
## Preconditions
|
|
68
|
+
|
|
69
|
+
- Python ≥ 3.12, and **`uv` on PATH** — acumen builds the target's venv with it.
|
|
70
|
+
- The target (`repo`: a GitHub URL or a local path) must be pip-installable and declare
|
|
71
|
+
`[project].name` in `pyproject.toml`. A local `repo` path is resolved **relative to
|
|
72
|
+
`config.yaml`**.
|
|
73
|
+
- Credentials: `bench` **always bills the Anthropic API** and refuses to run without
|
|
74
|
+
`ANTHROPIC_API_KEY` (or `ANTHROPIC_AUTH_TOKEN` / Bedrock / Vertex) — it records real
|
|
75
|
+
per-run `cost_usd`, and it has no `--auth` flag. `tasks`/`draft`/`improve`/`ship`
|
|
76
|
+
default to the Claude subscription if you are logged in (`--auth {auto,session,api}`).
|
|
77
|
+
- The target is cloned + installed into a venv cached under `~/.cache/acumen`, keyed by
|
|
78
|
+
(repo, ref). Use `--refresh-target` after changing the target's own source.
|
|
79
|
+
|
|
80
|
+
## What guessing gets wrong
|
|
81
|
+
|
|
82
|
+
1. **Grading is exact string match on `answer.md` after `strip()`, case-sensitive.**
|
|
83
|
+
Nothing is normalized. `**TOKEN**` against `TOKEN` fails (recorded as `format_error`,
|
|
84
|
+
not `wrong_answer`). Task answers must be one short unambiguous token.
|
|
85
|
+
2. **A pass is models × tasks × splits × replicates.** The scaffolded config lists 3
|
|
86
|
+
models and `n_replicates: 3`, so *one* task costs 18 agent runs *per arm*. Check with
|
|
87
|
+
`acumen bench --dry-run` (it plans and exits, spending nothing) and agree the size with
|
|
88
|
+
the user before spending. Trim with `models:`, `n_replicates: 1`, `--task ID`, `--split`.
|
|
89
|
+
3. **`max_turns`/`max_usd` in `config.yaml` cap benchmark agents only.** `draft`,
|
|
90
|
+
`improve`, `tasks`, and `ship` are **unbounded** unless you pass `--max-turns`/`--max-usd`.
|
|
91
|
+
4. **Skill versions are immutable.** `draft` refuses (exit 2) if any `skills/vN` exists;
|
|
92
|
+
`improve` always writes the next unused directory. Never edit a benched version in
|
|
93
|
+
place — its hash is recorded in every `result.json`.
|
|
94
|
+
5. **`SKILL.md` frontmatter `name` must equal `config.skill_name`**, which defaults to the
|
|
95
|
+
repo's last path component, slugified and lowercased (`.../My_Pkg` → `my_pkg`). A
|
|
96
|
+
mismatch makes `bench`/`improve`/`ship` fail on load. `description` must be non-empty.
|
|
97
|
+
6. **`improve` needs benched train evidence for its parent.** Run
|
|
98
|
+
`acumen bench --skill vN` before `acumen improve`, or it errors with nothing to read.
|
|
99
|
+
7. **Never leak the test split.** `improve` is structurally and hook-blocked from
|
|
100
|
+
`runs/*/test/`; don't defeat that by pasting test answers into `--feedback`. A widening
|
|
101
|
+
train/test gap in the report is the overfitting signal you are watching for.
|
|
102
|
+
8. **Task prompts must not name the target package** — the harness already tells the agent
|
|
103
|
+
which package to use and that it is installed.
|
|
104
|
+
9. **Resume is automatic**: a run is "complete" when its `result.json` exists and is
|
|
105
|
+
non-empty, and completed runs are skipped. `--no-resume` re-runs them. Renaming a task
|
|
106
|
+
`id` orphans its old runs (the id is a path component).
|
|
107
|
+
10. **Confirm the skill actually loaded.** `bench` prints `skill loaded in N/M runs` from
|
|
108
|
+
per-run `skill_loaded` evidence, and warns when a skill arm never fired the Skill tool
|
|
109
|
+
(that arm measures nothing) or when the baseline did.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Authoring skill versions: draft, improve, hand-edit
|
|
2
|
+
|
|
3
|
+
## The skill directory contract
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
skills/v1/
|
|
7
|
+
SKILL.md # required, YAML frontmatter with `name` and `description`
|
|
8
|
+
references/*.md # optional
|
|
9
|
+
meta.json # acumen bookkeeping — NOT part of the skill
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- `name` must equal `config.skill_name` exactly; `description` must be non-empty. Both are
|
|
13
|
+
enforced on load by `bench`, `improve`, and `ship`.
|
|
14
|
+
- `meta.json` (`version`, `parent`, `rationale`, `hash`, optional `feedback`) is written by
|
|
15
|
+
acumen. It is excluded from the content hash and is never copied to a consuming agent.
|
|
16
|
+
- The content hash covers every other file's relative path and bytes, and is recorded in
|
|
17
|
+
every `result.json` — so editing a version after benching it silently invalidates the
|
|
18
|
+
comparison. **Versions are immutable; always make a new one.**
|
|
19
|
+
|
|
20
|
+
## `acumen draft`
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
acumen draft [--force] [--feedback "…"] [--model M] [--max-turns N] [--max-usd X]
|
|
24
|
+
[--stream] [--log-dir logs] [--auth auto|session|api]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
One agent that reads the target's **source** (docstrings, examples, docs) and writes
|
|
28
|
+
`skills/v1/`. It works in a staging dir; only a skill that loads and validates is promoted,
|
|
29
|
+
so a failed draft leaves no broken version behind. Exits 2 if any version already exists —
|
|
30
|
+
pass `--force` to add another, or use `improve` to build on the latest.
|
|
31
|
+
|
|
32
|
+
## `acumen improve`
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
acumen improve [--from vN] [--feedback "…"] [--model M] [--max-turns N] [--max-usd X]
|
|
36
|
+
[--stream] [--log-dir logs] [--auth auto|session|api]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Reads **how the parent version performed on the train split** — not the package source —
|
|
40
|
+
and edits a copy of it into the next version.
|
|
41
|
+
|
|
42
|
+
- Defaults to the latest version; `--from vN` picks another. Always writes the next unused
|
|
43
|
+
directory.
|
|
44
|
+
- Requires `runs/skill_vN/train/**/result.json` to exist: bench the parent first.
|
|
45
|
+
- The agent gets a curated copy of train runs (`SUMMARY.md` with failures first, plus each
|
|
46
|
+
run's prompt, expected, actual, `script.py`, and transcript). The real `runs/` tree is not
|
|
47
|
+
in scope, and a `PreToolUse` hook denies any path resolving under `runs/*/test/`.
|
|
48
|
+
- The CLI warns if the new version is byte-identical to its parent (the improver changed
|
|
49
|
+
nothing) — that is a signal to give it sharper evidence or `--feedback`.
|
|
50
|
+
|
|
51
|
+
## `--feedback` on `tasks` / `draft` / `improve`
|
|
52
|
+
|
|
53
|
+
Free text injected into the agent's prompt as *subordinate* guidance — it never overrides
|
|
54
|
+
the isolation or anti-overfit rules. Use it for what the agent cannot infer: package
|
|
55
|
+
context, what to emphasise, which functionality to skip. For `draft`/`improve` it is
|
|
56
|
+
recorded in `meta.json` and shown in the report. **Never paste test-split answers into
|
|
57
|
+
`improve --feedback`** — that defeats the split.
|
|
58
|
+
|
|
59
|
+
## Writing or editing a version by hand
|
|
60
|
+
|
|
61
|
+
Perfectly supported; the agents are a convenience, not a requirement. A skill authored
|
|
62
|
+
outside the project is benched the same way: copy the directory in as the next unused
|
|
63
|
+
`skills/vN` — `bench` only ever loads versions from the project's `skills/` root.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cp -r skills/v2 skills/v3 # then edit skills/v3/, and delete its inherited meta.json
|
|
67
|
+
acumen bench --skill v3
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A hand-made version with no `meta.json` benches fine; it just has no rationale/diff in the
|
|
71
|
+
report. What makes a version score well:
|
|
72
|
+
|
|
73
|
+
- The `description` decides whether the skill loads at all — name the goals a user would
|
|
74
|
+
actually phrase, honestly. An unloaded skill is a wasted arm.
|
|
75
|
+
- Keep `SKILL.md` short and push depth into `references/`; every token is paid on every
|
|
76
|
+
task, including ones where the skill is irrelevant.
|
|
77
|
+
- Spend words on what an agent would get wrong by guessing: non-obvious defaults, required
|
|
78
|
+
preprocessing, the function that looks right but isn't, argument shapes/orientation,
|
|
79
|
+
where output lands, the right order of steps.
|
|
80
|
+
- **Never name a dataset, parameter value, column, or expected answer from the train
|
|
81
|
+
tasks.** That is overfitting, and the test split will catch it.
|
|
82
|
+
- Verify every claim against the installed package before writing it down.
|
|
83
|
+
|
|
84
|
+
## Watching a long agent run
|
|
85
|
+
|
|
86
|
+
`tasks`/`draft`/`improve`/`ship` each write `logs/acumen-<command>-<YYYYMMDD-HHMMSS>.jsonl`,
|
|
87
|
+
one compact event per SDK message, **flushed as it goes** — read that file to follow
|
|
88
|
+
progress instead of streaming into your context. Tool results are recorded by status and
|
|
89
|
+
size, not inlined. `--stream` mirrors the conversation to the terminal; `--log-dir` moves
|
|
90
|
+
the logs. A rendered `.html` transcript lands beside the jsonl at the end (skipped, with a
|
|
91
|
+
note, if `claude-code-log` is missing).
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Benchmarking and reading results
|
|
2
|
+
|
|
3
|
+
## `acumen bench`
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
acumen bench [--no-skill | --skill v1] [--split train|test]... [--task ID]...
|
|
7
|
+
[--replicates N] [--max-concurrency N] [--dry-run] [--no-resume]
|
|
8
|
+
[--keep-sandboxes] [--refresh-target]
|
|
9
|
+
[--config config.yaml] [--tasks tasks.yaml] [--runs runs] [--skills skills]
|
|
10
|
+
[--cache ~/.cache/acumen]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- Neither `--no-skill` nor `--skill` → the **baseline** arm (they are mutually exclusive).
|
|
14
|
+
- `--split` and `--task` are repeatable; omitted means all.
|
|
15
|
+
- `--dry-run` prints the planned matrix and exits **before** target prep and before any
|
|
16
|
+
agent runs — free, and the right way to check the size of a pass.
|
|
17
|
+
- `--replicates` / `--max-concurrency` override the config for this pass only.
|
|
18
|
+
- Always bills the API (`ANTHROPIC_API_KEY` etc.); there is no `--auth` here, because the
|
|
19
|
+
recorded `cost_usd` must be real metered spend.
|
|
20
|
+
- Ctrl-C is safe: completed runs are preserved and the next invocation resumes.
|
|
21
|
+
|
|
22
|
+
**Arm parity is the whole point.** Both arms get an identical prompt, tools, caps, and
|
|
23
|
+
environment; the only difference is that a skill arm copies the skill's content files into
|
|
24
|
+
`<sandbox>/.claude/skills/<skill_name>/` where project settings discovery finds it.
|
|
25
|
+
`meta.json` is never copied. Each run gets a fresh empty sandbox with the target venv on
|
|
26
|
+
PATH, a throwaway `HOME` and `CLAUDE_CONFIG_DIR` — no repo source, no user settings, no
|
|
27
|
+
`CLAUDE.md` memories, no visibility of other runs.
|
|
28
|
+
|
|
29
|
+
## The run tree
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
runs/{arm}/{split}/{model}/{task_id}/rep_{n}/
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`arm` is `noskill` or `skill_v1`, `skill_v2`, …; `model` is slugified. Each completed leaf
|
|
36
|
+
holds five files:
|
|
37
|
+
|
|
38
|
+
| File | What |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `answer.md` | What the agent wrote — the only thing graded |
|
|
41
|
+
| `script.py` | The agent's reproduction script (absent if it ran no code) |
|
|
42
|
+
| `transcript.jsonl` | SDK-native transcript |
|
|
43
|
+
| `transcript.html` | Rendered transcript (via `claude-code-log`) |
|
|
44
|
+
| `result.json` | The unit of record — written **last** |
|
|
45
|
+
|
|
46
|
+
`result.json` presence + non-zero size is what marks a run complete, which is what makes
|
|
47
|
+
resume safe. Useful fields: `success`, `reason`, `answer`, `expected`, `model`, `turns`,
|
|
48
|
+
`cost_usd`, `input_tokens`, `output_tokens`, `duration_s`, `skill_hash`, `skill_name`,
|
|
49
|
+
`skill_loaded`, `pkg_version`, `commit`, `session_id`, `subtype`, `error`.
|
|
50
|
+
|
|
51
|
+
## Reasons
|
|
52
|
+
|
|
53
|
+
| `reason` | Meaning |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `ok` | Exact match — the only success |
|
|
56
|
+
| `wrong_answer` | Content differs |
|
|
57
|
+
| `format_error` | Content would match but formatting broke the exact match (bold, quotes, "Answer:", code fence, trailing period) |
|
|
58
|
+
| `no_answer_file` | The agent never wrote `answer.md` |
|
|
59
|
+
| `budget` / `max_turns` | Cap breached — a **failure regardless** of what `answer.md` contains |
|
|
60
|
+
| `error` | The agent crashed or produced no result |
|
|
61
|
+
|
|
62
|
+
A run of `format_error`s means the harness's answer-format instruction is losing to the
|
|
63
|
+
task prompt; a run of `no_answer_file` usually means the task is too big for `max_turns`.
|
|
64
|
+
|
|
65
|
+
## Diagnosing a disappointing arm
|
|
66
|
+
|
|
67
|
+
1. `skill loaded in 0/N runs` on a skill arm → the arm measured nothing. The skill's
|
|
68
|
+
`description` is what decides whether it loads; make it name the goals the task prompts
|
|
69
|
+
actually phrase.
|
|
70
|
+
2. The Skill tool firing in *baseline* runs is also flagged — that baseline is not clean.
|
|
71
|
+
3. Open `runs/.../transcript.html` for a failing run, or re-run one cell with
|
|
72
|
+
`--task ID --split train --replicates 1 --keep-sandboxes` and inspect the sandbox.
|
|
73
|
+
4. Compare `answer` vs `expected` in `result.json` before blaming the skill — a systematic
|
|
74
|
+
near-miss is a task-authoring problem, not a skill problem.
|
|
75
|
+
|
|
76
|
+
## `acumen report`
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
acumen report [--runs runs] [--tasks tasks.yaml] [--skills skills] [--out report.html]
|
|
80
|
+
[--palette claude-opus-5=#3b7ea1]...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- Writes `report.html` **and a sidecar `report.csv`** next to it, overwriting both; the
|
|
84
|
+
report always reflects every run currently on disk across every arm.
|
|
85
|
+
- Reads only `result.json` files — never transcripts. Fails if `runs/` holds none.
|
|
86
|
+
- **The figures show the TEST split** (the held-out measure). The per-run table below them
|
|
87
|
+
lists both splits.
|
|
88
|
+
- Sections: Overview (success rate, tokens, cost, time per arm, bars coloured by model),
|
|
89
|
+
Per-task breakdown, Runs table (links to each `transcript.html`), and — when `--skills`
|
|
90
|
+
resolves — Skill versions with each version's rationale and diff against its parent.
|
|
91
|
+
- `--tasks` / `--skills` are optional; missing ones only drop their sections (it says so).
|
|
92
|
+
- `--palette MODEL=COLOUR` is repeatable and accepts comma-separated pairs. A key that
|
|
93
|
+
matches no model in the data, or an unparseable colour, is rejected up front.
|
|
94
|
+
|
|
95
|
+
Run `acumen report` after every bench — it is how a version is judged, and the only view
|
|
96
|
+
that compares arms and splits side by side. Then take the results to the user: say what
|
|
97
|
+
improved, what did not, and what you would do next (`improve` and re-bench, more tasks,
|
|
98
|
+
`ship`, or stop). Train improving while test does not is overfitting — that gap is exactly
|
|
99
|
+
what the split exists to expose — but the call on when to stop and what to ship is the
|
|
100
|
+
user's.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Python API
|
|
2
|
+
|
|
3
|
+
Everything is re-exported from the top level: `from acumen import build_report, load_config, …`.
|
|
4
|
+
The CLI (`acumen.cli:main`) is a thin shell over it, so anything the CLI does is scriptable.
|
|
5
|
+
`main(argv)` returns an exit code and converts the library's error types into `error: …` on
|
|
6
|
+
stderr — call the functions directly if you want the exceptions.
|
|
7
|
+
|
|
8
|
+
## Loading config and tasks
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from acumen import load_config, load_tasks, parse_config, parse_tasks
|
|
13
|
+
|
|
14
|
+
cfg = load_config(Path("config.yaml")) # validates; resolves a local repo relative to the file
|
|
15
|
+
tasks = load_tasks(Path("tasks.yaml")) # list[Task]; task.split("train") -> TaskSplit(prompt, answer)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`Config` is a frozen dataclass — override with `dataclasses.replace(cfg, n_replicates=1)`.
|
|
19
|
+
`parse_config` / `parse_tasks` take already-parsed dicts, for building them in memory.
|
|
20
|
+
|
|
21
|
+
## Preparing the target
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from acumen import prepare_target
|
|
25
|
+
from acumen.env import DEFAULT_CACHE_ROOT
|
|
26
|
+
|
|
27
|
+
target = prepare_target(cfg, DEFAULT_CACHE_ROOT, refresh=False)
|
|
28
|
+
target.python # venv interpreter with the package installed
|
|
29
|
+
target.bin_dir # what goes on an agent's PATH
|
|
30
|
+
target.fingerprint # "<pkg> <version>", as recorded in result.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Needs `uv` on PATH. Cached by (repo, ref) under `~/.cache/acumen`.
|
|
34
|
+
|
|
35
|
+
## Planning and running a pass
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
from acumen import build_matrix, pending, run_matrix, load_skill, summarize
|
|
40
|
+
|
|
41
|
+
skill = load_skill(Path("skills"), "v1", expect_name=cfg.skill_name) # None for the baseline
|
|
42
|
+
planned = build_matrix(cfg, tasks, skill="v1", splits=("train",), task_ids=["t1"])
|
|
43
|
+
todo = pending(planned, Path("runs"), resume=True)
|
|
44
|
+
outcomes = asyncio.run(run_matrix(
|
|
45
|
+
todo, target=target, runs_root=Path("runs"),
|
|
46
|
+
max_concurrency=cfg.max_concurrency, auth_mode="api", skill=skill,
|
|
47
|
+
on_start=lambda p: None, on_done=lambda o: None,
|
|
48
|
+
env_passthrough=cfg.env_passthrough,
|
|
49
|
+
))
|
|
50
|
+
summarize(outcomes) # {"ok": 5, "wrong_answer": 1, ...}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`run_matrix` and `run_once` are coroutines. `run_once` records agent crashes as failed runs
|
|
54
|
+
rather than raising, so one bad run never kills a pass. `run_once` raises `ValueError` if
|
|
55
|
+
`key.arm` and the `skill` argument disagree — the arm is the source of truth.
|
|
56
|
+
|
|
57
|
+
`build_matrix` (and `--dry-run`) is pure planning: no agents, no network, no cost.
|
|
58
|
+
|
|
59
|
+
## Grading and paths
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from acumen import grade_answer, grade_run, run_dir, parse_run_dir, is_complete, RunKey, arm_name
|
|
63
|
+
|
|
64
|
+
grade_answer("**SPI1**", "SPI1") # Grade(success=False, reason='format_error', answer='**SPI1**')
|
|
65
|
+
run_dir(Path("runs"), RunKey(arm=arm_name("v1"), split="test", model="claude-opus-5",
|
|
66
|
+
task_id="t1", rep=1))
|
|
67
|
+
is_complete(d) # a non-empty result.json is what "done" means
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## The meta-agents
|
|
71
|
+
|
|
72
|
+
`draft_skill`, `improve_skill`, `generate_tasks`, `ship_skill` are all coroutines taking
|
|
73
|
+
keyword-only args (`cfg=`, `target=`, plus their own roots) and returning a result dataclass
|
|
74
|
+
(`DraftResult`, `ImproveResult`, `TaskGenResult`, `ShipResult`) carrying the new `Skill`,
|
|
75
|
+
`cost_usd`, `turns`, and log paths. `max_turns`/`max_usd` default to `None` = **unbounded**.
|
|
76
|
+
Pass a `LiveLog` as `log=` for the JSONL feed:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from acumen import LiveLog, draft_skill
|
|
80
|
+
log = LiveLog.open(Path("logs"), "draft", stream=False)
|
|
81
|
+
with log:
|
|
82
|
+
result = asyncio.run(draft_skill(cfg=cfg, target=target, skills_root=Path("skills"),
|
|
83
|
+
auth_mode="session", log=log))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Aggregating results
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from acumen import load_results, arm_metrics, build_report
|
|
90
|
+
|
|
91
|
+
df = load_results(Path("runs")) # one row per result.json, + total_tokens, arm_label
|
|
92
|
+
arm_metrics(df[df["split"] == "test"]) # per-arm rate, stderr, tokens, cost, time, n
|
|
93
|
+
report = build_report(Path("runs"), Path("report.html"), tasks, skills_root=Path("skills"))
|
|
94
|
+
report.n_runs, report.results # the DataFrame behind the HTML
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`build_report` writes `report.html` **and `report.csv`** (same stem) and returns `Report`.
|
|
98
|
+
Filter `df` to a split yourself before `arm_metrics` — it does not.
|
|
99
|
+
|
|
100
|
+
## Introspection helpers
|
|
101
|
+
|
|
102
|
+
`available_versions`, `latest_version`, `next_version`, `skill_hash`, `skill_content`
|
|
103
|
+
(for diffing), `installer_exists(src_dir)`, `collect_train_runs(runs_root, arm, tasks)`,
|
|
104
|
+
and the two pure guards `find_test_access` / `find_skill_access` (testable without an agent).
|
|
105
|
+
`scrubbed_env` / `build_agent_env` / `sandbox` / `install_skill` let you reproduce a run's
|
|
106
|
+
exact isolated environment by hand.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Project setup: `config.yaml` and `tasks.yaml`
|
|
2
|
+
|
|
3
|
+
`acumen init [--dir DIR] [--force]` writes both files as annotated placeholders. It refuses
|
|
4
|
+
to clobber either unless `--force`. Both loaders are **strict: unknown keys are rejected**.
|
|
5
|
+
|
|
6
|
+
Nothing else can run until `config.yaml` is filled in: the scaffold ships
|
|
7
|
+
`repo: https://github.com/OWNER/REPO` and a `REPLACE_ME` example task. Ask the user for the
|
|
8
|
+
values only they know (target repo/path, ref, extras, which models, budgets) instead of
|
|
9
|
+
inventing them.
|
|
10
|
+
|
|
11
|
+
## config.yaml
|
|
12
|
+
|
|
13
|
+
Only `repo` is required; delete a line to take its default.
|
|
14
|
+
|
|
15
|
+
| Key | Default | Notes |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `repo` | — | GitHub URL (`https://`, `git@`, `ssh://`, `git://`) or a local path. A local path is resolved relative to `config.yaml` and must exist. |
|
|
18
|
+
| `ref` | `main` | Branch/tag/commit; ignored for local paths. |
|
|
19
|
+
| `extras` | `[]` | pip extras installed with the target, e.g. `[test]`. |
|
|
20
|
+
| `python` | `"3.12"` | Interpreter for the target's venv (quote it — `3.10` unquoted is a float). |
|
|
21
|
+
| `env_passthrough` | `[]` | Extra env var names agents may keep. The agent env is a clean allowlist (auth/proxy/TLS only); **everything else from your shell is blanked**. A target needing `OMP_NUM_THREADS`, `R_HOME`, a service key, etc. must name it here. |
|
|
22
|
+
| `models` | `[claude-opus-5]` | Benchmark models. Duplicates rejected. `models[0]` is the default for the four `*_model` keys below. |
|
|
23
|
+
| `n_replicates` | `3` | Runs per (model, task, split) cell. |
|
|
24
|
+
| `max_concurrency` | `4` | Simultaneous benchmark agents. |
|
|
25
|
+
| `max_turns` | `40` | **Benchmark agents only.** |
|
|
26
|
+
| `max_usd` | `3.0` | **Benchmark agents only.** |
|
|
27
|
+
| `draft_model` / `improve_model` / `tasks_model` / `ship_model` | `models[0]` | Meta-agent models; overridable per command with `--model`. |
|
|
28
|
+
| `skill_name` | repo basename, slugified + lowercased | Must equal the `name:` in the skill's frontmatter. |
|
|
29
|
+
|
|
30
|
+
The scaffold's `models:` line lists three models — with `n_replicates: 3` that is 18 runs
|
|
31
|
+
per task per arm. Cut it down before the first real pass.
|
|
32
|
+
|
|
33
|
+
## tasks.yaml
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
tasks:
|
|
37
|
+
- id: some_analysis # unique, filesystem-safe: [A-Za-z0-9._-]; used as a path component
|
|
38
|
+
train:
|
|
39
|
+
prompt: >-
|
|
40
|
+
One paragraph: the goal, the input, and exactly what to report.
|
|
41
|
+
answer: ONE_TOKEN
|
|
42
|
+
test:
|
|
43
|
+
prompt: >-
|
|
44
|
+
The same analysis on a different input / target.
|
|
45
|
+
answer: ANOTHER_TOKEN
|
|
46
|
+
# optional per-task overrides — the only other allowed keys:
|
|
47
|
+
# max_turns: 60
|
|
48
|
+
# max_usd: 5.0
|
|
49
|
+
# model: claude-sonnet-5
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Both `train` and `test` are required, each with non-empty `prompt` and `answer`. Both
|
|
53
|
+
splits always run in a pass; only train results ever reach `improve`.
|
|
54
|
+
|
|
55
|
+
### Writing a task that measures anything
|
|
56
|
+
|
|
57
|
+
- **Answers are graded by exact string match after `strip()`, case-sensitive.** Keep the
|
|
58
|
+
answer to one token: a name, a category, a count, a number at a stated precision. End the
|
|
59
|
+
prompt by stating exactly what to report and in what form.
|
|
60
|
+
- **Do not name the target package**, and do not mention a version — the harness preamble
|
|
61
|
+
already tells the agent the package is installed and which interpreter to use.
|
|
62
|
+
- **State the goal, not the recipe.** No numbered steps, no function/argument names, no
|
|
63
|
+
description of the data's shape or columns. What is being measured is whether the agent
|
|
64
|
+
can find the "how" itself; a prompt that spells out the calls measures nothing.
|
|
65
|
+
- Train and test must be **the same analysis on different inputs**, with different correct
|
|
66
|
+
answers — otherwise a skill can pass by memorizing one answer.
|
|
67
|
+
- A good task is one the target package solves and the **no-skill baseline gets wrong** —
|
|
68
|
+
a task the baseline already passes leaves no room to show a gain. The `--no-skill` arm is
|
|
69
|
+
what tells you; it can be run before or after the first skill exists.
|
|
70
|
+
- Renaming an `id` orphans every existing run under it.
|
|
71
|
+
|
|
72
|
+
## `acumen tasks` — generate tasks.yaml
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
acumen tasks [--out tasks.yaml] [--force] [--feedback "skip the plotting API"] \
|
|
76
|
+
[--model M] [--max-turns N] [--max-usd X] [--stream] [--log-dir logs]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
One autonomous agent that reads the package source **and executes it in the target venv**:
|
|
80
|
+
every ground-truth answer comes from a script it actually ran, not from docs. It enumerates
|
|
81
|
+
the package's tutorials/vignettes and writes at least one task per tutorial, so expect a
|
|
82
|
+
sizeable file. Unbounded in turns and cost by default.
|
|
83
|
+
|
|
84
|
+
- Refuses to overwrite an existing `--out` without `--force` (the check runs *before* the
|
|
85
|
+
costly target prep).
|
|
86
|
+
- **There is no append mode.** The generator never reads your existing tasks file, so
|
|
87
|
+
appending would silently duplicate. To combine, generate to a separate path and merge by
|
|
88
|
+
hand.
|
|
89
|
+
- Existing skills and agent-instruction files (`SKILL.md`, `.claude/`, `CLAUDE.md`,
|
|
90
|
+
`AGENTS.md`, `.cursor/`, root `skills/`) are stripped from the source copy it reads and
|
|
91
|
+
blocked by a hook, so pre-written guidance cannot bias which tasks it picks.
|
|
92
|
+
- The output is validated through the strict tasks loader before it is written — it can
|
|
93
|
+
never emit a file the rest of the pipeline would reject.
|
|
94
|
+
- **Review what it produces.** The answers are only as good as the scripts it ran.
|