lex-agentic-social 0.1.9 → 0.1.10

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: 56e3b653aee6a0da01599ef4ed7dfcb409c637f3b730ce7231b532db528e2e8b
4
- data.tar.gz: 7669f59e7277f6418734d2f9ab55057151876896894f0aacc56a93fa923ffa3e
3
+ metadata.gz: 0fc83af9a2de72c88452553af56512be4b8fd0ae78460677ede2acb8741b4717
4
+ data.tar.gz: c73bca2d3963eb2d3b565e5086e471e34a822bc5b51e4ea6e7beb40cf8a065f3
5
5
  SHA512:
6
- metadata.gz: 5832f51b26be00617b30a934b8e62759b8ee0f3837afdb6a42af04bdd505d65280e8ca2877513c0f5ebea9f39f14d3cc0e7d748a772c0bc21a166e88c62a0065
7
- data.tar.gz: e117c35dda203a26a34aa3a879878c42e32c9cd4b9d53e64227a8bc8d02ca884696eba6262e7fab4ead62a066db752147ece0ca8f3423ca8a7bef01ad172be9c
6
+ metadata.gz: b49a7f8ff8a8c950c03fd88291efa84ccbdf73be77c3581572149fcfa99d4084d041584c63c6044da0fd275803ad75a2ef10cc930ac1c0f0561b624fab73190e
7
+ data.tar.gz: d37843a8af2d489580cb9f74923a82390e763939f8a81edc1870d28e42476b383e33d88074305df3c371bcf8ed4b8e01fa6f469ca5753b99cc71fcc1e15f1736
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.10] - 2026-03-31
4
+
5
+ ### Fixed
6
+ - `extract_style_signals`: replace hardcoded `frequency_variance: 0.0` and `reciprocity_imbalance: 0.0` with real computations (`compute_frequency_variance` from hourly bucket counts, `compute_reciprocity_imbalance` from agent/human direction ratio); `:anxious` attachment style is now reachable
7
+ - `reflect_on_bonds`: `narrative:` field now returns a mechanically generated sentence (stage, style, health, chapter, milestones) instead of always nil
8
+ - `reflect_on_bonds`: add `absence_exceeds_pattern:` field backed by `Legion::Extensions::Agentic::Memory::CommunicationPattern` when available, falling back to Apollo Local communication pattern data
9
+ - `retrieve_interaction_traces` in calibration: split into `retrieve_from_memory` + `retrieve_from_apollo_local` fallback; now returns data from Apollo Local when lex-agentic-memory is absent
10
+ - `check_airb_compliance` in shadow_ai: return `reason:` field alongside `source: :unavailable` so callers know why the check could not run; emit debug log entry
11
+
3
12
  ## [0.1.9] - 2026-03-31
4
13
 
5
14
  ### Added
@@ -39,12 +39,13 @@ module Legion
39
39
  bonds_reflected: attachment_store.all_models.size,
40
40
  partner_bond: if model
41
41
  {
42
- stage: model.bond_stage,
43
- strength: model.attachment_strength,
44
- style: model.attachment_style,
45
- health: health,
46
- milestones_today: arc_state[:milestones_today] || [],
47
- narrative: nil
42
+ stage: model.bond_stage,
43
+ strength: model.attachment_strength,
44
+ style: model.attachment_style,
45
+ health: health,
46
+ milestones_today: arc_state[:milestones_today] || [],
47
+ narrative: build_narrative(model, health, arc_state),
48
+ absence_exceeds_pattern: absence_exceeds_pattern?(partner_id)
48
49
  }
49
50
  end
50
51
  }
@@ -98,13 +99,37 @@ module Legion
98
99
  obs = human_observations.select { |o| o[:agent_id].to_s == agent_id }
99
100
  direct_count = obs.count { |o| o[:direct_address] }
100
101
  {
101
- frequency_variance: 0.0,
102
- reciprocity_imbalance: 0.0,
102
+ frequency_variance: compute_frequency_variance(obs),
103
+ reciprocity_imbalance: compute_reciprocity_imbalance(obs),
103
104
  frequency: obs.size.clamp(0, 10) / 10.0,
104
105
  direct_address_ratio: obs.empty? ? 0.0 : direct_count.to_f / obs.size
105
106
  }
106
107
  end
107
108
 
109
+ def compute_frequency_variance(observations)
110
+ return 0.0 if observations.size < 3
111
+
112
+ timestamps = observations.filter_map { |o| o[:observed_at] || o[:timestamp] }
113
+ return 0.0 if timestamps.size < 3
114
+
115
+ buckets = timestamps.group_by { |t| t.to_i / 3600 }
116
+ counts = buckets.values.map { |b| b.size.to_f }
117
+ mean = counts.sum / counts.size
118
+ variance = counts.sum { |c| (c - mean)**2 } / counts.size
119
+ [variance / [mean, 1.0].max, 1.0].min
120
+ end
121
+
122
+ def compute_reciprocity_imbalance(observations)
123
+ return 0.0 if observations.empty?
124
+
125
+ initiated = observations.count { |o| o[:initiated_by] == :agent || o[:direction] == :outgoing }
126
+ received = observations.size - initiated
127
+ total = observations.size.to_f
128
+ return 0.0 if total.zero?
129
+
130
+ ((initiated - received).abs / total).clamp(0.0, 1.0)
131
+ end
132
+
108
133
  def resolve_partner_id
109
134
  if defined?(Legion::Gaia::BondRegistry)
110
135
  bond = Legion::Gaia::BondRegistry.all_bonds.find { |b| b[:role] == :partner }
@@ -154,6 +179,69 @@ module Legion
154
179
  (strength_component + reciprocity_component + consistency_component).clamp(0.0, 1.0)
155
180
  end
156
181
 
182
+ def build_narrative(model, health, arc_state)
183
+ return nil unless model
184
+
185
+ stage = model.bond_stage
186
+ style = model.attachment_style
187
+ chapter = arc_state[:current_chapter]
188
+ parts = ["Bond is #{stage} (#{style} style)"]
189
+ parts << "health: #{format('%.1f', health)}" if health
190
+ parts << "chapter: #{chapter}" if chapter
191
+ milestones = arc_state[:milestones_today]
192
+ parts << "#{milestones.size} milestone(s) today" if milestones&.any?
193
+ "#{parts.join(', ')}."
194
+ end
195
+
196
+ def absence_exceeds_pattern?(agent_id)
197
+ return false unless agent_id
198
+
199
+ if defined?(Legion::Extensions::Agentic::Memory::CommunicationPattern::Runners::CommunicationPattern)
200
+ begin
201
+ runner = Object.new
202
+ runner.extend(Legion::Extensions::Agentic::Memory::CommunicationPattern::Runners::CommunicationPattern)
203
+ stats = runner.partner_stats(agent_id: agent_id)
204
+ return false unless stats.is_a?(Hash)
205
+
206
+ avg_gap = stats[:average_gap_seconds] || stats[:avg_gap]
207
+ return false unless avg_gap&.positive?
208
+
209
+ last_interaction = stats[:last_interaction_at]
210
+ return false unless last_interaction
211
+
212
+ current_gap = Time.now - Time.parse(last_interaction.to_s)
213
+ return current_gap > (avg_gap * 2.0)
214
+ rescue StandardError => _e
215
+ return false
216
+ end
217
+ end
218
+
219
+ absence_exceeds_pattern_from_observations?(agent_id)
220
+ end
221
+
222
+ def absence_exceeds_pattern_from_observations?(agent_id)
223
+ store = apollo_local_store
224
+ return false unless store
225
+
226
+ result = store.query(text: 'communication_pattern',
227
+ tags: ['bond', 'communication_pattern', agent_id.to_s])
228
+ return false unless result[:success] && result[:results]&.any?
229
+
230
+ data = deserialize(result[:results].first[:content])
231
+ return false unless data.is_a?(Hash)
232
+
233
+ avg_gap = (data[:average_gap_seconds] || data[:avg_gap])&.to_f
234
+ return false unless avg_gap&.positive?
235
+
236
+ last_str = data[:last_interaction_at]
237
+ return false unless last_str
238
+
239
+ current_gap = Time.now - Time.parse(last_str.to_s)
240
+ current_gap > (avg_gap * 2.0)
241
+ rescue StandardError => _e
242
+ false
243
+ end
244
+
157
245
  def deserialize(content)
158
246
  parsed = defined?(Legion::JSON) ? Legion::JSON.parse(content) : ::JSON.parse(content, symbolize_names: true)
159
247
  parsed.is_a?(Hash) ? parsed.transform_keys(&:to_sym) : {}
@@ -113,6 +113,13 @@ module Legion
113
113
  end
114
114
 
115
115
  def retrieve_interaction_traces
116
+ traces = retrieve_from_memory
117
+ return traces if traces.any?
118
+
119
+ retrieve_from_apollo_local
120
+ end
121
+
122
+ def retrieve_from_memory
116
123
  return [] unless defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
117
124
 
118
125
  runner = Object.new
@@ -122,7 +129,17 @@ module Legion
122
129
 
123
130
  result[:traces] || []
124
131
  rescue StandardError => e
125
- Legion::Logging.warn("[calibration] retrieve_interaction_traces error: #{e.message}")
132
+ Legion::Logging.warn("[calibration] retrieve_from_memory error: #{e.message}")
133
+ []
134
+ end
135
+
136
+ def retrieve_from_apollo_local
137
+ return [] unless defined?(Legion::Apollo::Local) && Legion::Apollo::Local.started?
138
+
139
+ entries = Legion::Apollo::Local.query_by_tags(tags: ['partner_interaction'], limit: 50)
140
+ entries.map { |e| { content: e[:content], tags: e[:tags], confidence: e[:confidence] } }
141
+ rescue StandardError => e
142
+ Legion::Logging.warn("[calibration] retrieve_from_apollo_local error: #{e.message}")
126
143
  []
127
144
  end
128
145
 
@@ -25,7 +25,10 @@ module Legion
25
25
  end
26
26
 
27
27
  def check_airb_compliance(**)
28
- return { checked: 0, source: :unavailable } unless defined?(Legion::Data::Model::DigitalWorker)
28
+ unless defined?(Legion::Data::Model::DigitalWorker)
29
+ Legion::Logging.debug('[governance:shadow_ai] AIRB compliance check unavailable — DigitalWorker model not loaded')
30
+ return { checked: 0, source: :unavailable, reason: 'DigitalWorker model not loaded' }
31
+ end
29
32
 
30
33
  workers = Legion::Data::Model::DigitalWorker.where(lifecycle_state: 'active').all
31
34
  non_compliant = workers.select do |w|
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Social
7
- VERSION = '0.1.9'
7
+ VERSION = '0.1.10'
8
8
  end
9
9
  end
10
10
  end
@@ -97,8 +97,8 @@ RSpec.describe Legion::Extensions::Agentic::Social::Attachment::Runners::Attachm
97
97
 
98
98
  it 'reads communication patterns from Apollo Local' do
99
99
  host.reflect_on_bonds(tick_results: {}, bond_summary: {})
100
- expect(mock_store).to have_received(:query)
101
- .with(hash_including(tags: array_including('bond', 'communication_pattern')))
100
+ expect(mock_store).to have_received(:query).at_least(:once)
101
+ .with(hash_including(tags: array_including('bond', 'communication_pattern')))
102
102
  end
103
103
 
104
104
  it 'reads relationship arc from Apollo Local' do
@@ -127,4 +127,234 @@ RSpec.describe Legion::Extensions::Agentic::Social::Attachment::Runners::Attachm
127
127
  expect(result).to have_key(:partner_bond)
128
128
  end
129
129
  end
130
+
131
+ describe '#compute_frequency_variance' do
132
+ it 'returns 0.0 for fewer than 3 observations' do
133
+ obs = [{ timestamp: Time.now }, { timestamp: Time.now }]
134
+ expect(host.send(:compute_frequency_variance, obs)).to eq(0.0)
135
+ end
136
+
137
+ it 'returns 0.0 when observations have no timestamps' do
138
+ obs = [{ agent_id: 'x' }, { agent_id: 'x' }, { agent_id: 'x' }]
139
+ expect(host.send(:compute_frequency_variance, obs)).to eq(0.0)
140
+ end
141
+
142
+ it 'returns a positive value for unevenly distributed timestamps' do
143
+ base = Time.now
144
+ # Cluster 6 in one bucket and 1 in another — high variance
145
+ ts = Array.new(6) { base } + [base + 7200]
146
+ obs = ts.map { |t| { timestamp: t } }
147
+ result = host.send(:compute_frequency_variance, obs)
148
+ expect(result).to be > 0.0
149
+ expect(result).to be_between(0.0, 1.0)
150
+ end
151
+
152
+ it 'returns low variance for evenly distributed timestamps' do
153
+ base = Time.now
154
+ # One per hour — perfectly even
155
+ ts = (0..4).map { |i| base + (i * 3600) }
156
+ obs = ts.map { |t| { timestamp: t } }
157
+ result = host.send(:compute_frequency_variance, obs)
158
+ expect(result).to eq(0.0)
159
+ end
160
+
161
+ it 'uses :observed_at field when present' do
162
+ base = Time.now
163
+ ts = Array.new(5) { base } + [base + 7200]
164
+ obs = ts.map { |t| { observed_at: t } }
165
+ result = host.send(:compute_frequency_variance, obs)
166
+ expect(result).to be > 0.0
167
+ end
168
+ end
169
+
170
+ describe '#compute_reciprocity_imbalance' do
171
+ it 'returns 0.0 for empty observations' do
172
+ expect(host.send(:compute_reciprocity_imbalance, [])).to eq(0.0)
173
+ end
174
+
175
+ it 'returns 0.0 for balanced initiated/received' do
176
+ obs = [
177
+ { direction: :outgoing },
178
+ { direction: :incoming }
179
+ ]
180
+ expect(host.send(:compute_reciprocity_imbalance, obs)).to eq(0.0)
181
+ end
182
+
183
+ it 'returns 1.0 when all interactions are agent-initiated' do
184
+ obs = Array.new(4) { { initiated_by: :agent } }
185
+ expect(host.send(:compute_reciprocity_imbalance, obs)).to eq(1.0)
186
+ end
187
+
188
+ it 'returns 1.0 when all interactions are received' do
189
+ obs = Array.new(4) { { direction: :incoming } }
190
+ expect(host.send(:compute_reciprocity_imbalance, obs)).to eq(1.0)
191
+ end
192
+
193
+ it 'computes partial imbalance correctly' do
194
+ # 3 outgoing, 1 incoming => |3-1|/4 = 0.5
195
+ obs = Array.new(3) { { direction: :outgoing } } + [{ direction: :incoming }]
196
+ expect(host.send(:compute_reciprocity_imbalance, obs)).to be_within(0.001).of(0.5)
197
+ end
198
+ end
199
+
200
+ describe '#extract_style_signals — anxious style reachability' do
201
+ it 'produces frequency_variance and reciprocity_imbalance values that can trigger anxious style' do
202
+ base = Time.now
203
+ # 6 in one bucket + 1 isolated — high frequency variance
204
+ timestamps = Array.new(6) { base } + [base + 7200]
205
+ # All agent-initiated — max reciprocity imbalance
206
+ obs = timestamps.map.with_index do |t, i|
207
+ { agent_id: 'partner-1', timestamp: t, initiated_by: :agent, direction: :outgoing,
208
+ direct_address: i.even? }
209
+ end
210
+
211
+ signals = host.send(:extract_style_signals, 'partner-1', obs)
212
+ expect(signals[:frequency_variance]).to be > 0.4
213
+ expect(signals[:reciprocity_imbalance]).to be > 0.3
214
+ end
215
+
216
+ it 'actually drives derive_style! to :anxious with high-variance, high-imbalance signals' do
217
+ model = Legion::Extensions::Agentic::Social::Attachment::Helpers::AttachmentModel.new(agent_id: 'p')
218
+ model.derive_style!(
219
+ frequency_variance: 0.5,
220
+ reciprocity_imbalance: 0.4,
221
+ frequency: 0.6,
222
+ direct_address_ratio: 0.3
223
+ )
224
+ expect(model.attachment_style).to eq(:anxious)
225
+ end
226
+ end
227
+
228
+ describe '#build_narrative' do
229
+ let(:model) do
230
+ m = Legion::Extensions::Agentic::Social::Attachment::Helpers::AttachmentModel.new(agent_id: 'p')
231
+ m.instance_variable_set(:@bond_stage, :established)
232
+ m.instance_variable_set(:@attachment_style, :secure)
233
+ m
234
+ end
235
+
236
+ it 'returns nil when model is nil' do
237
+ expect(host.send(:build_narrative, nil, 0.5, {})).to be_nil
238
+ end
239
+
240
+ it 'returns a non-empty string' do
241
+ result = host.send(:build_narrative, model, 0.75, {})
242
+ expect(result).to be_a(String)
243
+ expect(result).not_to be_empty
244
+ end
245
+
246
+ it 'includes stage and style' do
247
+ result = host.send(:build_narrative, model, 0.75, {})
248
+ expect(result).to include('established')
249
+ expect(result).to include('secure')
250
+ end
251
+
252
+ it 'includes health value' do
253
+ result = host.send(:build_narrative, model, 0.75, {})
254
+ expect(result).to include('0.8') # format('%.1f', 0.75) => '0.8'
255
+ end
256
+
257
+ it 'includes chapter when present in arc_state' do
258
+ result = host.send(:build_narrative, model, 0.5, { current_chapter: 'discovery' })
259
+ expect(result).to include('discovery')
260
+ end
261
+
262
+ it 'includes milestone count when milestones present' do
263
+ arc = { milestones_today: %w[first_conflict repair] }
264
+ result = host.send(:build_narrative, model, 0.5, arc)
265
+ expect(result).to include('2 milestone(s) today')
266
+ end
267
+
268
+ it 'does not include milestone text when milestones empty' do
269
+ result = host.send(:build_narrative, model, 0.5, { milestones_today: [] })
270
+ expect(result).not_to include('milestone')
271
+ end
272
+ end
273
+
274
+ describe '#absence_exceeds_pattern?' do
275
+ it 'returns false when agent_id is nil' do
276
+ expect(host.send(:absence_exceeds_pattern?, nil)).to be false
277
+ end
278
+
279
+ it 'returns false when CommunicationPattern not defined and no apollo store' do
280
+ allow(host).to receive(:apollo_local_store).and_return(nil)
281
+ expect(host.send(:absence_exceeds_pattern?, 'partner-1')).to be false
282
+ end
283
+
284
+ it 'returns false when apollo store returns no results' do
285
+ mock_store = double('apollo_local')
286
+ allow(host).to receive(:apollo_local_store).and_return(mock_store)
287
+ allow(mock_store).to receive(:query).and_return({ success: true, results: [] })
288
+ expect(host.send(:absence_exceeds_pattern?, 'partner-1')).to be false
289
+ end
290
+
291
+ it 'returns false when apollo data has no avg_gap' do
292
+ mock_store = double('apollo_local')
293
+ allow(host).to receive(:apollo_local_store).and_return(mock_store)
294
+ content = Legion::JSON.dump({ last_interaction_at: (Time.now - 3600).to_s })
295
+ allow(mock_store).to receive(:query).and_return({
296
+ success: true,
297
+ results: [{ content: content }]
298
+ })
299
+ expect(host.send(:absence_exceeds_pattern?, 'partner-1')).to be false
300
+ end
301
+
302
+ it 'returns true when current gap exceeds 2x average gap' do
303
+ mock_store = double('apollo_local')
304
+ allow(host).to receive(:apollo_local_store).and_return(mock_store)
305
+ last_seen = Time.now - 7200 # 2 hours ago
306
+ content = Legion::JSON.dump({
307
+ average_gap_seconds: 1800.0, # normal = 30 min
308
+ last_interaction_at: last_seen.to_s
309
+ })
310
+ allow(mock_store).to receive(:query).and_return({
311
+ success: true,
312
+ results: [{ content: content }]
313
+ })
314
+ expect(host.send(:absence_exceeds_pattern?, 'partner-1')).to be true
315
+ end
316
+
317
+ it 'returns false when current gap is within normal range' do
318
+ mock_store = double('apollo_local')
319
+ allow(host).to receive(:apollo_local_store).and_return(mock_store)
320
+ last_seen = Time.now - 600 # 10 minutes ago
321
+ content = Legion::JSON.dump({
322
+ average_gap_seconds: 1800.0, # normal = 30 min
323
+ last_interaction_at: last_seen.to_s
324
+ })
325
+ allow(mock_store).to receive(:query).and_return({
326
+ success: true,
327
+ results: [{ content: content }]
328
+ })
329
+ expect(host.send(:absence_exceeds_pattern?, 'partner-1')).to be false
330
+ end
331
+ end
332
+
333
+ describe '#reflect_on_bonds — narrative and absence fields' do
334
+ let(:mock_store) { double('apollo_local') }
335
+
336
+ before do
337
+ allow(host).to receive(:apollo_local_store).and_return(mock_store)
338
+ allow(mock_store).to receive(:query).and_return({ success: true, results: [] })
339
+
340
+ store = host.send(:attachment_store)
341
+ model = store.get_or_create('partner-1')
342
+ model.update_from_signals(frequency_score: 0.6, reciprocity_score: 0.5,
343
+ prediction_accuracy: 0.7, direct_address_ratio: 0.4,
344
+ channel_consistency: 0.8)
345
+ model.instance_variable_set(:@interaction_count, 55)
346
+ model.instance_variable_set(:@bond_stage, :established)
347
+ end
348
+
349
+ it 'includes a non-nil narrative in partner_bond' do
350
+ result = host.reflect_on_bonds(tick_results: {}, bond_summary: {})
351
+ expect(result[:partner_bond][:narrative]).not_to be_nil
352
+ expect(result[:partner_bond][:narrative]).to be_a(String)
353
+ end
354
+
355
+ it 'includes absence_exceeds_pattern in partner_bond' do
356
+ result = host.reflect_on_bonds(tick_results: {}, bond_summary: {})
357
+ expect(result[:partner_bond]).to have_key(:absence_exceeds_pattern)
358
+ end
359
+ end
130
360
  end
@@ -72,6 +72,73 @@ RSpec.describe Legion::Extensions::Agentic::Social::Calibration::Runners::Calibr
72
72
  end
73
73
  end
74
74
 
75
+ describe '#retrieve_from_memory' do
76
+ it 'returns empty array when lex-agentic-memory is not loaded' do
77
+ result = client.send(:retrieve_from_memory)
78
+ expect(result).to eq([])
79
+ end
80
+ end
81
+
82
+ describe '#retrieve_from_apollo_local' do
83
+ it 'returns empty array when Apollo Local is not available' do
84
+ result = client.send(:retrieve_from_apollo_local)
85
+ expect(result).to eq([])
86
+ end
87
+
88
+ it 'maps Apollo Local entries to trace-compatible hashes when available' do
89
+ mock_local = double('apollo_local')
90
+ stub_const('Legion::Apollo', Module.new)
91
+ stub_const('Legion::Apollo::Local', mock_local)
92
+ allow(mock_local).to receive(:started?).and_return(true)
93
+ allow(mock_local).to receive(:query_by_tags).with(tags: ['partner_interaction'], limit: 50).and_return([
94
+ {
95
+ content: 'hello partner',
96
+ tags: ['partner_interaction'],
97
+ confidence: 0.8
98
+ }
99
+ ])
100
+
101
+ result = client.send(:retrieve_from_apollo_local)
102
+ expect(result).to be_an(Array)
103
+ expect(result.size).to eq(1)
104
+ expect(result.first[:content]).to eq('hello partner')
105
+ expect(result.first[:confidence]).to eq(0.8)
106
+ end
107
+
108
+ it 'returns empty array and logs warning when Apollo Local raises' do
109
+ mock_local = double('apollo_local')
110
+ stub_const('Legion::Apollo', Module.new)
111
+ stub_const('Legion::Apollo::Local', mock_local)
112
+ allow(mock_local).to receive(:started?).and_return(true)
113
+ allow(mock_local).to receive(:query_by_tags).and_raise(StandardError, 'db error')
114
+ allow(Legion::Logging).to receive(:warn)
115
+
116
+ result = client.send(:retrieve_from_apollo_local)
117
+ expect(result).to eq([])
118
+ expect(Legion::Logging).to have_received(:warn).with(a_string_including('retrieve_from_apollo_local'))
119
+ end
120
+ end
121
+
122
+ describe '#retrieve_interaction_traces' do
123
+ it 'falls through to Apollo Local when memory returns no traces' do
124
+ allow(client).to receive(:retrieve_from_memory).and_return([])
125
+ allow(client).to receive(:retrieve_from_apollo_local).and_return([{ content: 'trace', tags: [], confidence: 0.5 }])
126
+
127
+ result = client.send(:retrieve_interaction_traces)
128
+ expect(result).not_to be_empty
129
+ expect(result.first[:content]).to eq('trace')
130
+ end
131
+
132
+ it 'returns memory traces directly without calling Apollo Local when memory has results' do
133
+ memory_trace = { content: 'memory trace', tags: ['partner_interaction'], recorded_at: Time.now }
134
+ allow(client).to receive(:retrieve_from_memory).and_return([memory_trace])
135
+ expect(client).not_to receive(:retrieve_from_apollo_local)
136
+
137
+ result = client.send(:retrieve_interaction_traces)
138
+ expect(result).to eq([memory_trace])
139
+ end
140
+ end
141
+
75
142
  describe '#promote_partner_knowledge' do
76
143
  it 'skips when Apollo Local unavailable' do
77
144
  result = client.promote_partner_knowledge
@@ -43,6 +43,20 @@ RSpec.describe Legion::Extensions::Agentic::Social::Governance::Runners::ShadowA
43
43
  result = host.check_airb_compliance
44
44
  expect(result[:source]).to eq(:unavailable)
45
45
  end
46
+
47
+ it 'includes a reason field when unavailable' do
48
+ result = host.check_airb_compliance
49
+ expect(result).to have_key(:reason)
50
+ expect(result[:reason]).to be_a(String)
51
+ expect(result[:reason]).not_to be_empty
52
+ end
53
+
54
+ it 'logs a debug message when DigitalWorker model is not loaded' do
55
+ allow(Legion::Logging).to receive(:debug)
56
+ host.check_airb_compliance
57
+ expect(Legion::Logging).to have_received(:debug)
58
+ .with(a_string_including('AIRB compliance check unavailable'))
59
+ end
46
60
  end
47
61
 
48
62
  describe '#full_scan' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-agentic-social
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity