The agent harness for Ruby applications.
watch it work ↓demos
Watch it work.
Five short recordings of real runs, replayed in the browser. No model behind them, so click around as much as you like.
quickstartweather.rb
A tool-using agent in one file.
Define a tool, hand it to an agent, stream the run. The block gets every event as it happens.
require "mistri"
weather = Mistri::Tool.define( "get_weather", "Current weather for a city.", schema: -> { string :city, "City name", required: true },) do |args| Weather.for(args["city"])end
agent = Mistri.agent("claude-opus-4-8", tools: [weather])
agent.run("What should I wear in Lahore today?") do |event| print event.delta if event.type == :text_deltaendcapabilitiesapproval.rb session.rb stream.rb compact.rb task.rb team.rb mcp.rb Gemfile
What it handles.
The parts you would otherwise build and babysit yourself. Every panel is the real API, not pseudocode.
send_gift = Mistri::Tool.define( "send_gift", "Sends a real gift.", needs_approval: ->(args) { args["amount"].to_i > 100 },) { |args| Gifts.send!(args) }
result = agent.run("Send Ana a $200 gift")result.awaiting_approval? # => true, nothing executed
# Days later, from a controller, then a worker:session.approve(call_id)agent.resumestore = Mistri::Stores::ActiveRecord.new(AgentEntry)session = Mistri::Session.new(store:)
Mistri.agent("claude-opus-4-8", session:) .run("Start a haiku about the sea.")
# Another day, another process: same id, full history.reloaded = Mistri::Session.new(store:, id: session.id)Mistri.agent("claude-opus-4-8", session: reloaded) .run("Now finish it.")channel = "agent_#{session.id}"cable = Mistri::Sinks::ActionCable.new(channel)sink = Mistri::Sinks::Coalesced.new(cable)
agent.run(input, &sink)
# or handle the events yourselfagent.run(input) do |event| print event.delta if event.type == :text_deltaendagent.context_usage# => { tokens: 141_000, window: 200_000, fraction: 0.705 }
agent.compact # the manual button
# It also happens on its own near the limit.# :compacting and :compaction events carry the summary,# so users see exactly what the model still remembers.schema = { type: "object", properties: { "tiers" => { type: "array", items: { type: "string" } }, }, required: ["tiers"],}
result = agent.task("Extract the pricing tiers.", schema: schema)result.output # => { "tiers" => [...] }, validatedresearcher = Mistri::SubAgent.new( name: "researcher", description: "Reads pages and answers factual questions.", provider: Mistri.provider("claude-haiku-4-5-20251001"), system: "Research. Report findings only.", tools: [fetch_page],)
agent = Mistri.agent("claude-opus-4-8", tools: [researcher.tool])linear = Mistri::MCP::Client.new( url: "https://mcp.linear.app/mcp", token: -> { connection.bearer_token },)tools = Mistri::MCP.tools( linear, prefix: "linear", gates: { "create_issue" => true },)
# and the whole "give it a browser" story:browser = Mistri::MCP::Client.new( command: ["npx", "-y", "@playwright/mcp@latest", "--headless"],)gem "mistri" # the whole dependency tree
# Anthropic, OpenAI, and Gemini, each streamed natively:# thinking, prompt caching, parallel tool calls, and# constrained JSON output. One message model across all.
# The provider comes from the model id, the key from ENV.agent = Mistri.agent("claude-opus-4-8")agent = Mistri.agent("gpt-5.5")agent = Mistri.agent("gemini-3-pro")