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 +7 -0
- data/Gemfile +11 -0
- data/lex-mirror.gemspec +31 -0
- data/lib/legion/extensions/mirror/actors/decay.rb +41 -0
- data/lib/legion/extensions/mirror/client.rb +24 -0
- data/lib/legion/extensions/mirror/helpers/constants.rb +58 -0
- data/lib/legion/extensions/mirror/helpers/mirror_system.rb +158 -0
- data/lib/legion/extensions/mirror/helpers/observed_behavior.rb +63 -0
- data/lib/legion/extensions/mirror/runners/mirror.rb +95 -0
- data/lib/legion/extensions/mirror/version.rb +9 -0
- data/lib/legion/extensions/mirror.rb +17 -0
- data/spec/legion/extensions/mirror/client_spec.rb +40 -0
- data/spec/legion/extensions/mirror/helpers/mirror_system_spec.rb +144 -0
- data/spec/legion/extensions/mirror/helpers/observed_behavior_spec.rb +98 -0
- data/spec/legion/extensions/mirror/runners/mirror_spec.rb +122 -0
- data/spec/spec_helper.rb +20 -0
- metadata +77 -0
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
data/lex-mirror.gemspec
ADDED
|
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
|
@@ -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: []
|