solid_agent 0.2.0 → 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 +31 -0
- data/README.md +22 -0
- data/lib/solid_agent/has_context.rb +51 -5
- data/lib/solid_agent/version.rb +1 -1
- data/solid_agent.gemspec +41 -0
- metadata +7 -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
CHANGED
|
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
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
|
+
|
|
10
41
|
### Added
|
|
11
42
|
|
|
12
43
|
- **`SolidAgent::Records::*` — behavior for the agent-configuration records.**
|
data/README.md
CHANGED
|
@@ -94,6 +94,28 @@ the user turn and the response as the assistant turn, both after the
|
|
|
94
94
|
provider call — so reach for `add_conversation_user_message` only with
|
|
95
95
|
`auto_save: false`, or the turn is stored twice.
|
|
96
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
|
+
|
|
97
119
|
> **Naming a context also names its models.** `has_context :conversation`
|
|
98
120
|
> infers `Conversation`, `ConversationMessage` and `ConversationGeneration`,
|
|
99
121
|
> not the `AgentContext` family the installer wrote — hence `class_name:`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
|
+
require "set"
|
|
4
5
|
|
|
5
6
|
# HasContext provides database-backed prompt context management for agents.
|
|
6
7
|
#
|
|
@@ -542,9 +543,34 @@ module SolidAgent
|
|
|
542
543
|
self.class.public_send(reader)&.except(:access_token, :api_key)
|
|
543
544
|
end
|
|
544
545
|
|
|
545
|
-
#
|
|
546
|
+
# Marks this generation as resuming one that paused for user input, for a
|
|
547
|
+
# host that replays a stored conversation itself. Private, like
|
|
548
|
+
# #resuming_generation?, because every public method on an agent is one
|
|
549
|
+
# of its actions.
|
|
550
|
+
#
|
|
551
|
+
# @example
|
|
552
|
+
# before_generation { self.resuming_generation = params[:checkpoint].present? }
|
|
553
|
+
attr_writer :resuming_generation
|
|
554
|
+
|
|
555
|
+
# Returns whether this generation resumes one that paused for user input:
|
|
556
|
+
# true when `self.resuming_generation = true` was set, or when the agent
|
|
557
|
+
# defines a public `resuming?` (the framework's resume flag) that returns
|
|
558
|
+
# true. A resuming generation does not persist its prompt.
|
|
559
|
+
#
|
|
560
|
+
# @return [Boolean]
|
|
561
|
+
def resuming_generation?
|
|
562
|
+
return true if @resuming_generation
|
|
563
|
+
return false unless respond_to?(:resuming?)
|
|
564
|
+
|
|
565
|
+
resuming? ? true : false
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# After prompt callback - persists the rendered prompt message to context.
|
|
569
|
+
# Skipped for a resumed generation, because the generation that paused
|
|
570
|
+
# already persisted the user turn and the resumed prompt replays it.
|
|
546
571
|
def persist_prompt_to_context
|
|
547
572
|
return unless context
|
|
573
|
+
return if resuming_generation?
|
|
548
574
|
|
|
549
575
|
if prompt_options[:messages].present?
|
|
550
576
|
rendered_message = prompt_options[:messages].last
|
|
@@ -560,10 +586,18 @@ module SolidAgent
|
|
|
560
586
|
generation_response
|
|
561
587
|
end
|
|
562
588
|
|
|
563
|
-
# Persists the generation response to context
|
|
589
|
+
# Persists the generation response to context. A response paused for user
|
|
590
|
+
# input is skipped, because its last message is an unfinished turn. The
|
|
591
|
+
# response of the generation that resumes it repeats the restored tool
|
|
592
|
+
# results, so they are persisted with the final answer.
|
|
564
593
|
def persist_generation_to_context
|
|
565
594
|
return unless context && generation_response
|
|
566
595
|
|
|
596
|
+
if generation_paused?
|
|
597
|
+
Rails.logger.info "[SolidAgent] Skipping persistence - generation is awaiting user input"
|
|
598
|
+
return
|
|
599
|
+
end
|
|
600
|
+
|
|
567
601
|
persist_tool_messages_to_context
|
|
568
602
|
|
|
569
603
|
begin
|
|
@@ -584,6 +618,12 @@ module SolidAgent
|
|
|
584
618
|
end
|
|
585
619
|
end
|
|
586
620
|
|
|
621
|
+
def generation_paused?
|
|
622
|
+
return false unless generation_response.respond_to?(:awaiting_input?)
|
|
623
|
+
|
|
624
|
+
generation_response.awaiting_input? ? true : false
|
|
625
|
+
end
|
|
626
|
+
|
|
587
627
|
# Overridable enrichment hook for tool persistence. Executors that run
|
|
588
628
|
# tools server-side (a platform's execution service, a job) can
|
|
589
629
|
# override this to return their own invocation records — an array of
|
|
@@ -602,19 +642,25 @@ module SolidAgent
|
|
|
602
642
|
#
|
|
603
643
|
# Requires the context model to expose add_tool_message (the install
|
|
604
644
|
# generator's AgentContext does); contexts without it are skipped.
|
|
605
|
-
# Messages are deduped by tool_call_id
|
|
606
|
-
#
|
|
645
|
+
# Messages are deduped by tool_call_id. A response's stack repeats
|
|
646
|
+
# earlier turns (a multi-turn conversation's history, a resumed
|
|
647
|
+
# generation's restored conversation), so a call already on the context
|
|
648
|
+
# or earlier in the same stack is skipped.
|
|
607
649
|
def persist_tool_messages_to_context
|
|
608
650
|
return unless context.respond_to?(:add_tool_message)
|
|
609
651
|
return unless generation_response.respond_to?(:messages)
|
|
610
652
|
|
|
611
653
|
tool_index = -1
|
|
654
|
+
seen_tool_call_ids = Set.new
|
|
612
655
|
Array(generation_response.messages).each do |message|
|
|
613
656
|
next unless message.respond_to?(:role) && message.role.to_s == "tool"
|
|
614
657
|
|
|
615
658
|
tool_index += 1
|
|
616
659
|
tool_call_id = message.respond_to?(:tool_call_id) ? message.tool_call_id : nil
|
|
617
|
-
|
|
660
|
+
if tool_call_id.present?
|
|
661
|
+
next unless seen_tool_call_ids.add?(tool_call_id.to_s)
|
|
662
|
+
next if tool_message_persisted?(tool_call_id)
|
|
663
|
+
end
|
|
618
664
|
|
|
619
665
|
invocation = tool_invocation_for(tool_call_id, tool_index)
|
|
620
666
|
# Provider tool messages often carry no name (Ollama's don't); the
|
data/lib/solid_agent/version.rb
CHANGED
data/solid_agent.gemspec
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/solid_agent/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "solid_agent"
|
|
7
|
+
spec.version = SolidAgent::VERSION
|
|
8
|
+
spec.authors = ["Justin Bowen"]
|
|
9
|
+
spec.email = ["JusBowen@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Database-backed context, tools, and streaming for ActiveAgent"
|
|
12
|
+
spec.description = "SolidAgent extends ActiveAgent with database-backed prompt context management, declarative tool schemas, and real-time streaming updates. Provides has_context, has_tools, and streams_tool_updates concerns for building robust AI agents."
|
|
13
|
+
spec.homepage = "https://docs.activeagents.ai/solid_agent"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/activeagents/solid_agent"
|
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/activeagents/solid_agent/blob/main/CHANGELOG.md"
|
|
20
|
+
spec.metadata["documentation_uri"] = "https://docs.activeagents.ai/solid_agent"
|
|
21
|
+
|
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
26
|
+
(File.expand_path(f) == __FILE__) ||
|
|
27
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git assets/ appveyor Gemfile])
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
spec.bindir = "exe"
|
|
31
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
32
|
+
spec.require_paths = ["lib"]
|
|
33
|
+
|
|
34
|
+
spec.add_dependency "activeagent", ">= 1.0.0"
|
|
35
|
+
spec.add_dependency "activesupport", ">= 7.0"
|
|
36
|
+
spec.add_dependency "activerecord", ">= 7.0"
|
|
37
|
+
# AgentManifest is an ActiveModel, and lib/solid_agent.rb now requires it by
|
|
38
|
+
# name so the gem loads outside Rails. It arrived transitively through
|
|
39
|
+
# activerecord before; a require deserves a declaration.
|
|
40
|
+
spec.add_dependency "activemodel", ">= 7.0"
|
|
41
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Bowen
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-10-08 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: activeagent
|
|
@@ -175,6 +176,7 @@ files:
|
|
|
175
176
|
- lib/solid_agent/tool_cache.rb
|
|
176
177
|
- lib/solid_agent/version.rb
|
|
177
178
|
- sig/solid_agent.rbs
|
|
179
|
+
- solid_agent.gemspec
|
|
178
180
|
homepage: https://docs.activeagents.ai/solid_agent
|
|
179
181
|
licenses:
|
|
180
182
|
- MIT
|
|
@@ -183,6 +185,7 @@ metadata:
|
|
|
183
185
|
source_code_uri: https://github.com/activeagents/solid_agent
|
|
184
186
|
changelog_uri: https://github.com/activeagents/solid_agent/blob/main/CHANGELOG.md
|
|
185
187
|
documentation_uri: https://docs.activeagents.ai/solid_agent
|
|
188
|
+
post_install_message:
|
|
186
189
|
rdoc_options: []
|
|
187
190
|
require_paths:
|
|
188
191
|
- lib
|
|
@@ -197,7 +200,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
197
200
|
- !ruby/object:Gem::Version
|
|
198
201
|
version: '0'
|
|
199
202
|
requirements: []
|
|
200
|
-
rubygems_version: 3.
|
|
203
|
+
rubygems_version: 3.5.22
|
|
204
|
+
signing_key:
|
|
201
205
|
specification_version: 4
|
|
202
206
|
summary: Database-backed context, tools, and streaming for ActiveAgent
|
|
203
207
|
test_files: []
|