Models
Protege persists everything an agent does as ordinary ActiveRecord models under the Protege:: namespace. You rarely construct them by hand — the engine does — but you'll read them constantly: in the dashboard, in a resolver or tool, or when building a report on your agents. This page covers the models a host app actually touches and the parts of their API that matter to you; pure internals are left out.
| Model | What it is |
|---|---|
Persona | An agent identity — the class is the role, the record is the agent. Covered in Personas. |
Message | One email, inbound or outbound — the source of truth for everything the agent works from. |
EmailThread | A conversation: the messages that belong to one email thread. |
Responsibility | A standing, cron-scheduled duty owned by a persona. |
ResponsibilityRun | One execution of a responsibility, with its status and audit trail. |
ToolUse | One tool call and its result, recorded during a run. |
Trace | A durable snapshot of one inference turn, captured when tracing is on. |
AccessRule | One per-persona allow/deny rule — the runtime layer of the access guardrail. |
EmailDomain | A domain Protege receives mail for, with its DKIM keypair and DNS records. |
Message
The record of a single email — the application's source of truth for everything the engine works from. The dashboard inbox, and the ThreadHistoryResolver, both read from here. Every message belongs to one EmailThread and one Persona.
direction— an enum,inboundoroutbound.processing_status— an inbound-only state machine:pending → processing → processed, orfailed/bounced. Outbound messages are terminal and carry no status.readable_body— the best plain-text content for display or replay: the text body, falling back to the HTML body with tags stripped.nilwhen the message has neither.content_parts— the message as provider content for the opening user turn: plain text when there are no attachments, otherwise the body plus a vision part per viewable attachment and a summary note.attachments— an Active Storage attachment collection (has_many_attached).tool_uses— theToolUserows recorded while the agent handled this message.
EmailThread
Groups every Message in one conversation, identified by a canonical thread_id derived from the mail's Message-ID / References chain. A thread belongs to one persona and carries denormalized counters (message_count, last_message_at) so the inbox sorts without aggregating on every render.
latest_message— the most recently sent message in the thread.for_inbox(scope) — threads most-recently-active first, with persona and messages eager-loaded.find_or_create_for(mail:, persona:)— resolve (or start) the thread for a piece of mail; used on both ingress and delivery, which is how a reply lands on the same thread as the message it answers.
Responsibility
A persona's standing, cron-scheduled duty — the record behind the Loop. It's a lean, dashboard-managed record (not a subclass): "which persona, what task, how often." Scout's daily low-stock report is a Responsibility.
name,instructions(the prompt the scheduled run is seeded with),schedule(a 5-field cron string),active.due?(time)— whether it should fire at a given minute (active and its cron matches).dispatch!— enqueue a run: records a pendingResponsibilityRun, mints a correlation id, and hands off to the job. This is what the dashboard's "Run now" calls.- Scopes
activeandwith_active_persona(an archived persona's duties don't fire).
ResponsibilityRun
One execution of a Responsibility — its run-state record and audit trail. The dashboard reads these to show a duty's recent history.
status— an enum:pending → running → completed, orfailed.start!/mark_completed!/mark_failed!(error)— the lifecycle transitions the job drives; the failure variant records the error class and message.tool_uses— the tool calls the scheduled run made, for audit beside the status.
ToolUse
One tool call the agent made and the result it got back, recorded during a run so a run's tool activity is durable. It's what lets the ThreadHistoryResolver replay an agent's earlier tool use on later turns, and what gives a scheduled run an audit trail.
source— polymorphic: aMessage(a reply run) or aResponsibilityRun(a scheduled run).tool_name,arguments,result,succeeded— the call and its outcome (payloads are size-capped so a large result can't bloat the row).to_provider_tool_call/replay_result(limit:)— rebuild the call/result for history replay.in_replay_order(scope) — ordered by run, round, then position within the round.
Trace
A durable, self-contained snapshot of one inference turn — the exact request as sent, the response it produced, and the model/settings that shaped it — captured only when tracing is enabled. Deliberately isolated: no foreign keys, so the table can be truncated or dropped without touching anything else.
request,response,settings— JSON snapshots of the turn.label— a review verdict enum:unlabeled → good/bad/neutral.apply_label(label:, annotation:)— record a human review verdict and note.- Scopes
recentandreviewed. Search is viaTraceSearch.
AccessRule
One runtime allow/deny rule for a single persona — the dashboard-editable layer of the inbound access guardrail. It only ever narrows the global ceiling.
kind— an enum,allowordeny.pattern— an exact address or a single*wildcard (e.g.*@thescoop.com).policy_for(persona)— folds a persona's rules into a single access policy.
EmailDomain
A domain Protege is configured to receive mail for. Each domain owns a 2048-bit RSA keypair, generated on create, and surfaces the DNS records an operator must publish. See Mail.
receives?(address)— whether Protege receives mail for an address's domain; drives inbound routing (until a domain is added, nothing routes).mx_record,spf_txt_record,dkim_txt_record,dmarc_txt_record,dkim_dns_name— the DNS record values to publish.

