Smith
Workflow-first multi-agent orchestration for Ruby. Smith sits on top of RubyLLM and adds explicit state machines, typed contracts, budgets, guardrails, persistence, tools, and tracing for production agent systems.
[!WARNING] Smith is pre-1.0. Expect contract tightening between minor versions. Pin to an exact version in production.
Installation
# Gemfile
gem "smith-agents", "~> 0.2.0", require: "smith"
bundle install
The Ruby module namespace stays Smith::; only the gem name is namespaced because smith on RubyGems is taken. The require: "smith" in the Gemfile tells bundler to load the actual file name.
Quickstart
require "ruby_llm"
require "smith"
RubyLLM.configure do |config|
config.openai_api_key = ENV.fetch("OPENAI_API_KEY")
end
class ReplyAgent < Smith::Agent
register_as :reply_agent
model "gpt-4.1-nano"
instructions { "Write a concise, professional reply." }
end
class ReplyContext < Smith::Context
persist :user_message
inject_state { |p| "User message: #{p[:user_message]}" }
end
class ReplyWorkflow < Smith::Workflow
context_manager ReplyContext
initial_state :idle
state :done
state :failed
transition :reply, from: :idle, to: :done do
execute :reply_agent
on_failure :fail
end
end
result = ReplyWorkflow.new(context: { user_message: "Charged twice." }).run!
result.state # => :done
result.output # => assistant reply
result.steps # => [{ transition: :reply, from: :idle, to: :done, output: ... }]
Core Concepts
| Concept | Purpose |
|---|---|
Smith::Agent |
A RubyLLM agent plus model, instructions, output schema, tools, budget, and fallback models. Identifies itself to the workflow via register_as :name. |
Smith::Workflow |
A state machine of named transitions. Each transition calls an agent, runs deterministic code, routes, or composes a nested workflow. |
Smith::Context |
Declares which workflow context keys persist across restore, and how those keys become agent-visible input via inject_state. |
Smith::Tool |
A RubyLLM tool plus provider-compatibility metadata and guardrail hooks. |
| Persistence adapters | Host-owned storage. Smith ships Memory, RedisStore, CacheStore, RailsCache, ActiveRecordStore. |
| Trace adapters | Host-owned observability. Smith ships Memory, Logger, OpenTelemetry. |
Agents register at class load. In Rails, register workflow-facing agents in a to_prepare hook so autoload doesn't drop them:
# config/initializers/smith_agents.rb
Rails.application.config.to_prepare do
ReplyAgent
TriageAgent
end
Patterns
| Pattern | DSL | Use case |
|---|---|---|
| Single execute | execute :agent |
One agent call per transition. |
| Pipeline | sequential transitions | Multi-step workflow with explicit success/failure routing. |
| Router | route :classifier, routes: {...} |
Branch on a classifier agent's output. |
| Parallel fan-out | execute :agent, parallel: true |
Concurrent agent calls under one ledger. |
| Nested workflow | workflow OtherWorkflow |
Reuse a subflow as one transition. |
| Evaluator-Optimizer | optimize generator:, evaluator:, ... |
Generate-then-critique refinement loops. |
| Orchestrator-Worker | orchestrate orchestrator:, worker:, ... |
Dynamic task fan-out with delegation rounds. |
| Deterministic | `compute { | step |
The full pattern guide with working examples for each lives in docs/PATTERNS.md.
Configuration
require "logger"
require "smith"
Smith.configure do |config|
config.logger = Logger.new($stdout)
config.trace_adapter = Smith::Trace::Memory.new
config.artifact_store = Smith::Artifacts::Memory.new
# Persistence
config.persistence_adapter = :rails_cache
config. = { namespace: "smith" }
config.persistence_ttl = 1.day.to_i
config.persistence_retry_policy = { attempts: 3, base_delay: 0.1, max_delay: 1.0 }
# OpenAI /v1/responses routing for gpt-5 + tools + thinking. :auto (default) or :off.
config.openai_api_mode = :auto
config.pricing = {
"gpt-4.1-nano" => { input_cost_per_token: 1.0e-7, output_cost_per_token: 4.0e-7 }
}
end
All settings are optional for a first run. See docs/CONFIGURATION.md for the full reference.
Persistence and Resume
# Persist after every advance
result = ReplyWorkflow.run_persisted!(
context: { user_message: "..." },
adapter: Smith.persistence_adapter
)
# Resume later
result = ReplyWorkflow.run_persisted!(
key: "ticket:T-1042",
adapter: Smith.persistence_adapter
)
Built-in adapters (all support TTL where the backend allows; Redis, ActiveRecord, Memory also support optimistic locking via store_versioned):
:memory— in-process Hash, intended for tests andtest_mode = true:redis— Redis client; uses WATCH/MULTI/EXEC for CAS:rails_cache,:solid_cache— Rails cache backends:cache_store— any object responding towrite/read/delete:active_record— keyed ActiveRecord model withlock_versioncolumn for CAS
See docs/PERSISTENCE.md for schema versioning, seed-drift validation, and the idempotency_mode :strict step-in-progress contract.
Tools and Guardrails
Smith ships Tools::WebSearch, Tools::UrlFetcher, and Tools::Think. Tools declare provider compatibility via compatible_with; Smith's normalizer routes or drops them per-attempt.
class SearchAgent < Smith::Agent
register_as :search_agent
model "claude-opus-4-7"
tools Smith::Tools::WebSearch, Smith::Tools::UrlFetcher
end
Guardrails run as input/output gates around agent calls. See docs/TOOLS_AND_GUARDRAILS.md.
Budgets and Deadlines
class BudgetedWorkflow < Smith::Workflow
budget total_tokens: 10_000, total_cost: 0.50, wall_clock_ms: 30_000
end
Budgets reserve serially at each step and reconcile after the agent call. Parallel branches reserve scoped envelopes that release back to the parent ledger. The Workflow::RunResult carries total_tokens, total_cost, and per-call usage_entries.
Doctor
After adding Smith, verify the integration:
# Plain Ruby
smith doctor # offline checks
smith doctor --live # live provider call
smith doctor --durability # persistence round-trip
smith install # scaffold config/smith.rb
# Rails
bin/rails smith:doctor
bin/rails smith:doctor:live
bin/rails smith:doctor:durability
bin/rails generate smith:install
Doctor verifies: Smith loads, RubyLLM loads, minimal workflow boots, configuration is non-empty, serialization round-trips, persistence adapter works, and (with --live) a real provider call succeeds.
Capability-aware request shaping
Smith ships a per-attempt normalizer that translates the request payload to whatever the resolved model's provider family expects:
- Anthropic Opus 4.7+ adaptive thinking via
output_config[:effort] - Anthropic 4.0–4.6 budget_tokens
- OpenAI gpt-5 family reasoning_effort with
/v1/responsesrouting when tools + thinking are combined - Gemini 2.5+ budget_tokens
Override the inferred profile per-app via Smith::Models.register(Profile.new(...)). Hosts pin to specific model_ids by registering profiles; Smith never hardcodes model_ids in the library.
Errors and retry
Smith::Errors.retryable?(error)
# AgentError, DeadlineExceeded => true (always)
# DeterministicStepFailure, ToolGuardrailFailed => honors error.retryable
# everything else => false
Smith::Errors.retryable_classes
# => [Smith::AgentError, Smith::DeadlineExceeded] (for ActiveJob retry_on)
Development
bundle install
bundle exec rspec
bundle exec rubocop
770 examples, MIT licensed. See CHANGELOG.md for the 0.2.0 surface and UPSTREAM_PROPOSAL.md for the vendored Responses adapter retirement path.