Skip to content

About

Persistence layer for active agent prompt context and generation response records

Resources

Stars

7 stars

Watchers

0 watching

Forks

Repository files navigation

SolidAgent

SolidAgent

Gem Version Downloads CI Docs Ruby ActiveAgent License

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_memory function-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 by Rails.cache; error results are never cached and replays are tagged cached: 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: conversation_messages + [
      { role: "user", content: params[:message] }
    ]
  end
end

This generates helper methods like:

  • load_conversation(contextable:) - Load or create a context
  • conversation_messages - Get formatted message history
  • add_conversation_user_message(content) - Add a user message
  • add_conversation_assistant_message(content) - Add an AI response
  • conversation_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.

A generation that pauses for user input (its response answers awaiting_input? with true) persists only its prompt, the user turn. The paused response writes no generation record, assistant row or tool rows. The generation that resumes it persists the tool results and the final answer, and skips its own prompt, which replays the user turn already stored. Tool results are deduped by tool_call_id, so each is stored once.

The resumed generation writes to whichever context it holds, so it must load the paused generation's context. Give has_context a contextual: param that names the same record on both runs, or load the context by id in the action (load_conversation(context_id: ...)). Without either, each agent instance creates its own anonymous context, and the user turn and the answer land in two different contexts.

The agent counts as resuming when the framework's resuming? returns true. A host that replays a stored conversation itself sets the flag from inside the agent:

before_generation { self.resuming_generation = params[:checkpoint].present? }

Naming a context also names its models. has_context :conversation infers Conversation, ConversationMessage and ConversationGeneration, not the AgentContext family the installer wrote — hence class_name: above, which infers AgentMessage and AgentGeneration alongside it. Unnamed has_context resolves to those models directly; for genuinely separate tables per context, run rails generate solid_agent:context conversation.

Note: contexts are persisted under self.class.name — agents built with anonymous Class.new(...) must define a class name or context creation will fail the agent_name presence 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
  prompt_options[:trace_id] = my_telemetry_trace_id
  load_conversation(contextable: current_user)
  prompt messages: conversation_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: context_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: 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.

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.

About

Persistence layer for active agent prompt context and generation response records

Resources

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages