An AI tool is just a service object
The agent frameworks renamed a pattern Rails developers have used for a decade.
Strip the vocabulary away and an AI “tool” is a function the model can ask your application to run. Rails has had a name for that for a decade: a service object.
class Tools::LookupOrder
def self.schema
{
name: "lookup_order",
description: "Fetch an order by number for the current customer",
parameters: { order_number: { type: "string", required: true } }
}
end
def initialize(customer)
@customer = customer
end
def call(order_number:)
order = @customer.orders.find_by(number: order_number)
return { error: "not_found" } unless order
{ number: order.number, status: order.status, total: order.total.to_s }
end
end
Notice what the old discipline gives you for free:
- Scoping. The tool takes a customer and can only reach that customer’s orders. The model cannot ask for data the tool cannot reach. Authorization lives in code you control, never in the prompt.
- Testability. It is a PORO. You test it like any service object, without a model in the loop.
- A registry. A list of tool classes is your capability surface. Reviewing what the AI can do means reading one directory.
The mistake to avoid is the reverse direction: handing the model a generic “run this query” tool. That is not a tool, it is a hole. Every tool should do one narrow thing on behalf of one authorized actor.
If you have written good service objects, you already know how to build AI tools. The caller changed. The discipline did not.
Previously: never call a model in a request. Next: that new caller lies.