lex-mesh 0.3.4 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4153ff1a3ae99b32c170e6d190817a51e27318ea532505186ec16dcc1ae45ae6
4
- data.tar.gz: 467dcb6344f58e2019d35e21d6ede60ea335d63f3aacf82bc4aed9dd05a14134
3
+ metadata.gz: fbc0df66608354b5104c675cc838ceede54f6efde1e7877a38d1dda1f53a96bd
4
+ data.tar.gz: efc6aaffe3bb7bbc45df398c0b303dcc8945a513d1c949c545e0707ebfdfd368
5
5
  SHA512:
6
- metadata.gz: cb648562211012a2569cf6a345397a7283d6fff71de672fda94452e631dfd2d6943f868a1ac39a62c2c9273d46dfc727ac85a3595a27e1eaae071bd4fff9b771
7
- data.tar.gz: 98b4fce6a1c579507f4201243b739d46a10e7a649fb7939214c27c29af21abaf1f06d12b24ee63e05d3f0143106b9377a7ce51c864916adea85ec80dbd04956a
6
+ metadata.gz: a1da77aeb0c9c9f9eea9a9b506457e55c19a5cb68605a8ed2069c37bf35ecc6337d7ff28b781dd50afa96805d08b9288d261d1072fa7dd4078c8aca2d0069fc3
7
+ data.tar.gz: 1700abac8d24b5b1362d4112128fe02fbcc991bb5e4779ecf427196fa8f0fb919419b20d85778d07d7d11621023c1c8e7523b125958ca3fea5ba5c614dabbbbe
@@ -121,6 +121,84 @@ module Legion
121
121
  lines.join(' ')
122
122
  end
123
123
 
124
+ MESH_CACHE_TTL = 3600 # 1 hour default
125
+
126
+ def store_mesh_profile(agent_id:, profile:, source_agent_id:)
127
+ @mesh_cache ||= {}
128
+ @mesh_cache[agent_id.to_s] = {
129
+ profile: profile,
130
+ source_agent_id: source_agent_id,
131
+ origin: :mesh_transfer,
132
+ cached_at: Time.now
133
+ }
134
+
135
+ if memory_available?
136
+ store_preference(
137
+ owner_id: agent_id,
138
+ domain: 'mesh_profile',
139
+ value: profile.to_s,
140
+ source: 'mesh_transfer'
141
+ )
142
+ end
143
+
144
+ { stored: true, agent_id: agent_id, origin: :mesh_transfer }
145
+ rescue StandardError => e
146
+ { stored: false, error: e.message }
147
+ end
148
+
149
+ def cached_mesh_profile(agent_id:, ttl: MESH_CACHE_TTL)
150
+ @mesh_cache ||= {}
151
+ entry = @mesh_cache[agent_id.to_s]
152
+ return nil unless entry
153
+
154
+ if Time.now - entry[:cached_at] > ttl
155
+ @mesh_cache.delete(agent_id.to_s)
156
+ return nil
157
+ end
158
+
159
+ entry[:profile]
160
+ rescue StandardError
161
+ nil
162
+ end
163
+
164
+ def clear_mesh_cache(agent_id: nil)
165
+ @mesh_cache ||= {}
166
+ if agent_id
167
+ @mesh_cache.delete(agent_id.to_s)
168
+ else
169
+ @mesh_cache.clear
170
+ end
171
+ end
172
+
173
+ def for_agent(agent_id:, ttl: MESH_CACHE_TTL)
174
+ cached = cached_mesh_profile(agent_id: agent_id, ttl: ttl)
175
+ if cached
176
+ return {
177
+ source: :mesh_cache,
178
+ profile: cached,
179
+ agent_id: agent_id,
180
+ compatibility: compute_compatibility(cached)
181
+ }
182
+ end
183
+
184
+ profile = resolve(owner_id: agent_id)
185
+
186
+ {
187
+ source: :local,
188
+ profile: profile,
189
+ agent_id: agent_id,
190
+ compatibility: compute_compatibility(profile)
191
+ }
192
+ rescue StandardError => e
193
+ {
194
+ source: :local,
195
+ profile: DEFAULTS.dup,
196
+ agent_id: agent_id,
197
+ compatibility: nil,
198
+ error: e.message
199
+ }
200
+ end
201
+
124
202
  def memory_available?
125
203
  defined?(Legion::Extensions::Memory::Runners::Traces)
126
204
  end
@@ -151,6 +229,23 @@ module Legion
151
229
  rescue StandardError
152
230
  nil
153
231
  end
232
+
233
+ def compute_compatibility(profile)
234
+ return nil unless profile.is_a?(Hash) && profile[:personality]
235
+ return nil unless personality_available?
236
+
237
+ personality_runner = Object.new.extend(
238
+ Legion::Extensions::Agentic::Self::Personality::Runners::Personality
239
+ )
240
+ result = personality_runner.personality_compatibility(other_profile: profile[:personality])
241
+ { score: result[:compatibility], interpretation: result[:interpretation] }
242
+ rescue StandardError
243
+ nil
244
+ end
245
+
246
+ def personality_available?
247
+ defined?(Legion::Extensions::Agentic::Self::Personality::Runners::Personality)
248
+ end
154
249
  end
155
250
  end
156
251
  end
@@ -30,7 +30,12 @@ module Legion
30
30
  { success: true, source: :local_default, profile: default_profile, error: e.message }
31
31
  end
32
32
 
33
- def handle_preference_query(**)
33
+ def handle_preference_query(requesting_agent_id: nil, **)
34
+ if trust_available? && requesting_agent_id
35
+ trust_result = check_requester_trust(requesting_agent_id)
36
+ return { success: false, reason: :insufficient_trust, responding_agent_id: local_agent_id } if trust_result == :denied
37
+ end
38
+
34
39
  owner_id = local_agent_id
35
40
  profile = Helpers::PreferenceProfile.resolve(owner_id: owner_id)
36
41
 
@@ -39,7 +44,15 @@ module Legion
39
44
  { success: false, error: e.message }
40
45
  end
41
46
 
42
- def handle_preference_response(correlation_id:, profile:, **)
47
+ def handle_preference_response(correlation_id:, profile:, responding_agent_id: nil, **)
48
+ if responding_agent_id && profile.is_a?(Hash)
49
+ Helpers::PreferenceProfile.store_mesh_profile(
50
+ agent_id: responding_agent_id,
51
+ profile: profile,
52
+ source_agent_id: responding_agent_id
53
+ )
54
+ end
55
+
43
56
  resolved = pending_requests.resolve(correlation_id: correlation_id, result: profile)
44
57
  { resolved: resolved }
45
58
  end
@@ -57,8 +70,9 @@ module Legion
57
70
  profile
58
71
  when 'preference_response'
59
72
  handle_preference_response(
60
- correlation_id: msg[:correlation_id],
61
- profile: msg[:profile] || {}
73
+ correlation_id: msg[:correlation_id],
74
+ profile: msg[:profile] || {},
75
+ responding_agent_id: msg[:responding_agent_id]
62
76
  )
63
77
  else
64
78
  { success: false, error: "unknown preference message type: #{type}" }
@@ -110,11 +124,28 @@ module Legion
110
124
  end
111
125
 
112
126
  def default_preference_callback(target_agent_id:)
113
- lambda do |profile|
114
- log_debug("[mesh] received preferences for #{target_agent_id}: #{profile.keys.join(', ')}")
127
+ lambda do |_profile|
128
+ log_debug("[mesh] received and cached preferences for #{target_agent_id}")
115
129
  end
116
130
  end
117
131
 
132
+ def trust_available?
133
+ defined?(Legion::Extensions::Agentic::Social::Trust::Runners::Trust)
134
+ end
135
+
136
+ def check_requester_trust(agent_id)
137
+ trust_mod = Legion::Extensions::Agentic::Social::Trust::Runners::Trust
138
+ evaluator = Object.new.extend(trust_mod)
139
+ result = evaluator.get_trust(agent_id: agent_id, domain: :general)
140
+
141
+ return :allowed unless result[:found]
142
+
143
+ composite = result.dig(:trust, :composite) || 0.0
144
+ composite >= Helpers::Topology::TRUST_CONSIDER_THRESHOLD ? :allowed : :denied
145
+ rescue StandardError
146
+ :allowed
147
+ end
148
+
118
149
  def log_debug(msg)
119
150
  log.debug(msg)
120
151
  end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Mesh
6
- VERSION = '0.3.4'
6
+ VERSION = '0.4.0'
7
7
  end
8
8
  end
9
9
  end
@@ -105,6 +105,88 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
105
105
  end
106
106
  end
107
107
 
108
+ describe '.store_mesh_profile' do
109
+ it 'stores a profile with mesh_transfer origin' do
110
+ result = described_class.store_mesh_profile(
111
+ agent_id: 'agent-42',
112
+ profile: { verbosity: :concise, tone: :casual },
113
+ source_agent_id: 'agent-42'
114
+ )
115
+ expect(result[:stored]).to be true
116
+ expect(result[:origin]).to eq(:mesh_transfer)
117
+ end
118
+ end
119
+
120
+ describe '.cached_mesh_profile' do
121
+ it 'returns nil when no cached profile exists' do
122
+ result = described_class.cached_mesh_profile(agent_id: 'agent-unknown')
123
+ expect(result).to be_nil
124
+ end
125
+
126
+ it 'returns cached profile after store' do
127
+ described_class.store_mesh_profile(
128
+ agent_id: 'agent-42',
129
+ profile: { verbosity: :concise, tone: :casual },
130
+ source_agent_id: 'agent-42'
131
+ )
132
+ result = described_class.cached_mesh_profile(agent_id: 'agent-42')
133
+ expect(result).to be_a(Hash)
134
+ expect(result[:verbosity]).to eq(:concise)
135
+ end
136
+
137
+ it 'returns nil for expired cache entries' do
138
+ described_class.store_mesh_profile(
139
+ agent_id: 'agent-42',
140
+ profile: { verbosity: :concise },
141
+ source_agent_id: 'agent-42'
142
+ )
143
+ # Expire the entry by manipulating the cache timestamp
144
+ cache = described_class.instance_variable_get(:@mesh_cache)
145
+ cache['agent-42'][:cached_at] = Time.now - 7200 if cache&.dig('agent-42')
146
+ result = described_class.cached_mesh_profile(agent_id: 'agent-42', ttl: 3600)
147
+ expect(result).to be_nil
148
+ end
149
+ end
150
+
151
+ describe '.for_agent' do
152
+ before { described_class.clear_mesh_cache }
153
+
154
+ it 'returns local profile when no mesh cache exists' do
155
+ result = described_class.for_agent(agent_id: 'agent-99')
156
+ expect(result[:source]).to eq(:local)
157
+ expect(result[:profile]).to be_a(Hash)
158
+ expect(result[:profile][:verbosity]).to eq(:normal)
159
+ end
160
+
161
+ it 'returns cached mesh profile when available' do
162
+ described_class.store_mesh_profile(
163
+ agent_id: 'agent-42',
164
+ profile: { verbosity: :concise, tone: :formal },
165
+ source_agent_id: 'agent-42'
166
+ )
167
+ result = described_class.for_agent(agent_id: 'agent-42')
168
+ expect(result[:source]).to eq(:mesh_cache)
169
+ expect(result[:profile][:verbosity]).to eq(:concise)
170
+ end
171
+
172
+ it 'falls back to local when mesh cache is expired' do
173
+ described_class.store_mesh_profile(
174
+ agent_id: 'agent-42',
175
+ profile: { verbosity: :concise },
176
+ source_agent_id: 'agent-42'
177
+ )
178
+ cache = described_class.instance_variable_get(:@mesh_cache)
179
+ cache['agent-42'][:cached_at] = Time.now - 7200
180
+ result = described_class.for_agent(agent_id: 'agent-42')
181
+ expect(result[:source]).to eq(:local)
182
+ end
183
+
184
+ it 'includes compatibility when available' do
185
+ result = described_class.for_agent(agent_id: 'agent-99')
186
+ expect(result).to have_key(:compatibility)
187
+ end
188
+ end
189
+
108
190
  describe '.preference_instructions' do
109
191
  it 'generates natural language prompt instructions from profile' do
110
192
  profile = {
@@ -65,6 +65,41 @@ RSpec.describe Legion::Extensions::Mesh::Runners::Preferences do
65
65
  result = runner.handle_preference_query(requesting_agent_id: 'agent-1')
66
66
  expect(result[:profile]).to have_key(:verbosity)
67
67
  end
68
+
69
+ context 'with trust module available' do
70
+ let(:trust_runner) do
71
+ Module.new do
72
+ def get_trust(agent_id:, domain: :general, **)
73
+ case agent_id
74
+ when 'trusted-agent' then { found: true, trust: { composite: 0.8 } }
75
+ when 'untrusted-agent' then { found: true, trust: { composite: 0.1 } }
76
+ else { found: false, agent_id: agent_id, domain: domain }
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ before do
83
+ stub_const('Legion::Extensions::Agentic::Social::Trust::Runners::Trust', trust_runner)
84
+ end
85
+
86
+ it 'returns profile for trusted agents' do
87
+ result = runner.handle_preference_query(requesting_agent_id: 'trusted-agent')
88
+ expect(result[:success]).to be true
89
+ expect(result[:profile]).to be_a(Hash)
90
+ end
91
+
92
+ it 'refuses preferences for untrusted agents' do
93
+ result = runner.handle_preference_query(requesting_agent_id: 'untrusted-agent')
94
+ expect(result[:success]).to be false
95
+ expect(result[:reason]).to eq(:insufficient_trust)
96
+ end
97
+
98
+ it 'returns profile for unknown agents (trust not found defaults to open)' do
99
+ result = runner.handle_preference_query(requesting_agent_id: 'unknown-agent')
100
+ expect(result[:success]).to be true
101
+ end
102
+ end
68
103
  end
69
104
 
70
105
  describe '#handle_preference_response' do
@@ -125,6 +160,22 @@ RSpec.describe Legion::Extensions::Mesh::Runners::Preferences do
125
160
  )
126
161
  expect(result[:resolved]).to be false
127
162
  end
163
+
164
+ it 'passes responding_agent_id for mesh caching' do
165
+ pending_req = runner.send(:pending_requests)
166
+ pending_req.register(correlation_id: 'corr-dispatch', callback: ->(_p) {})
167
+ runner.dispatch_preference_message(
168
+ type: 'preference_response',
169
+ correlation_id: 'corr-dispatch',
170
+ profile: { verbosity: :terse },
171
+ responding_agent_id: 'agent-77'
172
+ )
173
+ cached = Legion::Extensions::Mesh::Helpers::PreferenceProfile.cached_mesh_profile(
174
+ agent_id: 'agent-77'
175
+ )
176
+ expect(cached).to be_a(Hash)
177
+ expect(cached[:verbosity]).to eq(:terse)
178
+ end
128
179
  end
129
180
 
130
181
  context 'with unknown type' do
@@ -143,6 +194,73 @@ RSpec.describe Legion::Extensions::Mesh::Runners::Preferences do
143
194
  end
144
195
  end
145
196
 
197
+ describe '#handle_preference_response with mesh caching' do
198
+ it 'caches the received profile' do
199
+ pending_req = runner.send(:pending_requests)
200
+ pending_req.register(
201
+ correlation_id: 'corr-cache',
202
+ callback: ->(profile) {}
203
+ )
204
+ runner.handle_preference_response(
205
+ correlation_id: 'corr-cache',
206
+ profile: { verbosity: :concise, tone: :casual },
207
+ responding_agent_id: 'agent-42'
208
+ )
209
+ cached = Legion::Extensions::Mesh::Helpers::PreferenceProfile.cached_mesh_profile(
210
+ agent_id: 'agent-42'
211
+ )
212
+ expect(cached).to be_a(Hash)
213
+ expect(cached[:verbosity]).to eq(:concise)
214
+ end
215
+ end
216
+
217
+ describe 'personality compatibility' do
218
+ context 'without personality module' do
219
+ it 'returns nil compatibility in for_agent' do
220
+ result = Legion::Extensions::Mesh::Helpers::PreferenceProfile.for_agent(agent_id: 'agent-99')
221
+ expect(result[:compatibility]).to be_nil
222
+ end
223
+ end
224
+
225
+ context 'with personality module available' do
226
+ let(:personality_runner) do
227
+ Module.new do
228
+ def personality_compatibility(**)
229
+ { compatibility: 0.82, interpretation: :compatible }
230
+ end
231
+
232
+ private
233
+
234
+ def personality_store
235
+ @personality_store ||= Struct.new(:model).new(nil)
236
+ end
237
+ end
238
+ end
239
+
240
+ before do
241
+ stub_const(
242
+ 'Legion::Extensions::Agentic::Self::Personality::Runners::Personality',
243
+ personality_runner
244
+ )
245
+ end
246
+
247
+ it 'returns compatibility score when personality data exists' do
248
+ Legion::Extensions::Mesh::Helpers::PreferenceProfile.store_mesh_profile(
249
+ agent_id: 'agent-42',
250
+ profile: {
251
+ verbosity: :concise,
252
+ personality: { openness: 0.8, conscientiousness: 0.6 }
253
+ },
254
+ source_agent_id: 'agent-42'
255
+ )
256
+ result = Legion::Extensions::Mesh::Helpers::PreferenceProfile.for_agent(agent_id: 'agent-42')
257
+ expect(result[:compatibility]).to be_a(Hash)
258
+ expect(result[:compatibility][:score]).to eq(0.82)
259
+ expect(result[:compatibility][:interpretation]).to eq(:compatible)
260
+ end
261
+ end
262
+ end
263
+
146
264
  describe '#expire_pending_requests' do
147
265
  it 'cleans up expired entries and returns count' do
148
266
  pending_req = runner.send(:pending_requests)
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.3.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity