lex-agentic-social 0.1.6 → 0.1.7
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/trust/helpers/trust_map.rb +94 -64
- data/lib/legion/extensions/agentic/social/trust.rb +0 -7
- data/lib/legion/extensions/agentic/social/version.rb +1 -1
- data/spec/legion/extensions/agentic/social/trust/local_persistence_spec.rb +245 -258
- 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: 2a637b1280b8b3159e22384a36f90577406b8f3531189100d5d379cadd2dc00b
|
|
4
|
+
data.tar.gz: 16d221421b1212af948a0afffa6bb54e149087b13b4d473f1dcfe4cdd459ba27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94e0f0c04272c236d18011f3b8bb63798f06be346335bb0dcebac5472439f927817465d2f07bffa5c0633302c0d3bdb8b8a64bb7c30b3d7633a6b38e8cb52029
|
|
7
|
+
data.tar.gz: 5fbd869c1827ee00431b24d9ac317d001dfd4af19926a241f70f06cccad44656467585b6adea2499df1938524151dc096be4ceac3a784c34d52d8461fc744920
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.7] - 2026-03-31
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Migrate `TrustMap` persistence from Data::Local SQLite to Apollo Local (`to_apollo_entries`, `from_apollo`)
|
|
7
|
+
- Add dirty tracking (`dirty?`, `mark_clean!`) to `TrustMap` matching SocialGraph/MentalStateTracker pattern
|
|
8
|
+
- Tag schema: `['trust', 'trust_entry', '<agent_id>', '<domain>']` with optional `'partner'` tag via BondRegistry
|
|
9
|
+
- Remove Data::Local migration registration from trust entry point (migration file retained for existing installs)
|
|
10
|
+
- Add one-time `scripts/migrate_trust_to_apollo.rb` for legacy SQLite data migration
|
|
11
|
+
|
|
3
12
|
## [0.1.6] - 2026-03-31
|
|
4
13
|
|
|
5
14
|
### Added
|
|
@@ -11,7 +11,16 @@ module Legion
|
|
|
11
11
|
|
|
12
12
|
def initialize
|
|
13
13
|
@entries = {} # key: "agent_id:domain"
|
|
14
|
-
|
|
14
|
+
@dirty = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def dirty?
|
|
18
|
+
@dirty
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def mark_clean!
|
|
22
|
+
@dirty = false
|
|
23
|
+
self
|
|
15
24
|
end
|
|
16
25
|
|
|
17
26
|
def get(agent_id, domain: :general)
|
|
@@ -40,6 +49,7 @@ module Legion
|
|
|
40
49
|
end
|
|
41
50
|
|
|
42
51
|
entry[:composite] = TrustModel.composite_score(entry[:dimensions])
|
|
52
|
+
@dirty = true
|
|
43
53
|
entry
|
|
44
54
|
end
|
|
45
55
|
|
|
@@ -49,6 +59,7 @@ module Legion
|
|
|
49
59
|
|
|
50
60
|
entry[:dimensions][dimension] = TrustModel.clamp(entry[:dimensions][dimension] + amount)
|
|
51
61
|
entry[:composite] = TrustModel.composite_score(entry[:dimensions])
|
|
62
|
+
@dirty = true
|
|
52
63
|
end
|
|
53
64
|
|
|
54
65
|
def decay_all
|
|
@@ -61,6 +72,7 @@ module Legion
|
|
|
61
72
|
entry[:composite] = TrustModel.composite_score(entry[:dimensions])
|
|
62
73
|
decayed += 1
|
|
63
74
|
end
|
|
75
|
+
@dirty = true if decayed.positive?
|
|
64
76
|
decayed
|
|
65
77
|
end
|
|
66
78
|
|
|
@@ -78,72 +90,33 @@ module Legion
|
|
|
78
90
|
@entries.size
|
|
79
91
|
end
|
|
80
92
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
positive_count: entry[:positive_count],
|
|
97
|
-
negative_count: entry[:negative_count],
|
|
98
|
-
last_interaction: entry[:last_interaction],
|
|
99
|
-
created_at: entry[:created_at]
|
|
100
|
-
}
|
|
101
|
-
existing = dataset.where(agent_id: row[:agent_id], domain: row[:domain]).first
|
|
102
|
-
if existing
|
|
103
|
-
dataset.where(agent_id: row[:agent_id], domain: row[:domain])
|
|
104
|
-
.update(row.except(:agent_id, :domain))
|
|
105
|
-
else
|
|
106
|
-
dataset.insert(row)
|
|
107
|
-
end
|
|
93
|
+
def to_apollo_entries
|
|
94
|
+
@entries.map do |_key, entry|
|
|
95
|
+
tags = build_apollo_tags(entry[:agent_id], entry[:domain])
|
|
96
|
+
content = Legion::JSON.dump({
|
|
97
|
+
agent_id: entry[:agent_id].to_s,
|
|
98
|
+
domain: entry[:domain].to_s,
|
|
99
|
+
dimensions: entry[:dimensions],
|
|
100
|
+
composite: entry[:composite],
|
|
101
|
+
interaction_count: entry[:interaction_count],
|
|
102
|
+
positive_count: entry[:positive_count],
|
|
103
|
+
negative_count: entry[:negative_count],
|
|
104
|
+
last_interaction: entry[:last_interaction]&.iso8601,
|
|
105
|
+
created_at: entry[:created_at]&.iso8601
|
|
106
|
+
})
|
|
107
|
+
{ content: content, tags: tags }
|
|
108
108
|
end
|
|
109
|
+
end
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
rescue StandardError => e
|
|
117
|
-
Legion::Logging.warn "[trust] save_to_local failed: #{e.message}"
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def load_from_local
|
|
121
|
-
return unless defined?(Legion::Data::Local) && Legion::Data::Local.connected?
|
|
122
|
-
|
|
123
|
-
Legion::Data::Local.connection[:trust_entries].each do |row|
|
|
124
|
-
agent_id = row[:agent_id]
|
|
125
|
-
domain_str = row[:domain]
|
|
126
|
-
domain_val = domain_str.to_sym
|
|
127
|
-
entry_key = "#{agent_id}:#{domain_str}"
|
|
128
|
-
@entries[entry_key] = {
|
|
129
|
-
agent_id: agent_id,
|
|
130
|
-
domain: domain_val,
|
|
131
|
-
dimensions: {
|
|
132
|
-
reliability: row[:reliability].to_f,
|
|
133
|
-
competence: row[:competence].to_f,
|
|
134
|
-
integrity: row[:integrity].to_f,
|
|
135
|
-
benevolence: row[:benevolence].to_f
|
|
136
|
-
},
|
|
137
|
-
composite: row[:composite].to_f,
|
|
138
|
-
interaction_count: row[:interaction_count].to_i,
|
|
139
|
-
positive_count: row[:positive_count].to_i,
|
|
140
|
-
negative_count: row[:negative_count].to_i,
|
|
141
|
-
last_interaction: row[:last_interaction],
|
|
142
|
-
created_at: row[:created_at]
|
|
143
|
-
}
|
|
144
|
-
end
|
|
111
|
+
def from_apollo(store:)
|
|
112
|
+
result = store.query(text: 'trust trust_entry', tags: %w[trust trust_entry])
|
|
113
|
+
return false unless result[:success] && result[:results]&.any?
|
|
114
|
+
|
|
115
|
+
result[:results].each { |entry| restore_from_entry(entry) }
|
|
116
|
+
true
|
|
145
117
|
rescue StandardError => e
|
|
146
|
-
Legion::Logging.warn
|
|
118
|
+
Legion::Logging.warn("[trust_map] from_apollo error: #{e.message}")
|
|
119
|
+
false
|
|
147
120
|
end
|
|
148
121
|
|
|
149
122
|
private
|
|
@@ -151,6 +124,63 @@ module Legion
|
|
|
151
124
|
def key(agent_id, domain)
|
|
152
125
|
"#{agent_id}:#{domain}"
|
|
153
126
|
end
|
|
127
|
+
|
|
128
|
+
def build_apollo_tags(agent_id, domain)
|
|
129
|
+
tags = ['trust', 'trust_entry', agent_id.to_s, domain.to_s]
|
|
130
|
+
tags << 'partner' if defined?(Legion::Gaia::BondRegistry) && partner_agent?(agent_id)
|
|
131
|
+
tags
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def partner_agent?(agent_id)
|
|
135
|
+
Legion::Gaia::BondRegistry.partner?(agent_id.to_s)
|
|
136
|
+
rescue StandardError => e
|
|
137
|
+
Legion::Logging.debug("[trust_map] BondRegistry check failed: #{e.message}")
|
|
138
|
+
false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def restore_from_entry(entry)
|
|
142
|
+
data = Legion::JSON.parse(entry[:content])
|
|
143
|
+
agent_id = flex(data, 'agent_id')
|
|
144
|
+
return unless agent_id
|
|
145
|
+
|
|
146
|
+
domain_val = (flex(data, 'domain') || 'general').to_sym
|
|
147
|
+
dims = restore_dimensions(flex(data, 'dimensions') || {})
|
|
148
|
+
|
|
149
|
+
@entries[key(agent_id, domain_val)] = {
|
|
150
|
+
agent_id: agent_id,
|
|
151
|
+
domain: domain_val,
|
|
152
|
+
dimensions: dims,
|
|
153
|
+
composite: (flex(data, 'composite') || TrustModel::NEUTRAL_TRUST).to_f,
|
|
154
|
+
interaction_count: (flex(data, 'interaction_count') || 0).to_i,
|
|
155
|
+
positive_count: (flex(data, 'positive_count') || 0).to_i,
|
|
156
|
+
negative_count: (flex(data, 'negative_count') || 0).to_i,
|
|
157
|
+
last_interaction: parse_time(flex(data, 'last_interaction')),
|
|
158
|
+
created_at: parse_time(flex(data, 'created_at')) || Time.now.utc
|
|
159
|
+
}
|
|
160
|
+
rescue StandardError => e
|
|
161
|
+
Legion::Logging.debug("[trust_map] restore entry failed: #{e.message}")
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def flex(hash, string_key)
|
|
166
|
+
hash[string_key] || hash[string_key.to_sym]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def restore_dimensions(dims)
|
|
170
|
+
TrustModel::TRUST_DIMENSIONS.to_h do |dim|
|
|
171
|
+
[dim, (flex(dims, dim.to_s) || TrustModel::NEUTRAL_TRUST).to_f]
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def parse_time(value)
|
|
176
|
+
return nil if value.nil?
|
|
177
|
+
return value if value.is_a?(Time)
|
|
178
|
+
|
|
179
|
+
Time.parse(value.to_s)
|
|
180
|
+
rescue ArgumentError => e
|
|
181
|
+
Legion::Logging.debug("[trust_map] parse_time failed: #{e.message}")
|
|
182
|
+
nil
|
|
183
|
+
end
|
|
154
184
|
end
|
|
155
185
|
end
|
|
156
186
|
end
|
|
@@ -1,204 +1,200 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
-
require 'sequel'
|
|
5
|
-
require 'tmpdir'
|
|
6
|
-
|
|
7
|
-
RSpec.describe 'lex-trust local SQLite persistence' do
|
|
8
|
-
let(:db_path) { File.join(Dir.tmpdir, "trust_test_#{Process.pid}_#{rand(9999)}.db") }
|
|
9
|
-
let(:db) { Sequel.sqlite(db_path) }
|
|
10
|
-
|
|
11
|
-
before do
|
|
12
|
-
db.create_table(:trust_entries) do
|
|
13
|
-
primary_key :id
|
|
14
|
-
String :agent_id, null: false
|
|
15
|
-
String :domain, null: false
|
|
16
|
-
Float :reliability, default: 0.3
|
|
17
|
-
Float :competence, default: 0.3
|
|
18
|
-
Float :integrity, default: 0.3
|
|
19
|
-
Float :benevolence, default: 0.3
|
|
20
|
-
Float :composite, default: 0.3
|
|
21
|
-
Integer :interaction_count, default: 0
|
|
22
|
-
Integer :positive_count, default: 0
|
|
23
|
-
Integer :negative_count, default: 0
|
|
24
|
-
DateTime :last_interaction
|
|
25
|
-
DateTime :created_at, null: false
|
|
26
|
-
unique %i[agent_id domain]
|
|
27
|
-
index [:agent_id]
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
stub_const('Legion::Data::Local', Module.new do
|
|
31
|
-
def self.connected?
|
|
32
|
-
true
|
|
33
|
-
end
|
|
34
4
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
5
|
+
RSpec.describe 'lex-trust Apollo Local persistence' do
|
|
6
|
+
subject(:map) { described_class.new }
|
|
38
7
|
|
|
39
|
-
|
|
40
|
-
@_connection = conn
|
|
41
|
-
end
|
|
42
|
-
end)
|
|
8
|
+
let(:described_class) { Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap }
|
|
43
9
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
db.disconnect
|
|
49
|
-
FileUtils.rm_f(db_path)
|
|
50
|
-
end
|
|
10
|
+
describe '#dirty?' do
|
|
11
|
+
it 'starts clean' do
|
|
12
|
+
expect(map.dirty?).to be false
|
|
13
|
+
end
|
|
51
14
|
|
|
52
|
-
|
|
53
|
-
it 'writes an entry to the database' do
|
|
54
|
-
map = Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new
|
|
15
|
+
it 'becomes dirty after record_interaction' do
|
|
55
16
|
map.record_interaction('agent-001', positive: true)
|
|
56
|
-
map.
|
|
57
|
-
|
|
58
|
-
row = db[:trust_entries].where(agent_id: 'agent-001', domain: 'general').first
|
|
59
|
-
expect(row).not_to be_nil
|
|
60
|
-
expect(row[:agent_id]).to eq('agent-001')
|
|
61
|
-
expect(row[:domain]).to eq('general')
|
|
62
|
-
expect(row[:positive_count]).to eq(1)
|
|
63
|
-
expect(row[:interaction_count]).to eq(1)
|
|
17
|
+
expect(map.dirty?).to be true
|
|
64
18
|
end
|
|
65
19
|
|
|
66
|
-
it '
|
|
67
|
-
map
|
|
68
|
-
map.
|
|
69
|
-
map.
|
|
20
|
+
it 'becomes dirty after reinforce_dimension' do
|
|
21
|
+
map.get_or_create('agent-001')
|
|
22
|
+
map.reinforce_dimension('agent-001', dimension: :competence)
|
|
23
|
+
expect(map.dirty?).to be true
|
|
24
|
+
end
|
|
70
25
|
|
|
71
|
-
|
|
72
|
-
map.
|
|
26
|
+
it 'becomes dirty after decay_all with entries' do
|
|
27
|
+
map.get_or_create('agent-001')
|
|
28
|
+
map.decay_all
|
|
29
|
+
expect(map.dirty?).to be true
|
|
30
|
+
end
|
|
73
31
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
expect(
|
|
77
|
-
expect(rows.first[:interaction_count]).to eq(2)
|
|
32
|
+
it 'stays clean after decay_all with no entries' do
|
|
33
|
+
map.decay_all
|
|
34
|
+
expect(map.dirty?).to be false
|
|
78
35
|
end
|
|
79
36
|
|
|
80
|
-
it '
|
|
81
|
-
map = Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new
|
|
37
|
+
it 'becomes clean after mark_clean!' do
|
|
82
38
|
map.record_interaction('agent-001', positive: true)
|
|
83
|
-
map.
|
|
39
|
+
map.mark_clean!
|
|
40
|
+
expect(map.dirty?).to be false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
84
43
|
|
|
85
|
-
|
|
86
|
-
|
|
44
|
+
describe '#mark_clean!' do
|
|
45
|
+
it 'returns self' do
|
|
46
|
+
expect(map.mark_clean!).to eq(map)
|
|
87
47
|
end
|
|
88
48
|
|
|
89
|
-
it '
|
|
90
|
-
map = Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new
|
|
49
|
+
it 'resets dirty flag' do
|
|
91
50
|
map.record_interaction('agent-001', positive: true)
|
|
92
|
-
map.
|
|
51
|
+
map.mark_clean!
|
|
52
|
+
expect(map.dirty?).to be false
|
|
53
|
+
end
|
|
54
|
+
end
|
|
93
55
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
expect(
|
|
97
|
-
expect(row[:integrity]).to be > 0.3
|
|
98
|
-
expect(row[:benevolence]).to be > 0.3
|
|
56
|
+
describe '#to_apollo_entries' do
|
|
57
|
+
it 'returns empty array when no entries' do
|
|
58
|
+
expect(map.to_apollo_entries).to eq([])
|
|
99
59
|
end
|
|
100
60
|
|
|
101
|
-
it '
|
|
102
|
-
map = Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new
|
|
61
|
+
it 'returns one entry per trust record' do
|
|
103
62
|
map.record_interaction('agent-a', positive: true)
|
|
104
63
|
map.record_interaction('agent-b', positive: false)
|
|
105
|
-
map.
|
|
106
|
-
|
|
107
|
-
expect(db[:trust_entries].count).to eq(2)
|
|
64
|
+
expect(map.to_apollo_entries.size).to eq(2)
|
|
108
65
|
end
|
|
109
66
|
|
|
110
|
-
it '
|
|
111
|
-
map = Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new
|
|
67
|
+
it 'returns separate entries for different domains of the same agent' do
|
|
112
68
|
map.record_interaction('agent-001', positive: true, domain: :code)
|
|
113
69
|
map.record_interaction('agent-001', positive: false, domain: :ops)
|
|
114
|
-
map.
|
|
70
|
+
expect(map.to_apollo_entries.size).to eq(2)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'entry content is a JSON string with agent_id' do
|
|
74
|
+
map.record_interaction('agent-001', positive: true)
|
|
75
|
+
entry = map.to_apollo_entries.first
|
|
76
|
+
parsed = JSON.parse(entry[:content])
|
|
77
|
+
expect(parsed).to have_key('agent_id')
|
|
78
|
+
expect(parsed['agent_id']).to eq('agent-001')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'entry content includes domain' do
|
|
82
|
+
map.record_interaction('agent-001', positive: true, domain: :code)
|
|
83
|
+
entry = map.to_apollo_entries.first
|
|
84
|
+
parsed = JSON.parse(entry[:content])
|
|
85
|
+
expect(parsed['domain']).to eq('code')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'entry content includes all four dimensions' do
|
|
89
|
+
map.record_interaction('agent-001', positive: true)
|
|
90
|
+
entry = map.to_apollo_entries.first
|
|
91
|
+
parsed = JSON.parse(entry[:content])
|
|
92
|
+
expect(parsed['dimensions']).to have_key('reliability')
|
|
93
|
+
expect(parsed['dimensions']).to have_key('competence')
|
|
94
|
+
expect(parsed['dimensions']).to have_key('integrity')
|
|
95
|
+
expect(parsed['dimensions']).to have_key('benevolence')
|
|
96
|
+
end
|
|
115
97
|
|
|
116
|
-
|
|
98
|
+
it 'entry content includes composite score' do
|
|
99
|
+
map.record_interaction('agent-001', positive: true)
|
|
100
|
+
entry = map.to_apollo_entries.first
|
|
101
|
+
parsed = JSON.parse(entry[:content])
|
|
102
|
+
expect(parsed['composite']).to be > 0.3
|
|
117
103
|
end
|
|
118
104
|
|
|
119
|
-
it '
|
|
120
|
-
map
|
|
121
|
-
map.
|
|
122
|
-
|
|
123
|
-
|
|
105
|
+
it 'entry content includes interaction counts' do
|
|
106
|
+
map.record_interaction('agent-001', positive: true)
|
|
107
|
+
entry = map.to_apollo_entries.first
|
|
108
|
+
parsed = JSON.parse(entry[:content])
|
|
109
|
+
expect(parsed['interaction_count']).to eq(1)
|
|
110
|
+
expect(parsed['positive_count']).to eq(1)
|
|
111
|
+
expect(parsed['negative_count']).to eq(0)
|
|
112
|
+
end
|
|
124
113
|
|
|
125
|
-
|
|
114
|
+
it 'entry tags include trust, trust_entry, agent_id, and domain' do
|
|
115
|
+
map.record_interaction('agent-001', positive: true, domain: :code)
|
|
116
|
+
entry = map.to_apollo_entries.first
|
|
117
|
+
expect(entry[:tags]).to include('trust', 'trust_entry', 'agent-001', 'code')
|
|
118
|
+
end
|
|
126
119
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
120
|
+
it 'entry tags include partner when BondRegistry reports partner' do
|
|
121
|
+
bond_registry = Module.new do
|
|
122
|
+
def self.partner?(agent_id)
|
|
123
|
+
agent_id == 'agent-001'
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
stub_const('Legion::Gaia::BondRegistry', bond_registry)
|
|
131
127
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
128
|
+
map.record_interaction('agent-001', positive: true)
|
|
129
|
+
entry = map.to_apollo_entries.first
|
|
130
|
+
expect(entry[:tags]).to include('partner')
|
|
131
|
+
end
|
|
135
132
|
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
it 'entry tags exclude partner for non-partner agents' do
|
|
134
|
+
bond_registry = Module.new do
|
|
135
|
+
def self.partner?(_agent_id)
|
|
136
|
+
false
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
stub_const('Legion::Gaia::BondRegistry', bond_registry)
|
|
140
|
+
|
|
141
|
+
map.record_interaction('agent-001', positive: true)
|
|
142
|
+
entry = map.to_apollo_entries.first
|
|
143
|
+
expect(entry[:tags]).not_to include('partner')
|
|
138
144
|
end
|
|
139
145
|
end
|
|
140
146
|
|
|
141
|
-
describe '
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
competence: 0.45,
|
|
148
|
-
integrity: 0.45,
|
|
149
|
-
benevolence: 0.45,
|
|
150
|
-
composite: 0.45,
|
|
151
|
-
interaction_count: 3,
|
|
152
|
-
positive_count: 3,
|
|
153
|
-
negative_count: 0,
|
|
154
|
-
created_at: Time.now.utc
|
|
155
|
-
)
|
|
147
|
+
describe '#from_apollo' do
|
|
148
|
+
let(:mock_store) do
|
|
149
|
+
double('ApolloLocal').tap do |store|
|
|
150
|
+
allow(store).to receive(:query).and_return({ success: false, results: [] })
|
|
151
|
+
end
|
|
152
|
+
end
|
|
156
153
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
expect(
|
|
160
|
-
expect(entry[:composite]).to be_within(0.0001).of(0.45)
|
|
161
|
-
expect(entry[:interaction_count]).to eq(3)
|
|
162
|
-
expect(entry[:positive_count]).to eq(3)
|
|
154
|
+
it 'returns false when store query fails or returns no results' do
|
|
155
|
+
result = map.from_apollo(store: mock_store)
|
|
156
|
+
expect(result).to be false
|
|
163
157
|
end
|
|
164
158
|
|
|
165
|
-
it '
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
interaction_count: 0,
|
|
175
|
-
positive_count: 0,
|
|
176
|
-
negative_count: 0,
|
|
177
|
-
created_at: Time.now.utc
|
|
159
|
+
it 'populates entries from stored JSON' do
|
|
160
|
+
content = JSON.dump({
|
|
161
|
+
agent_id: 'agent-001', domain: 'general',
|
|
162
|
+
dimensions: { reliability: 0.6, competence: 0.5, integrity: 0.4, benevolence: 0.3 },
|
|
163
|
+
composite: 0.45, interaction_count: 3, positive_count: 3, negative_count: 0,
|
|
164
|
+
last_interaction: Time.now.utc.iso8601, created_at: Time.now.utc.iso8601
|
|
165
|
+
})
|
|
166
|
+
allow(mock_store).to receive(:query).and_return(
|
|
167
|
+
{ success: true, results: [{ content: content, tags: %w[trust trust_entry agent-001 general] }] }
|
|
178
168
|
)
|
|
169
|
+
map.from_apollo(store: mock_store)
|
|
170
|
+
expect(map.get('agent-001')).not_to be_nil
|
|
171
|
+
end
|
|
179
172
|
|
|
180
|
-
|
|
173
|
+
it 'restores domain as a symbol' do
|
|
174
|
+
content = JSON.dump({
|
|
175
|
+
agent_id: 'agent-001', domain: 'code',
|
|
176
|
+
dimensions: { reliability: 0.3, competence: 0.3, integrity: 0.3, benevolence: 0.3 },
|
|
177
|
+
composite: 0.3, interaction_count: 0, positive_count: 0, negative_count: 0
|
|
178
|
+
})
|
|
179
|
+
allow(mock_store).to receive(:query).and_return(
|
|
180
|
+
{ success: true, results: [{ content: content, tags: %w[trust trust_entry agent-001 code] }] }
|
|
181
|
+
)
|
|
182
|
+
map.from_apollo(store: mock_store)
|
|
181
183
|
entry = map.get('agent-001', domain: :code)
|
|
182
184
|
expect(entry).not_to be_nil
|
|
183
185
|
expect(entry[:domain]).to eq(:code)
|
|
184
186
|
end
|
|
185
187
|
|
|
186
188
|
it 'restores all four dimension values' do
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
composite: 0.45,
|
|
195
|
-
interaction_count: 0,
|
|
196
|
-
positive_count: 0,
|
|
197
|
-
negative_count: 0,
|
|
198
|
-
created_at: Time.now.utc
|
|
189
|
+
content = JSON.dump({
|
|
190
|
+
agent_id: 'agent-001', domain: 'general',
|
|
191
|
+
dimensions: { reliability: 0.6, competence: 0.5, integrity: 0.4, benevolence: 0.3 },
|
|
192
|
+
composite: 0.45, interaction_count: 0, positive_count: 0, negative_count: 0
|
|
193
|
+
})
|
|
194
|
+
allow(mock_store).to receive(:query).and_return(
|
|
195
|
+
{ success: true, results: [{ content: content, tags: %w[trust trust_entry agent-001 general] }] }
|
|
199
196
|
)
|
|
200
|
-
|
|
201
|
-
map = Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new
|
|
197
|
+
map.from_apollo(store: mock_store)
|
|
202
198
|
entry = map.get('agent-001')
|
|
203
199
|
expect(entry[:dimensions][:reliability]).to be_within(0.0001).of(0.6)
|
|
204
200
|
expect(entry[:dimensions][:competence]).to be_within(0.0001).of(0.5)
|
|
@@ -207,64 +203,98 @@ RSpec.describe 'lex-trust local SQLite persistence' do
|
|
|
207
203
|
end
|
|
208
204
|
|
|
209
205
|
it 'restores multiple entries' do
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
206
|
+
content_a = JSON.dump({
|
|
207
|
+
agent_id: 'agent-a', domain: 'general',
|
|
208
|
+
dimensions: { reliability: 0.3, competence: 0.3, integrity: 0.3, benevolence: 0.3 },
|
|
209
|
+
composite: 0.3, interaction_count: 0, positive_count: 0, negative_count: 0
|
|
210
|
+
})
|
|
211
|
+
content_b = JSON.dump({
|
|
212
|
+
agent_id: 'agent-b', domain: 'ops',
|
|
213
|
+
dimensions: { reliability: 0.3, competence: 0.3, integrity: 0.3, benevolence: 0.3 },
|
|
214
|
+
composite: 0.3, interaction_count: 0, positive_count: 0, negative_count: 0
|
|
215
|
+
})
|
|
216
|
+
allow(mock_store).to receive(:query).and_return(
|
|
217
|
+
{ success: true, results: [
|
|
218
|
+
{ content: content_a, tags: %w[trust trust_entry agent-a general] },
|
|
219
|
+
{ content: content_b, tags: %w[trust trust_entry agent-b ops] }
|
|
220
|
+
] }
|
|
215
221
|
)
|
|
216
|
-
|
|
217
|
-
agent_id: 'agent-b', domain: 'ops',
|
|
218
|
-
reliability: 0.3, competence: 0.3, integrity: 0.3, benevolence: 0.3,
|
|
219
|
-
composite: 0.3, interaction_count: 0, positive_count: 0, negative_count: 0,
|
|
220
|
-
created_at: Time.now.utc
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
map = Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new
|
|
222
|
+
map.from_apollo(store: mock_store)
|
|
224
223
|
expect(map.count).to eq(2)
|
|
225
224
|
end
|
|
226
225
|
|
|
227
|
-
it '
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
composite: 0.4,
|
|
236
|
-
interaction_count: 2,
|
|
237
|
-
positive_count: 2,
|
|
238
|
-
negative_count: 0,
|
|
239
|
-
created_at: Time.now.utc
|
|
226
|
+
it 'restores interaction counts' do
|
|
227
|
+
content = JSON.dump({
|
|
228
|
+
agent_id: 'agent-001', domain: 'general',
|
|
229
|
+
dimensions: { reliability: 0.45, competence: 0.45, integrity: 0.45, benevolence: 0.45 },
|
|
230
|
+
composite: 0.45, interaction_count: 5, positive_count: 3, negative_count: 2
|
|
231
|
+
})
|
|
232
|
+
allow(mock_store).to receive(:query).and_return(
|
|
233
|
+
{ success: true, results: [{ content: content, tags: %w[trust trust_entry agent-001 general] }] }
|
|
240
234
|
)
|
|
235
|
+
map.from_apollo(store: mock_store)
|
|
236
|
+
entry = map.get('agent-001')
|
|
237
|
+
expect(entry[:interaction_count]).to eq(5)
|
|
238
|
+
expect(entry[:positive_count]).to eq(3)
|
|
239
|
+
expect(entry[:negative_count]).to eq(2)
|
|
240
|
+
end
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
it 'allows normal operations on restored entries' do
|
|
243
|
+
content = JSON.dump({
|
|
244
|
+
agent_id: 'agent-001', domain: 'general',
|
|
245
|
+
dimensions: { reliability: 0.4, competence: 0.4, integrity: 0.4, benevolence: 0.4 },
|
|
246
|
+
composite: 0.4, interaction_count: 2, positive_count: 2, negative_count: 0
|
|
247
|
+
})
|
|
248
|
+
allow(mock_store).to receive(:query).and_return(
|
|
249
|
+
{ success: true, results: [{ content: content, tags: %w[trust trust_entry agent-001 general] }] }
|
|
250
|
+
)
|
|
251
|
+
map.from_apollo(store: mock_store)
|
|
243
252
|
map.record_interaction('agent-001', positive: true)
|
|
244
253
|
entry = map.get('agent-001')
|
|
245
254
|
expect(entry[:interaction_count]).to eq(3)
|
|
246
255
|
expect(entry[:positive_count]).to eq(3)
|
|
247
256
|
end
|
|
257
|
+
|
|
258
|
+
it 'trusted_agents works correctly after hydration' do
|
|
259
|
+
content = JSON.dump({
|
|
260
|
+
agent_id: 'agent-001', domain: 'general',
|
|
261
|
+
dimensions: { reliability: 0.6, competence: 0.6, integrity: 0.6, benevolence: 0.6 },
|
|
262
|
+
composite: 0.6, interaction_count: 5, positive_count: 5, negative_count: 0
|
|
263
|
+
})
|
|
264
|
+
allow(mock_store).to receive(:query).and_return(
|
|
265
|
+
{ success: true, results: [{ content: content, tags: %w[trust trust_entry agent-001 general] }] }
|
|
266
|
+
)
|
|
267
|
+
map.from_apollo(store: mock_store)
|
|
268
|
+
trusted = map.trusted_agents
|
|
269
|
+
expect(trusted).not_to be_empty
|
|
270
|
+
expect(trusted.first[:agent_id]).to eq('agent-001')
|
|
271
|
+
end
|
|
248
272
|
end
|
|
249
273
|
|
|
250
|
-
describe 'round-trip
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
274
|
+
describe 'round-trip via to_apollo_entries / from_apollo' do
|
|
275
|
+
let(:mock_store) { double('ApolloLocal') }
|
|
276
|
+
|
|
277
|
+
it 'preserves trust scores across serialize/deserialize' do
|
|
278
|
+
5.times { map.record_interaction('agent-001', positive: true) }
|
|
279
|
+
composite_before = map.get('agent-001')[:composite]
|
|
280
|
+
|
|
281
|
+
entries = map.to_apollo_entries
|
|
282
|
+
allow(mock_store).to receive(:query).and_return({ success: true, results: entries })
|
|
256
283
|
|
|
257
|
-
map2 =
|
|
284
|
+
map2 = described_class.new
|
|
285
|
+
map2.from_apollo(store: mock_store)
|
|
258
286
|
expect(map2.get('agent-001')[:composite]).to be_within(0.0001).of(composite_before)
|
|
259
287
|
end
|
|
260
288
|
|
|
261
289
|
it 'round-trips interaction counts accurately' do
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
2.times { map1.record_interaction('agent-001', positive: false) }
|
|
265
|
-
map1.save_to_local
|
|
290
|
+
3.times { map.record_interaction('agent-001', positive: true) }
|
|
291
|
+
2.times { map.record_interaction('agent-001', positive: false) }
|
|
266
292
|
|
|
267
|
-
|
|
293
|
+
entries = map.to_apollo_entries
|
|
294
|
+
allow(mock_store).to receive(:query).and_return({ success: true, results: entries })
|
|
295
|
+
|
|
296
|
+
map2 = described_class.new
|
|
297
|
+
map2.from_apollo(store: mock_store)
|
|
268
298
|
entry = map2.get('agent-001')
|
|
269
299
|
expect(entry[:interaction_count]).to eq(5)
|
|
270
300
|
expect(entry[:positive_count]).to eq(3)
|
|
@@ -272,88 +302,45 @@ RSpec.describe 'lex-trust local SQLite persistence' do
|
|
|
272
302
|
end
|
|
273
303
|
|
|
274
304
|
it 'round-trips multiple agents independently' do
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
305
|
+
5.times { map.record_interaction('agent-high', positive: true) }
|
|
306
|
+
map.record_interaction('agent-low', positive: false)
|
|
307
|
+
|
|
308
|
+
entries = map.to_apollo_entries
|
|
309
|
+
allow(mock_store).to receive(:query).and_return({ success: true, results: entries })
|
|
279
310
|
|
|
280
|
-
map2 =
|
|
311
|
+
map2 = described_class.new
|
|
312
|
+
map2.from_apollo(store: mock_store)
|
|
281
313
|
expect(map2.get('agent-high')[:composite]).to be > map2.get('agent-low')[:composite]
|
|
282
314
|
end
|
|
283
315
|
|
|
284
316
|
it 'round-trips domain-scoped entries independently' do
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
map1.record_interaction('agent-001', positive: false, domain: :ops)
|
|
288
|
-
map1.save_to_local
|
|
317
|
+
5.times { map.record_interaction('agent-001', positive: true, domain: :code) }
|
|
318
|
+
map.record_interaction('agent-001', positive: false, domain: :ops)
|
|
289
319
|
|
|
290
|
-
|
|
320
|
+
entries = map.to_apollo_entries
|
|
321
|
+
allow(mock_store).to receive(:query).and_return({ success: true, results: entries })
|
|
322
|
+
|
|
323
|
+
map2 = described_class.new
|
|
324
|
+
map2.from_apollo(store: mock_store)
|
|
291
325
|
code_entry = map2.get('agent-001', domain: :code)
|
|
292
326
|
ops_entry = map2.get('agent-001', domain: :ops)
|
|
293
327
|
expect(code_entry[:composite]).to be > ops_entry[:composite]
|
|
294
328
|
end
|
|
295
329
|
|
|
296
|
-
it '
|
|
297
|
-
map1 = Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new
|
|
298
|
-
5.times { map1.record_interaction('agent-001', positive: true) }
|
|
299
|
-
map1.save_to_local
|
|
300
|
-
|
|
301
|
-
map2 = Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new
|
|
302
|
-
trusted = map2.trusted_agents
|
|
303
|
-
expect(trusted).not_to be_empty
|
|
304
|
-
expect(trusted.first[:agent_id]).to eq('agent-001')
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
describe 'graceful no-op when Legion::Data::Local is unavailable' do
|
|
309
|
-
before do
|
|
310
|
-
hide_const('Legion::Data::Local')
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
it 'initialize completes without raising' do
|
|
314
|
-
expect { Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new }.not_to raise_error
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
it 'save_to_local does nothing without raising' do
|
|
318
|
-
map = Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new
|
|
319
|
-
map.record_interaction('agent-001', positive: true)
|
|
320
|
-
expect { map.save_to_local }.not_to raise_error
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
it 'starts with empty in-memory state' do
|
|
324
|
-
map = Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new
|
|
325
|
-
expect(map.count).to eq(0)
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
it 'operates normally in memory when DB unavailable' do
|
|
329
|
-
map = Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new
|
|
330
|
+
it 'round-trips all four dimension values' do
|
|
330
331
|
map.record_interaction('agent-001', positive: true)
|
|
331
|
-
|
|
332
|
-
end
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
describe 'graceful no-op when Legion::Data::Local is defined but not connected' do
|
|
336
|
-
before do
|
|
337
|
-
stub_const('Legion::Data::Local', Module.new do
|
|
338
|
-
def self.connected?
|
|
339
|
-
false
|
|
340
|
-
end
|
|
341
|
-
end)
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
it 'initialize completes without raising' do
|
|
345
|
-
expect { Legion::Extensions::Agentic::Social::Trust::Helpers::TrustMap.new }.not_to raise_error
|
|
346
|
-
end
|
|
332
|
+
map.reinforce_dimension('agent-001', dimension: :competence, amount: 0.2)
|
|
347
333
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
map.record_interaction('agent-001', positive: true)
|
|
351
|
-
expect { map.save_to_local }.not_to raise_error
|
|
352
|
-
end
|
|
334
|
+
entries = map.to_apollo_entries
|
|
335
|
+
allow(mock_store).to receive(:query).and_return({ success: true, results: entries })
|
|
353
336
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
337
|
+
map2 = described_class.new
|
|
338
|
+
map2.from_apollo(store: mock_store)
|
|
339
|
+
original = map.get('agent-001')[:dimensions]
|
|
340
|
+
restored = map2.get('agent-001')[:dimensions]
|
|
341
|
+
Legion::Extensions::Agentic::Social::Trust::Helpers::TrustModel::TRUST_DIMENSIONS.each do |dim|
|
|
342
|
+
expect(restored[dim]).to be_within(0.0001).of(original[dim])
|
|
343
|
+
end
|
|
357
344
|
end
|
|
358
345
|
end
|
|
359
346
|
end
|