Skip to content

More tools & attachments

Sundae can look up an order, knows her customer, and remembers the conversation. Now we round out her job — refunds, stock checks, new orders — and teach her to handle the photos customers inevitably send.

More tools

Each new capability is just another tool. They follow the same shape as LookupOrder, and each auto-registers the moment it loads.

ruby
# app/tools/process_refund.rb
class ProcessRefund < Protege::Tool
  description "Issues a refund for an order. Use only when the customer is clearly owed one."

  input_schema(
    type:                 "object",
    additionalProperties: false,
    required:             %w[order_number reason],
    properties:           {
      order_number: { type: "string",  description: "The order to refund, e.g. SCP-1042." },
      reason:       { type: "string",  description: "Why the refund is warranted." }
    }
  )

  def use(context:, order_number:, reason:)
    order = Order.find_by(number: order_number)
    return failure(reason: "no order #{order_number}") unless order
    return failure(reason: "order already refunded") if order.refunded?

    refund = order.refunds.create!(amount_cents: order.total_cents, reason:)
    success(refund_id: refund.id, amount_cents: refund.amount_cents)
  end
end
ruby
# app/tools/check_flavor_stock.rb
class CheckFlavorStock < Protege::Tool
  description "Checks whether a flavor is currently in stock."

  input_schema(
    type:                 "object",
    additionalProperties: false,
    required:             %w[flavor],
    properties:           { flavor: { type: "string", description: "The flavor name, e.g. Pistachio." } }
  )

  def use(context:, flavor:)
    record = Flavor.find_by("lower(name) = ?", flavor.downcase)
    return failure(reason: "we don't carry #{flavor}") unless record

    success(flavor: record.name, in_stock: record.in_stock?, seasonal: record.seasonal?)
  end
end

CreateOrder follows the same pattern. Notice the guardrails live in the toolProcessRefund refuses an already-refunded order — because a tool is your app's power in the agent's hands, and the right place to enforce policy is at that boundary, not in the prompt. (See Security.)

Attachments: photos in

Customers send pictures. Say one emails Sundae a photo of a melted delivery. Protege stores the attachment and, because the model is multimodal, includes it automatically as a vision part on the opening turn — Sundae can literally see the photo without any tool call.

For files she needs to open deliberately — or to re-read one from earlier in the thread — there's the built-in read_attachment tool. It reads a stored file by the id shown in the conversation:

  • 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.
  • Other types (video, archives) can't be read, and the tool says so.

So Sundae sees the melted-delivery photo, recognizes the problem, checks the order with LookupOrder, and issues a refund with ProcessRefund — a full resolution from a single emailed image.

Attachments: files out

Sundae can also create a file and attach it to her reply. The built-in create_file turns content she authors into a stored file and returns a blob_id; send_email then attaches it by that id. To send a refund receipt:

  1. Sundae calls create_file with format: "txt" (or md, csv, html, json, svg), a filename, and the receipt text. She gets back a blob_id.
  2. She calls send_email in reply mode with attachments: [blob_id].

The receipt lands as a real attachment on the threaded reply, subject to the configured attachment limits.

Limitations

The agent only ever authors text. There's no binary generation yet — no PDF or raster image output — so create_file stores your chosen format as-is (an html file is HTML text, not a rendered page). Inbound video is stored but not understood, and Office formats aren't viewable. For a true PDF receipt, generate it in a host tool with your own library and hand the bytes back.

Next

Sundae is a capable support agent. But some work is internal, sensitive, or scheduled — and shouldn't run under a public, customer-facing identity. Time for a second agent.

A second persona & scoping.