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")endTwo 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 }, )endThe 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"])endHooks
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"endredact = 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.