lex-agentic-social 0.1.19 → 0.1.20

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: 49f5148e440e4e58060bf6ff47c9dc8d31bfde5092ab17b7ad078fb95344e711
4
- data.tar.gz: 191fddf9d835866196a20ed7efafcbfb4ef34729d7d5b585f7d68a95dc225b00
3
+ metadata.gz: a649d923310d4a98480e6069c03d586585c2a27d3045c625f5c76d608b066016
4
+ data.tar.gz: d5983148aebfa19722b6ae1c5c4e564bd7be94455f3838401bbbe36cdbc087fe
5
5
  SHA512:
6
- metadata.gz: e052b2d246149565565a6f0a7654bb7d7bf689bc149c2bb17b22fdcc59c15339178805127b1a07e8a602a3d9fd1fcebb3e5b3a1bacef01622f6f4ac934be120f
7
- data.tar.gz: 92f66448450959900ac87054fc9a5b4e9e442b3217f52d35e7b8a6e17c377317932c6f92ed129d64c82c177b101d7c95d30ce0a7e52a818f4d74c49edc87a695
6
+ metadata.gz: cc6fc74f2620bc3e2cd96c2ba567f61dd0d1596127e33c75db88af80048879944d8d8719f861c7d2b9368d0d53a87f4ea2978c0c7e0e52b949f91b4168b5d650
7
+ data.tar.gz: e6de6e946ebb7468dd4adf14e35e97e6104cfd13f288b7730cd1f92420764713ea0afb4ed6c3a137f2146765da060de24e0e41a70a6000a9c73148c3d904b3f8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.20] - 2026-07-16
4
+ ### Added
5
+ - `CalibrationRegistry`: per-identity `CalibrationStore` instances keyed by identity string, fixing the grading-contamination bug where one interlocutor's reactions moved another's calibration weights.
6
+ - `CalibrationRegistry#erase_partner!(identity:)`: removes all calibration data for a given identity, leaving others untouched.
7
+ - `CalibrationRegistry#known_identities`: returns all registered identity keys.
8
+ - `CalibrationRegistry#to_apollo_entries_for(identity:)`: returns Apollo persistence entries scoped to one identity.
9
+ - `TrustMap` gains `partner_identity:` keyword on `get`, `get_or_create`, and `record_interaction` — entries are keyed as `(partner_identity, agent_id, domain)` for H7 scoping.
10
+ - `TrustMap#erase_partner!(identity:)`: removes all trust entries scoped to the given partner identity and marks the map dirty.
11
+
3
12
  ## [0.1.19] - 2026-06-01
4
13
  ### Fixed
5
14
  - Calibration LLM preference extraction now uses `Legion::LLM.chat` with explicit system/user messages instead of legacy `Legion::LLM.ask`.
@@ -25,7 +25,7 @@ module Legion
25
25
  { agents_updated: agents.size, models: agents.map { |id| attachment_store.get(id)&.to_h } }
26
26
  end
27
27
 
28
- def reflect_on_bonds(_tick_results: {}, _bond_summary: {}, **)
28
+ def reflect_on_bonds(**)
29
29
  store = apollo_local_store
30
30
  return { success: false, error: :no_store } unless store
31
31
 
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Social
7
+ module Calibration
8
+ module Helpers
9
+ class CalibrationRegistry
10
+ def initialize
11
+ @stores = {}
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ def for(identity:)
16
+ @mutex.synchronize { @stores[identity.to_s] ||= CalibrationStore.new }
17
+ end
18
+
19
+ def erase_partner!(identity:)
20
+ @mutex.synchronize { @stores.delete(identity.to_s) }
21
+ true
22
+ end
23
+
24
+ def known_identities
25
+ @mutex.synchronize { @stores.keys.dup }
26
+ end
27
+
28
+ def to_apollo_entries_for(identity:)
29
+ store = @mutex.synchronize { @stores[identity.to_s] }
30
+ return [] unless store
31
+
32
+ store.to_apollo_entries
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'calibration/helpers/constants'
4
4
  require_relative 'calibration/helpers/calibration_store'
5
+ require_relative 'calibration/helpers/calibration_registry'
5
6
  require_relative 'calibration/runners/calibration'
6
7
  require_relative 'calibration/client'
7
8
 
@@ -10,7 +10,7 @@ module Legion
10
10
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
11
11
  Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
12
 
13
- def check_consent(domain:, _action_type: :general, **)
13
+ def check_consent(domain:, **)
14
14
  tier = consent_map.get_tier(domain)
15
15
  log.debug "[consent] check: domain=#{domain} tier=#{tier} allowed=#{tier == :autonomous}"
16
16
 
@@ -61,7 +61,7 @@ module Legion
61
61
  validate_action(layer: :agent_validation, action: action, _context: context)
62
62
  end
63
63
 
64
- def validate_action(layer:, action: nil, _context: {}, **)
64
+ def validate_action(layer:, action: nil, **)
65
65
  return { error: :invalid_layer } unless Helpers::Layers.valid_layer?(layer)
66
66
 
67
67
  log.info "[governance] validating action=#{action} layer=#{layer}"
@@ -10,7 +10,7 @@ module Legion
10
10
  attr_reader :entries
11
11
 
12
12
  def initialize
13
- @entries = {} # key: "agent_id:domain"
13
+ @entries = {} # key: "partner_identity|agent_id:domain"
14
14
  @dirty = false
15
15
  end
16
16
 
@@ -23,16 +23,16 @@ module Legion
23
23
  self
24
24
  end
25
25
 
26
- def get(agent_id, domain: :general)
27
- @entries[key(agent_id, domain)]
26
+ def get(agent_id, domain: :general, partner_identity: nil)
27
+ @entries[key(agent_id, domain, partner_identity)]
28
28
  end
29
29
 
30
- def get_or_create(agent_id, domain: :general)
31
- @entries[key(agent_id, domain)] ||= TrustModel.new_trust_entry(agent_id: agent_id, domain: domain)
30
+ def get_or_create(agent_id, domain: :general, partner_identity: nil)
31
+ @entries[key(agent_id, domain, partner_identity)] ||= TrustModel.new_trust_entry(agent_id: agent_id, domain: domain)
32
32
  end
33
33
 
34
- def record_interaction(agent_id, positive:, domain: :general)
35
- entry = get_or_create(agent_id, domain: domain)
34
+ def record_interaction(agent_id, positive:, domain: :general, partner_identity: nil)
35
+ entry = get_or_create(agent_id, domain: domain, partner_identity: partner_identity)
36
36
  entry[:interaction_count] += 1
37
37
  entry[:last_interaction] = Time.now.utc
38
38
 
@@ -53,6 +53,14 @@ module Legion
53
53
  entry
54
54
  end
55
55
 
56
+ def erase_partner!(identity:)
57
+ prefix = "#{identity}|"
58
+ removed = @entries.keys.select { |k| k.start_with?(prefix) }
59
+ removed.each { |k| @entries.delete(k) }
60
+ @dirty = true if removed.any?
61
+ removed.size
62
+ end
63
+
56
64
  def reinforce_dimension(agent_id, dimension:, domain: :general, amount: TrustModel::TRUST_REINFORCEMENT)
57
65
  entry = get_or_create(agent_id, domain: domain)
58
66
  return unless TrustModel::TRUST_DIMENSIONS.include?(dimension)
@@ -121,8 +129,9 @@ module Legion
121
129
 
122
130
  private
123
131
 
124
- def key(agent_id, domain)
125
- "#{agent_id}:#{domain}"
132
+ def key(agent_id, domain, partner_identity = nil)
133
+ base = "#{agent_id}:#{domain}"
134
+ partner_identity ? "#{partner_identity}|#{base}" : base
126
135
  end
127
136
 
128
137
  def build_apollo_tags(agent_id, domain)
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Social
7
- VERSION = '0.1.19'
7
+ VERSION = '0.1.20'
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Legion::Extensions::Agentic::Social::Calibration::Helpers::CalibrationRegistry do
6
+ subject(:registry) { described_class.new }
7
+
8
+ let(:identity_x) { 'user:alice' }
9
+ let(:identity_y) { 'user:bob' }
10
+
11
+ describe '#for(identity:)' do
12
+ it 'returns a CalibrationStore instance' do
13
+ expect(registry.for(identity: identity_x)).to be_a(
14
+ Legion::Extensions::Agentic::Social::Calibration::Helpers::CalibrationStore
15
+ )
16
+ end
17
+
18
+ it 'returns the same instance on repeated calls for the same identity' do
19
+ first = registry.for(identity: identity_x)
20
+ second = registry.for(identity: identity_x)
21
+ expect(first).to equal(second)
22
+ end
23
+
24
+ it 'returns independent instances per identity' do
25
+ x = registry.for(identity: identity_x)
26
+ y = registry.for(identity: identity_y)
27
+ expect(x).not_to equal(y)
28
+ end
29
+
30
+ it 'X reads never see Y data — weights are independent' do
31
+ store_x = registry.for(identity: identity_x)
32
+ store_x.record_advisory(advisory_id: 'ax', advisory_types: [:tone_adjustment])
33
+ store_x.evaluate_reaction(observation: { content: 'thanks, perfect!', direct_address: true, content_length: 20 })
34
+
35
+ store_y = registry.for(identity: identity_y)
36
+ expect(store_y.weights['tone_adjustment']).to eq(0.5)
37
+ end
38
+
39
+ it 'Y negative reaction never moves X calibration weights (contamination guard)' do
40
+ store_x = registry.for(identity: identity_x)
41
+ store_x.record_advisory(advisory_id: 'ax', advisory_types: [:tone_adjustment])
42
+
43
+ store_y = registry.for(identity: identity_y)
44
+ store_y.record_advisory(advisory_id: 'ay', advisory_types: [:tone_adjustment])
45
+ store_y.evaluate_reaction(observation: { content: 'no, wrong', content_length: 5 })
46
+
47
+ # X advisory meta should still be present (Y's reaction didn't consume X's advisory)
48
+ result = store_x.evaluate_reaction(observation: { content: 'hello' })
49
+ expect(result).not_to be_nil
50
+ end
51
+ end
52
+
53
+ describe '#erase_partner!(identity:)' do
54
+ before do
55
+ store = registry.for(identity: identity_x)
56
+ store.record_advisory(advisory_id: 'ax', advisory_types: [:tone_adjustment])
57
+ store.evaluate_reaction(observation: { content: 'thanks' })
58
+
59
+ registry.for(identity: identity_y)
60
+ end
61
+
62
+ it 'removes the store for the given identity' do
63
+ registry.erase_partner!(identity: identity_x)
64
+ # A new store is created — weights reset to neutral
65
+ new_store = registry.for(identity: identity_x)
66
+ expect(new_store.weights['tone_adjustment']).to eq(0.5)
67
+ expect(new_store.history).to be_empty
68
+ end
69
+
70
+ it 'leaves other identities untouched' do
71
+ store_y = registry.for(identity: identity_y)
72
+ original_y = store_y.object_id
73
+ registry.erase_partner!(identity: identity_x)
74
+ expect(registry.for(identity: identity_y).object_id).to eq(original_y)
75
+ end
76
+
77
+ it 'returns true' do
78
+ expect(registry.erase_partner!(identity: identity_x)).to be true
79
+ end
80
+
81
+ it 'returns true even when identity does not exist' do
82
+ expect(registry.erase_partner!(identity: 'unknown')).to be true
83
+ end
84
+ end
85
+
86
+ describe '#known_identities' do
87
+ it 'returns empty array when no identities registered' do
88
+ expect(registry.known_identities).to be_empty
89
+ end
90
+
91
+ it 'returns registered identities' do
92
+ registry.for(identity: identity_x)
93
+ registry.for(identity: identity_y)
94
+ expect(registry.known_identities).to contain_exactly(identity_x, identity_y)
95
+ end
96
+
97
+ it 'does not include erased identities' do
98
+ registry.for(identity: identity_x)
99
+ registry.erase_partner!(identity: identity_x)
100
+ expect(registry.known_identities).not_to include(identity_x)
101
+ end
102
+ end
103
+
104
+ describe 'boot-order / idempotent registration' do
105
+ it 're-registering an already-registered identity is a no-op (no duplicate store)' do
106
+ first = registry.for(identity: identity_x)
107
+ second = registry.for(identity: identity_x)
108
+ expect(first).to equal(second)
109
+ end
110
+ end
111
+
112
+ describe 'fail-closed guard' do
113
+ it 'to_apollo_entries_for returns empty array when identity not registered' do
114
+ expect(registry.to_apollo_entries_for(identity: 'unregistered')).to eq([])
115
+ end
116
+
117
+ it 'to_apollo_entries_for returns entries for a registered identity' do
118
+ registry.for(identity: identity_x)
119
+ entries = registry.to_apollo_entries_for(identity: identity_x)
120
+ expect(entries).to be_an(Array)
121
+ expect(entries).not_to be_empty
122
+ end
123
+ end
124
+ end
@@ -296,4 +296,70 @@ RSpec.describe Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap do
296
296
  expect(map.count).to eq(3)
297
297
  end
298
298
  end
299
+
300
+ describe 'partner_identity keying (H0)' do
301
+ let(:partner_x) { 'user:alice' }
302
+ let(:partner_y) { 'user:bob' }
303
+
304
+ it 'entries scoped to partner_x are not visible under partner_y' do
305
+ map.record_interaction(agent_id, positive: true, partner_identity: partner_x)
306
+ expect(map.get(agent_id, partner_identity: partner_y)).to be_nil
307
+ end
308
+
309
+ it 'independent entries exist per partner_identity' do
310
+ map.record_interaction(agent_id, positive: true, partner_identity: partner_x)
311
+ map.record_interaction(agent_id, positive: false, partner_identity: partner_y)
312
+ expect(map.get(agent_id, partner_identity: partner_x)[:composite]).to be >
313
+ map.get(agent_id, partner_identity: partner_y)[:composite]
314
+ end
315
+
316
+ it 'entries keyed without partner_identity are independent from partner-scoped entries' do
317
+ map.record_interaction(agent_id, positive: true)
318
+ map.record_interaction(agent_id, positive: false, partner_identity: partner_x)
319
+ expect(map.get(agent_id)[:composite]).not_to eq(map.get(agent_id, partner_identity: partner_x)[:composite])
320
+ end
321
+
322
+ it 'count includes all partner-scoped entries' do
323
+ map.get_or_create(agent_id, partner_identity: partner_x)
324
+ map.get_or_create(agent_id, partner_identity: partner_y)
325
+ expect(map.count).to eq(2)
326
+ end
327
+ end
328
+
329
+ describe '#erase_partner!(identity:)' do
330
+ let(:partner_x) { 'user:alice' }
331
+ let(:partner_y) { 'user:bob' }
332
+
333
+ before do
334
+ map.record_interaction(agent_id, positive: true, partner_identity: partner_x)
335
+ map.record_interaction(agent_id, positive: true, partner_identity: partner_y)
336
+ map.record_interaction('other-agent', positive: true, partner_identity: partner_x)
337
+ end
338
+
339
+ it 'removes all entries for the given partner_identity' do
340
+ map.erase_partner!(identity: partner_x)
341
+ expect(map.get(agent_id, partner_identity: partner_x)).to be_nil
342
+ expect(map.get('other-agent', partner_identity: partner_x)).to be_nil
343
+ end
344
+
345
+ it 'leaves entries for other partner identities untouched' do
346
+ map.erase_partner!(identity: partner_x)
347
+ expect(map.get(agent_id, partner_identity: partner_y)).not_to be_nil
348
+ end
349
+
350
+ it 'marks the map as dirty' do
351
+ map.mark_clean!
352
+ map.erase_partner!(identity: partner_x)
353
+ expect(map).to be_dirty
354
+ end
355
+
356
+ it 'returns the count of removed entries' do
357
+ count = map.erase_partner!(identity: partner_x)
358
+ expect(count).to eq(2)
359
+ end
360
+
361
+ it 'returns 0 when no entries exist for that identity' do
362
+ expect(map.erase_partner!(identity: 'unknown')).to eq(0)
363
+ end
364
+ end
299
365
  end
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.19
4
+ version: 0.1.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -192,6 +192,7 @@ files:
192
192
  - lib/legion/extensions/agentic/social/attachment/version.rb
193
193
  - lib/legion/extensions/agentic/social/calibration.rb
194
194
  - lib/legion/extensions/agentic/social/calibration/client.rb
195
+ - lib/legion/extensions/agentic/social/calibration/helpers/calibration_registry.rb
195
196
  - lib/legion/extensions/agentic/social/calibration/helpers/calibration_store.rb
196
197
  - lib/legion/extensions/agentic/social/calibration/helpers/constants.rb
197
198
  - lib/legion/extensions/agentic/social/calibration/runners/calibration.rb
@@ -335,6 +336,7 @@ files:
335
336
  - spec/legion/extensions/agentic/social/attachment/helpers/attachment_store_spec.rb
336
337
  - spec/legion/extensions/agentic/social/attachment/helpers/constants_spec.rb
337
338
  - spec/legion/extensions/agentic/social/attachment/runners/attachment_spec.rb
339
+ - spec/legion/extensions/agentic/social/calibration/helpers/calibration_registry_spec.rb
338
340
  - spec/legion/extensions/agentic/social/calibration/helpers/calibration_store_spec.rb
339
341
  - spec/legion/extensions/agentic/social/calibration/helpers/constants_spec.rb
340
342
  - spec/legion/extensions/agentic/social/calibration/runners/calibration_spec.rb