Skip to content

Tools

Tools are the actions the model can take — the agent's hands in your Rails app. A tool is a subclass of Protege::Tool. Subclasses are discovered automatically: define one and it appears in the model's tool catalog, no registration needed.

Anatomy

Here is LookupOrder, the first tool The Scoop gives Sundae — it looks up an order by number:

ruby
# app/tools/lookup_order.rb
class LookupOrder < Protege::Tool
  description "Looks up an order by its number and returns its current status."

  input_schema(
    type:                 "object",
    additionalProperties: false,
    required:             %w[order_number],
    properties:           {
      order_number: { type: "string", description: "The order number, e.g. SCP-1042." }
    }
  )

  def use(context:, order_number:)
    order = Order.find_by(number: order_number)   # your app's model
    return failure(reason: "no order #{order_number}") unless order

    success(status: order.status, total_cents: order.total_cents, placed_at: order.created_at)
  end
end
  • description and input_schema define what the model sees. The schema is standard JSON Schema; its properties become the keyword arguments to use.
  • use(context:, **input) runs the action. It's use — not call or run. Return success(**data) or failure(reason:, **data); both are fed back to the model so it can react. reason may be a string or an exception.
  • This is the seam where the agent reaches into your app. A tool is ordinary Ruby, so it calls your models, services, and jobs directly.
  • The class name is the tool's identity. LookupOrder becomes the id :lookup_order (the basename, with a trailing Tool stripped if present, snake-cased). The Tool suffix is optional on your own classes — the generator adds it, and the id comes out the same either way.

Nothing wires a tool in. Because discovery is by subclass, the moment LookupOrder is loaded it's in Sundae's catalog — subject to her tool scope.

The context

Every use receives a context carrying the run:

  • context.persona — the agent handling the run (here, Sundae).
  • context.message — the inbound message, on a reply run (nil on a scheduled run).
  • context.responsibility — the scheduled duty, on a responsibility run (nil on a reply).
  • context.deliver(mail:, attachments:) — send outbound mail through the Gateway.
  • context.config, context.logger, context.correlation_id — the frozen config snapshot, the logger, and the id tying this run's events together.

Tool calls and their results are emitted as events and persisted, so a later turn can remember what the agent did.

Success and failure

A tool must return a Protege::Result. The success/failure helpers build one:

ruby
success(refund_id: refund.id, amount_cents: refund.amount_cents)
failure(reason: "order already refunded")
failure(reason: e)   # an exception works too

A failure is not an error — it's information. The model sees the reason and can adjust: apologize, ask for a missing detail, or try a different tool. If a tool raises, the engine catches it and turns it into a failure result too, so a bug in one tool never crashes the run — it just tells the model that call didn't work.

Sending mail is a tool

The agent's plain assistant text is never delivered — mail goes out only through the built-in send_email tool. Every agent that talks back needs it in scope. Treat send_email as the one tool you always include.

It has two modes:

  • mode: "reply" — continue the current conversation. The subject and threading are taken from the inbound message (not chosen by the model — an invented subject would split the thread in the customer's client), and the original sender is always a recipient. The agent may add to/cc/bcc.
  • mode: "new" — start a fresh conversation. The agent supplies to and subject; no threading headers are set.

body is always required; From is always forced to the agent's own address. The agent can attach files by referencing blob ids (see Attachments).

Built-in tools

Protege ships a handful of tools so a new agent is useful immediately:

ToolPurpose
send_emailThe only way to send mail (From enforced). Modes "reply" (subject + threading from the inbound message; original sender included; add to/cc/bcc) and "new" (agent supplies to + subject). Attach files by blob id via attachments: [123], subject to the attachment limits.
search_emailsFree-text search across the agent's own archive (participants + subject/body), so it can recall conversations beyond the current thread. Returns matches newest-first with bodies and each message's attachments (filename, type, size, id).
read_attachmentReads a stored file by its id. Text files (txt/csv/markdown/json/xml) come back as text; images and PDFs are shown to the model to view (it sees the actual file); video and other types can't be read.
create_fileTurns content the agent authors into a stored file it can attach. Give it a format (csv, txt, md, html, json, svg), a filename, and the full content as text; returns a blob_id. Feed that id to send_email. The agent only ever authors text — there's no binary generation yet.
web_searchKeyless best-effort web search (scrapes DuckDuckGo) returning {title, url, snippet} results the agent can then web_fetch.
web_fetchFetches an http(s) url and returns its readable text. Guards against SSRF (internal/private addresses refused) and caps redirects and response size.

Keyed search is a host-app tool

web_search is deliberately keyless, so it's safe to expose to every agent. Provider-specific search (Tavily, Perplexity, Brave, …) needs an API key, so it belongs in your app as a tool in app/tools/ that reads the key from ENV — not an engine built-in. That keeps keyed tools opt-in rather than force-exposed everywhere.

Attachments

send_email, create_file, and read_attachment are the agent's hands for files. How inbound images/PDFs are auto-included for a multimodal model, the type-by-type behavior, the limits, and how to extend handling are covered in the tutorial: More tools & attachments.

Next

  • Resolvers — give the agent the right context before it reaches for a tool.
  • Personas — scope which tools an agent may use.