lex-mirror 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: c3e497d574566a1b283837eff61746dacf6be493caaf2720ccb74c7ce8d6b332
4
+ data.tar.gz: a13b1c4de5ff079a611b9933196cc375e70cf4c78cab9e484513ea0f01599d02
5
+ SHA512:
6
+ metadata.gz: 64e7a587ef7c672da68005abdbf9b52fdb7f68fe1cc6191d9df2728f56677f02c63c5a9a9c771d1a9ea810b9c81ab11cc6446c893f91d8437e6bee6a3db69853
7
+ data.tar.gz: 8c02d2159b5d3b7bbc97ff75043703b165aa974b5877723606993a56f658a4132dd0b0fe0698621418d664ce6cc1200db125a8817298bf741ee45f9cd1816297
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,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/mirror/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-mirror'
7
+ spec.version = Legion::Extensions::Mirror::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Mirror'
12
+ spec.description = 'Mirror neuron system for brain-modeled agentic AI — automatic behavioral ' \
13
+ 'observation, imitation learning with fidelity tracking, and empathic ' \
14
+ 'resonance from watching other agents act.'
15
+ spec.homepage = 'https://github.com/LegionIO/lex-mirror'
16
+ spec.license = 'MIT'
17
+ spec.required_ruby_version = '>= 3.4'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-mirror'
21
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-mirror'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-mirror'
23
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-mirror/issues'
24
+ spec.metadata['rubygems_mfa_required'] = 'true'
25
+
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ Dir.glob('{lib,spec}/**/*') + %w[lex-mirror.gemspec Gemfile]
28
+ end
29
+ spec.require_paths = ['lib']
30
+ spec.add_development_dependency 'legion-gaia'
31
+ 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 Mirror
8
+ module Actor
9
+ class Decay < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::Mirror::Runners::Mirror
12
+ end
13
+
14
+ def runner_function
15
+ 'update_mirror'
16
+ end
17
+
18
+ def time
19
+ 120
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/mirror/helpers/constants'
4
+ require 'legion/extensions/mirror/helpers/observed_behavior'
5
+ require 'legion/extensions/mirror/helpers/mirror_system'
6
+ require 'legion/extensions/mirror/runners/mirror'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Mirror
11
+ class Client
12
+ include Runners::Mirror
13
+
14
+ def initialize(mirror_system: nil, **)
15
+ @mirror_system = mirror_system || Helpers::MirrorSystem.new
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :mirror_system
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mirror
6
+ module Helpers
7
+ module Constants
8
+ # Maximum observed behaviors to store
9
+ MAX_OBSERVATIONS = 200
10
+
11
+ # Maximum imitation candidates per domain
12
+ MAX_IMITATIONS = 50
13
+
14
+ # How quickly observed behaviors are internalized
15
+ MIRROR_ALPHA = 0.15
16
+
17
+ # Default resonance strength for new observations
18
+ DEFAULT_RESONANCE = 0.3
19
+
20
+ # Minimum resonance to keep an observation
21
+ RESONANCE_FLOOR = 0.05
22
+
23
+ # Resonance decay per tick
24
+ RESONANCE_DECAY = 0.01
25
+
26
+ # Boost when observation matches own repertoire
27
+ FAMILIARITY_BOOST = 0.2
28
+
29
+ # Boost from repeated observation of same behavior
30
+ REPETITION_BOOST = 0.1
31
+
32
+ # Maximum resonance value
33
+ MAX_RESONANCE = 1.0
34
+
35
+ # Imitation fidelity: how accurately behaviors are copied (0..1)
36
+ DEFAULT_FIDELITY = 0.7
37
+
38
+ # Fidelity improvement per successful imitation
39
+ FIDELITY_LEARNING_RATE = 0.05
40
+
41
+ # Maximum agents to mirror simultaneously
42
+ MAX_MIRRORED_AGENTS = 20
43
+
44
+ # Maximum behaviors in own repertoire
45
+ MAX_REPERTOIRE = 100
46
+
47
+ RESONANCE_LABELS = {
48
+ (0.8..) => :strong_mirror,
49
+ (0.6...0.8) => :resonating,
50
+ (0.4...0.6) => :echoing,
51
+ (0.2...0.4) => :faint,
52
+ (..0.2) => :silent
53
+ }.freeze
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mirror
6
+ module Helpers
7
+ class MirrorSystem
8
+ include Constants
9
+
10
+ attr_reader :observations, :repertoire, :fidelity_scores, :imitation_history
11
+
12
+ def initialize
13
+ @observations = {}
14
+ @repertoire = {}
15
+ @fidelity_scores = {}
16
+ @imitation_history = []
17
+ end
18
+
19
+ def observe(agent_id:, action:, domain:, context: nil, outcome: nil)
20
+ key = observation_key(agent_id, action, domain)
21
+ if @observations.key?(key)
22
+ @observations[key].observe_again
23
+ @observations[key]
24
+ else
25
+ ensure_observation_capacity
26
+ obs = ObservedBehavior.new(
27
+ agent_id: agent_id,
28
+ action: action,
29
+ domain: domain,
30
+ context: context,
31
+ outcome: outcome
32
+ )
33
+ obs.boost_familiarity if repertoire_includes?(action, domain)
34
+ @observations[key] = obs
35
+ end
36
+ end
37
+
38
+ def imitate(action:, domain:, source_agent: nil)
39
+ candidates = if source_agent
40
+ observations_for_agent(source_agent).select { |o| o.action == action && o.domain == domain }
41
+ else
42
+ @observations.values.select { |o| o.action == action && o.domain == domain }
43
+ end
44
+ return nil if candidates.empty?
45
+
46
+ best = candidates.max_by(&:resonance)
47
+ fidelity = fidelity_for(action, domain)
48
+ add_to_repertoire(action, domain, fidelity)
49
+ record_imitation(action, domain, source_agent, fidelity)
50
+ { observation: best, fidelity: fidelity }
51
+ end
52
+
53
+ def add_to_repertoire(action, domain, fidelity = DEFAULT_FIDELITY)
54
+ key = repertoire_key(action, domain)
55
+ if @repertoire.key?(key)
56
+ old = @repertoire[key][:fidelity]
57
+ @repertoire[key][:fidelity] = ema(old, fidelity, MIRROR_ALPHA)
58
+ @repertoire[key][:practice_count] += 1
59
+ else
60
+ trim_repertoire if @repertoire.size >= MAX_REPERTOIRE
61
+ @repertoire[key] = { action: action, domain: domain, fidelity: fidelity, practice_count: 1 }
62
+ end
63
+ end
64
+
65
+ def update_fidelity(action:, domain:, success:)
66
+ key = repertoire_key(action, domain)
67
+ @fidelity_scores[key] ||= DEFAULT_FIDELITY
68
+ delta = success ? FIDELITY_LEARNING_RATE : -FIDELITY_LEARNING_RATE
69
+ @fidelity_scores[key] = (@fidelity_scores[key] + delta).clamp(0.0, 1.0)
70
+ end
71
+
72
+ def fidelity_for(action, domain)
73
+ @fidelity_scores.fetch(repertoire_key(action, domain), DEFAULT_FIDELITY)
74
+ end
75
+
76
+ def observations_for_agent(agent_id)
77
+ @observations.values.select { |o| o.agent_id == agent_id }
78
+ end
79
+
80
+ def observations_in_domain(domain)
81
+ @observations.values.select { |o| o.domain == domain }
82
+ end
83
+
84
+ def strongest_mirrors(count = 5)
85
+ @observations.values.sort_by { |o| -o.resonance }.first(count)
86
+ end
87
+
88
+ def repertoire_includes?(action, domain)
89
+ @repertoire.key?(repertoire_key(action, domain))
90
+ end
91
+
92
+ def decay_all
93
+ @observations.each_value(&:decay)
94
+ @observations.reject! { |_, o| o.faded? }
95
+ end
96
+
97
+ def observation_count
98
+ @observations.size
99
+ end
100
+
101
+ def repertoire_size
102
+ @repertoire.size
103
+ end
104
+
105
+ def mirrored_agents
106
+ @observations.values.map(&:agent_id).uniq
107
+ end
108
+
109
+ def to_h
110
+ {
111
+ observations: observation_count,
112
+ repertoire_size: repertoire_size,
113
+ mirrored_agents: mirrored_agents.size,
114
+ history_size: @imitation_history.size
115
+ }
116
+ end
117
+
118
+ private
119
+
120
+ def observation_key(agent_id, action, domain)
121
+ :"#{agent_id}:#{action}:#{domain}"
122
+ end
123
+
124
+ def repertoire_key(action, domain)
125
+ :"#{action}:#{domain}"
126
+ end
127
+
128
+ def ema(old_val, new_val, alpha)
129
+ old_val + (alpha * (new_val - old_val))
130
+ end
131
+
132
+ def ensure_observation_capacity
133
+ return if @observations.size < MAX_OBSERVATIONS
134
+
135
+ weakest = @observations.min_by { |_, o| o.resonance }
136
+ @observations.delete(weakest.first) if weakest
137
+ end
138
+
139
+ def trim_repertoire
140
+ weakest = @repertoire.min_by { |_, r| r[:fidelity] }
141
+ @repertoire.delete(weakest.first) if weakest
142
+ end
143
+
144
+ def record_imitation(action, domain, source_agent, fidelity)
145
+ @imitation_history << {
146
+ action: action,
147
+ domain: domain,
148
+ source_agent: source_agent,
149
+ fidelity: fidelity,
150
+ at: Time.now.utc
151
+ }
152
+ @imitation_history.shift while @imitation_history.size > MAX_OBSERVATIONS
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mirror
6
+ module Helpers
7
+ class ObservedBehavior
8
+ attr_reader :id, :agent_id, :action, :domain, :context, :outcome, :created_at
9
+ attr_accessor :resonance, :observation_count
10
+
11
+ def initialize(agent_id:, action:, domain:, context: nil, outcome: nil, resonance: nil)
12
+ @id = SecureRandom.uuid
13
+ @agent_id = agent_id
14
+ @action = action
15
+ @domain = domain
16
+ @context = context
17
+ @outcome = outcome
18
+ @resonance = (resonance || Constants::DEFAULT_RESONANCE).clamp(0.0, Constants::MAX_RESONANCE)
19
+ @observation_count = 1
20
+ @created_at = Time.now.utc
21
+ end
22
+
23
+ def observe_again
24
+ @observation_count += 1
25
+ @resonance = [@resonance + Constants::REPETITION_BOOST, Constants::MAX_RESONANCE].min
26
+ end
27
+
28
+ def boost_familiarity
29
+ @resonance = [@resonance + Constants::FAMILIARITY_BOOST, Constants::MAX_RESONANCE].min
30
+ end
31
+
32
+ def decay
33
+ @resonance = [@resonance - Constants::RESONANCE_DECAY, Constants::RESONANCE_FLOOR].max
34
+ end
35
+
36
+ def faded?
37
+ @resonance <= Constants::RESONANCE_FLOOR
38
+ end
39
+
40
+ def label
41
+ Constants::RESONANCE_LABELS.each { |range, lbl| return lbl if range.cover?(@resonance) }
42
+ :silent
43
+ end
44
+
45
+ def to_h
46
+ {
47
+ id: @id,
48
+ agent_id: @agent_id,
49
+ action: @action,
50
+ domain: @domain,
51
+ context: @context,
52
+ outcome: @outcome,
53
+ resonance: @resonance,
54
+ observation_count: @observation_count,
55
+ label: label,
56
+ created_at: @created_at
57
+ }
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mirror
6
+ module Runners
7
+ module Mirror
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def observe_behavior(agent_id:, action:, domain:, context: nil, outcome: nil, **)
12
+ obs = mirror_system.observe(agent_id: agent_id, action: action, domain: domain, context: context, outcome: outcome)
13
+ Legion::Logging.debug "[mirror] observe: agent=#{agent_id} action=#{action} domain=#{domain} " \
14
+ "resonance=#{obs.resonance.round(3)} count=#{obs.observation_count}"
15
+ { success: true, observation: obs.to_h }
16
+ end
17
+
18
+ def imitate_behavior(action:, domain:, source_agent: nil, **)
19
+ result = mirror_system.imitate(action: action, domain: domain, source_agent: source_agent)
20
+ if result
21
+ Legion::Logging.debug "[mirror] imitate: action=#{action} domain=#{domain} " \
22
+ "fidelity=#{result[:fidelity].round(3)}"
23
+ {
24
+ success: true,
25
+ imitated: true,
26
+ action: action,
27
+ domain: domain,
28
+ fidelity: result[:fidelity].round(4),
29
+ source: result[:observation].agent_id,
30
+ resonance: result[:observation].resonance.round(4)
31
+ }
32
+ else
33
+ Legion::Logging.debug "[mirror] imitate: action=#{action} domain=#{domain} no_observation"
34
+ { success: true, imitated: false, action: action, domain: domain }
35
+ end
36
+ end
37
+
38
+ def report_imitation_outcome(action:, domain:, success_flag:, **)
39
+ mirror_system.update_fidelity(action: action, domain: domain, success: success_flag)
40
+ fidelity = mirror_system.fidelity_for(action, domain)
41
+ Legion::Logging.debug "[mirror] outcome: action=#{action} domain=#{domain} " \
42
+ "success=#{success_flag} fidelity=#{fidelity.round(3)}"
43
+ { success: true, action: action, domain: domain, fidelity: fidelity.round(4) }
44
+ end
45
+
46
+ def strongest_mirrors(count: 5, **)
47
+ mirrors = mirror_system.strongest_mirrors(count)
48
+ Legion::Logging.debug "[mirror] strongest: count=#{mirrors.size}"
49
+ { success: true, mirrors: mirrors.map(&:to_h), count: mirrors.size }
50
+ end
51
+
52
+ def observations_for(agent_id:, **)
53
+ observations = mirror_system.observations_for_agent(agent_id)
54
+ Legion::Logging.debug "[mirror] observations_for: agent=#{agent_id} count=#{observations.size}"
55
+ { success: true, agent_id: agent_id, observations: observations.map(&:to_h), count: observations.size }
56
+ end
57
+
58
+ def observations_in(domain:, **)
59
+ observations = mirror_system.observations_in_domain(domain)
60
+ Legion::Logging.debug "[mirror] observations_in: domain=#{domain} count=#{observations.size}"
61
+ { success: true, domain: domain, observations: observations.map(&:to_h), count: observations.size }
62
+ end
63
+
64
+ def repertoire_status(**)
65
+ rep = mirror_system.repertoire
66
+ Legion::Logging.debug "[mirror] repertoire: size=#{rep.size}"
67
+ { success: true, repertoire: rep.values, size: rep.size }
68
+ end
69
+
70
+ def update_mirror(**)
71
+ mirror_system.decay_all
72
+ Legion::Logging.debug "[mirror] tick: observations=#{mirror_system.observation_count} " \
73
+ "repertoire=#{mirror_system.repertoire_size} agents=#{mirror_system.mirrored_agents.size}"
74
+ {
75
+ success: true,
76
+ observations: mirror_system.observation_count,
77
+ repertoire: mirror_system.repertoire_size,
78
+ agents: mirror_system.mirrored_agents.size
79
+ }
80
+ end
81
+
82
+ def mirror_stats(**)
83
+ { success: true, stats: mirror_system.to_h }
84
+ end
85
+
86
+ private
87
+
88
+ def mirror_system
89
+ @mirror_system ||= Helpers::MirrorSystem.new
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mirror
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/mirror/version'
5
+ require 'legion/extensions/mirror/helpers/constants'
6
+ require 'legion/extensions/mirror/helpers/observed_behavior'
7
+ require 'legion/extensions/mirror/helpers/mirror_system'
8
+ require 'legion/extensions/mirror/runners/mirror'
9
+ require 'legion/extensions/mirror/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module Mirror
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Mirror::Client do
4
+ subject(:client) { described_class.new }
5
+
6
+ it 'includes Runners::Mirror' do
7
+ expect(described_class.ancestors).to include(Legion::Extensions::Mirror::Runners::Mirror)
8
+ end
9
+
10
+ it 'responds to all runner methods' do
11
+ expect(client).to respond_to(:observe_behavior, :imitate_behavior, :report_imitation_outcome)
12
+ expect(client).to respond_to(:strongest_mirrors, :observations_for, :observations_in)
13
+ expect(client).to respond_to(:repertoire_status, :update_mirror, :mirror_stats)
14
+ end
15
+
16
+ it 'supports full mirror lifecycle' do
17
+ # Observe another agent deploying successfully
18
+ client.observe_behavior(agent_id: 'expert', action: :canary_deploy, domain: :production, outcome: :success)
19
+ client.observe_behavior(agent_id: 'expert', action: :canary_deploy, domain: :production, outcome: :success)
20
+
21
+ # Imitate the behavior
22
+ result = client.imitate_behavior(action: :canary_deploy, domain: :production)
23
+ expect(result[:imitated]).to be true
24
+ expect(result[:source]).to eq('expert')
25
+
26
+ # Report outcome
27
+ client.report_imitation_outcome(action: :canary_deploy, domain: :production, success_flag: true)
28
+
29
+ # Check repertoire
30
+ rep = client.repertoire_status
31
+ expect(rep[:size]).to eq(1)
32
+
33
+ # Tick
34
+ client.update_mirror
35
+
36
+ # Stats
37
+ stats = client.mirror_stats
38
+ expect(stats[:stats][:mirrored_agents]).to eq(1)
39
+ end
40
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Mirror::Helpers::MirrorSystem do
4
+ subject(:system) { described_class.new }
5
+
6
+ describe '#observe' do
7
+ it 'creates a new observation' do
8
+ obs = system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
9
+ expect(obs).to be_a(Legion::Extensions::Mirror::Helpers::ObservedBehavior)
10
+ expect(system.observation_count).to eq(1)
11
+ end
12
+
13
+ it 'increments count on repeated observation' do
14
+ system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
15
+ obs = system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
16
+ expect(obs.observation_count).to eq(2)
17
+ expect(system.observation_count).to eq(1)
18
+ end
19
+
20
+ it 'boosts familiarity when behavior is in repertoire' do
21
+ system.add_to_repertoire(:deploy, :infra)
22
+ obs = system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
23
+ default = Legion::Extensions::Mirror::Helpers::Constants::DEFAULT_RESONANCE
24
+ boost = Legion::Extensions::Mirror::Helpers::Constants::FAMILIARITY_BOOST
25
+ expect(obs.resonance).to be_within(0.001).of(default + boost)
26
+ end
27
+
28
+ it 'tracks separate observations per agent' do
29
+ system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
30
+ system.observe(agent_id: 'bob', action: :deploy, domain: :infra)
31
+ expect(system.observation_count).to eq(2)
32
+ end
33
+ end
34
+
35
+ describe '#imitate' do
36
+ it 'returns nil when no observations exist' do
37
+ expect(system.imitate(action: :deploy, domain: :infra)).to be_nil
38
+ end
39
+
40
+ it 'imitates observed behavior and adds to repertoire' do
41
+ system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
42
+ result = system.imitate(action: :deploy, domain: :infra)
43
+ expect(result).not_to be_nil
44
+ expect(result[:observation].agent_id).to eq('alice')
45
+ expect(result[:fidelity]).to be_a(Float)
46
+ expect(system.repertoire_includes?(:deploy, :infra)).to be true
47
+ end
48
+
49
+ it 'filters by source_agent when specified' do
50
+ system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
51
+ system.observe(agent_id: 'bob', action: :deploy, domain: :infra)
52
+ result = system.imitate(action: :deploy, domain: :infra, source_agent: 'alice')
53
+ expect(result[:observation].agent_id).to eq('alice')
54
+ end
55
+
56
+ it 'picks highest resonance observation' do
57
+ system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
58
+ obs_bob = system.observe(agent_id: 'bob', action: :deploy, domain: :infra)
59
+ obs_bob.resonance = 0.9
60
+ result = system.imitate(action: :deploy, domain: :infra)
61
+ expect(result[:observation].agent_id).to eq('bob')
62
+ end
63
+ end
64
+
65
+ describe '#update_fidelity' do
66
+ it 'increases fidelity on success' do
67
+ before = system.fidelity_for(:deploy, :infra)
68
+ system.update_fidelity(action: :deploy, domain: :infra, success: true)
69
+ expect(system.fidelity_for(:deploy, :infra)).to be > before
70
+ end
71
+
72
+ it 'decreases fidelity on failure' do
73
+ before = system.fidelity_for(:deploy, :infra)
74
+ system.update_fidelity(action: :deploy, domain: :infra, success: false)
75
+ expect(system.fidelity_for(:deploy, :infra)).to be < before
76
+ end
77
+
78
+ it 'clamps to 0..1' do
79
+ 20.times { system.update_fidelity(action: :deploy, domain: :infra, success: true) }
80
+ expect(system.fidelity_for(:deploy, :infra)).to be <= 1.0
81
+ end
82
+ end
83
+
84
+ describe '#observations_for_agent' do
85
+ it 'returns observations by specific agent' do
86
+ system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
87
+ system.observe(agent_id: 'alice', action: :test, domain: :infra)
88
+ system.observe(agent_id: 'bob', action: :deploy, domain: :infra)
89
+ expect(system.observations_for_agent('alice').size).to eq(2)
90
+ end
91
+ end
92
+
93
+ describe '#observations_in_domain' do
94
+ it 'returns observations in domain' do
95
+ system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
96
+ system.observe(agent_id: 'alice', action: :test, domain: :code)
97
+ expect(system.observations_in_domain(:infra).size).to eq(1)
98
+ end
99
+ end
100
+
101
+ describe '#strongest_mirrors' do
102
+ it 'returns top N by resonance' do
103
+ system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
104
+ obs = system.observe(agent_id: 'bob', action: :test, domain: :code)
105
+ obs.resonance = 0.9
106
+ top = system.strongest_mirrors(1)
107
+ expect(top.size).to eq(1)
108
+ expect(top.first.agent_id).to eq('bob')
109
+ end
110
+ end
111
+
112
+ describe '#decay_all' do
113
+ it 'decays all observations' do
114
+ system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
115
+ before = system.observations.values.first.resonance
116
+ system.decay_all
117
+ after = system.observations.values.first&.resonance
118
+ expect(after).to be < before if after
119
+ end
120
+
121
+ it 'prunes faded observations' do
122
+ obs = system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
123
+ obs.resonance = Legion::Extensions::Mirror::Helpers::Constants::RESONANCE_FLOOR + 0.005
124
+ system.decay_all
125
+ expect(system.observation_count).to eq(0)
126
+ end
127
+ end
128
+
129
+ describe '#mirrored_agents' do
130
+ it 'lists unique observed agents' do
131
+ system.observe(agent_id: 'alice', action: :deploy, domain: :infra)
132
+ system.observe(agent_id: 'bob', action: :test, domain: :code)
133
+ system.observe(agent_id: 'alice', action: :test, domain: :code)
134
+ expect(system.mirrored_agents).to contain_exactly('alice', 'bob')
135
+ end
136
+ end
137
+
138
+ describe '#to_h' do
139
+ it 'returns stats hash' do
140
+ h = system.to_h
141
+ expect(h).to include(:observations, :repertoire_size, :mirrored_agents, :history_size)
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Mirror::Helpers::ObservedBehavior do
4
+ subject(:obs) { described_class.new(agent_id: 'alice', action: :deploy, domain: :infra) }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns fields' do
8
+ expect(obs.agent_id).to eq('alice')
9
+ expect(obs.action).to eq(:deploy)
10
+ expect(obs.domain).to eq(:infra)
11
+ expect(obs.observation_count).to eq(1)
12
+ end
13
+
14
+ it 'assigns uuid and timestamp' do
15
+ expect(obs.id).to match(/\A[0-9a-f-]{36}\z/)
16
+ expect(obs.created_at).to be_a(Time)
17
+ end
18
+
19
+ it 'defaults resonance to DEFAULT_RESONANCE' do
20
+ expect(obs.resonance).to eq(Legion::Extensions::Mirror::Helpers::Constants::DEFAULT_RESONANCE)
21
+ end
22
+
23
+ it 'clamps resonance' do
24
+ high = described_class.new(agent_id: 'a', action: :x, domain: :d, resonance: 5.0)
25
+ expect(high.resonance).to eq(Legion::Extensions::Mirror::Helpers::Constants::MAX_RESONANCE)
26
+ end
27
+ end
28
+
29
+ describe '#observe_again' do
30
+ it 'increments observation count' do
31
+ obs.observe_again
32
+ expect(obs.observation_count).to eq(2)
33
+ end
34
+
35
+ it 'boosts resonance by REPETITION_BOOST' do
36
+ before = obs.resonance
37
+ obs.observe_again
38
+ expect(obs.resonance).to be > before
39
+ end
40
+ end
41
+
42
+ describe '#boost_familiarity' do
43
+ it 'boosts resonance by FAMILIARITY_BOOST' do
44
+ before = obs.resonance
45
+ obs.boost_familiarity
46
+ boost = Legion::Extensions::Mirror::Helpers::Constants::FAMILIARITY_BOOST
47
+ expect(obs.resonance).to be_within(0.001).of(before + boost)
48
+ end
49
+
50
+ it 'caps at MAX_RESONANCE' do
51
+ obs.resonance = 0.95
52
+ obs.boost_familiarity
53
+ expect(obs.resonance).to eq(Legion::Extensions::Mirror::Helpers::Constants::MAX_RESONANCE)
54
+ end
55
+ end
56
+
57
+ describe '#decay' do
58
+ it 'reduces resonance' do
59
+ before = obs.resonance
60
+ obs.decay
61
+ expect(obs.resonance).to be < before
62
+ end
63
+
64
+ it 'does not drop below RESONANCE_FLOOR' do
65
+ 50.times { obs.decay }
66
+ expect(obs.resonance).to be >= Legion::Extensions::Mirror::Helpers::Constants::RESONANCE_FLOOR
67
+ end
68
+ end
69
+
70
+ describe '#faded?' do
71
+ it 'returns false for strong observation' do
72
+ expect(obs.faded?).to be false
73
+ end
74
+
75
+ it 'returns true at floor' do
76
+ obs.resonance = Legion::Extensions::Mirror::Helpers::Constants::RESONANCE_FLOOR
77
+ expect(obs.faded?).to be true
78
+ end
79
+ end
80
+
81
+ describe '#label' do
82
+ it 'returns :faint for default resonance' do
83
+ expect(obs.label).to eq(:faint)
84
+ end
85
+
86
+ it 'returns :strong_mirror for high resonance' do
87
+ obs.resonance = 0.9
88
+ expect(obs.label).to eq(:strong_mirror)
89
+ end
90
+ end
91
+
92
+ describe '#to_h' do
93
+ it 'returns hash with all fields' do
94
+ h = obs.to_h
95
+ expect(h).to include(:id, :agent_id, :action, :domain, :resonance, :observation_count, :label, :created_at)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Mirror::Runners::Mirror do
4
+ let(:client) { Legion::Extensions::Mirror::Client.new }
5
+
6
+ describe '#observe_behavior' do
7
+ it 'creates an observation' do
8
+ result = client.observe_behavior(agent_id: 'alice', action: :deploy, domain: :infra)
9
+ expect(result[:success]).to be true
10
+ expect(result[:observation][:agent_id]).to eq('alice')
11
+ expect(result[:observation][:action]).to eq(:deploy)
12
+ end
13
+
14
+ it 'boosts resonance on repeat observation' do
15
+ client.observe_behavior(agent_id: 'alice', action: :deploy, domain: :infra)
16
+ result = client.observe_behavior(agent_id: 'alice', action: :deploy, domain: :infra)
17
+ expect(result[:observation][:observation_count]).to eq(2)
18
+ end
19
+ end
20
+
21
+ describe '#imitate_behavior' do
22
+ it 'returns imitated: false with no observations' do
23
+ result = client.imitate_behavior(action: :deploy, domain: :infra)
24
+ expect(result[:success]).to be true
25
+ expect(result[:imitated]).to be false
26
+ end
27
+
28
+ it 'imitates observed behavior' do
29
+ client.observe_behavior(agent_id: 'alice', action: :deploy, domain: :infra)
30
+ result = client.imitate_behavior(action: :deploy, domain: :infra)
31
+ expect(result[:imitated]).to be true
32
+ expect(result[:source]).to eq('alice')
33
+ expect(result[:fidelity]).to be_a(Float)
34
+ end
35
+
36
+ it 'filters by source_agent' do
37
+ client.observe_behavior(agent_id: 'alice', action: :deploy, domain: :infra)
38
+ client.observe_behavior(agent_id: 'bob', action: :deploy, domain: :infra)
39
+ result = client.imitate_behavior(action: :deploy, domain: :infra, source_agent: 'bob')
40
+ expect(result[:source]).to eq('bob')
41
+ end
42
+ end
43
+
44
+ describe '#report_imitation_outcome' do
45
+ it 'updates fidelity on success' do
46
+ result = client.report_imitation_outcome(action: :deploy, domain: :infra, success_flag: true)
47
+ expect(result[:success]).to be true
48
+ expect(result[:fidelity]).to be > Legion::Extensions::Mirror::Helpers::Constants::DEFAULT_FIDELITY
49
+ end
50
+
51
+ it 'updates fidelity on failure' do
52
+ result = client.report_imitation_outcome(action: :deploy, domain: :infra, success_flag: false)
53
+ expect(result[:fidelity]).to be < Legion::Extensions::Mirror::Helpers::Constants::DEFAULT_FIDELITY
54
+ end
55
+ end
56
+
57
+ describe '#strongest_mirrors' do
58
+ it 'returns top mirrors' do
59
+ client.observe_behavior(agent_id: 'alice', action: :deploy, domain: :infra)
60
+ client.observe_behavior(agent_id: 'bob', action: :test, domain: :code)
61
+ result = client.strongest_mirrors(count: 2)
62
+ expect(result[:success]).to be true
63
+ expect(result[:count]).to eq(2)
64
+ end
65
+ end
66
+
67
+ describe '#observations_for' do
68
+ it 'returns observations for agent' do
69
+ client.observe_behavior(agent_id: 'alice', action: :deploy, domain: :infra)
70
+ client.observe_behavior(agent_id: 'alice', action: :test, domain: :code)
71
+ result = client.observations_for(agent_id: 'alice')
72
+ expect(result[:count]).to eq(2)
73
+ end
74
+
75
+ it 'returns empty for unknown agent' do
76
+ result = client.observations_for(agent_id: 'nobody')
77
+ expect(result[:count]).to eq(0)
78
+ end
79
+ end
80
+
81
+ describe '#observations_in' do
82
+ it 'returns observations in domain' do
83
+ client.observe_behavior(agent_id: 'alice', action: :deploy, domain: :infra)
84
+ client.observe_behavior(agent_id: 'bob', action: :test, domain: :code)
85
+ result = client.observations_in(domain: :infra)
86
+ expect(result[:count]).to eq(1)
87
+ end
88
+ end
89
+
90
+ describe '#repertoire_status' do
91
+ it 'starts empty' do
92
+ result = client.repertoire_status
93
+ expect(result[:size]).to eq(0)
94
+ end
95
+
96
+ it 'grows after imitation' do
97
+ client.observe_behavior(agent_id: 'alice', action: :deploy, domain: :infra)
98
+ client.imitate_behavior(action: :deploy, domain: :infra)
99
+ result = client.repertoire_status
100
+ expect(result[:size]).to eq(1)
101
+ end
102
+ end
103
+
104
+ describe '#update_mirror' do
105
+ it 'decays and returns stats' do
106
+ client.observe_behavior(agent_id: 'alice', action: :deploy, domain: :infra)
107
+ result = client.update_mirror
108
+ expect(result[:success]).to be true
109
+ expect(result).to have_key(:observations)
110
+ expect(result).to have_key(:repertoire)
111
+ expect(result).to have_key(:agents)
112
+ end
113
+ end
114
+
115
+ describe '#mirror_stats' do
116
+ it 'returns comprehensive stats' do
117
+ result = client.mirror_stats
118
+ expect(result[:success]).to be true
119
+ expect(result[:stats]).to include(:observations, :repertoire_size, :mirrored_agents)
120
+ end
121
+ end
122
+ 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/mirror'
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,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-mirror
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: Mirror neuron system for brain-modeled agentic AI — automatic behavioral
27
+ observation, imitation learning with fidelity tracking, and empathic resonance from
28
+ watching other agents act.
29
+ email:
30
+ - matthewdiverson@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - Gemfile
36
+ - lex-mirror.gemspec
37
+ - lib/legion/extensions/mirror.rb
38
+ - lib/legion/extensions/mirror/actors/decay.rb
39
+ - lib/legion/extensions/mirror/client.rb
40
+ - lib/legion/extensions/mirror/helpers/constants.rb
41
+ - lib/legion/extensions/mirror/helpers/mirror_system.rb
42
+ - lib/legion/extensions/mirror/helpers/observed_behavior.rb
43
+ - lib/legion/extensions/mirror/runners/mirror.rb
44
+ - lib/legion/extensions/mirror/version.rb
45
+ - spec/legion/extensions/mirror/client_spec.rb
46
+ - spec/legion/extensions/mirror/helpers/mirror_system_spec.rb
47
+ - spec/legion/extensions/mirror/helpers/observed_behavior_spec.rb
48
+ - spec/legion/extensions/mirror/runners/mirror_spec.rb
49
+ - spec/spec_helper.rb
50
+ homepage: https://github.com/LegionIO/lex-mirror
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ homepage_uri: https://github.com/LegionIO/lex-mirror
55
+ source_code_uri: https://github.com/LegionIO/lex-mirror
56
+ documentation_uri: https://github.com/LegionIO/lex-mirror
57
+ changelog_uri: https://github.com/LegionIO/lex-mirror
58
+ bug_tracker_uri: https://github.com/LegionIO/lex-mirror/issues
59
+ rubygems_mfa_required: 'true'
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '3.4'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.6.9
75
+ specification_version: 4
76
+ summary: LEX Mirror
77
+ test_files: []