lex-agentic-memory 0.1.8 → 0.1.11
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 +24 -1
- data/lib/legion/extensions/agentic/memory/archaeology/actors/decay.rb +22 -0
- data/lib/legion/extensions/agentic/memory/archaeology/runners/cognitive_archaeology.rb +6 -0
- data/lib/legion/extensions/agentic/memory/compression/actors/maintenance.rb +22 -0
- data/lib/legion/extensions/agentic/memory/echo/actors/decay.rb +22 -0
- data/lib/legion/extensions/agentic/memory/echo_chamber/actors/decay.rb +22 -0
- data/lib/legion/extensions/agentic/memory/echo_chamber/runners/cognitive_echo_chamber.rb +6 -0
- data/lib/legion/extensions/agentic/memory/immune_memory/actors/decay.rb +22 -0
- data/lib/legion/extensions/agentic/memory/nostalgia/actors/maintenance.rb +22 -0
- data/lib/legion/extensions/agentic/memory/palimpsest/actors/decay.rb +22 -0
- data/lib/legion/extensions/agentic/memory/reserve/actors/maintenance.rb +22 -0
- data/lib/legion/extensions/agentic/memory/semantic_priming/actors/decay.rb +22 -0
- data/lib/legion/extensions/agentic/memory/semantic_satiation/actors/recovery.rb +22 -0
- data/lib/legion/extensions/agentic/memory/trace/actors/quota.rb +22 -0
- data/lib/legion/extensions/agentic/memory/trace/helpers/hot_tier.rb +98 -0
- data/lib/legion/extensions/agentic/memory/trace/helpers/postgres_store.rb +393 -0
- data/lib/legion/extensions/agentic/memory/trace/runners/consolidation.rb +7 -0
- data/lib/legion/extensions/agentic/memory/trace.rb +22 -1
- data/lib/legion/extensions/agentic/memory/version.rb +1 -1
- data/spec/legion/extensions/agentic/memory/archaeology/actors/decay_spec.rb +24 -0
- data/spec/legion/extensions/agentic/memory/compression/actors/maintenance_spec.rb +24 -0
- data/spec/legion/extensions/agentic/memory/echo/actors/decay_spec.rb +24 -0
- data/spec/legion/extensions/agentic/memory/echo_chamber/actors/decay_spec.rb +24 -0
- data/spec/legion/extensions/agentic/memory/immune_memory/actors/decay_spec.rb +24 -0
- data/spec/legion/extensions/agentic/memory/nostalgia/actors/maintenance_spec.rb +24 -0
- data/spec/legion/extensions/agentic/memory/palimpsest/actors/decay_spec.rb +24 -0
- data/spec/legion/extensions/agentic/memory/reserve/actors/maintenance_spec.rb +24 -0
- data/spec/legion/extensions/agentic/memory/semantic_priming/actors/decay_spec.rb +24 -0
- data/spec/legion/extensions/agentic/memory/semantic_satiation/actors/recovery_spec.rb +24 -0
- data/spec/legion/extensions/agentic/memory/trace/actors/quota_spec.rb +24 -0
- data/spec/legion/extensions/agentic/memory/trace/helpers/hot_tier_spec.rb +337 -0
- data/spec/legion/extensions/agentic/memory/trace/helpers/postgres_store_spec.rb +464 -0
- metadata +27 -1
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'hot_tier'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Agentic
|
|
8
|
+
module Memory
|
|
9
|
+
module Trace
|
|
10
|
+
module Helpers
|
|
11
|
+
# Write-through durable store backed by Legion::Data (PostgreSQL or MySQL).
|
|
12
|
+
# All writes go directly to the database — no in-memory dirty tracking, no flush.
|
|
13
|
+
# Scoped by tenant_id so multiple agents can share the same DB tables safely.
|
|
14
|
+
class PostgresStore
|
|
15
|
+
TRACES_TABLE = :memory_traces
|
|
16
|
+
ASSOCIATIONS_TABLE = :memory_associations
|
|
17
|
+
|
|
18
|
+
def initialize(tenant_id: nil)
|
|
19
|
+
@tenant_id = tenant_id
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Store (upsert) a trace by trace_id.
|
|
23
|
+
# Returns the trace_id on success, nil if the DB is not ready.
|
|
24
|
+
def store(trace)
|
|
25
|
+
return nil unless db_ready?
|
|
26
|
+
|
|
27
|
+
row = serialize_trace(trace)
|
|
28
|
+
begin
|
|
29
|
+
db[TRACES_TABLE].insert_conflict(:replace).insert(row)
|
|
30
|
+
rescue Sequel::UniqueConstraintViolation
|
|
31
|
+
db[TRACES_TABLE].where(trace_id: trace[:trace_id]).update(row.except(:trace_id))
|
|
32
|
+
end
|
|
33
|
+
HotTier.cache_trace(trace, tenant_id: @tenant_id) if HotTier.available?
|
|
34
|
+
trace[:trace_id]
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
log_warn("store failed: #{e.message}")
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Retrieve a single trace by trace_id (tenant-scoped).
|
|
41
|
+
# Checks the Redis hot tier first; falls through to DB on a miss and caches the result.
|
|
42
|
+
# Returns a trace hash or nil.
|
|
43
|
+
def retrieve(trace_id)
|
|
44
|
+
if HotTier.available?
|
|
45
|
+
cached = HotTier.fetch_trace(trace_id, tenant_id: @tenant_id)
|
|
46
|
+
return cached if cached
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
return nil unless db_ready?
|
|
50
|
+
|
|
51
|
+
row = traces_ds.where(trace_id: trace_id).first
|
|
52
|
+
trace = row ? deserialize_trace(row) : nil
|
|
53
|
+
HotTier.cache_trace(trace, tenant_id: @tenant_id) if HotTier.available? && trace
|
|
54
|
+
trace
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
log_warn("retrieve failed: #{e.message}")
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Retrieve traces by type, ordered by strength descending.
|
|
61
|
+
def retrieve_by_type(type, limit: 100, min_strength: 0.0)
|
|
62
|
+
return [] unless db_ready?
|
|
63
|
+
|
|
64
|
+
rows = traces_ds
|
|
65
|
+
.where(trace_type: type.to_s)
|
|
66
|
+
.where { strength >= min_strength }
|
|
67
|
+
.order(Sequel.desc(:strength))
|
|
68
|
+
.limit(limit)
|
|
69
|
+
.all
|
|
70
|
+
rows.map { |r| deserialize_trace(r) }
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
log_warn("retrieve_by_type failed: #{e.message}")
|
|
73
|
+
[]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Retrieve traces whose domain_tags column contains the given tag string.
|
|
77
|
+
def retrieve_by_domain(tag, limit: 50)
|
|
78
|
+
return [] unless db_ready?
|
|
79
|
+
|
|
80
|
+
rows = traces_ds
|
|
81
|
+
.where(Sequel.like(:domain_tags, "%#{tag}%"))
|
|
82
|
+
.order(Sequel.desc(:strength))
|
|
83
|
+
.limit(limit)
|
|
84
|
+
.all
|
|
85
|
+
rows.map { |r| deserialize_trace(r) }
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
log_warn("retrieve_by_domain failed: #{e.message}")
|
|
88
|
+
[]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Return all traces for this tenant.
|
|
92
|
+
def all_traces
|
|
93
|
+
return [] unless db_ready?
|
|
94
|
+
|
|
95
|
+
traces_ds.all.map { |r| deserialize_trace(r) }
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
log_warn("all_traces failed: #{e.message}")
|
|
98
|
+
[]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Delete a trace and its association rows.
|
|
102
|
+
def delete(trace_id)
|
|
103
|
+
HotTier.evict_trace(trace_id, tenant_id: @tenant_id) if HotTier.available?
|
|
104
|
+
return unless db_ready?
|
|
105
|
+
|
|
106
|
+
db[ASSOCIATIONS_TABLE].where(trace_id_a: trace_id).delete
|
|
107
|
+
db[ASSOCIATIONS_TABLE].where(trace_id_b: trace_id).delete
|
|
108
|
+
db[TRACES_TABLE].where(trace_id: trace_id).delete
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
log_warn("delete failed: #{e.message}")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Partial update of a trace by trace_id.
|
|
114
|
+
# Evicts the hot-tier entry so a stale cached version cannot be served.
|
|
115
|
+
def update(trace_id, **fields)
|
|
116
|
+
return unless db_ready?
|
|
117
|
+
|
|
118
|
+
db[TRACES_TABLE].where(trace_id: trace_id).update(map_update_fields(fields))
|
|
119
|
+
HotTier.evict_trace(trace_id, tenant_id: @tenant_id) if HotTier.available?
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
log_warn("update failed: #{e.message}")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Create or increment a coactivation association between two traces.
|
|
125
|
+
def record_coactivation(id_a, id_b)
|
|
126
|
+
return unless db_ready?
|
|
127
|
+
return if id_a == id_b
|
|
128
|
+
|
|
129
|
+
now = Time.now.utc
|
|
130
|
+
existing = db[ASSOCIATIONS_TABLE]
|
|
131
|
+
.where(trace_id_a: id_a, trace_id_b: id_b)
|
|
132
|
+
.first
|
|
133
|
+
|
|
134
|
+
if existing
|
|
135
|
+
db[ASSOCIATIONS_TABLE]
|
|
136
|
+
.where(id: existing[:id])
|
|
137
|
+
.update(
|
|
138
|
+
coactivation_count: existing[:coactivation_count] + 1,
|
|
139
|
+
updated_at: now
|
|
140
|
+
)
|
|
141
|
+
else
|
|
142
|
+
db[ASSOCIATIONS_TABLE].insert(
|
|
143
|
+
trace_id_a: id_a,
|
|
144
|
+
trace_id_b: id_b,
|
|
145
|
+
coactivation_count: 1,
|
|
146
|
+
linked: false,
|
|
147
|
+
tenant_id: @tenant_id,
|
|
148
|
+
created_at: now,
|
|
149
|
+
updated_at: now
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
log_warn("record_coactivation failed: #{e.message}")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Return the set of trace IDs associated with a given trace (bidirectional).
|
|
157
|
+
def associations_for(trace_id)
|
|
158
|
+
return [] unless db_ready?
|
|
159
|
+
|
|
160
|
+
a_side = db[ASSOCIATIONS_TABLE]
|
|
161
|
+
.where(trace_id_a: trace_id)
|
|
162
|
+
.select_map(:trace_id_b)
|
|
163
|
+
b_side = db[ASSOCIATIONS_TABLE]
|
|
164
|
+
.where(trace_id_b: trace_id)
|
|
165
|
+
.select_map(:trace_id_a)
|
|
166
|
+
(a_side + b_side).uniq
|
|
167
|
+
rescue StandardError => e
|
|
168
|
+
log_warn("associations_for failed: #{e.message}")
|
|
169
|
+
[]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# BFS traversal starting from start_id.
|
|
173
|
+
# Returns an array of { trace_id:, depth:, path: } hashes.
|
|
174
|
+
def walk_associations(start_id:, max_hops: 12, min_strength: 0.1)
|
|
175
|
+
return [] unless db_ready?
|
|
176
|
+
|
|
177
|
+
start_row = traces_ds.where(trace_id: start_id).first
|
|
178
|
+
return [] unless start_row
|
|
179
|
+
|
|
180
|
+
results = []
|
|
181
|
+
visited = Set.new([start_id])
|
|
182
|
+
queue = [[start_id, 0, [start_id]]]
|
|
183
|
+
|
|
184
|
+
until queue.empty?
|
|
185
|
+
current_id, depth, path = queue.shift
|
|
186
|
+
neighbor_ids = associations_for(current_id)
|
|
187
|
+
|
|
188
|
+
neighbor_ids.each do |nid|
|
|
189
|
+
next if visited.include?(nid)
|
|
190
|
+
|
|
191
|
+
neighbor_row = traces_ds
|
|
192
|
+
.where(trace_id: nid)
|
|
193
|
+
.where { strength >= min_strength }
|
|
194
|
+
.first
|
|
195
|
+
next unless neighbor_row
|
|
196
|
+
|
|
197
|
+
visited << nid
|
|
198
|
+
neighbor_path = path + [nid]
|
|
199
|
+
results << { trace_id: nid, depth: depth + 1, path: neighbor_path }
|
|
200
|
+
queue << [nid, depth + 1, neighbor_path] if depth + 1 < max_hops
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
results
|
|
205
|
+
rescue StandardError => e
|
|
206
|
+
log_warn("walk_associations failed: #{e.message}")
|
|
207
|
+
[]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Delete the N traces with the lowest confidence for a given type (quota enforcement).
|
|
211
|
+
def delete_lowest_confidence(trace_type:, count:)
|
|
212
|
+
return unless db_ready?
|
|
213
|
+
|
|
214
|
+
ids = traces_ds
|
|
215
|
+
.where(trace_type: trace_type.to_s)
|
|
216
|
+
.order(:confidence)
|
|
217
|
+
.limit(count)
|
|
218
|
+
.select_map(:trace_id)
|
|
219
|
+
|
|
220
|
+
ids.each { |tid| delete(tid) }
|
|
221
|
+
rescue StandardError => e
|
|
222
|
+
log_warn("delete_lowest_confidence failed: #{e.message}")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Delete the N least-recently-used traces for a given type (quota enforcement).
|
|
226
|
+
def delete_least_recently_used(trace_type:, count:)
|
|
227
|
+
return unless db_ready?
|
|
228
|
+
|
|
229
|
+
ids = traces_ds
|
|
230
|
+
.where(trace_type: trace_type.to_s)
|
|
231
|
+
.order(:last_reinforced)
|
|
232
|
+
.limit(count)
|
|
233
|
+
.select_map(:trace_id)
|
|
234
|
+
|
|
235
|
+
ids.each { |tid| delete(tid) }
|
|
236
|
+
rescue StandardError => e
|
|
237
|
+
log_warn("delete_least_recently_used failed: #{e.message}")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Convenience: retrieve firmware-type traces.
|
|
241
|
+
def firmware_traces
|
|
242
|
+
retrieve_by_type(:firmware)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# No-op — this store is write-through; nothing to flush.
|
|
246
|
+
def flush; end
|
|
247
|
+
|
|
248
|
+
# Returns true when both required tables exist in the connected DB.
|
|
249
|
+
def db_ready?
|
|
250
|
+
defined?(Legion::Data) &&
|
|
251
|
+
Legion::Data.respond_to?(:connection) &&
|
|
252
|
+
Legion::Data.connection&.table_exists?(TRACES_TABLE) &&
|
|
253
|
+
Legion::Data.connection.table_exists?(ASSOCIATIONS_TABLE)
|
|
254
|
+
rescue StandardError
|
|
255
|
+
false
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
private
|
|
259
|
+
|
|
260
|
+
def db
|
|
261
|
+
Legion::Data.connection
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Dataset for memory_traces scoped by tenant_id (if set).
|
|
265
|
+
def traces_ds
|
|
266
|
+
ds = db[TRACES_TABLE]
|
|
267
|
+
@tenant_id ? ds.where(tenant_id: @tenant_id) : ds
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def serialize_trace(trace)
|
|
271
|
+
payload = trace[:content_payload] || trace[:content]
|
|
272
|
+
tags = trace[:domain_tags]
|
|
273
|
+
assocs = trace[:associated_traces]
|
|
274
|
+
conf = trace[:confidence]
|
|
275
|
+
ev = trace[:emotional_valence]
|
|
276
|
+
|
|
277
|
+
{
|
|
278
|
+
trace_id: trace[:trace_id],
|
|
279
|
+
tenant_id: @tenant_id,
|
|
280
|
+
trace_type: trace[:trace_type].to_s,
|
|
281
|
+
content: payload.is_a?(Hash) ? Legion::JSON.dump(payload) : payload.to_s,
|
|
282
|
+
significance: conf,
|
|
283
|
+
confidence: conf,
|
|
284
|
+
associations: assocs.is_a?(Array) ? Legion::JSON.dump(assocs) : '[]',
|
|
285
|
+
domain_tags: tags.is_a?(Array) ? Legion::JSON.dump(tags) : nil,
|
|
286
|
+
strength: trace[:strength],
|
|
287
|
+
peak_strength: trace[:peak_strength],
|
|
288
|
+
base_decay_rate: trace[:base_decay_rate],
|
|
289
|
+
emotional_valence: ev.is_a?(Numeric) ? ev.to_f : 0.0,
|
|
290
|
+
emotional_intensity: trace[:emotional_intensity],
|
|
291
|
+
origin: trace[:origin].to_s,
|
|
292
|
+
source_agent_id: trace[:source_agent_id],
|
|
293
|
+
storage_tier: trace[:storage_tier].to_s,
|
|
294
|
+
last_reinforced: trace[:last_reinforced],
|
|
295
|
+
last_decayed: trace[:last_decayed],
|
|
296
|
+
reinforcement_count: trace[:reinforcement_count],
|
|
297
|
+
unresolved: trace[:unresolved] || false,
|
|
298
|
+
consolidation_candidate: trace[:consolidation_candidate] || false,
|
|
299
|
+
parent_trace_id: trace[:parent_trace_id],
|
|
300
|
+
encryption_key_id: trace[:encryption_key_id],
|
|
301
|
+
partition_id: trace[:partition_id],
|
|
302
|
+
created_at: trace[:created_at] || Time.now.utc,
|
|
303
|
+
accessed_at: Time.now.utc
|
|
304
|
+
}
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def deserialize_trace(row)
|
|
308
|
+
content = parse_json_or_raw(row[:content])
|
|
309
|
+
{
|
|
310
|
+
trace_id: row[:trace_id],
|
|
311
|
+
trace_type: row[:trace_type]&.to_sym,
|
|
312
|
+
content_payload: content,
|
|
313
|
+
content: content,
|
|
314
|
+
strength: row[:strength],
|
|
315
|
+
peak_strength: row[:peak_strength],
|
|
316
|
+
base_decay_rate: row[:base_decay_rate],
|
|
317
|
+
emotional_valence: row[:emotional_valence].to_f,
|
|
318
|
+
emotional_intensity: row[:emotional_intensity],
|
|
319
|
+
domain_tags: parse_json_array(row[:domain_tags]),
|
|
320
|
+
origin: row[:origin]&.to_sym,
|
|
321
|
+
source_agent_id: row[:source_agent_id],
|
|
322
|
+
created_at: row[:created_at],
|
|
323
|
+
last_reinforced: row[:last_reinforced],
|
|
324
|
+
last_decayed: row[:last_decayed],
|
|
325
|
+
reinforcement_count: row[:reinforcement_count],
|
|
326
|
+
confidence: row[:confidence],
|
|
327
|
+
storage_tier: row[:storage_tier]&.to_sym,
|
|
328
|
+
partition_id: row[:partition_id],
|
|
329
|
+
encryption_key_id: row[:encryption_key_id],
|
|
330
|
+
associated_traces: parse_json_array(row[:associations]),
|
|
331
|
+
parent_trace_id: row[:parent_trace_id],
|
|
332
|
+
child_trace_ids: [],
|
|
333
|
+
unresolved: row[:unresolved] || false,
|
|
334
|
+
consolidation_candidate: row[:consolidation_candidate] || false
|
|
335
|
+
}
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Map keyword fields for partial updates, translating to DB column names.
|
|
339
|
+
def map_update_fields(fields)
|
|
340
|
+
mapping = {
|
|
341
|
+
content_payload: :content,
|
|
342
|
+
associated_traces: :associations,
|
|
343
|
+
parent_trace_id: :parent_trace_id,
|
|
344
|
+
child_trace_ids: nil # not stored as a column
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
fields.each_with_object({}) do |(k, v), row|
|
|
348
|
+
col = mapping.key?(k) ? mapping[k] : k
|
|
349
|
+
next if col.nil?
|
|
350
|
+
|
|
351
|
+
row[col] = case col
|
|
352
|
+
when :content
|
|
353
|
+
v.is_a?(Hash) ? Legion::JSON.dump(v) : v.to_s
|
|
354
|
+
when :associations
|
|
355
|
+
v.is_a?(Array) ? Legion::JSON.dump(v) : '[]'
|
|
356
|
+
when :domain_tags
|
|
357
|
+
v.is_a?(Array) ? Legion::JSON.dump(v) : nil
|
|
358
|
+
when :trace_type, :origin, :storage_tier
|
|
359
|
+
v.to_s
|
|
360
|
+
else
|
|
361
|
+
v
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def parse_json_or_raw(raw)
|
|
367
|
+
return raw unless raw.is_a?(String)
|
|
368
|
+
|
|
369
|
+
parsed = Legion::JSON.load(raw)
|
|
370
|
+
parsed.is_a?(Hash) ? parsed : raw
|
|
371
|
+
rescue StandardError
|
|
372
|
+
raw
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def parse_json_array(raw)
|
|
376
|
+
return [] unless raw.is_a?(String)
|
|
377
|
+
|
|
378
|
+
result = Legion::JSON.load(raw)
|
|
379
|
+
result.is_a?(Array) ? result : []
|
|
380
|
+
rescue StandardError
|
|
381
|
+
[]
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def log_warn(message)
|
|
385
|
+
Legion::Logging.warn "[memory:postgres_store] #{message}" if defined?(Legion::Logging)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
@@ -86,6 +86,13 @@ module Legion
|
|
|
86
86
|
{ linked: true }
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
def enforce_quota(store: nil, **)
|
|
90
|
+
store ||= default_store
|
|
91
|
+
quota = Quota.new
|
|
92
|
+
quota.enforce!(store)
|
|
93
|
+
{ success: true, within_limits: quota.within_limits?(store) }
|
|
94
|
+
end
|
|
95
|
+
|
|
89
96
|
def erase_by_type(type:, store: nil, **)
|
|
90
97
|
store ||= default_store
|
|
91
98
|
type = type.to_sym
|
|
@@ -5,6 +5,7 @@ require 'legion/extensions/agentic/memory/trace/helpers/trace'
|
|
|
5
5
|
require 'legion/extensions/agentic/memory/trace/helpers/decay'
|
|
6
6
|
require 'legion/extensions/agentic/memory/trace/helpers/store'
|
|
7
7
|
require 'legion/extensions/agentic/memory/trace/helpers/cache_store'
|
|
8
|
+
require 'legion/extensions/agentic/memory/trace/helpers/postgres_store'
|
|
8
9
|
require 'legion/extensions/agentic/memory/trace/helpers/error_tracer'
|
|
9
10
|
require 'legion/extensions/agentic/memory/trace/runners/traces'
|
|
10
11
|
require 'legion/extensions/agentic/memory/trace/runners/consolidation'
|
|
@@ -31,7 +32,10 @@ module Legion
|
|
|
31
32
|
private
|
|
32
33
|
|
|
33
34
|
def create_store
|
|
34
|
-
if
|
|
35
|
+
if postgres_available?
|
|
36
|
+
Legion::Logging.debug '[memory] Using shared PostgresStore (write-through)'
|
|
37
|
+
Helpers::PostgresStore.new(tenant_id: resolve_tenant_id)
|
|
38
|
+
elsif defined?(Legion::Cache) && Legion::Cache.respond_to?(:connected?) && Legion::Cache.connected?
|
|
35
39
|
Legion::Logging.debug '[memory] Using shared CacheStore (memcached)'
|
|
36
40
|
Helpers::CacheStore.new
|
|
37
41
|
else
|
|
@@ -39,6 +43,23 @@ module Legion
|
|
|
39
43
|
Helpers::Store.new
|
|
40
44
|
end
|
|
41
45
|
end
|
|
46
|
+
|
|
47
|
+
def postgres_available?
|
|
48
|
+
defined?(Legion::Data) &&
|
|
49
|
+
Legion::Data.respond_to?(:connection) &&
|
|
50
|
+
Legion::Data.connection &&
|
|
51
|
+
%i[postgres mysql2].include?(Legion::Data.connection.adapter_scheme) &&
|
|
52
|
+
Legion::Data.connection.table_exists?(:memory_traces) &&
|
|
53
|
+
Legion::Data.connection.table_exists?(:memory_associations)
|
|
54
|
+
rescue StandardError
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resolve_tenant_id
|
|
59
|
+
Legion::Settings[:data]&.dig(:tenant_id)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
42
63
|
end
|
|
43
64
|
end
|
|
44
65
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Actors
|
|
6
|
+
class Every # rubocop:disable Lint/EmptyClass
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
$LOADED_FEATURES << 'legion/extensions/actors/every'
|
|
12
|
+
|
|
13
|
+
require 'legion/extensions/agentic/memory/archaeology/actors/decay'
|
|
14
|
+
|
|
15
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::Archaeology::Actors::Decay do
|
|
16
|
+
subject(:actor) { described_class.new }
|
|
17
|
+
|
|
18
|
+
it { expect(actor.runner_class).to eq(Legion::Extensions::Agentic::Memory::Archaeology::Runners::CognitiveArchaeology) }
|
|
19
|
+
it { expect(actor.runner_function).to eq('decay_all') }
|
|
20
|
+
it { expect(actor.time).to eq(120) }
|
|
21
|
+
it { expect(actor.use_runner?).to be false }
|
|
22
|
+
it { expect(actor.check_subtask?).to be false }
|
|
23
|
+
it { expect(actor.generate_task?).to be false }
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Actors
|
|
6
|
+
class Every # rubocop:disable Lint/EmptyClass
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
$LOADED_FEATURES << 'legion/extensions/actors/every'
|
|
12
|
+
|
|
13
|
+
require 'legion/extensions/agentic/memory/compression/actors/maintenance'
|
|
14
|
+
|
|
15
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::Compression::Actors::Maintenance do
|
|
16
|
+
subject(:actor) { described_class.new }
|
|
17
|
+
|
|
18
|
+
it { expect(actor.runner_class).to eq(Legion::Extensions::Agentic::Memory::Compression::Runners::CognitiveCompression) }
|
|
19
|
+
it { expect(actor.runner_function).to eq('compress_all') }
|
|
20
|
+
it { expect(actor.time).to eq(300) }
|
|
21
|
+
it { expect(actor.use_runner?).to be false }
|
|
22
|
+
it { expect(actor.check_subtask?).to be false }
|
|
23
|
+
it { expect(actor.generate_task?).to be false }
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Actors
|
|
6
|
+
class Every # rubocop:disable Lint/EmptyClass
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
$LOADED_FEATURES << 'legion/extensions/actors/every'
|
|
12
|
+
|
|
13
|
+
require 'legion/extensions/agentic/memory/echo/actors/decay'
|
|
14
|
+
|
|
15
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::Echo::Actors::Decay do
|
|
16
|
+
subject(:actor) { described_class.new }
|
|
17
|
+
|
|
18
|
+
it { expect(actor.runner_class).to eq(Legion::Extensions::Agentic::Memory::Echo::Runners::CognitiveEcho) }
|
|
19
|
+
it { expect(actor.runner_function).to eq('decay_all') }
|
|
20
|
+
it { expect(actor.time).to eq(60) }
|
|
21
|
+
it { expect(actor.use_runner?).to be false }
|
|
22
|
+
it { expect(actor.check_subtask?).to be false }
|
|
23
|
+
it { expect(actor.generate_task?).to be false }
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Actors
|
|
6
|
+
class Every # rubocop:disable Lint/EmptyClass
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
$LOADED_FEATURES << 'legion/extensions/actors/every'
|
|
12
|
+
|
|
13
|
+
require 'legion/extensions/agentic/memory/echo_chamber/actors/decay'
|
|
14
|
+
|
|
15
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::EchoChamber::Actors::Decay do
|
|
16
|
+
subject(:actor) { described_class.new }
|
|
17
|
+
|
|
18
|
+
it { expect(actor.runner_class).to eq(Legion::Extensions::Agentic::Memory::EchoChamber::Runners::CognitiveEchoChamber) }
|
|
19
|
+
it { expect(actor.runner_function).to eq('decay_all') }
|
|
20
|
+
it { expect(actor.time).to eq(60) }
|
|
21
|
+
it { expect(actor.use_runner?).to be false }
|
|
22
|
+
it { expect(actor.check_subtask?).to be false }
|
|
23
|
+
it { expect(actor.generate_task?).to be false }
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Actors
|
|
6
|
+
class Every # rubocop:disable Lint/EmptyClass
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
$LOADED_FEATURES << 'legion/extensions/actors/every'
|
|
12
|
+
|
|
13
|
+
require 'legion/extensions/agentic/memory/immune_memory/actors/decay'
|
|
14
|
+
|
|
15
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::ImmuneMemory::Actors::Decay do
|
|
16
|
+
subject(:actor) { described_class.new }
|
|
17
|
+
|
|
18
|
+
it { expect(actor.runner_class).to eq(Legion::Extensions::Agentic::Memory::ImmuneMemory::Runners::CognitiveImmuneMemory) }
|
|
19
|
+
it { expect(actor.runner_function).to eq('decay_all') }
|
|
20
|
+
it { expect(actor.time).to eq(60) }
|
|
21
|
+
it { expect(actor.use_runner?).to be false }
|
|
22
|
+
it { expect(actor.check_subtask?).to be false }
|
|
23
|
+
it { expect(actor.generate_task?).to be false }
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Actors
|
|
6
|
+
class Every # rubocop:disable Lint/EmptyClass
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
$LOADED_FEATURES << 'legion/extensions/actors/every'
|
|
12
|
+
|
|
13
|
+
require 'legion/extensions/agentic/memory/nostalgia/actors/maintenance'
|
|
14
|
+
|
|
15
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::Nostalgia::Actors::Maintenance do
|
|
16
|
+
subject(:actor) { described_class.new }
|
|
17
|
+
|
|
18
|
+
it { expect(actor.runner_class).to eq(Legion::Extensions::Agentic::Memory::Nostalgia::Runners::Recall) }
|
|
19
|
+
it { expect(actor.runner_function).to eq('age_memories') }
|
|
20
|
+
it { expect(actor.time).to eq(120) }
|
|
21
|
+
it { expect(actor.use_runner?).to be false }
|
|
22
|
+
it { expect(actor.check_subtask?).to be false }
|
|
23
|
+
it { expect(actor.generate_task?).to be false }
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Actors
|
|
6
|
+
class Every # rubocop:disable Lint/EmptyClass
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
$LOADED_FEATURES << 'legion/extensions/actors/every'
|
|
12
|
+
|
|
13
|
+
require 'legion/extensions/agentic/memory/palimpsest/actors/decay'
|
|
14
|
+
|
|
15
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::Palimpsest::Actors::Decay do
|
|
16
|
+
subject(:actor) { described_class.new }
|
|
17
|
+
|
|
18
|
+
it { expect(actor.runner_class).to eq(Legion::Extensions::Agentic::Memory::Palimpsest::Runners::CognitivePalimpsest) }
|
|
19
|
+
it { expect(actor.runner_function).to eq('decay_all_ghosts') }
|
|
20
|
+
it { expect(actor.time).to eq(60) }
|
|
21
|
+
it { expect(actor.use_runner?).to be false }
|
|
22
|
+
it { expect(actor.check_subtask?).to be false }
|
|
23
|
+
it { expect(actor.generate_task?).to be false }
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Actors
|
|
6
|
+
class Every # rubocop:disable Lint/EmptyClass
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
$LOADED_FEATURES << 'legion/extensions/actors/every'
|
|
12
|
+
|
|
13
|
+
require 'legion/extensions/agentic/memory/reserve/actors/maintenance'
|
|
14
|
+
|
|
15
|
+
RSpec.describe Legion::Extensions::Agentic::Memory::Reserve::Actors::Maintenance do
|
|
16
|
+
subject(:actor) { described_class.new }
|
|
17
|
+
|
|
18
|
+
it { expect(actor.runner_class).to eq(Legion::Extensions::Agentic::Memory::Reserve::Runners::CognitiveReserve) }
|
|
19
|
+
it { expect(actor.runner_function).to eq('update_cognitive_reserve') }
|
|
20
|
+
it { expect(actor.time).to eq(60) }
|
|
21
|
+
it { expect(actor.use_runner?).to be false }
|
|
22
|
+
it { expect(actor.check_subtask?).to be false }
|
|
23
|
+
it { expect(actor.generate_task?).to be false }
|
|
24
|
+
end
|