SolidAgent extends the ActiveAgent framework with database-backed persistence for everything an agent does in a Rails application: conversations, generations, tool/MCP interactions, reasoning, and long-term memory.
Agent-side concerns:
- HasContext - Database-backed prompt context management for maintaining conversation history and agent state, including the full tool/MCP interaction stream
- HasMemory - An agent-curated summary list the model reads/writes via
save_memory/recall_memoryfunction-calling tools; scoped to a subject record so agents hand off to each other through shared memory - HasTools - Declarative, schema-based tool definitions compatible with LLM function-calling APIs
- HasReasons - Capture and inspect extended-thinking/reasoning output across a generation
- StreamsToolUpdates - Real-time UI feedback during tool execution via ActionCable
Model-side and standalone:
- Reasonable - Persist reasoning content/tokens/metadata on your generation records
- AgentRun - Durable run records (installed by the generator): lifecycle status, append-only progress events for live UIs, token/duration accounting, and instruction-fingerprint cohorts for comparing configuration changes
- ToolCache - Cache tool/MCP/service results by
(tool, normalized args)with TTL, backed byRails.cache; error results are never cached and replays are taggedcached: true - ModelPricing - Token-count → estimated USD cost, using RubyLLM's model registry when available with a static pattern-table fallback
- AgentManifest - Load, validate, export, and build agent classes from portable manifests (
.agent.md, dotprompt, CrewAI)
Add this line to your application's Gemfile:
gem "solid_agent"And then execute:
$ bundle installInstall the persistence tables and models (AgentContext, AgentMessage, AgentGeneration, AgentMemory, AgentMemoryEntry, AgentRun), then generate an agent with context support:
$ rails generate solid_agent:install
$ rails db:migrate
$ rails generate solid_agent:agent WritingAssistant --context --context_name conversation --contextual userAdd database-backed context management to your agents:
class WritingAssistantAgent < ApplicationAgent
include SolidAgent::HasContext
has_context :conversation, contextual: :user
def improve
load_conversation(contextable: current_user) # contextable is the polymorphic association
add_conversation_user_message(params[:message])
prompt messages: conversation_messages
end
endThis generates helper methods like:
load_conversation(contextable:)- Load or create a contextconversation_messages- Get formatted message historyadd_conversation_user_message(content)- Add a user messageadd_conversation_assistant_message(content)- Add an AI responseconversation_result- Get the last assistant message
Note: contexts are persisted under
self.class.name— agents built with anonymousClass.new(...)must define a class name or context creation will fail theagent_namepresence validation.
Every persisted generation records a trace_id and a provenance snapshot
(agent/prompt/context checksums). Thread a distributed trace id — for
example an ActiveAgent::Telemetry trace — through prompt options and it
lands on the agent_generations row, joining conversation records to
telemetry traces:
def improve
prompt_options[:trace_id] = my_telemetry_trace_id
load_conversation(contextable: current_user)
prompt messages: conversation_messages
endQuery with AgentGeneration.with_trace(trace_id) or
AgentContext.with_trace(trace_id).
Define tools inline with a clean DSL:
class ResearchAgent < ApplicationAgent
include SolidAgent::HasTools
tool :search do
description "Search for information"
parameter :query, type: :string, required: true
parameter :limit, type: :integer, default: 10
end
def research
prompt tools: tools
end
def search(query:, limit: 10)
# Tool implementation
end
endOr use JSON templates in app/views/research_agent/tools/search.json.erb.
Broadcast tool execution status to your UI:
class BrowserAgent < ApplicationAgent
include SolidAgent::HasTools
include SolidAgent::StreamsToolUpdates
has_tools :navigate, :click
tool_description :navigate, ->(args) { "Visiting #{args[:url]}..." }
endGive an agent a durable summary list it decides when to read and write, scoped to a subject record rather than the agent class — so different agents operating on the same subject share memory, with source_agent provenance on every entry:
class SupportAgent < ApplicationAgent
include SolidAgent::HasContext
include SolidAgent::HasMemory
has_context contextual: :user
has_memory # scope: "default", class_name: "AgentMemory"
def assist
load_context(contextable: params[:user])
prompt messages: context_messages, tools: memory_tool_definitions
end
endThe model calls save_memory(content:, category:) and recall_memory(category:, limit:) as ordinary function-calling tools. SolidAgent::HasMemory.tool_definitions exposes the same schemas module-level for non-agent executors (platform services, MCP servers). Inject agent.memory.to_prompt into instructions to prime a handoff.
result = SolidAgent::ToolCache.fetch(tool: "fetch_url", args: { url: url }, ttl: 300) do
expensive_call(url)
end
result[:cached] # => true on a replayError-shaped results ({ error: ... }) are never cached, so transient failures don't stick; cache keys are stable across argument ordering and symbol/string keys.
Executors record each agent execution as an AgentRun: lifecycle (start!/complete!/fail!/cancel!), correlation with contexts, generations, and telemetry via trace_id, and an append-only progress-event stream a UI can poll mid-run:
run = AgentRun.create!(runnable: document, agent_name: "SupportAgent", input_prompt: message)
run.record_instructions(agent.instructions) # cohort fingerprint ("calm-heron")
run.start!
run.append_event(kind: "tool", label: "fetch_url", eid: "e1", status: "started")
# ... execute ...
run.append_event(kind: "tool", label: "fetch_url", eid: "e1", status: "done", duration_ms: 120)
run.complete!(output: response.message.content, input_tokens: usage.input_tokens, output_tokens: usage.output_tokens)AgentRun#instructions_codename names each instruction cohort deterministically (SolidAgent::RunFingerprint), so comparing "what changed between these two batches of runs" reads as calm-heron vs misty-atoll instead of hex digests.
SolidAgent::ModelPricing.estimate(model: "claude-sonnet-5", input_tokens: 12_000, output_tokens: 800)
# => 0.048 (USD, estimated)The generated AgentGeneration#estimated_cost uses this automatically. Rates come from RubyLLM's registry when that gem is present, else a static pattern table.
# Install persistence tables + models (contexts, messages, generations, memories)
$ rails generate solid_agent:install
# Generate a new agent
$ rails generate solid_agent:agent MyAgent
# Generate with context support
$ rails generate solid_agent:agent MyAgent --context --context_name session
# Generate a tool template
$ rails generate solid_agent:tool search MyAgent --parameters query:string:required
# Generate custom-named context models
$ rails generate solid_agent:context conversation
# Add reasoning columns to a generation model
$ rails generate solid_agent:reasons AgentGeneration
# Scaffold an agent manifest (.agent.md)
$ rails generate solid_agent:manifest researchSee SolidAgent in action:
- Fizzy - AI-enhanced Kanban tracking tool with writing, research, and file analysis agents
- Writebook - Collaborative writing platform with integrated AI writing assistance, research, and document analysis
After checking out the repo, run bin/setup to install dependencies. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/activeagents/solid_agent.
