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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b204dd708358a41a24ed81cb3e27a76c83b97bd6b69fb098eeaf9a3c54d4ddd
4
- data.tar.gz: 3ee800cb2936615ee7ac57072da8a26c6152bf73e92088c8853a8cbcdd3b4a6b
3
+ metadata.gz: 73594fa8639580da1945c6445fad2fef3eaf0500e0f48fd8842115c9f4e2ec33
4
+ data.tar.gz: 250018ddb7579ff3aabad5da2f8f7b43cf53d963a5d25ad0a241721d3591a304
5
5
  SHA512:
6
- metadata.gz: 4a9d1c5133162f5ebd32fe1cf90ac999cd9b0f0f9c64396eb4e59ef9b60ca3d871e79c5d6e83fe50a830bfbbea6677837ff5ad0e0ca8e32277b885c577d63e9c
7
- data.tar.gz: 6e3d799d6cb5321f10455d2a954cb24d2c41a0768c88489a86eb24f6bfe84f5b4ae25fffc614e60f0edfd05c517dc6dfef66c79c078e125d9ed275fa581e27a9
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
- # After prompt callback - persists the rendered prompt message to context
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 so re-persisting a shared
606
- # message stack (multi-turn conversations) doesn't duplicate rows.
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
- next if tool_call_id.present? && tool_message_persisted?(tool_call_id)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidAgent
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -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.0
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: 1980-01-02 00:00:00.000000000 Z
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.7.2
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: []