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,399 @@
|
|
|
1
|
+
"""The stale-act replay: one definition, applied to both research questions.
|
|
2
|
+
|
|
3
|
+
A step is a list of file events in the order the agent's action performed
|
|
4
|
+
them, each (op, path, kind, landed); see src/parsers/file_events.py for the
|
|
5
|
+
kinds. The replay keeps, per file, when the agent last saw it and when it last
|
|
6
|
+
changed, and scores every *anchored* edit (one located by existing text or
|
|
7
|
+
line numbers) against those two times:
|
|
8
|
+
|
|
9
|
+
opportunity an anchored edit to a file the agent had seen before
|
|
10
|
+
hazard an opportunity where the file changed after the agent last
|
|
11
|
+
saw it
|
|
12
|
+
|
|
13
|
+
Counts are in (step, file) units: several anchored edits to one file in one
|
|
14
|
+
action are one act, composed against one view. The rate is pooled, hazards
|
|
15
|
+
over opportunities summed across tasks, so a task with one edit weighs one
|
|
16
|
+
edit and a task with none contributes nothing.
|
|
17
|
+
|
|
18
|
+
What counts as seeing a file: any output that prints its content, whole or in
|
|
19
|
+
part (cat, sed -n, grep lines, an editor's view, the snippet an editor echoes
|
|
20
|
+
after an edit). A whole-file overwrite with text the command itself carries
|
|
21
|
+
also leaves the agent with a current view, since it wrote every byte. What
|
|
22
|
+
counts as a change: a write that landed. A failed command, a rejected editor
|
|
23
|
+
call and a `git checkout` of a file the agent never modified change nothing.
|
|
24
|
+
|
|
25
|
+
Arms act on the harness's own record of views, not on ours. An arm that puts
|
|
26
|
+
current content in the prompt (store-forwarding appends it, in-place
|
|
27
|
+
replacement writes it over the old view) supplies a file only when the harness
|
|
28
|
+
had bound that file; the credited count treats a supplied file as seen at the
|
|
29
|
+
start of the step, the strict count does not. The coherent-prompt rate is
|
|
30
|
+
measured the same way: a stale view is withdrawn only if the arm withdraws and
|
|
31
|
+
the harness had bound that view.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
from dataclasses import dataclass, field
|
|
37
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple
|
|
38
|
+
|
|
39
|
+
from wt_src.parsers.file_events import (ALL_PATHS, CONTAINER_ROOTS, command_cwd, copy_source, map_kind_path,
|
|
40
|
+
output_views, shell_events_with_outcome)
|
|
41
|
+
|
|
42
|
+
Event = Tuple[str, str, str, bool]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ReplayResult:
|
|
47
|
+
steps: int = 0
|
|
48
|
+
opportunities: int = 0
|
|
49
|
+
hazards_strict: int = 0
|
|
50
|
+
hazards_credited: int = 0
|
|
51
|
+
coherent_steps: int = 0
|
|
52
|
+
agent_views: int = 0
|
|
53
|
+
harness_bound_views: int = 0
|
|
54
|
+
hazard_steps: List[int] = field(default_factory=list)
|
|
55
|
+
hazard_files: List[str] = field(default_factory=list)
|
|
56
|
+
hazard_pairs: List[Tuple[int, str]] = field(default_factory=list)
|
|
57
|
+
# Secondary, outside the headline definition: a whole-file overwrite or
|
|
58
|
+
# replace of a file the agent had seen, and how many of those landed on a
|
|
59
|
+
# version it had not seen -- a rewrite that may silently discard a change.
|
|
60
|
+
overwrite_opportunities: int = 0
|
|
61
|
+
lost_updates: int = 0
|
|
62
|
+
# Opportunities on a file some write had changed since the agent first saw
|
|
63
|
+
# it: the only ones that can go stale when nothing else writes the file
|
|
64
|
+
# (in the live runs, repeat edits). Every hazard is one of these.
|
|
65
|
+
at_risk_opportunities: int = 0
|
|
66
|
+
at_risk_hazards: int = 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class _View:
|
|
71
|
+
version: int
|
|
72
|
+
bound: bool
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _Replayer:
|
|
76
|
+
"""Per-file state for one trajectory, advanced one step at a time.
|
|
77
|
+
|
|
78
|
+
Staleness follows content, as the model in Section II defines it: every
|
|
79
|
+
landed write gives the file a new version, a restore returns it to the
|
|
80
|
+
committed version (0), and the agent holds a valid view when any view it
|
|
81
|
+
has seen shows the file's current version. A `git checkout` back to
|
|
82
|
+
content the agent had already seen therefore leaves it with a valid view.
|
|
83
|
+
|
|
84
|
+
An agent composes a whole action before it sees any of that action's
|
|
85
|
+
output, so an edit is scored against the views held when the step began.
|
|
86
|
+
What the step prints (a `cat`, an editor's echo, a compiler quoting a
|
|
87
|
+
line) counts from the next step on.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, withdraws: bool, forwards: bool, refreshes: bool, known_substitutions: bool = False):
|
|
91
|
+
self.withdraws, self.forwards, self.refreshes = withdraws, forwards, refreshes
|
|
92
|
+
# Sensitivity: a substitution on a version the agent knew leaves a
|
|
93
|
+
# version it knows, as a whole-file write does.
|
|
94
|
+
self.known_substitutions = known_substitutions
|
|
95
|
+
self.out = ReplayResult()
|
|
96
|
+
self.next_version = 1
|
|
97
|
+
self.version: Dict[Tuple, int] = {} # current content version
|
|
98
|
+
self.seen: Dict[Tuple, Set[int]] = {} # versions the agent has seen
|
|
99
|
+
self.supplied: Dict[Tuple, int] = {} # version the harness last supplied
|
|
100
|
+
self.views: Dict[Tuple, List[_View]] = {}
|
|
101
|
+
self.modified: Set[Tuple] = set()
|
|
102
|
+
self.changed_after_seen: Set[Tuple] = set()
|
|
103
|
+
self.h_bound: Set[Tuple] = set() # the harness has a binding
|
|
104
|
+
self.h_invalid: Set[Tuple] = set() # ... and has marked it invalid
|
|
105
|
+
self.h_kept: Set[Tuple] = set() # in-place can refresh it
|
|
106
|
+
self.hazard_files: Set[str] = set()
|
|
107
|
+
|
|
108
|
+
def current(self, k: Tuple) -> int:
|
|
109
|
+
return self.version.get(k, 0)
|
|
110
|
+
|
|
111
|
+
def supply(self, sess) -> None:
|
|
112
|
+
"""What the arm puts in front of the agent before it acts."""
|
|
113
|
+
if not (self.forwards or self.refreshes):
|
|
114
|
+
return
|
|
115
|
+
for k in self.h_invalid:
|
|
116
|
+
if k[0] == sess and (self.forwards or k in self.h_kept):
|
|
117
|
+
self.supplied[k] = self.current(k)
|
|
118
|
+
|
|
119
|
+
def prompt_is_coherent(self) -> bool:
|
|
120
|
+
"""No stale view left in the prompt after the arm's withdrawals."""
|
|
121
|
+
for k, vs in self.views.items():
|
|
122
|
+
cur = self.current(k)
|
|
123
|
+
if any(v.version != cur and not (self.withdraws and v.bound) for v in vs):
|
|
124
|
+
return False
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
def score_edit(self, k: Tuple, held: Dict[Tuple, Set[int]]) -> bool:
|
|
128
|
+
"""Score an anchored edit against the views held at step start;
|
|
129
|
+
True when none of them shows the file as it now stands."""
|
|
130
|
+
if k not in held:
|
|
131
|
+
return False
|
|
132
|
+
self.out.opportunities += 1
|
|
133
|
+
at_risk = k in self.changed_after_seen
|
|
134
|
+
self.out.at_risk_opportunities += at_risk
|
|
135
|
+
cur = self.current(k)
|
|
136
|
+
if cur not in held[k] and cur != self.supplied.get(k):
|
|
137
|
+
self.out.hazards_credited += 1
|
|
138
|
+
if cur not in held[k]:
|
|
139
|
+
self.out.hazards_strict += 1
|
|
140
|
+
self.out.at_risk_hazards += at_risk
|
|
141
|
+
self.hazard_files.add(k[1])
|
|
142
|
+
return True
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
def land(self, k: Tuple, kind: str) -> None:
|
|
146
|
+
if k in self.seen:
|
|
147
|
+
self.changed_after_seen.add(k)
|
|
148
|
+
if kind == "restore":
|
|
149
|
+
if k in self.modified:
|
|
150
|
+
self.version[k] = 0
|
|
151
|
+
return
|
|
152
|
+
self.version[k] = self.next_version
|
|
153
|
+
self.next_version += 1
|
|
154
|
+
self.modified.add(k)
|
|
155
|
+
|
|
156
|
+
def land_all(self, sess, kind: str) -> None:
|
|
157
|
+
for k in list(self.modified):
|
|
158
|
+
if k[0] == sess:
|
|
159
|
+
self.land(k, kind)
|
|
160
|
+
|
|
161
|
+
def step(self, step: Dict[str, Any]) -> List[str]:
|
|
162
|
+
"""Advance one step; return the list of file paths hazarded (empty if none)."""
|
|
163
|
+
sess = step.get("session", 0)
|
|
164
|
+
self.supply(sess)
|
|
165
|
+
# Files whose current contents the prompt itself carried (the synced-
|
|
166
|
+
# context arm): the agent sees their current version as it acts.
|
|
167
|
+
for path in step.get("presented") or ():
|
|
168
|
+
k = (sess, path)
|
|
169
|
+
self.seen.setdefault(k, set()).add(self.current(k))
|
|
170
|
+
self.out.coherent_steps += self.prompt_is_coherent()
|
|
171
|
+
harness_reads = set(step.get("harness_reads") or ())
|
|
172
|
+
held = {k: set(v) for k, v in self.seen.items()}
|
|
173
|
+
acted: Set[Tuple] = set()
|
|
174
|
+
overwrote: Set[Tuple] = set()
|
|
175
|
+
last_op: Dict[Tuple, str] = {}
|
|
176
|
+
shown: List[Tuple[Tuple, int, bool]] = []
|
|
177
|
+
hazarded_paths: List[str] = []
|
|
178
|
+
for op, path, kind, landed in step.get("events", []):
|
|
179
|
+
k = (sess, path)
|
|
180
|
+
if op == "R":
|
|
181
|
+
shown.append((k, self.current(k), path in harness_reads))
|
|
182
|
+
last_op[k] = "R"
|
|
183
|
+
elif path == ALL_PATHS:
|
|
184
|
+
if landed:
|
|
185
|
+
self.land_all(sess, kind)
|
|
186
|
+
else:
|
|
187
|
+
last_op[k] = "W"
|
|
188
|
+
src = copy_source(kind)
|
|
189
|
+
if kind in ("anchored", "subst") and k not in acted:
|
|
190
|
+
acted.add(k)
|
|
191
|
+
if self.score_edit(k, held):
|
|
192
|
+
hazarded_paths.append(path)
|
|
193
|
+
elif (kind in ("overwrite", "replace") or src is not None) and k not in overwrote and k in held:
|
|
194
|
+
overwrote.add(k)
|
|
195
|
+
self.out.overwrite_opportunities += 1
|
|
196
|
+
if self.current(k) not in held[k]:
|
|
197
|
+
self.out.lost_updates += 1
|
|
198
|
+
if landed:
|
|
199
|
+
# The agent knows what a write leaves when it supplied
|
|
200
|
+
# every byte, or copied a file whose current version it
|
|
201
|
+
# had seen: later edits in this same action are composed
|
|
202
|
+
# against that, not against the views held at step start.
|
|
203
|
+
s_k = (sess, src) if src is not None else None
|
|
204
|
+
known = kind == "overwrite" or (
|
|
205
|
+
s_k is not None and self.current(s_k) in self.seen.get(s_k, ())) or (
|
|
206
|
+
kind == "subst" and self.known_substitutions and self.current(k) in self.seen.get(k, ()))
|
|
207
|
+
self.land(k, "replace" if src is not None else kind)
|
|
208
|
+
if known:
|
|
209
|
+
self.seen.setdefault(k, set()).add(self.current(k))
|
|
210
|
+
held.setdefault(k, set()).add(self.current(k))
|
|
211
|
+
for k, ver, bound in shown:
|
|
212
|
+
self.seen.setdefault(k, set()).add(ver)
|
|
213
|
+
self.views.setdefault(k, []).append(_View(ver, bound))
|
|
214
|
+
self.out.agent_views += 1
|
|
215
|
+
self.out.harness_bound_views += bound
|
|
216
|
+
for path in step.get("output_reads") or ():
|
|
217
|
+
k = (sess, path)
|
|
218
|
+
self.seen.setdefault(k, set()).add(self.current(k))
|
|
219
|
+
self.harness_update(sess, step, harness_reads, last_op)
|
|
220
|
+
return hazarded_paths
|
|
221
|
+
|
|
222
|
+
def harness_update(self, sess, step, harness_reads, last_op) -> None:
|
|
223
|
+
"""The harness's own bookkeeping, which is what the arms act on."""
|
|
224
|
+
for p in step.get("harness_writes") or ():
|
|
225
|
+
if (sess, p) in self.h_bound:
|
|
226
|
+
self.h_invalid.add((sess, p))
|
|
227
|
+
kept = step.get("harness_kept")
|
|
228
|
+
for p in harness_reads:
|
|
229
|
+
k = (sess, p)
|
|
230
|
+
self.h_bound.add(k)
|
|
231
|
+
if p == kept:
|
|
232
|
+
self.h_kept.add(k)
|
|
233
|
+
if last_op.get(k, "R") == "R":
|
|
234
|
+
self.h_invalid.discard(k)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def replay(steps: Sequence[Dict[str, Any]], *, withdraws: bool = False,
|
|
238
|
+
forwards: bool = False, refreshes: bool = False, known_substitutions: bool = False) -> ReplayResult:
|
|
239
|
+
"""Score a trajectory. Each step dict carries `events`, optionally
|
|
240
|
+
`session` (interleaved sessions keep separate state), and for live runs
|
|
241
|
+
`harness_reads`, `harness_writes`, `harness_kept`."""
|
|
242
|
+
r = _Replayer(withdraws, forwards, refreshes, known_substitutions)
|
|
243
|
+
for idx, step in enumerate(steps):
|
|
244
|
+
paths = r.step(step)
|
|
245
|
+
if paths:
|
|
246
|
+
r.out.hazard_steps.append(idx)
|
|
247
|
+
for p in paths:
|
|
248
|
+
r.out.hazard_pairs.append((idx, p))
|
|
249
|
+
r.out.steps = len(steps)
|
|
250
|
+
r.out.hazard_files = sorted(r.hazard_files)
|
|
251
|
+
return r.out
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# --- live (RQ2) traces --------------------------------------------------
|
|
255
|
+
|
|
256
|
+
_CONTAINER_ROOTS = CONTAINER_ROOTS
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _canon(path: str) -> Optional[str]:
|
|
260
|
+
"""Workspace-relative spelling, or None for a path outside the workspace."""
|
|
261
|
+
if path == ALL_PATHS:
|
|
262
|
+
return path
|
|
263
|
+
for root in _CONTAINER_ROOTS:
|
|
264
|
+
if path.startswith(root):
|
|
265
|
+
path = path[len(root):]
|
|
266
|
+
break
|
|
267
|
+
else:
|
|
268
|
+
if path.startswith("/"):
|
|
269
|
+
return None
|
|
270
|
+
path = os.path.normpath(path)
|
|
271
|
+
return None if path.startswith("..") else path
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _as_list(v: Any) -> List[str]:
|
|
275
|
+
if isinstance(v, list):
|
|
276
|
+
return v
|
|
277
|
+
if isinstance(v, str) and v.startswith("["):
|
|
278
|
+
import ast
|
|
279
|
+
try:
|
|
280
|
+
return list(ast.literal_eval(v))
|
|
281
|
+
except (ValueError, SyntaxError):
|
|
282
|
+
return []
|
|
283
|
+
return []
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _live_events(s: Dict[str, Any], scripts: Optional[Dict[str, str]] = None) -> List[Event]:
|
|
287
|
+
cmd = s.get("command") or ""
|
|
288
|
+
try:
|
|
289
|
+
rc = int(s.get("returncode"))
|
|
290
|
+
except (TypeError, ValueError):
|
|
291
|
+
rc = None
|
|
292
|
+
detected = [p for p in (_canon(x) for x in _as_list(s.get("writes_detected"))) if p]
|
|
293
|
+
# mini-SWE-agent runs each command in a fresh `docker exec` at the
|
|
294
|
+
# repository root: a `cd` lasts only for the command that makes it.
|
|
295
|
+
raw_events = shell_events_with_outcome(cmd, str(s.get("observation") or ""), rc, cwd="", scripts=scripts)
|
|
296
|
+
|
|
297
|
+
events: List[Event] = []
|
|
298
|
+
parsed_w: Set[str] = set()
|
|
299
|
+
restore_all = False
|
|
300
|
+
restored_dirs: List[str] = []
|
|
301
|
+
for op, raw, kind, landed in raw_events:
|
|
302
|
+
p = _canon(raw)
|
|
303
|
+
if p is None:
|
|
304
|
+
continue
|
|
305
|
+
src = copy_source(kind)
|
|
306
|
+
if src is not None:
|
|
307
|
+
# A source outside the workspace carries content nothing tracks.
|
|
308
|
+
kind = map_kind_path(kind, _canon) if _canon(src) else "replace"
|
|
309
|
+
if op == "R":
|
|
310
|
+
events.append(("R", p, kind, True))
|
|
311
|
+
elif p == ALL_PATHS:
|
|
312
|
+
restore_all = True
|
|
313
|
+
else:
|
|
314
|
+
parsed_w.add(p)
|
|
315
|
+
if kind == "restore" and landed:
|
|
316
|
+
restored_dirs.append(p)
|
|
317
|
+
# The harness recorded a write it parsed or saw change on disk;
|
|
318
|
+
# a target it did not record did not change.
|
|
319
|
+
events.append(("W", p, kind, landed and p in detected))
|
|
320
|
+
if not raw_events and not cmd.strip():
|
|
321
|
+
return events
|
|
322
|
+
# Changes the command did not name: a restore, a script that named the
|
|
323
|
+
# file, or something else that touched it.
|
|
324
|
+
unparsed: List[Event] = []
|
|
325
|
+
for p in detected:
|
|
326
|
+
if p in parsed_w:
|
|
327
|
+
continue
|
|
328
|
+
# `git checkout -- dir/` names a directory: every file it rewrote
|
|
329
|
+
# under that directory went back to the committed content, not to a
|
|
330
|
+
# new version.
|
|
331
|
+
if restore_all or any(p.startswith(d + "/") for d in restored_dirs):
|
|
332
|
+
unparsed.append(("W", p, "restore", True))
|
|
333
|
+
elif p in cmd or os.path.basename(p) in cmd:
|
|
334
|
+
unparsed.append(("W", p, "anchored", True))
|
|
335
|
+
else:
|
|
336
|
+
unparsed.append(("W", p, "replace", True))
|
|
337
|
+
|
|
338
|
+
if unparsed:
|
|
339
|
+
# Event chronology: unparsed harness-detected writes (e.g. from scripts or
|
|
340
|
+
# build steps) that precede inspection reads in the same turn must be placed
|
|
341
|
+
# before those reads, so the view reflects the landed write.
|
|
342
|
+
read_paths = {p for op, p, _, _ in events if op == "R"}
|
|
343
|
+
if any(p in read_paths for _, p, _, _ in unparsed):
|
|
344
|
+
new_events: List[Event] = []
|
|
345
|
+
unparsed_placed: Set[str] = set()
|
|
346
|
+
for ev in events:
|
|
347
|
+
op, p, kind, landed = ev
|
|
348
|
+
if op == "R" and p in {up[1] for up in unparsed} and p not in unparsed_placed:
|
|
349
|
+
for up in unparsed:
|
|
350
|
+
if up[1] == p:
|
|
351
|
+
new_events.append(up)
|
|
352
|
+
unparsed_placed.add(p)
|
|
353
|
+
new_events.append(ev)
|
|
354
|
+
for up in unparsed:
|
|
355
|
+
if up[1] not in unparsed_placed:
|
|
356
|
+
new_events.append(up)
|
|
357
|
+
events = new_events
|
|
358
|
+
else:
|
|
359
|
+
events.extend(unparsed)
|
|
360
|
+
return events
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
_DEPENDENCY_NOTICE = "\n\n[DEPENDENCY NOTICE:"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _command_output(s: Dict[str, Any]) -> str:
|
|
367
|
+
"""The step's observation without the dependency notice.
|
|
368
|
+
|
|
369
|
+
The notice lists `file:line: text` for uses in other files. Read as
|
|
370
|
+
command output, each line would count as a view of that file, and the
|
|
371
|
+
dependency arm would be credited with views it was only pointed at. The
|
|
372
|
+
write-through viewport and diff stay in: they show the written file itself.
|
|
373
|
+
"""
|
|
374
|
+
obs = str(s.get("observation") or "")
|
|
375
|
+
cut = obs.find(_DEPENDENCY_NOTICE)
|
|
376
|
+
return obs if cut < 0 else obs[:cut]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def live_steps(trace_steps: Sequence[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
380
|
+
"""Replay steps for a trace recorded by the live harness.
|
|
381
|
+
|
|
382
|
+
Events come from the command text. The harness's `writes_detected` (parsed
|
|
383
|
+
targets plus files whose size or modification time changed) decides which
|
|
384
|
+
writes landed, and its `reads_detected` is the record the arms acted on.
|
|
385
|
+
"""
|
|
386
|
+
out = []
|
|
387
|
+
scripts: Dict[str, str] = {}
|
|
388
|
+
for s in trace_steps:
|
|
389
|
+
reads_detected = [p for p in (_canon(x) for x in _as_list(s.get("reads_detected"))) if p]
|
|
390
|
+
out.append({
|
|
391
|
+
"events": _live_events(s, scripts),
|
|
392
|
+
"output_reads": [p for p in (_canon(x) for x in output_views(
|
|
393
|
+
_command_output(s), cwd=command_cwd(str(s.get("command") or s.get("action") or ""), ""))) if p],
|
|
394
|
+
"harness_reads": reads_detected,
|
|
395
|
+
"harness_writes": [p for p in (_canon(x) for x in _as_list(s.get("writes_detected"))) if p],
|
|
396
|
+
"harness_kept": reads_detected[-1] if reads_detected else None,
|
|
397
|
+
"presented": [p for p in (_canon(x) for x in _as_list(s.get("synced_files"))) if p],
|
|
398
|
+
})
|
|
399
|
+
return out
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Vendored from src/parsers by tool/build.sh."""
|