lex-mesh 0.4.6 → 0.4.8

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: 5e748e565682b21ecca099dff05a5192840b124d01697a572dcb34e4fd8ddf4b
4
- data.tar.gz: 01ba8a7a0c658ef92be7bd39faa85e3d616e9b65ff8a05f8eb476816877a5e03
3
+ metadata.gz: 1de324c2997f1d71286302c373a002c92bde5a389cfc9baf9bdce04d373f4fc5
4
+ data.tar.gz: 365de50aed497f545fb7deb881c9c3b87156acf6f8e2a5d443393004868ae278
5
5
  SHA512:
6
- metadata.gz: 5c13da093e85c3524da32a7415600c8e6f46376fa3b9c1f86fc64115ec9b44f375a304e799edf2bb0fcd19647fa9fadbb5bc669332cfc65d8094a2ed51cf8da2
7
- data.tar.gz: f12094b18f644e91eca7fe9ac0cdd8d987ce41d910830d89bb149f9843368c76511f53a7e8aba0110d15918a0d7410053bf552bb0480101ab1227097bbe86011
6
+ metadata.gz: 506f44f20bec8e35b4b5fc3eb389796e4664511fa0feea50dd962ead40b10cb977a4c0ebc518d4ab20d5cdeaed2b1c2374e3a4ef5a9011121b4cb614b6540ce3
7
+ data.tar.gz: 188f168003f79c0ed067cc75f98888fa2ecb0dee5244ad79d76eeeb5f5f538ddbd414614be318137806ce69b2d87894b1ec0e479f46ab7d2238700f0286a7989
@@ -6,7 +6,7 @@ module Legion
6
6
  module Extensions
7
7
  module Mesh
8
8
  module Actor
9
- class Gossip < Legion::Extensions::Actors::Every # rubocop:disable Legion/Extension/EveryActorRequiresTime
9
+ class Gossip < Legion::Extensions::Actors::Every
10
10
  def runner_class
11
11
  Legion::Extensions::Mesh::Runners::Mesh
12
12
  end
@@ -6,7 +6,7 @@ module Legion
6
6
  module Extensions
7
7
  module Mesh
8
8
  module Actor
9
- class Heartbeat < Legion::Extensions::Actors::Every # rubocop:disable Legion/Extension/EveryActorRequiresTime
9
+ class Heartbeat < Legion::Extensions::Actors::Every
10
10
  def runner_class
11
11
  Legion::Extensions::Mesh::Runners::Mesh
12
12
  end
@@ -6,7 +6,7 @@ module Legion
6
6
  module Extensions
7
7
  module Mesh
8
8
  module Actor
9
- class PendingExpiry < Legion::Extensions::Actors::Every # rubocop:disable Legion/Extension/EveryActorRequiresTime
9
+ class PendingExpiry < Legion::Extensions::Actors::Every
10
10
  def runner_class
11
11
  Legion::Extensions::Mesh::Runners::Preferences
12
12
  end
@@ -6,7 +6,7 @@ module Legion
6
6
  module Extensions
7
7
  module Mesh
8
8
  module Actor
9
- class SilenceWatchdog < Legion::Extensions::Actors::Every # rubocop:disable Legion/Extension/EveryActorRequiresTime
9
+ class SilenceWatchdog < Legion::Extensions::Actors::Every
10
10
  def runner_class
11
11
  Legion::Extensions::Mesh::Runners::Mesh
12
12
  end
@@ -90,7 +90,7 @@ module Legion
90
90
  confidence = SOURCE_CONFIDENCE.fetch(source.to_s, 0.5)
91
91
  memory_runner.store_trace(
92
92
  type: :semantic,
93
- content_payload: { domain: domain, value: value, source: source }.to_s,
93
+ content_payload: pref_json_dump({ domain: domain, value: value, source: source }),
94
94
  domain_tags: ['preference', "preference:#{domain}", "owner:#{owner_id}"],
95
95
  origin: :direct_experience,
96
96
  confidence: confidence
@@ -103,7 +103,34 @@ module Legion
103
103
  def clear_preferences(owner_id:, source: nil)
104
104
  return { cleared: false, reason: :memory_not_available } unless memory_available?
105
105
 
106
- { cleared: true, owner_id: owner_id, source: source }
106
+ runner = memory_runner
107
+ result = runner.retrieve_by_domain(domain_tag: "owner:#{owner_id}")
108
+ traces = result[:traces] || []
109
+ traces.each { |t| runner.delete_trace(trace_id: t[:trace_id]) }
110
+ { cleared: true, owner_id: owner_id, source: source, count: traces.size }
111
+ rescue StandardError => e
112
+ { cleared: false, reason: :error, message: e.message }
113
+ end
114
+
115
+ def erase_partner!(identity:)
116
+ key = identity.to_s
117
+ @observation_counts&.delete(key) # rubocop:disable ThreadSafety/ClassInstanceVariable
118
+ @observation_signals&.delete(key) # rubocop:disable ThreadSafety/ClassInstanceVariable
119
+ @inferred_preferences&.delete(key) # rubocop:disable ThreadSafety/ClassInstanceVariable
120
+ clear_mesh_cache(agent_id: key)
121
+
122
+ traces_deleted = 0
123
+ if memory_available?
124
+ runner = memory_runner
125
+ result = runner.retrieve_by_domain(domain_tag: "owner:#{key}")
126
+ traces = result[:traces] || []
127
+ traces.each { |t| runner.delete_trace(trace_id: t[:trace_id]) }
128
+ traces_deleted = traces.size
129
+ end
130
+
131
+ { erased: true, identity: key, traces_deleted: traces_deleted }
132
+ rescue StandardError => e
133
+ { erased: false, identity: key, reason: :error, message: e.message }
107
134
  end
108
135
 
109
136
  def preference_instructions(profile:)
@@ -224,14 +251,31 @@ module Legion
224
251
  payload = trace[:content_payload]
225
252
  return nil unless payload.is_a?(String)
226
253
 
227
- match = payload.match(/domain.*?(\w+).*?value.*?(\w+).*?source.*?(\w+)/)
228
- return nil unless match
254
+ parsed = parse_preference_payload_json(payload)
255
+ parsed ||= parse_preference_payload_legacy(payload)
256
+ return nil unless parsed
257
+
258
+ { domain: parsed[:domain], value: parsed[:value], source: parsed[:source], confidence: trace[:confidence] }
259
+ rescue StandardError => _e
260
+ nil
261
+ end
262
+
263
+ def parse_preference_payload_json(payload)
264
+ data = pref_json_load(payload)
265
+ return nil unless data.is_a?(Hash) && data[:domain] && data[:value] && data[:source]
229
266
 
230
- { domain: match[1], value: match[2], source: match[3], confidence: trace[:confidence] }
267
+ data
231
268
  rescue StandardError => _e
232
269
  nil
233
270
  end
234
271
 
272
+ def parse_preference_payload_legacy(payload)
273
+ match = payload.match(/domain.*?(\w+).*?value.*?(\w+).*?source.*?(\w+)/)
274
+ return nil unless match
275
+
276
+ { domain: match[1], value: match[2], source: match[3] }
277
+ end
278
+
235
279
  def compute_compatibility(profile)
236
280
  return nil unless profile.is_a?(Hash) && profile[:personality]
237
281
  return nil unless personality_available?
@@ -326,7 +370,15 @@ module Legion
326
370
  unique_channels = signals.map { |s| s[:channel] }.uniq
327
371
  return nil unless unique_channels.size >= 3
328
372
 
329
- { domain: 'format', value: 'adaptive', source: 'observation', confidence: 0.55 }
373
+ { domain: 'format', value: 'structured', source: 'observation', confidence: 0.55 }
374
+ end
375
+
376
+ def pref_json_dump(data)
377
+ Legion::JSON.dump(data) # rubocop:disable Legion/HelperMigration/DirectJson
378
+ end
379
+
380
+ def pref_json_load(payload)
381
+ Legion::JSON.load(payload) # rubocop:disable Legion/HelperMigration/DirectJson
330
382
  end
331
383
  end
332
384
  end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Mesh
6
- VERSION = '0.4.6'
6
+ VERSION = '0.4.8'
7
7
  end
8
8
  end
9
9
  end
@@ -12,12 +12,16 @@ unless defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
12
12
  module Runners
13
13
  module Traces
14
14
  def retrieve_by_domain(**)
15
- []
15
+ { count: 0, traces: [] }
16
16
  end
17
17
 
18
18
  def store_trace(**)
19
19
  { stored: true }
20
20
  end
21
+
22
+ def delete_trace(trace_id:, **)
23
+ { deleted: true, trace_id: trace_id }
24
+ end
21
25
  end
22
26
  end
23
27
  end
@@ -100,6 +104,24 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
100
104
  expect(result[:stored]).to be false
101
105
  expect(result[:reason]).to eq(:memory_not_available)
102
106
  end
107
+
108
+ it 'serializes payload as JSON string' do
109
+ captured_payload = nil
110
+ runner_double = double('memory_runner')
111
+ allow(profile_mod).to receive(:memory_available?).and_return(true)
112
+ allow(profile_mod).to receive(:memory_runner).and_return(runner_double)
113
+ allow(runner_double).to receive(:store_trace) do |**args|
114
+ captured_payload = args[:content_payload]
115
+ { stored: true }
116
+ end
117
+
118
+ profile_mod.store_preference(owner_id: 'u1', domain: 'verbosity', value: 'concise', source: 'explicit')
119
+
120
+ parsed = Legion::JSON.load(captured_payload)
121
+ expect(parsed[:domain]).to eq('verbosity')
122
+ expect(parsed[:value]).to eq('concise')
123
+ expect(parsed[:source]).to eq('explicit')
124
+ end
103
125
  end
104
126
 
105
127
  describe '.clear_preferences' do
@@ -107,6 +129,122 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
107
129
  result = profile_mod.clear_preferences(owner_id: 'user1', source: 'explicit')
108
130
  expect(result[:cleared]).to be true
109
131
  end
132
+
133
+ it 'actually deletes traces via memory runner when memory is available' do
134
+ trace = { trace_id: 'abc-123', domain_tags: ['preference', 'owner:user1'], confidence: 0.9 }
135
+ runner_double = double('memory_runner')
136
+ allow(profile_mod).to receive(:memory_available?).and_return(true)
137
+ allow(profile_mod).to receive(:memory_runner).and_return(runner_double)
138
+ allow(runner_double).to receive(:retrieve_by_domain).and_return({ traces: [trace] })
139
+ allow(runner_double).to receive(:delete_trace).and_return({ deleted: true })
140
+
141
+ result = profile_mod.clear_preferences(owner_id: 'user1')
142
+
143
+ expect(runner_double).to have_received(:retrieve_by_domain).with(hash_including(domain_tag: 'owner:user1'))
144
+ expect(runner_double).to have_received(:delete_trace).with(hash_including(trace_id: 'abc-123'))
145
+ expect(result[:cleared]).to be true
146
+ expect(result[:count]).to eq(1)
147
+ end
148
+
149
+ it 'returns not_available when memory is unavailable' do
150
+ allow(profile_mod).to receive(:memory_available?).and_return(false)
151
+ result = profile_mod.clear_preferences(owner_id: 'user1')
152
+ expect(result[:cleared]).to be false
153
+ expect(result[:reason]).to eq(:memory_not_available)
154
+ end
155
+ end
156
+
157
+ describe '.parse_preference_trace' do
158
+ it 'parses JSON-encoded payload and returns symbol-keyed hash' do
159
+ payload = Legion::JSON.dump({ domain: 'verbosity', value: 'concise', source: 'explicit' })
160
+ trace = { content_payload: payload, confidence: 0.9 }
161
+ result = profile_mod.parse_preference_trace(trace)
162
+ expect(result[:domain]).to eq('verbosity')
163
+ expect(result[:value]).to eq('concise')
164
+ expect(result[:source]).to eq('explicit')
165
+ expect(result[:confidence]).to eq(0.9)
166
+ end
167
+
168
+ it 'falls back to legacy regex for old .to_s hash format' do
169
+ legacy_payload = '{:domain=>"verbosity", :value=>"concise", :source=>"explicit"}'
170
+ trace = { content_payload: legacy_payload, confidence: 0.7 }
171
+ result = profile_mod.parse_preference_trace(trace)
172
+ expect(result[:domain]).to eq('verbosity')
173
+ expect(result[:value]).to eq('concise')
174
+ expect(result[:source]).to eq('explicit')
175
+ expect(result[:confidence]).to eq(0.7)
176
+ end
177
+
178
+ it 'returns nil for a non-string payload' do
179
+ trace = { content_payload: nil, confidence: 0.5 }
180
+ expect(profile_mod.parse_preference_trace(trace)).to be_nil
181
+ end
182
+
183
+ it 'returns nil for a payload that cannot be parsed by either method' do
184
+ trace = { content_payload: 'garbage data without structure', confidence: 0.5 }
185
+ expect(profile_mod.parse_preference_trace(trace)).to be_nil
186
+ end
187
+ end
188
+
189
+ describe '.erase_partner!' do
190
+ before do
191
+ described_class.clear_observations
192
+ described_class.clear_mesh_cache
193
+ end
194
+
195
+ it 'removes observation counts for the identity' do
196
+ 5.times { described_class.update_from_observation(owner_id: 'partner-x', signals: { channel: :cli, direct_address: false }) }
197
+ described_class.erase_partner!(identity: 'partner-x')
198
+ expect(described_class.observation_counts['partner-x']).to be_nil
199
+ end
200
+
201
+ it 'removes inferred preferences for the identity' do
202
+ 20.times { described_class.update_from_observation(owner_id: 'partner-x', signals: { channel: :cli, direct_address: false }) }
203
+ described_class.erase_partner!(identity: 'partner-x')
204
+ expect(described_class.inferred_preferences('partner-x')).to be_empty
205
+ end
206
+
207
+ it 'removes mesh cache entries for the identity' do
208
+ described_class.store_mesh_profile(
209
+ agent_id: 'partner-x',
210
+ profile: { verbosity: :concise },
211
+ source_agent_id: 'partner-x'
212
+ )
213
+ described_class.erase_partner!(identity: 'partner-x')
214
+ expect(described_class.cached_mesh_profile(agent_id: 'partner-x')).to be_nil
215
+ end
216
+
217
+ it 'deletes memory traces tagged with the identity' do
218
+ trace = { trace_id: 'xyz-789', domain_tags: ['preference', 'owner:partner-x'], confidence: 0.8 }
219
+ runner_double = double('memory_runner')
220
+ allow(described_class).to receive(:memory_available?).and_return(true)
221
+ allow(described_class).to receive(:memory_runner).and_return(runner_double)
222
+ allow(runner_double).to receive(:retrieve_by_domain).and_return({ traces: [trace] })
223
+ allow(runner_double).to receive(:delete_trace).and_return({ deleted: true })
224
+
225
+ described_class.erase_partner!(identity: 'partner-x')
226
+
227
+ expect(runner_double).to have_received(:delete_trace).with(hash_including(trace_id: 'xyz-789'))
228
+ end
229
+
230
+ it 'leaves data for other identities untouched' do
231
+ 3.times { described_class.update_from_observation(owner_id: 'partner-y', signals: { channel: :cli, direct_address: false }) }
232
+ described_class.store_mesh_profile(
233
+ agent_id: 'partner-y',
234
+ profile: { verbosity: :detailed },
235
+ source_agent_id: 'partner-y'
236
+ )
237
+ described_class.erase_partner!(identity: 'partner-x')
238
+ expect(described_class.observation_counts['partner-y']).to eq(3)
239
+ expect(described_class.cached_mesh_profile(agent_id: 'partner-y')).not_to be_nil
240
+ end
241
+
242
+ it 'returns a result hash with erased identity and counts' do
243
+ result = described_class.erase_partner!(identity: 'partner-x')
244
+ expect(result).to be_a(Hash)
245
+ expect(result[:erased]).to be true
246
+ expect(result[:identity]).to eq('partner-x')
247
+ end
110
248
  end
111
249
 
112
250
  describe '.store_mesh_profile' do
@@ -233,7 +371,7 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
233
371
  expect(tone[:source]).to eq('observation')
234
372
  end
235
373
 
236
- it 'derives :adaptive format for mixed channel usage after threshold' do
374
+ it 'derives :structured format for mixed channel usage after threshold' do
237
375
  channels = %i[cli api chat rest]
238
376
  20.times.with_index do |i, _|
239
377
  ch = channels[i % channels.length]
@@ -245,10 +383,27 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
245
383
  inferred = described_class.inferred_preferences('user-mix')
246
384
  format = inferred.find { |p| p[:domain] == 'format' }
247
385
  expect(format).not_to be_nil
248
- expect(format[:value]).to eq('adaptive')
386
+ expect(format[:value]).to eq('structured')
249
387
  expect(format[:source]).to eq('observation')
250
388
  end
251
389
 
390
+ it 'derive_format never returns a value outside VALID_VALUES[:format]' do
391
+ channels = %i[cli api chat rest grpc websocket]
392
+ 20.times.with_index do |i, _|
393
+ ch = channels[i % channels.length]
394
+ described_class.update_from_observation(
395
+ owner_id: 'user-valid',
396
+ signals: { content_type: :text, channel: ch, direct_address: false }
397
+ )
398
+ end
399
+ inferred = described_class.inferred_preferences('user-valid')
400
+ format_pref = inferred.find { |p| p[:domain] == 'format' }
401
+ if format_pref
402
+ valid_format_values = described_class::VALID_VALUES['format'].map(&:to_s)
403
+ expect(valid_format_values).to include(format_pref[:value])
404
+ end
405
+ end
406
+
252
407
  it 'keeps separate observation counts per owner_id' do
253
408
  3.times { described_class.update_from_observation(owner_id: 'user-a', signals: cli_signal) }
254
409
  7.times { described_class.update_from_observation(owner_id: 'user-b', signals: cli_signal) }
@@ -31,7 +31,7 @@ RSpec.describe Legion::Extensions::Mesh::Runners::Preferences do
31
31
  end)
32
32
  stub_const('Legion::Extensions::Mesh::Transport::Messages::PreferenceQuery',
33
33
  Class.new do
34
- def initialize(**_opts); end
34
+ def initialize(**opts); end
35
35
 
36
36
  def publish; end
37
37
  end)
data/spec/spec_helper.rb CHANGED
@@ -35,8 +35,8 @@ module Legion
35
35
  end
36
36
  end
37
37
 
38
- Legion::Settings.loader.settings[:client] ||= {}
39
- Legion::Settings.loader.settings[:client][:name] = 'test-agent'
38
+ Legion::Settings[:client] ||= {}
39
+ Legion::Settings[:client][:name] = 'test-agent'
40
40
 
41
41
  require 'legion/extensions/mesh'
42
42
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-mesh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.6
4
+ version: 0.4.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity