solid_agent 0.1.1 → 0.2.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/LICENSE +21 -0
- data/README.md +209 -18
- data/Rakefile +22 -2
- data/docs/agent-md-spec.md +803 -0
- data/docs/parser-design.md +1369 -0
- data/docs/registry-api.md +882 -0
- data/examples/README.md +60 -0
- data/examples/manifests/changelog_writer.agent.md +81 -0
- data/examples/manifests/usage.rb +96 -0
- data/examples/memory_handoff/app/agents/researcher_agent.rb +36 -0
- data/examples/memory_handoff/app/agents/writer_agent.rb +41 -0
- data/examples/memory_handoff/usage.rb +45 -0
- data/examples/persistent_conversation/app/agents/support_agent.rb +59 -0
- data/examples/persistent_conversation/app/controllers/support_conversations_controller.rb +24 -0
- data/examples/persistent_conversation/app/views/agents/support/instructions.md.erb +8 -0
- data/examples/persistent_conversation/usage.rb +51 -0
- data/examples/reasoning/app/agents/analysis_agent.rb +52 -0
- data/examples/reasoning/usage.rb +52 -0
- data/examples/run_tracking/app/agents/report_agent.rb +30 -0
- data/examples/run_tracking/app/controllers/agent_runs_controller.rb +43 -0
- data/examples/run_tracking/app/jobs/document_analysis_job.rb +17 -0
- data/examples/run_tracking/app/services/document_analysis_run.rb +68 -0
- data/examples/run_tracking/usage.rb +85 -0
- data/examples/tool_streaming/app/agents/browser_agent.rb +65 -0
- data/examples/tool_streaming/app/channels/tool_status_channel.rb +24 -0
- data/examples/tool_streaming/app/views/browser_agent/tools/fetch_url.json.erb +15 -0
- data/examples/tool_streaming/usage.rb +47 -0
- data/lib/generators/solid_agent/agent/agent_generator.rb +2 -2
- data/lib/generators/solid_agent/agent/templates/agent.rb.erb +3 -3
- data/lib/generators/solid_agent/context/templates/context_model.rb.erb +50 -16
- data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +8 -0
- data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +4 -0
- data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +11 -0
- data/lib/generators/solid_agent/install/install_generator.rb +9 -0
- data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +60 -17
- data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +23 -6
- data/lib/generators/solid_agent/install/templates/agent_memory.rb.erb +51 -0
- data/lib/generators/solid_agent/install/templates/agent_memory_entry.rb.erb +12 -0
- data/lib/generators/solid_agent/install/templates/agent_run.rb.erb +122 -0
- data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +13 -0
- data/lib/generators/solid_agent/install/templates/create_agent_memories.rb.erb +35 -0
- data/lib/generators/solid_agent/install/templates/create_agent_messages.rb.erb +5 -0
- data/lib/generators/solid_agent/install/templates/create_agent_runs.rb.erb +46 -0
- data/lib/generators/solid_agent/manifest/manifest_generator.rb +209 -0
- data/lib/generators/solid_agent/manifest/templates/agent.md.erb +39 -0
- data/lib/generators/solid_agent/manifest/templates/prompt.erb +13 -0
- data/lib/generators/solid_agent/reasons/reasons_generator.rb +83 -0
- data/lib/generators/solid_agent/reasons/templates/add_reasoning_columns.rb.erb +12 -0
- data/lib/solid_agent/agent_manifest/agent_builder.rb +323 -0
- data/lib/solid_agent/agent_manifest/errors.rb +26 -0
- data/lib/solid_agent/agent_manifest/exporter_registry.rb +117 -0
- data/lib/solid_agent/agent_manifest/exporters/agent_md_exporter.rb +115 -0
- data/lib/solid_agent/agent_manifest/exporters/base_exporter.rb +152 -0
- data/lib/solid_agent/agent_manifest/exporters/crewai_exporter.rb +125 -0
- data/lib/solid_agent/agent_manifest/exporters/dotprompt_exporter.rb +92 -0
- data/lib/solid_agent/agent_manifest/input_schema.rb +154 -0
- data/lib/solid_agent/agent_manifest/manifest.rb +306 -0
- data/lib/solid_agent/agent_manifest/parser_registry.rb +185 -0
- data/lib/solid_agent/agent_manifest/parsers/agent_md_parser.rb +87 -0
- data/lib/solid_agent/agent_manifest/parsers/base_parser.rb +223 -0
- data/lib/solid_agent/agent_manifest/parsers/crewai_parser.rb +201 -0
- data/lib/solid_agent/agent_manifest/parsers/dotprompt_parser.rb +122 -0
- data/lib/solid_agent/agent_manifest/parsers/github_prompt_parser.rb +143 -0
- data/lib/solid_agent/agent_manifest/picoschema.rb +254 -0
- data/lib/solid_agent/agent_manifest/registry/auth.rb +103 -0
- data/lib/solid_agent/agent_manifest/registry/client.rb +384 -0
- data/lib/solid_agent/agent_manifest/resource.rb +103 -0
- data/lib/solid_agent/agent_manifest/tool.rb +160 -0
- data/lib/solid_agent/agent_manifest/validator.rb +368 -0
- data/lib/solid_agent/agent_manifest.rb +381 -0
- data/lib/solid_agent/has_context.rb +251 -30
- data/lib/solid_agent/has_memory.rb +136 -0
- data/lib/solid_agent/has_reasons.rb +230 -0
- data/lib/solid_agent/model_naming.rb +42 -0
- data/lib/solid_agent/model_pricing.rb +93 -0
- data/lib/solid_agent/reasonable/reason.rb +205 -0
- data/lib/solid_agent/reasonable.rb +181 -0
- data/lib/solid_agent/records/agent.rb +520 -0
- data/lib/solid_agent/records/agent_run.rb +520 -0
- data/lib/solid_agent/records/agent_template.rb +142 -0
- data/lib/solid_agent/records/agent_version.rb +141 -0
- data/lib/solid_agent/records/ownable.rb +130 -0
- data/lib/solid_agent/records.rb +152 -0
- data/lib/solid_agent/run_fingerprint.rb +51 -0
- data/lib/solid_agent/tool_cache.rb +91 -0
- data/lib/solid_agent/version.rb +1 -1
- data/lib/solid_agent.rb +70 -3
- metadata +87 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2b204dd708358a41a24ed81cb3e27a76c83b97bd6b69fb098eeaf9a3c54d4ddd
|
|
4
|
+
data.tar.gz: 3ee800cb2936615ee7ac57072da8a26c6152bf73e92088c8853a8cbcdd3b4a6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a9d1c5133162f5ebd32fe1cf90ac999cd9b0f0f9c64396eb4e59ef9b60ca3d871e79c5d6e83fe50a830bfbbea6677837ff5ad0e0ca8e32277b885c577d63e9c
|
|
7
|
+
data.tar.gz: 6e3d799d6cb5321f10455d2a954cb24d2c41a0768c88489a86eb24f6bfe84f5b4ae25fffc614e60f0edfd05c517dc6dfef66c79c078e125d9ed275fa581e27a9
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`SolidAgent::Records::*` — behavior for the agent-configuration records.**
|
|
13
|
+
`Agent`, `AgentVersion`, `AgentTemplate`, `AgentRun` and `Ownable` ship as
|
|
14
|
+
concerns; the model classes stay host-owned in `app/models`. Every
|
|
15
|
+
cross-model reference resolves through `SolidAgent.agent_class` and its
|
|
16
|
+
siblings at call time, so the gem never names `Agent` as a constant and never
|
|
17
|
+
constantizes during load. The concerns are required eagerly, which is safe
|
|
18
|
+
because nothing in them touches an ActiveRecord API until a host model
|
|
19
|
+
includes one — `require "solid_agent"` in a process with no ActiveRecord
|
|
20
|
+
defines the modules and loads nothing else.
|
|
21
|
+
|
|
22
|
+
This folds the ActiveAgents platform's drifted model copies back onto the
|
|
23
|
+
gem, as tracked in activeagent's `docs/framework/v2-extraction-roadmap.md`.
|
|
24
|
+
|
|
25
|
+
- **`SolidAgent.run_executor`** — the seam for executing an agent record.
|
|
26
|
+
Building an agent class from stored provider/model/instructions is
|
|
27
|
+
execution, which belongs to activeagent and the host, so the gem defines the
|
|
28
|
+
contract and the host fills it. The default raises with instructions rather
|
|
29
|
+
than returning mock data.
|
|
30
|
+
|
|
31
|
+
- **`SolidAgent.records_installed?`** — answers false both when the model
|
|
32
|
+
constant is missing and when its migration has not run, so a consumer that
|
|
33
|
+
must degrade (activeagent's dashboard being the motivating one) can check
|
|
34
|
+
once instead of failing late.
|
|
35
|
+
|
|
36
|
+
- **Record test harness** (`test/records/`, `rake test:records`) running
|
|
37
|
+
against a real ActiveRecord on sqlite `:memory:`. The unit harness mocks
|
|
38
|
+
`ActiveRecord::Base`, which would put a fake under the real one, so the two
|
|
39
|
+
cannot share a process. `rake` runs both.
|
|
40
|
+
|
|
41
|
+
- **`CHANGELOG.md`**, which the gemspec already advertised.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- `SolidAgent.context_class`, `message_class` and `generation_class` had no
|
|
46
|
+
consumers while the shipped initializer template told hosts to set them, so
|
|
47
|
+
uncommenting it did nothing. `HasContext#infer_class_names` now reads them.
|
|
48
|
+
|
|
49
|
+
- Sibling class-name derivation chained
|
|
50
|
+
`delete_suffix("Context").delete_suffix("Session")`, reducing
|
|
51
|
+
`SessionContext` to `""` and yielding a bare `Message`/`Generation` pair that
|
|
52
|
+
collided across every context in an app. Extracted to
|
|
53
|
+
`SolidAgent::ModelNaming`, which strips at most one suffix and is now the
|
|
54
|
+
single place that knows the rule — the context generator derived the same
|
|
55
|
+
names independently, which is how they drifted.
|
|
56
|
+
|
|
57
|
+
- `require "solid_agent"` outside Rails raised `NameError` on
|
|
58
|
+
`ActiveSupport::Concern`; the gem assumed a host had already loaded
|
|
59
|
+
ActiveSupport for it. It now requires the pieces it calls. The unit
|
|
60
|
+
harness's hand-rolled String inflections went with it — they were defined
|
|
61
|
+
*after* `require "solid_agent"` and had been shadowing ActiveSupport's, so
|
|
62
|
+
the suite was exercising a toy `camelize` while production ran the real one.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- `activemodel` is now a declared dependency. `AgentManifest` has always been
|
|
67
|
+
an ActiveModel; it arrived transitively through `activerecord`, and a require
|
|
68
|
+
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
|
-
|
|
7
|
+
[](https://rubygems.org/gems/solid_agent)
|
|
8
|
+
[](https://rubygems.org/gems/solid_agent)
|
|
9
|
+
[](https://github.com/activeagents/solid_agent/actions/workflows/ci.yml)
|
|
10
|
+
[](https://docs.activeagents.ai/solid_agent)
|
|
11
|
+
[](https://www.ruby-lang.org)
|
|
12
|
+
[](https://github.com/activeagents/activeagent)
|
|
13
|
+
[](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
|
-
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
73
|
+
has_context :conversation, class_name: "AgentContext", contextual: :user
|
|
50
74
|
|
|
51
75
|
def improve
|
|
52
|
-
load_conversation(contextable:
|
|
53
|
-
|
|
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,42 @@ 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
|
+
> **Naming a context also names its models.** `has_context :conversation`
|
|
98
|
+
> infers `Conversation`, `ConversationMessage` and `ConversationGeneration`,
|
|
99
|
+
> not the `AgentContext` family the installer wrote — hence `class_name:`
|
|
100
|
+
> above, which infers `AgentMessage` and `AgentGeneration` alongside it.
|
|
101
|
+
> Unnamed `has_context` resolves to those models directly; for genuinely
|
|
102
|
+
> separate tables per context, run
|
|
103
|
+
> `rails generate solid_agent:context conversation`.
|
|
104
|
+
|
|
105
|
+
> **Note:** contexts are persisted under `self.class.name` — agents built
|
|
106
|
+
> with anonymous `Class.new(...)` must define a class name or context
|
|
107
|
+
> creation will fail the `agent_name` presence validation.
|
|
108
|
+
|
|
109
|
+
#### Telemetry trace correlation
|
|
110
|
+
|
|
111
|
+
Every persisted generation records a `trace_id` and a provenance snapshot
|
|
112
|
+
(agent/prompt/context checksums). Thread a distributed trace id — for
|
|
113
|
+
example an `ActiveAgent::Telemetry` trace — through prompt options and it
|
|
114
|
+
lands on the `agent_generations` row, joining conversation records to
|
|
115
|
+
telemetry traces:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
def improve
|
|
119
|
+
prompt_options[:trace_id] = my_telemetry_trace_id
|
|
120
|
+
load_conversation(contextable: current_user)
|
|
121
|
+
prompt messages: conversation_messages
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Query with `AgentGeneration.with_trace(trace_id)` or
|
|
126
|
+
`AgentContext.with_trace(trace_id)`.
|
|
127
|
+
|
|
66
128
|
### HasTools - Declarative Tool Schemas
|
|
67
129
|
|
|
68
130
|
Define tools inline with a clean DSL:
|
|
@@ -103,28 +165,157 @@ class BrowserAgent < ApplicationAgent
|
|
|
103
165
|
end
|
|
104
166
|
```
|
|
105
167
|
|
|
168
|
+
### HasMemory - Agent-Curated Long-Term Memory
|
|
169
|
+
|
|
170
|
+
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:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
class SupportAgent < ApplicationAgent
|
|
174
|
+
include SolidAgent::HasContext
|
|
175
|
+
include SolidAgent::HasMemory
|
|
176
|
+
|
|
177
|
+
has_context contextual: :user
|
|
178
|
+
has_memory # scope: "default", class_name: "AgentMemory"
|
|
179
|
+
|
|
180
|
+
def assist
|
|
181
|
+
load_context(contextable: params[:user])
|
|
182
|
+
prompt messages: context_messages, tools: memory_tool_definitions
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
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.
|
|
188
|
+
|
|
189
|
+
### ToolCache - Cached Tool Results
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
result = SolidAgent::ToolCache.fetch(tool: "fetch_url", args: { url: url }, ttl: 300) do
|
|
193
|
+
expensive_call(url)
|
|
194
|
+
end
|
|
195
|
+
result[:cached] # => true on a replay
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Error-shaped results (`{ error: ... }`) are never cached, so transient failures don't stick; cache keys are stable across argument ordering and symbol/string keys.
|
|
199
|
+
|
|
200
|
+
### AgentRun - Durable Run Records
|
|
201
|
+
|
|
202
|
+
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:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
run = AgentRun.create!(runnable: document, agent_name: "SupportAgent", input_prompt: message)
|
|
206
|
+
run.record_instructions(agent.instructions) # cohort fingerprint ("calm-heron")
|
|
207
|
+
run.start!
|
|
208
|
+
run.append_event(kind: "tool", label: "fetch_url", eid: "e1", status: "started")
|
|
209
|
+
# ... execute ...
|
|
210
|
+
run.append_event(kind: "tool", label: "fetch_url", eid: "e1", status: "done", duration_ms: 120)
|
|
211
|
+
run.complete!(output: response.message.content, input_tokens: usage.input_tokens, output_tokens: usage.output_tokens)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`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.
|
|
215
|
+
|
|
216
|
+
### ModelPricing - Estimated Spend
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
SolidAgent::ModelPricing.estimate(model: "claude-sonnet-5", input_tokens: 12_000, output_tokens: 800)
|
|
220
|
+
# => 0.048 (USD, estimated)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The generated `AgentGeneration#estimated_cost` uses this automatically. Rates come from RubyLLM's registry when that gem is present, else a static pattern table.
|
|
224
|
+
|
|
106
225
|
### Generators
|
|
107
226
|
|
|
108
227
|
```bash
|
|
228
|
+
# Install persistence tables + models (contexts, messages, generations, memories)
|
|
229
|
+
$ rails generate solid_agent:install
|
|
230
|
+
|
|
109
231
|
# Generate a new agent
|
|
110
232
|
$ rails generate solid_agent:agent MyAgent
|
|
111
233
|
|
|
112
|
-
# Generate with context support
|
|
234
|
+
# Generate with context support. --context_name emits
|
|
235
|
+
# `has_context :session`, which resolves Session/SessionMessage/
|
|
236
|
+
# SessionGeneration — pair it with the context generator below, or drop the
|
|
237
|
+
# option to use the installed AgentContext models.
|
|
113
238
|
$ rails generate solid_agent:agent MyAgent --context --context_name session
|
|
114
239
|
|
|
115
240
|
# Generate a tool template
|
|
116
241
|
$ rails generate solid_agent:tool search MyAgent --parameters query:string:required
|
|
117
242
|
|
|
118
|
-
# Generate context models
|
|
243
|
+
# Generate custom-named context models
|
|
119
244
|
$ rails generate solid_agent:context conversation
|
|
245
|
+
|
|
246
|
+
# Add reasoning columns to a generation model
|
|
247
|
+
$ rails generate solid_agent:reasons AgentGeneration
|
|
248
|
+
|
|
249
|
+
# Scaffold an agent manifest (.agent.md)
|
|
250
|
+
$ rails generate solid_agent:manifest research
|
|
120
251
|
```
|
|
121
252
|
|
|
253
|
+
## Examples
|
|
254
|
+
|
|
255
|
+
The [`examples/`](examples) directory has a worked example per concern —
|
|
256
|
+
agent classes, views, controllers and console walkthroughs laid out the way
|
|
257
|
+
they'd sit in a Rails app:
|
|
258
|
+
|
|
259
|
+
| Example | Concerns |
|
|
260
|
+
|---------|----------|
|
|
261
|
+
| [persistent_conversation](examples/persistent_conversation) | `HasContext` |
|
|
262
|
+
| [memory_handoff](examples/memory_handoff) | `HasMemory` |
|
|
263
|
+
| [tool_streaming](examples/tool_streaming) | `HasTools`, `StreamsToolUpdates`, `ToolCache` |
|
|
264
|
+
| [reasoning](examples/reasoning) | `HasReasons`, `Reasonable` |
|
|
265
|
+
| [run_tracking](examples/run_tracking) | `AgentRun`, `RunFingerprint`, `ModelPricing` |
|
|
266
|
+
| [manifests](examples/manifests) | `AgentManifest` |
|
|
267
|
+
|
|
268
|
+
The narrated versions live at
|
|
269
|
+
[docs.activeagents.ai/solid_agent](https://docs.activeagents.ai/solid_agent).
|
|
270
|
+
|
|
271
|
+
## Example Apps
|
|
272
|
+
|
|
273
|
+
See SolidAgent in action:
|
|
274
|
+
|
|
275
|
+
- [Fizzy](https://github.com/tonsoffun/fizzy) - AI-enhanced Kanban tracking tool with writing, research, and file analysis agents
|
|
276
|
+
- [Writebook](https://github.com/tonsoffun/writebook) - Collaborative writing platform with integrated AI writing assistance, research, and document analysis
|
|
277
|
+
|
|
122
278
|
## Development
|
|
123
279
|
|
|
124
280
|
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
281
|
|
|
126
|
-
|
|
282
|
+
```bash
|
|
283
|
+
bundle exec rake test
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Testing against ActiveAgent
|
|
287
|
+
|
|
288
|
+
This suite runs against mocks — deliberately, so it stays fast and
|
|
289
|
+
dependency-free — which means it can pass while these concerns no longer
|
|
290
|
+
compose with the framework they extend. ActiveAgent carries a dummy Rails
|
|
291
|
+
app and a cross-repo suite for exactly that. Point it at your working tree:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
git clone https://github.com/activeagents/activeagent ../activeagent
|
|
295
|
+
cd ../activeagent
|
|
296
|
+
|
|
297
|
+
SOLID_AGENT_PATH=../solid_agent \
|
|
298
|
+
BUNDLE_GEMFILE=gemfiles/solid_agent_main.gemfile \
|
|
299
|
+
SOLID_AGENT_STRICT=1 \
|
|
300
|
+
bin/test test/integration/solid_agent/*_test.rb \
|
|
301
|
+
actionagent/test/agent_execution_service_test.rb
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`SOLID_AGENT_STRICT=1` fails on anything the suite would otherwise skip for
|
|
305
|
+
a missing API — here the resolved gem *is* your checkout, so a skip means
|
|
306
|
+
something was removed. CI runs this on every pull request against
|
|
307
|
+
ActiveAgent's main branch and its latest release, and again nightly. See
|
|
308
|
+
[Releasing & Cross-Repo Testing](https://docs.activeagents.ai/contributing/releasing).
|
|
309
|
+
|
|
310
|
+
### Releasing
|
|
311
|
+
|
|
312
|
+
Releases publish from a `v*` tag through
|
|
313
|
+
[.github/workflows/release.yml](.github/workflows/release.yml) using RubyGems
|
|
314
|
+
trusted publishing, gated on CI including the cross-repo suite. Bump
|
|
315
|
+
`SolidAgent::VERSION`, tag, push. This gem depends on `activeagent` and
|
|
316
|
+
`actionagent` depends on this gem, so anything requiring a new framework API
|
|
317
|
+
waits for that release to land on RubyGems first.
|
|
127
318
|
|
|
128
319
|
## Contributing
|
|
129
320
|
|
|
130
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
321
|
+
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
|
-
|
|
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" ]
|