Add canonical event log and renderer skeleton (A10)
Build-order step 4, sequenced before the LLM client (step 3) because the client interface consumes the canonical log. - canonical.py: typed events for the full A10 table (user_message through compaction); canonical tool-call IDs minted locally, provider IDs never a join key; strict payload validation on load - StateStore: canonical_events table via schema v2 — the first real migration, exercised in a test against a hand-built v1 database — with append_event/read_events on the same fsync-per-commit path - render.py: request bodies rendered from the log at call time; first adapter targets the OpenAI-compatible shape of LiteLLM's normalized route; compaction events replace their id range at render time while staying in the durable log - Crash integration test now also verifies transcript appends survive process death Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b05a402c02
commit
ac81dd6234
6 files changed
+501
-2
No files matched your search
@@ -18,14 +18,35 @@ from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Self
|
||||
|
||||
from nisse_dev.canonical import (
|
||||
CanonicalEvent,
|
||||
StoredEvent,
|
||||
event_type,
|
||||
from_payload,
|
||||
to_payload,
|
||||
)
|
||||
from nisse_dev.oplog import OpLog
|
||||
|
||||
CURRENT_SCHEMA_VERSION = 1
|
||||
CURRENT_SCHEMA_VERSION = 2
|
||||
|
||||
Migration = Callable[[sqlite3.Connection], None]
|
||||
|
||||
_CANONICAL_EVENTS_DDL = """
|
||||
CREATE TABLE canonical_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
payload TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def _migrate_v1_add_canonical_events(conn: sqlite3.Connection) -> None:
|
||||
conn.execute(_CANONICAL_EVENTS_DDL)
|
||||
|
||||
|
||||
# Maps version N to the migration that takes the schema from N to N+1.
|
||||
MIGRATIONS: Mapping[int, Migration] = {}
|
||||
MIGRATIONS: Mapping[int, Migration] = {1: _migrate_v1_add_canonical_events}
|
||||
|
||||
|
||||
class StateSchemaError(RuntimeError):
|
||||
@@ -71,6 +92,7 @@ _SCHEMA = [
|
||||
resolved_at TEXT
|
||||
)
|
||||
""",
|
||||
_CANONICAL_EVENTS_DDL,
|
||||
]
|
||||
|
||||
|
||||
@@ -249,6 +271,29 @@ class StateStore:
|
||||
for row in rows
|
||||
]
|
||||
|
||||
def append_event(self, event: CanonicalEvent) -> int:
|
||||
"""Append to the canonical transcript (A10); committed and fsynced
|
||||
before return, like every other durable write."""
|
||||
cursor = self._conn.execute(
|
||||
"INSERT INTO canonical_events (ts, type, payload) VALUES (?, ?, ?)",
|
||||
(_now(), event_type(event), json.dumps(to_payload(event))),
|
||||
)
|
||||
assert cursor.lastrowid is not None
|
||||
return cursor.lastrowid
|
||||
|
||||
def read_events(self, after: int = 0) -> list[StoredEvent]:
|
||||
rows = self._conn.execute(
|
||||
"SELECT * FROM canonical_events WHERE id > ? ORDER BY id", (after,)
|
||||
).fetchall()
|
||||
return [
|
||||
StoredEvent(
|
||||
id=row["id"],
|
||||
ts=row["ts"],
|
||||
event=from_payload(row["type"], json.loads(row["payload"])),
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
def close(self) -> None:
|
||||
self._conn.close()
|
||||
|
||||
|
||||
Reference in new issue
Block a user