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")
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("edit_page", "Applies a page edit.") do |args|
page = apply(args)
Mistri::ToolResult.new(content: "Saved.", ui: { "html" => page })
end

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("slow_api", "Calls a flaky API.", timeout: 10) do |args|
Flaky.call(args)
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:

policy = lambda do |call, _context|
"read-only during freeze" if call.name == "deploy"
end
redact = 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.