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.
# Gemfile
gem "smith-agents", "~> 0.2.0", require: "smith"bundle installThe 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.
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: ... }]| 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| 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.
Smith can inspect a workflow's declared graph without running agents or advancing state. This is useful for host apps that want to render, lint, or cache a workflow shape before execution.
report = ReplyWorkflow.validate_graph
report.valid? # => true
report.transitions # => read-only transition snapshots
report.diagnostics # => errors and warnings for missing states or routes
report.metrics # => state, transition, reachability, and terminal-state countsGraph inspection is static and diagnostic-only. Runtime execution, persistence, progress projection, retries, and recovery remain host-owned concerns.
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.persistence_options = { 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 }
}
endAll settings are optional for a first run. See docs/CONFIGURATION.md for the full reference.
# 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.
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
endGuardrails run as input/output gates around agent calls. See docs/TOOLS_AND_GUARDRAILS.md.
class BudgetedWorkflow < Smith::Workflow
budget total_tokens: 10_000, total_cost: 0.50, wall_clock_ms: 30_000
endBudgets 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.
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:installDoctor 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.
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.
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)bundle install
bundle exec rspec
bundle exec rubocop770 examples, MIT licensed. See CHANGELOG.md for the 0.2.0 surface and UPSTREAM_PROPOSAL.md for the vendored Responses adapter retirement path.