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.
weather = Mistri::Tool.define( "get_weather", "Current weather for a city.", schema: lambda { string :city, "City name", required: true string :units, "Temperature units", enum: %w[celsius fahrenheit] },) do |args| Weather.for(args["city"], units: args["units"] || "celsius")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("edit_page", "Applies a page edit.") do |args| page = apply(args) Mistri::ToolResult.new(content: "Saved.", ui: { "html" => page })endTimeouts
timeout: bounds a single execution. A tool that overruns answers the
model with a timeout error instead of hanging the run.
Mistri::Tool.define("slow_api", "Calls a flaky API.", timeout: 10) do |args| Flaky.call(args)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:
policy = lambda do |call, _context| "read-only during freeze" if call.name == "deploy"endredact = lambda do |_call, result, _ctx| Mistri::ToolResult.new(content: result.to_s.gsub(SSN, "[redacted]"))end
agent = Mistri.agent("claude-opus-4-8", tools: tools, before_tool: policy, 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.