Skip to content

Invoking from your app

Tools and resolvers let an agent reach into your app. This is the reverse: how ordinary application code — a controller, a service object, a background job, a state machine — hands an agent something to act on. The entry point is Protege::Gateway.message.

Gateway.message

ruby
Protege::Gateway.message(
  to:      "ops@thescoop.com",
  subject: "Low-stock check",
  body:    "Pistachio is down to 4 tubs. Decide whether to reorder and email the supplier if so."
)

This hands Scout (the agent at ops@thescoop.com) a brief and lets it act. It's the same path the dashboard console uses, so the message flows through the normal "an inbound message starts a thread" machinery — the reply harness and the persona's message_resolvers.

When The Scoop's checkout flow flags a problem, a service object can loop Sundae in the same way — no need to load the persona record or build any mail:

ruby
class OrderIssueReporter
  def report(order)
    Protege::Gateway.message(
      to:      "support@thescoop.com",
      subject: "Order #{order.number} flagged at checkout",
      body:    "Payment was declined twice for #{order.customer.email}. Reach out and offer help."
    )
  end
end

to is an address, not a record

to is resolved through the same lookup inbound mail uses, honouring case and plus-tag subaddressing — so you can pass a literal address or persona.email_address without loading the record first. If no active persona owns the address, Gateway.message raises ArgumentError.

It's fire-and-forget

Gateway.message returns the persisted inbound Message (useful for reference or tracing) — not an LLM response. There's nothing the agent must reply to; it reads the brief, runs inference, and uses its (scoped) tools. The outcome is whatever actions it takes — the mail it chooses to send, the records it updates. If you want the agent to email someone, say so in the body; the agent decides and sends via its send_email tool like any other run.

Because it's the console path, a message whose conversation stays at the console_address never touches SMTP — handy for internal, in-app agent work you don't want leaving the building.

Attachments

You can hand the agent files, and they meet your app where the file already is — no need to build a mail attachment by hand. Each entry may be:

  • a stored Active Storage blob or attachment (reused by reference, no re-upload),
  • an ActionDispatch::Http::UploadedFile (a controller upload, passed straight through), or
  • a plain { filename:, content_type:, bytes: } hash (raw or generated bytes, uploaded once).
ruby
Protege::Gateway.message(
  to:          "support@thescoop.com",
  subject:     "Damaged delivery report",
  body:        "A customer sent this photo of a melted delivery. Assess and offer a refund if warranted.",
  attachments: [params[:photo]]   # an uploaded file straight from a controller
)

The agent sees them exactly as it sees an emailed attachment — via read_attachment and, for images and PDFs, as vision parts on a multimodal model.

  • Gateway — the inbound/outbound edge this rides on.
  • Tools — the reverse direction, the agent reaching into your app.
  • Configuration — the console_address that keeps a conversation in-app.