align-agent 0.4.3__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.
- align_agent/__init__.py +11 -0
- align_agent/_audit/common.py +58 -0
- align_agent/_audit/deepswe.py +260 -0
- align_agent/_audit/excerpts.py +203 -0
- align_agent/_audit/extract.py +798 -0
- align_agent/_audit/materialize.py +624 -0
- align_agent/_audit/score.py +325 -0
- align_agent/_audit/shellparse.py +1141 -0
- align_agent/_audit/tarsur.py +153 -0
- align_agent/_vendor/src/__init__.py +3 -0
- align_agent/_vendor/src/harness/__init__.py +13 -0
- align_agent/_vendor/src/harness/arms.py +524 -0
- align_agent/_vendor/src/harness/completion.py +104 -0
- align_agent/_vendor/src/harness/dependency_notice.py +616 -0
- align_agent/_vendor/src/harness/freshness_hook.py +131 -0
- align_agent/_vendor/src/harness/lean.py +41 -0
- align_agent/_vendor/src/harness/mini_real.py +1040 -0
- align_agent/_vendor/src/harness/miniswe_scaffold.py +661 -0
- align_agent/_vendor/src/harness/tail_diff.py +292 -0
- align_agent/_vendor/src/harness/viewport.py +220 -0
- align_agent/_vendor/src/metrics/__init__.py +38 -0
- align_agent/_vendor/src/metrics/coherence.py +354 -0
- align_agent/_vendor/src/metrics/corruption_oracles.py +139 -0
- align_agent/_vendor/src/metrics/replay.py +399 -0
- align_agent/_vendor/src/metrics/statistics.py +185 -0
- align_agent/_vendor/src/metrics/verification.py +104 -0
- align_agent/_vendor/src/parsers/__init__.py +33 -0
- align_agent/_vendor/src/parsers/base_parser.py +349 -0
- align_agent/_vendor/src/parsers/file_events.py +806 -0
- align_agent/_vendor/src/parsers/miniswe_parser.py +96 -0
- align_agent/_vendor/src/parsers/openhands_parser.py +414 -0
- align_agent/_vendor/src/parsers/sweagent_parser.py +180 -0
- align_agent/_vendor/src/parsers/terminus_parser.py +106 -0
- align_agent/cli.py +101 -0
- align_agent/core.py +161 -0
- align_agent/run.py +84 -0
- align_agent/sessions.py +985 -0
- align_agent/writethrough/__init__.py +4 -0
- align_agent/writethrough/_lib/__init__.py +1 -0
- align_agent/writethrough/_lib/wt_src/__init__.py +3 -0
- align_agent/writethrough/_lib/wt_src/harness/__init__.py +1 -0
- align_agent/writethrough/_lib/wt_src/harness/dependency_notice.py +616 -0
- align_agent/writethrough/_lib/wt_src/harness/lean.py +41 -0
- align_agent/writethrough/_lib/wt_src/harness/tail_diff.py +292 -0
- align_agent/writethrough/_lib/wt_src/harness/viewport.py +220 -0
- align_agent/writethrough/_lib/wt_src/metrics/__init__.py +38 -0
- align_agent/writethrough/_lib/wt_src/metrics/replay.py +399 -0
- align_agent/writethrough/_lib/wt_src/parsers/__init__.py +1 -0
- align_agent/writethrough/_lib/wt_src/parsers/file_events.py +806 -0
- align_agent/writethrough/claude_code.py +81 -0
- align_agent/writethrough/engine.py +647 -0
- align_agent/writethrough/hooks.py +227 -0
- align_agent/writethrough/mcp_server.py +125 -0
- align_agent/writethrough/protocol.py +68 -0
- align_agent-0.4.3.dist-info/METADATA +224 -0
- align_agent-0.4.3.dist-info/RECORD +60 -0
- align_agent-0.4.3.dist-info/WHEEL +5 -0
- align_agent-0.4.3.dist-info/entry_points.txt +6 -0
- align_agent-0.4.3.dist-info/licenses/LICENSE +21 -0
- align_agent-0.4.3.dist-info/top_level.txt +1 -0
align_agent/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""ALIGN: find edits a coding agent composed against a stale view of a file.
|
|
2
|
+
|
|
3
|
+
`align-agent analyze` replays agent trajectories (mini-SWE-agent, SWE-agent,
|
|
4
|
+
OpenHands, Terminus 2, Harbor/ATIF, OpenAI-style tool calls) and the session
|
|
5
|
+
logs of Claude Code and Codex CLI, and reports every anchored edit made to a
|
|
6
|
+
file whose last view the agent's own writes had outdated, with the step that
|
|
7
|
+
held the view, how the view was delivered, and whether the edit went wrong
|
|
8
|
+
because of it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.4.3"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Shared read-only helpers for the RQ1 audit. Usage: imported by the other scripts/audit/*.py files.
|
|
2
|
+
|
|
3
|
+
Reads the manifest from the worktree and raw trajectories straight from the
|
|
4
|
+
shared cache. Never downloads, never writes into the cache.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
12
|
+
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
|
13
|
+
MANIFEST = os.path.join(ROOT, "data", "manifests", "bench_manifest.verified.jsonl")
|
|
14
|
+
# The extracted CodeTraceBench trajectories (one JSON per traj_id) are gitignored;
|
|
15
|
+
# point RQ1_CACHE at them, or place them under data/cache/extracted.
|
|
16
|
+
CACHE = os.environ.get("RQ1_CACHE", os.path.join(ROOT, "data", "cache", "extracted"))
|
|
17
|
+
OUT = os.path.join(ROOT, "docs", "audit", "rq1")
|
|
18
|
+
WORK = os.path.join(OUT, "work") # intermediate machine-readable outputs
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_manifest(path: str = MANIFEST) -> List[Dict[str, Any]]:
|
|
22
|
+
with open(path, encoding="utf-8") as f:
|
|
23
|
+
return [json.loads(line) for line in f if line.strip()]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def cache_path(traj_id: str) -> str:
|
|
27
|
+
return os.path.join(CACHE, f"{traj_id}.json")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_raw(traj_id: str) -> Optional[Any]:
|
|
31
|
+
"""Raw trajectory from the cache, or None when the cache has no file."""
|
|
32
|
+
p = cache_path(traj_id)
|
|
33
|
+
if not os.path.exists(p):
|
|
34
|
+
return None
|
|
35
|
+
with open(p, encoding="utf-8") as f:
|
|
36
|
+
return json.load(f)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def annotated_ids(item: Dict[str, Any]):
|
|
40
|
+
inc, unu = set(), set()
|
|
41
|
+
for st in item.get("incorrect_stages") or []:
|
|
42
|
+
inc.update(st.get("incorrect_step_ids") or [])
|
|
43
|
+
unu.update(st.get("unuseful_step_ids") or [])
|
|
44
|
+
return inc, unu
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def short_model(m: str) -> str:
|
|
48
|
+
return {"Anthropic/Claude-Sonnet-4-20250514-Thinking": "Claude-Sonnet-4",
|
|
49
|
+
"DeepSeek/DeepSeek-V3.2": "DeepSeek-V3.2",
|
|
50
|
+
"OpenAI/GPT-5": "GPT-5"}.get(m, m)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def dump(obj: Any, name: str) -> str:
|
|
54
|
+
os.makedirs(WORK, exist_ok=True)
|
|
55
|
+
p = os.path.join(WORK, name)
|
|
56
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
57
|
+
json.dump(obj, f, indent=1, default=str)
|
|
58
|
+
return p
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Recount on the DeepSWE leaderboard trajectories (mini-swe-agent, converted to ATIF). Usage: python3 scripts/audit/deepswe.py [--limit N]
|
|
2
|
+
|
|
3
|
+
DeepSWE (deepswe.datacurve.ai) is a long-horizon benchmark: 113 original tasks
|
|
4
|
+
in 91 repositories, reference solutions of 668 added lines over 7 files on
|
|
5
|
+
average, graded by hand-written verifiers. Its leaderboard dataset
|
|
6
|
+
(datacurve/deep-swe-leaderboard, CC-BY-4.0) holds every official trial run
|
|
7
|
+
with mini-swe-agent, stored as ATIF v1.7 (Harbor's trajectory format).
|
|
8
|
+
|
|
9
|
+
Adapter, as scripts/audit/tarsur.py: each agent step's bash tool call is one
|
|
10
|
+
step; its observation is the JSON mini-swe-agent returns ({returncode,
|
|
11
|
+
output} or, when elided, {returncode, output_head, output_tail}). Commands are
|
|
12
|
+
parsed with scripts/audit/shellparse.py, outcomes with extract.shell_outcome,
|
|
13
|
+
scored with score.Replay and classified with materialize.classify (automated
|
|
14
|
+
classifier only). Reward comes from the trial's verifier/reward.txt.
|
|
15
|
+
|
|
16
|
+
Reads data/deepswe/leaderboard/raw/jobs/*/*/; writes work/deepswe.json and
|
|
17
|
+
docs/audit/rq1/deepswe.md.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import glob
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
from collections import Counter, defaultdict
|
|
27
|
+
|
|
28
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
29
|
+
sys.path.insert(0, HERE)
|
|
30
|
+
from common import OUT, ROOT, WORK, dump # noqa: E402
|
|
31
|
+
import shellparse as sp # noqa: E402
|
|
32
|
+
from extract import shell_outcome, canonicalize, tok # noqa: E402
|
|
33
|
+
from score import Replay, cp_ci # noqa: E402
|
|
34
|
+
from materialize import classify, describe # noqa: E402
|
|
35
|
+
|
|
36
|
+
BASE = os.path.join(ROOT, "data", "deepswe", "leaderboard", "raw", "jobs")
|
|
37
|
+
TASKS = os.path.join(ROOT, "data", "deepswe", "tasks", "tasks")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def observation(step, k):
|
|
41
|
+
"""(text, return code, elided) of the k-th tool result of an ATIF step."""
|
|
42
|
+
results = (step.get("observation") or {}).get("results") or []
|
|
43
|
+
if k >= len(results):
|
|
44
|
+
return "", None, False
|
|
45
|
+
raw = results[k].get("content")
|
|
46
|
+
raw = raw if isinstance(raw, str) else json.dumps(raw)
|
|
47
|
+
try:
|
|
48
|
+
d = json.loads(raw)
|
|
49
|
+
except ValueError:
|
|
50
|
+
return raw, None, False
|
|
51
|
+
if not isinstance(d, dict):
|
|
52
|
+
return raw, None, False
|
|
53
|
+
rc = d.get("returncode")
|
|
54
|
+
if "output" in d:
|
|
55
|
+
return str(d.get("output") or ""), rc, False
|
|
56
|
+
head, tail = str(d.get("output_head") or ""), str(d.get("output_tail") or "")
|
|
57
|
+
return head + "\n[... output elided ...]\n" + tail, rc, True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def adapt(d, cwd="/testbed"):
|
|
61
|
+
steps, known, cov = [], {}, Counter()
|
|
62
|
+
for s in d.get("steps") or []:
|
|
63
|
+
if s.get("source") != "agent":
|
|
64
|
+
continue
|
|
65
|
+
tcs = s.get("tool_calls") or []
|
|
66
|
+
if not tcs:
|
|
67
|
+
cov["agent_step_without_tool_call"] += 1
|
|
68
|
+
continue
|
|
69
|
+
for k, tc in enumerate(tcs):
|
|
70
|
+
cov["tool_calls"] += 1
|
|
71
|
+
fn = tc.get("function_name")
|
|
72
|
+
args = tc.get("arguments") or {}
|
|
73
|
+
if isinstance(args, str):
|
|
74
|
+
try:
|
|
75
|
+
args = json.loads(args)
|
|
76
|
+
except ValueError:
|
|
77
|
+
args = {}
|
|
78
|
+
cmd = args.get("command", "") if fn == "bash" else ""
|
|
79
|
+
cov[f"fn:{fn}"] += 1
|
|
80
|
+
obs, rc, elided = observation(s, k)
|
|
81
|
+
cov["elided"] += elided
|
|
82
|
+
ops, _, info = sp.extract(cmd, cwd, known, persist_cd=False) if cmd else ([], cwd, {})
|
|
83
|
+
cov["unparsed"] += bool(info.get("unparsed"))
|
|
84
|
+
if ops:
|
|
85
|
+
shell_outcome(ops, obs, rc, cmd, truncated=elided)
|
|
86
|
+
steps.append({"i": len(steps), "unit": len(steps), "conv": 0, "bench": None, "tool": "shell",
|
|
87
|
+
"action": cmd, "thought": str(s.get("message") or "")[-1500:], "obs": obs, "rc": rc,
|
|
88
|
+
"cwd": cwd, "ran": True, "ops": ops, "tok_a": tok(cmd), "tok_o": tok(obs)})
|
|
89
|
+
return steps, cov
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _result(trial_dir):
|
|
93
|
+
try:
|
|
94
|
+
return json.load(open(os.path.join(trial_dir, "result.json")))
|
|
95
|
+
except (OSError, ValueError):
|
|
96
|
+
return {}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def trial_model(d, trial_dir):
|
|
100
|
+
"""Model and reasoning effort, as the leaderboard labels a configuration."""
|
|
101
|
+
model = ((d.get("agent") or {}).get("model_name") or "unknown").split("/")[-1]
|
|
102
|
+
cfg = (_result(trial_dir).get("config") or {}).get("agent") or {}
|
|
103
|
+
kw = cfg.get("kwargs") or {}
|
|
104
|
+
effort = kw.get("reasoning_effort") or kw.get("effort") or (kw.get("model_kwargs") or {}).get("reasoning_effort")
|
|
105
|
+
return f"{model} [{effort}]" if effort else model
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def reward(trial_dir):
|
|
109
|
+
"""The verifier's binary reward (result.json, else verifier/reward.txt); None if the trial has none."""
|
|
110
|
+
rw = ((_result(trial_dir).get("verifier_result") or {}).get("rewards") or {}).get("reward")
|
|
111
|
+
if rw is not None:
|
|
112
|
+
return float(rw) >= 1.0
|
|
113
|
+
try:
|
|
114
|
+
return open(os.path.join(trial_dir, "verifier", "reward.txt")).read().strip() == "1"
|
|
115
|
+
except OSError:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
DEV_CSV = os.path.join(OUT, "deepswe_m3_handcheck.csv")
|
|
120
|
+
HELDOUT_CSV = os.path.join(OUT, "deepswe_m3_heldout.csv")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def handcheck_sections():
|
|
124
|
+
"""The hand checks of the automated label, rendered from their CSVs."""
|
|
125
|
+
import csv
|
|
126
|
+
L = []
|
|
127
|
+
if os.path.exists(DEV_CSV):
|
|
128
|
+
rows = list(csv.DictReader(open(DEV_CSV)))
|
|
129
|
+
c = Counter(r["label"] for r in rows)
|
|
130
|
+
L += ["", f"## Development check of the automated \"materialized\" label ({len(rows)} cases, classifier v1)", "",
|
|
131
|
+
"`docs/audit/rq1/deepswe_m3_handcheck.csv`: a seeded sample of the first classifier's 277 labels, one reader. "
|
|
132
|
+
"Its spurious cases (different text sharing a line, a re-insert after a restore, prepends at line 1, inserts "
|
|
133
|
+
"where a block was deleted, runs of sequential inserts) became the fixtures in `tests/test_materialize.py`, "
|
|
134
|
+
"and the classifier was revised against them. Its precision is therefore not an estimate for the revised "
|
|
135
|
+
"classifier; the held-out check below is.", "",
|
|
136
|
+
"| Label | Cases |", "|---|---|"] + [f"| {k} | {c[k]} |" for k in ("real", "plausible", "ambiguous", "spurious")]
|
|
137
|
+
hj = os.path.join(WORK, "deepswe_heldout.json")
|
|
138
|
+
if os.path.exists(hj):
|
|
139
|
+
h = json.load(open(hj))
|
|
140
|
+
a, o, e = h["auto_m3"], h["auto_other"], h.get("estimate", {})
|
|
141
|
+
pc = lambda x: f"{100 * x:.0f}%" # noqa: E731
|
|
142
|
+
L += ["", "## Held-out check (classifier v2.1, frozen before any held-out label was read)", "",
|
|
143
|
+
"`docs/audit/rq1/deepswe_m3_heldout.csv`, drawn by `scripts/audit/deepswe_heldout.py` (seed 20261001) from trials the "
|
|
144
|
+
"development check did not touch: 60 edits the classifier labelled materialized, round-robin by model, and 40 line-anchored "
|
|
145
|
+
"or inserting stale edits it did not. Each was labelled blind to stratum and class from a census-format excerpt with the "
|
|
146
|
+
f"audit's rubric by the census labeller (a language model). "
|
|
147
|
+
+ (f"A second, independent run of blind model labellers on the same packets agreed on {h['agree_census_second']} of "
|
|
148
|
+
f"{h['pairs_census_second']} (coarse kappa {h['kappa_census_second_coarse']}); the {h.get('disagreements_undecided', 0)} items they "
|
|
149
|
+
f"disagree on count as undecided. " if h.get("pairs_census_second") else "")
|
|
150
|
+
+ f"Human labels: {'none yet' if h.get('kappa_census_human_coarse') is None else 'kappa ' + str(h['kappa_census_human_coarse'])}. "
|
|
151
|
+
f"{h.get('drawn_m3_no_longer_m3', 0)} edits drawn as materialized are not materialized under the frozen classifier and are left out.", "",
|
|
152
|
+
"| Stratum | Labelled | Materialized | Undecided | Not | Rate (95% CI) |", "|---|---|---|---|---|---|",
|
|
153
|
+
f"| flagged materialized | {a['n']} | {a['counts'].get('MATERIALIZED', 0)} | {a['counts'].get('INDETERMINATE', 0)} | {a['counts'].get('NOT', 0)} | "
|
|
154
|
+
f"{pc(a['rate_low'])} ({pc(a['ci_low'][0])}–{pc(a['ci_low'][1])}) |",
|
|
155
|
+
f"| not flagged (line-anchored or insert) | {o['n']} | {o['counts'].get('MATERIALIZED', 0)} | {o['counts'].get('INDETERMINATE', 0)} | {o['counts'].get('NOT', 0)} | "
|
|
156
|
+
f"{pc(o['rate_low'])} ({pc(o['ci_low'][0])}–{pc(o['ci_low'][1])}) |", ""]
|
|
157
|
+
if e:
|
|
158
|
+
L += [f"Edits that went wrong in the corpus: {e['flagged']} flagged x precision + {e['pool']} unflagged line-anchored or inserting "
|
|
159
|
+
f"stale edits x miss rate = {e['went_wrong']:.0f} of {e['opportunities']:,} anchored edits "
|
|
160
|
+
f"({100 * e['went_wrong'] / e['opportunities']:.2f}%; {100 * e['went_wrong_low'] / e['opportunities']:.2f}% to "
|
|
161
|
+
f"{100 * e['went_wrong_high'] / e['opportunities']:.2f}% from the intervals)."]
|
|
162
|
+
return L
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def main():
|
|
166
|
+
ap = argparse.ArgumentParser()
|
|
167
|
+
ap.add_argument("--limit", type=int, default=0)
|
|
168
|
+
args = ap.parse_args()
|
|
169
|
+
files = sorted(glob.glob(os.path.join(BASE, "*", "*", "agent", "trajectory.json")))
|
|
170
|
+
if args.limit:
|
|
171
|
+
files = files[:args.limit]
|
|
172
|
+
per_model, classes = defaultdict(Counter), defaultdict(Counter)
|
|
173
|
+
resolved = defaultdict(lambda: defaultdict(list))
|
|
174
|
+
cov_all = defaultdict(Counter)
|
|
175
|
+
hazards_out = []
|
|
176
|
+
for n, f in enumerate(files, 1):
|
|
177
|
+
trial_dir = os.path.dirname(os.path.dirname(f))
|
|
178
|
+
try:
|
|
179
|
+
d = json.load(open(f))
|
|
180
|
+
except ValueError:
|
|
181
|
+
continue
|
|
182
|
+
model = trial_model(d, trial_dir)
|
|
183
|
+
steps, cov = adapt(d)
|
|
184
|
+
cov_all[model].update(cov)
|
|
185
|
+
t = {"traj_id": os.path.basename(trial_dir), "agent": "mini-SWE-agent (DeepSWE)", "model": model, "steps": steps,
|
|
186
|
+
"final_diff": None}
|
|
187
|
+
t["root"] = canonicalize(steps)
|
|
188
|
+
r = Replay(t).run()
|
|
189
|
+
c = per_model[model]
|
|
190
|
+
c["trajectories"] += 1
|
|
191
|
+
c["steps"] += len(steps)
|
|
192
|
+
c["zero_op_steps"] += sum(1 for s in steps if not s["ops"])
|
|
193
|
+
c["anchored_writes"] += sum(1 for s in steps for o in s["ops"] if o["op"] == "W" and o["kind"] == "anchored")
|
|
194
|
+
c["opportunities"] += len(r.opps)
|
|
195
|
+
c["hazards"] += len(r.hazards)
|
|
196
|
+
c["hazard_trajectories"] += bool(r.hazards)
|
|
197
|
+
for h in r.hazards:
|
|
198
|
+
cls, why, ev = classify(h, t)
|
|
199
|
+
classes[model][cls] += 1
|
|
200
|
+
e = steps[h["step"]]["ops"][h["op"]]
|
|
201
|
+
de = describe(e, steps[h["step"]].get("action"))
|
|
202
|
+
inserts = any(c.get("cmd") in ("a", "i") for c in ((e.get("anchor") or {}).get("cmds") or [])) or \
|
|
203
|
+
(e.get("anchor") or {}).get("type") == "insert" or e.get("kind") == "append"
|
|
204
|
+
hazards_out.append({**{k: h.get(k) for k in ("step", "op", "path", "mech", "last_view")}, "traj_id": t["traj_id"],
|
|
205
|
+
"job": os.path.basename(os.path.dirname(trial_dir)), "model": model, "class": cls, "why": why,
|
|
206
|
+
"line_anchored": bool(de["line_anchors"]), "inserts": inserts, "task": t["traj_id"].split("__")[0]})
|
|
207
|
+
res = reward(trial_dir)
|
|
208
|
+
c["scored"] += res is not None
|
|
209
|
+
c["resolved"] += bool(res)
|
|
210
|
+
if res is not None:
|
|
211
|
+
resolved[model]["haz" if r.hazards else "nohaz"].append(res)
|
|
212
|
+
if n % 500 == 0:
|
|
213
|
+
print(f"{n}/{len(files)}", flush=True)
|
|
214
|
+
out, tot, allcls = {}, Counter(), Counter()
|
|
215
|
+
for model, c in sorted(per_model.items()):
|
|
216
|
+
tot.update(c)
|
|
217
|
+
allcls.update(classes[model])
|
|
218
|
+
rv = resolved[model]
|
|
219
|
+
out[model] = dict(c, rate=round(c["hazards"] / c["opportunities"], 4) if c["opportunities"] else None,
|
|
220
|
+
ci=cp_ci(c["hazards"], c["opportunities"]), classes=dict(classes[model]),
|
|
221
|
+
resolved_with_hazard=[sum(rv["haz"]), len(rv["haz"])],
|
|
222
|
+
resolved_without_hazard=[sum(rv["nohaz"]), len(rv["nohaz"])], coverage=dict(cov_all[model]))
|
|
223
|
+
out["ALL"] = dict(tot, rate=round(tot["hazards"] / tot["opportunities"], 4) if tot["opportunities"] else None,
|
|
224
|
+
ci=cp_ci(tot["hazards"], tot["opportunities"]), classes=dict(allcls))
|
|
225
|
+
dump(out, "deepswe.json")
|
|
226
|
+
dump(hazards_out, "deepswe_hazards.json")
|
|
227
|
+
L = ["# File-level stale edits on DeepSWE (published mini-swe-agent trials)", "",
|
|
228
|
+
"Generated by `scripts/audit/deepswe.py`, the RQ1 audit's recount applied to the DeepSWE leaderboard trajectories "
|
|
229
|
+
"(datacurve/deep-swe-leaderboard). Classes are the automated classifier's; no human labels exist for this corpus.", "",
|
|
230
|
+
"| Model | Trials | Mean steps | Opportunities | Stale edits | SAR | 95% CI | Trials with one | Resolved, with / without a stale edit | Automated classes |",
|
|
231
|
+
"|---|---|---|---|---|---|---|---|---|---|"]
|
|
232
|
+
for model, o in sorted(out.items(), key=lambda kv: (kv[0] == "ALL", kv[0])):
|
|
233
|
+
if model == "ALL":
|
|
234
|
+
continue
|
|
235
|
+
ci = o["ci"]
|
|
236
|
+
L.append(f"| {model} | {o['trajectories']} | {o['steps'] // max(1, o['trajectories'])} | {o['opportunities']} | {o['hazards']} | "
|
|
237
|
+
f"{100 * (o['rate'] or 0):.1f}% | {100 * ci[0]:.1f}–{100 * ci[1]:.1f}% | {o['hazard_trajectories']} | "
|
|
238
|
+
f"{o['resolved_with_hazard'][0]}/{o['resolved_with_hazard'][1]} / {o['resolved_without_hazard'][0]}/{o['resolved_without_hazard'][1]} | "
|
|
239
|
+
f"{', '.join(f'{k} {v}' for k, v in sorted(o['classes'].items(), key=lambda kv: -kv[1]))} |")
|
|
240
|
+
a = out["ALL"]
|
|
241
|
+
L.append(f"| **All** | {a['trajectories']} | {a['steps'] // max(1, a['trajectories'])} | {a['opportunities']} | {a['hazards']} | "
|
|
242
|
+
f"{100 * (a['rate'] or 0):.1f}% | | {a['hazard_trajectories']} | | {', '.join(f'{k} {v}' for k, v in sorted(a['classes'].items(), key=lambda kv: -kv[1]))} |")
|
|
243
|
+
L += handcheck_sections()
|
|
244
|
+
open(os.path.join(OUT, "deepswe.md"), "w").write("\n".join(L) + "\n")
|
|
245
|
+
print("\n".join(L))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def rewrite_handcheck():
|
|
249
|
+
"""Replace the hand-check sections of deepswe.md without recounting the corpus."""
|
|
250
|
+
path = os.path.join(OUT, "deepswe.md")
|
|
251
|
+
text = open(path).read()
|
|
252
|
+
cut = text.find("\n## Development check")
|
|
253
|
+
if cut < 0:
|
|
254
|
+
cut = text.find("\n## Held-out check")
|
|
255
|
+
head = text[:cut].rstrip("\n").split("\n") if cut >= 0 else text.rstrip("\n").split("\n")
|
|
256
|
+
open(path, "w").write("\n".join(head + handcheck_sections()) + "\n")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if __name__ == "__main__":
|
|
260
|
+
rewrite_handcheck() if sys.argv[1:] == ["--handcheck-only"] else main()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Build blind trajectory excerpts for hazard labeling. Usage: python3 scripts/audit/excerpts.py [--show ITEM_ID] [--write]
|
|
2
|
+
|
|
3
|
+
For every structural hazard in docs/audit/rq1/work/hazards.jsonl (and every
|
|
4
|
+
intra-unit repeated edit in intra_unit.jsonl, marked as such), writes a
|
|
5
|
+
plain-text excerpt: the last view of the file before the edit, every write to
|
|
6
|
+
the file between that view and the edit, the edit itself with the agent's
|
|
7
|
+
stated reasoning, the next five steps, and the final diff when the trajectory
|
|
8
|
+
has one. Outputs are trimmed around the anchor text.
|
|
9
|
+
|
|
10
|
+
Blindness: excerpts never include the manifest's annotations, `solved`,
|
|
11
|
+
OpenHands metadata (is_resolved, parser_results, test results), or any
|
|
12
|
+
automated classification. Items are identified by an opaque id (H###/I###)
|
|
13
|
+
whose mapping to trajectories is kept in work/excerpt_index.json.
|
|
14
|
+
With --write, excerpts go to docs/audit/rq1/census/items/<id>.txt.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
25
|
+
sys.path.insert(0, HERE)
|
|
26
|
+
from common import OUT, WORK # noqa: E402
|
|
27
|
+
|
|
28
|
+
CENSUS = os.path.join(OUT, "census")
|
|
29
|
+
MAXOBS = 1800
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_trajs(ids):
|
|
33
|
+
out = {}
|
|
34
|
+
with open(os.path.join(WORK, "extracted.jsonl"), encoding="utf-8") as f:
|
|
35
|
+
for line in f:
|
|
36
|
+
tid = line[len('{"traj_id": "'):line.index('"', len('{"traj_id": "'))]
|
|
37
|
+
if tid in ids:
|
|
38
|
+
out[tid] = json.loads(line)
|
|
39
|
+
return out
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def anchor_hint(op):
|
|
43
|
+
a = op.get("anchor") or {}
|
|
44
|
+
t = a.get("type")
|
|
45
|
+
if t == "str_replace":
|
|
46
|
+
return a.get("old") or ""
|
|
47
|
+
if t == "py":
|
|
48
|
+
vals = [r.get("old") for r in a.get("replaces") or [] if r.get("old")] + [r.get("pat") for r in a.get("regex") or []]
|
|
49
|
+
vals += a.get("finds") or []
|
|
50
|
+
return max(vals, key=len) if vals else ""
|
|
51
|
+
if t in ("sed", "perl"):
|
|
52
|
+
pats = [c.get("pat") or "" for c in a.get("cmds") or []] + [x.get("pat") or "" for x in a.get("subs") or []]
|
|
53
|
+
pats += [ad.get("re", "") for c in a.get("cmds") or [] for ad in (c.get("addr") or []) if isinstance(ad, dict) and ad.get("re")]
|
|
54
|
+
return max(pats, key=len) if pats else ""
|
|
55
|
+
if t == "patch":
|
|
56
|
+
lines = [ln[1:] for h in a.get("hunks") or [] for ln in h.get("lines", []) if ln[:1] in ("-", " ")]
|
|
57
|
+
return "\n".join(lines[:6])
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def trim(text, keys, limit=MAXOBS):
|
|
62
|
+
"""Keep the part of text around the first occurrence of any key."""
|
|
63
|
+
text = text or ""
|
|
64
|
+
if len(text) <= limit:
|
|
65
|
+
return text
|
|
66
|
+
for k in keys:
|
|
67
|
+
for frag in [k] + [x for x in re.split(r"\\[.*+?^$()\[\]{}|]|\n", k or "") if len(x) >= 6]:
|
|
68
|
+
if frag and frag in text:
|
|
69
|
+
i = text.index(frag)
|
|
70
|
+
lo = max(0, i - limit // 2)
|
|
71
|
+
return ("[...]" if lo else "") + text[lo:lo + limit] + ("[...]" if lo + limit < len(text) else "")
|
|
72
|
+
return text[:limit // 2] + "\n[... output trimmed ...]\n" + text[-limit // 2:]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def episode_obs(t, s):
|
|
76
|
+
obs = s.get("obs") or ""
|
|
77
|
+
if t["agent"] == "Terminus2" and not obs:
|
|
78
|
+
same = [x for x in t["steps"] if x.get("episode") == s.get("episode") and x.get("obs")]
|
|
79
|
+
if same:
|
|
80
|
+
obs = same[-1]["obs"]
|
|
81
|
+
return obs
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def step_block(t, s, keys, label, show_thought=False, cmd_lim=900, obs_lim=900, printed_eps=None):
|
|
85
|
+
head = f"--- {label}: step {s['i']}" + (f" (episode {s['episode']})" if s.get("episode") is not None else "")
|
|
86
|
+
out = [head]
|
|
87
|
+
if show_thought and (s.get("thought") or "").strip():
|
|
88
|
+
out.append("AGENT REASONING (excerpt):\n" + trim(s["thought"], keys, 600))
|
|
89
|
+
out.append("COMMAND:\n" + trim(s.get("action") or "", keys, cmd_lim))
|
|
90
|
+
obs = episode_obs(t, s)
|
|
91
|
+
if t["agent"] == "Terminus2" and printed_eps is not None:
|
|
92
|
+
if s.get("episode") in printed_eps:
|
|
93
|
+
out.append("OUTPUT: (terminal output for this episode shown above)")
|
|
94
|
+
return "\n".join(out)
|
|
95
|
+
printed_eps.add(s.get("episode"))
|
|
96
|
+
obs = "(terminal screen after the whole episode)\n" + obs if obs else obs
|
|
97
|
+
out.append("OUTPUT:\n" + (trim(obs, keys, obs_lim) if obs.strip() else "(no output)"))
|
|
98
|
+
return "\n".join(out)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def final_diff_for(t, path):
|
|
102
|
+
d = t.get("final_diff") or ""
|
|
103
|
+
if not d:
|
|
104
|
+
return None
|
|
105
|
+
base = path.rsplit("/", 1)[-1]
|
|
106
|
+
parts = re.split(r"(?m)^(?=diff --git )", d)
|
|
107
|
+
keep = [p for p in parts if base in p.split("\n", 1)[0]]
|
|
108
|
+
return "\n".join(keep)[:3000] if keep else "(final diff does not touch this file)"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def build(item, t):
|
|
112
|
+
s_e = t["steps"][item["step"]]
|
|
113
|
+
e_op = s_e["ops"][item["op"]]
|
|
114
|
+
keys = [anchor_hint(e_op)]
|
|
115
|
+
eps = set()
|
|
116
|
+
parts = [f"ITEM {item['id']}", f"Framework: {t['agent']}", f"File: {item['path']}",
|
|
117
|
+
f"Edit mechanism: {e_op.get('mech')}"]
|
|
118
|
+
if item.get("kind_of_item") == "intra_unit":
|
|
119
|
+
parts.append("Note: the edit under review was issued in the same action as an earlier write to this file.")
|
|
120
|
+
lv = item.get("last_view")
|
|
121
|
+
shown = set()
|
|
122
|
+
if lv:
|
|
123
|
+
s_v = t["steps"][lv[0]]
|
|
124
|
+
parts.append(step_block(t, s_v, keys, "LAST VIEW OF THE FILE BEFORE THE EDIT", cmd_lim=500, obs_lim=1500, printed_eps=eps))
|
|
125
|
+
shown.add(s_v["i"])
|
|
126
|
+
else:
|
|
127
|
+
parts.append("--- LAST VIEW: none on record before the edit")
|
|
128
|
+
ws = item.get("W") or item.get("prior_same_unit_writes") or []
|
|
129
|
+
for w in ws[-4:]:
|
|
130
|
+
s_w = t["steps"][w[0]]
|
|
131
|
+
if s_w["i"] in shown or s_w["i"] == s_e["i"]:
|
|
132
|
+
continue
|
|
133
|
+
shown.add(s_w["i"])
|
|
134
|
+
parts.append(step_block(t, s_w, keys, "WRITE TO THE FILE AFTER THAT VIEW", cmd_lim=900, obs_lim=400, printed_eps=eps))
|
|
135
|
+
if len(ws) > 4:
|
|
136
|
+
parts.append(f"({len(ws) - 4} earlier writes to the file after the view are not shown)")
|
|
137
|
+
parts.append(step_block(t, s_e, keys, "THE EDIT UNDER REVIEW", show_thought=True, cmd_lim=1400, obs_lim=600, printed_eps=eps))
|
|
138
|
+
later = [x for x in t["steps"] if x["i"] > s_e["i"]][:5]
|
|
139
|
+
for x in later:
|
|
140
|
+
parts.append(step_block(t, x, keys + [anchor_hint(o) for o in x["ops"]], "FOLLOWING STEP", cmd_lim=300, obs_lim=450, printed_eps=eps))
|
|
141
|
+
fd = final_diff_for(t, item["path"])
|
|
142
|
+
if fd:
|
|
143
|
+
parts.append("--- FINAL SUBMITTED DIFF FOR THIS FILE:\n" + fd[:1500])
|
|
144
|
+
return "\n\n".join(parts)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def items():
|
|
148
|
+
out = []
|
|
149
|
+
for fname, prefix in (("hazards.jsonl", "H"), ("intra_unit.jsonl", "I")):
|
|
150
|
+
with open(os.path.join(WORK, fname), encoding="utf-8") as f:
|
|
151
|
+
for n, line in enumerate(f, 1):
|
|
152
|
+
it = json.loads(line)
|
|
153
|
+
it["kind_of_item"] = "hazard" if prefix == "H" else "intra_unit"
|
|
154
|
+
raw_id = f"{it['traj_id']}|{it['step']}|{it['op']}|{it['path']}"
|
|
155
|
+
it["id"] = prefix + hashlib.sha1(raw_id.encode()).hexdigest()[:6]
|
|
156
|
+
out.append(it)
|
|
157
|
+
return out
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def v1_keys():
|
|
161
|
+
"""(traj_id, step, path) of the items in the census's frozen v1 set."""
|
|
162
|
+
p = os.path.join(WORK, "excerpt_index_v1.json")
|
|
163
|
+
if not os.path.exists(p):
|
|
164
|
+
return set()
|
|
165
|
+
return {(v["traj_id"], v["step"], v["path"]) for v in json.load(open(p)).values()}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def main():
|
|
169
|
+
ap = argparse.ArgumentParser()
|
|
170
|
+
ap.add_argument("--show", default="")
|
|
171
|
+
ap.add_argument("--write", action="store_true",
|
|
172
|
+
help="write excerpts of items NOT in the frozen v1 set to census/items_v2/ "
|
|
173
|
+
"(census/items/ is frozen: the census labeler read it)")
|
|
174
|
+
args = ap.parse_args()
|
|
175
|
+
its = items()
|
|
176
|
+
trajs = load_trajs({it["traj_id"] for it in its})
|
|
177
|
+
index = {}
|
|
178
|
+
frozen = v1_keys()
|
|
179
|
+
outdir = os.path.join(CENSUS, "items_v2")
|
|
180
|
+
if args.write:
|
|
181
|
+
os.makedirs(outdir, exist_ok=True)
|
|
182
|
+
n_new = 0
|
|
183
|
+
for it in its:
|
|
184
|
+
index[it["id"]] = {k: it[k] for k in ("traj_id", "step", "op", "path", "kind_of_item", "agent", "model")}
|
|
185
|
+
index[it["id"]]["in_v1"] = (it["traj_id"], it["step"], it["path"]) in frozen
|
|
186
|
+
if args.show and it["id"] != args.show:
|
|
187
|
+
continue
|
|
188
|
+
if args.show:
|
|
189
|
+
print(build(it, trajs[it["traj_id"]]))
|
|
190
|
+
if args.write and not index[it["id"]]["in_v1"] and it["kind_of_item"] == "hazard":
|
|
191
|
+
n_new += 1
|
|
192
|
+
with open(os.path.join(outdir, it["id"] + ".txt"), "w", encoding="utf-8") as f:
|
|
193
|
+
f.write(build(it, trajs[it["traj_id"]]))
|
|
194
|
+
with open(os.path.join(WORK, "excerpt_index.json"), "w") as f:
|
|
195
|
+
json.dump(index, f, indent=1)
|
|
196
|
+
if args.write:
|
|
197
|
+
print("new hazard excerpts written:", n_new)
|
|
198
|
+
if not args.show:
|
|
199
|
+
print(len(its), "items;", sum(1 for i in its if i["kind_of_item"] == "hazard"), "hazards")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
main()
|