coordination.py
python
| 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 |