Spaces:
Sleeping
Sleeping
File size: 6,510 Bytes
f0347b4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | """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]}")
|