Files
FarmHouse-Holdings/engine/schemas/events.py
T
fez5587andClaude Fable 5 ad4f603799 Step 5+6: event schema v1, core DDL, policy table, Slice 1 acceptance demo
- db/schema.sql: 9 core tables, append-only event log (trigger-enforced),
  deny-by-default policy_rule with seeded globals, runaway budgets on work items.
  Validated against Postgres 16 (DDL applies, trigger rejects UPDATE).
- engine/schemas/events.py: AgentMessage (flat, inference-enforced) +
  EventEnvelope pydantic models; models are the schema (ADR-003).
- adr/ADR-003: event schema and data model decisions.
- SLICE1_DEMO.md: verbatim acceptance script for Slice 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 23:26:49 -05:00

109 lines
3.3 KiB
Python

"""Event schema v1 — the versioned contract between agents, engine, and interfaces.
Two layers:
* AgentMessage — what an employee (Hermes run) reports back. Small, flat,
enforced at inference time via Ollama/Hermes structured output
(qwen3.5:9b scored 100% validity on this shape; see benchmarks/RESULTS.md).
* EventEnvelope — what the engine appends to the immutable event log after
policy evaluation. Superset of agent messages: engine/system events too.
Rule from benchmarks: `task.blocked` from an agent is a TRIAGE state. The
engine (manager step) reclassifies to clarification.requested /
approval.requested before anything reaches the user.
"""
from datetime import datetime
from typing import Literal
from uuid import UUID
from pydantic import BaseModel, Field
SCHEMA_VERSION = 1
# ---------------------------------------------------------------- agent -> engine
AgentMessageType = Literal[
"task.accepted",
"task.progressed",
"task.blocked",
"task.completed",
"clarification.requested",
"approval.requested",
"artifact.created",
"test.started",
"test.completed",
"review.completed",
"consultant.requested",
"incident.declared",
]
class AgentMessage(BaseModel):
"""Kept flat on purpose — 9B models degrade on nested schemas (PRD_ADDENDUM §2)."""
message_type: AgentMessageType
work_item_id: str
summary: str = Field(max_length=500)
detail: str = ""
# clarification/approval: 2-3 options with a recommendation (PRD §7.6)
options: list[str] = []
recommended_option: str = ""
# artifact.created / test.completed
artifact_uri: str = ""
tests_passed: int | None = None
tests_failed: int | None = None
# JSON Schema handed to Hermes/Ollama `format` for enforcement at inference time.
AGENT_MESSAGE_FORMAT: dict = AgentMessage.model_json_schema()
# ---------------------------------------------------------------- engine event log
EngineEventType = Literal[
# lifecycle
"company.created", "company.paused", "company.resumed", "company.archived",
"employee.hired", "employee.updated",
"work_item.created", "work_item.updated", "work_item.assigned",
# dispatch loop
"dispatch.sent", "dispatch.failed", "run.capped", # run.capped = runaway guard fired
# decisions
"clarification.answered", "approval.decided", "policy.denied",
# accounting
"cost.recorded",
]
EventType = AgentMessageType | EngineEventType
Source = Literal["engine", "web", "api", "discord", "hermes", "scheduler", "cli"]
class PolicyDecision(BaseModel):
allowed: bool
rule_id: UUID | None = None
level: int
reason: str = ""
class Provenance(BaseModel):
model: str = ""
worker: str = ""
tool: str = ""
hermes_run_id: str = ""
class EventEnvelope(BaseModel):
"""Mirrors db/schema.sql `event` table. Append-only; never mutated."""
event_id: UUID
company_id: UUID | None = None
actor: str # 'employee:<uuid>' | 'user:philip' | 'system'
event_type: EventType
schema_version: int = SCHEMA_VERSION
work_item_id: UUID | None = None
parent_event_id: UUID | None = None
correlation_id: UUID | None = None
source: Source = "engine"
payload: dict = {}
policy_decision: PolicyDecision | None = None
provenance: Provenance | None = None
created_at: datetime