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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +99 -0
- data/LICENSE +21 -0
- data/README.md +231 -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 +299 -32
- 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
- data/solid_agent.gemspec +41 -0
- metadata +93 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 73594fa8639580da1945c6445fad2fef3eaf0500e0f48fd8842115c9f4e2ec33
|
|
4
|
+
data.tar.gz: 250018ddb7579ff3aabad5da2f8f7b43cf53d963a5d25ad0a241721d3591a304
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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,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
|
-
|
|
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/
|
|
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
|
-
|
|
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" ]
|