Skip to content

samuelralak/smith

Repository files navigation

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.

Workflow Graph Inspection

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 counts

Graph inspection is static and diagnostic-only. Runtime execution, persistence, progress projection, retries, and recovery remain host-owned concerns.

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.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 }
  }
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 and test_mode = true
  • :redis — Redis client; uses WATCH/MULTI/EXEC for CAS
  • :rails_cache, :solid_cache — Rails cache backends
  • :cache_store — any object responding to write/read/delete
  • :active_record — keyed ActiveRecord model with lock_version column 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/responses routing 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.

About

Workflow-first multi-agent orchestration for Ruby, built on RubyLLM.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages