main
py 187 lines 4.54 KB
Raw
1 from __future__ import annotations
2
3 from typing import Any, TypedDict
4
5
6 LOCAL_FRESHNESS_KEY = "_text_editor_mtimes"
7 REMOTE_FRESHNESS_KEY = "_a0_connector_text_editor_remote_mtimes"
8
9
10 class FileMetadata(TypedDict):
11 realpath: str
12 mtime: float | None
13 total_lines: int
14
15
16 def coerce_file_metadata(
17 file_data: Any, *, total_lines: int | None = None
18 ) -> FileMetadata | None:
19 if not isinstance(file_data, dict):
20 return None
21
22 realpath = str(file_data.get("realpath", "")).strip()
23 if not realpath:
24 return None
25
26 try:
27 line_count = (
28 int(total_lines)
29 if total_lines is not None
30 else int(file_data.get("total_lines", 0))
31 )
32 except (TypeError, ValueError):
33 return None
34
35 raw_mtime = file_data.get("mtime")
36 if raw_mtime is None:
37 mtime: float | None = None
38 else:
39 try:
40 mtime = float(raw_mtime)
41 except (TypeError, ValueError):
42 mtime = None
43
44 return FileMetadata(
45 realpath=realpath,
46 mtime=mtime,
47 total_lines=max(line_count, 0),
48 )
49
50
51 def record_file_state(
52 agent,
53 file_data: Any,
54 *,
55 key: str,
56 total_lines: int | None = None,
57 ) -> None:
58 file_meta = coerce_file_metadata(file_data, total_lines=total_lines)
59 if file_meta is None or file_meta["mtime"] is None:
60 return
61
62 freshness = agent.data.setdefault(key, {})
63 freshness[file_meta["realpath"]] = {
64 "mtime": file_meta["mtime"],
65 "total_lines": file_meta["total_lines"],
66 }
67
68
69 def mark_file_state_stale(
70 agent,
71 file_data: Any,
72 *,
73 key: str,
74 total_lines: int | None = None,
75 ) -> None:
76 file_meta = coerce_file_metadata(file_data, total_lines=total_lines)
77 if file_meta is None:
78 return
79
80 freshness = agent.data.setdefault(key, {})
81 freshness[file_meta["realpath"]] = {"mtime": 0, "total_lines": 0}
82
83
84 def check_patch_freshness(agent, file_data: Any, *, key: str) -> str | None:
85 file_meta = coerce_file_metadata(file_data)
86 if file_meta is None:
87 return "patch_need_read"
88
89 freshness = agent.data.get(key, {})
90 realpath = file_meta["realpath"]
91 if realpath not in freshness:
92 return "patch_need_read"
93
94 stored = freshness[realpath]
95 mtime = stored.get("mtime") if isinstance(stored, dict) else stored
96 if mtime is None:
97 freshness.pop(realpath, None)
98 return "patch_need_read"
99
100 current = file_meta["mtime"]
101 if current is None:
102 return None
103 if current != mtime:
104 return "patch_stale_read"
105 return None
106
107
108 def apply_patch_post_state(
109 agent,
110 file_data: Any,
111 edits: list[Any] | None,
112 *,
113 key: str,
114 total_lines: int | None = None,
115 ) -> None:
116 file_meta = coerce_file_metadata(file_data, total_lines=total_lines)
117 if file_meta is None:
118 return
119
120 freshness = agent.data.setdefault(key, {})
121 realpath = file_meta["realpath"]
122
123 if not all_edits_in_place(edits):
124 freshness[realpath] = {"mtime": 0, "total_lines": 0}
125 return
126
127 stored = freshness.get(realpath)
128 if not isinstance(stored, dict) or "total_lines" not in stored:
129 freshness[realpath] = {"mtime": 0, "total_lines": 0}
130 return
131
132 if file_meta["total_lines"] != int(stored["total_lines"]):
133 freshness[realpath] = {"mtime": 0, "total_lines": 0}
134 return
135
136 if file_meta["mtime"] is None:
137 freshness[realpath] = {"mtime": 0, "total_lines": 0}
138 return
139
140 freshness[realpath] = {
141 "mtime": file_meta["mtime"],
142 "total_lines": file_meta["total_lines"],
143 }
144
145
146 def all_edits_in_place(edits: list[Any] | None) -> bool:
147 if not isinstance(edits, list):
148 return False
149
150 for edit in edits:
151 if not isinstance(edit, dict):
152 return False
153 if edit.get("insert"):
154 return False
155
156 try:
157 start = int(edit.get("from", 0) or 0)
158 except (TypeError, ValueError):
159 return False
160 if start < 1:
161 return False
162
163 raw_to = edit.get("to")
164 if raw_to is None:
165 return False
166
167 try:
168 end = int(raw_to)
169 except (TypeError, ValueError):
170 return False
171 if end < start:
172 return False
173
174 removed = end - start + 1
175 added = count_content_lines(edit.get("content"))
176 if removed != added:
177 return False
178
179 return True
180
181
182 def count_content_lines(content: Any) -> int:
183 if not content:
184 return 0
185
186 text = str(content)
187 return text.count("\n") + (1 if not text.endswith("\n") else 0)