Extensions
You build an agent by writing four kinds of plain-Ruby extension. Each is a subclass of a Protege base class living in your host app's app/ directory, and each has a different job. Together they are the whole surface you write against — the engine wires them in and calls them at the right moments.
| Surface | Base class | What it does | How it's discovered |
|---|---|---|---|
| Tools | Protege::Tool | Actions the model can take in your app | Automatically (every subclass) |
| Providers | Protege::Provider | Model backend adapters | Selected by config.provider_id |
| Resolvers | Protege::Resolver | Assemble the context sent to the model | Added explicitly to an agent's chain |
| Hooks | Protege::Hook | React to lifecycle events | Automatically (every subclass) |
Where they live
The generators put each surface in its own conventional directory. This is The Scoop's app/ after a few chapters of the tutorial:
app/
├── personas/ # persona classes (roles) — Sundae is a CustomerServicePersona record, Scout an OperationsPersona
│ ├── customer_service_persona.rb
│ └── operations_persona.rb
├── tools/ # actions — auto-discovered
│ ├── lookup_order.rb
│ ├── check_flavor_stock.rb
│ ├── process_refund.rb
│ └── create_order.rb
├── resolvers/ # context assembly — wired onto a persona
│ └── customer_context_resolver.rb
├── hooks/ # event reactions — auto-discovered
│ └── refund_audit_hook.rb
└── providers/ # model adapters — selected by config
└── scoop_llm_provider.rb # (optional; OpenRouter ships built-in)The directories are conventions, not requirements — each is configurable (config.tools_path and friends) — but everything must stay under an autoloaded path so Rails (and Protege) can find it.
Two discovery models
There is one distinction worth internalizing, because it explains why some extensions "just work" and others need a line of wiring:
- Auto-discovered — Tools and Hooks. Every subclass is found by the engine automatically. Write a
Protege::Toolsubclass and it appears in the model's catalog; write aProtege::Hooksubclass and its event handlers are live at boot. Nothing to register. - Explicitly wired — Resolvers and Providers. A resolver only runs if you add it to a persona's resolver chain — order matters, and different agents want different context. A provider is chosen by id in configuration, because an app runs one at a time.
Scaffolding
Every surface has a generator that writes a correctly-shaped starter file into the right directory:
bin/rails g protege:tool lookup_order
bin/rails g protege:resolver customer_context
bin/rails g protege:hook refund_audit
bin/rails g protege:provider scoop_llm
bin/rails g protege:persona customer_serviceThe generators are idempotent about the type suffix — protege:tool send_email, protege:tool SendEmail, and protege:tool SendEmailTool all produce the same SendEmailTool class. See Generators & CLI for the full set.
Where to start
Read Tools first — they're the agent's hands and the surface you'll write most of. Then Resolvers for giving the agent the right context, then Hooks for observing and reacting to runs. Providers you'll rarely touch — the bundled OpenRouter adapter covers most models.

