solid_agent 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -0
  3. data/LICENSE +21 -0
  4. data/README.md +231 -18
  5. data/Rakefile +22 -2
  6. data/docs/agent-md-spec.md +803 -0
  7. data/docs/parser-design.md +1369 -0
  8. data/docs/registry-api.md +882 -0
  9. data/examples/README.md +60 -0
  10. data/examples/manifests/changelog_writer.agent.md +81 -0
  11. data/examples/manifests/usage.rb +96 -0
  12. data/examples/memory_handoff/app/agents/researcher_agent.rb +36 -0
  13. data/examples/memory_handoff/app/agents/writer_agent.rb +41 -0
  14. data/examples/memory_handoff/usage.rb +45 -0
  15. data/examples/persistent_conversation/app/agents/support_agent.rb +59 -0
  16. data/examples/persistent_conversation/app/controllers/support_conversations_controller.rb +24 -0
  17. data/examples/persistent_conversation/app/views/agents/support/instructions.md.erb +8 -0
  18. data/examples/persistent_conversation/usage.rb +51 -0
  19. data/examples/reasoning/app/agents/analysis_agent.rb +52 -0
  20. data/examples/reasoning/usage.rb +52 -0
  21. data/examples/run_tracking/app/agents/report_agent.rb +30 -0
  22. data/examples/run_tracking/app/controllers/agent_runs_controller.rb +43 -0
  23. data/examples/run_tracking/app/jobs/document_analysis_job.rb +17 -0
  24. data/examples/run_tracking/app/services/document_analysis_run.rb +68 -0
  25. data/examples/run_tracking/usage.rb +85 -0
  26. data/examples/tool_streaming/app/agents/browser_agent.rb +65 -0
  27. data/examples/tool_streaming/app/channels/tool_status_channel.rb +24 -0
  28. data/examples/tool_streaming/app/views/browser_agent/tools/fetch_url.json.erb +15 -0
  29. data/examples/tool_streaming/usage.rb +47 -0
  30. data/lib/generators/solid_agent/agent/agent_generator.rb +2 -2
  31. data/lib/generators/solid_agent/agent/templates/agent.rb.erb +3 -3
  32. data/lib/generators/solid_agent/context/templates/context_model.rb.erb +50 -16
  33. data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +8 -0
  34. data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +4 -0
  35. data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +11 -0
  36. data/lib/generators/solid_agent/install/install_generator.rb +9 -0
  37. data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +60 -17
  38. data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +23 -6
  39. data/lib/generators/solid_agent/install/templates/agent_memory.rb.erb +51 -0
  40. data/lib/generators/solid_agent/install/templates/agent_memory_entry.rb.erb +12 -0
  41. data/lib/generators/solid_agent/install/templates/agent_run.rb.erb +122 -0
  42. data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +13 -0
  43. data/lib/generators/solid_agent/install/templates/create_agent_memories.rb.erb +35 -0
  44. data/lib/generators/solid_agent/install/templates/create_agent_messages.rb.erb +5 -0
  45. data/lib/generators/solid_agent/install/templates/create_agent_runs.rb.erb +46 -0
  46. data/lib/generators/solid_agent/manifest/manifest_generator.rb +209 -0
  47. data/lib/generators/solid_agent/manifest/templates/agent.md.erb +39 -0
  48. data/lib/generators/solid_agent/manifest/templates/prompt.erb +13 -0
  49. data/lib/generators/solid_agent/reasons/reasons_generator.rb +83 -0
  50. data/lib/generators/solid_agent/reasons/templates/add_reasoning_columns.rb.erb +12 -0
  51. data/lib/solid_agent/agent_manifest/agent_builder.rb +323 -0
  52. data/lib/solid_agent/agent_manifest/errors.rb +26 -0
  53. data/lib/solid_agent/agent_manifest/exporter_registry.rb +117 -0
  54. data/lib/solid_agent/agent_manifest/exporters/agent_md_exporter.rb +115 -0
  55. data/lib/solid_agent/agent_manifest/exporters/base_exporter.rb +152 -0
  56. data/lib/solid_agent/agent_manifest/exporters/crewai_exporter.rb +125 -0
  57. data/lib/solid_agent/agent_manifest/exporters/dotprompt_exporter.rb +92 -0
  58. data/lib/solid_agent/agent_manifest/input_schema.rb +154 -0
  59. data/lib/solid_agent/agent_manifest/manifest.rb +306 -0
  60. data/lib/solid_agent/agent_manifest/parser_registry.rb +185 -0
  61. data/lib/solid_agent/agent_manifest/parsers/agent_md_parser.rb +87 -0
  62. data/lib/solid_agent/agent_manifest/parsers/base_parser.rb +223 -0
  63. data/lib/solid_agent/agent_manifest/parsers/crewai_parser.rb +201 -0
  64. data/lib/solid_agent/agent_manifest/parsers/dotprompt_parser.rb +122 -0
  65. data/lib/solid_agent/agent_manifest/parsers/github_prompt_parser.rb +143 -0
  66. data/lib/solid_agent/agent_manifest/picoschema.rb +254 -0
  67. data/lib/solid_agent/agent_manifest/registry/auth.rb +103 -0
  68. data/lib/solid_agent/agent_manifest/registry/client.rb +384 -0
  69. data/lib/solid_agent/agent_manifest/resource.rb +103 -0
  70. data/lib/solid_agent/agent_manifest/tool.rb +160 -0
  71. data/lib/solid_agent/agent_manifest/validator.rb +368 -0
  72. data/lib/solid_agent/agent_manifest.rb +381 -0
  73. data/lib/solid_agent/has_context.rb +299 -32
  74. data/lib/solid_agent/has_memory.rb +136 -0
  75. data/lib/solid_agent/has_reasons.rb +230 -0
  76. data/lib/solid_agent/model_naming.rb +42 -0
  77. data/lib/solid_agent/model_pricing.rb +93 -0
  78. data/lib/solid_agent/reasonable/reason.rb +205 -0
  79. data/lib/solid_agent/reasonable.rb +181 -0
  80. data/lib/solid_agent/records/agent.rb +520 -0
  81. data/lib/solid_agent/records/agent_run.rb +520 -0
  82. data/lib/solid_agent/records/agent_template.rb +142 -0
  83. data/lib/solid_agent/records/agent_version.rb +141 -0
  84. data/lib/solid_agent/records/ownable.rb +130 -0
  85. data/lib/solid_agent/records.rb +152 -0
  86. data/lib/solid_agent/run_fingerprint.rb +51 -0
  87. data/lib/solid_agent/tool_cache.rb +91 -0
  88. data/lib/solid_agent/version.rb +1 -1
  89. data/lib/solid_agent.rb +70 -3
  90. data/solid_agent.gemspec +41 -0
  91. metadata +93 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52f173d6c45fc416105fb4a93760dc7aacc81061b75bada9820fce835e9652dd
4
- data.tar.gz: '07348df42b87220dea84691d61a11cac6e6576cebbcbe2272c702a0bed6f539c'
3
+ metadata.gz: 73594fa8639580da1945c6445fad2fef3eaf0500e0f48fd8842115c9f4e2ec33
4
+ data.tar.gz: 250018ddb7579ff3aabad5da2f8f7b43cf53d963a5d25ad0a241721d3591a304
5
5
  SHA512:
6
- metadata.gz: ffd6386b0d71a95df1ac198c9590bbf337bf7e1a37ccc19e17eb57afbbcf544260fe7e2e552ba8470d4091d636a5b7b074aa5e7745d9d17021af839704b81f46
7
- data.tar.gz: bcb89c355432829006b38157c727c271b4b63c86aa09a67d14611b16896f515406dfbf5d84f45c1b2b53a9e4adae6d4cfa250397271f3bb0f8e029f15417d5b4
6
+ metadata.gz: fb3671bd7db8f8616ef267cdbab425a3280cf8961346ab2e1643557245bea283e0dacf97c3a656ce647f9718714541fbcd547043c7c488a0f478d73ea634d33c
7
+ data.tar.gz: 1e65df9005fdecceb369a2b30427c2003bb8f658bc2211e9ff2144a5b6fd72a40f7541df83e74f8a2eefab92498b5823bd4f9061209ec3ad0c617fd2fdc06604
data/CHANGELOG.md ADDED
@@ -0,0 +1,99 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## Unreleased
9
+
10
+ ## [0.2.1] - 2026-10-07
11
+
12
+ A patch release for activeagent 1.9, whose generations can pause to ask the
13
+ user and resume with the answer.
14
+
15
+ ### Added
16
+
17
+ - **`HasContext#resuming_generation?`** answers whether a generation resumes
18
+ one that paused for user input: true when the agent defines a public
19
+ `resuming?` that returns true, or after `self.resuming_generation = true`.
20
+ The writer is for a host that replays a stored conversation itself. Both
21
+ are private, so neither becomes one of the agent's actions.
22
+
23
+ ### Fixed
24
+
25
+ - **`HasContext` skips persisting the response of a generation paused for
26
+ user input, and the prompt of the generation that resumes it.** A response
27
+ answering `awaiting_input?` with true is returned untouched: no generation
28
+ record, no assistant row and no tool rows, because its last message is an
29
+ unfinished turn. The paused generation's prompt, the user turn, is still
30
+ persisted. The resumed generation skips prompt persistence, because its
31
+ prompt replays that turn, and persists its response as usual. Both checks
32
+ are duck-typed, so framework releases without a pause behave as before.
33
+
34
+ - Tool messages are now deduped by `tool_call_id` within one response's
35
+ stack as well as against rows already on the context, so a stack that
36
+ repeats a call persists it once even when the context's `messages` scope
37
+ cannot see rows written earlier in the same pass.
38
+
39
+ ## [0.2.0] - 2026-08-18
40
+
41
+ ### Added
42
+
43
+ - **`SolidAgent::Records::*` — behavior for the agent-configuration records.**
44
+ `Agent`, `AgentVersion`, `AgentTemplate`, `AgentRun` and `Ownable` ship as
45
+ concerns; the model classes stay host-owned in `app/models`. Every
46
+ cross-model reference resolves through `SolidAgent.agent_class` and its
47
+ siblings at call time, so the gem never names `Agent` as a constant and never
48
+ constantizes during load. The concerns are required eagerly, which is safe
49
+ because nothing in them touches an ActiveRecord API until a host model
50
+ includes one — `require "solid_agent"` in a process with no ActiveRecord
51
+ defines the modules and loads nothing else.
52
+
53
+ This folds the ActiveAgents platform's drifted model copies back onto the
54
+ gem, as tracked in activeagent's `docs/framework/v2-extraction-roadmap.md`.
55
+
56
+ - **`SolidAgent.run_executor`** — the seam for executing an agent record.
57
+ Building an agent class from stored provider/model/instructions is
58
+ execution, which belongs to activeagent and the host, so the gem defines the
59
+ contract and the host fills it. The default raises with instructions rather
60
+ than returning mock data.
61
+
62
+ - **`SolidAgent.records_installed?`** — answers false both when the model
63
+ constant is missing and when its migration has not run, so a consumer that
64
+ must degrade (activeagent's dashboard being the motivating one) can check
65
+ once instead of failing late.
66
+
67
+ - **Record test harness** (`test/records/`, `rake test:records`) running
68
+ against a real ActiveRecord on sqlite `:memory:`. The unit harness mocks
69
+ `ActiveRecord::Base`, which would put a fake under the real one, so the two
70
+ cannot share a process. `rake` runs both.
71
+
72
+ - **`CHANGELOG.md`**, which the gemspec already advertised.
73
+
74
+ ### Fixed
75
+
76
+ - `SolidAgent.context_class`, `message_class` and `generation_class` had no
77
+ consumers while the shipped initializer template told hosts to set them, so
78
+ uncommenting it did nothing. `HasContext#infer_class_names` now reads them.
79
+
80
+ - Sibling class-name derivation chained
81
+ `delete_suffix("Context").delete_suffix("Session")`, reducing
82
+ `SessionContext` to `""` and yielding a bare `Message`/`Generation` pair that
83
+ collided across every context in an app. Extracted to
84
+ `SolidAgent::ModelNaming`, which strips at most one suffix and is now the
85
+ single place that knows the rule — the context generator derived the same
86
+ names independently, which is how they drifted.
87
+
88
+ - `require "solid_agent"` outside Rails raised `NameError` on
89
+ `ActiveSupport::Concern`; the gem assumed a host had already loaded
90
+ ActiveSupport for it. It now requires the pieces it calls. The unit
91
+ harness's hand-rolled String inflections went with it — they were defined
92
+ *after* `require "solid_agent"` and had been shadowing ActiveSupport's, so
93
+ the suite was exercising a toy `camelize` while production ran the real one.
94
+
95
+ ### Changed
96
+
97
+ - `activemodel` is now a declared dependency. `AgentManifest` has always been
98
+ an ActiveModel; it arrived transitively through `activerecord`, and a require
99
+ deserves a declaration.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Active Agents AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,13 +1,41 @@
1
+ <p align="center">
2
+ <img src="assets/solid_agent.png" alt="SolidAgent" width="200">
3
+ </p>
4
+
1
5
  # SolidAgent
2
6
 
3
- SolidAgent extends the [ActiveAgent](https://github.com/activeagents/activeagent) framework with enterprise-grade features for building robust AI agents in Rails applications. It provides three core concerns that add database-backed persistence, declarative tool schemas, and real-time streaming capabilities to your agents.
7
+ [![Gem Version](https://img.shields.io/gem/v/solid_agent?logo=rubygems&color=CC342D)](https://rubygems.org/gems/solid_agent)
8
+ [![Downloads](https://img.shields.io/gem/dt/solid_agent?label=downloads)](https://rubygems.org/gems/solid_agent)
9
+ [![CI](https://github.com/activeagents/solid_agent/actions/workflows/ci.yml/badge.svg)](https://github.com/activeagents/solid_agent/actions/workflows/ci.yml)
10
+ [![Docs](https://img.shields.io/badge/docs-docs.activeagents.ai%2Fsolid__agent-2563eb)](https://docs.activeagents.ai/solid_agent)
11
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0-CC342D)](https://www.ruby-lang.org)
12
+ [![ActiveAgent](https://img.shields.io/badge/activeagent-%3E%3D%201.0-D30001)](https://github.com/activeagents/activeagent)
13
+ [![License](https://img.shields.io/github/license/activeagents/solid_agent)](LICENSE)
14
+
15
+ SolidAgent extends the [ActiveAgent](https://github.com/activeagents/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.
16
+
17
+ **[Documentation](https://docs.activeagents.ai/solid_agent)** ·
18
+ **[Examples](examples)** ·
19
+ **[`.agent.md` spec](docs/agent-md-spec.md)**
4
20
 
5
21
  ## Features
6
22
 
7
- - **HasContext** - Database-backed prompt context management for maintaining conversation history and agent state
23
+ Agent-side concerns:
24
+
25
+ - **HasContext** - Database-backed prompt context management for maintaining conversation history and agent state, including the full tool/MCP interaction stream
26
+ - **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
8
27
  - **HasTools** - Declarative, schema-based tool definitions compatible with LLM function-calling APIs
28
+ - **HasReasons** - Capture and inspect extended-thinking/reasoning output across a generation
9
29
  - **StreamsToolUpdates** - Real-time UI feedback during tool execution via ActionCable
10
30
 
31
+ Model-side and standalone:
32
+
33
+ - **Reasonable** - Persist reasoning content/tokens/metadata on your generation records
34
+ - **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
35
+ - **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`
36
+ - **ModelPricing** - Token-count → estimated USD cost, using RubyLLM's model registry when available with a static pattern-table fallback
37
+ - **AgentManifest** - Load, validate, export, and build agent classes from portable manifests (`.agent.md`, dotprompt, CrewAI)
38
+
11
39
  ## Installation
12
40
 
13
41
  Add this line to your application's Gemfile:
@@ -22,20 +50,16 @@ And then execute:
22
50
  $ bundle install
23
51
  ```
24
52
 
25
- Or install it yourself as:
26
-
27
- ```bash
28
- $ gem install solid_agent
29
- ```
30
-
31
53
  ## Usage
32
54
 
33
55
  ### Quick Start
34
56
 
35
- Generate a new agent with context support:
57
+ Install the persistence tables and models (`AgentContext`, `AgentMessage`, `AgentGeneration`, `AgentMemory`, `AgentMemoryEntry`, `AgentRun`), then generate an agent with context support:
36
58
 
37
59
  ```bash
38
- $ rails generate solid_agent:agent WritingAssistant --context --context_name conversation --contextable user
60
+ $ rails generate solid_agent:install
61
+ $ rails db:migrate
62
+ $ rails generate solid_agent:agent WritingAssistant --context --context_name conversation --contextual user
39
63
  ```
40
64
 
41
65
  ### HasContext - Persistent Conversation History
@@ -46,12 +70,14 @@ Add database-backed context management to your agents:
46
70
  class WritingAssistantAgent < ApplicationAgent
47
71
  include SolidAgent::HasContext
48
72
 
49
- has_context :conversation, contextable: :user
73
+ has_context :conversation, class_name: "AgentContext", contextual: :user
50
74
 
51
75
  def improve
52
- load_conversation(contextable: current_user)
53
- add_conversation_user_message(params[:message])
54
- prompt messages: conversation_messages
76
+ load_conversation(contextable: params[:user]) # contextable is the polymorphic association
77
+
78
+ prompt messages: conversation_messages + [
79
+ { role: "user", content: params[:message] }
80
+ ]
55
81
  end
56
82
  end
57
83
  ```
@@ -63,6 +89,64 @@ This generates helper methods like:
63
89
  - `add_conversation_assistant_message(content)` - Add an AI response
64
90
  - `conversation_result` - Get the last assistant message
65
91
 
92
+ With `auto_save` on (the default), the last prompt message is persisted as
93
+ the user turn and the response as the assistant turn, both after the
94
+ provider call — so reach for `add_conversation_user_message` only with
95
+ `auto_save: false`, or the turn is stored twice.
96
+
97
+ A generation that pauses for user input (its response answers
98
+ `awaiting_input?` with true) persists only its prompt, the user turn. The
99
+ paused response writes no generation record, assistant row or tool rows. The
100
+ generation that resumes it persists the tool results and the final answer,
101
+ and skips its own prompt, which replays the user turn already stored. Tool
102
+ results are deduped by `tool_call_id`, so each is stored once.
103
+
104
+ The resumed generation writes to whichever context it holds, so it must load
105
+ the paused generation's context. Give `has_context` a `contextual:` param
106
+ that names the same record on both runs, or load the context by id in the
107
+ action (`load_conversation(context_id: ...)`).
108
+ Without either, each agent instance creates its own anonymous context, and
109
+ the user turn and the answer land in two different contexts.
110
+
111
+ The agent counts as resuming when the framework's `resuming?` returns true.
112
+ A host that replays a stored conversation itself sets the flag from inside
113
+ the agent:
114
+
115
+ ```ruby
116
+ before_generation { self.resuming_generation = params[:checkpoint].present? }
117
+ ```
118
+
119
+ > **Naming a context also names its models.** `has_context :conversation`
120
+ > infers `Conversation`, `ConversationMessage` and `ConversationGeneration`,
121
+ > not the `AgentContext` family the installer wrote — hence `class_name:`
122
+ > above, which infers `AgentMessage` and `AgentGeneration` alongside it.
123
+ > Unnamed `has_context` resolves to those models directly; for genuinely
124
+ > separate tables per context, run
125
+ > `rails generate solid_agent:context conversation`.
126
+
127
+ > **Note:** contexts are persisted under `self.class.name` — agents built
128
+ > with anonymous `Class.new(...)` must define a class name or context
129
+ > creation will fail the `agent_name` presence validation.
130
+
131
+ #### Telemetry trace correlation
132
+
133
+ Every persisted generation records a `trace_id` and a provenance snapshot
134
+ (agent/prompt/context checksums). Thread a distributed trace id — for
135
+ example an `ActiveAgent::Telemetry` trace — through prompt options and it
136
+ lands on the `agent_generations` row, joining conversation records to
137
+ telemetry traces:
138
+
139
+ ```ruby
140
+ def improve
141
+ prompt_options[:trace_id] = my_telemetry_trace_id
142
+ load_conversation(contextable: current_user)
143
+ prompt messages: conversation_messages
144
+ end
145
+ ```
146
+
147
+ Query with `AgentGeneration.with_trace(trace_id)` or
148
+ `AgentContext.with_trace(trace_id)`.
149
+
66
150
  ### HasTools - Declarative Tool Schemas
67
151
 
68
152
  Define tools inline with a clean DSL:
@@ -103,28 +187,157 @@ class BrowserAgent < ApplicationAgent
103
187
  end
104
188
  ```
105
189
 
190
+ ### HasMemory - Agent-Curated Long-Term Memory
191
+
192
+ 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:
193
+
194
+ ```ruby
195
+ class SupportAgent < ApplicationAgent
196
+ include SolidAgent::HasContext
197
+ include SolidAgent::HasMemory
198
+
199
+ has_context contextual: :user
200
+ has_memory # scope: "default", class_name: "AgentMemory"
201
+
202
+ def assist
203
+ load_context(contextable: params[:user])
204
+ prompt messages: context_messages, tools: memory_tool_definitions
205
+ end
206
+ end
207
+ ```
208
+
209
+ 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.
210
+
211
+ ### ToolCache - Cached Tool Results
212
+
213
+ ```ruby
214
+ result = SolidAgent::ToolCache.fetch(tool: "fetch_url", args: { url: url }, ttl: 300) do
215
+ expensive_call(url)
216
+ end
217
+ result[:cached] # => true on a replay
218
+ ```
219
+
220
+ Error-shaped results (`{ error: ... }`) are never cached, so transient failures don't stick; cache keys are stable across argument ordering and symbol/string keys.
221
+
222
+ ### AgentRun - Durable Run Records
223
+
224
+ 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:
225
+
226
+ ```ruby
227
+ run = AgentRun.create!(runnable: document, agent_name: "SupportAgent", input_prompt: message)
228
+ run.record_instructions(agent.instructions) # cohort fingerprint ("calm-heron")
229
+ run.start!
230
+ run.append_event(kind: "tool", label: "fetch_url", eid: "e1", status: "started")
231
+ # ... execute ...
232
+ run.append_event(kind: "tool", label: "fetch_url", eid: "e1", status: "done", duration_ms: 120)
233
+ run.complete!(output: response.message.content, input_tokens: usage.input_tokens, output_tokens: usage.output_tokens)
234
+ ```
235
+
236
+ `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.
237
+
238
+ ### ModelPricing - Estimated Spend
239
+
240
+ ```ruby
241
+ SolidAgent::ModelPricing.estimate(model: "claude-sonnet-5", input_tokens: 12_000, output_tokens: 800)
242
+ # => 0.048 (USD, estimated)
243
+ ```
244
+
245
+ The generated `AgentGeneration#estimated_cost` uses this automatically. Rates come from RubyLLM's registry when that gem is present, else a static pattern table.
246
+
106
247
  ### Generators
107
248
 
108
249
  ```bash
250
+ # Install persistence tables + models (contexts, messages, generations, memories)
251
+ $ rails generate solid_agent:install
252
+
109
253
  # Generate a new agent
110
254
  $ rails generate solid_agent:agent MyAgent
111
255
 
112
- # Generate with context support
256
+ # Generate with context support. --context_name emits
257
+ # `has_context :session`, which resolves Session/SessionMessage/
258
+ # SessionGeneration — pair it with the context generator below, or drop the
259
+ # option to use the installed AgentContext models.
113
260
  $ rails generate solid_agent:agent MyAgent --context --context_name session
114
261
 
115
262
  # Generate a tool template
116
263
  $ rails generate solid_agent:tool search MyAgent --parameters query:string:required
117
264
 
118
- # Generate context models
265
+ # Generate custom-named context models
119
266
  $ rails generate solid_agent:context conversation
267
+
268
+ # Add reasoning columns to a generation model
269
+ $ rails generate solid_agent:reasons AgentGeneration
270
+
271
+ # Scaffold an agent manifest (.agent.md)
272
+ $ rails generate solid_agent:manifest research
120
273
  ```
121
274
 
275
+ ## Examples
276
+
277
+ The [`examples/`](examples) directory has a worked example per concern —
278
+ agent classes, views, controllers and console walkthroughs laid out the way
279
+ they'd sit in a Rails app:
280
+
281
+ | Example | Concerns |
282
+ |---------|----------|
283
+ | [persistent_conversation](examples/persistent_conversation) | `HasContext` |
284
+ | [memory_handoff](examples/memory_handoff) | `HasMemory` |
285
+ | [tool_streaming](examples/tool_streaming) | `HasTools`, `StreamsToolUpdates`, `ToolCache` |
286
+ | [reasoning](examples/reasoning) | `HasReasons`, `Reasonable` |
287
+ | [run_tracking](examples/run_tracking) | `AgentRun`, `RunFingerprint`, `ModelPricing` |
288
+ | [manifests](examples/manifests) | `AgentManifest` |
289
+
290
+ The narrated versions live at
291
+ [docs.activeagents.ai/solid_agent](https://docs.activeagents.ai/solid_agent).
292
+
293
+ ## Example Apps
294
+
295
+ See SolidAgent in action:
296
+
297
+ - [Fizzy](https://github.com/tonsoffun/fizzy) - AI-enhanced Kanban tracking tool with writing, research, and file analysis agents
298
+ - [Writebook](https://github.com/tonsoffun/writebook) - Collaborative writing platform with integrated AI writing assistance, research, and document analysis
299
+
122
300
  ## Development
123
301
 
124
302
  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.
125
303
 
126
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
304
+ ```bash
305
+ bundle exec rake test
306
+ ```
307
+
308
+ ### Testing against ActiveAgent
309
+
310
+ This suite runs against mocks — deliberately, so it stays fast and
311
+ dependency-free — which means it can pass while these concerns no longer
312
+ compose with the framework they extend. ActiveAgent carries a dummy Rails
313
+ app and a cross-repo suite for exactly that. Point it at your working tree:
314
+
315
+ ```bash
316
+ git clone https://github.com/activeagents/activeagent ../activeagent
317
+ cd ../activeagent
318
+
319
+ SOLID_AGENT_PATH=../solid_agent \
320
+ BUNDLE_GEMFILE=gemfiles/solid_agent_main.gemfile \
321
+ SOLID_AGENT_STRICT=1 \
322
+ bin/test test/integration/solid_agent/*_test.rb \
323
+ actionagent/test/agent_execution_service_test.rb
324
+ ```
325
+
326
+ `SOLID_AGENT_STRICT=1` fails on anything the suite would otherwise skip for
327
+ a missing API — here the resolved gem *is* your checkout, so a skip means
328
+ something was removed. CI runs this on every pull request against
329
+ ActiveAgent's main branch and its latest release, and again nightly. See
330
+ [Releasing & Cross-Repo Testing](https://docs.activeagents.ai/contributing/releasing).
331
+
332
+ ### Releasing
333
+
334
+ Releases publish from a `v*` tag through
335
+ [.github/workflows/release.yml](.github/workflows/release.yml) using RubyGems
336
+ trusted publishing, gated on CI including the cross-repo suite. Bump
337
+ `SolidAgent::VERSION`, tag, push. This gem depends on `activeagent` and
338
+ `actionagent` depends on this gem, so anything requiring a new framework API
339
+ waits for that release to land on RubyGems first.
127
340
 
128
341
  ## Contributing
129
342
 
130
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/solid_agent.
343
+ Bug reports and pull requests are welcome on GitHub at https://github.com/activeagents/solid_agent.
data/Rakefile CHANGED
@@ -3,10 +3,30 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
5
 
6
+ # Two suites, two processes. Rake::TestTask forks one Ruby per task, which is
7
+ # the point: these harnesses cannot share a process.
8
+ #
9
+ # * test/ (unit) hand-rolls stand-ins for Rails and ActiveModel so the concerns
10
+ # run with no Rails and no database.
11
+ # * test/records/ boots a real ActiveRecord on sqlite :memory:. Loading it into
12
+ # the unit process would put a genuine ActiveRecord::Base underneath the mock
13
+ # one, and the two disagree about everything.
14
+ #
15
+ # Splitting them is what keeps both harnesses honest — and what makes a failure
16
+ # name the harness it happened in.
6
17
  Rake::TestTask.new(:test) do |t|
7
18
  t.libs << "test"
8
19
  t.libs << "lib"
9
- t.test_files = FileList["test/**/*_test.rb"]
20
+ t.test_files = FileList["test/**/*_test.rb"].exclude("test/records/**/*_test.rb")
10
21
  end
11
22
 
12
- task default: :test
23
+ namespace :test do
24
+ desc "Run the record concerns against a real ActiveRecord on sqlite"
25
+ Rake::TestTask.new(:records) do |t|
26
+ t.libs << "test"
27
+ t.libs << "lib"
28
+ t.test_files = FileList["test/records/**/*_test.rb"]
29
+ end
30
+ end
31
+
32
+ task default: [ :test, "test:records" ]