lex-mentalizing 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 15a3ade481ea7a8a8d77fc7ce5e21f3182e8fa37ad5895a61c8922988224eff2
4
+ data.tar.gz: 7aad6db5216975d38f4dae1b087cc8750412e8cac7a79fe0d36d23e7aaff8c6b
5
+ SHA512:
6
+ metadata.gz: 7a8d22b17f0fb84925452f67f7055e7e8a81ba8c53d822b59dadbd0f9934871ea5e4ccdcf45008555fda5a8cf32a7dd34039e350de7e7e821825f1dcd96a4c4f
7
+ data.tar.gz: 9bfd174741ef5e1f2195c7a6e761e7abca42665d059de3e309b8830f9f7622ef5246dd95e696bb4d9500d205edaba36078c06adaa768a684aede601dc48f6f34
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+
11
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/mentalizing/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-mentalizing'
7
+ spec.version = Legion::Extensions::Mentalizing::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Mentalizing'
12
+ spec.description = 'Second-order Theory of Mind for brain-modeled agentic AI. ' \
13
+ 'Recursive belief attribution, false-belief detection, and social alignment modeling.'
14
+ spec.homepage = 'https://github.com/LegionIO/lex-mentalizing'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>= 3.4'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-mentalizing'
20
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-mentalizing'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-mentalizing'
22
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-mentalizing/issues'
23
+ spec.metadata['rubygems_mfa_required'] = 'true'
24
+
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ Dir.glob('{lib,spec}/**/*') + %w[lex-mentalizing.gemspec Gemfile]
27
+ end
28
+ spec.require_paths = ['lib']
29
+ spec.add_development_dependency 'legion-gaia'
30
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/every'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Mentalizing
8
+ module Actor
9
+ class Decay < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::Mentalizing::Runners::Mentalizing
12
+ end
13
+
14
+ def runner_function
15
+ 'update_mentalizing'
16
+ end
17
+
18
+ def time
19
+ 300
20
+ end
21
+
22
+ def run_now?
23
+ false
24
+ end
25
+
26
+ def use_runner?
27
+ false
28
+ end
29
+
30
+ def check_subtask?
31
+ false
32
+ end
33
+
34
+ def generate_task?
35
+ false
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/mentalizing/helpers/constants'
4
+ require 'legion/extensions/mentalizing/helpers/belief_attribution'
5
+ require 'legion/extensions/mentalizing/helpers/mental_model'
6
+ require 'legion/extensions/mentalizing/runners/mentalizing'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Mentalizing
11
+ class Client
12
+ include Runners::Mentalizing
13
+
14
+ def initialize(**)
15
+ @mental_model = Helpers::MentalModel.new
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :mental_model
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mentalizing
6
+ module Helpers
7
+ class BeliefAttribution
8
+ attr_reader :id, :agent_id, :subject, :content, :depth, :about_agent_id, :created_at
9
+ attr_accessor :confidence
10
+
11
+ def initialize(agent_id:, subject:, content:, confidence:, depth: 0, about_agent_id: nil)
12
+ @id = SecureRandom.uuid
13
+ @agent_id = agent_id
14
+ @subject = subject
15
+ @content = content
16
+ @confidence = confidence.clamp(0.0, 1.0)
17
+ @depth = depth
18
+ @about_agent_id = about_agent_id
19
+ @created_at = Time.now.utc
20
+ end
21
+
22
+ def decay
23
+ @confidence = [@confidence - Constants::BELIEF_DECAY, Constants::BELIEF_FLOOR].max
24
+ end
25
+
26
+ def reinforce(amount: Constants::CONFIDENCE_ALPHA)
27
+ @confidence = [@confidence + amount, 1.0].min
28
+ end
29
+
30
+ def label
31
+ Constants::CONFIDENCE_LABELS.each do |range, lbl|
32
+ return lbl if range.cover?(@confidence)
33
+ end
34
+ :unknown
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ id: @id,
40
+ agent_id: @agent_id,
41
+ subject: @subject,
42
+ content: @content,
43
+ confidence: @confidence,
44
+ depth: @depth,
45
+ about_agent_id: @about_agent_id,
46
+ label: label,
47
+ created_at: @created_at
48
+ }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mentalizing
6
+ module Helpers
7
+ module Constants
8
+ MAX_AGENTS = 50
9
+ MAX_BELIEFS_PER_AGENT = 30
10
+ MAX_RECURSION_DEPTH = 4
11
+ BELIEF_DECAY = 0.02
12
+ BELIEF_FLOOR = 0.05
13
+ CONFIDENCE_ALPHA = 0.12
14
+ DEFAULT_CONFIDENCE = 0.3
15
+ MAX_HISTORY = 200
16
+ PROJECTION_DISCOUNT = 0.7
17
+
18
+ CONFIDENCE_LABELS = {
19
+ (0.8..) => :certain,
20
+ (0.6...0.8) => :confident,
21
+ (0.4...0.6) => :uncertain,
22
+ (0.2...0.4) => :speculative,
23
+ (..0.2) => :unknown
24
+ }.freeze
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mentalizing
6
+ module Helpers
7
+ class MentalModel
8
+ def initialize
9
+ @models = {}
10
+ end
11
+
12
+ def attribute_belief(agent_id:, subject:, content:, confidence:, depth: 0, about_agent_id: nil)
13
+ ensure_agent_capacity(agent_id)
14
+ capped_depth = [depth.to_i, Constants::MAX_RECURSION_DEPTH].min
15
+ belief = BeliefAttribution.new(
16
+ agent_id: agent_id,
17
+ subject: subject,
18
+ content: content,
19
+ confidence: confidence,
20
+ depth: capped_depth,
21
+ about_agent_id: about_agent_id
22
+ )
23
+ @models[agent_id] ||= []
24
+ @models[agent_id] << belief
25
+ prune_agent_beliefs(agent_id)
26
+ belief
27
+ end
28
+
29
+ def beliefs_for(agent_id:)
30
+ @models[agent_id] || []
31
+ end
32
+
33
+ def beliefs_about(about_agent_id:)
34
+ @models.values.flatten.select { |b| b.about_agent_id == about_agent_id }
35
+ end
36
+
37
+ def recursive_belief(agent_id:, about_agent_id:, subject:)
38
+ beliefs = beliefs_for(agent_id: agent_id)
39
+ beliefs.select { |b| b.about_agent_id == about_agent_id && b.subject == subject }
40
+ .max_by(&:confidence)
41
+ end
42
+
43
+ def project_self(subject:, own_belief:, other_agent_id:)
44
+ discounted = (own_belief * Constants::PROJECTION_DISCOUNT).clamp(0.0, 1.0)
45
+ attribute_belief(
46
+ agent_id: :self,
47
+ subject: subject,
48
+ content: "projected: #{other_agent_id} thinks I believe this",
49
+ confidence: discounted,
50
+ depth: 1,
51
+ about_agent_id: other_agent_id
52
+ )
53
+ end
54
+
55
+ def alignment(agent_a:, agent_b:, subject:)
56
+ beliefs_a = beliefs_on_subject(agent_a, subject)
57
+ beliefs_b = beliefs_on_subject(agent_b, subject)
58
+ return 0.0 if beliefs_a.empty? || beliefs_b.empty?
59
+
60
+ conf_a = beliefs_a.map(&:confidence).sum / beliefs_a.size
61
+ conf_b = beliefs_b.map(&:confidence).sum / beliefs_b.size
62
+ 1.0 - (conf_a - conf_b).abs
63
+ end
64
+
65
+ def detect_false_belief(agent_id:, subject:, reality:)
66
+ relevant = beliefs_for(agent_id: agent_id).select { |b| b.subject == subject }
67
+ return { false_belief: false, reason: :no_beliefs } if relevant.empty?
68
+
69
+ strongest = relevant.max_by(&:confidence)
70
+ false_belief = strongest.content != reality
71
+ {
72
+ false_belief: false_belief,
73
+ agent_id: agent_id,
74
+ subject: subject,
75
+ held_belief: strongest.content,
76
+ reality: reality,
77
+ confidence: strongest.confidence
78
+ }
79
+ end
80
+
81
+ def decay_all
82
+ @models.each_value { |beliefs| beliefs.each(&:decay) }
83
+ prune_expired
84
+ end
85
+
86
+ def remove_agent(agent_id:)
87
+ @models.delete(agent_id)
88
+ end
89
+
90
+ def agent_count
91
+ @models.size
92
+ end
93
+
94
+ def belief_count
95
+ @models.values.sum(&:size)
96
+ end
97
+
98
+ def to_h
99
+ @models.transform_values { |beliefs| beliefs.map(&:to_h) }
100
+ end
101
+
102
+ private
103
+
104
+ def beliefs_on_subject(agent_id, subject)
105
+ beliefs_for(agent_id: agent_id).select { |b| b.subject == subject }
106
+ end
107
+
108
+ def ensure_agent_capacity(agent_id)
109
+ return if @models.size < Constants::MAX_AGENTS
110
+ return if @models.key?(agent_id)
111
+
112
+ oldest_key = @models.keys.first
113
+ @models.delete(oldest_key)
114
+ end
115
+
116
+ def prune_agent_beliefs(agent_id)
117
+ list = @models[agent_id]
118
+ return unless list && list.size > Constants::MAX_BELIEFS_PER_AGENT
119
+
120
+ @models[agent_id] = list.sort_by(&:confidence).last(Constants::MAX_BELIEFS_PER_AGENT)
121
+ end
122
+
123
+ def prune_expired
124
+ @models.each_value do |beliefs|
125
+ beliefs.reject! { |b| b.confidence <= Constants::BELIEF_FLOOR }
126
+ end
127
+ @models.reject! { |_, v| v.empty? }
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mentalizing
6
+ module Runners
7
+ module Mentalizing
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def attribute_belief(agent_id:, subject:, content:, confidence: nil, depth: 0, about_agent_id: nil, **)
12
+ depth = [depth.to_i, Helpers::Constants::MAX_RECURSION_DEPTH].min
13
+ conf = confidence || Helpers::Constants::DEFAULT_CONFIDENCE
14
+ belief = mental_model.attribute_belief(
15
+ agent_id: agent_id,
16
+ subject: subject,
17
+ content: content,
18
+ confidence: conf.to_f,
19
+ depth: depth,
20
+ about_agent_id: about_agent_id
21
+ )
22
+ Legion::Logging.debug "[mentalizing] attribute agent=#{agent_id} subject=#{subject} depth=#{depth} conf=#{belief.confidence.round(2)}"
23
+ { attributed: true, belief: belief.to_h }
24
+ end
25
+
26
+ def project_belief(subject:, own_belief:, other_agent_id:, **)
27
+ belief = mental_model.project_self(subject: subject, own_belief: own_belief.to_f, other_agent_id: other_agent_id)
28
+ Legion::Logging.debug "[mentalizing] project subject=#{subject} other=#{other_agent_id} discounted_conf=#{belief.confidence.round(2)}"
29
+ { projected: true, belief: belief.to_h }
30
+ end
31
+
32
+ def check_alignment(agent_a:, agent_b:, subject:, **)
33
+ score = mental_model.alignment(agent_a: agent_a, agent_b: agent_b, subject: subject)
34
+ Legion::Logging.debug "[mentalizing] alignment agent_a=#{agent_a} agent_b=#{agent_b} subject=#{subject} score=#{score.round(2)}"
35
+ { agent_a: agent_a, agent_b: agent_b, subject: subject, alignment: score.round(4) }
36
+ end
37
+
38
+ def detect_false_belief(agent_id:, subject:, reality:, **)
39
+ result = mental_model.detect_false_belief(agent_id: agent_id, subject: subject, reality: reality)
40
+ Legion::Logging.info "[mentalizing] false_belief_check agent=#{agent_id} subject=#{subject} false=#{result[:false_belief]}"
41
+ result
42
+ end
43
+
44
+ def beliefs_for_agent(agent_id:, **)
45
+ beliefs = mental_model.beliefs_for(agent_id: agent_id)
46
+ Legion::Logging.debug "[mentalizing] beliefs_for agent=#{agent_id} count=#{beliefs.size}"
47
+ { agent_id: agent_id, beliefs: beliefs.map(&:to_h), count: beliefs.size }
48
+ end
49
+
50
+ def beliefs_about_agent(about_agent_id:, **)
51
+ beliefs = mental_model.beliefs_about(about_agent_id: about_agent_id)
52
+ Legion::Logging.debug "[mentalizing] beliefs_about about=#{about_agent_id} count=#{beliefs.size}"
53
+ { about_agent_id: about_agent_id, beliefs: beliefs.map(&:to_h), count: beliefs.size }
54
+ end
55
+
56
+ def recursive_belief_lookup(agent_id:, about_agent_id:, subject:, **)
57
+ belief = mental_model.recursive_belief(agent_id: agent_id, about_agent_id: about_agent_id, subject: subject)
58
+ if belief
59
+ Legion::Logging.debug "[mentalizing] recursive agent=#{agent_id} about=#{about_agent_id} subject=#{subject} found=true"
60
+ { found: true, belief: belief.to_h }
61
+ else
62
+ Legion::Logging.debug "[mentalizing] recursive agent=#{agent_id} about=#{about_agent_id} subject=#{subject} found=false"
63
+ { found: false, agent_id: agent_id, about_agent_id: about_agent_id, subject: subject }
64
+ end
65
+ end
66
+
67
+ def update_mentalizing(**)
68
+ mental_model.decay_all
69
+ Legion::Logging.debug "[mentalizing] decay cycle agents=#{mental_model.agent_count} beliefs=#{mental_model.belief_count}"
70
+ { decayed: true, agents: mental_model.agent_count, beliefs: mental_model.belief_count }
71
+ end
72
+
73
+ def mentalizing_stats(**)
74
+ {
75
+ agents: mental_model.agent_count,
76
+ beliefs: mental_model.belief_count
77
+ }
78
+ end
79
+
80
+ private
81
+
82
+ def mental_model
83
+ @mental_model ||= Helpers::MentalModel.new
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mentalizing
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'legion/extensions/mentalizing/version'
5
+ require 'legion/extensions/mentalizing/helpers/constants'
6
+ require 'legion/extensions/mentalizing/helpers/belief_attribution'
7
+ require 'legion/extensions/mentalizing/helpers/mental_model'
8
+ require 'legion/extensions/mentalizing/runners/mentalizing'
9
+ require 'legion/extensions/mentalizing/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module Mentalizing
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/mentalizing/client'
4
+
5
+ RSpec.describe Legion::Extensions::Mentalizing::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to all runner methods' do
9
+ expect(client).to respond_to(:attribute_belief)
10
+ expect(client).to respond_to(:project_belief)
11
+ expect(client).to respond_to(:check_alignment)
12
+ expect(client).to respond_to(:detect_false_belief)
13
+ expect(client).to respond_to(:beliefs_for_agent)
14
+ expect(client).to respond_to(:beliefs_about_agent)
15
+ expect(client).to respond_to(:recursive_belief_lookup)
16
+ expect(client).to respond_to(:update_mentalizing)
17
+ expect(client).to respond_to(:mentalizing_stats)
18
+ end
19
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Mentalizing::Helpers::BeliefAttribution do
4
+ subject(:belief) do
5
+ described_class.new(
6
+ agent_id: 'alice',
7
+ subject: 'weather',
8
+ content: 'it will rain',
9
+ confidence: 0.7,
10
+ depth: 1
11
+ )
12
+ end
13
+
14
+ describe '#initialize' do
15
+ it 'assigns fields correctly' do
16
+ expect(belief.agent_id).to eq('alice')
17
+ expect(belief.subject).to eq('weather')
18
+ expect(belief.content).to eq('it will rain')
19
+ expect(belief.depth).to eq(1)
20
+ expect(belief.about_agent_id).to be_nil
21
+ end
22
+
23
+ it 'clamps confidence to 0.0..1.0' do
24
+ over = described_class.new(agent_id: 'a', subject: 's', content: 'c', confidence: 1.5)
25
+ under = described_class.new(agent_id: 'a', subject: 's', content: 'c', confidence: -0.5)
26
+ expect(over.confidence).to eq(1.0)
27
+ expect(under.confidence).to eq(0.0)
28
+ end
29
+
30
+ it 'assigns a uuid id' do
31
+ expect(belief.id).to match(/\A[0-9a-f-]{36}\z/)
32
+ end
33
+
34
+ it 'records created_at' do
35
+ expect(belief.created_at).to be_a(Time)
36
+ end
37
+ end
38
+
39
+ describe '#decay' do
40
+ it 'reduces confidence by BELIEF_DECAY' do
41
+ before = belief.confidence
42
+ belief.decay
43
+ expect(belief.confidence).to be_within(0.001).of(before - Legion::Extensions::Mentalizing::Helpers::Constants::BELIEF_DECAY)
44
+ end
45
+
46
+ it 'does not drop below BELIEF_FLOOR' do
47
+ 20.times { belief.decay }
48
+ expect(belief.confidence).to be >= Legion::Extensions::Mentalizing::Helpers::Constants::BELIEF_FLOOR
49
+ end
50
+ end
51
+
52
+ describe '#reinforce' do
53
+ it 'increases confidence by CONFIDENCE_ALPHA by default' do
54
+ belief_low = described_class.new(agent_id: 'a', subject: 's', content: 'c', confidence: 0.3)
55
+ before = belief_low.confidence
56
+ belief_low.reinforce
57
+ expect(belief_low.confidence).to be_within(0.001).of(before + Legion::Extensions::Mentalizing::Helpers::Constants::CONFIDENCE_ALPHA)
58
+ end
59
+
60
+ it 'does not exceed 1.0' do
61
+ high = described_class.new(agent_id: 'a', subject: 's', content: 'c', confidence: 0.95)
62
+ high.reinforce(amount: 0.5)
63
+ expect(high.confidence).to eq(1.0)
64
+ end
65
+
66
+ it 'accepts custom amount' do
67
+ low = described_class.new(agent_id: 'a', subject: 's', content: 'c', confidence: 0.2)
68
+ low.reinforce(amount: 0.3)
69
+ expect(low.confidence).to be_within(0.001).of(0.5)
70
+ end
71
+ end
72
+
73
+ describe '#label' do
74
+ it 'returns :certain for high confidence' do
75
+ high = described_class.new(agent_id: 'a', subject: 's', content: 'c', confidence: 0.9)
76
+ expect(high.label).to eq(:certain)
77
+ end
78
+
79
+ it 'returns :confident for 0.6..0.8 range' do
80
+ mid = described_class.new(agent_id: 'a', subject: 's', content: 'c', confidence: 0.7)
81
+ expect(mid.label).to eq(:confident)
82
+ end
83
+
84
+ it 'returns :uncertain for 0.4..0.6 range' do
85
+ unc = described_class.new(agent_id: 'a', subject: 's', content: 'c', confidence: 0.5)
86
+ expect(unc.label).to eq(:uncertain)
87
+ end
88
+
89
+ it 'returns :speculative for 0.2..0.4 range' do
90
+ spec_b = described_class.new(agent_id: 'a', subject: 's', content: 'c', confidence: 0.3)
91
+ expect(spec_b.label).to eq(:speculative)
92
+ end
93
+
94
+ it 'returns :unknown for very low confidence' do
95
+ low = described_class.new(agent_id: 'a', subject: 's', content: 'c', confidence: 0.1)
96
+ expect(low.label).to eq(:unknown)
97
+ end
98
+ end
99
+
100
+ describe '#to_h' do
101
+ it 'returns a hash with all fields' do
102
+ h = belief.to_h
103
+ expect(h).to include(:id, :agent_id, :subject, :content, :confidence, :depth, :about_agent_id, :label, :created_at)
104
+ expect(h[:agent_id]).to eq('alice')
105
+ expect(h[:depth]).to eq(1)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Mentalizing::Helpers::MentalModel do
4
+ subject(:model) { described_class.new }
5
+
6
+ describe '#attribute_belief' do
7
+ it 'stores a belief and returns a BeliefAttribution' do
8
+ belief = model.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.8)
9
+ expect(belief).to be_a(Legion::Extensions::Mentalizing::Helpers::BeliefAttribution)
10
+ expect(belief.agent_id).to eq('alice')
11
+ end
12
+
13
+ it 'stores separate beliefs per agent' do
14
+ model.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.8)
15
+ model.attribute_belief(agent_id: 'bob', subject: 'rain', content: 'no', confidence: 0.6)
16
+ expect(model.beliefs_for(agent_id: 'alice').size).to eq(1)
17
+ expect(model.beliefs_for(agent_id: 'bob').size).to eq(1)
18
+ end
19
+
20
+ it 'caps depth at MAX_RECURSION_DEPTH' do
21
+ belief = model.attribute_belief(agent_id: 'alice', subject: 's', content: 'c', confidence: 0.5, depth: 99)
22
+ expect(belief.depth).to eq(Legion::Extensions::Mentalizing::Helpers::Constants::MAX_RECURSION_DEPTH)
23
+ end
24
+
25
+ it 'stores about_agent_id when provided' do
26
+ belief = model.attribute_belief(agent_id: 'alice', subject: 'trust', content: 'bob trusts me', confidence: 0.7, about_agent_id: 'bob')
27
+ expect(belief.about_agent_id).to eq('bob')
28
+ end
29
+ end
30
+
31
+ describe '#beliefs_for' do
32
+ it 'returns empty array for unknown agent' do
33
+ expect(model.beliefs_for(agent_id: 'nobody')).to eq([])
34
+ end
35
+
36
+ it 'returns all beliefs for known agent' do
37
+ model.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.8)
38
+ model.attribute_belief(agent_id: 'alice', subject: 'snow', content: 'no', confidence: 0.4)
39
+ expect(model.beliefs_for(agent_id: 'alice').size).to eq(2)
40
+ end
41
+ end
42
+
43
+ describe '#beliefs_about' do
44
+ it 'returns beliefs where about_agent_id matches' do
45
+ model.attribute_belief(agent_id: 'alice', subject: 'my_trust', content: 'low', confidence: 0.4, about_agent_id: 'me')
46
+ model.attribute_belief(agent_id: 'bob', subject: 'my_trust', content: 'high', confidence: 0.9, about_agent_id: 'me')
47
+ model.attribute_belief(agent_id: 'carol', subject: 'other', content: 'n/a', confidence: 0.5, about_agent_id: 'other')
48
+ result = model.beliefs_about(about_agent_id: 'me')
49
+ expect(result.size).to eq(2)
50
+ expect(result.map(&:agent_id)).to contain_exactly('alice', 'bob')
51
+ end
52
+
53
+ it 'returns empty array when no beliefs about agent' do
54
+ expect(model.beliefs_about(about_agent_id: 'nobody')).to eq([])
55
+ end
56
+ end
57
+
58
+ describe '#recursive_belief' do
59
+ it 'returns nil when no matching recursive belief exists' do
60
+ result = model.recursive_belief(agent_id: 'alice', about_agent_id: 'bob', subject: 'rain')
61
+ expect(result).to be_nil
62
+ end
63
+
64
+ it 'returns the highest-confidence matching belief' do
65
+ model.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'low', confidence: 0.4, about_agent_id: 'bob')
66
+ model.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'high', confidence: 0.9, about_agent_id: 'bob')
67
+ result = model.recursive_belief(agent_id: 'alice', about_agent_id: 'bob', subject: 'rain')
68
+ expect(result.content).to eq('high')
69
+ end
70
+ end
71
+
72
+ describe '#project_self' do
73
+ it 'creates a depth-1 belief attributed to :self' do
74
+ belief = model.project_self(subject: 'plan', own_belief: 0.8, other_agent_id: 'bob')
75
+ expect(belief.agent_id).to eq(:self)
76
+ expect(belief.depth).to eq(1)
77
+ expect(belief.about_agent_id).to eq('bob')
78
+ end
79
+
80
+ it 'discounts confidence by PROJECTION_DISCOUNT' do
81
+ discount = Legion::Extensions::Mentalizing::Helpers::Constants::PROJECTION_DISCOUNT
82
+ belief = model.project_self(subject: 'plan', own_belief: 1.0, other_agent_id: 'bob')
83
+ expect(belief.confidence).to be_within(0.001).of(discount)
84
+ end
85
+
86
+ it 'clamps discounted confidence to 1.0' do
87
+ belief = model.project_self(subject: 'plan', own_belief: 0.1, other_agent_id: 'bob')
88
+ expect(belief.confidence).to be >= 0.0
89
+ expect(belief.confidence).to be <= 1.0
90
+ end
91
+ end
92
+
93
+ describe '#alignment' do
94
+ it 'returns 0.0 when either agent has no beliefs on subject' do
95
+ model.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.8)
96
+ expect(model.alignment(agent_a: 'alice', agent_b: 'bob', subject: 'rain')).to eq(0.0)
97
+ end
98
+
99
+ it 'returns 1.0 when both agents have identical confidence' do
100
+ model.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.7)
101
+ model.attribute_belief(agent_id: 'bob', subject: 'rain', content: 'yes', confidence: 0.7)
102
+ expect(model.alignment(agent_a: 'alice', agent_b: 'bob', subject: 'rain')).to be_within(0.001).of(1.0)
103
+ end
104
+
105
+ it 'returns lower score for divergent beliefs' do
106
+ model.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'high', confidence: 0.9)
107
+ model.attribute_belief(agent_id: 'bob', subject: 'rain', content: 'low', confidence: 0.1)
108
+ score = model.alignment(agent_a: 'alice', agent_b: 'bob', subject: 'rain')
109
+ expect(score).to be < 0.5
110
+ end
111
+ end
112
+
113
+ describe '#detect_false_belief' do
114
+ it 'returns false_belief: false when belief matches reality' do
115
+ model.attribute_belief(agent_id: 'alice', subject: 'weather', content: 'sunny', confidence: 0.8)
116
+ result = model.detect_false_belief(agent_id: 'alice', subject: 'weather', reality: 'sunny')
117
+ expect(result[:false_belief]).to be false
118
+ end
119
+
120
+ it 'returns false_belief: true when belief contradicts reality' do
121
+ model.attribute_belief(agent_id: 'alice', subject: 'weather', content: 'sunny', confidence: 0.8)
122
+ result = model.detect_false_belief(agent_id: 'alice', subject: 'weather', reality: 'raining')
123
+ expect(result[:false_belief]).to be true
124
+ expect(result[:held_belief]).to eq('sunny')
125
+ expect(result[:reality]).to eq('raining')
126
+ end
127
+
128
+ it 'returns no_beliefs reason when agent has no beliefs on subject' do
129
+ result = model.detect_false_belief(agent_id: 'nobody', subject: 'weather', reality: 'sunny')
130
+ expect(result[:false_belief]).to be false
131
+ expect(result[:reason]).to eq(:no_beliefs)
132
+ end
133
+ end
134
+
135
+ describe '#decay_all' do
136
+ it 'reduces confidence on all beliefs' do
137
+ model.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.8)
138
+ before = model.beliefs_for(agent_id: 'alice').first.confidence
139
+ model.decay_all
140
+ after = model.beliefs_for(agent_id: 'alice').first&.confidence
141
+ expect(after).to be_nil.or be < before
142
+ end
143
+
144
+ it 'prunes beliefs at or below BELIEF_FLOOR' do
145
+ model.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes',
146
+ confidence: Legion::Extensions::Mentalizing::Helpers::Constants::BELIEF_FLOOR)
147
+ model.decay_all
148
+ expect(model.beliefs_for(agent_id: 'alice')).to be_empty
149
+ end
150
+ end
151
+
152
+ describe '#remove_agent' do
153
+ it 'removes all beliefs for the agent' do
154
+ model.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.8)
155
+ model.remove_agent(agent_id: 'alice')
156
+ expect(model.beliefs_for(agent_id: 'alice')).to eq([])
157
+ end
158
+ end
159
+
160
+ describe '#agent_count and #belief_count' do
161
+ it 'tracks counts correctly' do
162
+ expect(model.agent_count).to eq(0)
163
+ expect(model.belief_count).to eq(0)
164
+ model.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.8)
165
+ model.attribute_belief(agent_id: 'bob', subject: 'snow', content: 'no', confidence: 0.5)
166
+ expect(model.agent_count).to eq(2)
167
+ expect(model.belief_count).to eq(2)
168
+ end
169
+ end
170
+
171
+ describe '#to_h' do
172
+ it 'serializes all beliefs as hashes' do
173
+ model.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.8)
174
+ h = model.to_h
175
+ expect(h).to have_key('alice')
176
+ expect(h['alice'].first).to include(:agent_id, :subject, :confidence)
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/mentalizing/client'
4
+
5
+ RSpec.describe Legion::Extensions::Mentalizing::Runners::Mentalizing do
6
+ let(:client) { Legion::Extensions::Mentalizing::Client.new }
7
+
8
+ describe '#attribute_belief' do
9
+ it 'returns attributed: true with belief hash' do
10
+ result = client.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.8)
11
+ expect(result[:attributed]).to be true
12
+ expect(result[:belief]).to include(:id, :agent_id, :subject, :content, :confidence, :depth)
13
+ end
14
+
15
+ it 'defaults confidence to DEFAULT_CONFIDENCE when not provided' do
16
+ result = client.attribute_belief(agent_id: 'alice', subject: 'snow', content: 'maybe')
17
+ expect(result[:belief][:confidence]).to eq(Legion::Extensions::Mentalizing::Helpers::Constants::DEFAULT_CONFIDENCE)
18
+ end
19
+
20
+ it 'caps depth at MAX_RECURSION_DEPTH' do
21
+ result = client.attribute_belief(agent_id: 'alice', subject: 's', content: 'c', depth: 100)
22
+ expect(result[:belief][:depth]).to eq(Legion::Extensions::Mentalizing::Helpers::Constants::MAX_RECURSION_DEPTH)
23
+ end
24
+
25
+ it 'accepts depth 0 (first-order)' do
26
+ result = client.attribute_belief(agent_id: 'alice', subject: 'trust', content: 'high', confidence: 0.9, depth: 0)
27
+ expect(result[:belief][:depth]).to eq(0)
28
+ end
29
+
30
+ it 'stores about_agent_id for second-order beliefs' do
31
+ result = client.attribute_belief(agent_id: 'alice', subject: 'plan', content: 'I will act', confidence: 0.7,
32
+ depth: 1, about_agent_id: 'bob')
33
+ expect(result[:belief][:about_agent_id]).to eq('bob')
34
+ end
35
+ end
36
+
37
+ describe '#project_belief' do
38
+ it 'returns projected: true with discounted confidence' do
39
+ result = client.project_belief(subject: 'plan', own_belief: 0.8, other_agent_id: 'bob')
40
+ expect(result[:projected]).to be true
41
+ discount = Legion::Extensions::Mentalizing::Helpers::Constants::PROJECTION_DISCOUNT
42
+ expect(result[:belief][:confidence]).to be_within(0.001).of(0.8 * discount)
43
+ end
44
+
45
+ it 'creates depth-1 belief about the other agent' do
46
+ result = client.project_belief(subject: 'plan', own_belief: 1.0, other_agent_id: 'carol')
47
+ expect(result[:belief][:depth]).to eq(1)
48
+ expect(result[:belief][:about_agent_id]).to eq('carol')
49
+ end
50
+ end
51
+
52
+ describe '#check_alignment' do
53
+ it 'returns alignment score of 0.0 when no shared beliefs' do
54
+ result = client.check_alignment(agent_a: 'alice', agent_b: 'bob', subject: 'rain')
55
+ expect(result[:alignment]).to eq(0.0)
56
+ end
57
+
58
+ it 'returns alignment near 1.0 for identical confidences' do
59
+ client.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.7)
60
+ client.attribute_belief(agent_id: 'bob', subject: 'rain', content: 'yes', confidence: 0.7)
61
+ result = client.check_alignment(agent_a: 'alice', agent_b: 'bob', subject: 'rain')
62
+ expect(result[:alignment]).to be_within(0.001).of(1.0)
63
+ end
64
+
65
+ it 'returns subject in result' do
66
+ result = client.check_alignment(agent_a: 'alice', agent_b: 'bob', subject: 'rain')
67
+ expect(result[:subject]).to eq('rain')
68
+ end
69
+ end
70
+
71
+ describe '#detect_false_belief' do
72
+ it 'detects false belief when agent belief contradicts reality' do
73
+ client.attribute_belief(agent_id: 'alice', subject: 'weather', content: 'sunny', confidence: 0.9)
74
+ result = client.detect_false_belief(agent_id: 'alice', subject: 'weather', reality: 'raining')
75
+ expect(result[:false_belief]).to be true
76
+ expect(result[:held_belief]).to eq('sunny')
77
+ end
78
+
79
+ it 'returns false_belief: false when belief matches reality' do
80
+ client.attribute_belief(agent_id: 'alice', subject: 'weather', content: 'sunny', confidence: 0.9)
81
+ result = client.detect_false_belief(agent_id: 'alice', subject: 'weather', reality: 'sunny')
82
+ expect(result[:false_belief]).to be false
83
+ end
84
+
85
+ it 'returns no_beliefs reason when agent unknown' do
86
+ result = client.detect_false_belief(agent_id: 'nobody', subject: 'weather', reality: 'sunny')
87
+ expect(result[:reason]).to eq(:no_beliefs)
88
+ end
89
+ end
90
+
91
+ describe '#beliefs_for_agent' do
92
+ it 'returns empty beliefs for unknown agent' do
93
+ result = client.beliefs_for_agent(agent_id: 'nobody')
94
+ expect(result[:beliefs]).to eq([])
95
+ expect(result[:count]).to eq(0)
96
+ end
97
+
98
+ it 'returns beliefs for known agent' do
99
+ client.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.8)
100
+ result = client.beliefs_for_agent(agent_id: 'alice')
101
+ expect(result[:count]).to eq(1)
102
+ expect(result[:beliefs].first[:agent_id]).to eq('alice')
103
+ end
104
+ end
105
+
106
+ describe '#beliefs_about_agent' do
107
+ it 'returns beliefs where about_agent_id matches' do
108
+ client.attribute_belief(agent_id: 'alice', subject: 'my_reliability', content: 'high', confidence: 0.8, about_agent_id: 'me')
109
+ client.attribute_belief(agent_id: 'bob', subject: 'my_reliability', content: 'low', confidence: 0.3, about_agent_id: 'me')
110
+ result = client.beliefs_about_agent(about_agent_id: 'me')
111
+ expect(result[:count]).to eq(2)
112
+ end
113
+
114
+ it 'returns empty when no beliefs about agent' do
115
+ result = client.beliefs_about_agent(about_agent_id: 'nobody')
116
+ expect(result[:count]).to eq(0)
117
+ end
118
+ end
119
+
120
+ describe '#recursive_belief_lookup' do
121
+ it 'returns found: false when no match' do
122
+ result = client.recursive_belief_lookup(agent_id: 'alice', about_agent_id: 'bob', subject: 'rain')
123
+ expect(result[:found]).to be false
124
+ end
125
+
126
+ it 'returns found: true with belief when match exists' do
127
+ client.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'bob thinks rain', confidence: 0.75, about_agent_id: 'bob')
128
+ result = client.recursive_belief_lookup(agent_id: 'alice', about_agent_id: 'bob', subject: 'rain')
129
+ expect(result[:found]).to be true
130
+ expect(result[:belief][:content]).to eq('bob thinks rain')
131
+ end
132
+ end
133
+
134
+ describe '#update_mentalizing' do
135
+ it 'returns decayed: true' do
136
+ result = client.update_mentalizing
137
+ expect(result[:decayed]).to be true
138
+ end
139
+
140
+ it 'returns agent and belief counts' do
141
+ client.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.8)
142
+ result = client.update_mentalizing
143
+ expect(result).to have_key(:agents)
144
+ expect(result).to have_key(:beliefs)
145
+ end
146
+ end
147
+
148
+ describe '#mentalizing_stats' do
149
+ it 'returns agents and beliefs counts' do
150
+ result = client.mentalizing_stats
151
+ expect(result).to include(:agents, :beliefs)
152
+ end
153
+
154
+ it 'reflects current state' do
155
+ client.attribute_belief(agent_id: 'alice', subject: 'rain', content: 'yes', confidence: 0.8)
156
+ client.attribute_belief(agent_id: 'bob', subject: 'snow', content: 'no', confidence: 0.5)
157
+ result = client.mentalizing_stats
158
+ expect(result[:agents]).to eq(2)
159
+ expect(result[:beliefs]).to eq(2)
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ module Legion
6
+ module Logging
7
+ def self.debug(_msg); end
8
+ def self.info(_msg); end
9
+ def self.warn(_msg); end
10
+ def self.error(_msg); end
11
+ end
12
+ end
13
+
14
+ require 'legion/extensions/mentalizing'
15
+
16
+ RSpec.configure do |config|
17
+ config.example_status_persistence_file_path = '.rspec_status'
18
+ config.disable_monkey_patching!
19
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
20
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-mentalizing
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Esity
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-gaia
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: Second-order Theory of Mind for brain-modeled agentic AI. Recursive belief
27
+ attribution, false-belief detection, and social alignment modeling.
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - lex-mentalizing.gemspec
36
+ - lib/legion/extensions/mentalizing.rb
37
+ - lib/legion/extensions/mentalizing/actors/decay.rb
38
+ - lib/legion/extensions/mentalizing/client.rb
39
+ - lib/legion/extensions/mentalizing/helpers/belief_attribution.rb
40
+ - lib/legion/extensions/mentalizing/helpers/constants.rb
41
+ - lib/legion/extensions/mentalizing/helpers/mental_model.rb
42
+ - lib/legion/extensions/mentalizing/runners/mentalizing.rb
43
+ - lib/legion/extensions/mentalizing/version.rb
44
+ - spec/legion/extensions/mentalizing/client_spec.rb
45
+ - spec/legion/extensions/mentalizing/helpers/belief_attribution_spec.rb
46
+ - spec/legion/extensions/mentalizing/helpers/mental_model_spec.rb
47
+ - spec/legion/extensions/mentalizing/runners/mentalizing_spec.rb
48
+ - spec/spec_helper.rb
49
+ homepage: https://github.com/LegionIO/lex-mentalizing
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ homepage_uri: https://github.com/LegionIO/lex-mentalizing
54
+ source_code_uri: https://github.com/LegionIO/lex-mentalizing
55
+ documentation_uri: https://github.com/LegionIO/lex-mentalizing
56
+ changelog_uri: https://github.com/LegionIO/lex-mentalizing
57
+ bug_tracker_uri: https://github.com/LegionIO/lex-mentalizing/issues
58
+ rubygems_mfa_required: 'true'
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '3.4'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.6.9
74
+ specification_version: 4
75
+ summary: LEX Mentalizing
76
+ test_files: []