Architecture
Protege is organized around the LOGI pattern — Loop, Orchestrator, Gateway, Inference — running entirely inside your Rails app. There is no separate agent process, no external runtime: an inbound email is an Action Mailbox delivery, a reply is an SMTP send, and everything in between is plain Ruby and Active Job.
This section is the internals tour. If you only want to build on Protege, the extensions and reference sections are enough — you never have to open the engine. Read on when you want to know how the pieces fit, or when you are debugging a run and need to know exactly which layer owns what.
Request lifecycle
Here is a full round-trip for The Scoop, our running example — an ice-cream company running customer service on Protege. A customer emails support@thescoop.com asking where their order is; the Sundae persona looks it up and replies:
Customer email ("where is order SCP-1042?")
│ Action Mailbox
▼
Protege::AgentMailbox ──► recursion guard · persona lookup (→ Sundae) · access check · bounce-if-unrouted
│ Gateway.accept_smtp_message
▼
Protege::InferenceJob (enqueued via Active Job)
│
▼
Orchestrator::ReplyHarness
├─ message_resolvers ──► assembles context (system prompt · CustomerContextResolver · thread history)
├─ appends the inbound body as the user turn
├─ provider tool loop ──► LLM ⇄ tools (LookupOrder → send_email · bounded by max_tool_turns)
└─ persists messages + tool rounds (Protege::Message / EmailThread / ToolUse)
│
▼
send_email tool ──► Gateway.deliver ──► SMTP (From forced to support@thescoop.com)
│
▼
Reply lands in the customer's inbox, threaded to their original message.The customer's plain question goes in; a threaded, on-brand reply comes out. The persona's assistant chatter is never delivered on its own — the only way mail leaves the building is the send_email tool, and every send is re-stamped with the persona's own address.
The same tool loop serves scheduled work too. When Scout (The Scoop's ops persona) runs its nightly low-stock report, there is no inbound email: the Loop dispatches a ResponsibilityHarness whose responsibility_resolvers chain assembles the entire turn from the responsibility's instructions. Same inference core, different front door.
Components
| Component | Module | Responsibility |
|---|---|---|
| Gateway | Protege::Gateway + Protege::AgentMailbox | Inbound via Action Mailbox; outbound via SMTP; persona routing, recursion guard, access checks, threading. |
| Harness | Protege::Orchestrator::Harness | Context assembly, the provider tool loop, persistence — the inference core. |
| Scheduler | Protege::Loop | Cron-scheduled responsibilities that generate autonomous work. |
| Inference | Protege::Inference | Provider-agnostic LLM adapters behind a normalized request/response. |
Everything is hosted by Rails: Action Mailbox for ingress, Active Job for async work (any queue backend; Solid Queue by default in Rails 8), Active Storage for attachments, and ActiveRecord for persistence. Protege ships as a mountable engine, so your host app keeps its own routes, models, and auth — the agent is a guest in your codebase, not a new home for it.
Where to go next
- The LOGI model — the four layers and why they're separated this way.
- Gateway — the email edge: routing, bounces, threading, From-forcing.
- Harness — the tool loop, its bounds, and the reply/responsibility split.
- Scheduler — how a persona initiates work on a cron.

