phronomy 0.2.2 → 0.4.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -30
  3. data/README.md +106 -122
  4. data/lib/phronomy/agent/base.rb +135 -57
  5. data/lib/phronomy/agent/checkpoint.rb +53 -0
  6. data/lib/phronomy/agent/orchestrator.rb +119 -0
  7. data/lib/phronomy/agent/react_agent.rb +18 -28
  8. data/lib/phronomy/agent/shared_state.rb +303 -0
  9. data/lib/phronomy/agent/suspend_signal.rb +35 -0
  10. data/lib/phronomy/agent/team_coordinator.rb +285 -0
  11. data/lib/phronomy/agent.rb +2 -1
  12. data/lib/phronomy/configuration.rb +0 -24
  13. data/lib/phronomy/generator_verifier.rb +250 -0
  14. data/lib/phronomy/guardrail/builtin/pii_pattern_detector.rb +10 -27
  15. data/lib/phronomy/railtie.rb +0 -6
  16. data/lib/phronomy/ruby_llm_patches.rb +20 -0
  17. data/lib/phronomy/tool/mcp_tool.rb +23 -26
  18. data/lib/phronomy/tracing/langfuse_tracer.rb +3 -6
  19. data/lib/phronomy/vector_store/redis_search.rb +4 -4
  20. data/lib/phronomy/version.rb +1 -1
  21. data/lib/phronomy/workflow.rb +4 -7
  22. data/lib/phronomy/workflow_runner.rb +42 -30
  23. data/lib/phronomy.rb +18 -0
  24. data/scripts/check_readme_ruby.rb +38 -0
  25. metadata +12 -38
  26. data/docs/trustworthy_ai_enhancements.md +0 -332
  27. data/lib/phronomy/active_record/acts_as.rb +0 -48
  28. data/lib/phronomy/active_record/checkpoint.rb +0 -20
  29. data/lib/phronomy/active_record/extensions.rb +0 -14
  30. data/lib/phronomy/active_record/message.rb +0 -20
  31. data/lib/phronomy/actor.rb +0 -68
  32. data/lib/phronomy/memory/compression/base.rb +0 -37
  33. data/lib/phronomy/memory/compression/summary.rb +0 -107
  34. data/lib/phronomy/memory/compression/tool_output_pruner.rb +0 -67
  35. data/lib/phronomy/memory/compression.rb +0 -11
  36. data/lib/phronomy/memory/conversation_manager.rb +0 -213
  37. data/lib/phronomy/memory/retrieval/base.rb +0 -22
  38. data/lib/phronomy/memory/retrieval/composite.rb +0 -76
  39. data/lib/phronomy/memory/retrieval/recent.rb +0 -35
  40. data/lib/phronomy/memory/retrieval/semantic.rb +0 -114
  41. data/lib/phronomy/memory/retrieval.rb +0 -12
  42. data/lib/phronomy/memory/storage/active_record.rb +0 -248
  43. data/lib/phronomy/memory/storage/base.rb +0 -155
  44. data/lib/phronomy/memory/storage/in_memory.rb +0 -152
  45. data/lib/phronomy/memory/storage.rb +0 -11
  46. data/lib/phronomy/memory.rb +0 -21
  47. data/lib/phronomy/rails/agent_job.rb +0 -75
  48. data/lib/phronomy/state_store/active_record.rb +0 -76
  49. data/lib/phronomy/state_store/base.rb +0 -112
  50. data/lib/phronomy/state_store/encryptor/active_support.rb +0 -49
  51. data/lib/phronomy/state_store/encryptor/base.rb +0 -34
  52. data/lib/phronomy/state_store/encryptor.rb +0 -16
  53. data/lib/phronomy/state_store/file.rb +0 -85
  54. data/lib/phronomy/state_store/in_memory.rb +0 -53
  55. data/lib/phronomy/state_store/redis.rb +0 -70
  56. data/lib/phronomy/state_store.rb +0 -9
  57. data/lib/phronomy/thread_actor_registry.rb +0 -85
  58. data/lib/phronomy/trust_pipeline.rb +0 -264
@@ -1,76 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Memory
5
- module Retrieval
6
- # Retrieval strategy that merges results from multiple child retrieval strategies.
7
- #
8
- # Each child is given a weight that controls what fraction of a token budget
9
- # it should consume. Results are deduplicated (by role + content) and
10
- # system messages are sorted to the front.
11
- #
12
- # @example
13
- # composite = Phronomy::Memory::Retrieval::Composite.new(
14
- # sources: [
15
- # { retrieval: Phronomy::Memory::Retrieval::Recent.new(k: 5), weight: 0.4 },
16
- # { retrieval: Phronomy::Memory::Retrieval::Semantic.new(...), weight: 0.6 }
17
- # ]
18
- # )
19
- # manager = Phronomy::Memory::ConversationManager.new(
20
- # storage: Phronomy::Memory::Storage::InMemory.new,
21
- # retrieval: composite
22
- # )
23
- class Composite < Base
24
- # @param sources [Array<Hash>] each entry: { retrieval:, weight: } (weight default 1.0)
25
- def initialize(sources:)
26
- @sources = sources.map { |s| {retrieval: s[:retrieval], weight: (s[:weight] || 1.0).to_f} }
27
- end
28
-
29
- # Merge results from all child retrievals, deduplicating by role+content.
30
- # System messages are sorted to the front; others preserve insertion order.
31
- #
32
- # @param messages [Array] full chronological history
33
- # @param query [String, nil] forwarded to each child retrieval
34
- # @param thread_id [String, nil] forwarded to each child retrieval
35
- # @return [Array]
36
- def select(messages, query: nil, thread_id: nil)
37
- all_messages = []
38
- seen = {}
39
-
40
- @sources.each do |source|
41
- source[:retrieval].select(messages, query: query, thread_id: thread_id).each do |msg|
42
- key = "#{msg.role}:#{msg.content}"
43
- next if seen[key]
44
-
45
- seen[key] = true
46
- all_messages << msg
47
- end
48
- end
49
-
50
- systems = all_messages.select { |m| m.role.to_sym == :system }
51
- others = all_messages.reject { |m| m.role.to_sym == :system }
52
- systems + others
53
- end
54
-
55
- # Forward index calls to all child retrievals that support it.
56
- #
57
- # @param thread_id [String]
58
- # @param messages [Array]
59
- def index(thread_id:, messages:)
60
- @sources.each do |source|
61
- source[:retrieval].index(thread_id: thread_id, messages: messages) if source[:retrieval].respond_to?(:index)
62
- end
63
- end
64
-
65
- # Forward clear_index to all child retrievals that support it.
66
- #
67
- # @param thread_id [String]
68
- def clear_index(thread_id:)
69
- @sources.each do |source|
70
- source[:retrieval].clear_index(thread_id: thread_id) if source[:retrieval].respond_to?(:clear_index)
71
- end
72
- end
73
- end
74
- end
75
- end
76
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Memory
5
- module Retrieval
6
- # Retrieval strategy that returns the most recent k turns (k*2 messages).
7
- #
8
- # This is the simplest and most predictable strategy: older messages are
9
- # discarded without compression.
10
- #
11
- # @example
12
- # retrieval = Phronomy::Memory::Retrieval::Recent.new(k: 10)
13
- # manager = Phronomy::Memory::ConversationManager.new(
14
- # storage: storage,
15
- # retrieval: retrieval
16
- # )
17
- class Recent < Base
18
- # @param k [Integer] number of turns to retain (each turn = 1 user + 1 assistant message)
19
- def initialize(k: 10)
20
- @k = k
21
- end
22
-
23
- # Returns the last k*2 messages from the history.
24
- #
25
- # @param messages [Array] full chronological history
26
- # @param query [String, nil] unused for recency-based retrieval
27
- # @param thread_id [String, nil] unused for recency-based retrieval
28
- # @return [Array]
29
- def select(messages, query: nil, thread_id: nil)
30
- messages.last(@k * 2)
31
- end
32
- end
33
- end
34
- end
35
- end
@@ -1,114 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Memory
5
- module Retrieval
6
- # Retrieval strategy that returns the k semantically closest messages to the query.
7
- #
8
- # Messages are indexed in a VectorStore on save. On retrieval, the query is
9
- # embedded and the k nearest messages are returned. Falls back to the k most
10
- # recent messages when no query is provided.
11
- #
12
- # @example
13
- # retrieval = Phronomy::Memory::Retrieval::Semantic.new(
14
- # embeddings: Phronomy::Embeddings::RubyLLMEmbeddings.new(model: "text-embedding-3-small"),
15
- # k: 10
16
- # )
17
- class Semantic < Base
18
- # @param store [Phronomy::VectorStore::Base] vector store (default InMemory)
19
- # @param embeddings [Phronomy::Embeddings::Base] embeddings adapter
20
- # @param k [Integer] number of messages to retrieve
21
- # @param max_index_size [Integer, nil] maximum number of entries kept in the
22
- # local index. When nil, the index grows unboundedly. When exceeded, the
23
- # oldest entries (by insertion order) are evicted.
24
- def initialize(embeddings:, store: nil, k: 10, max_index_size: nil)
25
- @store = store || Phronomy::VectorStore::InMemory.new
26
- @embeddings = embeddings
27
- @k = k
28
- @index = {} # id => message (insertion-ordered via Ruby Hash)
29
- @counter = 0
30
- @max_index_size = max_index_size
31
- @actor = Phronomy::Actor.new
32
- @indexed_object_ids = {} # thread_id => { object_id => true }
33
- end
34
-
35
- # Index a new batch of messages so they are searchable on future #select calls.
36
- # Called by ConversationManager#save.
37
- #
38
- # Messages are deduplicated by object identity: if a message object has already
39
- # been indexed for the given thread_id, it is skipped (no duplicate embed call).
40
- #
41
- # @param thread_id [String]
42
- # @param messages [Array]
43
- def index(thread_id:, messages:)
44
- messages.each do |msg|
45
- # Fast path: skip already-indexed messages without calling embed.
46
- already_indexed = @actor.call do
47
- (@indexed_object_ids[thread_id] ||= {})[msg.object_id]
48
- end
49
- next if already_indexed
50
-
51
- embedding = @embeddings.embed(msg.content.to_s)
52
- @actor.call do
53
- # Re-check inside Actor to handle concurrent callers for the same thread.
54
- indexed = (@indexed_object_ids[thread_id] ||= {})
55
- next if indexed[msg.object_id]
56
-
57
- id = "#{thread_id}:#{@counter}"
58
- @counter += 1
59
- @store.add(id: id, embedding: embedding, metadata: {thread_id: thread_id, message: msg})
60
- @index[id] = msg
61
- indexed[msg.object_id] = true
62
- evict_oldest! if @max_index_size && @index.size > @max_index_size
63
- end
64
- end
65
- end
66
-
67
- # Clear indexed messages for a thread.
68
- #
69
- # @param thread_id [String]
70
- def clear_index(thread_id:)
71
- @actor.call do
72
- ids = @index.keys.select { |id| id.start_with?("#{thread_id}:") }
73
- ids.each do |id|
74
- @index.delete(id)
75
- @store.remove(id: id)
76
- end
77
- @indexed_object_ids.delete(thread_id)
78
- end
79
- end
80
-
81
- # Return semantically relevant messages, or recent messages when query is nil.
82
- #
83
- # @param messages [Array] full history (used as fallback when query is nil)
84
- # @param query [String, nil] current user input for semantic search
85
- # @param thread_id [String, nil] when provided, results are filtered to this thread
86
- # @return [Array]
87
- def select(messages, query: nil, thread_id: nil)
88
- if query && !query.strip.empty?
89
- query_embedding = @embeddings.embed(query)
90
- results = @actor.call { @store.search(query_embedding: query_embedding, k: @k * 3) }
91
- results
92
- .select { |r| thread_id.nil? || r[:metadata][:thread_id] == thread_id }
93
- .first(@k)
94
- .map { |r| r[:metadata][:message] }
95
- else
96
- messages.last(@k)
97
- end
98
- end
99
-
100
- private
101
-
102
- # Evicts the oldest index entry to enforce max_index_size.
103
- # Must be called inside the Actor.
104
- def evict_oldest!
105
- oldest_id = @index.keys.first
106
- return unless oldest_id
107
-
108
- @index.delete(oldest_id)
109
- @store.remove(id: oldest_id)
110
- end
111
- end
112
- end
113
- end
114
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Memory
5
- # Retrieval is the selection axis of conversation management.
6
- # Implementations decide which messages from a full history to return
7
- # given a query and a maximum message count or token limit.
8
- # Token budgeting is NOT their responsibility — that belongs to Context::Assembler.
9
- module Retrieval
10
- end
11
- end
12
- end
@@ -1,248 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module Phronomy
6
- module Memory
7
- module Storage
8
- # ActiveRecord-backed storage for conversation messages.
9
- # Persists messages to a relational database via user-supplied AR model classes.
10
- #
11
- # The message model_class must respond to:
12
- # .where(thread_id:).order(:created_at) — returns a collection of records
13
- # .where(thread_id:).delete_all
14
- # .create!(thread_id:, role:, content:, tool_calls_json:, model_id:)
15
- # Each record must expose: #role, #content, #tool_calls_json, #model_id
16
- #
17
- # The raw_model_class (optional) must respond to:
18
- # .where(thread_id:).order(:seq) — returns records in seq order
19
- # .where(thread_id:).delete_all
20
- # .create!(thread_id:, seq:, role:, content:, tool_calls_json:, model_id:)
21
- # Each record must expose: #seq, #role, #content, #tool_calls_json, #model_id
22
- #
23
- # The compaction_model_class (optional) must respond to:
24
- # .where(thread_id:).order(:start_seq)
25
- # .where(thread_id:).delete_all
26
- # .create!(thread_id:, start_seq:, end_seq:, summary_text:)
27
- # Each record must expose: #start_seq, #end_seq, #summary_text
28
- #
29
- # When raw_model_class or compaction_model_class are nil, the corresponding
30
- # operations raise NotImplementedError — use InMemory storage if you do not
31
- # need full raw/compaction persistence.
32
- #
33
- # @example
34
- # storage = Phronomy::Memory::Storage::ActiveRecord.new(
35
- # model_class: PhronomyMessage,
36
- # raw_model_class: PhronomyRawMessage,
37
- # compaction_model_class: PhronomyCompaction
38
- # )
39
- # manager = Phronomy::Memory::ConversationManager.new(storage: storage, ...)
40
- # Internal value object representing a loaded message record.
41
- MessageStruct = Data.define(:role, :content, :tool_calls, :model_id)
42
- private_constant :MessageStruct
43
-
44
- class ActiveRecord < Base
45
- # @param model_class [Class] AR model for the legacy load/save interface
46
- # @param raw_model_class [Class, nil] AR model for raw message storage
47
- # @param compaction_model_class [Class, nil] AR model for compaction records
48
- def initialize(model_class:, raw_model_class: nil, compaction_model_class: nil)
49
- @model_class = model_class
50
- @raw_model_class = raw_model_class
51
- @compaction_model_class = compaction_model_class
52
- end
53
-
54
- # -----------------------------------------------------------------------
55
- # Legacy interface
56
- # -----------------------------------------------------------------------
57
-
58
- # Load all messages for a thread, ordered by creation time.
59
- #
60
- # @param thread_id [String]
61
- # @return [Array<MessageStruct>]
62
- def load(thread_id:)
63
- records = @model_class.where(thread_id: thread_id).order(:created_at).to_a
64
- records.map { |r| to_message_struct(r) }
65
- end
66
-
67
- # Replace all stored messages for a thread.
68
- #
69
- # @param thread_id [String]
70
- # @param messages [Array]
71
- def save(thread_id:, messages:)
72
- @model_class.transaction do
73
- @model_class.where(thread_id: thread_id).delete_all
74
- messages.each do |msg|
75
- @model_class.create!(
76
- thread_id: thread_id,
77
- role: msg.role.to_s,
78
- content: msg.content,
79
- tool_calls_json: serialize_tool_calls(msg),
80
- model_id: (msg.model_id if msg.respond_to?(:model_id))
81
- )
82
- end
83
- end
84
- end
85
-
86
- # @param thread_id [String]
87
- def clear(thread_id:)
88
- @model_class.where(thread_id: thread_id).delete_all
89
- clear_raw(thread_id: thread_id)
90
- clear_compactions(thread_id: thread_id)
91
- end
92
-
93
- # -----------------------------------------------------------------------
94
- # Raw message interface
95
- # -----------------------------------------------------------------------
96
-
97
- # @param thread_id [String]
98
- # @param messages [Array]
99
- # @param starting_seq [Integer]
100
- def append_raw(thread_id:, messages:, starting_seq:)
101
- return unless @raw_model_class
102
-
103
- @raw_model_class.transaction do
104
- messages.each_with_index do |msg, i|
105
- @raw_model_class.create!(
106
- thread_id: thread_id,
107
- seq: starting_seq + i,
108
- role: msg.role.to_s,
109
- content: msg.content,
110
- tool_calls_json: serialize_tool_calls(msg),
111
- model_id: (msg.model_id if msg.respond_to?(:model_id))
112
- )
113
- end
114
- end
115
- end
116
-
117
- # @param thread_id [String]
118
- # @return [Array<Hash>]
119
- def load_raw(thread_id:)
120
- return [] unless @raw_model_class
121
-
122
- records = @raw_model_class.where(thread_id: thread_id).order(:seq).to_a
123
- records.map { |r| {seq: r.seq, message: to_message_struct(r)} }
124
- end
125
-
126
- # @param thread_id [String]
127
- def clear_raw(thread_id:)
128
- @raw_model_class&.where(thread_id: thread_id)&.delete_all
129
- end
130
-
131
- # -----------------------------------------------------------------------
132
- # Compaction record interface
133
- # -----------------------------------------------------------------------
134
-
135
- # @param thread_id [String]
136
- # @param start_seq [Integer]
137
- # @param end_seq [Integer]
138
- # @param summary_text [String]
139
- def save_compaction(thread_id:, start_seq:, end_seq:, summary_text:)
140
- ensure_compaction_model!
141
- @compaction_model_class.create!(
142
- thread_id: thread_id,
143
- start_seq: start_seq,
144
- end_seq: end_seq,
145
- summary_text: summary_text
146
- )
147
- end
148
-
149
- # @param thread_id [String]
150
- # @return [Array<Hash>]
151
- def load_compactions(thread_id:)
152
- return [] unless @compaction_model_class
153
-
154
- records = @compaction_model_class.where(thread_id: thread_id).order(:start_seq).to_a
155
- records.map { |r| {start_seq: r.start_seq, end_seq: r.end_seq, summary_text: r.summary_text} }
156
- end
157
-
158
- # @param thread_id [String]
159
- def clear_compactions(thread_id:)
160
- @compaction_model_class&.where(thread_id: thread_id)&.delete_all
161
- end
162
-
163
- # Remove messages for a thread that were created before +older_than+.
164
- # Only the legacy message store is filtered; raw and compaction records
165
- # are left untouched because they use seq-based addressing.
166
- #
167
- # @param thread_id [String]
168
- # @param older_than [Time]
169
- def purge_older_than(thread_id:, older_than:)
170
- @model_class.where(thread_id: thread_id).where("created_at < ?", older_than).delete_all
171
- end
172
-
173
- # Returns the next seq number to use for new raw messages for +thread_id+.
174
- # Derived from MAX(seq) in the database; since purge_older_than does not
175
- # touch raw records, this value is always correct.
176
- #
177
- # @param thread_id [String]
178
- # @return [Integer]
179
- def next_seq(thread_id:)
180
- return 0 unless @raw_model_class
181
-
182
- ((@raw_model_class.where(thread_id: thread_id).maximum(:seq) || -1) + 1)
183
- end
184
-
185
- # Delegates to the block directly; serialisation of concurrent saves
186
- # for the same thread_id is the caller's responsibility (e.g. DB-level
187
- # transaction isolation or application-layer queuing).
188
- # @param thread_id [String]
189
- def with_thread_lock(thread_id:)
190
- yield
191
- end
192
-
193
- private
194
-
195
- def ensure_raw_model!
196
- raise NotImplementedError, "raw_model_class is required for raw message storage" unless @raw_model_class
197
- end
198
-
199
- def ensure_compaction_model!
200
- raise NotImplementedError, "compaction_model_class is required for compaction record storage" unless @compaction_model_class
201
- end
202
-
203
- def serialize_tool_calls(msg)
204
- return unless msg.respond_to?(:tool_calls) && msg.tool_calls
205
-
206
- serializable = case msg.tool_calls
207
- when Hash
208
- msg.tool_calls.transform_values { |tc| tc.respond_to?(:to_h) ? tc.to_h : tc }
209
- when Array
210
- msg.tool_calls.map { |tc| tc.respond_to?(:to_h) ? tc.to_h : tc }
211
- else
212
- msg.tool_calls
213
- end
214
- JSON.generate(serializable)
215
- end
216
-
217
- def to_message_struct(record)
218
- tool_calls = if record.tool_calls_json
219
- parsed = JSON.parse(record.tool_calls_json)
220
- case parsed
221
- when Hash
222
- parsed.transform_values { |tc| restore_tool_call(tc) }
223
- when Array
224
- parsed.map { |tc| restore_tool_call(tc) }
225
- else
226
- parsed
227
- end
228
- end
229
- MessageStruct.new(
230
- role: record.role.to_sym,
231
- content: record.content,
232
- tool_calls: tool_calls,
233
- model_id: record.respond_to?(:model_id) ? record.model_id : nil
234
- )
235
- end
236
-
237
- def restore_tool_call(tc)
238
- return tc unless tc.is_a?(Hash) && tc["id"] && tc["name"]
239
- RubyLLM::ToolCall.new(
240
- id: tc["id"],
241
- name: tc["name"],
242
- arguments: tc["arguments"] || {}
243
- )
244
- end
245
- end
246
- end
247
- end
248
- end
@@ -1,155 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Memory
5
- module Storage
6
- # Abstract base class for conversation storage backends.
7
- #
8
- # Each backend manages two independent datasets per thread:
9
- #
10
- # 1. Raw messages — the original, unmodified conversation history.
11
- # Every message is stored with a monotonically increasing seq number
12
- # (0-based, scoped to thread_id). Raw messages are never modified or
13
- # deleted; they are the authoritative record.
14
- #
15
- # 2. Compaction records — the output of LLM-based compaction (summarization).
16
- # Each record covers a contiguous range [start_seq..end_seq] of raw
17
- # messages and stores the summary text produced for that range.
18
- # Multiple non-overlapping compaction records may exist per thread.
19
- #
20
- # The conventional load/save/clear interface is kept for use by
21
- # ConversationManager (compression path) and direct storage access.
22
- #
23
- # @abstract Subclass and implement all abstract methods.
24
- class Base
25
- # -----------------------------------------------------------------------
26
- # Conventional load/save/clear interface
27
- # -----------------------------------------------------------------------
28
-
29
- # Load all messages for a thread in chronological order.
30
- #
31
- # @param thread_id [String]
32
- # @return [Array]
33
- def load(thread_id:)
34
- raise NotImplementedError, "#{self.class}#load is not implemented"
35
- end
36
-
37
- # Persist messages for a thread (replaces existing messages).
38
- #
39
- # @param thread_id [String]
40
- # @param messages [Array]
41
- def save(thread_id:, messages:)
42
- raise NotImplementedError, "#{self.class}#save is not implemented"
43
- end
44
-
45
- # Delete all messages for a thread.
46
- #
47
- # @param thread_id [String]
48
- def clear(thread_id:)
49
- raise NotImplementedError, "#{self.class}#clear is not implemented"
50
- end
51
-
52
- # -----------------------------------------------------------------------
53
- # Raw message interface
54
- # -----------------------------------------------------------------------
55
-
56
- # Append new messages to the raw history for a thread.
57
- # Each message is stored together with its seq number.
58
- #
59
- # @param thread_id [String]
60
- # @param messages [Array] message objects to append
61
- # @param starting_seq [Integer] seq number to assign to messages[0]
62
- def append_raw(thread_id:, messages:, starting_seq:)
63
- raise NotImplementedError, "#{self.class}#append_raw is not implemented"
64
- end
65
-
66
- # Return all raw messages for a thread as an array of hashes.
67
- #
68
- # @param thread_id [String]
69
- # @return [Array<Hash>] each entry: { seq: Integer, message: Object }
70
- def load_raw(thread_id:)
71
- raise NotImplementedError, "#{self.class}#load_raw is not implemented"
72
- end
73
-
74
- # Delete raw messages for a thread (used in #clear).
75
- #
76
- # @param thread_id [String]
77
- def clear_raw(thread_id:)
78
- raise NotImplementedError, "#{self.class}#clear_raw is not implemented"
79
- end
80
-
81
- # -----------------------------------------------------------------------
82
- # Compaction record interface
83
- # -----------------------------------------------------------------------
84
-
85
- # Persist a compaction record for a thread.
86
- # A compaction record stores the LLM-generated summary that covers raw
87
- # messages from start_seq to end_seq (inclusive).
88
- #
89
- # @param thread_id [String]
90
- # @param start_seq [Integer]
91
- # @param end_seq [Integer]
92
- # @param summary_text [String]
93
- def save_compaction(thread_id:, start_seq:, end_seq:, summary_text:)
94
- raise NotImplementedError, "#{self.class}#save_compaction is not implemented"
95
- end
96
-
97
- # Return all compaction records for a thread in ascending start_seq order.
98
- #
99
- # @param thread_id [String]
100
- # @return [Array<Hash>] each entry: { start_seq:, end_seq:, summary_text: }
101
- def load_compactions(thread_id:)
102
- raise NotImplementedError, "#{self.class}#load_compactions is not implemented"
103
- end
104
-
105
- # Delete all compaction records for a thread.
106
- #
107
- # @param thread_id [String]
108
- def clear_compactions(thread_id:)
109
- raise NotImplementedError, "#{self.class}#clear_compactions is not implemented"
110
- end
111
-
112
- # Remove all stored data (raw messages, compaction records, legacy store)
113
- # for a thread. Equivalent to {#clear}, provided as a named alias to make
114
- # the "right to erasure" intent explicit.
115
- #
116
- # @param thread_id [String]
117
- def purge(thread_id:)
118
- clear(thread_id: thread_id)
119
- end
120
-
121
- # Remove raw messages recorded before +older_than+ for a thread.
122
- # The default implementation is a no-op; backends that support
123
- # timestamp-based deletion should override this method.
124
- #
125
- # @param thread_id [String]
126
- # @param older_than [Time]
127
- def purge_older_than(thread_id:, older_than:)
128
- # no-op by default
129
- end
130
-
131
- # Returns the next seq number to assign when appending new raw messages
132
- # for +thread_id+. Must be monotonically increasing and must survive
133
- # purge_older_than (i.e. the counter must not reset when old raw records
134
- # are deleted by a TTL purge).
135
- #
136
- # @param thread_id [String]
137
- # @return [Integer]
138
- def next_seq(thread_id:)
139
- raise NotImplementedError, "#{self.class}#next_seq is not implemented"
140
- end
141
-
142
- # Executes the block while holding a per-thread-id lock for +thread_id+.
143
- # Used by ConversationManager to prevent concurrent compaction for the
144
- # same thread. The default implementation yields without locking; backends
145
- # that require serialisation should override this method.
146
- #
147
- # @param thread_id [String]
148
- # @yield
149
- def with_thread_lock(thread_id:)
150
- yield
151
- end
152
- end
153
- end
154
- end
155
- end