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:
classWritingAssistantAgent < ApplicationAgentincludeSolidAgent::HasContexthas_context:conversation,contextual: :userdefimproveload_conversation(contextable: current_user)# contextable is the polymorphic associationadd_conversation_user_message(params[:message])promptmessages: conversation_messagesendendThis 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:
defimproveprompt_options[:trace_id]=my_telemetry_trace_idload_conversation(contextable: current_user)promptmessages: conversation_messagesendQuery with AgentGeneration.with_trace(trace_id) or
AgentContext.with_trace(trace_id).
Define tools inline with a clean DSL:
classResearchAgent < ApplicationAgentincludeSolidAgent::HasToolstool:searchdodescription"Search for information"parameter:query,type: :string,required: trueparameter:limit,type: :integer,default: 10enddefresearchprompttools: toolsenddefsearch(query:,limit: 10)# Tool implementationendendOr use JSON templates in app/views/research_agent/tools/search.json.erb.
Broadcast tool execution status to your UI:
classBrowserAgent < ApplicationAgentincludeSolidAgent::HasToolsincludeSolidAgent::StreamsToolUpdateshas_tools:navigate,:clicktool_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:
classSupportAgent < ApplicationAgentincludeSolidAgent::HasContextincludeSolidAgent::HasMemoryhas_contextcontextual: :userhas_memory# scope: "default", class_name: "AgentMemory"defassistload_context(contextable: params[:user])promptmessages: context_messages,tools: memory_tool_definitionsendendThe 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)doexpensive_call(url)endresult[: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.
