- 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>
2.3 KiB
2.3 KiB
ADR-003: Event Schema and Core Data Model
Status: Accepted Date: 2026-08-03
Decision
Sources of truth:
db/schema.sql— DDL for the Slice 1 entity subset: company, employee, work_item, event, approval, artifact, tool_execution, cost_entry, policy_rule. Remaining PRD §14.3 entities (Schedule, KnowledgeItem, Incident, ConsultantEngagement, …) are added by the slice that needs them, not up front.engine/schemas/events.py— pydantic models.AgentMessage(agent→engine, enforced at inference via structured output) andEventEnvelope(immutable log record). The pydantic models ARE the schema; no parallel JSON-schema files to drift.
Key choices:
- Append-only event log in Postgres with a trigger rejecting UPDATE/DELETE. Projections (work_item.status, employee.status) are ordinary tables updated in the same transaction as the event insert. No event-sourcing framework.
- Flat agent message schema. One shape for all 12 agent message types, optional fields per type. Benchmarked at 100% validity on qwen3.5:9b under
formatenforcement; nested per-type schemas degrade small models and multiply retry logic. task.blockedis triage (benchmark finding: model confuses blocked vs clarification vs approval). Engine manager step reclassifies before user-facing routing.- Deny-by-default policy table.
policy_rulerows grant max authority per (company, employee, tool-glob, environment); most specific enabled rule wins; no matching rule = denied. Seeded globals: sandbox=2, internal=1, external/production=0. Every tool execution stores the decision it ran under. - Runaway caps live in
work_item.budget(max_iterations 15, max_tokens 100k, max_wall_seconds 1800 default) and are engine-enforced;run.cappedevent fires when tripped (PRD_ADDENDUM §3). - UUIDs everywhere, UTC timestamptz everywhere. Display timezone (
America/Indiana/Indianapolis) is a UI concern only.
Rejected
- Kafka/NATS event bus — Postgres insert + Redis pub/sub notify covers 4-worker scale.
- Full event-sourcing (rebuild state from log) — projections are authoritative for current state; the log is authoritative for history and audit.
- Per-company database — schema-shared, row-scoped by company_id; revisit if isolation demands grow.