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,131 @@
|
|
|
1
|
+
"""Observation layer freshness tracking hook for agent scaffolds.
|
|
2
|
+
|
|
3
|
+
Monitors file observations and detects on-disk mutations before prompt re-serialization.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import hashlib
|
|
8
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ObservationBinding:
|
|
14
|
+
"""Binds an observation message index in the conversation history to a file path and hash."""
|
|
15
|
+
message_index: int
|
|
16
|
+
step_id: int
|
|
17
|
+
file_path: str
|
|
18
|
+
content_hash: str
|
|
19
|
+
raw_content: str
|
|
20
|
+
# The file as it stood on disk when it was read. `raw_content` is the whole
|
|
21
|
+
# observation message, which also carries the command's exit code and any
|
|
22
|
+
# surrounding harness text, so it cannot be diffed against the file itself:
|
|
23
|
+
# hunk-level invalidation compared the two and found every line changed,
|
|
24
|
+
# collapsing that arm into whole-observation eviction.
|
|
25
|
+
#
|
|
26
|
+
# None means "never read" (e.g. a binding constructed without going
|
|
27
|
+
# through FreshnessHook.register_read); "" means "read, and the file is
|
|
28
|
+
# genuinely empty." Collapsing these to the same falsy value previously
|
|
29
|
+
# made a real empty file fall back to raw_content, reintroducing the
|
|
30
|
+
# whole-observation-eviction bug for that one case.
|
|
31
|
+
file_content: Optional[str] = None
|
|
32
|
+
is_invalid: bool = False
|
|
33
|
+
invalidation_step: Optional[int] = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FreshnessHook:
|
|
37
|
+
"""Manages file dependency tracking and invalidation detection for agent message history."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, workspace_root: str = ".", use_mtime_cache: bool = True):
|
|
40
|
+
self.workspace_root = os.path.abspath(workspace_root)
|
|
41
|
+
self.use_mtime_cache = use_mtime_cache
|
|
42
|
+
self.bindings: List[ObservationBinding] = []
|
|
43
|
+
self.file_hashes: Dict[str, str] = {}
|
|
44
|
+
# Stores abs_path -> (mtime, size, sha256_hash)
|
|
45
|
+
self.stat_cache: Dict[str, Tuple[float, int, str]] = {}
|
|
46
|
+
|
|
47
|
+
def _hash_bytes(self, abs_path: str) -> str:
|
|
48
|
+
"""Chunked SHA-256 read of the file at abs_path, truncated to 16 hex chars."""
|
|
49
|
+
hasher = hashlib.sha256()
|
|
50
|
+
with open(abs_path, "rb") as f:
|
|
51
|
+
while chunk := f.read(65536):
|
|
52
|
+
hasher.update(chunk)
|
|
53
|
+
return hasher.hexdigest()[:16]
|
|
54
|
+
|
|
55
|
+
def compute_file_hash(self, file_path: str) -> Optional[str]:
|
|
56
|
+
"""Compute real SHA256 hash of file on disk with mtime gating, or simulated hash."""
|
|
57
|
+
abs_path = os.path.join(self.workspace_root, file_path) if not os.path.isabs(file_path) else file_path
|
|
58
|
+
if os.path.exists(abs_path) and os.path.isfile(abs_path):
|
|
59
|
+
try:
|
|
60
|
+
if self.use_mtime_cache:
|
|
61
|
+
st = os.stat(abs_path)
|
|
62
|
+
cached = self.stat_cache.get(abs_path)
|
|
63
|
+
if cached and cached[0] == st.st_mtime and cached[1] == st.st_size:
|
|
64
|
+
return cached[2]
|
|
65
|
+
|
|
66
|
+
h = self._hash_bytes(abs_path)
|
|
67
|
+
self.stat_cache[abs_path] = (st.st_mtime, st.st_size, h)
|
|
68
|
+
return h
|
|
69
|
+
else:
|
|
70
|
+
return self._hash_bytes(abs_path)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
return self.file_hashes.get(file_path)
|
|
74
|
+
|
|
75
|
+
def _read_file(self, file_path: str) -> Optional[str]:
|
|
76
|
+
"""The file's current text, or None if it cannot be read.
|
|
77
|
+
|
|
78
|
+
A genuinely empty file returns "", distinct from None -- callers that
|
|
79
|
+
need to tell "read, and empty" apart from "never read" depend on that.
|
|
80
|
+
"""
|
|
81
|
+
abs_path = os.path.join(self.workspace_root, file_path) if not os.path.isabs(file_path) else file_path
|
|
82
|
+
try:
|
|
83
|
+
with open(abs_path, "r", encoding="utf-8", errors="replace") as f:
|
|
84
|
+
return f.read()
|
|
85
|
+
except OSError:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def register_read(self, msg_idx: int, step_id: int, file_path: str, content: str) -> ObservationBinding:
|
|
89
|
+
"""Register a new observation buffer for a read file."""
|
|
90
|
+
h = self.compute_file_hash(file_path)
|
|
91
|
+
if not h:
|
|
92
|
+
h = hashlib.sha256(content.encode("utf-8")).hexdigest()[:16]
|
|
93
|
+
self.file_hashes[file_path] = h
|
|
94
|
+
|
|
95
|
+
binding = ObservationBinding(
|
|
96
|
+
message_index=msg_idx,
|
|
97
|
+
step_id=step_id,
|
|
98
|
+
file_path=file_path,
|
|
99
|
+
content_hash=h,
|
|
100
|
+
raw_content=content,
|
|
101
|
+
file_content=self._read_file(file_path),
|
|
102
|
+
is_invalid=False
|
|
103
|
+
)
|
|
104
|
+
self.bindings.append(binding)
|
|
105
|
+
return binding
|
|
106
|
+
|
|
107
|
+
def register_write(self, step_id: int, file_path: str, new_content: str = "") -> List[ObservationBinding]:
|
|
108
|
+
"""Record file modification and invalidate all prior observation bindings for this file."""
|
|
109
|
+
# Evict from stat cache to force SHA-256 recomputation and prevent sub-millisecond mtime cache collisions
|
|
110
|
+
abs_path = os.path.join(self.workspace_root, file_path) if not os.path.isabs(file_path) else file_path
|
|
111
|
+
self.stat_cache.pop(abs_path, None)
|
|
112
|
+
|
|
113
|
+
# Update current hash
|
|
114
|
+
new_h = self.compute_file_hash(file_path)
|
|
115
|
+
if not new_h:
|
|
116
|
+
hasher = hashlib.sha256()
|
|
117
|
+
hasher.update(f"{file_path}::step{step_id}::{new_content}".encode("utf-8"))
|
|
118
|
+
new_h = hasher.hexdigest()[:16]
|
|
119
|
+
self.file_hashes[file_path] = new_h
|
|
120
|
+
|
|
121
|
+
invalidated = []
|
|
122
|
+
for b in self.bindings:
|
|
123
|
+
if b.file_path == file_path and not b.is_invalid:
|
|
124
|
+
b.is_invalid = True
|
|
125
|
+
b.invalidation_step = step_id
|
|
126
|
+
invalidated.append(b)
|
|
127
|
+
return invalidated
|
|
128
|
+
|
|
129
|
+
def get_stale_bindings(self) -> List[ObservationBinding]:
|
|
130
|
+
"""Return all observation bindings that have been invalidated by subsequent writes."""
|
|
131
|
+
return [b for b in self.bindings if b.is_invalid]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Which written files need no write-through block.
|
|
2
|
+
|
|
3
|
+
A block costs tokens in every later prompt, so it should appear only when it
|
|
4
|
+
tells the agent something it cannot already know. Three cases do not:
|
|
5
|
+
|
|
6
|
+
* a whole-file write whose content the agent supplied (a heredoc or echo into
|
|
7
|
+
the file): it typed every byte;
|
|
8
|
+
* a file the same command printed after its last write to it: the output
|
|
9
|
+
already shows the result;
|
|
10
|
+
* the submission's own patch.txt.
|
|
11
|
+
|
|
12
|
+
The rules match the replay's (src/metrics/replay.py): an overwrite leaves
|
|
13
|
+
known content and a print is a view, so skipping the block never turns a
|
|
14
|
+
fresh edit into a stale one in the score.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import Dict, Iterable, Optional, Set
|
|
18
|
+
|
|
19
|
+
from src.metrics.replay import _live_events
|
|
20
|
+
|
|
21
|
+
SUBMISSION_FILES = {"patch.txt"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def redundant_writes(command: str, observation: str, writes: Iterable[str],
|
|
25
|
+
scripts: Optional[Dict[str, str]] = None) -> Set[str]:
|
|
26
|
+
"""The written files whose block would repeat what the agent already has."""
|
|
27
|
+
writes = list(writes)
|
|
28
|
+
events = _live_events({"command": command, "observation": observation, "writes_detected": writes},
|
|
29
|
+
scripts if scripts is not None else {})
|
|
30
|
+
out: Set[str] = set()
|
|
31
|
+
for w in writes:
|
|
32
|
+
if w.rsplit("/", 1)[-1] in SUBMISSION_FILES:
|
|
33
|
+
out.add(w)
|
|
34
|
+
continue
|
|
35
|
+
mine = [(op, kind) for op, path, kind, landed in events if path == w]
|
|
36
|
+
kinds = [kind for op, kind in mine if op == "W"]
|
|
37
|
+
if kinds and all(k == "overwrite" for k in kinds):
|
|
38
|
+
out.add(w)
|
|
39
|
+
elif mine and mine[-1][0] == "R" and any(op == "W" for op, _ in mine):
|
|
40
|
+
out.add(w) # printed after its last write
|
|
41
|
+
return out
|