daimon / engine /spec_bridge.py
davidquicast's picture
chore: initial commit
f0347b4
Raw
History Blame Contribute Delete
6.51 kB
"""Pure-Python spec engine for personaxis.md personas (F1).
The living loop (F2) must never write state.json directly. Every mutation
goes through `mutate()` here: it reads the declared envelope (range) for a
field straight from <slug>/personaxis.md, clamps the requested delta to that
envelope, appends an audit-log entry, and writes <slug>/state.json. There is
no external CLI or subprocess involved - personaxis.md (the spec) and
state.json (the runtime values) are just YAML/JSON files this module reads
and writes directly.
Usage:
from engine.spec_bridge import mutate, validate, get_state
result = mutate("daimon", "mood.tone", 0.05, reason="user seemed pleased")
state = get_state("daimon")
"""
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Literal
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
PERSONAS_DIR = REPO_ROOT / ".personaxis" / "personas"
Actor = Literal[
"actor-llm",
"runtime-decay",
"runtime-context",
"human-operator",
"judge-correction",
]
# field prefix -> path of dict keys to reach {mean, range} in personaxis.md
_ENVELOPE_PATH: dict[str, tuple[str, ...]] = {
"traits": ("personality", "traits"),
"affect": ("affect", "baseline", "core_affect"),
"mood": ("affect", "baseline", "mood"),
}
class SpecBridgeError(RuntimeError):
"""Raised when a mutation targets a field with no declared envelope, or
when personaxis.md / state.json cannot be found or parsed."""
def _persona_md_path(slug: str) -> Path:
path = PERSONAS_DIR / slug / "personaxis.md"
if not path.exists():
raise SpecBridgeError(f"No personaxis.md for persona '{slug}' at {path}")
return path
def _state_path(slug: str) -> Path:
path = PERSONAS_DIR / slug / "state.json"
if not path.exists():
raise SpecBridgeError(f"No state.json for persona '{slug}' at {path}")
return path
def _load_spec(slug: str) -> dict[str, Any]:
text = _persona_md_path(slug).read_text(encoding="utf-8")
_, frontmatter, _ = text.split("---", 2)
return yaml.safe_load(frontmatter)
def _envelope(spec: dict[str, Any], field: str) -> tuple[float, float] | None:
"""Return the declared (lo, hi) range for `field` (e.g. "mood.tone"), or
None if `field` has no envelope in personaxis.md (personality/affect
layers only - identity, character, etc. are not reachable here)."""
layer, _, name = field.partition(".")
path = _ENVELOPE_PATH.get(layer)
if path is None:
return None
node: Any = spec
for key in path:
node = node.get(key, {})
spec_field = node.get(name)
if not spec_field or "range" not in spec_field:
return None
lo, hi = spec_field["range"]
return float(lo), float(hi)
def mutate(
slug: str,
field: str,
delta: float,
*,
reason: str,
actor: Actor = "actor-llm",
) -> dict[str, Any]:
"""Apply a clamped, audited mutation to <slug>/state.json.
Returns {"field", "from", "to", "clamped", "blocked", "raw"}.
Raises SpecBridgeError if `field` has no declared envelope in
personaxis.md (structural rejection - e.g. identity.*, character.*).
"""
spec = _load_spec(slug)
envelope = _envelope(spec, field)
if envelope is None:
raise SpecBridgeError(
f"No envelope declared for '{field}' in {slug}'s personaxis.md "
"(only personality.traits.*, affect.baseline.core_affect.* and "
"affect.baseline.mood.* are mutable) - mutation refused."
)
lo, hi = envelope
state_path = _state_path(slug)
state = json.loads(state_path.read_text(encoding="utf-8"))
if field not in state["values"]:
raise SpecBridgeError(f"'{field}' has a declared envelope but no entry in {slug}'s state.json values.")
before = float(state["values"][field])
requested = before + delta
after = min(max(requested, lo), hi)
clamped = abs(after - requested) > 1e-12
state["values"][field] = after
state["mutation_log"].append(
{
"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.") + f"{datetime.now(timezone.utc).microsecond // 1000:03d}Z",
"field": field,
"from": before,
"to": after,
"delta_requested": delta,
"clamped": clamped,
"reason": reason,
"actor": actor,
"governance_blocked": False,
}
)
state_path.write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8")
raw = f"ok {field}: {before} -> {after}"
if clamped:
raw += f" (clamped to [{lo}, {hi}])"
return {"field": field, "from": before, "to": after, "clamped": clamped, "blocked": False, "raw": raw}
def validate(slug: str) -> dict[str, Any]:
"""Check that every value in <slug>/state.json sits within its declared
envelope. Returns {"ok", "status", "raw"}."""
spec = _load_spec(slug)
state = get_state(slug)
violations = []
for field, value in state["values"].items():
envelope = _envelope(spec, field)
if envelope is None:
continue
lo, hi = envelope
if not (lo - 1e-9 <= value <= hi + 1e-9):
violations.append(f"{field}={value} outside [{lo}, {hi}]")
if violations:
return {"ok": False, "status": "FAIL", "raw": "FAIL: " + "; ".join(violations)}
return {"ok": True, "status": "PASS", "raw": "PASS: all values within declared envelopes"}
def get_state(slug: str) -> dict[str, Any]:
"""Read the current <slug>/state.json (values, mutation_log, etc.)."""
return json.loads(_state_path(slug).read_text(encoding="utf-8"))
if __name__ == "__main__":
import sys
sys.stdout.reconfigure(encoding="utf-8")
# Smoke test for Gate G1: a clamped mutation + audit log entry, end-to-end from Python.
slug = "daimon"
print(f"validate({slug}) ->", validate(slug))
before = get_state(slug)["values"]["mood.tone"]
print(f"mood.tone before: {before}")
result = mutate(
slug,
"mood.tone",
1.0,
reason="smoke test: large positive delta should clamp to range max",
actor="actor-llm",
)
print("mutate ->", result)
after = get_state(slug)
print(f"mood.tone after: {after['values']['mood.tone']}")
print(f"mutation_log entries: {len(after['mutation_log'])}")
print(f"last entry: {after['mutation_log'][-1]}")