SolidAgent
ActiveAgent runs agents; it deliberately doesn't store anything. No Active Record, no tables, no migrations — a generation happens and the response is yours to do something with.
SolidAgent is the gem that remembers. It adds database-backed persistence for everything an agent does: the conversation and its full tool/MCP exchange, every generation with tokens and provenance, agent-curated long-term memory, reasoning traces, durable run records, and cost estimates on top of the token counts.
It is a separate gem because persistence is a real dependency — installing activeagent shouldn't drag in Active Record for apps that never need it. solid_agent depends on activeagent, never the other way round.
# Gemfile
gem "activeagent"
gem "solid_agent"bundle install
rails generate solid_agent:install
rails db:migrateAlready running the dashboard?
The dashboard engine (actionagent) depends on solid_agent and installs its own copy of this schema, prefixed and namespaced under ActionAgent::. Your app's own agents still want the generator above — the two sets of tables are independent.
What the generator installs
Migrations and models, into app/models/, where they're yours to edit. SolidAgent's concerns talk to them through a duck-typed contract, so renaming a class or adding columns is a supported thing to do rather than a fork.
| Model | Holds |
|---|---|
AgentContext | One conversation or task session: agent, action, the record it's about, instructions, cumulative tokens, trace_id |
AgentMessage | Every turn — user, assistant, system and tool — with tool call ids, arguments, results, attachments and a content checksum |
AgentGeneration | One provider call: content, model, finish reason, input/output/cached/reasoning tokens, duration, raw payload, provenance |
AgentMemory / AgentMemoryEntry | Agent-curated notes about a subject record, with source_agent provenance |
AgentRun | One execution: lifecycle status, input, output, an append-only progress stream, and an instructions fingerprint |
The concerns
Include what you need; nothing is all-or-nothing.
| Concern | Adds |
|---|---|
HasContext | has_context — persists prompts, responses and the tool stream, and replays them on the next turn |
HasMemory | has_memory — save_memory / recall_memory tools the model calls, scoped to a subject so agents hand off through it |
HasTools | has_tools / tool — tool schemas from JSON view templates or an inline DSL |
StreamsToolUpdates | tool_description — broadcasts "what is it doing" over ActionCable while tools run |
HasReasons | has_reasons — collects extended-thinking output; Reasonable persists it on generation records |
And three things that are useful without an agent at all:
| Module | Does |
|---|---|
ToolCache | Caches tool/MCP results by (tool, normalized args) with a TTL |
ModelPricing | Turns token counts into estimated USD |
AgentManifest | Reads, validates, converts and builds agents from portable .agent.md, Dotprompt and CrewAI files |
The shortest useful example
class SupportAgent < ApplicationAgent
include SolidAgent::HasContext
# class_name points the named context at the installed models; without it,
# :conversation infers Conversation / ConversationMessage /
# ConversationGeneration. See Conversation Context for why.
has_context :conversation, class_name: "AgentContext", contextual: :user
def answer
load_conversation(contextable: params[:user])
prompt messages: conversation_messages + [
{ role: "user", content: params[:message] }
]
end
endSupportAgent.with(user: user, message: "My invoice is wrong").answer.generate_now
SupportAgent.with(user: user, message: "It's the VAT line").answer.generate_now
AgentContext.for_agent("SupportAgent").find_by(contextable: user)
.messages.chronological.map { |m| [ m.role, m.content ] }
# => [["user", "My invoice is wrong"], ["assistant", "..."],
# ["user", "It's the VAT line"], ["assistant", "..."]]Two requests, four rows, no session state — and the second request knew about the first because it read the table, not a cache.
Where to go next
- Conversation context —
has_contextin full: naming, multiple contexts, the tool stream, provenance and trace correlation - Long-term memory — notes an agent curates itself, and hand-offs between agents
- Tools, streaming and caching — schemas, live status, cached results
- Reasoning — capturing and persisting extended thinking
- Runs, cohorts and cost — durable run records, progress events, instruction cohorts, spend
- Agent manifests — agents defined in files, portable across frameworks
- Examples — a worked example per concern
Related
- Dev Console (Dashboard Engine) — reads this schema and renders it
- Telemetry — the
trace_idthat joins generations to traces - Tools — the framework's own tool calling, which
HasToolswrites schemas for - solid_agent on GitHub