gabriel / muse public
coordination.py python
295 lines 9.3 KB
a748f814 feat(code): Phase 7 — semantic versioning metadata on StructuredDelta +… Gabriel Cardona <cgcardona@gmail.com> 5d ago
1 """Multi-agent coordination layer for the Muse VCS.
2
3 Coordination data lives under ``.muse/coordination/``. It is purely advisory —
4 the VCS engine never reads it for correctness decisions. Its purpose is to
5 enable agents working in parallel to announce their intentions, detect likely
6 conflicts *before* they happen, and plan merges without writing to the repo.
7
8 Layout::
9
10 .muse/coordination/
11 reservations/<uuid>.json advisory symbol lease
12 intents/<uuid>.json declared operation before an edit
13
14 Reservation schema (v1)::
15
16 {
17 "schema_version": 1,
18 "reservation_id": "<uuid>",
19 "run_id": "<agent-supplied ID>",
20 "branch": "<branch name>",
21 "addresses": ["src/billing.py::compute_total", ...],
22 "created_at": "2026-03-18T12:00:00+00:00",
23 "expires_at": "2026-03-18T13:00:00+00:00",
24 "operation": null | "rename" | "move" | "extract" | "modify" | "delete"
25 }
26
27 Intent schema (v1)::
28
29 {
30 "schema_version": 1,
31 "intent_id": "<uuid>",
32 "reservation_id": "<uuid>",
33 "run_id": "<agent-supplied ID>",
34 "branch": "<branch name>",
35 "addresses": ["src/billing.py::compute_total"],
36 "operation": "rename",
37 "created_at": "2026-03-18T12:00:00+00:00",
38 "detail": "rename to compute_invoice_total"
39 }
40
41 All coordination records are write-once, never mutated. Expiry is enforced
42 by ``is_active()`` — expired records are ignored but not deleted (they provide
43 a historical audit trail for the coordination session).
44 """
45 from __future__ import annotations
46
47 import datetime
48 import json
49 import logging
50 import pathlib
51 import uuid as _uuid_mod
52
53 logger = logging.getLogger(__name__)
54
55 _SCHEMA_VERSION = 1
56
57
58 # ---------------------------------------------------------------------------
59 # Directory helpers
60 # ---------------------------------------------------------------------------
61
62
63 def _coord_dir(root: pathlib.Path) -> pathlib.Path:
64 return root / ".muse" / "coordination"
65
66
67 def _reservations_dir(root: pathlib.Path) -> pathlib.Path:
68 return _coord_dir(root) / "reservations"
69
70
71 def _intents_dir(root: pathlib.Path) -> pathlib.Path:
72 return _coord_dir(root) / "intents"
73
74
75 def _ensure_coord_dirs(root: pathlib.Path) -> None:
76 _reservations_dir(root).mkdir(parents=True, exist_ok=True)
77 _intents_dir(root).mkdir(parents=True, exist_ok=True)
78
79
80 def _now_utc() -> datetime.datetime:
81 return datetime.datetime.now(datetime.timezone.utc)
82
83
84 def _parse_dt(s: str) -> datetime.datetime:
85 return datetime.datetime.fromisoformat(s)
86
87
88 # ---------------------------------------------------------------------------
89 # Reservation
90 # ---------------------------------------------------------------------------
91
92
93 class Reservation:
94 """An advisory lock on a set of symbol addresses."""
95
96 def __init__(
97 self,
98 reservation_id: str,
99 run_id: str,
100 branch: str,
101 addresses: list[str],
102 created_at: datetime.datetime,
103 expires_at: datetime.datetime,
104 operation: str | None,
105 ) -> None:
106 self.reservation_id = reservation_id
107 self.run_id = run_id
108 self.branch = branch
109 self.addresses = addresses
110 self.created_at = created_at
111 self.expires_at = expires_at
112 self.operation = operation
113
114 def is_active(self) -> bool:
115 """Return True if this reservation has not yet expired."""
116 return _now_utc() < self.expires_at
117
118 def to_dict(self) -> dict[str, str | int | list[str] | None]:
119 return {
120 "schema_version": _SCHEMA_VERSION,
121 "reservation_id": self.reservation_id,
122 "run_id": self.run_id,
123 "branch": self.branch,
124 "addresses": self.addresses,
125 "created_at": self.created_at.isoformat(),
126 "expires_at": self.expires_at.isoformat(),
127 "operation": self.operation,
128 }
129
130 @classmethod
131 def from_dict(cls, d: dict[str, str | int | list[str] | None]) -> "Reservation":
132 expires_at_raw = d.get("expires_at")
133 created_at_raw = d.get("created_at")
134 expires_at = _parse_dt(str(expires_at_raw)) if expires_at_raw else _now_utc()
135 created_at = _parse_dt(str(created_at_raw)) if created_at_raw else _now_utc()
136 addrs_raw = d.get("addresses", [])
137 addrs = list(addrs_raw) if isinstance(addrs_raw, list) else []
138 op_raw = d.get("operation")
139 return cls(
140 reservation_id=str(d.get("reservation_id", "")),
141 run_id=str(d.get("run_id", "")),
142 branch=str(d.get("branch", "")),
143 addresses=addrs,
144 created_at=created_at,
145 expires_at=expires_at,
146 operation=str(op_raw) if op_raw is not None else None,
147 )
148
149
150 def create_reservation(
151 root: pathlib.Path,
152 run_id: str,
153 branch: str,
154 addresses: list[str],
155 ttl_seconds: int = 3600,
156 operation: str | None = None,
157 ) -> Reservation:
158 """Write and return a new reservation for *addresses*."""
159 _ensure_coord_dirs(root)
160 now = _now_utc()
161 res = Reservation(
162 reservation_id=str(_uuid_mod.uuid4()),
163 run_id=run_id,
164 branch=branch,
165 addresses=addresses,
166 created_at=now,
167 expires_at=now + datetime.timedelta(seconds=ttl_seconds),
168 operation=operation,
169 )
170 path = _reservations_dir(root) / f"{res.reservation_id}.json"
171 path.write_text(json.dumps(res.to_dict(), indent=2) + "\n")
172 logger.debug("✅ Created reservation %s for %d addresses", res.reservation_id[:8], len(addresses))
173 return res
174
175
176 def load_all_reservations(root: pathlib.Path) -> list[Reservation]:
177 """Load all reservation files (including expired ones)."""
178 rdir = _reservations_dir(root)
179 if not rdir.exists():
180 return []
181 reservations: list[Reservation] = []
182 for path in rdir.glob("*.json"):
183 try:
184 raw = json.loads(path.read_text())
185 reservations.append(Reservation.from_dict(raw))
186 except (json.JSONDecodeError, KeyError) as exc:
187 logger.warning("⚠️ Corrupt reservation %s: %s", path.name, exc)
188 return reservations
189
190
191 def active_reservations(root: pathlib.Path) -> list[Reservation]:
192 """Return only non-expired reservations."""
193 return [r for r in load_all_reservations(root) if r.is_active()]
194
195
196 # ---------------------------------------------------------------------------
197 # Intent
198 # ---------------------------------------------------------------------------
199
200
201 class Intent:
202 """A declared operational intent extending a reservation."""
203
204 def __init__(
205 self,
206 intent_id: str,
207 reservation_id: str,
208 run_id: str,
209 branch: str,
210 addresses: list[str],
211 operation: str,
212 created_at: datetime.datetime,
213 detail: str,
214 ) -> None:
215 self.intent_id = intent_id
216 self.reservation_id = reservation_id
217 self.run_id = run_id
218 self.branch = branch
219 self.addresses = addresses
220 self.operation = operation
221 self.created_at = created_at
222 self.detail = detail
223
224 def to_dict(self) -> dict[str, str | int | list[str]]:
225 return {
226 "schema_version": _SCHEMA_VERSION,
227 "intent_id": self.intent_id,
228 "reservation_id": self.reservation_id,
229 "run_id": self.run_id,
230 "branch": self.branch,
231 "addresses": self.addresses,
232 "operation": self.operation,
233 "created_at": self.created_at.isoformat(),
234 "detail": self.detail,
235 }
236
237 @classmethod
238 def from_dict(cls, d: dict[str, str | int | list[str]]) -> "Intent":
239 created_raw = d.get("created_at")
240 created_at = _parse_dt(str(created_raw)) if created_raw else _now_utc()
241 addrs_raw = d.get("addresses", [])
242 addrs = list(addrs_raw) if isinstance(addrs_raw, list) else []
243 return cls(
244 intent_id=str(d.get("intent_id", "")),
245 reservation_id=str(d.get("reservation_id", "")),
246 run_id=str(d.get("run_id", "")),
247 branch=str(d.get("branch", "")),
248 addresses=addrs,
249 operation=str(d.get("operation", "")),
250 created_at=created_at,
251 detail=str(d.get("detail", "")),
252 )
253
254
255 def create_intent(
256 root: pathlib.Path,
257 reservation_id: str,
258 run_id: str,
259 branch: str,
260 addresses: list[str],
261 operation: str,
262 detail: str = "",
263 ) -> Intent:
264 """Write and return a new intent record."""
265 _ensure_coord_dirs(root)
266 now = _now_utc()
267 intent = Intent(
268 intent_id=str(_uuid_mod.uuid4()),
269 reservation_id=reservation_id,
270 run_id=run_id,
271 branch=branch,
272 addresses=addresses,
273 operation=operation,
274 created_at=now,
275 detail=detail,
276 )
277 path = _intents_dir(root) / f"{intent.intent_id}.json"
278 path.write_text(json.dumps(intent.to_dict(), indent=2) + "\n")
279 logger.debug("✅ Created intent %s (%s)", intent.intent_id[:8], operation)
280 return intent
281
282
283 def load_all_intents(root: pathlib.Path) -> list[Intent]:
284 """Load all intent files."""
285 idir = _intents_dir(root)
286 if not idir.exists():
287 return []
288 intents: list[Intent] = []
289 for path in idir.glob("*.json"):
290 try:
291 raw = json.loads(path.read_text())
292 intents.append(Intent.from_dict(raw))
293 except (json.JSONDecodeError, KeyError) as exc:
294 logger.warning("⚠️ Corrupt intent %s: %s", path.name, exc)
295 return intents