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,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