lex-agentic-social 0.1.21 → 0.2.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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +22 -1
- data/lib/legion/extensions/agentic/social/calibration/client.rb +3 -6
- data/lib/legion/extensions/agentic/social/calibration/helpers/calibration_store.rb +54 -37
- data/lib/legion/extensions/agentic/social/calibration/runners/calibration.rb +30 -17
- data/lib/legion/extensions/agentic/social/consent/helpers/consent_map.rb +5 -0
- data/lib/legion/extensions/agentic/social/consent/runners/consent.rb +5 -0
- data/lib/legion/extensions/agentic/social/trust/helpers/trust_map.rb +18 -10
- data/lib/legion/extensions/agentic/social/trust/runners/trust.rb +11 -11
- data/lib/legion/extensions/agentic/social/version.rb +1 -1
- data/spec/legion/extensions/agentic/social/calibration/helpers/calibration_store_spec.rb +94 -1
- data/spec/legion/extensions/agentic/social/calibration/runners/calibration_spec.rb +54 -0
- data/spec/legion/extensions/agentic/social/consent/runners/consent_spec.rb +40 -0
- data/spec/legion/extensions/agentic/social/trust/helpers/trust_map_spec.rb +17 -0
- data/spec/legion/extensions/agentic/social/trust/runners/trust_spec.rb +16 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c6d1a18883c8b134af22df999670a6f0cd58ad633c193af3ae9d04cf05cad356
|
|
4
|
+
data.tar.gz: 9c83991c4a5787d6b2f75a21cd199d28eac62e823e00d76d8a88240464c02f93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 27f56a7138662a32573fef178cffebd154d386bb620ec48eedf59248a4f8412e1ce12ee70a4a3598725678c8d1e9baa88076fe2f9888834af2c3c7fa480df105
|
|
7
|
+
data.tar.gz: 32e06a56b8c36e1f70dca344ce6836ba502f4ac4d1f7a1b32ca9535c9e82fb2d89dae0cc6b851b795103f67f780f17b88f7fd120cb9b0341127061a6512e18f0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-10-29
|
|
4
|
+
### Changed
|
|
5
|
+
- Scope calibration by human and match pending feedback to existing exchange attribution. Identified feedback cannot consume legacy unattributed receipts; callers must pass matching identity and applied exchange context.
|
|
6
|
+
- Preserve partner identity in serialized trust state. Older readers that discard this identity are not safe rollback targets for scoped trust data.
|
|
7
|
+
### Fixed
|
|
8
|
+
- Reject ambiguous and duplicate feedback; evaluate against prior baselines and prioritize explicit corrections over weak timing/length cues.
|
|
9
|
+
- Stop treating delayed or terse replies as negative evidence, and leave missing measurements out of baseline updates.
|
|
10
|
+
- Honor partner-scoped trust during recording, reinforcement, lookup, and delegation queries.
|
|
11
|
+
- Reject expired consent approvals synchronously and invalidate pending approval after a tier change without affecting other domains.
|
|
12
|
+
|
|
3
13
|
## [0.1.21] - 2026-07-16
|
|
4
14
|
### Added
|
|
5
15
|
- `CalibrationStore#record_advisory` gains `applied:` kwarg to store which behavioral synapses were applied alongside the advisory.
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Domain consolidation gem for social cognition, governance, and multi-agent inter
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
7
|
**Gem**: `lex-agentic-social`
|
|
8
|
-
**Version**: 0.
|
|
8
|
+
**Version**: 0.2.0
|
|
9
9
|
**Namespace**: `Legion::Extensions::Agentic::Social`
|
|
10
10
|
|
|
11
11
|
## Sub-Modules
|
|
@@ -48,6 +48,27 @@ Domain consolidation gem for social cognition, governance, and multi-agent inter
|
|
|
48
48
|
|
|
49
49
|
`Social::Consent` governs whether actions require human approval — the HITL gate must not be bypassed. `Social::Governance` provides the council approval mechanism used by `Defense::Extinction` for containment escalation.
|
|
50
50
|
|
|
51
|
+
## Scoped calibration and trust
|
|
52
|
+
|
|
53
|
+
Pass a verified `identity:` when recording an advisory, or include it in the
|
|
54
|
+
existing `applied:` hash. Carry the same identity and available `exchange_id`
|
|
55
|
+
or `conversation_id` into the feedback observation. Multiple pending
|
|
56
|
+
advisories are matched by this attribution; ambiguous, mismatched, expired,
|
|
57
|
+
or repeated feedback cannot reinforce another response.
|
|
58
|
+
|
|
59
|
+
`Calibration::Client.new(calibration_registry: registry)` shares an explicitly
|
|
60
|
+
owned registry across clients. Without injection, clients remain independent.
|
|
61
|
+
Legacy unscoped receipts only accept unscoped feedback. GAIA must supply the
|
|
62
|
+
matching receipt attribution before identified feedback can be graded.
|
|
63
|
+
Receipt deduplication is process-local within the existing one-hour lifetime,
|
|
64
|
+
not durable exactly-once processing.
|
|
65
|
+
|
|
66
|
+
Trust runner methods accept `partner_identity:` independently of the worker's
|
|
67
|
+
`agent_id:`. Scoped trust is preserved through Apollo serialization and is
|
|
68
|
+
separate from legacy unscoped trust. Do not roll persisted scoped trust back
|
|
69
|
+
to readers that discard partner identity: they can merge separate people.
|
|
70
|
+
Calibration and trust do not grant consent or tool authority.
|
|
71
|
+
|
|
51
72
|
## Installation
|
|
52
73
|
|
|
53
74
|
```ruby
|
|
@@ -8,13 +8,10 @@ module Legion
|
|
|
8
8
|
class Client
|
|
9
9
|
include Runners::Calibration
|
|
10
10
|
|
|
11
|
-
def initialize(**)
|
|
12
|
-
@calibration_store = Helpers::CalibrationStore.new
|
|
11
|
+
def initialize(calibration_store: nil, calibration_registry: nil, **)
|
|
12
|
+
@calibration_store = calibration_store || Legion::Extensions::Agentic::Social::Calibration::Helpers::CalibrationStore.new
|
|
13
|
+
@calibration_registry = calibration_registry || Legion::Extensions::Agentic::Social::Calibration::Helpers::CalibrationRegistry.new
|
|
13
14
|
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
attr_reader :calibration_store
|
|
18
15
|
end
|
|
19
16
|
end
|
|
20
17
|
end
|
|
@@ -14,7 +14,8 @@ module Legion
|
|
|
14
14
|
[type.to_s, Constants::NEUTRAL_SCORE]
|
|
15
15
|
end
|
|
16
16
|
@history = {}
|
|
17
|
-
@
|
|
17
|
+
@pending_advisories = {}
|
|
18
|
+
@evaluated_advisories = {}
|
|
18
19
|
@partner_baselines = { 'avg_latency' => nil, 'avg_length' => nil }
|
|
19
20
|
@dirty = false
|
|
20
21
|
end
|
|
@@ -28,33 +29,41 @@ module Legion
|
|
|
28
29
|
self
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
def record_advisory(advisory_id:, advisory_types:, applied: nil)
|
|
32
|
-
|
|
32
|
+
def record_advisory(advisory_id:, advisory_types:, applied: nil, identity: nil)
|
|
33
|
+
return if identity && applied&.dig(:identity) && identity.to_s != applied[:identity].to_s
|
|
34
|
+
|
|
35
|
+
expire_advisories!
|
|
36
|
+
return if @evaluated_advisories.key?(advisory_id) || @pending_advisories.key?(advisory_id)
|
|
37
|
+
|
|
38
|
+
attribution = (applied || {}).slice(:identity, :exchange_id, :conversation_id).compact
|
|
39
|
+
attribution[:identity] = identity unless identity.nil?
|
|
40
|
+
@pending_advisories[advisory_id] = {
|
|
33
41
|
advisory_id: advisory_id,
|
|
34
|
-
advisory_types: Array(advisory_types).map(&:to_s),
|
|
42
|
+
advisory_types: Array(advisory_types).map(&:to_s).uniq,
|
|
35
43
|
applied: applied,
|
|
44
|
+
attribution: attribution,
|
|
36
45
|
timestamp: Time.now.utc
|
|
37
46
|
}
|
|
38
47
|
end
|
|
39
48
|
|
|
40
49
|
def evaluate_reaction(observation:)
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
expire_advisories!
|
|
51
|
+
candidates = @pending_advisories.values.select { |meta| matches_observation?(meta, observation) }
|
|
52
|
+
return nil unless candidates.one?
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
return nil if
|
|
54
|
+
meta = candidates.first
|
|
55
|
+
return nil if meta[:advisory_types].empty?
|
|
46
56
|
|
|
47
57
|
reaction_score = compute_reaction_score(observation)
|
|
48
58
|
confidence = compute_confidence(observation)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
deltas = @last_advisory_meta[:advisory_types].map do |type|
|
|
59
|
+
deltas = meta[:advisory_types].map do |type|
|
|
52
60
|
apply_delta(type: type, reaction_score: reaction_score, confidence: confidence)
|
|
53
61
|
end
|
|
54
62
|
|
|
55
|
-
@
|
|
63
|
+
@pending_advisories.delete(meta[:advisory_id])
|
|
64
|
+
@evaluated_advisories[meta[:advisory_id]] = meta[:timestamp]
|
|
56
65
|
@dirty = true
|
|
57
|
-
{ success: true, deltas: deltas, reaction_score: reaction_score, applied:
|
|
66
|
+
{ success: true, deltas: deltas, reaction_score: reaction_score, applied: meta[:applied] }
|
|
58
67
|
end
|
|
59
68
|
|
|
60
69
|
def calibration_weights
|
|
@@ -63,23 +72,19 @@ module Legion
|
|
|
63
72
|
|
|
64
73
|
def detect_explicit_feedback(content)
|
|
65
74
|
text = content.to_s
|
|
66
|
-
return :positive if text.match?(Constants::POSITIVE_PATTERNS)
|
|
67
75
|
return :negative if text.match?(Constants::NEGATIVE_PATTERNS)
|
|
76
|
+
return :positive if text.match?(Constants::POSITIVE_PATTERNS)
|
|
68
77
|
|
|
69
78
|
:neutral
|
|
70
79
|
end
|
|
71
80
|
|
|
72
81
|
def update_baseline(latency:, length:)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
(@partner_baselines['avg_length'] * 0.9) + (length.to_f * 0.1)
|
|
80
|
-
else
|
|
81
|
-
length.to_f
|
|
82
|
-
end
|
|
82
|
+
{ 'avg_latency' => latency, 'avg_length' => length }.each do |key, value|
|
|
83
|
+
next if value.nil?
|
|
84
|
+
|
|
85
|
+
previous = @partner_baselines[key]
|
|
86
|
+
@partner_baselines[key] = previous ? (previous * 0.9) + (value.to_f * 0.1) : value.to_f
|
|
87
|
+
end
|
|
83
88
|
end
|
|
84
89
|
|
|
85
90
|
def to_apollo_entries
|
|
@@ -120,28 +125,40 @@ module Legion
|
|
|
120
125
|
|
|
121
126
|
private
|
|
122
127
|
|
|
128
|
+
def expire_advisories!
|
|
129
|
+
cutoff = Time.now.utc - 3600
|
|
130
|
+
@pending_advisories.delete_if { |_id, meta| meta[:timestamp] < cutoff }
|
|
131
|
+
@evaluated_advisories.delete_if { |_id, timestamp| timestamp < cutoff }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def matches_observation?(meta, observation)
|
|
135
|
+
return false if observation[:advisory_id] && observation[:advisory_id] != meta[:advisory_id]
|
|
136
|
+
|
|
137
|
+
attribution = meta[:attribution]
|
|
138
|
+
return false unless attribution.all? do |key, value|
|
|
139
|
+
!value.to_s.strip.empty? && observation[key] && observation[key].to_s == value.to_s
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
%i[identity exchange_id conversation_id].none? do |key|
|
|
143
|
+
observation[key] && !attribution.key?(key)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
123
147
|
def compute_reaction_score(observation)
|
|
124
148
|
score = 0.0
|
|
125
149
|
|
|
126
150
|
feedback = detect_explicit_feedback(observation[:content] || '')
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
when :negative then score -= Constants::SIGNAL_WEIGHTS[:explicit_feedback]
|
|
130
|
-
end
|
|
151
|
+
return (1.0 + Constants::SIGNAL_WEIGHTS[:explicit_feedback]) / 2.0 if feedback == :positive
|
|
152
|
+
return (1.0 - Constants::SIGNAL_WEIGHTS[:explicit_feedback]) / 2.0 if feedback == :negative
|
|
131
153
|
|
|
132
|
-
if @partner_baselines['avg_latency'] && observation[:latency]
|
|
154
|
+
if @partner_baselines['avg_latency']&.positive? && observation[:latency]
|
|
133
155
|
ratio = observation[:latency].to_f / @partner_baselines['avg_latency']
|
|
134
|
-
|
|
135
|
-
1.0
|
|
136
|
-
else
|
|
137
|
-
(ratio > 1.5 ? -1.0 : 0.0)
|
|
138
|
-
end
|
|
139
|
-
score += Constants::SIGNAL_WEIGHTS[:response_latency] * latency_signal
|
|
156
|
+
score += Constants::SIGNAL_WEIGHTS[:response_latency] if ratio < 0.8
|
|
140
157
|
end
|
|
141
158
|
|
|
142
159
|
if @partner_baselines['avg_length'] && observation[:content_length]
|
|
143
|
-
|
|
144
|
-
score += Constants::SIGNAL_WEIGHTS[:message_length]
|
|
160
|
+
ratio = observation[:content_length].to_f / [@partner_baselines['avg_length'], 1].max
|
|
161
|
+
score += Constants::SIGNAL_WEIGHTS[:message_length] if ratio > 0.5
|
|
145
162
|
end
|
|
146
163
|
|
|
147
164
|
score += Constants::SIGNAL_WEIGHTS[:direct_address] if observation[:direct_address]
|
|
@@ -10,23 +10,32 @@ module Legion
|
|
|
10
10
|
include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers) &&
|
|
11
11
|
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
12
12
|
|
|
13
|
-
def update_calibration(observation: nil, **)
|
|
13
|
+
def update_calibration(observation: nil, identity: nil, **)
|
|
14
14
|
return { success: true, skipped: :no_observation } unless observation
|
|
15
|
+
return { success: true, skipped: :identity_mismatch } if identity && observation[:identity] && identity.to_s != observation[:identity].to_s
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
length: observation[:content_length] || 0
|
|
19
|
-
)
|
|
17
|
+
identity ||= observation[:identity]
|
|
18
|
+
return { success: true, skipped: :unattributed_identity } if identity && identity.to_s.strip.empty?
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
store = calibration_store(identity: identity)
|
|
21
|
+
observation = observation.merge(identity: identity) if identity
|
|
22
|
+
result = store.evaluate_reaction(observation: observation)
|
|
23
|
+
store.update_baseline(latency: observation[:latency], length: observation[:content_length])
|
|
22
24
|
return { success: true, skipped: :no_advisory } unless result
|
|
23
25
|
|
|
24
|
-
{ success: true, deltas: result[:deltas], weights:
|
|
26
|
+
{ success: true, deltas: result[:deltas], weights: store.calibration_weights,
|
|
25
27
|
reaction_score: result[:reaction_score], applied: result[:applied] }
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
def record_advisory_meta(advisory_id:, advisory_types:, applied: nil, **)
|
|
29
|
-
|
|
30
|
+
def record_advisory_meta(advisory_id:, advisory_types:, applied: nil, identity: nil, **)
|
|
31
|
+
return { success: false, error: :identity_mismatch } if identity && applied&.dig(:identity) && identity.to_s != applied[:identity].to_s
|
|
32
|
+
|
|
33
|
+
identity ||= applied&.dig(:identity)
|
|
34
|
+
return { success: false, error: :unattributed_identity } if identity && identity.to_s.strip.empty?
|
|
35
|
+
|
|
36
|
+
calibration_store(identity: identity).record_advisory(
|
|
37
|
+
advisory_id: advisory_id, advisory_types: advisory_types, applied: applied, identity: identity
|
|
38
|
+
)
|
|
30
39
|
{ success: true }
|
|
31
40
|
end
|
|
32
41
|
|
|
@@ -35,16 +44,17 @@ module Legion
|
|
|
35
44
|
{ success: true, feedback: feedback }
|
|
36
45
|
end
|
|
37
46
|
|
|
38
|
-
def calibration_weights(**)
|
|
39
|
-
{ success: true, weights: calibration_store.calibration_weights }
|
|
47
|
+
def calibration_weights(identity: nil, **)
|
|
48
|
+
{ success: true, weights: calibration_store(identity: identity).calibration_weights }
|
|
40
49
|
end
|
|
41
50
|
|
|
42
|
-
def calibration_stats(**)
|
|
51
|
+
def calibration_stats(identity: nil, **)
|
|
52
|
+
store = calibration_store(identity: identity)
|
|
43
53
|
{
|
|
44
54
|
success: true,
|
|
45
|
-
weights:
|
|
46
|
-
history_counts:
|
|
47
|
-
dirty:
|
|
55
|
+
weights: store.calibration_weights,
|
|
56
|
+
history_counts: store.history.transform_values(&:size),
|
|
57
|
+
dirty: store.dirty?
|
|
48
58
|
}
|
|
49
59
|
end
|
|
50
60
|
|
|
@@ -105,8 +115,11 @@ module Legion
|
|
|
105
115
|
|
|
106
116
|
private
|
|
107
117
|
|
|
108
|
-
def calibration_store
|
|
109
|
-
@calibration_store ||= Helpers::CalibrationStore.new
|
|
118
|
+
def calibration_store(identity: nil)
|
|
119
|
+
return @calibration_store ||= Legion::Extensions::Agentic::Social::Calibration::Helpers::CalibrationStore.new if identity.nil?
|
|
120
|
+
|
|
121
|
+
@calibration_registry ||= Legion::Extensions::Agentic::Social::Calibration::Helpers::CalibrationRegistry.new
|
|
122
|
+
@calibration_registry.for(identity: identity)
|
|
110
123
|
end
|
|
111
124
|
|
|
112
125
|
def should_extract_preferences?
|
|
@@ -31,6 +31,11 @@ module Legion
|
|
|
31
31
|
ensure_domain(domain)
|
|
32
32
|
entry = @domains[domain]
|
|
33
33
|
old_tier = entry[:tier]
|
|
34
|
+
if old_tier != tier
|
|
35
|
+
entry[:pending_tier] = nil
|
|
36
|
+
entry[:pending_since] = nil
|
|
37
|
+
entry[:pending_requested_by] = nil
|
|
38
|
+
end
|
|
34
39
|
entry[:tier] = tier
|
|
35
40
|
entry[:last_changed_at] = Time.now.utc
|
|
36
41
|
entry[:history] << { from: old_tier, to: tier, at: Time.now.utc }
|
|
@@ -119,6 +119,11 @@ module Legion
|
|
|
119
119
|
map = consent_map
|
|
120
120
|
return { approved: false, error: 'no_pending_approval' } unless map.pending?(domain)
|
|
121
121
|
|
|
122
|
+
if map.pending_expired?(domain)
|
|
123
|
+
map.clear_pending(domain)
|
|
124
|
+
return { approved: false, error: 'approval_expired' }
|
|
125
|
+
end
|
|
126
|
+
|
|
122
127
|
pending_tier = map.domains[domain][:pending_tier]
|
|
123
128
|
old_tier = map.get_tier(domain)
|
|
124
129
|
map.clear_pending(domain)
|
|
@@ -28,7 +28,9 @@ module Legion
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
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)
|
|
31
|
+
@entries[key(agent_id, domain, partner_identity)] ||= TrustModel.new_trust_entry(agent_id: agent_id, domain: domain).tap do |entry|
|
|
32
|
+
entry[:partner_identity] = partner_identity.to_s if partner_identity
|
|
33
|
+
end
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def record_interaction(agent_id, positive:, domain: :general, partner_identity: nil)
|
|
@@ -61,8 +63,8 @@ module Legion
|
|
|
61
63
|
removed.size
|
|
62
64
|
end
|
|
63
65
|
|
|
64
|
-
def reinforce_dimension(agent_id, dimension:, domain: :general, amount: TrustModel::TRUST_REINFORCEMENT)
|
|
65
|
-
entry = get_or_create(agent_id, domain: domain)
|
|
66
|
+
def reinforce_dimension(agent_id, dimension:, domain: :general, amount: TrustModel::TRUST_REINFORCEMENT, partner_identity: nil)
|
|
67
|
+
entry = get_or_create(agent_id, domain: domain, partner_identity: partner_identity)
|
|
66
68
|
return unless TrustModel::TRUST_DIMENSIONS.include?(dimension)
|
|
67
69
|
|
|
68
70
|
entry[:dimensions][dimension] = TrustModel.clamp(entry[:dimensions][dimension] + amount)
|
|
@@ -84,14 +86,16 @@ module Legion
|
|
|
84
86
|
decayed
|
|
85
87
|
end
|
|
86
88
|
|
|
87
|
-
def trusted_agents(domain: :general, min_trust: TrustModel::TRUST_CONSIDER_THRESHOLD)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
def trusted_agents(domain: :general, min_trust: TrustModel::TRUST_CONSIDER_THRESHOLD, partner_identity: nil)
|
|
90
|
+
identity = partner_identity&.to_s
|
|
91
|
+
selected = @entries.values.select do |entry|
|
|
92
|
+
entry[:domain] == domain && entry[:composite] >= min_trust && entry[:partner_identity] == identity
|
|
93
|
+
end
|
|
94
|
+
selected.sort_by! { |entry| -entry[:composite] }
|
|
91
95
|
end
|
|
92
96
|
|
|
93
|
-
def delegatable_agents(domain: :general)
|
|
94
|
-
trusted_agents(domain: domain, min_trust: TrustModel::TRUST_DELEGATE_THRESHOLD)
|
|
97
|
+
def delegatable_agents(domain: :general, partner_identity: nil)
|
|
98
|
+
trusted_agents(domain: domain, min_trust: TrustModel::TRUST_DELEGATE_THRESHOLD, partner_identity: partner_identity)
|
|
95
99
|
end
|
|
96
100
|
|
|
97
101
|
def count
|
|
@@ -101,8 +105,10 @@ module Legion
|
|
|
101
105
|
def to_apollo_entries
|
|
102
106
|
@entries.map do |_key, entry|
|
|
103
107
|
tags = build_apollo_tags(entry[:agent_id], entry[:domain])
|
|
108
|
+
tags << "partner:#{entry[:partner_identity]}" if entry[:partner_identity]
|
|
104
109
|
content = Legion::JSON.dump({
|
|
105
110
|
agent_id: entry[:agent_id].to_s,
|
|
111
|
+
partner_identity: entry[:partner_identity],
|
|
106
112
|
domain: entry[:domain].to_s,
|
|
107
113
|
dimensions: entry[:dimensions],
|
|
108
114
|
composite: entry[:composite],
|
|
@@ -155,8 +161,10 @@ module Legion
|
|
|
155
161
|
domain_val = (flex(data, 'domain') || 'general').to_sym
|
|
156
162
|
dims = restore_dimensions(flex(data, 'dimensions') || {})
|
|
157
163
|
|
|
158
|
-
|
|
164
|
+
partner_identity = flex(data, 'partner_identity')
|
|
165
|
+
@entries[key(agent_id, domain_val, partner_identity)] = {
|
|
159
166
|
agent_id: agent_id,
|
|
167
|
+
partner_identity: partner_identity&.to_s,
|
|
160
168
|
domain: domain_val,
|
|
161
169
|
dimensions: dims,
|
|
162
170
|
composite: (flex(data, 'composite') || TrustModel::NEUTRAL_TRUST).to_f,
|
|
@@ -10,8 +10,8 @@ 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 get_trust(agent_id:, domain: :general, **)
|
|
14
|
-
entry = trust_map.get(agent_id, domain: domain)
|
|
13
|
+
def get_trust(agent_id:, domain: :general, partner_identity: nil, **)
|
|
14
|
+
entry = trust_map.get(agent_id, domain: domain, partner_identity: partner_identity)
|
|
15
15
|
if entry
|
|
16
16
|
log.debug "[trust] get agent=#{agent_id} domain=#{domain} composite=#{entry[:composite].round(2)}"
|
|
17
17
|
{ found: true, trust: entry }
|
|
@@ -21,8 +21,8 @@ module Legion
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def record_trust_interaction(agent_id:, positive:, domain: :general, **)
|
|
25
|
-
entry = trust_map.record_interaction(agent_id, domain: domain, positive: positive)
|
|
24
|
+
def record_trust_interaction(agent_id:, positive:, domain: :general, partner_identity: nil, **)
|
|
25
|
+
entry = trust_map.record_interaction(agent_id, domain: domain, positive: positive, partner_identity: partner_identity)
|
|
26
26
|
msg = "[trust] interaction: agent=#{agent_id} domain=#{domain} positive=#{positive} " \
|
|
27
27
|
"composite=#{entry[:composite].round(2)} total=#{entry[:interaction_count]}"
|
|
28
28
|
log.info msg
|
|
@@ -35,10 +35,10 @@ module Legion
|
|
|
35
35
|
}
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
def reinforce_trust_dimension(agent_id:, dimension:, domain: :general, amount: nil, **)
|
|
38
|
+
def reinforce_trust_dimension(agent_id:, dimension:, domain: :general, amount: nil, partner_identity: nil, **)
|
|
39
39
|
amt = amount || Helpers::TrustModel::TRUST_REINFORCEMENT
|
|
40
|
-
trust_map.reinforce_dimension(agent_id, domain: domain, dimension: dimension, amount: amt)
|
|
41
|
-
entry = trust_map.get(agent_id, domain: domain)
|
|
40
|
+
trust_map.reinforce_dimension(agent_id, domain: domain, dimension: dimension, amount: amt, partner_identity: partner_identity)
|
|
41
|
+
entry = trust_map.get(agent_id, domain: domain, partner_identity: partner_identity)
|
|
42
42
|
log.debug "[trust] reinforce: agent=#{agent_id} dimension=#{dimension} amount=#{amt} composite=#{entry[:composite].round(2)}"
|
|
43
43
|
{ agent_id: agent_id, domain: domain, dimension: dimension, composite: entry[:composite] }
|
|
44
44
|
end
|
|
@@ -49,15 +49,15 @@ module Legion
|
|
|
49
49
|
{ decayed: decayed }
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
def trusted_agents(domain: :general, min_trust: nil, **)
|
|
52
|
+
def trusted_agents(domain: :general, min_trust: nil, partner_identity: nil, **)
|
|
53
53
|
min = min_trust || Helpers::TrustModel::TRUST_CONSIDER_THRESHOLD
|
|
54
|
-
agents = trust_map.trusted_agents(domain: domain, min_trust: min)
|
|
54
|
+
agents = trust_map.trusted_agents(domain: domain, min_trust: min, partner_identity: partner_identity)
|
|
55
55
|
log.debug "[trust] trusted agents: domain=#{domain} min=#{min} count=#{agents.size}"
|
|
56
56
|
{ agents: agents, count: agents.size }
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
def delegatable_agents(domain: :general, **)
|
|
60
|
-
agents = trust_map.delegatable_agents(domain: domain)
|
|
59
|
+
def delegatable_agents(domain: :general, partner_identity: nil, **)
|
|
60
|
+
agents = trust_map.delegatable_agents(domain: domain, partner_identity: partner_identity)
|
|
61
61
|
log.debug "[trust] delegatable agents: domain=#{domain} count=#{agents.size}"
|
|
62
62
|
{ agents: agents, count: agents.size }
|
|
63
63
|
end
|
|
@@ -114,7 +114,7 @@ RSpec.describe Legion::Extensions::Agentic::Social::Calibration::Helpers::Calibr
|
|
|
114
114
|
store.mark_clean!
|
|
115
115
|
applied_data = { synapse_ids: [10, 20] }
|
|
116
116
|
store.record_advisory(advisory_id: 'test2', advisory_types: [:tone_adjustment], applied: applied_data)
|
|
117
|
-
result = store.evaluate_reaction(observation: { content: 'ok' })
|
|
117
|
+
result = store.evaluate_reaction(observation: { advisory_id: 'test2', content: 'ok' })
|
|
118
118
|
expect(result[:applied]).to eq(applied_data)
|
|
119
119
|
end
|
|
120
120
|
end
|
|
@@ -138,6 +138,91 @@ RSpec.describe Legion::Extensions::Agentic::Social::Calibration::Helpers::Calibr
|
|
|
138
138
|
expect(result[:reaction_score]).to be_between(0.4, 0.6)
|
|
139
139
|
end
|
|
140
140
|
end
|
|
141
|
+
|
|
142
|
+
it 'matches out-of-order feedback to its exchange without consuming the newer advisory' do
|
|
143
|
+
first = { identity: 'alice', exchange_id: 'first' }
|
|
144
|
+
second = { identity: 'alice', exchange_id: 'second' }
|
|
145
|
+
store.record_advisory(advisory_id: 'first', advisory_types: [:tone_adjustment], applied: first)
|
|
146
|
+
store.record_advisory(advisory_id: 'second', advisory_types: [:verbosity_adjustment], applied: second)
|
|
147
|
+
|
|
148
|
+
result = store.evaluate_reaction(observation: first.merge(content: 'wrong'))
|
|
149
|
+
expect(result[:applied]).to eq(first)
|
|
150
|
+
expect(store.calibration_weights['tone_adjustment']).to be < 0.5
|
|
151
|
+
expect(store.calibration_weights['verbosity_adjustment']).to eq(0.5)
|
|
152
|
+
expect(store.evaluate_reaction(observation: second.merge(content: 'thanks'))[:applied]).to eq(second)
|
|
153
|
+
expect(store.calibration_weights['verbosity_adjustment']).to be > 0.5
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'leaves ambiguous and unattributed feedback ungraded until the exchange is identified' do
|
|
157
|
+
%w[first second].each do |exchange|
|
|
158
|
+
store.record_advisory(advisory_id: exchange, advisory_types: [:tone_adjustment],
|
|
159
|
+
applied: { identity: 'alice', exchange_id: exchange })
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
expect(store.evaluate_reaction(observation: { identity: 'alice', content: 'wrong' })).to be_nil
|
|
163
|
+
expect(store.evaluate_reaction(observation: { exchange_id: 'first', content: 'wrong' })).to be_nil
|
|
164
|
+
expect(store.calibration_weights['tone_adjustment']).to eq(0.5)
|
|
165
|
+
result = store.evaluate_reaction(observation: { identity: 'alice', exchange_id: 'first', content: 'wrong' })
|
|
166
|
+
expect(result[:deltas].first[:new_weight]).to be < 0.5
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'does not grade an unattributed advisory using an identified human' do
|
|
170
|
+
store.record_advisory(advisory_id: 'legacy', advisory_types: [:tone_adjustment])
|
|
171
|
+
expect(store.evaluate_reaction(observation: { identity: 'bob', content: 'wrong' })).to be_nil
|
|
172
|
+
expect(store.calibration_weights['tone_adjustment']).to eq(0.5)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'grades an advisory once despite repeated receipt and feedback delivery' do
|
|
176
|
+
applied = { identity: 'alice', exchange_id: 'exchange' }
|
|
177
|
+
2.times do
|
|
178
|
+
store.record_advisory(advisory_id: 'receipt', advisory_types: %i[tone_adjustment tone_adjustment], applied: applied)
|
|
179
|
+
end
|
|
180
|
+
store.evaluate_reaction(observation: applied.merge(content: 'thanks'))
|
|
181
|
+
weight = store.calibration_weights['tone_adjustment']
|
|
182
|
+
store.record_advisory(advisory_id: 'receipt', advisory_types: [:tone_adjustment], applied: applied)
|
|
183
|
+
|
|
184
|
+
expect(store.evaluate_reaction(observation: applied.merge(content: 'thanks'))).to be_nil
|
|
185
|
+
expect(store.calibration_weights['tone_adjustment']).to eq(weight)
|
|
186
|
+
expect(store.history['tone_adjustment'].size).to eq(1)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it 'does not guess which pending legacy advisory a reaction describes' do
|
|
190
|
+
store.record_advisory(advisory_id: 'first', advisory_types: [:tone_adjustment])
|
|
191
|
+
store.record_advisory(advisory_id: 'second', advisory_types: [:verbosity_adjustment])
|
|
192
|
+
expect(store.evaluate_reaction(observation: { content: 'wrong' })).to be_nil
|
|
193
|
+
expect(store.calibration_weights.values).to all(eq(0.5))
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it 'keeps an explicit correction negative despite polite words and positive weak cues' do
|
|
197
|
+
store.update_baseline(latency: 10, length: 20)
|
|
198
|
+
store.record_advisory(advisory_id: 'correction', advisory_types: [:tone_adjustment])
|
|
199
|
+
result = store.evaluate_reaction(observation: {
|
|
200
|
+
content: 'thanks, but that is wrong', latency: 1,
|
|
201
|
+
content_length: 100, direct_address: true
|
|
202
|
+
})
|
|
203
|
+
expect(result[:reaction_score]).to be < 0.5
|
|
204
|
+
expect(store.calibration_weights['tone_adjustment']).to be < 0.5
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'does not treat delayed terse replies as negative evidence' do
|
|
208
|
+
store.update_baseline(latency: 2, length: 100)
|
|
209
|
+
store.record_advisory(advisory_id: 'delayed', advisory_types: [:tone_adjustment])
|
|
210
|
+
result = store.evaluate_reaction(observation: { content: 'ok', latency: 500, content_length: 2 })
|
|
211
|
+
expect(result[:reaction_score]).to eq(0.5)
|
|
212
|
+
expect(store.calibration_weights['tone_adjustment']).to eq(0.5)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it 'expires old receipts without discarding a fresh exchange' do
|
|
216
|
+
now = Time.utc(2026, 10, 29)
|
|
217
|
+
allow(Time).to receive(:now).and_return(now)
|
|
218
|
+
store.record_advisory(advisory_id: 'old', advisory_types: [:tone_adjustment])
|
|
219
|
+
allow(Time).to receive(:now).and_return(now + 3601)
|
|
220
|
+
store.record_advisory(advisory_id: 'fresh', advisory_types: [:verbosity_adjustment])
|
|
221
|
+
expect(store.evaluate_reaction(observation: { advisory_id: 'old', content: 'wrong' })).to be_nil
|
|
222
|
+
store.evaluate_reaction(observation: { advisory_id: 'fresh', content: 'thanks' })
|
|
223
|
+
expect(store.calibration_weights['tone_adjustment']).to eq(0.5)
|
|
224
|
+
expect(store.calibration_weights['verbosity_adjustment']).to be > 0.5
|
|
225
|
+
end
|
|
141
226
|
end
|
|
142
227
|
|
|
143
228
|
describe '#detect_explicit_feedback' do
|
|
@@ -182,6 +267,14 @@ RSpec.describe Legion::Extensions::Agentic::Social::Calibration::Helpers::Calibr
|
|
|
182
267
|
parsed = Legion::JSON.parse(weights_entry[:content])
|
|
183
268
|
expect(parsed[:baselines][:avg_latency]).to be_between(2.0, 4.0)
|
|
184
269
|
end
|
|
270
|
+
|
|
271
|
+
it 'does not turn missing measurements into a zero baseline' do
|
|
272
|
+
store.update_baseline(latency: 10, length: 100)
|
|
273
|
+
store.update_baseline(latency: nil, length: nil)
|
|
274
|
+
store.record_advisory(advisory_id: 'baseline', advisory_types: [:tone_adjustment])
|
|
275
|
+
result = store.evaluate_reaction(observation: { latency: 7.5, content_length: 48 })
|
|
276
|
+
expect(result[:reaction_score]).to eq(0.6)
|
|
277
|
+
end
|
|
185
278
|
end
|
|
186
279
|
|
|
187
280
|
describe '#to_apollo_entries' do
|
|
@@ -36,6 +36,60 @@ RSpec.describe Legion::Extensions::Agentic::Social::Calibration::Runners::Calibr
|
|
|
36
36
|
result = client.update_calibration(observation: { content: 'thanks', direct_address: true, content_length: 20 })
|
|
37
37
|
expect(result[:applied]).to eq(applied_data)
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
it 'keeps two humans baselines, pending exchanges and weights independent' do
|
|
41
|
+
client.update_calibration(observation: { identity: 'alice', latency: 10 })
|
|
42
|
+
client.update_calibration(observation: { identity: 'bob', latency: 1 })
|
|
43
|
+
client.record_advisory_meta(advisory_id: 'alice-reply', advisory_types: [:tone_adjustment],
|
|
44
|
+
applied: { identity: 'alice', exchange_id: 'a' })
|
|
45
|
+
client.record_advisory_meta(advisory_id: 'bob-reply', advisory_types: [:verbosity_adjustment],
|
|
46
|
+
applied: { identity: 'bob', exchange_id: 'b' })
|
|
47
|
+
|
|
48
|
+
wrong_human = client.update_calibration(observation: { identity: 'bob', exchange_id: 'a', content: 'wrong' })
|
|
49
|
+
expect(wrong_human[:skipped]).to eq(:no_advisory)
|
|
50
|
+
bob = client.update_calibration(observation: { identity: 'bob', exchange_id: 'b', content: 'wrong' })
|
|
51
|
+
alice = client.update_calibration(observation: { identity: 'alice', exchange_id: 'a', latency: 7.9 })
|
|
52
|
+
|
|
53
|
+
expect(bob[:weights]['verbosity_adjustment']).to be < 0.5
|
|
54
|
+
expect(bob[:weights]['tone_adjustment']).to eq(0.5)
|
|
55
|
+
expect(alice[:reaction_score]).to eq(0.6)
|
|
56
|
+
expect(alice[:weights]['tone_adjustment']).to be > 0.5
|
|
57
|
+
expect(alice[:weights]['verbosity_adjustment']).to eq(0.5)
|
|
58
|
+
expect(client.calibration_weights[:weights].values).to all(eq(0.5))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'does not consume scoped feedback using missing or contradictory identity' do
|
|
62
|
+
client.record_advisory_meta(advisory_id: 'alice', advisory_types: [:tone_adjustment],
|
|
63
|
+
applied: { identity: 'alice', exchange_id: 'a' })
|
|
64
|
+
expect(client.update_calibration(observation: { exchange_id: 'a', content: 'wrong' })[:skipped]).to eq(:no_advisory)
|
|
65
|
+
result = client.update_calibration(identity: 'alice', observation: { identity: 'bob', exchange_id: 'a', content: 'wrong' })
|
|
66
|
+
expect(result[:skipped]).to eq(:identity_mismatch)
|
|
67
|
+
expect(client.calibration_weights(identity: 'alice')[:weights]['tone_adjustment']).to eq(0.5)
|
|
68
|
+
result = client.update_calibration(observation: { identity: 'alice', exchange_id: 'a', content: 'thanks' })
|
|
69
|
+
expect(result[:weights]['tone_adjustment']).to be > 0.5
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'uses an injected registry as the state owner across client instances' do
|
|
73
|
+
registry = Legion::Extensions::Agentic::Social::Calibration::Helpers::CalibrationRegistry.new
|
|
74
|
+
writer = Legion::Extensions::Agentic::Social::Calibration::Client.new(calibration_registry: registry)
|
|
75
|
+
reader = Legion::Extensions::Agentic::Social::Calibration::Client.new(calibration_registry: registry)
|
|
76
|
+
writer.record_advisory_meta(advisory_id: 'shared', identity: 'alice', advisory_types: [:tone_adjustment])
|
|
77
|
+
reader.update_calibration(observation: { identity: 'alice', content: 'wrong' })
|
|
78
|
+
|
|
79
|
+
expect(writer.calibration_weights(identity: 'alice')[:weights]['tone_adjustment']).to be < 0.5
|
|
80
|
+
expect(reader.calibration_stats(identity: 'alice')[:history_counts]['tone_adjustment']).to eq(1)
|
|
81
|
+
expect(client.calibration_weights(identity: 'alice')[:weights]['tone_adjustment']).to eq(0.5)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'keeps a bare runner on the same scoped contract as clients' do
|
|
85
|
+
runner = Object.new.extend(described_class)
|
|
86
|
+
runner.record_advisory_meta(advisory_id: 'bare', identity: 'alice', advisory_types: [:tone_adjustment])
|
|
87
|
+
runner.update_calibration(observation: { identity: 'bob', content: 'wrong' })
|
|
88
|
+
runner.update_calibration(observation: { identity: 'alice', content: 'thanks' })
|
|
89
|
+
|
|
90
|
+
expect(runner.calibration_weights(identity: 'alice')[:weights]['tone_adjustment']).to be > 0.5
|
|
91
|
+
expect(runner.calibration_weights(identity: 'bob')[:weights]['tone_adjustment']).to eq(0.5)
|
|
92
|
+
end
|
|
39
93
|
end
|
|
40
94
|
|
|
41
95
|
describe '#record_advisory_meta' do
|
|
@@ -136,6 +136,46 @@ RSpec.describe Legion::Extensions::Agentic::Social::Consent::Runners::Consent do
|
|
|
136
136
|
expect(result[:approved]).to be false
|
|
137
137
|
expect(result[:error]).to eq('no_pending_approval')
|
|
138
138
|
end
|
|
139
|
+
|
|
140
|
+
it 'rejects expired approval without relying on a scheduled expiry sweep' do
|
|
141
|
+
now = Time.utc(2026, 10, 29)
|
|
142
|
+
allow(Time).to receive(:now).and_return(now)
|
|
143
|
+
client.request_autonomous_approval(domain: 'email')
|
|
144
|
+
timeout = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap::APPROVAL_TIMEOUT
|
|
145
|
+
allow(Time).to receive(:now).and_return(now + timeout + 1)
|
|
146
|
+
|
|
147
|
+
expect(client.approve_promotion(domain: 'email', approved_by: 'admin')[:approved]).to be false
|
|
148
|
+
expect(client.check_consent(domain: 'email')[:allowed]).to be false
|
|
149
|
+
expect(client.request_autonomous_approval(domain: 'email')[:requested]).to be true
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'does not let an old approval undo a subsequent demotion in another domain' do
|
|
153
|
+
client.request_autonomous_approval(domain: 'email')
|
|
154
|
+
client.request_autonomous_approval(domain: 'calendar')
|
|
155
|
+
client.apply_tier_change(domain: 'email', new_tier: :human_only)
|
|
156
|
+
|
|
157
|
+
expect(client.approve_promotion(domain: 'email', approved_by: 'admin')[:approved]).to be false
|
|
158
|
+
expect(client.check_consent(domain: 'email')[:human_only]).to be true
|
|
159
|
+
expect(client.approve_promotion(domain: 'calendar', approved_by: 'admin')[:approved]).to be true
|
|
160
|
+
expect(client.check_consent(domain: 'email')[:allowed]).to be false
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it 'does not grant consent from positive calibration or trust feedback' do
|
|
164
|
+
calibration = Legion::Extensions::Agentic::Social::Calibration::Client.new
|
|
165
|
+
trust = Legion::Extensions::Agentic::Social::Trust::Client.new
|
|
166
|
+
client.apply_tier_change(domain: 'email', new_tier: :human_only)
|
|
167
|
+
client.request_autonomous_approval(domain: 'calendar')
|
|
168
|
+
20.times do |index|
|
|
169
|
+
calibration.record_advisory_meta(advisory_id: index, identity: 'alice', advisory_types: [:tone_adjustment])
|
|
170
|
+
calibration.update_calibration(observation: { identity: 'alice', content: 'thanks' })
|
|
171
|
+
trust.record_trust_interaction(agent_id: 'alice', positive: true)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
expect(client.check_consent(domain: 'email')[:human_only]).to be true
|
|
175
|
+
expect(client.check_consent(domain: 'calendar')[:allowed]).to be false
|
|
176
|
+
expect(client.approve_promotion(domain: 'calendar', approved_by: 'admin')[:approved]).to be true
|
|
177
|
+
expect(client.check_consent(domain: 'email')[:allowed]).to be false
|
|
178
|
+
end
|
|
139
179
|
end
|
|
140
180
|
|
|
141
181
|
describe '#reject_promotion' do
|
|
@@ -324,6 +324,23 @@ RSpec.describe Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap do
|
|
|
324
324
|
map.get_or_create(agent_id, partner_identity: partner_y)
|
|
325
325
|
expect(map.count).to eq(2)
|
|
326
326
|
end
|
|
327
|
+
|
|
328
|
+
it 'preserves scoped trust across serialization without overwriting legacy or another partner' do
|
|
329
|
+
map.record_interaction(agent_id, positive: true, partner_identity: partner_x)
|
|
330
|
+
map.record_interaction(agent_id, positive: false, partner_identity: partner_y)
|
|
331
|
+
map.record_interaction(agent_id, positive: true)
|
|
332
|
+
restored = described_class.new
|
|
333
|
+
persisted = double('apollo', query: { success: true, results: map.to_apollo_entries })
|
|
334
|
+
expect(restored.from_apollo(store: persisted)).to be true
|
|
335
|
+
|
|
336
|
+
expect(restored.get(agent_id, partner_identity: partner_x)[:positive_count]).to eq(1)
|
|
337
|
+
expect(restored.get(agent_id, partner_identity: partner_y)[:negative_count]).to eq(1)
|
|
338
|
+
expect(restored.get(agent_id)[:positive_count]).to eq(1)
|
|
339
|
+
restored.erase_partner!(identity: partner_x)
|
|
340
|
+
expect(restored.get(agent_id, partner_identity: partner_x)).to be_nil
|
|
341
|
+
expect(restored.get(agent_id, partner_identity: partner_y)[:negative_count]).to eq(1)
|
|
342
|
+
expect(restored.get(agent_id)[:positive_count]).to eq(1)
|
|
343
|
+
end
|
|
327
344
|
end
|
|
328
345
|
|
|
329
346
|
describe '#erase_partner!(identity:)' do
|
|
@@ -44,6 +44,22 @@ RSpec.describe Legion::Extensions::Agentic::Social::Trust::Runners::Trust do
|
|
|
44
44
|
|
|
45
45
|
expect(code_trust[:trust][:composite]).to be > ops_trust[:trust][:composite]
|
|
46
46
|
end
|
|
47
|
+
|
|
48
|
+
it 'isolates partner trust through recording, reinforcement and delegation queries' do
|
|
49
|
+
20.times do
|
|
50
|
+
client.record_trust_interaction(agent_id: 'worker', positive: true, partner_identity: 'alice')
|
|
51
|
+
end
|
|
52
|
+
client.record_trust_interaction(agent_id: 'worker', positive: false, partner_identity: 'bob')
|
|
53
|
+
alice_before = client.get_trust(agent_id: 'worker', partner_identity: 'alice')[:trust][:composite]
|
|
54
|
+
client.reinforce_trust_dimension(agent_id: 'worker', dimension: :competence, amount: 0.1, partner_identity: 'bob')
|
|
55
|
+
|
|
56
|
+
expect(client.get_trust(agent_id: 'worker', partner_identity: 'alice')[:trust][:composite]).to eq(alice_before)
|
|
57
|
+
expect(client.get_trust(agent_id: 'worker', partner_identity: 'bob')[:trust][:interaction_count]).to eq(1)
|
|
58
|
+
expect(client.get_trust(agent_id: 'worker')[:found]).to be false
|
|
59
|
+
expect(client.delegatable_agents(partner_identity: 'alice')[:agents].map { |entry| entry[:agent_id] }).to eq(['worker'])
|
|
60
|
+
expect(client.trusted_agents(partner_identity: 'bob')[:agents]).to be_empty
|
|
61
|
+
expect(client.trusted_agents[:agents]).to be_empty
|
|
62
|
+
end
|
|
47
63
|
end
|
|
48
64
|
|
|
49
65
|
describe '#reinforce_trust_dimension' do
|