dfm-agent 0.0.1__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.
- dfm/__init__.py +10 -0
- dfm/benchmark/__init__.py +25 -0
- dfm/benchmark/runner.py +379 -0
- dfm/benchmark/tasks.py +385 -0
- dfm/ci/__init__.py +14 -0
- dfm/ci/logs.py +77 -0
- dfm/ci/workflows.py +123 -0
- dfm/cli/__init__.py +1 -0
- dfm/cli/app.py +117 -0
- dfm/cli/ask_cmd.py +64 -0
- dfm/cli/benchmark_cmd.py +104 -0
- dfm/cli/ci_cmd.py +64 -0
- dfm/cli/config_cmd.py +77 -0
- dfm/cli/diff_cmd.py +49 -0
- dfm/cli/doctor_cmd.py +44 -0
- dfm/cli/github_cmd.py +255 -0
- dfm/cli/logs_cmd.py +95 -0
- dfm/cli/memory_cmd.py +79 -0
- dfm/cli/models_cmd.py +184 -0
- dfm/cli/processes_cmd.py +69 -0
- dfm/cli/repo_cmd.py +104 -0
- dfm/cli/review_cmd.py +130 -0
- dfm/cli/services_cmd.py +89 -0
- dfm/cli/sessions_cmd.py +78 -0
- dfm/cli/setup_cmd.py +46 -0
- dfm/cli/status_cmd.py +116 -0
- dfm/cli/work_cmd.py +160 -0
- dfm/config/__init__.py +17 -0
- dfm/config/loader.py +235 -0
- dfm/config/model.py +119 -0
- dfm/config/paths.py +90 -0
- dfm/config/writer.py +77 -0
- dfm/context/__init__.py +12 -0
- dfm/context/engine.py +400 -0
- dfm/docsgen.py +172 -0
- dfm/doctor/__init__.py +5 -0
- dfm/doctor/checks.py +385 -0
- dfm/execution/__init__.py +19 -0
- dfm/execution/environment.py +274 -0
- dfm/integrations/__init__.py +1 -0
- dfm/integrations/base.py +11 -0
- dfm/integrations/github/__init__.py +25 -0
- dfm/integrations/github/client.py +412 -0
- dfm/integrations/reflex/__init__.py +5 -0
- dfm/integrations/reflex/provider.py +221 -0
- dfm/integrations/repocanon/__init__.py +33 -0
- dfm/integrations/repocanon/provider.py +423 -0
- dfm/integrations/repokg/__init__.py +37 -0
- dfm/integrations/repokg/provider.py +314 -0
- dfm/integrations/repokg/structure.py +167 -0
- dfm/lsp/__init__.py +5 -0
- dfm/lsp/client.py +527 -0
- dfm/models/__init__.py +33 -0
- dfm/models/apple/__init__.py +5 -0
- dfm/models/apple/fm.py +248 -0
- dfm/models/base.py +127 -0
- dfm/models/mlx/__init__.py +6 -0
- dfm/models/mlx/discovery.py +225 -0
- dfm/models/mlx/evaluate.py +125 -0
- dfm/models/mlx/model.py +377 -0
- dfm/models/mlx/text.py +210 -0
- dfm/models/mlx/worker.py +519 -0
- dfm/models/pool.py +170 -0
- dfm/models/resources.py +238 -0
- dfm/models/schema.py +78 -0
- dfm/models/structured.py +113 -0
- dfm/models/tasks.py +65 -0
- dfm/models/thermal.py +58 -0
- dfm/observability/__init__.py +6 -0
- dfm/observability/logs.py +55 -0
- dfm/observability/metrics.py +160 -0
- dfm/orchestration/__init__.py +1 -0
- dfm/orchestration/agent.py +887 -0
- dfm/orchestration/ask.py +96 -0
- dfm/orchestration/ci.py +183 -0
- dfm/orchestration/github.py +340 -0
- dfm/orchestration/implement.py +117 -0
- dfm/orchestration/plan.py +243 -0
- dfm/orchestration/resume.py +150 -0
- dfm/orchestration/review.py +541 -0
- dfm/orchestration/runtime.py +246 -0
- dfm/orchestration/security_review.py +422 -0
- dfm/orchestration/setup.py +214 -0
- dfm/policy/__init__.py +35 -0
- dfm/policy/engine.py +273 -0
- dfm/policy/network.py +211 -0
- dfm/policy/types.py +104 -0
- dfm/processes/__init__.py +35 -0
- dfm/processes/manager.py +613 -0
- dfm/provenance/__init__.py +5 -0
- dfm/provenance/item.py +140 -0
- dfm/py.typed +0 -0
- dfm/recovery/__init__.py +5 -0
- dfm/recovery/checkpoint.py +198 -0
- dfm/recovery/reconcile.py +97 -0
- dfm/repository/__init__.py +1 -0
- dfm/repository/freshness.py +220 -0
- dfm/repository/intelligence.py +123 -0
- dfm/routing/__init__.py +23 -0
- dfm/routing/evidence.py +111 -0
- dfm/routing/router.py +288 -0
- dfm/routing/routes.py +13 -0
- dfm/secrets/__init__.py +29 -0
- dfm/secrets/redact.py +422 -0
- dfm/security/__init__.py +1 -0
- dfm/security/checklists/cairo.toml +17 -0
- dfm/security/checklists/ci.toml +41 -0
- dfm/security/checklists/generic.toml +43 -0
- dfm/security/checklists/go.toml +41 -0
- dfm/security/checklists/infra.toml +49 -0
- dfm/security/checklists/javascript.toml +82 -0
- dfm/security/checklists/python.toml +82 -0
- dfm/security/checklists/rust.toml +26 -0
- dfm/security/checklists/solidity.toml +49 -0
- dfm/security/injection.py +34 -0
- dfm/security/sandbox.py +169 -0
- dfm/services/__init__.py +14 -0
- dfm/services/catalog.py +174 -0
- dfm/services/manager.py +361 -0
- dfm/sessions/__init__.py +29 -0
- dfm/sessions/protocol.py +183 -0
- dfm/sessions/store.py +309 -0
- dfm/tools/__init__.py +1 -0
- dfm/tools/blockchain/__init__.py +19 -0
- dfm/tools/blockchain/chains.py +188 -0
- dfm/tools/docker/__init__.py +5 -0
- dfm/tools/docker/engine.py +158 -0
- dfm/tools/errors.py +5 -0
- dfm/tools/filesystem/__init__.py +35 -0
- dfm/tools/filesystem/kinds.py +209 -0
- dfm/tools/filesystem/patching.py +261 -0
- dfm/tools/filesystem/tools.py +448 -0
- dfm/tools/generated.py +231 -0
- dfm/tools/git/__init__.py +28 -0
- dfm/tools/git/guard.py +212 -0
- dfm/tools/git/hardening.py +30 -0
- dfm/tools/git/state.py +83 -0
- dfm/tools/git/tools.py +277 -0
- dfm/tools/git/worktree.py +120 -0
- dfm/tools/github/__init__.py +1 -0
- dfm/tools/kubernetes/__init__.py +5 -0
- dfm/tools/kubernetes/context.py +72 -0
- dfm/tools/languages/__init__.py +53 -0
- dfm/tools/languages/base.py +131 -0
- dfm/tools/languages/cairo.py +146 -0
- dfm/tools/languages/go.py +155 -0
- dfm/tools/languages/python.py +237 -0
- dfm/tools/languages/rust.py +157 -0
- dfm/tools/languages/solidity.py +156 -0
- dfm/tools/languages/swift.py +148 -0
- dfm/tools/languages/typescript.py +275 -0
- dfm/tools/logs.py +366 -0
- dfm/tools/packages.py +360 -0
- dfm/tools/results.py +945 -0
- dfm/tools/shell/__init__.py +1 -0
- dfm/tools/shell/classify.py +308 -0
- dfm/tools/shell/rules.py +2518 -0
- dfm/tools/shell/runner.py +170 -0
- dfm_agent-0.0.1.dist-info/METADATA +112 -0
- dfm_agent-0.0.1.dist-info/RECORD +163 -0
- dfm_agent-0.0.1.dist-info/WHEEL +4 -0
- dfm_agent-0.0.1.dist-info/entry_points.txt +2 -0
- dfm_agent-0.0.1.dist-info/licenses/LICENSE +21 -0
dfm/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""DFM — Developer Foundation Model: a local, privacy-first engineering agent."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("dfm-agent") # the distribution; the package and command are `dfm`
|
|
7
|
+
except PackageNotFoundError: # running from a source tree without an install
|
|
8
|
+
__version__ = "0.0.0+unknown"
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""`dfm benchmark`: ablation benchmarks over fixture tasks."""
|
|
2
|
+
|
|
3
|
+
from dfm.benchmark.runner import (
|
|
4
|
+
ABLATIONS,
|
|
5
|
+
ReferenceCoder,
|
|
6
|
+
RunResult,
|
|
7
|
+
run_suite,
|
|
8
|
+
run_task,
|
|
9
|
+
table,
|
|
10
|
+
write_results,
|
|
11
|
+
)
|
|
12
|
+
from dfm.benchmark.tasks import BY_ID, TASKS, BenchTask
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ABLATIONS",
|
|
16
|
+
"BY_ID",
|
|
17
|
+
"TASKS",
|
|
18
|
+
"BenchTask",
|
|
19
|
+
"ReferenceCoder",
|
|
20
|
+
"RunResult",
|
|
21
|
+
"run_suite",
|
|
22
|
+
"run_task",
|
|
23
|
+
"table",
|
|
24
|
+
"write_results",
|
|
25
|
+
]
|
dfm/benchmark/runner.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""Run benchmark tasks under ablations and report.
|
|
2
|
+
|
|
3
|
+
for each task, ablation and repeat:
|
|
4
|
+
fresh git copy of the fixture → its check must FAIL first (else the task is invalid)
|
|
5
|
+
→ DFM runs offline with the ablation's integrations switched off
|
|
6
|
+
→ the task's own check runs again, sandboxed (the model's edits are untrusted code)
|
|
7
|
+
→ score: success = check passes; unnecessary edits = changed lines outside the
|
|
8
|
+
expected files; plus DFM's session metrics (latency, tokens, context, memory, tools)
|
|
9
|
+
|
|
10
|
+
Results are JSON plus a markdown comparison table. Nothing uses the network.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import time
|
|
21
|
+
from collections.abc import AsyncIterator, Callable, Mapping, Sequence
|
|
22
|
+
from dataclasses import asdict, dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, cast
|
|
25
|
+
|
|
26
|
+
from dfm.benchmark.tasks import TASKS, BenchTask
|
|
27
|
+
from dfm.config import ConfigOverrides, DFMConfig, state_dirs
|
|
28
|
+
from dfm.execution import ExecutionEnvironment
|
|
29
|
+
from dfm.models.base import GenerateRequest, GenerateResult, Model, ModelHealth
|
|
30
|
+
from dfm.observability import load
|
|
31
|
+
from dfm.orchestration.ci import ci
|
|
32
|
+
from dfm.orchestration.implement import ImplementOptions, implement
|
|
33
|
+
from dfm.orchestration.runtime import Runtime
|
|
34
|
+
from dfm.policy import Policy
|
|
35
|
+
from dfm.processes import ProcessManager, ProcessRegistry
|
|
36
|
+
from dfm.security.sandbox import Sandbox
|
|
37
|
+
from dfm.tools.errors import ToolError
|
|
38
|
+
from dfm.tools.shell.runner import CommandRunner
|
|
39
|
+
|
|
40
|
+
_OFF = {
|
|
41
|
+
"integrations.repocanon.enabled": False,
|
|
42
|
+
"integrations.repokg.enabled": False,
|
|
43
|
+
"integrations.reflex.enabled": False,
|
|
44
|
+
"models.apple.enabled": False,
|
|
45
|
+
}
|
|
46
|
+
ABLATIONS: dict[str, dict[str, Any]] = {
|
|
47
|
+
"coder": dict(_OFF),
|
|
48
|
+
"+canon": {**_OFF, "integrations.repocanon.enabled": True},
|
|
49
|
+
"+kg": {**_OFF, "integrations.repocanon.enabled": True, "integrations.repokg.enabled": True},
|
|
50
|
+
"+reflex": {
|
|
51
|
+
**_OFF,
|
|
52
|
+
"integrations.repocanon.enabled": True,
|
|
53
|
+
"integrations.repokg.enabled": True,
|
|
54
|
+
"integrations.reflex.enabled": True,
|
|
55
|
+
},
|
|
56
|
+
"+apple": {}, # every integration on (as configured), Apple FM routing included
|
|
57
|
+
"full": {}, # the developer's configuration as it is
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_NO_CODER = ("coding model failed", "no coding model")
|
|
62
|
+
_CANNOT_RUN = (
|
|
63
|
+
"command not found",
|
|
64
|
+
"could not choose a version",
|
|
65
|
+
"No such file or directory: '",
|
|
66
|
+
"check refused:",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ReferenceCoder:
|
|
71
|
+
"""Replays a task's reference solution: tests the harness and the fixtures, not a model."""
|
|
72
|
+
|
|
73
|
+
name = "reference"
|
|
74
|
+
|
|
75
|
+
def __init__(self, actions: Sequence[Mapping[str, Any]]) -> None:
|
|
76
|
+
self.actions = [dict(a) for a in actions]
|
|
77
|
+
|
|
78
|
+
async def healthcheck(self) -> ModelHealth:
|
|
79
|
+
return ModelHealth(self.name, True, "replay")
|
|
80
|
+
|
|
81
|
+
async def generate(self, request: GenerateRequest) -> GenerateResult:
|
|
82
|
+
step = self.actions.pop(0) if len(self.actions) > 1 else self.actions[0]
|
|
83
|
+
return GenerateResult(json.dumps(step), self.name, 0)
|
|
84
|
+
|
|
85
|
+
async def stream(self, request: GenerateRequest) -> AsyncIterator[str]:
|
|
86
|
+
yield ""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class RunResult:
|
|
91
|
+
task: str
|
|
92
|
+
ablation: str
|
|
93
|
+
repeat: int
|
|
94
|
+
status: str # pass | fail | skipped | invalid | error
|
|
95
|
+
detail: str = ""
|
|
96
|
+
dfm_status: str | None = None
|
|
97
|
+
changed_files: list[str] = field(default_factory=lambda: [])
|
|
98
|
+
expected_touched: bool = False
|
|
99
|
+
unnecessary_lines: int = 0
|
|
100
|
+
wall_ms: float = 0.0
|
|
101
|
+
tokens_in: int = 0
|
|
102
|
+
tokens_out: int = 0
|
|
103
|
+
context_chars: int = 0
|
|
104
|
+
memory_peak_mb: float = 0.0
|
|
105
|
+
tool_calls: int = 0
|
|
106
|
+
repairs: int = 0
|
|
107
|
+
steps: int = 0
|
|
108
|
+
models: list[str] = field(default_factory=lambda: []) # models that actually answered
|
|
109
|
+
seed: int = 0 # sampling seed: rerun a cell with the same seed to replay it
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _git(repo: Path, *args: str) -> str:
|
|
113
|
+
return subprocess.run( # noqa: S603
|
|
114
|
+
[ # noqa: S607
|
|
115
|
+
"git",
|
|
116
|
+
"-c",
|
|
117
|
+
"user.email=bench@dfm.invalid",
|
|
118
|
+
"-c",
|
|
119
|
+
"user.name=DFM bench",
|
|
120
|
+
"-c",
|
|
121
|
+
"commit.gpgsign=false",
|
|
122
|
+
*args,
|
|
123
|
+
],
|
|
124
|
+
cwd=repo,
|
|
125
|
+
capture_output=True,
|
|
126
|
+
text=True,
|
|
127
|
+
check=True,
|
|
128
|
+
timeout=60,
|
|
129
|
+
).stdout
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def run_seed(task: str, ablation: str, repeat: int) -> int:
|
|
133
|
+
"""A stable seed per cell, so the same command samples the same way."""
|
|
134
|
+
return int.from_bytes(hashlib.sha256(f"{task}/{ablation}/{repeat}".encode()).digest()[:4])
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def materialize(task: BenchTask, root: Path) -> Path:
|
|
138
|
+
repo = root / "repo"
|
|
139
|
+
if repo.exists():
|
|
140
|
+
shutil.rmtree(repo)
|
|
141
|
+
for rel, content in task.files.items():
|
|
142
|
+
path = repo / rel
|
|
143
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
path.write_text(content)
|
|
145
|
+
if task.setup is not None:
|
|
146
|
+
task.setup(repo)
|
|
147
|
+
# build outputs and lockfiles toolchains create on first run are not the model's edits
|
|
148
|
+
(repo / ".gitignore").write_text(
|
|
149
|
+
"target/\nCargo.lock\n.pytest_cache/\n__pycache__/\nnode_modules/\nout/\ncache/\n"
|
|
150
|
+
)
|
|
151
|
+
_git(repo, "init", "-q", "-b", "main")
|
|
152
|
+
_git(repo, "add", ".")
|
|
153
|
+
_git(repo, "commit", "-qm", "fixture")
|
|
154
|
+
return repo
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def run_check(repo: Path, argv: Sequence[str], scratch: Path) -> tuple[bool, str]:
|
|
158
|
+
"""The task's own check, sandboxed and offline, exactly like DFM runs tests."""
|
|
159
|
+
env = ExecutionEnvironment.create(
|
|
160
|
+
repo,
|
|
161
|
+
DFMConfig(),
|
|
162
|
+
session_id=f"check-{int(time.time() * 1000)}",
|
|
163
|
+
overrides=ConfigOverrides(offline=True),
|
|
164
|
+
)
|
|
165
|
+
runner = CommandRunner(
|
|
166
|
+
env,
|
|
167
|
+
ProcessManager(ProcessRegistry(scratch / "procs")),
|
|
168
|
+
Policy(DFMConfig()),
|
|
169
|
+
wrap=Sandbox().wrap,
|
|
170
|
+
proposed_by="benchmark",
|
|
171
|
+
)
|
|
172
|
+
try:
|
|
173
|
+
outcome = await runner.run(list(argv), timeout=900)
|
|
174
|
+
finally:
|
|
175
|
+
env.close()
|
|
176
|
+
if outcome.result is None:
|
|
177
|
+
return False, f"check refused: {outcome.verdicts[-1].reason}"
|
|
178
|
+
return outcome.result.exit_code == 0, (outcome.result.stdout + outcome.result.stderr)[-1500:]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def score_diff(repo: Path, expected: frozenset[str]) -> tuple[list[str], int, bool]:
|
|
182
|
+
"""(changed files, changed lines outside `expected`, whether an expected file changed)."""
|
|
183
|
+
_git(repo, "add", "-A", "--intent-to-add")
|
|
184
|
+
changed: list[str] = []
|
|
185
|
+
outside = 0
|
|
186
|
+
for line in _git(repo, "diff", "--numstat", "HEAD").splitlines():
|
|
187
|
+
added, removed, path = line.split("\t", 2)
|
|
188
|
+
changed.append(path)
|
|
189
|
+
if path not in expected:
|
|
190
|
+
outside += (int(added) if added.isdigit() else 0) + (
|
|
191
|
+
int(removed) if removed.isdigit() else 0
|
|
192
|
+
)
|
|
193
|
+
return changed, outside, bool(expected & set(changed))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def run_task(
|
|
197
|
+
task: BenchTask,
|
|
198
|
+
ablation: str,
|
|
199
|
+
*,
|
|
200
|
+
workdir: Path,
|
|
201
|
+
coder: Model | None = None,
|
|
202
|
+
repeat: int = 1,
|
|
203
|
+
max_steps: int = 20,
|
|
204
|
+
settings: Mapping[str, Any] | None = None,
|
|
205
|
+
) -> RunResult:
|
|
206
|
+
result = RunResult(task.id, ablation, repeat, "error")
|
|
207
|
+
missing = [tool for tool in task.needs if shutil.which(tool) is None]
|
|
208
|
+
if missing:
|
|
209
|
+
result.status, result.detail = "skipped", f"needs {', '.join(missing)}"
|
|
210
|
+
return result
|
|
211
|
+
root = workdir / f"{task.id}-{ablation.lstrip('+')}-{repeat}"
|
|
212
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
repo = materialize(task, root)
|
|
214
|
+
passed, baseline = await run_check(repo, task.check, root)
|
|
215
|
+
if passed:
|
|
216
|
+
result.status, result.detail = "invalid", "the check passes before any change"
|
|
217
|
+
return result
|
|
218
|
+
if any(sign in baseline for sign in _CANNOT_RUN):
|
|
219
|
+
# failing because the toolchain cannot run is not the task's failure: nothing to measure
|
|
220
|
+
result.status, result.detail = (
|
|
221
|
+
"skipped",
|
|
222
|
+
f"check cannot run here: {baseline.strip()[-160:]}",
|
|
223
|
+
)
|
|
224
|
+
return result
|
|
225
|
+
result.seed = run_seed(task.id, ablation, repeat)
|
|
226
|
+
overrides = ConfigOverrides(
|
|
227
|
+
offline=True,
|
|
228
|
+
settings={**(settings or {}), **ABLATIONS[ablation], "models.seed": result.seed},
|
|
229
|
+
)
|
|
230
|
+
rt = Runtime.create(
|
|
231
|
+
repo,
|
|
232
|
+
request=task.request,
|
|
233
|
+
mode="ci" if task.kind == "ci" else "implement",
|
|
234
|
+
overrides=overrides,
|
|
235
|
+
)
|
|
236
|
+
try:
|
|
237
|
+
if task.kind == "ci":
|
|
238
|
+
log = root / "ci.log"
|
|
239
|
+
log.write_text(task.ci_log or "")
|
|
240
|
+
outcome = (await ci(rt, log_path=log, coder=coder, max_steps=max_steps)).outcome
|
|
241
|
+
else:
|
|
242
|
+
outcome = (
|
|
243
|
+
await implement(
|
|
244
|
+
rt, task.request, ImplementOptions(coder=coder, max_steps=max_steps)
|
|
245
|
+
)
|
|
246
|
+
).outcome
|
|
247
|
+
except ToolError as exc:
|
|
248
|
+
await rt.close("failed", str(exc))
|
|
249
|
+
result.detail = str(exc)
|
|
250
|
+
return result
|
|
251
|
+
await rt.close(outcome.session_status, outcome.summary)
|
|
252
|
+
result.dfm_status = outcome.status
|
|
253
|
+
metrics = load(rt.dirs.data / "metrics", rt.session.id) or {}
|
|
254
|
+
result.wall_ms = float(metrics.get("wall_ms", 0.0))
|
|
255
|
+
result.tokens_in = int(metrics.get("tokens", {}).get("in", 0))
|
|
256
|
+
result.tokens_out = int(metrics.get("tokens", {}).get("out", 0))
|
|
257
|
+
result.context_chars = int(metrics.get("context", {}).get("chars", 0))
|
|
258
|
+
result.memory_peak_mb = float(metrics.get("memory_peak_mb", 0.0))
|
|
259
|
+
result.tool_calls = int(metrics.get("tool_calls", {}).get("commands", 0))
|
|
260
|
+
result.repairs = int(metrics.get("repairs", 0))
|
|
261
|
+
result.steps = int(metrics.get("steps", 0))
|
|
262
|
+
points = cast(list[dict[str, Any]], metrics.get("points", []))
|
|
263
|
+
result.models = sorted({str(p["model"]) for p in points if p.get("model")})
|
|
264
|
+
result.changed_files, result.unnecessary_lines, result.expected_touched = score_diff(
|
|
265
|
+
repo, task.expected
|
|
266
|
+
)
|
|
267
|
+
passed, tail = await run_check(repo, task.check, root)
|
|
268
|
+
model_failed = outcome.summary.startswith(_NO_CODER)
|
|
269
|
+
if not passed and not result.changed_files and model_failed and not result.models:
|
|
270
|
+
# the coder never ran (no model, not enough memory): that measures the machine, not DFM.
|
|
271
|
+
# A model that answered with unusable output did run: that is a fail.
|
|
272
|
+
result.status, result.detail = "error", outcome.summary[:300]
|
|
273
|
+
return result
|
|
274
|
+
result.status = "pass" if passed else "fail"
|
|
275
|
+
result.detail = "" if passed else (outcome.summary if model_failed else tail)[-300:]
|
|
276
|
+
return result
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def run_suite(
|
|
280
|
+
tasks: Sequence[BenchTask] = TASKS,
|
|
281
|
+
ablations: Sequence[str] = ("coder", "full"),
|
|
282
|
+
*,
|
|
283
|
+
workdir: Path | None = None,
|
|
284
|
+
coder: Model | None = None,
|
|
285
|
+
reference: bool = False,
|
|
286
|
+
repeat: int = 1,
|
|
287
|
+
max_steps: int = 20,
|
|
288
|
+
settings: Mapping[str, Any] | None = None,
|
|
289
|
+
cooldown_s: float = 0.0,
|
|
290
|
+
) -> list[RunResult]:
|
|
291
|
+
workdir = workdir or state_dirs().data / "benchmarks" / time.strftime("%Y%m%dT%H%M%S")
|
|
292
|
+
results: list[RunResult] = []
|
|
293
|
+
for task in tasks:
|
|
294
|
+
for ablation in ablations:
|
|
295
|
+
if ablation not in ABLATIONS:
|
|
296
|
+
raise ValueError(f"unknown ablation {ablation!r} (known: {', '.join(ABLATIONS)})")
|
|
297
|
+
for n in range(1, repeat + 1):
|
|
298
|
+
model = ReferenceCoder(task.reference) if reference else coder
|
|
299
|
+
results.append(
|
|
300
|
+
await run_task(
|
|
301
|
+
task,
|
|
302
|
+
ablation,
|
|
303
|
+
workdir=workdir,
|
|
304
|
+
coder=model,
|
|
305
|
+
repeat=n,
|
|
306
|
+
max_steps=max_steps,
|
|
307
|
+
settings=settings,
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
if cooldown_s > 0 and results[-1].models:
|
|
311
|
+
# back-to-back model runs keep the GPU at full load: let the laptop cool
|
|
312
|
+
await asyncio.sleep(cooldown_s)
|
|
313
|
+
return results
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def table(results: Sequence[RunResult]) -> str:
|
|
317
|
+
"""A markdown comparison: tasks by ablations, then per-ablation aggregates."""
|
|
318
|
+
ablations = list(dict.fromkeys(r.ablation for r in results))
|
|
319
|
+
tasks = list(dict.fromkeys(r.task for r in results))
|
|
320
|
+
cell: dict[tuple[str, str], list[RunResult]] = {}
|
|
321
|
+
for r in results:
|
|
322
|
+
cell.setdefault((r.task, r.ablation), []).append(r)
|
|
323
|
+
lines = ["| task | " + " | ".join(ablations) + " |", "|---|" + "---|" * len(ablations)]
|
|
324
|
+
for task in tasks:
|
|
325
|
+
row: list[str] = []
|
|
326
|
+
for ablation in ablations:
|
|
327
|
+
runs = cell.get((task, ablation), [])
|
|
328
|
+
if not runs or all(r.status == "skipped" for r in runs):
|
|
329
|
+
row.append("skip")
|
|
330
|
+
continue
|
|
331
|
+
if all(r.status in {"error", "invalid"} for r in runs):
|
|
332
|
+
row.append("err")
|
|
333
|
+
continue
|
|
334
|
+
ok = sum(r.status == "pass" for r in runs)
|
|
335
|
+
secs = sum(r.wall_ms for r in runs) / len(runs) / 1000
|
|
336
|
+
row.append(
|
|
337
|
+
f"{'✓' if ok == len(runs) else '✗' if ok == 0 else '~'} {ok}/{len(runs)} {secs:.0f}s"
|
|
338
|
+
)
|
|
339
|
+
lines.append(f"| {task} | " + " | ".join(row) + " |")
|
|
340
|
+
groups: list[list[RunResult]] = [
|
|
341
|
+
[r for r in results if r.ablation == ablation and r.status in {"pass", "fail"}]
|
|
342
|
+
for ablation in ablations
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
def summary(label: str, fn: Callable[[list[RunResult]], str]) -> str:
|
|
346
|
+
return f"| **{label}** | " + " | ".join(fn(runs) if runs else "-" for runs in groups) + " |"
|
|
347
|
+
|
|
348
|
+
lines.append(summary("success", lambda rs: f"{sum(r.status == 'pass' for r in rs)}/{len(rs)}"))
|
|
349
|
+
lines.append(
|
|
350
|
+
summary("mean wall", lambda rs: f"{sum(r.wall_ms for r in rs) / len(rs) / 1000:.1f}s")
|
|
351
|
+
)
|
|
352
|
+
lines.append(
|
|
353
|
+
summary(
|
|
354
|
+
"tokens in/out",
|
|
355
|
+
lambda rs: f"{sum(r.tokens_in for r in rs)}/{sum(r.tokens_out for r in rs)}",
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
lines.append(
|
|
359
|
+
summary("context chars", lambda rs: f"{sum(r.context_chars for r in rs) // len(rs)}")
|
|
360
|
+
)
|
|
361
|
+
lines.append(summary("unnecessary lines", lambda rs: str(sum(r.unnecessary_lines for r in rs))))
|
|
362
|
+
lines.append(summary("repairs", lambda rs: str(sum(r.repairs for r in rs))))
|
|
363
|
+
lines.append(summary("peak MB", lambda rs: f"{max(r.memory_peak_mb for r in rs):.0f}"))
|
|
364
|
+
lines.append(
|
|
365
|
+
summary("models", lambda rs: ", ".join(sorted({m for r in rs for m in r.models})) or "-")
|
|
366
|
+
)
|
|
367
|
+
return "\n".join(lines)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def write_results(
|
|
371
|
+
results: Sequence[RunResult], out: Path, meta: Mapping[str, Any] | None = None
|
|
372
|
+
) -> tuple[Path, Path]:
|
|
373
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
374
|
+
data = {"meta": dict(meta or {}), "results": [asdict(r) for r in results]}
|
|
375
|
+
json_path = out / "results.json"
|
|
376
|
+
json_path.write_text(json.dumps(data, indent=1))
|
|
377
|
+
md_path = out / "results.md"
|
|
378
|
+
md_path.write_text(table(results) + "\n")
|
|
379
|
+
return json_path, md_path
|