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.

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.

ModelWhat it is
AgentThe addressable actor — the class is the role, the record is the individual agent. Covered in Agents.
ToolkitA named, reusable bundle of tools — the only way an agent gets tools. Covered in Toolkits.
AgentToolkitThe grant attaching one toolkit to one agent, carrying its sender gate and scheduled-runs flag.
AgentToolkitRuleOne allow/deny address pattern on a grant's sender gate.
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 an agent.
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-agent allow/deny rule — the runtime layer of the access guardrail.
EmailDomainA 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.

rb
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 by key.
  • 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.

rb
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 message
  • 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.
  • recursion_hops — the inbound X-Protege-Recursion count (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.

rb
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.

rb
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 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_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.

rb
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 made
  • start! / 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.

rb
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 result
  • to_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.

rb
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 recent and reviewed. Search is via TraceSearch.

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.

rb
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.

rb
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.
  • Agents — the Agent model in depth.
  • Toolkits — the tool-access trio in depth.
  • Tracing — the Trace model and its review flow.
  • Security — how AccessRule composes with the global ceiling.