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
|
@@ -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()
|