Ruby · 0.1 · MIT

Omakase

お任せ

You name what you want. The rest is left to the chef.

A light agent framework for Ruby — about 700 lines on top of RubyLLM. An agent is an object: fields are state, methods are what the model can call, and the methods you declare without a body are written by the model at runtime.

github ↗ rubygems ↗
declare

An agent is an object

Ordinary methods are what the model can call — there is no tool registry, so adding a tool is adding a method. Declared methods have no body: the name and prompt are the specification, the block is the contract. A field is memory: the chat is fresh every call, so what carries is what the object keeps — context puts it in the next prompt.

class RefundAgent < Omakase::Agent
  instructions "You are the refund desk of an online shop."

  describe "Every order this customer placed, newest first. An Order has items and total"
  def orders_for(email) = Order.where(email:).order(placed_on: :desc)

  describe "What the policy says about a topic, such as :damage or :late"
  def policy_on(topic) = POLICY.fetch(topic, "Refunds are allowed within 30 days.")

  generates :decide, "Decide this refund, and name the policy you applied.", returns: Refund
end

RefundAgent.decide(email: "ada@example.com", complaint: "the mug arrived cracked")
# => #<struct Refund order_id=1, amount=39.9, reason="Damaged goods are refunded in full…">
act

It acts by writing Ruby

The model gets one tool. Its code is evaluated on the agent itself, so the agent's methods and state are the API; what the code printed and returned comes back as the observation. It answers with finish(value) — computed, not retyped.

rubyorders_for("ada@example.com").each { |o| puts o.total, o.items.inspect }
out39.9
[#<Item name: "Stoneware mug", price: 34.0>, #<Item name: "Shipping", price: 5.9>]
rubyputs policy_on(:damage)
outDamaged goods are refunded in full, including shipping, within 90 days.
rubyfinish(Refund.new(order_id: 1, amount: 39.9, reason: "…"))
outanswer accepted

A failure comes back with the line that raised. An answer off contract is rejected into the same loop. Ten tool calls and thirty seconds per execution, then it answers with what it has.

contract

The return type is the contract

A block is a schema the provider must fill, so callers get validated data rather than text to parse. A scalar is the same thing, unwrapped. A Ruby class means the method hands back the object the code built.

generates :summarize                          # no prompt: the method name is the prompt
generates :score, returns: :integer          # => 7
generates :file_ticket, returns: Ticket        # => #<struct Ticket id="A-1">

generates :analyze, "Analyze the feedback." do
  string :sentiment, enum: %w[positive negative neutral mixed]
  array  :topics, of: :string
end

:predict

One call, straight into the schema. No code runs. Classification, extraction, rewriting.

:code_act  default

The loop above. Multi-step work, anything that should use the agent's own methods.

reach

The outside is a method too

An MCP server's tools arrive as methods on the agent. A skill — a SKILL.md directory, the same front matter Claude Code uses — arrives as one more, and memory adds two: remember and recall, by meaning. Nothing new to learn: external services, curated guidance and what the agent picked up join the list your own methods are on.

class DocsAgent < Omakase::Agent
  mcp :files,
    transport_type: :stdio,
    config: {command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", Rails.root.to_s]}

  skill "skills/commit-style"
  memory

  generates :subject_for, "Write the commit subject for this change.", returns: :string
end

mcp

Every tool the server lists, described with its arguments. Generated code calls one, then feeds the result to your own method in the same expression.

skill

The front matter's description sits in the prompt. The markdown body costs nothing until the code asks for it — which is all loading on demand has to mean.

rails

At home in Rails

Agents live in app/agents, reloading is safe, printing is per-thread so Puma is fine, and every call lands on the notification bus with its tokens and latency.

# config/initializers/omakase.rb
Omakase.configure do |config|
  config.anthropic_api_key = Rails.application.credentials.anthropic_api_key
  config.request_timeout   = 60
  config.instrumenter      = ActiveSupport::Notifications
end

class TriageJob < ApplicationJob
  def perform(ticket) = ticket.update!(SupportAgent.triage(message: ticket.body))
end

One rule. Generated code runs with instance_eval in your process, where ActiveRecord and ENV live. Untrusted input belongs to :predict; :code_act is for work you control — or swap in your own isolated executor.

Why this

against RubyLLM alone

The tool loop, the schema plumbing and the correction turn after an off-contract answer are what these 700 lines are. Providers, keys, models, streaming and tracing stay RubyLLM's, and stay reachable.

against a tool registry

Nothing to register, nothing to keep in sync. A tool's description is describe, one line above the method — not a JSON schema that drifts from the code it describes.

Not here, on purpose: a checkpoint inside a generation, a sandbox, multi-agent orchestration, a token stream. The library stops where your stack already has an answer.

serve

Install

0.1.0 — usable, not finished. Ruby 3.2+, and any provider RubyLLM supports: Anthropic, OpenAI, Gemini, Bedrock, Ollama, OpenRouter, and the rest.

gem install omakase-agents

# or, in a Gemfile
gem "omakase-agents"