Tools

A tool is a name, a description, an argument schema, and a block. The block returns a String, a Hash (sent as JSON), or content such as an image. The agent feeds results back and loops until the model answers; independent calls in one turn run in parallel.

classify_ticket = Mistri::Tool.define(
"classify_ticket", "Assigns a queue and priority to a support ticket.",
schema: lambda {
string :subject, "The ticket subject line", required: true
string :priority, "How urgent it looks", enum: %w[low normal high urgent]
},
) do |args|
Triage.route(args["subject"], priority: args["priority"] || "normal")
end

Two channels

A tool can speak to the model and to your interface separately. The ui payload rides the :tool_result event and persists with the session, but never reaches a provider:

Mistri::Tool.define("lookup_customer", "Pulls the customer's account.") do |args|
customer = Customer.find(args["id"])
Mistri::ToolResult.new(
content: "#{customer.name}, #{customer.plan} plan, #{customer.open_tickets} open tickets.",
ui: { "card" => customer.as_json },
)
end

The model reads a one-line summary it can act on; your interface renders the full account card, which the model never pays to read.

Timeouts

timeout: bounds a single execution. A tool that overruns answers the model with a timeout error instead of hanging the run.

Mistri::Tool.define("crm_lookup", "Reads a record from the CRM.", timeout: 10) do |args|
CRM.fetch(args["id"])
end

Hooks

before_tool screens every call and blocks with an in-band reason; after_tool can rewrite a result before it reaches the model. Both receive the call and a context with the session:

freeze = lambda do |call, _context|
"writes are frozen during the incident" if call.name == "issue_refund"
end
redact = lambda do |_call, result, _ctx|
Mistri::ToolResult.new(content: result.to_s.gsub(CARD_NUMBER, "[redacted]"))
end
agent = Mistri.agent("claude-opus-4-8", tools: tools,
before_tool: freeze, after_tool: redact)

A call a human already approved is screened again at settle time, so a policy that changed since the approval still wins.

The context a handler and a hook receive also carries the acting user, so tools stay authorization-aware without capturing request state. See App context.