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.
Files changed (60) hide show
  1. align_agent/__init__.py +11 -0
  2. align_agent/_audit/common.py +58 -0
  3. align_agent/_audit/deepswe.py +260 -0
  4. align_agent/_audit/excerpts.py +203 -0
  5. align_agent/_audit/extract.py +798 -0
  6. align_agent/_audit/materialize.py +624 -0
  7. align_agent/_audit/score.py +325 -0
  8. align_agent/_audit/shellparse.py +1141 -0
  9. align_agent/_audit/tarsur.py +153 -0
  10. align_agent/_vendor/src/__init__.py +3 -0
  11. align_agent/_vendor/src/harness/__init__.py +13 -0
  12. align_agent/_vendor/src/harness/arms.py +524 -0
  13. align_agent/_vendor/src/harness/completion.py +104 -0
  14. align_agent/_vendor/src/harness/dependency_notice.py +616 -0
  15. align_agent/_vendor/src/harness/freshness_hook.py +131 -0
  16. align_agent/_vendor/src/harness/lean.py +41 -0
  17. align_agent/_vendor/src/harness/mini_real.py +1040 -0
  18. align_agent/_vendor/src/harness/miniswe_scaffold.py +661 -0
  19. align_agent/_vendor/src/harness/tail_diff.py +292 -0
  20. align_agent/_vendor/src/harness/viewport.py +220 -0
  21. align_agent/_vendor/src/metrics/__init__.py +38 -0
  22. align_agent/_vendor/src/metrics/coherence.py +354 -0
  23. align_agent/_vendor/src/metrics/corruption_oracles.py +139 -0
  24. align_agent/_vendor/src/metrics/replay.py +399 -0
  25. align_agent/_vendor/src/metrics/statistics.py +185 -0
  26. align_agent/_vendor/src/metrics/verification.py +104 -0
  27. align_agent/_vendor/src/parsers/__init__.py +33 -0
  28. align_agent/_vendor/src/parsers/base_parser.py +349 -0
  29. align_agent/_vendor/src/parsers/file_events.py +806 -0
  30. align_agent/_vendor/src/parsers/miniswe_parser.py +96 -0
  31. align_agent/_vendor/src/parsers/openhands_parser.py +414 -0
  32. align_agent/_vendor/src/parsers/sweagent_parser.py +180 -0
  33. align_agent/_vendor/src/parsers/terminus_parser.py +106 -0
  34. align_agent/cli.py +101 -0
  35. align_agent/core.py +161 -0
  36. align_agent/run.py +84 -0
  37. align_agent/sessions.py +985 -0
  38. align_agent/writethrough/__init__.py +4 -0
  39. align_agent/writethrough/_lib/__init__.py +1 -0
  40. align_agent/writethrough/_lib/wt_src/__init__.py +3 -0
  41. align_agent/writethrough/_lib/wt_src/harness/__init__.py +1 -0
  42. align_agent/writethrough/_lib/wt_src/harness/dependency_notice.py +616 -0
  43. align_agent/writethrough/_lib/wt_src/harness/lean.py +41 -0
  44. align_agent/writethrough/_lib/wt_src/harness/tail_diff.py +292 -0
  45. align_agent/writethrough/_lib/wt_src/harness/viewport.py +220 -0
  46. align_agent/writethrough/_lib/wt_src/metrics/__init__.py +38 -0
  47. align_agent/writethrough/_lib/wt_src/metrics/replay.py +399 -0
  48. align_agent/writethrough/_lib/wt_src/parsers/__init__.py +1 -0
  49. align_agent/writethrough/_lib/wt_src/parsers/file_events.py +806 -0
  50. align_agent/writethrough/claude_code.py +81 -0
  51. align_agent/writethrough/engine.py +647 -0
  52. align_agent/writethrough/hooks.py +227 -0
  53. align_agent/writethrough/mcp_server.py +125 -0
  54. align_agent/writethrough/protocol.py +68 -0
  55. align_agent-0.4.3.dist-info/METADATA +224 -0
  56. align_agent-0.4.3.dist-info/RECORD +60 -0
  57. align_agent-0.4.3.dist-info/WHEEL +5 -0
  58. align_agent-0.4.3.dist-info/entry_points.txt +6 -0
  59. align_agent-0.4.3.dist-info/licenses/LICENSE +21 -0
  60. align_agent-0.4.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,798 @@
1
+ """Independent trajectory extractor for the RQ1 audit. Usage: python3 scripts/audit/extract.py [--only TRAJ_ID]
2
+
3
+ Reads raw trajectories straight from the cache (never src/parsers, never the
4
+ downloader) and writes one normalised record per trajectory to
5
+ docs/audit/rq1/work/extracted.jsonl (gitignored; about 60 MB):
6
+
7
+ steps[i] = {i, unit, conv, bench, tool, action, obs, rc, cwd, ops, tok_a, tok_o}
8
+
9
+ `unit` groups steps composed together (a Terminus2 episode, an OpenHands
10
+ reply with several tool calls); `bench` is the CodeTraceBench step id under
11
+ the step definition that reproduces step_count for that framework (None when
12
+ the trajectory does not align). ops come from scripts/audit/shellparse.py for
13
+ shell text and from the editor arguments for editor tools; every write op
14
+ carries `landed` (True/False/None) and the evidence used.
15
+ """
16
+
17
+ import argparse
18
+ import hashlib
19
+ import json
20
+ import os
21
+ import re
22
+ import shlex
23
+ import sys
24
+ from collections import Counter, defaultdict
25
+
26
+ HERE = os.path.dirname(os.path.abspath(__file__))
27
+ sys.path.insert(0, HERE)
28
+ from common import WORK, load_manifest, load_raw # noqa: E402
29
+ import shellparse as sp # noqa: E402
30
+
31
+ TOK = 4 # chars per token
32
+
33
+
34
+ def tok(s):
35
+ return len(s or "") // TOK
36
+
37
+
38
+ # ---------------------------------------------------------------- outcome evidence
39
+
40
+ SED_ERR = re.compile(r"^sed: (-e expression #\d+, char \d+: .*|can't read ([^:\n]+): .*|couldn't .*|cannot rename .*|no input files)$", re.M)
41
+ NOT_FOUND = re.compile(r"(?:^|[\s:])([\w./+-]+): (?:command )?not found", re.M)
42
+ PY_TB_FRAME = re.compile(r'File "([^"]+)", line (\d+)')
43
+ NO_SUCH = re.compile(r"([^\s:'\"`]+): No such file or directory")
44
+ PY_SYNTAX = re.compile(r"^\s*(SyntaxError|IndentationError|TabError): ", re.M)
45
+ TRACEBACK = re.compile(r"Traceback \(most recent call last\)")
46
+ BASH_SYNTAX = re.compile(r"syntax error near unexpected token|syntax error: unexpected end of file|Syntax error: ")
47
+ HEREDOC_EOF = re.compile(r"here-document at line \d+ delimited by end-of-file")
48
+ UNTERMINATED = re.compile(r"unexpected EOF while looking for matching|Unterminated quoted string")
49
+ TOOL_REFUSED = re.compile(r"^\s*ERROR: Cannot execute multiple commands at once")
50
+ PATCH_FAIL = re.compile(r"No valid patches in input|corrupt patch|Hunk #\d+ FAILED|hunks? FAILED|patch: \*\*\*\* |error: patch failed|does not apply|malformed patch|Only garbage was found")
51
+ EDIT_OK = re.compile(r"has been edited|created successfully|File created successfully|undone successfully|Text inserted|successfully inserted", re.I)
52
+ EDIT_FAIL = re.compile(r"No replacement was performed|did not appear verbatim|Multiple occurrences of old_str|is not a valid|does not exist|already exists|Invalid `|ERROR:|Parameter `\w+` is required|No edit history found|cannot be empty|must be different|should be different", re.I)
53
+
54
+
55
+ SH_SYNTAX_AT = re.compile(r"line (\d+): syntax error([^\n]*)|(?:^|\n)\S*sh: (\d+): Syntax error:([^\n]*)|line (\d+): unexpected EOF")
56
+
57
+
58
+ def unit_lines(cmd_text, ops):
59
+ """Line (1-based) on which each simple command's verb first appears,
60
+ found by a sequential search through the command text."""
61
+ lines = {}
62
+ pos = 0
63
+ text = sp.mask_heredocs(cmd_text or "")
64
+ for ci in sorted({o["cmd_i"] for o in ops}):
65
+ verb = next((o.get("verb") for o in ops if o["cmd_i"] == ci), "") or ""
66
+ f = text.find(verb, pos) if verb else -1
67
+ if f < 0:
68
+ f = pos
69
+ lines[ci] = text.count("\n", 0, f) + 1
70
+ pos = f + max(1, len(verb))
71
+ return lines
72
+
73
+
74
+ def shell_outcome(ops, obs, rc, cmd_text, truncated=False):
75
+ """Mark each op's `ran` and each write's `landed` (True/False/None) from
76
+ the command's own output. Rules, recorded in op['ev']:
77
+ shell syntax error: an unterminated construct (EOF) runs nothing; an
78
+ unexpected token at line N runs the commands on earlier lines
79
+ verb not found ("applypatch: not found"): that command ran nothing
80
+ sed: an error line; `can't read F` names the file
81
+ No such file or directory naming the target
82
+ python: SyntaxError with no traceback -> nothing ran; a traceback ->
83
+ landed iff the script's first write line precedes the raising line
84
+ (unknown when the frames cannot be matched)
85
+ patch: FAILED hunks / does not apply
86
+ && chains: a command after a failed command on an && guard did not run
87
+ Otherwise landed=True (no failure evidence)."""
88
+ obs = obs or ""
89
+ for o in ops:
90
+ o["ran"] = True
91
+ if TOOL_REFUSED.search(obs):
92
+ # OpenHands refused the multi-command input: nothing was executed
93
+ for o in ops:
94
+ o["ran"] = False
95
+ o["landed"], o["ev"] = False, "tool refused: multiple commands"
96
+ return
97
+ m = SH_SYNTAX_AT.search(obs)
98
+ if m or UNTERMINATED.search(obs):
99
+ if m and (m.group(1) or m.group(3)):
100
+ n = int(m.group(1) or m.group(3))
101
+ msg = (m.group(2) or m.group(4) or "")
102
+ eof = "end of file" in msg or "EOF" in msg
103
+ else:
104
+ n, eof = 1, True
105
+ ul = unit_lines(cmd_text, ops)
106
+ for o in ops:
107
+ if eof or ul.get(o["cmd_i"], 1) >= n:
108
+ o["ran"] = False
109
+ o["landed"], o["ev"] = False, "shell syntax error: did not run" + (" (EOF)" if eof else f" (line {n})")
110
+ sed_errs = SED_ERR.findall(obs)
111
+ sed_units = sorted({o["cmd_i"] for o in ops if o.get("mech") == "sed" and o["op"] == "W" and o["ran"]})
112
+ # A sed expression error is a compile error: that sed touched nothing,
113
+ # and with one error line the failing sed is the first whose script is at
114
+ # least as long as the reported character offset.
115
+ failing_sed = None
116
+ char_errs = re.findall(r"^sed: -e expression #(\d+), char (\d+): ", obs, re.M)
117
+ if len(char_errs) == 1 and len(sed_units) > 1:
118
+ e_no, ch = int(char_errs[0][0]), int(char_errs[0][1])
119
+ for ci in sed_units:
120
+ op0 = next(o for o in ops if o["cmd_i"] == ci and o.get("mech") == "sed" and o["op"] == "W")
121
+ raw = ((op0.get("anchor") or {}).get("raw") or [])
122
+ if len(raw) >= e_no and len(raw[e_no - 1]) >= ch:
123
+ failing_sed = ci
124
+ break
125
+ missing = set(mm.strip() for mm in NO_SUCH.findall(obs))
126
+ notfound = set(NOT_FOUND.findall(obs))
127
+ failed_units = set()
128
+ for o in ops:
129
+ if not o["ran"]:
130
+ failed_units.add(o["cmd_i"])
131
+ continue
132
+ ev, landed = "no failure evidence", True
133
+ if o.get("verb") in notfound:
134
+ landed, ev = False, f"{o.get('verb')}: not found"
135
+ o["ran"] = False
136
+ elif o.get("mech") == "sed" and o["op"] == "W" and sed_errs:
137
+ named = [x[1] for x in sed_errs if x[1]]
138
+ if named:
139
+ if any(o["path"].endswith(nm.lstrip("./")) or nm.endswith(os.path.basename(o["path"])) for nm in named):
140
+ landed, ev = False, "sed: can't read target"
141
+ elif len(sed_units) == 1:
142
+ landed, ev = False, "sed error (single sed unit)"
143
+ elif failing_sed is not None:
144
+ if o["cmd_i"] == failing_sed:
145
+ landed, ev = False, "sed expression error (identified by char offset)"
146
+ elif o["cmd_i"] < failing_sed:
147
+ landed, ev = True, "sed before the failing sed"
148
+ else:
149
+ landed, ev = None, "sed after the failing sed"
150
+ else:
151
+ landed, ev = None, "sed error (ambiguous unit)"
152
+ elif o.get("mech") in ("python",) and o["op"] == "W":
153
+ if PY_SYNTAX.search(obs) and not TRACEBACK.search(obs):
154
+ landed, ev = False, "python SyntaxError: nothing ran"
155
+ o["ran"] = False
156
+ elif TRACEBACK.search(obs):
157
+ src = o.get("script_src") or ""
158
+ base = os.path.basename(src[5:]) if src.startswith("file:") else None
159
+ fl = [int(nn) for f, nn in PY_TB_FRAME.findall(obs)
160
+ if (f in ("<stdin>", "<string>", "-") and not base) or (base and (f.endswith("/" + base) or f == base))]
161
+ wl = o.get("write_line")
162
+ if fl and wl:
163
+ if wl > max(fl):
164
+ landed, ev = False, f"python raised at line {max(fl)} before its write (line {wl})"
165
+ else:
166
+ landed, ev = True, f"python wrote (line {wl}) before raising at line {max(fl)}"
167
+ else:
168
+ landed, ev = None, "python traceback, write order unknown"
169
+ elif o.get("mech") in ("patch", "git-apply", "applypatch") and PATCH_FAIL.search(obs):
170
+ landed, ev = False, "patch failed"
171
+ if landed is not False and o["op"] == "W":
172
+ base = os.path.basename(o["path"])
173
+ for mm in missing:
174
+ if mm.endswith(base) and o["kind"] in ("anchored",) and o.get("mech") != "python":
175
+ landed, ev = False, "No such file or directory"
176
+ if o.get("guard") == "&&" and (o["cmd_i"] - 1) in failed_units:
177
+ landed, ev = False, "&& after a failed command"
178
+ o["ran"] = False
179
+ if landed is False:
180
+ failed_units.add(o["cmd_i"])
181
+ if o["op"] == "W":
182
+ o["landed"], o["ev"] = landed, ev
183
+ else:
184
+ o["landed"] = landed is not False and o["ran"]
185
+ if o["path"] in missing or any(mm.endswith(os.path.basename(o["path"])) and o["path"].endswith(mm.lstrip("./"))
186
+ for mm in missing):
187
+ o["landed"], o["ev"] = False, "No such file (view failed)"
188
+ if HEREDOC_EOF.search(obs):
189
+ for o in ops:
190
+ o.setdefault("flags", []).append("heredoc_eof")
191
+
192
+
193
+ def editor_outcome(sub, result):
194
+ if result is None:
195
+ return None, "no result on record"
196
+ if EDIT_FAIL.search(result) and not EDIT_OK.search(result):
197
+ return False, "editor rejected: " + EDIT_FAIL.search(result).group(0)
198
+ if EDIT_OK.search(result):
199
+ return True, "editor confirmed"
200
+ return None, "editor result unclear"
201
+
202
+
203
+ # ---------------------------------------------------------------- editor ops
204
+
205
+ def editor_ops(sub, path, args, result, cwd="/"):
206
+ """Ops for one str_replace_editor-style call."""
207
+ if not path:
208
+ return []
209
+ p = sp.resolve(path, cwd)
210
+ if sub in ("view", "open"):
211
+ if result is not None and re.match(r"\s*(ERROR|Invalid|The path .* does not exist)", result or ""):
212
+ return []
213
+ if result is not None and ("files and directories up to" in (result or "") or "is a directory" in (result or "")):
214
+ return [{"op": "R", "path": p, "kind": "listing", "mech": "editor-view", "view": {}, "landed": True}]
215
+ vr = args.get("view_range")
216
+ if isinstance(vr, str):
217
+ try:
218
+ vr = json.loads(vr)
219
+ except Exception:
220
+ vr = None
221
+ view = {"range": [int(vr[0]), int(vr[1]) if int(vr[1]) > 0 else 10 ** 9]} if isinstance(vr, (list, tuple)) and len(vr) == 2 else {"range": [1, 10 ** 9]}
222
+ view["numbered"] = True
223
+ kind = "full" if view["range"] == [1, 10 ** 9] else "range"
224
+ return [{"op": "R", "path": p, "kind": kind, "mech": "editor-view", "view": view, "landed": True}]
225
+ landed, ev = editor_outcome(sub, result)
226
+ if sub == "str_replace":
227
+ op = {"op": "W", "path": p, "kind": "anchored", "mech": "editor",
228
+ "anchor": {"type": "str_replace", "old": args.get("old_str"), "new": args.get("new_str") or ""},
229
+ "landed": landed, "ev": ev}
230
+ elif sub == "insert":
231
+ op = {"op": "W", "path": p, "kind": "anchored", "mech": "editor",
232
+ "anchor": {"type": "insert", "line": args.get("insert_line"), "new": args.get("new_str") or ""},
233
+ "landed": landed, "ev": ev}
234
+ elif sub == "create":
235
+ op = {"op": "W", "path": p, "kind": "overwrite", "mech": "editor-create",
236
+ "text": args.get("file_text"), "landed": landed, "ev": ev}
237
+ elif sub == "undo_edit":
238
+ op = {"op": "W", "path": p, "kind": "undo", "mech": "editor-undo", "landed": landed, "ev": ev}
239
+ else:
240
+ return []
241
+ ops = [op]
242
+ if landed and result and "result of running `cat -n`" in result:
243
+ ops.append({"op": "R", "path": p, "kind": "echo", "mech": "editor-echo", "view": {"numbered": True},
244
+ "landed": True})
245
+ return ops
246
+
247
+
248
+ # ---------------------------------------------------------------- mini-SWE-agent
249
+
250
+ MINI_ACTION = re.compile(r"```(?:mswea_bash_command|bash)\s*\n(.*?)\n```", re.S)
251
+ MINI_CWD = {"mini_swe_agent__multi": "/home", "mini_swe_agent__poly": "/testbed",
252
+ "mini_swe_agent__pro": "/app", "mini_swe_agent__verified": "/testbed"}
253
+
254
+
255
+ def subset_of(item):
256
+ sr = item.get("source_relpath") or ""
257
+ return sr.split("/")[1] if sr.startswith("swe_raw/") else "tbench"
258
+
259
+
260
+ def mini_obs(text):
261
+ rc_m = re.search(r"<returncode>(-?\d+)</returncode>", text or "")
262
+ rc = int(rc_m.group(1)) if rc_m else None
263
+ trunc = "<output_head>" in (text or "") or "<warning>" in (text or "")
264
+ m = re.search(r"<output>\n?(.*?)</output>", text or "", re.S)
265
+ if m:
266
+ out = m.group(1)
267
+ # Blocks a write-through hook appended after the command's output
268
+ # (src/harness): part of what the agent was shown.
269
+ tail = (text or "")[m.end():]
270
+ if re.search(r"(?m)^\[(?:TAIL DIFF|VIEWPORT|DEPENDENCY NOTICE)", tail):
271
+ out += tail
272
+ else:
273
+ h = re.search(r"<output_head>\n?(.*?)</output_head>", text or "", re.S)
274
+ t = re.search(r"<output_tail>\n?(.*?)</output_tail>", text or "", re.S)
275
+ out = ((h.group(1) if h else "") + "\n[...elided...]\n" + (t.group(1) if t else "")) if (h or t) else (text or "")
276
+ return rc, out, trunc
277
+
278
+
279
+ def extract_mini(raw, item):
280
+ msgs = raw.get("messages", [])
281
+ cwd0 = item.get("cwd") or MINI_CWD.get(subset_of(item), "/app")
282
+ steps, known = [], {}
283
+ k = 0
284
+ final_diff = None
285
+ for i, m in enumerate(msgs):
286
+ if m.get("role") != "assistant":
287
+ continue
288
+ k += 1
289
+ content = m.get("content") or ""
290
+ blocks = MINI_ACTION.findall(content)
291
+ nxt = msgs[i + 1] if i + 1 < len(msgs) else None
292
+ obs_raw = nxt.get("content", "") if nxt and nxt.get("role") in ("user", "tool") else ""
293
+ obs_raw = obs_raw if isinstance(obs_raw, str) else str(obs_raw)
294
+ rc, out, trunc = mini_obs(obs_raw)
295
+ ran = len(blocks) == 1 and "Please always provide EXACTLY ONE action" not in obs_raw
296
+ cmd = blocks[0] if ran else ""
297
+ ops = []
298
+ if ran:
299
+ ops, _, info = sp.extract(cmd, cwd0, known, persist_cd=False)
300
+ shell_outcome(ops, out, rc, cmd, trunc)
301
+ steps.append({"i": len(steps), "unit": len(steps), "conv": 0, "bench": k, "tool": "shell",
302
+ "action": cmd if ran else content[-2000:], "thought": content.split("```")[0][-1500:],
303
+ "obs": out, "rc": rc, "truncated": trunc, "cwd": cwd0, "ran": ran, "ops": ops,
304
+ "tok_a": tok(content), "tok_o": tok(obs_raw)})
305
+ last = msgs[-1] if msgs else {}
306
+ if last.get("role") == "user" and (last.get("content") or "").lstrip().startswith("diff --git"):
307
+ final_diff = last["content"]
308
+ return steps, {"n_conv": 1, "final_diff": final_diff, "bench_status": "exact" if k == item["step_count"] else "none"}
309
+
310
+
311
+ # ---------------------------------------------------------------- SWE-agent
312
+
313
+ def _state(e):
314
+ st = e.get("state")
315
+ if isinstance(st, str):
316
+ try:
317
+ st = json.loads(st)
318
+ except Exception:
319
+ st = {}
320
+ return st if isinstance(st, dict) else {}
321
+
322
+
323
+ def parse_sre(action):
324
+ """Parse a `str_replace_editor <sub> <path> --flag value ...` action."""
325
+ try:
326
+ toks = shlex.split(action, posix=True)
327
+ except ValueError:
328
+ toks = None
329
+ if not toks or toks[0] != "str_replace_editor" or len(toks) < 3:
330
+ m = re.match(r"str_replace_editor\s+(\w+)\s+(\S+)", action)
331
+ return (m.group(1), m.group(2), {"_unparsed": True}) if m else (None, None, {})
332
+ sub, path, args = toks[1], toks[2], {}
333
+ i = 3
334
+ while i < len(toks):
335
+ t = toks[i]
336
+ if t.startswith("--"):
337
+ key = t[2:]
338
+ if key == "view_range" and i + 2 < len(toks):
339
+ args["view_range"] = [toks[i + 1], toks[i + 2]]
340
+ i += 3
341
+ continue
342
+ args[key] = toks[i + 1] if i + 1 < len(toks) else ""
343
+ i += 2
344
+ else:
345
+ i += 1
346
+ if "insert_line" in args:
347
+ try:
348
+ args["insert_line"] = int(args["insert_line"])
349
+ except ValueError:
350
+ pass
351
+ return sub, path, args
352
+
353
+
354
+ SWE_OLD_VIEW = re.compile(r"^\[File: (\S+) \((\d+) lines total\)\]", re.M)
355
+
356
+
357
+ def extract_swe(raw, item):
358
+ tr = raw.get("trajectory", [])
359
+ steps, known = [], {}
360
+ cwd = "/testbed"
361
+ st0 = _state(tr[0]) if tr else {}
362
+ open_file = None
363
+ for idx, e in enumerate(tr, 1):
364
+ a = (e.get("action") or "").strip()
365
+ obs = e.get("observation") or ""
366
+ obs = obs if isinstance(obs, str) else str(obs)
367
+ st = _state(e)
368
+ cwd_before = cwd
369
+ ops = []
370
+ verb = a.split(None, 1)[0] if a else ""
371
+ tool = "shell"
372
+ if verb == "str_replace_editor":
373
+ tool = "editor"
374
+ sub, path, args = parse_sre(a)
375
+ if sub:
376
+ ops = editor_ops(sub, path, args, obs, cwd)
377
+ elif verb in ("open", "goto", "scroll_up", "scroll_down", "search_file", "create", "edit"):
378
+ tool = "editor-old"
379
+ m = SWE_OLD_VIEW.search(obs)
380
+ shown = m.group(1) if m else None
381
+ if verb == "open":
382
+ parts = a.split()
383
+ if len(parts) > 1:
384
+ open_file = sp.resolve(parts[1].strip('"\''), cwd)
385
+ if shown:
386
+ open_file = sp.resolve(shown, cwd)
387
+ ops = [{"op": "R", "path": open_file, "kind": "range", "mech": "swe-open", "view": {"window": True}, "landed": True}]
388
+ elif verb in ("goto", "scroll_up", "scroll_down"):
389
+ if shown:
390
+ open_file = sp.resolve(shown, cwd)
391
+ ops = [{"op": "R", "path": open_file, "kind": "range", "mech": "swe-" + verb, "view": {"window": True}, "landed": True}]
392
+ elif verb == "search_file":
393
+ try:
394
+ parts = shlex.split(a)
395
+ except ValueError:
396
+ parts = a.split()
397
+ f = sp.resolve(parts[2], cwd) if len(parts) > 2 else open_file
398
+ if f and "Line " in obs:
399
+ ops = [{"op": "R", "path": f, "kind": "lines", "mech": "swe-search", "view": {"grep": parts[1:2]}, "landed": True}]
400
+ elif verb == "create":
401
+ parts = a.split()
402
+ if len(parts) > 1:
403
+ f = sp.resolve(parts[1], cwd)
404
+ ok = "already exists" not in obs
405
+ ops = [{"op": "W", "path": f, "kind": "overwrite", "mech": "swe-create", "text": "", "landed": ok,
406
+ "ev": "create"}]
407
+ if ok:
408
+ open_file = f
409
+ elif verb == "edit":
410
+ m2 = re.match(r"edit\s+(?:(\S+)\s+)?(\d+):(\d+)", a)
411
+ if m2:
412
+ f = sp.resolve(m2.group(1), cwd) if m2.group(1) else open_file
413
+ body = a.split("\n", 1)[1] if "\n" in a else ""
414
+ body = re.sub(r"\n?end_of_edit\s*$", "", body)
415
+ rejected = any(x in obs for x in ("Usage: edit", "Your proposed edit has introduced new syntax error",
416
+ "This is how your edit would have looked if applied"))
417
+ if f:
418
+ ops = [{"op": "W", "path": f, "kind": "anchored", "mech": "swe-edit",
419
+ "anchor": {"type": "lines", "start": int(m2.group(2)), "end": int(m2.group(3)), "new": body},
420
+ "landed": not rejected, "ev": "swe edit " + ("rejected" if rejected else "applied")}]
421
+ if not rejected and shown:
422
+ ops.append({"op": "R", "path": f, "kind": "echo", "mech": "swe-edit-echo", "view": {"window": True}, "landed": True})
423
+ elif verb in ("submit", "exit_cost", "exit_format", "find_file", "search_dir", ""):
424
+ tool = "other"
425
+ else:
426
+ ops, cwd, _ = sp.extract(a, cwd, known, persist_cd=True)
427
+ shell_outcome(ops, obs, None, a)
428
+ if st.get("working_dir"):
429
+ cwd = st["working_dir"]
430
+ if st.get("open_file") and st.get("open_file") != "n/a":
431
+ open_file = sp.resolve(st["open_file"], cwd)
432
+ steps.append({"i": len(steps), "unit": len(steps), "conv": 0, "bench": idx, "tool": tool, "action": a,
433
+ "thought": (e.get("thought") or "")[-1500:], "obs": obs, "rc": None, "cwd": cwd_before,
434
+ "ran": True, "ops": ops, "tok_a": tok((e.get("response") or "") or a), "tok_o": tok(obs)})
435
+ sub = raw.get("info", {}).get("submission")
436
+ return steps, {"n_conv": 1, "final_diff": sub, "bench_status": "exact" if len(tr) == item["step_count"] else "none"}
437
+
438
+
439
+ # ---------------------------------------------------------------- OpenHands completion logs
440
+
441
+ def _txt(c):
442
+ if isinstance(c, list):
443
+ return " ".join(x.get("text", "") for x in c if isinstance(x, dict))
444
+ return "" if c is None else str(c)
445
+
446
+
447
+ def _reply(x):
448
+ ch = ((x.get("response") or {}).get("choices") or [])
449
+ return ch[0].get("message", {}) if ch else {}
450
+
451
+
452
+ def oh_conversations(calls):
453
+ conv_of, all_ids = [], []
454
+ for x in calls:
455
+ pids = set()
456
+ for m in x.get("messages") or []:
457
+ if not isinstance(m, dict):
458
+ continue
459
+ for tc in m.get("tool_calls") or []:
460
+ if isinstance(tc, dict) and tc.get("id"):
461
+ pids.add(tc["id"])
462
+ if m.get("role") == "tool" and m.get("tool_call_id"):
463
+ pids.add(m["tool_call_id"])
464
+ best, best_n = None, 0
465
+ for c, ids in enumerate(all_ids):
466
+ n = len(ids & pids)
467
+ if n > best_n:
468
+ best, best_n = c, n
469
+ if best is None:
470
+ best = len(all_ids)
471
+ all_ids.append(set())
472
+ conv_of.append(best)
473
+ all_ids[best] |= {tc.get("id") for tc in _reply(x).get("tool_calls") or [] if isinstance(tc, dict)}
474
+ return conv_of
475
+
476
+
477
+ OH_CWD = re.compile(r"\[Current working directory: ([^\]]+)\]")
478
+ OH_RC = re.compile(r"\[The command completed with exit code (-?\d+)\.\]|\[Command finished with exit code (-?\d+)\]")
479
+
480
+
481
+ def extract_oh_log(raw, item):
482
+ calls = [x for x in raw if isinstance(x, dict) and "response" in x]
483
+ conv = oh_conversations(calls)
484
+ results = {}
485
+ for x in calls:
486
+ for m in x.get("messages") or []:
487
+ if isinstance(m, dict) and m.get("role") == "tool" and m.get("tool_call_id"):
488
+ results.setdefault(m["tool_call_id"], _txt(m.get("content")))
489
+ first_user = ""
490
+ for x in calls[:1]:
491
+ for m in x.get("messages") or []:
492
+ if isinstance(m, dict) and m.get("role") == "user":
493
+ first_user = _txt(m.get("content"))
494
+ break
495
+ m0 = re.search(r"(/workspace/[\w.\-]+)", first_user)
496
+ cwd_conv = defaultdict(lambda: m0.group(1) if m0 else "/workspace")
497
+ known = defaultdict(dict)
498
+ steps = []
499
+ per_conv_nonfinish = Counter()
500
+ for ci, (x, c) in enumerate(zip(calls, conv)):
501
+ msg = _reply(x)
502
+ tcs = msg.get("tool_calls") or []
503
+ if not tcs:
504
+ steps.append({"i": len(steps), "unit": ci, "conv": c, "bench": None, "tool": "message",
505
+ "action": _txt(msg.get("content"))[:2000], "thought": "", "obs": "", "rc": None,
506
+ "cwd": cwd_conv[c], "ran": False, "ops": [], "tok_a": tok(_txt(msg.get("content"))), "tok_o": 0})
507
+ continue
508
+ for tc in tcs:
509
+ fn = (tc.get("function") or {}).get("name", "")
510
+ try:
511
+ args = json.loads((tc.get("function") or {}).get("arguments") or "{}")
512
+ except Exception:
513
+ args = {}
514
+ if not isinstance(args, dict):
515
+ args = {}
516
+ res = results.get(tc.get("id"))
517
+ cwd_before = cwd_conv[c]
518
+ ops, rc, tool, action = [], None, "other", f"{fn}: {json.dumps(args)[:1500]}"
519
+ if fn in ("execute_bash", "bash", "terminal"):
520
+ tool = "shell"
521
+ action = args.get("command", "")
522
+ m = OH_RC.search(res or "")
523
+ rc = int(m.group(1) or m.group(2)) if m else None
524
+ ops, _, _ = sp.extract(action, cwd_before, known[c], persist_cd=True)
525
+ shell_outcome(ops, res or "", rc, action)
526
+ mc = OH_CWD.search(res or "")
527
+ if mc:
528
+ cwd_conv[c] = mc.group(1).strip()
529
+ elif fn == "str_replace_editor":
530
+ tool = "editor"
531
+ sub = args.get("command", "")
532
+ ops = editor_ops(sub, args.get("path", ""), args, res, cwd_before)
533
+ action = f"str_replace_editor {sub} {args.get('path', '')}"
534
+ if fn != "finish":
535
+ per_conv_nonfinish[c] += 1
536
+ steps.append({"i": len(steps), "unit": ci, "conv": c, "bench": None, "tool": tool, "fn": fn,
537
+ "args": {k: v for k, v in args.items() if k in ("old_str", "new_str", "file_text", "insert_line", "view_range", "command", "path")},
538
+ "action": action, "thought": _txt(msg.get("content"))[-1500:], "obs": res or "",
539
+ "rc": rc, "cwd": cwd_before, "ran": res is not None, "ops": ops,
540
+ "tok_a": tok(json.dumps(args)), "tok_o": tok(res)})
541
+ # benchmark alignment: non-finish tool calls of the conversation whose count equals step_count
542
+ sc = item["step_count"]
543
+ match = [c for c, n in per_conv_nonfinish.items() if n == sc]
544
+ status, annotated_conv = "none", None
545
+ if len(match) == 1:
546
+ status, annotated_conv = "exact", match[0]
547
+ elif len(set(conv)) == 1 and sc == 48 and per_conv_nonfinish[0] > 48:
548
+ status, annotated_conv = "prefix48", 0
549
+ elif len(match) > 1:
550
+ status = "ambiguous"
551
+ if annotated_conv is not None:
552
+ k = 0
553
+ for s in steps:
554
+ if s["conv"] == annotated_conv and s.get("fn") and s.get("fn") != "finish":
555
+ k += 1
556
+ s["bench"] = k if k <= sc else None
557
+ return steps, {"n_conv": len(set(conv)), "final_diff": None, "bench_status": status,
558
+ "annotated_conv": annotated_conv}
559
+
560
+
561
+ # ---------------------------------------------------------------- OpenHands event streams
562
+
563
+ def extract_oh_events(raw, item):
564
+ byid = {}
565
+ for it in raw:
566
+ for e in (it if isinstance(it, list) else [it]):
567
+ if isinstance(e, dict) and isinstance(e.get("id"), int) and ("action" in e or "observation" in e):
568
+ byid.setdefault(e["id"], e)
569
+ evs = [byid[k] for k in sorted(byid)]
570
+ obs_by_cause = {}
571
+ for e in evs:
572
+ if "observation" in e and e.get("cause") is not None:
573
+ obs_by_cause.setdefault(e["cause"], e)
574
+ steps, known = [], {}
575
+ cwd = "/app"
576
+ k = 0
577
+ unit_ids = {}
578
+ for e in evs:
579
+ if e.get("source") != "agent" or not e.get("action"):
580
+ continue
581
+ tcm = e.get("tool_call_metadata")
582
+ if tcm is None:
583
+ continue
584
+ k += 1
585
+ mr = (tcm.get("model_response") or {}).get("id") or f"ev{e['id']}"
586
+ unit = unit_ids.setdefault(mr, len(unit_ids))
587
+ a = e.get("action")
588
+ args = e.get("args") or {}
589
+ o = obs_by_cause.get(e["id"])
590
+ res = None if o is None else _txt(o.get("content"))
591
+ extras = (o or {}).get("extras") or {}
592
+ cwd_before = cwd
593
+ ops, rc, tool, action = [], None, "other", f"{a}: {json.dumps(args)[:800]}"
594
+ if a == "run":
595
+ tool = "shell"
596
+ action = args.get("command", "")
597
+ md = extras.get("metadata") or {}
598
+ rc = md.get("exit_code")
599
+ ops, _, _ = sp.extract(action, cwd_before, known, persist_cd=True)
600
+ shell_outcome(ops, res or "", rc, action)
601
+ if md.get("working_dir"):
602
+ cwd = md["working_dir"]
603
+ elif a == "read":
604
+ tool = "editor"
605
+ ops = editor_ops("view", args.get("path", ""), {"view_range": args.get("view_range")}, res, cwd_before)
606
+ if o is not None and o.get("observation") == "error":
607
+ ops = []
608
+ action = f"read {args.get('path')} {args.get('view_range') or ''}"
609
+ elif a == "edit":
610
+ tool = "editor"
611
+ sub = args.get("command") or ("create" if args.get("file_text") else "str_replace")
612
+ ops = editor_ops(sub, args.get("path", ""), args, res if o is not None and o.get("observation") != "error" else (res or "ERROR: error observation"), cwd_before)
613
+ if o is not None and o.get("observation") == "edit":
614
+ for op in ops:
615
+ if op["op"] == "W":
616
+ op["oracle"] = {"old_content": extras.get("old_content"), "new_content": extras.get("new_content"),
617
+ "prev_exist": extras.get("prev_exist")}
618
+ action = f"edit {sub} {args.get('path')}"
619
+ elif a == "run_ipython":
620
+ tool = "ipython"
621
+ code = args.get("code", "")
622
+ action = code
623
+ w, _ = sp.python_script_ops(code, cwd_before)
624
+ for op in w:
625
+ op["landed"], op["ev"] = (None, "ipython traceback") if res and TRACEBACK.search(res) else (True, "no failure evidence")
626
+ ops = w
627
+ steps.append({"i": len(steps), "unit": unit, "conv": 0, "bench": k, "tool": tool, "fn": a,
628
+ "args": {kk: vv for kk, vv in args.items() if kk in ("old_str", "new_str", "file_text", "insert_line", "view_range", "command", "path")},
629
+ "action": action, "thought": (args.get("thought") or "")[-1500:], "obs": res or "", "rc": rc,
630
+ "cwd": cwd_before, "ran": o is not None, "ops": ops,
631
+ "tok_a": tok(json.dumps(args)), "tok_o": tok(res)})
632
+ status = "exact" if k == item["step_count"] else "none"
633
+ return steps, {"n_conv": 1, "final_diff": None, "bench_status": status}
634
+
635
+
636
+ # ---------------------------------------------------------------- Terminus2
637
+
638
+ T2_PROMPT = re.compile(r"root@[\w.-]+:(/[^#\n]*|~)#")
639
+
640
+
641
+ def t2_commands(resp):
642
+ """(commands, thought, strict): strict is True when the response parses
643
+ as JSON as a whole; the benchmark counts only those commands."""
644
+ obj, strict = None, False
645
+ for n, text in enumerate((resp, (re.search(r"\{.*\}", resp or "", re.S) or [None])[0] if resp else None)):
646
+ if not text:
647
+ continue
648
+ try:
649
+ obj = json.loads(text)
650
+ strict = n == 0
651
+ break
652
+ except Exception:
653
+ continue
654
+ if not isinstance(obj, dict):
655
+ return None, "", False
656
+ cmds = [c for c in obj.get("commands") or [] if isinstance(c, dict)]
657
+ return cmds, (obj.get("analysis") or "") + "\n" + (obj.get("plan") or ""), strict
658
+
659
+
660
+ def extract_t2(raw, item):
661
+ eps = raw if isinstance(raw, list) else []
662
+ steps, known = [], {}
663
+ cwd = "/app"
664
+ bench = 0
665
+ for ei, ep in enumerate(eps):
666
+ cmds, thought, strict = t2_commands(ep.get("response") or "")
667
+ nxt = eps[ei + 1].get("prompt", "") if ei + 1 < len(eps) else ""
668
+ obs = nxt.split("New Terminal Output:", 1)[1] if "New Terminal Output:" in (nxt or "") else (nxt or "")
669
+ if cmds is None:
670
+ steps.append({"i": len(steps), "unit": ei, "conv": 0, "bench": None, "tool": "unparsed",
671
+ "action": (ep.get("response") or "")[:1500], "thought": "", "obs": obs, "rc": None,
672
+ "cwd": cwd, "ran": False, "ops": [], "tok_a": tok(ep.get("response")), "tok_o": tok(obs),
673
+ "episode": ei})
674
+ continue
675
+ # Join the keystrokes into one shell text, remembering which command
676
+ # each character came from; heredocs may span entries.
677
+ text, owner = "", []
678
+ for j, c in enumerate(cmds):
679
+ ks = str(c.get("keystrokes", ""))
680
+ if re.fullmatch(r"(C-[a-z]|\^[A-Z]|Escape|Enter|q|:q!?|:wq)\n?", ks.strip() + ("\n" if ks.endswith("\n") else "")):
681
+ ks = "\n" # control keys, pager exits: no shell text
682
+ text += ks
683
+ owner += [j] * len(ks)
684
+ ops, cwd_after, info = sp.extract(text, cwd, known, persist_cd=True)
685
+ # attribute each op to the command whose keystrokes contain its unit
686
+ units = sp.simple_commands(text)
687
+ # map unit index -> char offset (approximate: search the unit's first word)
688
+ pos, unit_cmd = 0, {}
689
+ masked = sp.mask_heredocs(text)
690
+ for ui, u in enumerate(units):
691
+ w = u["argv"][0] if u["argv"] else ""
692
+ if u.get("paren") or not w:
693
+ unit_cmd[ui] = unit_cmd.get(ui - 1, 0)
694
+ continue
695
+ f = masked.find(w, pos)
696
+ if f >= 0:
697
+ unit_cmd[ui] = min(owner[f], len(cmds) - 1) if f < len(owner) else len(cmds) - 1
698
+ pos = f + len(w)
699
+ else:
700
+ unit_cmd[ui] = unit_cmd.get(ui - 1, 0)
701
+ shell_outcome(ops, obs, None, text)
702
+ for o in ops:
703
+ o["cmd_j"] = unit_cmd.get(o["cmd_i"], 0)
704
+ mprompt = list(T2_PROMPT.finditer(obs or ""))
705
+ cwd_before_ep = cwd
706
+ cwd = cwd_after
707
+ if mprompt and mprompt[-1].group(1) not in ("~",):
708
+ cwd = mprompt[-1].group(1).strip()
709
+ for j, c in enumerate(cmds):
710
+ if strict:
711
+ bench += 1
712
+ steps.append({"i": len(steps), "unit": ei, "conv": 0, "bench": bench if strict else None, "tool": "shell",
713
+ "action": str(c.get("keystrokes", "")), "thought": thought[-1500:] if j == 0 else "",
714
+ "obs": obs if j == len(cmds) - 1 else "", "rc": None, "cwd": cwd_before_ep, "ran": True,
715
+ "ops": [o for o in ops if o["cmd_j"] == j],
716
+ "tok_a": tok(ep.get("response")) if j == 0 else 0,
717
+ "tok_o": tok(obs) if j == len(cmds) - 1 else 0, "episode": ei})
718
+ if not cmds:
719
+ steps.append({"i": len(steps), "unit": ei, "conv": 0, "bench": None, "tool": "none",
720
+ "action": "", "thought": thought[-1500:], "obs": obs, "rc": None, "cwd": cwd,
721
+ "ran": False, "ops": [], "tok_a": tok(ep.get("response")), "tok_o": tok(obs), "episode": ei})
722
+ status = "exact" if bench == item["step_count"] else "none"
723
+ return steps, {"n_conv": 1, "final_diff": None, "bench_status": status}
724
+
725
+
726
+ # ---------------------------------------------------------------- canonical paths
727
+
728
+ ROOT_RE = re.compile(r"^(/testbed|/app|/workspace/[^/]+|/home/[^/]+|/repo)(/|$)")
729
+
730
+
731
+ def canonicalize(steps):
732
+ """Strip the trajectory's repository root; keep other paths absolute."""
733
+ cnt = Counter()
734
+ for s in steps:
735
+ for o in s["ops"]:
736
+ m = ROOT_RE.match(o["path"])
737
+ if m:
738
+ cnt[m.group(1)] += 1
739
+ root = cnt.most_common(1)[0][0] if cnt else None
740
+ for s in steps:
741
+ for o in s["ops"]:
742
+ o["abs"] = o["path"]
743
+ if root and (o["path"] == root or o["path"].startswith(root + "/")):
744
+ o["path"] = o["path"][len(root) + 1:] or "."
745
+ if o.get("src") and root and o["src"].startswith(root + "/"):
746
+ o["src"] = o["src"][len(root) + 1:]
747
+ return root
748
+
749
+
750
+ def extract_one(item):
751
+ raw = load_raw(item["traj_id"])
752
+ if raw is None:
753
+ return {"traj_id": item["traj_id"], "missing": True}
754
+ a = item["agent"]
755
+ if a == "mini-SWE-agent":
756
+ steps, meta = extract_mini(raw, item)
757
+ elif a == "SWE-agent":
758
+ steps, meta = extract_swe(raw, item)
759
+ elif a == "Terminus2":
760
+ steps, meta = extract_t2(raw, item)
761
+ elif a == "OpenHands":
762
+ if isinstance(raw, list) and any(isinstance(x, dict) and "response" in x for x in raw):
763
+ steps, meta = extract_oh_log(raw, item)
764
+ meta["layout"] = "completion_log"
765
+ else:
766
+ steps, meta = extract_oh_events(raw, item)
767
+ meta["layout"] = "event_stream"
768
+ else:
769
+ return {"traj_id": item["traj_id"], "missing": True}
770
+ root = canonicalize(steps)
771
+ return {"traj_id": item["traj_id"], "agent": a, "model": item["model"], "subset": subset_of(item),
772
+ "step_count": item["step_count"], "root": root, "n_steps": len(steps), **meta, "steps": steps}
773
+
774
+
775
+ def main():
776
+ ap = argparse.ArgumentParser()
777
+ ap.add_argument("--only", default="")
778
+ args = ap.parse_args()
779
+ os.makedirs(WORK, exist_ok=True)
780
+ rows = load_manifest()
781
+ if args.only:
782
+ rows = [r for r in rows if r["traj_id"] == args.only]
783
+ out_path = os.path.join(WORK, "extracted.jsonl" if not args.only else "extracted_one.jsonl")
784
+ stats = Counter()
785
+ with open(out_path, "w", encoding="utf-8") as f:
786
+ for r in rows:
787
+ rec = extract_one(r)
788
+ if rec.get("missing"):
789
+ stats["missing"] += 1
790
+ else:
791
+ stats[f"{r['agent']}:{rec.get('bench_status')}"] += 1
792
+ f.write(json.dumps(rec, default=str) + "\n")
793
+ print(dict(stats))
794
+ print("wrote", out_path)
795
+
796
+
797
+ if __name__ == "__main__":
798
+ main()