phronomy 0.2.2 → 0.3.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +88 -30
  3. data/README.md +26 -110
  4. data/lib/phronomy/agent/base.rb +127 -54
  5. data/lib/phronomy/agent/checkpoint.rb +53 -0
  6. data/lib/phronomy/agent/react_agent.rb +18 -28
  7. data/lib/phronomy/agent/suspend_signal.rb +35 -0
  8. data/lib/phronomy/agent.rb +2 -1
  9. data/lib/phronomy/configuration.rb +0 -24
  10. data/lib/phronomy/guardrail/builtin/pii_pattern_detector.rb +10 -27
  11. data/lib/phronomy/railtie.rb +0 -6
  12. data/lib/phronomy/ruby_llm_patches.rb +20 -0
  13. data/lib/phronomy/tool/mcp_tool.rb +23 -26
  14. data/lib/phronomy/tracing/langfuse_tracer.rb +3 -6
  15. data/lib/phronomy/trust_pipeline.rb +1 -2
  16. data/lib/phronomy/vector_store/redis_search.rb +4 -4
  17. data/lib/phronomy/version.rb +1 -1
  18. data/lib/phronomy/workflow.rb +4 -7
  19. data/lib/phronomy/workflow_runner.rb +1 -8
  20. data/lib/phronomy.rb +1 -0
  21. data/scripts/check_readme_ruby.rb +38 -0
  22. metadata +5 -33
  23. data/docs/trustworthy_ai_enhancements.md +0 -332
  24. data/lib/phronomy/active_record/acts_as.rb +0 -48
  25. data/lib/phronomy/active_record/checkpoint.rb +0 -20
  26. data/lib/phronomy/active_record/extensions.rb +0 -14
  27. data/lib/phronomy/active_record/message.rb +0 -20
  28. data/lib/phronomy/actor.rb +0 -68
  29. data/lib/phronomy/memory/compression/base.rb +0 -37
  30. data/lib/phronomy/memory/compression/summary.rb +0 -107
  31. data/lib/phronomy/memory/compression/tool_output_pruner.rb +0 -67
  32. data/lib/phronomy/memory/compression.rb +0 -11
  33. data/lib/phronomy/memory/conversation_manager.rb +0 -213
  34. data/lib/phronomy/memory/retrieval/base.rb +0 -22
  35. data/lib/phronomy/memory/retrieval/composite.rb +0 -76
  36. data/lib/phronomy/memory/retrieval/recent.rb +0 -35
  37. data/lib/phronomy/memory/retrieval/semantic.rb +0 -114
  38. data/lib/phronomy/memory/retrieval.rb +0 -12
  39. data/lib/phronomy/memory/storage/active_record.rb +0 -248
  40. data/lib/phronomy/memory/storage/base.rb +0 -155
  41. data/lib/phronomy/memory/storage/in_memory.rb +0 -152
  42. data/lib/phronomy/memory/storage.rb +0 -11
  43. data/lib/phronomy/memory.rb +0 -21
  44. data/lib/phronomy/rails/agent_job.rb +0 -75
  45. data/lib/phronomy/state_store/active_record.rb +0 -76
  46. data/lib/phronomy/state_store/base.rb +0 -112
  47. data/lib/phronomy/state_store/encryptor/active_support.rb +0 -49
  48. data/lib/phronomy/state_store/encryptor/base.rb +0 -34
  49. data/lib/phronomy/state_store/encryptor.rb +0 -16
  50. data/lib/phronomy/state_store/file.rb +0 -85
  51. data/lib/phronomy/state_store/in_memory.rb +0 -53
  52. data/lib/phronomy/state_store/redis.rb +0 -70
  53. data/lib/phronomy/state_store.rb +0 -9
  54. data/lib/phronomy/thread_actor_registry.rb +0 -85
@@ -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
@@ -1,152 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Memory
5
- module Storage
6
- # In-process storage for conversation messages backed by per-thread-id
7
- # {Phronomy::Actor} instances from {Phronomy::ThreadActorRegistry}.
8
- # Messages are lost when the process exits.
9
- #
10
- # @example
11
- # storage = Phronomy::Memory::Storage::InMemory.new
12
- # manager = Phronomy::Memory::ConversationManager.new(storage: storage, ...)
13
- class InMemory < Base
14
- # Thread-local key for per-thread-id storage data (namespaced by store
15
- # instance object_id to support multiple independent InMemory stores).
16
- THREAD_DATA_KEY = :phronomy_storage_in_memory_data
17
-
18
- def initialize
19
- end
20
-
21
- # -----------------------------------------------------------------------
22
- # Legacy interface
23
- # -----------------------------------------------------------------------
24
-
25
- # @param thread_id [String]
26
- # @return [Array]
27
- def load(thread_id:)
28
- Phronomy::ThreadActorRegistry.for(thread_id).call { (thread_data.store || []).dup }
29
- end
30
-
31
- # @param thread_id [String]
32
- # @param messages [Array]
33
- def save(thread_id:, messages:)
34
- Phronomy::ThreadActorRegistry.for(thread_id).call { thread_data.store = messages.dup }
35
- end
36
-
37
- # @param thread_id [String]
38
- def clear(thread_id:)
39
- store_id = object_id
40
- Phronomy::ThreadActorRegistry.for(thread_id).call do
41
- (Thread.current[THREAD_DATA_KEY] ||= {}).delete(store_id)
42
- end
43
- end
44
-
45
- # -----------------------------------------------------------------------
46
- # Raw message interface
47
- # -----------------------------------------------------------------------
48
-
49
- # @param thread_id [String]
50
- # @param messages [Array]
51
- # @param starting_seq [Integer]
52
- # @param recorded_at [Time, nil] timestamp for test overrides; defaults to +Time.now+
53
- def append_raw(thread_id:, messages:, starting_seq:, recorded_at: nil)
54
- now = recorded_at || Time.now
55
- Phronomy::ThreadActorRegistry.for(thread_id).call do
56
- data = thread_data
57
- messages.each_with_index do |msg, i|
58
- seq = starting_seq + i
59
- data.raw_messages << {seq: seq, message: msg, recorded_at: now}
60
- data.hwm = [data.hwm, seq].max
61
- end
62
- end
63
- end
64
-
65
- # @param thread_id [String]
66
- # @return [Integer]
67
- def next_seq(thread_id:)
68
- Phronomy::ThreadActorRegistry.for(thread_id).call { thread_data.hwm + 1 }
69
- end
70
-
71
- # Routes +block+ through the per-thread-id {Phronomy::Actor}, serialising
72
- # all operations for the same thread. Reentrant calls (the block itself
73
- # calling storage methods that also route through the Actor) are safe
74
- # because {Phronomy::Actor#call} detects the same-thread case and executes
75
- # inline.
76
- #
77
- # @param thread_id [String]
78
- def with_thread_lock(thread_id:, &block)
79
- Phronomy::ThreadActorRegistry.for(thread_id).call(&block)
80
- end
81
-
82
- # @param thread_id [String]
83
- # @return [Array<Hash>]
84
- def load_raw(thread_id:)
85
- Phronomy::ThreadActorRegistry.for(thread_id).call { thread_data.raw_messages.dup }
86
- end
87
-
88
- # @param thread_id [String]
89
- def clear_raw(thread_id:)
90
- Phronomy::ThreadActorRegistry.for(thread_id).call { thread_data.raw_messages.clear }
91
- end
92
-
93
- # -----------------------------------------------------------------------
94
- # Compaction record interface
95
- # -----------------------------------------------------------------------
96
-
97
- # @param thread_id [String]
98
- # @param start_seq [Integer]
99
- # @param end_seq [Integer]
100
- # @param summary_text [String]
101
- def save_compaction(thread_id:, start_seq:, end_seq:, summary_text:)
102
- Phronomy::ThreadActorRegistry.for(thread_id).call do
103
- thread_data.compactions << {start_seq: start_seq, end_seq: end_seq, summary_text: summary_text}
104
- end
105
- end
106
-
107
- # @param thread_id [String]
108
- # @return [Array<Hash>]
109
- def load_compactions(thread_id:)
110
- Phronomy::ThreadActorRegistry.for(thread_id).call { thread_data.compactions.dup }
111
- end
112
-
113
- # @param thread_id [String]
114
- def clear_compactions(thread_id:)
115
- Phronomy::ThreadActorRegistry.for(thread_id).call { thread_data.compactions.clear }
116
- end
117
-
118
- # Remove raw messages recorded before +older_than+ for this thread.
119
- #
120
- # @param thread_id [String]
121
- # @param older_than [Time]
122
- def purge_older_than(thread_id:, older_than:)
123
- Phronomy::ThreadActorRegistry.for(thread_id).call do
124
- thread_data.raw_messages.reject! { |entry| entry[:recorded_at] && entry[:recorded_at] < older_than }
125
- end
126
- end
127
-
128
- private
129
-
130
- # Returns (or lazily initialises) the {ThreadData} for the current Actor
131
- # thread and this storage instance. Must only be called from within a
132
- # {Phronomy::ThreadActorRegistry.for} block so that +Thread.current+ is
133
- # the correct Actor thread.
134
- def thread_data
135
- (Thread.current[THREAD_DATA_KEY] ||= {})[object_id] ||= ThreadData.new
136
- end
137
-
138
- # Value object holding all per-thread-id storage state.
139
- class ThreadData
140
- attr_accessor :store, :raw_messages, :compactions, :hwm
141
-
142
- def initialize
143
- @store = nil
144
- @raw_messages = []
145
- @compactions = []
146
- @hwm = -1
147
- end
148
- end
149
- end
150
- end
151
- end
152
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Memory
5
- # Storage is the persistence axis of conversation management.
6
- # Implementations are responsible only for saving and loading raw message arrays.
7
- # Token budgeting, retrieval selection, and compression are handled by other axes.
8
- module Storage
9
- end
10
- end
11
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Memory
5
- end
6
- end
7
-
8
- require_relative "memory/storage"
9
- require_relative "memory/storage/base"
10
- require_relative "memory/storage/in_memory"
11
- require_relative "memory/storage/active_record"
12
- require_relative "memory/retrieval"
13
- require_relative "memory/retrieval/base"
14
- require_relative "memory/retrieval/recent"
15
- require_relative "memory/retrieval/semantic"
16
- require_relative "memory/retrieval/composite"
17
- require_relative "memory/compression"
18
- require_relative "memory/compression/base"
19
- require_relative "memory/compression/summary"
20
- require_relative "memory/compression/tool_output_pruner"
21
- require_relative "memory/conversation_manager"
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Rails
5
- # ActiveJob-based job that runs a Phronomy agent in streaming mode and
6
- # broadcasts each event to an ActionCable stream.
7
- #
8
- # Enqueue with +perform_later+ to run the agent asynchronously in a background
9
- # worker. Every streaming event is forwarded to ActionCable subscribers in
10
- # real time.
11
- #
12
- # @example Enqueueing a streaming agent job
13
- # Phronomy::Rails::AgentJob.perform_later(
14
- # "MyAgent",
15
- # "What is the weather today?",
16
- # channel: "AgentChannel",
17
- # stream: "agent_#{current_user.id}"
18
- # )
19
- #
20
- # Events broadcast to the ActionCable stream:
21
- # { type: "token", content: "..." } — each content delta from the LLM
22
- # { type: "done", output: "..." } — final complete output
23
- # { type: "error", message: "..." } — when the agent or job raises
24
- #
25
- class AgentJob < ::ActiveJob::Base
26
- # @param agent_class_name [String]
27
- # The constantize-able class name of the agent to run (e.g. "MyAgent").
28
- # **Security**: only classes that are subclasses of +Phronomy::Agent::Base+
29
- # are accepted. Never pass a value derived from user-controlled input.
30
- # @param input [String, Hash]
31
- # User input forwarded unchanged to the agent's +#stream+ method.
32
- # @param channel [String]
33
- # ActionCable channel name. Retained for documentation / future routing.
34
- # @param stream [String]
35
- # ActionCable stream identifier passed to +ActionCable.server.broadcast+.
36
- # @param config [Hash]
37
- # Configuration forwarded to the agent's +#stream+ call. Both symbol and
38
- # string keys are accepted; all keys are converted to symbols before use.
39
- def perform(agent_class_name, input, channel:, stream:, config: {})
40
- klass = resolve_agent_class!(agent_class_name)
41
- agent = klass.new
42
- agent.stream(input, config: config.transform_keys(&:to_sym)) do |event|
43
- ActionCable.server.broadcast(stream, build_payload(event))
44
- end
45
- rescue => e
46
- ::Rails.logger.error("[Phronomy::Rails::AgentJob] agent error (#{e.class}): #{e.message}")
47
- ActionCable.server.broadcast(stream, {type: "error", message: "An error occurred while processing your request."})
48
- end
49
-
50
- private
51
-
52
- # Resolves and validates the agent class name.
53
- # Raises ArgumentError when the name does not resolve to a subclass of
54
- # Phronomy::Agent::Base, preventing arbitrary class instantiation.
55
- def resolve_agent_class!(class_name)
56
- klass = Object.const_get(class_name.to_s)
57
- unless klass.is_a?(Class) && klass < Phronomy::Agent::Base
58
- raise ArgumentError, "#{class_name.inspect} is not a Phronomy::Agent::Base subclass"
59
- end
60
- klass
61
- rescue NameError
62
- raise ArgumentError, "Unknown agent class: #{class_name.inspect}"
63
- end
64
-
65
- def build_payload(event)
66
- case event.type
67
- when :token then {type: "token", content: event.payload[:content]}
68
- when :done then {type: "done", output: event.payload[:output]}
69
- when :error then {type: "error", message: "An error occurred while processing your request."}
70
- else {type: event.type.to_s}
71
- end
72
- end
73
- end
74
- end
75
- end