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.
Each section shows the model as a schema — associations and attributes with their types (? marks a nullable field; timestamps are omitted) — followed by the behavior worth knowing.
| Model | What it is |
|---|---|
Agent | The addressable actor — the class is the role, the record is the individual agent. Covered in Agents. |
Toolkit | A named, reusable bundle of tools — the only way an agent gets tools. Covered in Toolkits. |
AgentToolkit | The grant attaching one toolkit to one agent, carrying its sender gate and scheduled-runs flag. |
AgentToolkitRule | One allow/deny address pattern on a grant's sender gate. |
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 an agent. |
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-agent allow/deny rule — the runtime layer of the access guardrail. |
EmailDomain | A domain Protege receives mail for, with its DKIM keypair and DNS records. |
Toolkit, AgentToolkit, AgentToolkitRule
The tool-access trio, managed in the dashboard's Toolkits section. The full model, including how per-sender gating behaves mid-thread, is covered in Toolkits.
Protege::Toolkit
name String # unique display name
description Text? # operator-facing blurb
key String? # stable system id ("all_tools" / "default_tools"); nil for yours
member_tool_ids Array<String> # tool ids, reconciled against the live manifest (stale ids skipped)
agent_toolkits [AgentToolkit] # the grants attaching this toolkit to agents
Protege::AgentToolkit
agent Protege::Agent # who holds the grant
toolkit Protege::Toolkit # what it grants
allow_proactive Boolean # tools usable on scheduled (senderless) runs; default false
agent_toolkit_rules [AgentToolkitRule] # the sender gate; no rules = admits every sender
Protege::AgentToolkitRule
agent_toolkit Protege::AgentToolkit
kind :allow | :deny
pattern String # exact address or single-* wildcard, e.g. "*@thescoop.com"Toolkit.all_tools/Toolkit.default_tools— the engine-maintained, undeletable system toolkits, looked up bykey.AgentToolkit#permits?(sender:)— folds the grant's rules into the same policy object access rules use and checks one sender.
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.
Protege::Message
agent Protege::Agent # the agent this mail belongs to
email_thread Protege::EmailThread # the conversation it lives in
inbound_email ActionMailbox::InboundEmail? # raw delivery artifact; inbound only
direction :inbound | :outbound
processing_status :pending | :processing | :processed | :failed | :bounced # inbound only
from_address String # sender, original form preserved
to_addresses Array<String> # To recipients (serialized)
cc_addresses Array<String>? # Cc recipients
bcc_addresses Array<String>? # Bcc recipients
reply_to String?
subject String?
text_body Text? # plain-text part
html_body Text? # HTML part
message_id String # canonical, angle-bracketed
in_reply_to String? # parent Message-ID
references_header String? # normalized References chain
sent_at DateTime
delivered_at DateTime? # outbound: SMTP handoff time
delivery_error Text? # outbound: why delivery failed
read_at DateTime? # dashboard read marker
last_processing_error Text? # inbound: error class, message, and top backtrace frames
correlation_id String? # ties the message to its run's events, logs, and traces
attachments ActiveStorage # has_many_attached
tool_uses [ToolUse] # tool calls recorded while handling this messagereadable_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.recursion_hops— the inboundX-Protege-Recursioncount (0 for human mail); a reply stamps this plus one. See Security.
EmailThread
Groups every Message in one conversation, identified by a canonical thread_id derived from the mail's Message-ID / References chain.
Protege::EmailThread
agent Protege::Agent # the agent whose conversation this is
thread_id String # canonical id derived from the mail headers
subject String? # the thread's display subject
message_count Integer # denormalized, so the inbox sorts without aggregating
last_message_at DateTime?
messages [Message]latest_message— the most recently sent message in the thread.for_inbox(scope) — threads most-recently-active first, with agent and messages eager-loaded.find_or_create_for(mail:, agent:)— 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
An agent's standing, cron-scheduled duty — the record behind the Loop. It's a lean, dashboard-managed record: "which agent, what task, how often." Scout's daily low-stock report is a Responsibility.
Protege::Responsibility
agent Protege::Agent # who carries the duty
name String
instructions Text # the prompt the scheduled run is seeded with
schedule String # 5-field cron, evaluated in the app's Time.zone
active Boolean # default true; inactive duties never fire
last_run_at DateTime?
responsibility_runs [ResponsibilityRun]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_agent(an archived agent'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.
Protege::ResponsibilityRun
responsibility Protege::Responsibility
status :pending | :running | :completed | :failed
correlation_id String? # ties the run to its events, log lines, and traces
started_at DateTime?
finished_at DateTime?
error_class String? # on failure
error_message Text? # on failure
tool_uses [ToolUse] # the tool calls the scheduled run madestart!/mark_completed!/mark_failed!(error)— the lifecycle transitions the job drives; the failure variant records the error class and message.
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.
Protege::ToolUse
agent Protege::Agent
source Message | ResponsibilityRun # polymorphic: the run that made the call
tool_name String # the tool id, e.g. "lookup_order"
arguments String? # the call's input as JSON text, size-capped
result String? # the outcome as JSON text, size-capped
succeeded Boolean
run_id String? # groups the calls of one harness run
turn_index Integer # which tool-loop turn within the run
position Integer # order within the turn (a model may call several tools at once)
tool_call_id String? # the provider's id correlating call to resultto_provider_tool_call/replay_result(limit:)— rebuild the call/result for history replay (replay truncates the result further so past payloads never dominate the context window).in_replay_order(scope) — ordered by run, round, then position within the round.
Trace
A durable, self-contained snapshot of one inference turn — captured only when tracing is enabled. Deliberately isolated: no foreign keys, so the table can be truncated or dropped without touching anything else.
Protege::Trace
correlation_id String? # ties every turn of one run together
turn_index Integer? # which turn of the run this snapshot is
model String? # the model that produced it
request Hash # the exact wire-format request sent (JSON snapshot)
response Hash # the wire-format response produced
settings Hash # the sampling settings in force
label :unlabeled | :good | :bad | :neutral # review verdict
annotation Text? # the reviewer's note
reviewed_at DateTime?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 agent — the dashboard-editable layer of the inbound access guardrail. It only ever narrows the global ceiling.
Protege::AccessRule
agent Protege::Agent
kind :allow | :deny
pattern String # exact address or single-* wildcard, e.g. "*@thescoop.com"policy_for(agent)— folds an agent'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.
Protege::EmailDomain
domain String # e.g. "thescoop.com"
dkim_selector String # DNS selector for the DKIM key; default "default"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.