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,325 @@
1
+ """Independent stale-edit scorer (version-level and region-level). Usage: python3 scripts/audit/score.py
2
+
3
+ Reads docs/audit/rq1/work/extracted.jsonl (from extract.py) and replays each
4
+ trajectory. Definitions (the RQ1 structural definition, re-implemented):
5
+
6
+ content id every landed write gives the file a new content id; a cp/mv
7
+ gives the destination the source's id; a git restore returns
8
+ the base id 0; an editor undo returns the id before the last
9
+ editor write.
10
+ view content reaching the agent: a read op, an editor echo, or a
11
+ file quoted in the output (grep -rn / compiler `path:N:` lines,
12
+ traceback frames, diff headers). A directory listing is not a
13
+ view. Views count from the next composition unit on.
14
+ opportunity the first anchored edit to file f in a composition unit, when
15
+ the agent had seen some content of f before that unit.
16
+ hazard an opportunity where f's content id at the edit is not among
17
+ the ids the agent had been shown (or had authored) before it.
18
+
19
+ Variants: without editor echoes; without output-derived views; landed=None
20
+ treated as failed; per-step (not per-unit) composition; and a region-level
21
+ variant in which a view refreshes only the lines it showed.
22
+ Writes docs/audit/rq1/work/score_summary.json and hazards.jsonl.
23
+ """
24
+
25
+ import json
26
+ import os
27
+ import re
28
+ import sys
29
+ from collections import Counter, defaultdict
30
+
31
+ HERE = os.path.dirname(os.path.abspath(__file__))
32
+ sys.path.insert(0, HERE)
33
+ from common import WORK, short_model # noqa: E402
34
+
35
+ OUT_TRACE = re.compile(r'File "([^"<>\n]+)", line (\d+)')
36
+ OUT_LOC = re.compile(r"(?m)^\s*(?:-->\s+)?((?:/|\./|[\w.\-]+/)?(?:[\w.\-]+/)*[\w.\-]+\.[A-Za-z0-9]{1,8}):(\d+)[:\s-]")
37
+ OUT_DIFF = re.compile(r"(?m)^(?:diff --git a/(\S+) b/|\+\+\+ b/(\S+))")
38
+ # Blocks the write-through hook (src/harness) appends to a command's output.
39
+ # Each shows the file's current state for its region, so it is a view.
40
+ OUT_HOOK = re.compile(r"(?m)^\[(?:TAIL DIFF: File '([^'\n]+)'|VIEWPORT \(lines \d+-\d+ of \d+\): ([^\]\n]+))")
41
+
42
+
43
+ def canon_path(p, cwd, root, known=None):
44
+ """Canonical spelling of a path quoted in tool output.
45
+
46
+ Compilers, test runners and diff headers quote paths relative to the
47
+ project root (cargo, pytest, git), not to the shell's cwd; tracebacks
48
+ quote absolute paths. Candidates are tried in order and the first one
49
+ naming a file the trajectory touches elsewhere wins: root-relative,
50
+ cwd-relative, then a unique known path ending with the quoted path."""
51
+ import posixpath
52
+ if not p:
53
+ return None
54
+ p = p.strip()
55
+ if p.startswith("/proc/self/cwd/"):
56
+ p = posixpath.join(cwd or "/", p[len("/proc/self/cwd/"):])
57
+ if p.startswith(("a/", "b/")) and not p.startswith("/"):
58
+ p = p[2:]
59
+ if p.startswith("./"):
60
+ p = p[2:]
61
+
62
+ def strip(ap):
63
+ if root and (ap == root or ap.startswith(root + "/")):
64
+ return ap[len(root) + 1:]
65
+ return ap
66
+
67
+ if p.startswith("/"):
68
+ cands = [strip(posixpath.normpath(p))]
69
+ else:
70
+ cands = [posixpath.normpath(p),
71
+ strip(posixpath.normpath(posixpath.join(cwd or "/", p)))]
72
+ if known:
73
+ for c in cands:
74
+ if c in known:
75
+ return c
76
+ tail = [k for k in known if k.endswith("/" + cands[0])]
77
+ if len(tail) == 1:
78
+ return tail[0]
79
+ return cands[-1] if p.startswith("/") else cands[1]
80
+
81
+
82
+ def output_views(step, root, known=None):
83
+ """Files quoted in the step's output, with the line numbers quoted."""
84
+ obs = step.get("obs") or ""
85
+ if len(obs) > 400000:
86
+ obs = obs[:400000]
87
+ found = defaultdict(set)
88
+ for m in OUT_TRACE.finditer(obs):
89
+ found[m.group(1)].add(int(m.group(2)))
90
+ for m in OUT_LOC.finditer(obs):
91
+ found[m.group(1)].add(int(m.group(2)))
92
+ for m in OUT_DIFF.finditer(obs):
93
+ found[m.group(1) or m.group(2)].add(0)
94
+ for m in OUT_HOOK.finditer(obs):
95
+ found[m.group(1) or m.group(2)].add(0)
96
+ out = {}
97
+ for p, lines in found.items():
98
+ if p.startswith(("/usr/", "/opt/", "/root/.", "<")) or "site-packages" in p or "/lib/python" in p:
99
+ continue
100
+ c = canon_path(p, step.get("cwd"), root, known)
101
+ if c:
102
+ out[c] = lines
103
+ return out
104
+
105
+
106
+ def known_paths(traj):
107
+ return {o["path"] for s in traj["steps"] for o in s["ops"]}
108
+
109
+
110
+ class Replay:
111
+ def __init__(self, traj, echo=True, diag=True, none_is_landed=True, per_step=False):
112
+ self.t = traj
113
+ self.echo, self.diag, self.none_landed, self.per_step = echo, diag, none_is_landed, per_step
114
+ self.next_id = 1
115
+ self.cur = {} # (conv, path) -> content id
116
+ self.seen = defaultdict(set)
117
+ self.editor_hist = defaultdict(list)
118
+ self.modified = set()
119
+ self.views = defaultdict(list) # (conv,path) -> [(step_i, cid, kind, op_index)]
120
+ self.writes = defaultdict(list) # (conv,path) -> [(step_i, op_index, cid_before, cid_after, landed)]
121
+ self.opps = []
122
+ self.hazards = []
123
+ self.intra = [] # later anchored edits in the same unit after a landed write to the file
124
+ self.not_executed = [] # anchored edit commands that never ran (not found, syntax error)
125
+ self.known = known_paths(traj)
126
+
127
+ def cid(self, k):
128
+ return self.cur.get(k, 0)
129
+
130
+ def new(self):
131
+ self.next_id += 1
132
+ return self.next_id
133
+
134
+ def run(self):
135
+ steps = self.t["steps"]
136
+ root = self.t.get("root")
137
+ groups = []
138
+ for s in steps:
139
+ key = (s["conv"], s["i"]) if self.per_step else (s["conv"], s["unit"])
140
+ if groups and groups[-1][0] == key:
141
+ groups[-1][1].append(s)
142
+ else:
143
+ groups.append((key, [s]))
144
+ for (conv, unit), members in groups:
145
+ held = {k: set(v) for k, v in self.seen.items() if k[0] == conv}
146
+ scored = set()
147
+ written_in_unit = set()
148
+ pending_views = []
149
+ for s in members:
150
+ for oi, o in enumerate(s["ops"]):
151
+ k = (conv, o["path"])
152
+ if o["op"] == "R":
153
+ if o.get("landed") is False or o.get("kind") == "listing":
154
+ continue
155
+ if o.get("kind") == "echo" and not self.echo:
156
+ continue
157
+ pending_views.append((k, self.cid(k), s["i"], o.get("kind"), oi))
158
+ continue
159
+ landed = o.get("landed")
160
+ lands = landed is True or (landed is None and self.none_landed)
161
+ if o["path"] == "*":
162
+ if lands and o["kind"] == "restore":
163
+ for kk in list(self.modified):
164
+ if kk[0] == conv:
165
+ before = self.cid(kk)
166
+ self.cur[kk] = 0
167
+ self.writes[kk].append((s["i"], oi, before, 0, landed, "restore*", unit))
168
+ continue
169
+ if o["kind"] == "anchored" and o.get("ran") is False:
170
+ self.not_executed.append({"step": s["i"], "op": oi, "path": o["path"], "ev": o.get("ev")})
171
+ elif o["kind"] == "anchored":
172
+ if k not in scored:
173
+ scored.add(k)
174
+ if k in held:
175
+ c = self.cid(k)
176
+ haz = c not in held[k]
177
+ lv = self.views[k][-1] if self.views[k] else None
178
+ lv_pos = (lv[0], 10 ** 6 if lv[3] == -1 else lv[3]) if lv else (-1, -1)
179
+ W = [w for w in self.writes[k] if lv_pos < (w[0], w[1]) < (s["i"], oi)]
180
+ rec = {"step": s["i"], "op": oi, "path": o["path"], "conv": conv, "unit": unit,
181
+ "bench": s.get("bench"), "hazard": haz, "landed": landed,
182
+ "mech": o.get("mech"), "at_risk": any(w[3] != w[2] for w in self.writes[k]),
183
+ "last_view": lv, "W": W, "n_views_before": len(self.views[k])}
184
+ self.opps.append(rec)
185
+ if haz:
186
+ self.hazards.append(rec)
187
+ elif k in written_in_unit:
188
+ prior = [w for w in self.writes[k] if w[6] == unit]
189
+ self.intra.append({"step": s["i"], "op": oi, "path": o["path"], "conv": conv, "unit": unit,
190
+ "bench": s.get("bench"), "landed": landed, "mech": o.get("mech"),
191
+ "prior_same_unit_writes": prior})
192
+ if not lands:
193
+ continue
194
+ before = self.cid(k)
195
+ kind = o["kind"]
196
+ if kind == "restore":
197
+ if k in self.modified:
198
+ self.cur[k] = 0
199
+ elif kind == "touch":
200
+ continue
201
+ elif kind == "replace" and o.get("src"):
202
+ sk = (conv, o["src"])
203
+ self.cur[k] = self.cid(sk) if sk in self.cur or sk in self.seen else self.new()
204
+ if self.cur[k] == 0:
205
+ self.cur[k] = ("base", o["src"])
206
+ elif kind == "undo":
207
+ hist = self.editor_hist[k]
208
+ self.cur[k] = hist.pop() if hist else self.new()
209
+ else:
210
+ self.cur[k] = self.new()
211
+ if o.get("mech", "").startswith("editor"):
212
+ self.editor_hist[k].append(before)
213
+ self.modified.add(k)
214
+ written_in_unit.add(k)
215
+ self.writes[k].append((s["i"], oi, before, self.cid(k), landed, kind, unit))
216
+ if kind == "overwrite":
217
+ # the agent authored every byte: a current view
218
+ held.setdefault(k, set()).add(self.cur[k])
219
+ pending_views.append((k, self.cur[k], s["i"], "authored", oi))
220
+ if self.diag:
221
+ for p, lines in output_views(s, root, self.known).items():
222
+ k = (conv, p)
223
+ pending_views.append((k, self.cid(k), s["i"], "diag", -1))
224
+ for k, c, si, kind, oi in pending_views:
225
+ self.seen[k].add(c)
226
+ self.views[k].append((si, c, kind, oi, unit))
227
+ return self
228
+
229
+
230
+ def cp_ci(k, n, a=0.05):
231
+ from scipy.stats import beta
232
+ if n == 0:
233
+ return (None, None)
234
+ lo = 0.0 if k == 0 else beta.ppf(a / 2, k, n - k + 1)
235
+ hi = 1.0 if k == n else beta.ppf(1 - a / 2, k + 1, n - k)
236
+ return (round(float(lo), 4), round(float(hi), 4))
237
+
238
+
239
+ def cell(t):
240
+ fw = t["agent"]
241
+ if fw == "OpenHands":
242
+ fw = "OpenHands"
243
+ return fw, short_model(t["model"])
244
+
245
+
246
+ def main():
247
+ variants = {
248
+ "primary": dict(),
249
+ "no_echo": dict(echo=False),
250
+ "no_diag": dict(diag=False),
251
+ "none_as_failed": dict(none_is_landed=False),
252
+ "per_step": dict(per_step=True),
253
+ }
254
+ tallies = {v: defaultdict(Counter) for v in variants}
255
+ per_traj = []
256
+ hz_out = open(os.path.join(WORK, "hazards.jsonl"), "w", encoding="utf-8")
257
+ opp_out = open(os.path.join(WORK, "opportunities.jsonl"), "w", encoding="utf-8")
258
+ intra_out = open(os.path.join(WORK, "intra_unit.jsonl"), "w", encoding="utf-8")
259
+ with open(os.path.join(WORK, "extracted.jsonl"), encoding="utf-8") as f:
260
+ for line in f:
261
+ t = json.loads(line)
262
+ if t.get("missing"):
263
+ continue
264
+ c = cell(t)
265
+ row = {"traj_id": t["traj_id"], "agent": t["agent"], "model": short_model(t["model"]),
266
+ "layout": t.get("layout"), "steps": t["n_steps"], "n_conv": t.get("n_conv")}
267
+ for v, kw in variants.items():
268
+ r = Replay(t, **kw).run()
269
+ tal = tallies[v][c]
270
+ tal["trajectories"] += 1
271
+ tal["steps"] += t["n_steps"]
272
+ tal["opportunities"] += len(r.opps)
273
+ tal["hazards"] += len(r.hazards)
274
+ tal["at_risk_opportunities"] += sum(1 for o in r.opps if o["at_risk"])
275
+ tal["zero_opp_trajectories"] += not r.opps
276
+ tal["hazard_trajectories"] += bool(r.hazards)
277
+ tal["intra_unit_repeat_edits"] += len(r.intra)
278
+ tal["anchored_not_executed"] += len(r.not_executed)
279
+ row[f"{v}_opps"] = len(r.opps)
280
+ row[f"{v}_haz"] = len(r.hazards)
281
+ if v == "primary":
282
+ meta = dict(traj_id=t["traj_id"], agent=t["agent"], model=short_model(t["model"]),
283
+ layout=t.get("layout"))
284
+ for o in r.opps:
285
+ opp_out.write(json.dumps(dict(o, **meta), default=str) + "\n")
286
+ for h in r.hazards:
287
+ hz_out.write(json.dumps(dict(h, **meta), default=str) + "\n")
288
+ for x in r.intra:
289
+ intra_out.write(json.dumps(dict(x, **meta), default=str) + "\n")
290
+ row["intra"] = len(r.intra)
291
+ ops = Counter()
292
+ for s in t["steps"]:
293
+ for o in s["ops"]:
294
+ ops[(o["op"], o["kind"])] += 1
295
+ row["ops"] = {f"{a}:{b}": n for (a, b), n in ops.items()}
296
+ row["zero_op_steps"] = sum(1 for s in t["steps"] if not s["ops"])
297
+ per_traj.append(row)
298
+ hz_out.close()
299
+ opp_out.close()
300
+ intra_out.close()
301
+ summary = {}
302
+ for v, cells in tallies.items():
303
+ summary[v] = {}
304
+ for (fw, m), tal in sorted(cells.items()):
305
+ n, k = tal["opportunities"], tal["hazards"]
306
+ summary[v][f"{fw}|{m}"] = dict(tal, rate=round(k / n, 4) if n else None, ci=cp_ci(k, n))
307
+ # framework totals
308
+ fwt = defaultdict(Counter)
309
+ for (fw, m), tal in cells.items():
310
+ fwt[fw].update(tal)
311
+ for fw, tal in fwt.items():
312
+ n, k = tal["opportunities"], tal["hazards"]
313
+ summary[v][f"{fw}|ALL"] = dict(tal, rate=round(k / n, 4) if n else None, ci=cp_ci(k, n))
314
+ with open(os.path.join(WORK, "score_summary.json"), "w") as f:
315
+ json.dump({"summary": summary, "per_trajectory": per_traj}, f, indent=1)
316
+ for key, v in summary["primary"].items():
317
+ print(f"{key:40s} traj {v['trajectories']:4d} steps {v['steps']:6d} opp {v['opportunities']:5d} "
318
+ f"haz {v['hazards']:5d} rate {v['rate']} ci {v['ci']} zeroOpp {v['zero_opp_trajectories']}")
319
+ print("--- variants (hazards/opportunities by framework)")
320
+ for var in variants:
321
+ print(var, {k: (v["hazards"], v["opportunities"]) for k, v in summary[var].items() if k.endswith("|ALL")})
322
+
323
+
324
+ if __name__ == "__main__":
325
+ main()