Skip to content

Extract the agent configuration records into SolidAgent::Records - #6

Merged
TonsOfFun merged 2 commits into
mainfrom
claude/agent-records
Aug 15, 2026
Merged

Extract the agent configuration records into SolidAgent::Records#6
TonsOfFun merged 2 commits into
mainfrom
claude/agent-records

Conversation

@TonsOfFun

Copy link
Copy Markdown
Contributor

Folds the ActiveAgents platform's drifted model copies back onto the gem, as tracked in activeagent's docs/framework/v2-extraction-roadmap.md:

Fold the platform's drifted model copies back onto the generator templates once the app's Gemfile.lock reaches solid_agent 0.2.

The platform app has run Agent, AgentVersion, AgentTemplate and AgentRun as host-owned models for a while. activeagent's dashboard ships an older, poorer copy of the same four — which the roadmap already flags as orphaned: "ships orphaned platform-shaped models with no controllers or routes. Decide: wire them or drop them from the gem."

The shape

The gem ships behavior only. Agent, AgentVersion, AgentTemplate, AgentRun and Ownable are ActiveSupport::Concerns under SolidAgent::Records; the model classes stay host-owned in app/models.

Three reasons the models are not gem-owned:

  1. activeagent's dashboard cannot depend on solid_agent — solid_agent already depends on activeagent, so the reverse edge is a cycle. Naming the models with configurable strings and resolving them at call time is what lets the dashboard and a plain host app read the same tables with neither gem requiring the other.
  2. Engine-namespacing them as SolidAgent::Agent would break production data.isolate_namespace would resolve the table to solid_agent_agents, and would invalidate the contextable_type: "Agent" strings already in production agent_contexts rows.
  3. Agent configuration is what applications most want to extend. A host-owned model can be edited; a gem-owned one can only be monkey-patched.

Every cross-model reference goes through SolidAgent.agent_class and its siblings, resolved with safe_constantize at call time — never at load, which would deadlock the Rails autoloader or pin a stale class across a reload.

SolidAgent.agent_model# => Agent, or nil when the host has not generated itSolidAgent.agent_model!# => raises with instructionsSolidAgent.records_installed?# => false when the constant is missing OR the migration has not runSolidAgent.run_executor# => callable (agent_record, run); default raises

records_installed? answers false in both failure modes so a consumer that must degrade — the dashboard being the motivating one — can check once instead of failing late.

run_executor is the seam for actually running an agent. Building a class from stored provider/model/instructions is execution, which belongs to activeagent and the host, so the gem defines the contract and its default raises with instructions rather than returning mock data the way the dashboard's stub does (agent.rb, build_and_execute_agent, a TODO returning "Mock response for: ...").

What deliberately did not come across

A persistence gem should not carry:

  • seed_defaults! — 135 lines of marketing copy and emoji with model IDs pinned to gpt-4o and claude-sonnet-4-20250514. A gem shipping that has its release cadence set by vendor deprecations.
  • The merchandising columns and scopesfeatured, free_tier, public, usage_count, by_category, popular. A gem whose default schema asserts a pricing model on every consumer is wrong. Template usage now emits template.used.solid_agent and a dashboard counts.
  • The closed vocabulariesPRESET_TYPES, INSTRUCTION_SETS, AVAILABLE_TOOLS, PROVIDERS. No inclusion validation uses them, they are defined by a React component that does not ship in this repo, and PROVIDERS has already drifted once (Requesty).
  • to_agent_class_code — code generation is not persistence.
  • ActionCable broadcasting — becomes a notification a dashboard subscribes to.

preset_type and appearance are the deliberate exception. They are cosmetic, but they already live inside configuration_snapshot in production agent_versions rows, so dropping them would make existing version rows lossy on restore.

Decisions I made — please sanity-check these

  • Owner association defaults to :user with a class_attribute escape hatch, matching production. Polymorphic would force the platform to migrate a live user_id carrying three indexes for no present benefit.
  • agent_runs adopts the platform's representation (integer status enum, agent_id). solid_agent's polymorphic runnable and instructions_digest survive as additions, and the events column name is a class_attribute so neither side renames a live column. subject returns runnable || agent.
  • AgentVersion#diff was fixed, not ported. The original iterates only its own snapshot keys, so a key removed in the newer version was silently missed.
  • Versioning is suppressed for observed agents — the telemetry registrar rewrites their model attribute from ingest, which would otherwise churn a version per trace.
  • with_tool is guarded. It uses Postgres containment and raises on every other adapter today; now it uses containment on Postgres with a portable fallback elsewhere.
  • activeagent is untouched by this PR. Deleting the orphaned dashboard models is the destructive half and deserves its own reviewable change.

Verification

  • Unit: 227 runs, 0 failures.Records: 192 runs, 0 failures (new).
  • require "solid_agent" in a process with no ActiveRecord and no Rails loads clean — verified. The concerns are required eagerly, which is safe because nothing in them touches an ActiveRecord API until a host model includes one.
  • No-cycle invariant holds: no ActiveAgent::Dashboard reference anywhere in lib/.
  • The only constantize sits inside a define_method block, so nothing resolves at load time.

Also fixed along the way: SolidAgent.context_class/message_class/generation_class had zero consumers while the shipped initializer told hosts to set them, so uncommenting it did nothing. And the chained delete_suffix("Context").delete_suffix("Session") reduced SessionContext to "", yielding a bare Message/Generation pair that collided across every context in an app.

The unit harness also lost its hand-rolled String inflections — they were defined afterrequire "solid_agent" and had been shadowing ActiveSupport's, so the suite was exercising a toy camelize while production ran the real one.

Not in this PR

The solid_agent:agents generator (migrations + model templates) and the solid_agent:upgrade path for apps that already own an agent_runs table. The concerns and their schema are the reviewable unit; wiring the generator on top is mechanical once this shape is agreed.


Generated by Claude Code

Groundwork for moving the agent / version / template / run models out of
activeagent's dashboard engine and into solid_agent. Two prerequisites, both
verifiable on their own.
A real database for record tests. test/test_helper.rb mocks ActiveRecord::Base
so the unit suite runs with no database and no Rails, which is worth keeping —
but it means an ActiveRecord concern tested under it only tests the mock. The
record concerns this refactor introduces are validations, enums, callbacks,
scopes and associations, so test/records/ boots a real ActiveRecord on sqlite
:memory: with the schema the generator emits. It needs its own process for the
same reason test/integration/ does: the unit harness defines a mock Rails
constant, and ActiveAgent requires its railtie whenever Rails is defined.
Postgres-only behavior (jsonb containment, partial unique indexes) is out of
scope here; sqlite is the portable floor, not the target.
Fix the model-name configuration seam. SolidAgent.context_class,
message_class and generation_class had no consumers, while the shipped
initializer template told hosts to set them — so uncommenting it did nothing.
HasContext#infer_class_names hardcoded the three names; it now reads the
config, which is what makes the documented knob real.
Fix sibling-name derivation. The chained
delete_suffix("Context").delete_suffix("Session") reduced "SessionContext" to
"", yielding a bare "Message"/"Generation" pair that collides across every
context in an app. Extracted to SolidAgent::ModelNaming, which strips at most
one suffix and is now the single place that knows the rule — the context
generator derives the same names independently today, which is how they drifted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbmULyN7NyPwv62s7eWRmD
The platform app has run Agent, AgentVersion, AgentTemplate and AgentRun as
host-owned models for a while, and activeagent's dashboard ships an older,
poorer copy of the same four with no controllers or routes pointing at them.
docs/framework/v2-extraction-roadmap.md in activeagent tracks folding the
platform's drifted copies back onto solid_agent's templates; this is that.
The gem ships behavior only. Agent, AgentVersion, AgentTemplate, AgentRun and
Ownable are ActiveSupport::Concerns under SolidAgent::Records; the model
classes stay host-owned in app/models. Three reasons the models are not
gem-owned:
- activeagent's dashboard cannot depend on solid_agent, because solid_agent
already depends on activeagent and the reverse edge is a cycle. Naming the
models with configurable strings and resolving them at call time is what
lets the dashboard and a plain host app read the same tables with neither
gem requiring the other.
- Engine-namespacing them as SolidAgent::Agent would make isolate_namespace
resolve the table to solid_agent_agents, and would invalidate the
contextable_type: "Agent" strings already in production agent_contexts rows.
- Agent configuration is what applications most want to extend. A host-owned
model can be edited; a gem-owned one can only be monkey-patched.
Every cross-model reference goes through SolidAgent.agent_class and its
siblings, resolved with safe_constantize at call time — never at load, which
would deadlock the Rails autoloader or pin a stale class across a reload.
SolidAgent.records_installed? answers false both when the constant is missing
and when the migration has not run, so consumers can degrade rather than fail
late. SolidAgent.run_executor is the seam for actually running an agent:
building a class from stored provider/model/instructions is execution, which
belongs to activeagent and the host, so the gem defines the contract and its
default raises with instructions rather than returning mock data the way the
dashboard's stub does.
What deliberately did not come across, because a persistence gem should not
carry it: the 135 lines of seed_defaults! marketing copy with model IDs pinned
to gpt-4o and claude-sonnet-4; the merchandising columns and scopes
(featured, free_tier, public, usage_count, by_category, popular) — a gem whose
default schema asserts a pricing model is wrong, so template usage emits a
notification and a dashboard counts; the closed vocabularies PRESET_TYPES,
INSTRUCTION_SETS, AVAILABLE_TOOLS and PROVIDERS, defined by a React component
that does not ship in this repo and already drifted once; to_agent_class_code,
since code generation is not persistence; and ActionCable broadcasting, which
becomes a notification a dashboard subscribes to.
preset_type and appearance are the deliberate exception. They are cosmetic,
but they already live inside configuration_snapshot in production
agent_versions rows, so dropping them would make existing version rows lossy
on restore.
Tests run against a real ActiveRecord on sqlite :memory: in test/records/,
which needs its own process for the same reason test/integration/ does. Along
the way the unit harness lost its hand-rolled ActiveSupport shims: its
String inflections were defined after require "solid_agent" and had been
shadowing ActiveSupport's, so the suite was exercising a toy camelize while
production ran the real one. The gem now requires the ActiveSupport pieces it
calls, so it loads from a plain Ruby process.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbmULyN7NyPwv62s7eWRmD
@superconductor-for-github

Copy link
Copy Markdown

Superconductor is workingView implementation


I'll get back to you soon!

@TonsOfFun
TonsOfFun marked this pull request as ready for review August 15, 2026 02:36
@TonsOfFun
TonsOfFun merged commit 7fe3ce1 into mainAug 15, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@TonsOfFun@claude