SidecarCommando / state_store.py
smlflg's picture
Initial public upload from Projekte/SidecarCommando
a53fc63 verified
Raw History Blame Contribute Delete
3.32 kB
"""Small JSON state store for the AI Cockpit."""
from __future__ import annotations
import copy
import json
import uuid
from datetime import datetime
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo
ROOT = Path(__file__).resolve().parent
DATA_DIR = ROOT / "data"
DEMO_STATE_PATH = DATA_DIR / "demo_state.json"
STATE_PATH = DATA_DIR / "state.json"
EVENTS_PATH = DATA_DIR / "events.jsonl"
REQUIRED_STATE_KEYS = {
"active_focus",
"current_work",
"sidecars",
"next_allowed_step",
"todos",
"open_threads",
}
class StateStore:
def __init__(
self,
state_path: Path = STATE_PATH,
demo_state_path: Path = DEMO_STATE_PATH,
events_path: Path = EVENTS_PATH,
) -> None:
self.state_path = state_path
self.demo_state_path = demo_state_path
self.events_path = events_path
def load_state(self) -> dict[str, Any]:
path = self.state_path if self.state_path.exists() else self.demo_state_path
with path.open("r", encoding="utf-8") as handle:
state = json.load(handle)
validate_state(state, path)
return copy.deepcopy(state)
def save_state(self, state: dict[str, Any]) -> None:
validate_state(state, self.state_path)
self.state_path.parent.mkdir(parents=True, exist_ok=True)
with self.state_path.open("w", encoding="utf-8") as handle:
json.dump(state, handle, ensure_ascii=False, indent=2)
handle.write("\n")
def append_event(self, event: dict[str, Any]) -> dict[str, Any]:
event = copy.deepcopy(event)
event.setdefault("id", str(uuid.uuid4()))
event.setdefault("created_at", datetime.now(ZoneInfo("Europe/Berlin")).isoformat(timespec="seconds"))
event.setdefault("payload", {})
if not isinstance(event.get("type"), str) or not event["type"]:
raise ValueError("Event needs a non-empty string 'type'.")
if not isinstance(event.get("payload"), dict):
raise ValueError("Event 'payload' must be an object.")
self.events_path.parent.mkdir(parents=True, exist_ok=True)
with self.events_path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(event, ensure_ascii=False, sort_keys=True))
handle.write("\n")
return event
def validate_state(state: Any, path: Path) -> None:
if not isinstance(state, dict):
raise ValueError(f"{path} must contain a JSON object.")
missing = sorted(REQUIRED_STATE_KEYS - set(state))
if missing:
raise ValueError(f"{path} is missing required state keys: {', '.join(missing)}")
if not isinstance(state["sidecars"], list) or len(state["sidecars"]) != 5:
raise ValueError(f"{path} must define exactly five sidecars.")
lead_count = sum(1 for sidecar in state["sidecars"] if sidecar.get("is_lead") is True)
if lead_count != 1:
raise ValueError(f"{path} must mark exactly one sidecar as is_lead=true.")
_DEFAULT_STORE = StateStore()
def load_state() -> dict[str, Any]:
return _DEFAULT_STORE.load_state()
def save_state(state: dict[str, Any]) -> None:
_DEFAULT_STORE.save_state(state)
def append_event(event: dict[str, Any]) -> dict[str, Any]:
return _DEFAULT_STORE.append_event(event)