feat(registry): añade FileSystemAgentRegistry con versionado tipo Git y diff

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Juan
2026-05-10 10:39:50 +02:00
co-authored by Claude Opus 4.7
parent 85906de832
commit 5cbec0d5d8
5 changed files with 256 additions and 0 deletions
@@ -0,0 +1,126 @@
"""AgentRegistry basado en sistema de ficheros (YAML versionado, simulación tipo Git)."""
from __future__ import annotations
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import yaml
from agentforge_core.domain.agent import AgentDefinition, AgentVersionMeta
from agentforge_core.registry.versioning import DiffResult, compute_hash, unified_diff
class FileSystemAgentRegistry:
"""Lee/escribe agentes en agents/<name>/{index.yaml,versions/vN.yaml}."""
def __init__(self, root: Path) -> None:
self._root = Path(root)
def list_agents(self) -> list[AgentDefinition]:
if not self._root.exists():
return []
agents: list[AgentDefinition] = []
for d in sorted(self._root.iterdir()):
if d.is_dir() and (d / "index.yaml").exists():
agents.append(self.get_agent(d.name))
return agents
def get_agent(self, name: str, version: str | None = None) -> AgentDefinition:
index = self._load_index(name)
v = version or index["active_version"]
return self.get_version(name, v)
def get_version(self, name: str, version: str) -> AgentDefinition:
path = self._root / name / "versions" / f"{version}.yaml"
if not path.exists():
raise FileNotFoundError(f"agent version not found: {name}@{version}")
with path.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
return AgentDefinition.model_validate(data)
def list_versions(self, name: str) -> list[AgentVersionMeta]:
index = self._load_index(name)
return [
AgentVersionMeta(
id=v["id"],
hash=v["hash"],
author=v["author"],
message=v["message"],
created_at=self._parse_dt(v["created_at"]),
)
for v in index["versions"]
]
def upsert_version(
self,
name: str,
body: AgentDefinition,
message: str,
author: str,
) -> AgentVersionMeta:
agent_dir = self._root / name
versions_dir = agent_dir / "versions"
versions_dir.mkdir(parents=True, exist_ok=True)
version_id = body.version
yaml_text = yaml.safe_dump(
body.model_dump(mode="json"),
sort_keys=False,
allow_unicode=True,
)
h = compute_hash(yaml_text)
version_path = versions_dir / f"{version_id}.yaml"
version_path.write_text(yaml_text, encoding="utf-8")
meta = AgentVersionMeta(
id=version_id,
hash=h,
author=author,
message=message,
created_at=datetime.now(UTC),
)
index_path = agent_dir / "index.yaml"
if index_path.exists():
with index_path.open(encoding="utf-8") as f:
index: dict[str, Any] = yaml.safe_load(f)
else:
index = {"name": name, "versions": [], "active_version": version_id}
# reemplaza si existía la versión, si no añade
index["versions"] = [v for v in index["versions"] if v["id"] != version_id]
index["versions"].append(meta.model_dump(mode="json"))
if body.state == "active":
index["active_version"] = version_id
with index_path.open("w", encoding="utf-8") as f:
yaml.safe_dump(index, f, sort_keys=False, allow_unicode=True)
return meta
def diff_versions(self, name: str, v1: str, v2: str) -> DiffResult:
path_a = self._root / name / "versions" / f"{v1}.yaml"
path_b = self._root / name / "versions" / f"{v2}.yaml"
text_a = path_a.read_text(encoding="utf-8") if path_a.exists() else ""
text_b = path_b.read_text(encoding="utf-8") if path_b.exists() else ""
return DiffResult(
from_version=v1,
to_version=v2,
unified_diff=unified_diff(text_a, text_b, f"{name}@{v1}", f"{name}@{v2}"),
)
def _load_index(self, name: str) -> dict[str, Any]:
index_path = self._root / name / "index.yaml"
if not index_path.exists():
raise FileNotFoundError(f"agent not found: {name}")
with index_path.open(encoding="utf-8") as f:
data: dict[str, Any] = yaml.safe_load(f)
return data
@staticmethod
def _parse_dt(value: str | datetime) -> datetime:
if isinstance(value, datetime):
return value
return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
@@ -0,0 +1,34 @@
"""Helpers de versionado: hash de contenido, diff unified entre versiones."""
from __future__ import annotations
import difflib
import hashlib
from pydantic import BaseModel
class DiffResult(BaseModel):
"""Resultado de comparar dos versiones (texto YAML) de un agente o política."""
from_version: str
to_version: str
unified_diff: str
def compute_hash(yaml_text: str) -> str:
"""Hash SHA-256 del contenido YAML normalizado (espacios al final eliminados)."""
normalized = "\n".join(line.rstrip() for line in yaml_text.splitlines())
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
def unified_diff(text_a: str, text_b: str, label_a: str, label_b: str) -> str:
"""Devuelve diff unificado entre dos textos."""
return "".join(
difflib.unified_diff(
text_a.splitlines(keepends=True),
text_b.splitlines(keepends=True),
fromfile=label_a,
tofile=label_b,
)
)