SolidAgent
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.
Documentation ·
Examples ·
.agent.md spec
Features
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)
Installation
Add this line to your application's Gemfile:
gem "solid_agent"
And then execute:
$ bundle install
Usage
Quick Start
Install 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 user
HasContext - Persistent Conversation History
Add database-backed context management to your agents:
class WritingAssistantAgent < ApplicationAgent
include SolidAgent::HasContext
has_context :conversation, class_name: "AgentContext", contextual: :user
def improve
load_conversation(contextable: params[:user]) # contextable is the polymorphic association
prompt messages: + [
{ role: "user", content: params[:message] }
]
end
end
This 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
With auto_save on (the default), the last prompt message is persisted as
the user turn and the response as the assistant turn, both after the
provider call — so reach for add_conversation_user_message only with
auto_save: false, or the turn is stored twice.
Naming a context also names its models.
has_context :conversationinfersConversation,ConversationMessageandConversationGeneration, not theAgentContextfamily the installer wrote — henceclass_name:above, which infersAgentMessageandAgentGenerationalongside it. Unnamedhas_contextresolves to those models directly; for genuinely separate tables per context, runrails generate solid_agent:context conversation.
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.
Telemetry trace correlation
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
[:trace_id] = my_telemetry_trace_id
load_conversation(contextable: current_user)
prompt messages:
end
Query with AgentGeneration.with_trace(trace_id) or
AgentContext.with_trace(trace_id).
HasTools - Declarative Tool Schemas
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
end
Or use JSON templates in app/views/research_agent/tools/search.json.erb.
StreamsToolUpdates - Real-Time Feedback
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]}..." }
end
HasMemory - Agent-Curated Long-Term Memory
Give 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: , tools: memory_tool_definitions
end
end
The 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.
ToolCache - Cached Tool Results
result = SolidAgent::ToolCache.fetch(tool: "fetch_url", args: { url: url }, ttl: 300) do
expensive_call(url)
end
result[:cached] # => true on a replay
Error-shaped results ({ error: ... }) are never cached, so transient failures don't stick; cache keys are stable across argument ordering and symbol/string keys.
AgentRun - Durable Run Records
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: )
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..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.
ModelPricing - Estimated Spend
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.
Generators
# 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. --context_name emits
# `has_context :session`, which resolves Session/SessionMessage/
# SessionGeneration — pair it with the context generator below, or drop the
# option to use the installed AgentContext models.
$ 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 research
Examples
The examples/ directory has a worked example per concern —
agent classes, views, controllers and console walkthroughs laid out the way
they'd sit in a Rails app:
| Example | Concerns |
|---|---|
| persistent_conversation | HasContext |
| memory_handoff | HasMemory |
| tool_streaming | HasTools, StreamsToolUpdates, ToolCache |
| reasoning | HasReasons, Reasonable |
| run_tracking | AgentRun, RunFingerprint, ModelPricing |
| manifests | AgentManifest |
The narrated versions live at docs.activeagents.ai/solid_agent.
Example Apps
See 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
Development
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.
bundle exec rake test
Testing against ActiveAgent
This suite runs against mocks — deliberately, so it stays fast and dependency-free — which means it can pass while these concerns no longer compose with the framework they extend. ActiveAgent carries a dummy Rails app and a cross-repo suite for exactly that. Point it at your working tree:
git clone https://github.com/activeagents/activeagent ../activeagent
cd ../activeagent
SOLID_AGENT_PATH=../solid_agent \
BUNDLE_GEMFILE=gemfiles/solid_agent_main.gemfile \
SOLID_AGENT_STRICT=1 \
bin/test test/integration/solid_agent/*_test.rb \
actionagent/test/agent_execution_service_test.rb
SOLID_AGENT_STRICT=1 fails on anything the suite would otherwise skip for
a missing API — here the resolved gem is your checkout, so a skip means
something was removed. CI runs this on every pull request against
ActiveAgent's main branch and its latest release, and again nightly. See
Releasing & Cross-Repo Testing.
Releasing
Releases publish from a v* tag through
.github/workflows/release.yml using RubyGems
trusted publishing, gated on CI including the cross-repo suite. Bump
SolidAgent::VERSION, tag, push. This gem depends on activeagent and
actionagent depends on this gem, so anything requiring a new framework API
waits for that release to land on RubyGems first.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/activeagents/solid_agent.