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 +4 -4
- data/CHANGELOG.md +9 -0
- data/lib/legion/extensions/agentic/social/attachment/runners/attachment.rb +1 -1
- data/lib/legion/extensions/agentic/social/calibration/helpers/calibration_registry.rb +40 -0
- data/lib/legion/extensions/agentic/social/calibration.rb +1 -0
- data/lib/legion/extensions/agentic/social/consent/runners/consent.rb +1 -1
- data/lib/legion/extensions/agentic/social/governance/runners/governance.rb +1 -1
- data/lib/legion/extensions/agentic/social/trust/helpers/trust_map.rb +18 -9
- data/lib/legion/extensions/agentic/social/version.rb +1 -1
- data/spec/legion/extensions/agentic/social/calibration/helpers/calibration_registry_spec.rb +124 -0
- data/spec/legion/extensions/agentic/social/trust/helpers/trust_map_spec.rb +66 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a649d923310d4a98480e6069c03d586585c2a27d3045c625f5c76d608b066016
|
|
4
|
+
data.tar.gz: d5983148aebfa19722b6ae1c5c4e564bd7be94455f3838401bbbe36cdbc087fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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(
|
|
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
|
|
@@ -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:,
|
|
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,
|
|
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)
|
|
@@ -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.
|
|
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
|