Skip to content

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.

ruby
# Gemfile
gem "activeagent"
gem "solid_agent"
bash
bundle install
rails generate solid_agent:install
rails db:migrate

Already 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.

ModelHolds
AgentContextOne conversation or task session: agent, action, the record it's about, instructions, cumulative tokens, trace_id
AgentMessageEvery turn — user, assistant, system and tool — with tool call ids, arguments, results, attachments and a content checksum
AgentGenerationOne provider call: content, model, finish reason, input/output/cached/reasoning tokens, duration, raw payload, provenance
AgentMemory / AgentMemoryEntryAgent-curated notes about a subject record, with source_agent provenance
AgentRunOne 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.

ConcernAdds
HasContexthas_context — persists prompts, responses and the tool stream, and replays them on the next turn
HasMemoryhas_memorysave_memory / recall_memory tools the model calls, scoped to a subject so agents hand off through it
HasToolshas_tools / tool — tool schemas from JSON view templates or an inline DSL
StreamsToolUpdatestool_description — broadcasts "what is it doing" over ActionCable while tools run
HasReasonshas_reasons — collects extended-thinking output; Reasonable persists it on generation records

And three things that are useful without an agent at all:

ModuleDoes
ToolCacheCaches tool/MCP results by (tool, normalized args) with a TTL
ModelPricingTurns token counts into estimated USD
AgentManifestReads, validates, converts and builds agents from portable .agent.md, Dotprompt and CrewAI files

The shortest useful example

ruby
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
end
ruby
SupportAgent.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