Skip to content

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.

SurfaceBase classWhat it doesHow it's discovered
ToolsProtege::ToolActions the model can take in your appAutomatically (every subclass)
ProvidersProtege::ProviderModel backend adaptersSelected by config.provider_id
ResolversProtege::ResolverAssemble the context sent to the modelAdded explicitly to an agent's chain
HooksProtege::HookReact to lifecycle eventsAutomatically (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::Tool subclass and it appears in the model's catalog; write a Protege::Hook subclass 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:

bash
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_service

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