Skip to content

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.

ModelWhat it is
PersonaAn agent identity — the class is the role, the record is the agent. Covered in Personas.
MessageOne email, inbound or outbound — the source of truth for everything the agent works from.
EmailThreadA conversation: the messages that belong to one email thread.
ResponsibilityA standing, cron-scheduled duty owned by a persona.
ResponsibilityRunOne execution of a responsibility, with its status and audit trail.
ToolUseOne tool call and its result, recorded during a run.
TraceA durable snapshot of one inference turn, captured when tracing is on.
AccessRuleOne per-persona allow/deny rule — the runtime layer of the access guardrail.
EmailDomainA 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, inbound or outbound.
  • processing_status — an inbound-only state machine: pending → processing → processed, or failed / 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. nil when 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 — the ToolUse rows 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 pending ResponsibilityRun, mints a correlation id, and hands off to the job. This is what the dashboard's "Run now" calls.
  • Scopes active and with_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, or failed.
  • 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: a Message (a reply run) or a ResponsibilityRun (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 recent and reviewed. Search is via TraceSearch.

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, allow or deny.
  • 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.
  • Personas — the Persona model in depth.
  • Tracing — the Trace model and its review flow.
  • Security — how AccessRule composes with the global ceiling.