Abhed — Data Model & API

Status: Draft · 2026-09-02 · Evidence status: [E], structured by P6 (event sourcing).

The event stream is the source of truth. Sessions, audit logs, replay, and the eval harness are all projections of it. Get this right and audit/replay/eval come nearly free; get it wrong and you will retrofit them expensively.

1. Event model

Every state change is an immutable, ordered event.

type Event struct {
    ID        string    // ULID — sortable by time
    SessionID string
    ParentID  string    // subagent parent, empty for root
    Seq       int64     // monotonic within session
    Type      EventType
    Payload   json.RawMessage
    Actor     Actor     // user | agent | system | tool
    Trust     Trust     // trusted | untrusted  ← provenance (arch §6)
    CreatedAt time.Time
}

Trust is the field that makes injection defense enforceable: content from files, tool output, MCP responses, and search results is tagged untrusted at ingest and stays tagged. Policy reads it; the context assembler renders it in a distinct structural block.

Event types

TypePayloadEmitted by
session.startedworkspace, model, mode, prompt hashsystem
user.messagetext, attachmentsuser
agent.messagetext, reasoning (stripped from history)agent
action.requestedtool, argsagent
action.approved / .deniedrule matched, actorpolicy
observationresult, truncated, exit codetool
subagent.spawned / .returnedprompt, summary, tokensorchestrator
compaction.started / .completedbefore/after tokens, summarycontext mgr
plan.updated / todo.updateditemsagent
session.endedterminal reason, totalssystem

Terminal reasons are an enum, not a string — CI exit codes and the eval harness both depend on distinguishing them (§09): completed · max_turns · max_budget · policy_denied · user_interrupt · error · shutdown · retry_exhausted

2. Schema

CREATE TABLE sessions (
  id             TEXT PRIMARY KEY,
  tenant_id      TEXT NOT NULL,
  user_id        TEXT NOT NULL,
  workspace      TEXT NOT NULL,
  model          TEXT NOT NULL,
  prompt_hash    TEXT NOT NULL,        -- traces results to exact prompt (§07)
  harness_version TEXT NOT NULL,
  mode           TEXT NOT NULL,
  parent_id      TEXT REFERENCES sessions(id),   -- subagents
  started_at     TIMESTAMPTZ NOT NULL,
  ended_at       TIMESTAMPTZ,
  terminal_reason TEXT,
  tokens_in      BIGINT DEFAULT 0,
  tokens_out     BIGINT DEFAULT 0,
  tokens_cached  BIGINT DEFAULT 0,     -- cache hit rate (P8)
  gpu_seconds    NUMERIC DEFAULT 0,
  cost_usd       NUMERIC DEFAULT 0
);

CREATE TABLE events (
  id          TEXT PRIMARY KEY,
  session_id  TEXT NOT NULL REFERENCES sessions(id),
  parent_id   TEXT,
  seq         BIGINT NOT NULL,
  type        TEXT NOT NULL,
  payload     JSONB NOT NULL,
  actor       TEXT NOT NULL,
  trust       TEXT NOT NULL DEFAULT 'trusted',
  created_at  TIMESTAMPTZ NOT NULL,
  UNIQUE (session_id, seq)
);
CREATE INDEX ON events (session_id, seq);
CREATE INDEX ON events (type, created_at);
CREATE INDEX ON events USING GIN (payload);

CREATE TABLE checkpoints (          -- backs /undo (§09)
  id         TEXT PRIMARY KEY,
  session_id TEXT NOT NULL REFERENCES sessions(id),
  event_seq  BIGINT NOT NULL,
  path       TEXT NOT NULL,
  before     BYTEA,                 -- NULL = file did not exist
  created_at TIMESTAMPTZ NOT NULL
);

CREATE TABLE models (               -- registry + capability profile (§08 L2)
  name         TEXT PRIMARY KEY,
  provider     TEXT NOT NULL,
  endpoint     TEXT NOT NULL,
  context_window INT NOT NULL,
  profile      JSONB NOT NULL,      -- conformance results
  enabled      BOOLEAN DEFAULT false,
  registered_at TIMESTAMPTZ NOT NULL
);

Events are append-only. No UPDATE, no DELETE. Retention is by partition drop, which keeps the audit guarantee intact — an audit log you can edit is not an audit log.

Tenant isolation is enforced in the query layer and by row-level security. A boundary that exists in only one place is not a boundary (§ops).

3. Session protocol

CLI and web console both consume the same stream over SSE. One implementation.

POST /v1/sessions                    → {session_id}
POST /v1/sessions/{id}/messages      → 202, events stream
GET  /v1/sessions/{id}/events        → SSE (resumable via Last-Event-ID)
POST /v1/sessions/{id}/interrupt     → 204
POST /v1/sessions/{id}/approve       → {event_id, decision, scope}
POST /v1/sessions/{id}/compact       → 202
GET  /v1/sessions/{id}/replay        → full event list
DELETE /v1/sessions/{id}             → end session

Wire format:

{"id":"01J...","seq":42,"type":"action.requested","actor":"agent","trust":"trusted",
 "payload":{"tool":"edit","args":{"path":"/w/auth.go","old_string":"...","new_string":"..."},
            "requires_approval":true,"reason":"mutating tool in default mode"}}

Last-Event-ID resumption is what makes /resume and reconnect-after-network-drop work.

4. Model adapter interface

The single seam that makes Abhed model-agnostic (arch §5, P12).

type Adapter interface {
    Complete(ctx context.Context, req Request) (<-chan Chunk, error)
    Capabilities() Profile
    CountTokens(msgs []Message) (int, error)
}

type Request struct {
    System    []Block          // cached prefix (§07 layers 1-4)
    Messages  []Message
    Tools     []ToolDef
    MaxTokens int
    Effort    EffortLevel      // per-model, per-task-class (P11)
    Stop      []string
}

type Profile struct {
    ContextWindow    int
    SupportsTools    bool
    SupportsStreaming bool
    ToolCallFormat   string   // json | xml | pythonic | harmony
    ReasoningTokens  bool
    GuidedDecoding   bool
    CachePrefix      bool     // if false, expect the 17x penalty (P8)
    Conformance      map[string]float64  // §08 L2 results
}

Everything model-specific lives behind this interface — tool-call parsing, reasoning-token stripping, chat templates, structured-output strategy. Per §07 anti-patterns, if you find yourself forking the prompt per model, the difference belongs here instead.

5. Configuration

# ~/.abhed/config.yaml  or  /etc/abhed/config.yaml (managed, wins)
model:
  default: local-qwen
  providers:
    local-qwen:
      type: openai-compatible          # works with vLLM, SGLang, llama.cpp, Ollama...
      base_url: http://localhost:8000/v1
      model: Qwen/Qwen3-32B
      api_key_env: ABHED_API_KEY
      context_window: 131072
      tool_call_format: json
permissions:
  mode: default
  deny:  ["bash(rm -rf *)", "bash(git push --force*)", "write(/etc/**)"]
  allow: ["bash(git status)", "bash(go test*)", "read(**)"]
context:
  compact_at: 0.90
  memory_files: [ABHED.md, ABHED.local.md]
limits:
  max_turns: 100
  max_subagents: 20
  nested_subagents: false

Precedence: managed (/etc/abhed) → project → user → flags. Managed always wins — that asymmetry is what makes org policy enforceable (P7).

The openai-compatible provider type is deliberately the primary path: it covers vLLM, SGLang, TensorRT-LLM's OpenAI server, llama.cpp, Ollama, and every hosted API worth supporting. One adapter, many backends.