lex-neuromodulation 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: 9699c48e6cd9388c89541b230de7e5bfdec101a9be590af7ac37d9ee61453ef2
4
+ data.tar.gz: 48ca816c952d030a32969e5f02b226377d51b28e7374a47e22ff4949d164e075
5
+ SHA512:
6
+ metadata.gz: 07ec0cda24a8ad10c7541477fd5f5da565c8f074f5d5faf580e073685ccdba703f8906af7422e9edff7d445f8df2442b453bc12f08062b3d4d5af13bf65a8a10
7
+ data.tar.gz: d4c84c120625ac383bd2f25428e80b6430dfd0bb51d4ff90a136eb4e91cb07e4644fdcc18ccaab181045fec41e66505f7fbadf7da29f57e591a39402e07dbf9b
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+ end
11
+
12
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/neuromodulation/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-neuromodulation'
7
+ spec.version = Legion::Extensions::Neuromodulation::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Neuromodulation'
12
+ spec.description = 'Neuromodulatory system modeling dopamine, serotonin, norepinephrine, and acetylcholine pathways for brain-modeled agentic AI'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-neuromodulation'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-neuromodulation'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-neuromodulation'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-neuromodulation'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-neuromodulation/issues'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
+
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ Dir.glob('{lib,spec}/**/*') + %w[lex-neuromodulation.gemspec Gemfile]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ 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 Neuromodulation
8
+ module Actor
9
+ class Drift < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::Neuromodulation::Runners::Neuromodulation
12
+ end
13
+
14
+ def runner_function
15
+ 'update_neuromodulation'
16
+ end
17
+
18
+ def time
19
+ 30
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/neuromodulation/helpers/constants'
4
+ require 'legion/extensions/neuromodulation/helpers/modulator'
5
+ require 'legion/extensions/neuromodulation/helpers/modulator_system'
6
+ require 'legion/extensions/neuromodulation/runners/neuromodulation'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Neuromodulation
11
+ class Client
12
+ include Runners::Neuromodulation
13
+
14
+ def initialize(system: nil, **)
15
+ @neuromod_system = system || Helpers::ModulatorSystem.new
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :neuromod_system
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Neuromodulation
6
+ module Helpers
7
+ module Constants
8
+ MODULATORS = %i[dopamine serotonin norepinephrine acetylcholine].freeze
9
+ DEFAULT_LEVEL = 0.5
10
+ LEVEL_FLOOR = 0.0
11
+ LEVEL_CEILING = 1.0
12
+ MODULATION_ALPHA = 0.15
13
+ BASELINE_DRIFT = 0.01
14
+ MAX_EVENTS = 200
15
+
16
+ OPTIMAL_RANGES = {
17
+ dopamine: (0.4..0.7),
18
+ serotonin: (0.4..0.7),
19
+ norepinephrine: (0.3..0.6),
20
+ acetylcholine: (0.4..0.7)
21
+ }.freeze
22
+
23
+ STATE_LABELS = {
24
+ dopamine: {
25
+ high: :surplus,
26
+ optimal: :optimal,
27
+ low: :deficit
28
+ },
29
+ serotonin: {
30
+ high: :surplus,
31
+ optimal: :optimal,
32
+ low: :deficit
33
+ },
34
+ norepinephrine: {
35
+ high: :surplus,
36
+ optimal: :optimal,
37
+ low: :deficit
38
+ },
39
+ acetylcholine: {
40
+ high: :surplus,
41
+ optimal: :optimal,
42
+ low: :deficit
43
+ }
44
+ }.freeze
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Neuromodulation
6
+ module Helpers
7
+ class Modulator
8
+ include Constants
9
+
10
+ attr_reader :name, :level, :baseline, :events
11
+
12
+ def initialize(name)
13
+ @name = name
14
+ @level = DEFAULT_LEVEL
15
+ @baseline = DEFAULT_LEVEL
16
+ @events = []
17
+ end
18
+
19
+ def boost(amount, reason: nil)
20
+ old_level = @level
21
+ @level = clamp(@level + amount)
22
+ record_event(:boost, amount, reason, old_level)
23
+ @level
24
+ end
25
+
26
+ def suppress(amount, reason: nil)
27
+ old_level = @level
28
+ @level = clamp(@level - amount)
29
+ record_event(:suppress, amount, reason, old_level)
30
+ @level
31
+ end
32
+
33
+ def drift_to_baseline
34
+ delta = @baseline - @level
35
+ @level = clamp(@level + (delta * BASELINE_DRIFT))
36
+ end
37
+
38
+ def optimal?
39
+ OPTIMAL_RANGES.fetch(@name).include?(@level)
40
+ end
41
+
42
+ def state_label
43
+ range = OPTIMAL_RANGES.fetch(@name)
44
+ if @level > range.end
45
+ STATE_LABELS.dig(@name, :high)
46
+ elsif @level < range.begin
47
+ STATE_LABELS.dig(@name, :low)
48
+ else
49
+ STATE_LABELS.dig(@name, :optimal)
50
+ end
51
+ end
52
+
53
+ INFLUENCE_MAP = {
54
+ dopamine: %i[learning_rate exploration_bias],
55
+ serotonin: %i[patience_factor],
56
+ norepinephrine: %i[arousal_level attention_precision],
57
+ acetylcholine: %i[memory_encoding attention_precision]
58
+ }.freeze
59
+
60
+ def influence_on(target_property)
61
+ relevant = INFLUENCE_MAP.fetch(@name, [])
62
+ return 0.0 unless relevant.include?(target_property)
63
+
64
+ scale(@level)
65
+ end
66
+
67
+ def to_h
68
+ {
69
+ name: @name,
70
+ level: @level.round(4),
71
+ baseline: @baseline.round(4),
72
+ state: state_label,
73
+ optimal: optimal?,
74
+ event_count: @events.size
75
+ }
76
+ end
77
+
78
+ private
79
+
80
+ def clamp(value)
81
+ value.clamp(LEVEL_FLOOR, LEVEL_CEILING)
82
+ end
83
+
84
+ def scale(value)
85
+ (value - DEFAULT_LEVEL) * 2.0
86
+ end
87
+
88
+ def record_event(type, amount, reason, old_level)
89
+ @events << {
90
+ type: type,
91
+ amount: amount,
92
+ reason: reason,
93
+ old_level: old_level.round(4),
94
+ new_level: @level.round(4),
95
+ timestamp: Time.now.utc
96
+ }
97
+ @events.shift while @events.size > MAX_EVENTS
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Neuromodulation
6
+ module Helpers
7
+ class ModulatorSystem
8
+ include Constants
9
+
10
+ attr_reader :modulators
11
+
12
+ def initialize
13
+ @modulators = MODULATORS.to_h { |name| [name, Modulator.new(name)] }
14
+ end
15
+
16
+ def boost(name, amount, reason: nil)
17
+ validate_name!(name)
18
+ result = @modulators[name].boost(amount, reason: reason)
19
+ apply_interactions(name)
20
+ result
21
+ end
22
+
23
+ def suppress(name, amount, reason: nil)
24
+ validate_name!(name)
25
+ result = @modulators[name].suppress(amount, reason: reason)
26
+ apply_interactions(name)
27
+ result
28
+ end
29
+
30
+ def level(name)
31
+ validate_name!(name)
32
+ @modulators[name].level
33
+ end
34
+
35
+ def all_levels
36
+ @modulators.transform_values(&:level)
37
+ end
38
+
39
+ def tick
40
+ @modulators.each_value(&:drift_to_baseline)
41
+ end
42
+
43
+ def learning_rate_modifier
44
+ da = @modulators[:dopamine].level
45
+ ach = @modulators[:acetylcholine].level
46
+ clamp((da * 0.6) + (ach * 0.4))
47
+ end
48
+
49
+ def attention_precision
50
+ ne = @modulators[:norepinephrine].level
51
+ ach = @modulators[:acetylcholine].level
52
+ clamp((ne * 0.5) + (ach * 0.5))
53
+ end
54
+
55
+ def exploration_bias
56
+ @modulators[:dopamine].level
57
+ end
58
+
59
+ def patience_factor
60
+ @modulators[:serotonin].level
61
+ end
62
+
63
+ def memory_encoding_strength
64
+ @modulators[:acetylcholine].level
65
+ end
66
+
67
+ def arousal_level
68
+ @modulators[:norepinephrine].level
69
+ end
70
+
71
+ def composite_influences
72
+ {
73
+ learning_rate_modifier: learning_rate_modifier.round(4),
74
+ attention_precision: attention_precision.round(4),
75
+ exploration_bias: exploration_bias.round(4),
76
+ patience_factor: patience_factor.round(4),
77
+ memory_encoding_strength: memory_encoding_strength.round(4),
78
+ arousal_level: arousal_level.round(4)
79
+ }
80
+ end
81
+
82
+ def balance_score
83
+ in_range = @modulators.values.count(&:optimal?)
84
+ in_range.to_f / @modulators.size
85
+ end
86
+
87
+ def to_h
88
+ {
89
+ modulators: @modulators.transform_values(&:to_h),
90
+ influences: composite_influences,
91
+ balance: balance_score.round(4)
92
+ }
93
+ end
94
+
95
+ private
96
+
97
+ def validate_name!(name)
98
+ raise ArgumentError, "Unknown modulator: #{name}" unless MODULATORS.include?(name)
99
+ end
100
+
101
+ def clamp(value)
102
+ value.clamp(LEVEL_FLOOR, LEVEL_CEILING)
103
+ end
104
+
105
+ def apply_interactions(changed)
106
+ case changed
107
+ when :dopamine
108
+ high_da = @modulators[:dopamine].level > 0.7
109
+ @modulators[:serotonin].suppress(0.05, reason: :dopamine_suppression) if high_da
110
+ when :norepinephrine
111
+ high_ne = @modulators[:norepinephrine].level > 0.8
112
+ @modulators[:acetylcholine].suppress(0.03, reason: :ne_suppression) if high_ne
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Neuromodulation
6
+ module Runners
7
+ module Neuromodulation
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def boost_modulator(name:, amount:, reason: nil, **)
12
+ mod_name = name.to_sym
13
+ return { success: false, error: "Unknown modulator: #{name}" } unless Helpers::Constants::MODULATORS.include?(mod_name)
14
+
15
+ new_level = neuromod_system.boost(mod_name, amount.to_f, reason: reason)
16
+ Legion::Logging.debug "[neuromodulation] boost #{mod_name} by #{amount} -> #{new_level.round(4)}"
17
+ {
18
+ success: true,
19
+ modulator: mod_name,
20
+ level: new_level.round(4),
21
+ state: neuromod_system.modulators[mod_name].state_label
22
+ }
23
+ end
24
+
25
+ def suppress_modulator(name:, amount:, reason: nil, **)
26
+ mod_name = name.to_sym
27
+ return { success: false, error: "Unknown modulator: #{name}" } unless Helpers::Constants::MODULATORS.include?(mod_name)
28
+
29
+ new_level = neuromod_system.suppress(mod_name, amount.to_f, reason: reason)
30
+ Legion::Logging.debug "[neuromodulation] suppress #{mod_name} by #{amount} -> #{new_level.round(4)}"
31
+ {
32
+ success: true,
33
+ modulator: mod_name,
34
+ level: new_level.round(4),
35
+ state: neuromod_system.modulators[mod_name].state_label
36
+ }
37
+ end
38
+
39
+ def modulator_level(name:, **)
40
+ mod_name = name.to_sym
41
+ return { success: false, error: "Unknown modulator: #{name}" } unless Helpers::Constants::MODULATORS.include?(mod_name)
42
+
43
+ level = neuromod_system.level(mod_name)
44
+ {
45
+ success: true,
46
+ modulator: mod_name,
47
+ level: level.round(4),
48
+ state: neuromod_system.modulators[mod_name].state_label
49
+ }
50
+ end
51
+
52
+ def all_modulator_levels(**)
53
+ levels = neuromod_system.all_levels
54
+ Legion::Logging.debug "[neuromodulation] all levels: #{levels.map { |k, v| "#{k}=#{v.round(3)}" }.join(' ')}"
55
+ { success: true, levels: levels.transform_values { |v| v.round(4) } }
56
+ end
57
+
58
+ def cognitive_influence(**)
59
+ influences = neuromod_system.composite_influences
60
+ Legion::Logging.debug '[neuromodulation] cognitive influence snapshot'
61
+ { success: true, influences: influences }
62
+ end
63
+
64
+ def is_optimal(name:, **)
65
+ mod_name = name.to_sym
66
+ return { success: false, error: "Unknown modulator: #{name}" } unless Helpers::Constants::MODULATORS.include?(mod_name)
67
+
68
+ optimal = neuromod_system.modulators[mod_name].optimal?
69
+ {
70
+ success: true,
71
+ modulator: mod_name,
72
+ optimal: optimal,
73
+ level: neuromod_system.level(mod_name).round(4),
74
+ range: Helpers::Constants::OPTIMAL_RANGES[mod_name].to_s
75
+ }
76
+ end
77
+
78
+ def system_balance(**)
79
+ score = neuromod_system.balance_score
80
+ states = neuromod_system.modulators.transform_values(&:state_label)
81
+ status = if score >= 1.0
82
+ :fully_balanced
83
+ elsif score >= 0.75
84
+ :mostly_balanced
85
+ elsif score >= 0.5
86
+ :partially_balanced
87
+ else
88
+ :imbalanced
89
+ end
90
+ Legion::Logging.debug "[neuromodulation] system balance: #{score.round(2)} status=#{status}"
91
+ {
92
+ success: true,
93
+ score: score.round(4),
94
+ status: status,
95
+ states: states
96
+ }
97
+ end
98
+
99
+ def modulator_history(name:, limit: 20, **)
100
+ mod_name = name.to_sym
101
+ return { success: false, error: "Unknown modulator: #{name}" } unless Helpers::Constants::MODULATORS.include?(mod_name)
102
+
103
+ events = neuromod_system.modulators[mod_name].events.last(limit.to_i)
104
+ {
105
+ success: true,
106
+ modulator: mod_name,
107
+ events: events,
108
+ count: events.size
109
+ }
110
+ end
111
+
112
+ def update_neuromodulation(**)
113
+ neuromod_system.tick
114
+ levels = neuromod_system.all_levels
115
+ Legion::Logging.debug '[neuromodulation] drift tick completed'
116
+ {
117
+ success: true,
118
+ action: :drift_tick,
119
+ levels: levels.transform_values { |v| v.round(4) }
120
+ }
121
+ end
122
+
123
+ def neuromodulation_stats(**)
124
+ {
125
+ success: true,
126
+ system: neuromod_system.to_h
127
+ }
128
+ end
129
+
130
+ private
131
+
132
+ def neuromod_system
133
+ @neuromod_system ||= Helpers::ModulatorSystem.new
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Neuromodulation
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/neuromodulation/version'
4
+ require 'legion/extensions/neuromodulation/helpers/constants'
5
+ require 'legion/extensions/neuromodulation/helpers/modulator'
6
+ require 'legion/extensions/neuromodulation/helpers/modulator_system'
7
+ require 'legion/extensions/neuromodulation/runners/neuromodulation'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Neuromodulation
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/neuromodulation/client'
4
+
5
+ RSpec.describe Legion::Extensions::Neuromodulation::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to all runner methods' do
9
+ expect(client).to respond_to(:boost_modulator)
10
+ expect(client).to respond_to(:suppress_modulator)
11
+ expect(client).to respond_to(:modulator_level)
12
+ expect(client).to respond_to(:all_modulator_levels)
13
+ expect(client).to respond_to(:cognitive_influence)
14
+ expect(client).to respond_to(:is_optimal)
15
+ expect(client).to respond_to(:system_balance)
16
+ expect(client).to respond_to(:modulator_history)
17
+ expect(client).to respond_to(:update_neuromodulation)
18
+ expect(client).to respond_to(:neuromodulation_stats)
19
+ end
20
+
21
+ it 'accepts an injected system' do
22
+ system = Legion::Extensions::Neuromodulation::Helpers::ModulatorSystem.new
23
+ system.boost(:dopamine, 0.3)
24
+ c = described_class.new(system: system)
25
+ result = c.modulator_level(name: :dopamine)
26
+ expect(result[:level]).to be > 0.5
27
+ end
28
+
29
+ it 'maintains state across calls' do
30
+ client.boost_modulator(name: :acetylcholine, amount: 0.2)
31
+ level_result = client.modulator_level(name: :acetylcholine)
32
+ expect(level_result[:level]).to be_within(0.001).of(0.7)
33
+ end
34
+
35
+ it 'round-trips a full neuromodulation cycle' do
36
+ client.boost_modulator(name: :dopamine, amount: 0.15)
37
+ client.suppress_modulator(name: :serotonin, amount: 0.1)
38
+ ci = client.cognitive_influence
39
+ expect(ci[:influences][:exploration_bias]).to be > 0.5
40
+ expect(ci[:influences][:patience_factor]).to be < 0.5
41
+ client.update_neuromodulation
42
+ stats = client.neuromodulation_stats
43
+ expect(stats[:system][:modulators][:dopamine][:level]).to be_a(Float)
44
+ end
45
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Neuromodulation::Helpers::Constants do
4
+ let(:mod) { Legion::Extensions::Neuromodulation::Helpers::Constants }
5
+
6
+ describe 'MODULATORS' do
7
+ it 'contains the four neuromodulator names' do
8
+ expect(mod::MODULATORS).to contain_exactly(:dopamine, :serotonin, :norepinephrine, :acetylcholine)
9
+ end
10
+
11
+ it 'is frozen' do
12
+ expect(mod::MODULATORS).to be_frozen
13
+ end
14
+ end
15
+
16
+ describe 'OPTIMAL_RANGES' do
17
+ it 'defines a range for each modulator' do
18
+ mod::MODULATORS.each do |name|
19
+ expect(mod::OPTIMAL_RANGES[name]).to be_a(Range)
20
+ end
21
+ end
22
+
23
+ it 'has non-overlapping floor/ceiling bounds' do
24
+ mod::OPTIMAL_RANGES.each_value do |range|
25
+ expect(range.begin).to be >= mod::LEVEL_FLOOR
26
+ expect(range.end).to be <= mod::LEVEL_CEILING
27
+ end
28
+ end
29
+ end
30
+
31
+ describe 'numeric constants' do
32
+ it 'DEFAULT_LEVEL is 0.5' do
33
+ expect(mod::DEFAULT_LEVEL).to eq(0.5)
34
+ end
35
+
36
+ it 'LEVEL_FLOOR is 0.0' do
37
+ expect(mod::LEVEL_FLOOR).to eq(0.0)
38
+ end
39
+
40
+ it 'LEVEL_CEILING is 1.0' do
41
+ expect(mod::LEVEL_CEILING).to eq(1.0)
42
+ end
43
+
44
+ it 'MODULATION_ALPHA is 0.15' do
45
+ expect(mod::MODULATION_ALPHA).to eq(0.15)
46
+ end
47
+
48
+ it 'BASELINE_DRIFT is 0.01' do
49
+ expect(mod::BASELINE_DRIFT).to eq(0.01)
50
+ end
51
+
52
+ it 'MAX_EVENTS is 200' do
53
+ expect(mod::MAX_EVENTS).to eq(200)
54
+ end
55
+ end
56
+
57
+ describe 'STATE_LABELS' do
58
+ it 'defines high/optimal/low labels for each modulator' do
59
+ mod::MODULATORS.each do |name|
60
+ expect(mod::STATE_LABELS[name]).to have_key(:high)
61
+ expect(mod::STATE_LABELS[name]).to have_key(:optimal)
62
+ expect(mod::STATE_LABELS[name]).to have_key(:low)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Neuromodulation::Helpers::Modulator do
4
+ subject(:mod) { described_class.new(:dopamine) }
5
+
6
+ describe '#initialize' do
7
+ it 'sets name' do
8
+ expect(mod.name).to eq(:dopamine)
9
+ end
10
+
11
+ it 'sets level to DEFAULT_LEVEL' do
12
+ expect(mod.level).to eq(0.5)
13
+ end
14
+
15
+ it 'sets baseline to DEFAULT_LEVEL' do
16
+ expect(mod.baseline).to eq(0.5)
17
+ end
18
+
19
+ it 'starts with empty events' do
20
+ expect(mod.events).to be_empty
21
+ end
22
+ end
23
+
24
+ describe '#boost' do
25
+ it 'increases level' do
26
+ mod.boost(0.2)
27
+ expect(mod.level).to be_within(0.001).of(0.7)
28
+ end
29
+
30
+ it 'clamps at LEVEL_CEILING' do
31
+ mod.boost(1.0)
32
+ expect(mod.level).to eq(1.0)
33
+ end
34
+
35
+ it 'records an event' do
36
+ mod.boost(0.1, reason: :reward)
37
+ expect(mod.events.size).to eq(1)
38
+ expect(mod.events.last[:type]).to eq(:boost)
39
+ expect(mod.events.last[:reason]).to eq(:reward)
40
+ end
41
+
42
+ it 'returns the new level' do
43
+ result = mod.boost(0.1)
44
+ expect(result).to be_within(0.001).of(0.6)
45
+ end
46
+ end
47
+
48
+ describe '#suppress' do
49
+ it 'decreases level' do
50
+ mod.suppress(0.2)
51
+ expect(mod.level).to be_within(0.001).of(0.3)
52
+ end
53
+
54
+ it 'clamps at LEVEL_FLOOR' do
55
+ mod.suppress(1.0)
56
+ expect(mod.level).to eq(0.0)
57
+ end
58
+
59
+ it 'records an event' do
60
+ mod.suppress(0.1, reason: :fatigue)
61
+ expect(mod.events.size).to eq(1)
62
+ expect(mod.events.last[:type]).to eq(:suppress)
63
+ expect(mod.events.last[:reason]).to eq(:fatigue)
64
+ end
65
+
66
+ it 'returns the new level' do
67
+ result = mod.suppress(0.1)
68
+ expect(result).to be_within(0.001).of(0.4)
69
+ end
70
+ end
71
+
72
+ describe '#drift_to_baseline' do
73
+ it 'moves level toward baseline' do
74
+ mod.boost(0.3)
75
+ level_before = mod.level
76
+ mod.drift_to_baseline
77
+ expect(mod.level).to be < level_before
78
+ end
79
+
80
+ it 'does not overshoot baseline' do
81
+ mod.boost(0.3)
82
+ 100.times { mod.drift_to_baseline }
83
+ expect(mod.level).to be >= mod.baseline
84
+ end
85
+ end
86
+
87
+ describe '#optimal?' do
88
+ it 'returns true when level is in optimal range' do
89
+ expect(mod.optimal?).to be true
90
+ end
91
+
92
+ it 'returns false when level is too high' do
93
+ mod.boost(0.5)
94
+ expect(mod.optimal?).to be false
95
+ end
96
+
97
+ it 'returns false when level is too low' do
98
+ mod.suppress(0.4)
99
+ expect(mod.optimal?).to be false
100
+ end
101
+ end
102
+
103
+ describe '#state_label' do
104
+ it 'returns :optimal at default level' do
105
+ expect(mod.state_label).to eq(:optimal)
106
+ end
107
+
108
+ it 'returns :surplus when high' do
109
+ mod.boost(0.4)
110
+ expect(mod.state_label).to eq(:surplus)
111
+ end
112
+
113
+ it 'returns :deficit when low' do
114
+ mod.suppress(0.4)
115
+ expect(mod.state_label).to eq(:deficit)
116
+ end
117
+ end
118
+
119
+ describe '#influence_on' do
120
+ it 'returns a numeric influence for known property' do
121
+ expect(mod.influence_on(:learning_rate)).to be_a(Numeric)
122
+ end
123
+
124
+ it 'returns 0.0 for unknown property' do
125
+ expect(mod.influence_on(:unknown_property)).to eq(0.0)
126
+ end
127
+
128
+ it 'returns higher influence when level is high' do
129
+ mod.boost(0.3)
130
+ high_influence = mod.influence_on(:learning_rate)
131
+ mod2 = described_class.new(:dopamine)
132
+ mod2.suppress(0.3)
133
+ low_influence = mod2.influence_on(:learning_rate)
134
+ expect(high_influence).to be > low_influence
135
+ end
136
+ end
137
+
138
+ describe '#to_h' do
139
+ it 'returns a hash with required keys' do
140
+ h = mod.to_h
141
+ expect(h).to have_key(:name)
142
+ expect(h).to have_key(:level)
143
+ expect(h).to have_key(:baseline)
144
+ expect(h).to have_key(:state)
145
+ expect(h).to have_key(:optimal)
146
+ expect(h).to have_key(:event_count)
147
+ end
148
+
149
+ it 'reflects current state' do
150
+ mod.boost(0.3)
151
+ h = mod.to_h
152
+ expect(h[:level]).to be > 0.5
153
+ expect(h[:event_count]).to eq(1)
154
+ end
155
+ end
156
+
157
+ describe 'event ring buffer' do
158
+ it 'caps events at MAX_EVENTS' do
159
+ 250.times { mod.boost(0.001) }
160
+ expect(mod.events.size).to eq(200)
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Neuromodulation::Helpers::ModulatorSystem do
4
+ subject(:system) { described_class.new }
5
+
6
+ describe '#initialize' do
7
+ it 'creates all four modulators' do
8
+ expect(system.modulators.keys).to contain_exactly(:dopamine, :serotonin, :norepinephrine, :acetylcholine)
9
+ end
10
+
11
+ it 'starts each at default level' do
12
+ system.modulators.each_value do |mod|
13
+ expect(mod.level).to eq(0.5)
14
+ end
15
+ end
16
+ end
17
+
18
+ describe '#boost' do
19
+ it 'raises for unknown modulator' do
20
+ expect { system.boost(:cortisol, 0.1) }.to raise_error(ArgumentError)
21
+ end
22
+
23
+ it 'increases the target modulator level' do
24
+ system.boost(:dopamine, 0.2)
25
+ expect(system.level(:dopamine)).to be_within(0.001).of(0.7)
26
+ end
27
+
28
+ it 'applies dopamine->serotonin suppression when dopamine goes high' do
29
+ serotonin_before = system.level(:serotonin)
30
+ system.boost(:dopamine, 0.3)
31
+ expect(system.level(:serotonin)).to be < serotonin_before
32
+ end
33
+
34
+ it 'does not suppress serotonin for moderate dopamine boost' do
35
+ serotonin_before = system.level(:serotonin)
36
+ system.boost(:dopamine, 0.05)
37
+ expect(system.level(:serotonin)).to eq(serotonin_before)
38
+ end
39
+ end
40
+
41
+ describe '#suppress' do
42
+ it 'raises for unknown modulator' do
43
+ expect { system.suppress(:cortisol, 0.1) }.to raise_error(ArgumentError)
44
+ end
45
+
46
+ it 'decreases the target modulator level' do
47
+ system.suppress(:serotonin, 0.2)
48
+ expect(system.level(:serotonin)).to be_within(0.001).of(0.3)
49
+ end
50
+
51
+ it 'applies norepinephrine->acetylcholine suppression when NE goes very high' do
52
+ ach_before = system.level(:acetylcholine)
53
+ system.boost(:norepinephrine, 0.35)
54
+ expect(system.level(:acetylcholine)).to be < ach_before
55
+ end
56
+ end
57
+
58
+ describe '#level' do
59
+ it 'raises for unknown modulator' do
60
+ expect { system.level(:unknown) }.to raise_error(ArgumentError)
61
+ end
62
+
63
+ it 'returns current level' do
64
+ expect(system.level(:dopamine)).to eq(0.5)
65
+ end
66
+ end
67
+
68
+ describe '#all_levels' do
69
+ it 'returns a hash with all four modulators' do
70
+ levels = system.all_levels
71
+ expect(levels.keys).to contain_exactly(:dopamine, :serotonin, :norepinephrine, :acetylcholine)
72
+ end
73
+
74
+ it 'reflects current state' do
75
+ system.boost(:dopamine, 0.2)
76
+ levels = system.all_levels
77
+ expect(levels[:dopamine]).to be > 0.5
78
+ end
79
+ end
80
+
81
+ describe '#tick' do
82
+ it 'drifts all modulators toward baseline' do
83
+ system.boost(:dopamine, 0.3)
84
+ level_before = system.level(:dopamine)
85
+ system.tick
86
+ expect(system.level(:dopamine)).to be < level_before
87
+ end
88
+
89
+ it 'processes all four modulators' do
90
+ system.boost(:norepinephrine, 0.2)
91
+ system.suppress(:serotonin, 0.2)
92
+ ne_before = system.level(:norepinephrine)
93
+ ser_before = system.level(:serotonin)
94
+ system.tick
95
+ expect(system.level(:norepinephrine)).to be < ne_before
96
+ expect(system.level(:serotonin)).to be > ser_before
97
+ end
98
+ end
99
+
100
+ describe 'composite influences' do
101
+ describe '#learning_rate_modifier' do
102
+ it 'returns a value between 0 and 1' do
103
+ expect(system.learning_rate_modifier).to be_between(0.0, 1.0)
104
+ end
105
+
106
+ it 'increases with higher dopamine' do
107
+ base = system.learning_rate_modifier
108
+ system.boost(:dopamine, 0.2)
109
+ expect(system.learning_rate_modifier).to be > base
110
+ end
111
+ end
112
+
113
+ describe '#attention_precision' do
114
+ it 'returns a value between 0 and 1' do
115
+ expect(system.attention_precision).to be_between(0.0, 1.0)
116
+ end
117
+
118
+ it 'increases with higher norepinephrine' do
119
+ base = system.attention_precision
120
+ system.boost(:norepinephrine, 0.1)
121
+ expect(system.attention_precision).to be > base
122
+ end
123
+ end
124
+
125
+ describe '#exploration_bias' do
126
+ it 'equals dopamine level' do
127
+ expect(system.exploration_bias).to eq(system.level(:dopamine))
128
+ end
129
+ end
130
+
131
+ describe '#patience_factor' do
132
+ it 'equals serotonin level' do
133
+ expect(system.patience_factor).to eq(system.level(:serotonin))
134
+ end
135
+ end
136
+
137
+ describe '#memory_encoding_strength' do
138
+ it 'equals acetylcholine level' do
139
+ expect(system.memory_encoding_strength).to eq(system.level(:acetylcholine))
140
+ end
141
+ end
142
+
143
+ describe '#arousal_level' do
144
+ it 'equals norepinephrine level' do
145
+ expect(system.arousal_level).to eq(system.level(:norepinephrine))
146
+ end
147
+ end
148
+
149
+ describe '#composite_influences' do
150
+ it 'returns a hash with all influence keys' do
151
+ ci = system.composite_influences
152
+ expect(ci).to have_key(:learning_rate_modifier)
153
+ expect(ci).to have_key(:attention_precision)
154
+ expect(ci).to have_key(:exploration_bias)
155
+ expect(ci).to have_key(:patience_factor)
156
+ expect(ci).to have_key(:memory_encoding_strength)
157
+ expect(ci).to have_key(:arousal_level)
158
+ end
159
+ end
160
+ end
161
+
162
+ describe '#balance_score' do
163
+ it 'returns 1.0 when all modulators are optimal' do
164
+ expect(system.balance_score).to eq(1.0)
165
+ end
166
+
167
+ it 'decreases when a modulator goes out of range' do
168
+ system.boost(:dopamine, 0.5)
169
+ expect(system.balance_score).to be < 1.0
170
+ end
171
+ end
172
+
173
+ describe '#to_h' do
174
+ it 'returns modulators, influences, and balance' do
175
+ h = system.to_h
176
+ expect(h).to have_key(:modulators)
177
+ expect(h).to have_key(:influences)
178
+ expect(h).to have_key(:balance)
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/neuromodulation/client'
4
+
5
+ RSpec.describe Legion::Extensions::Neuromodulation::Runners::Neuromodulation do
6
+ let(:client) { Legion::Extensions::Neuromodulation::Client.new }
7
+
8
+ describe '#boost_modulator' do
9
+ it 'returns success true' do
10
+ result = client.boost_modulator(name: :dopamine, amount: 0.1)
11
+ expect(result[:success]).to be true
12
+ end
13
+
14
+ it 'returns updated level' do
15
+ result = client.boost_modulator(name: :dopamine, amount: 0.1)
16
+ expect(result[:level]).to be > 0.5
17
+ end
18
+
19
+ it 'returns state label' do
20
+ result = client.boost_modulator(name: :dopamine, amount: 0.1)
21
+ expect(result[:state]).to be_a(Symbol)
22
+ end
23
+
24
+ it 'returns error for unknown modulator' do
25
+ result = client.boost_modulator(name: :cortisol, amount: 0.1)
26
+ expect(result[:success]).to be false
27
+ expect(result[:error]).to include('cortisol')
28
+ end
29
+
30
+ it 'accepts string modulator name' do
31
+ result = client.boost_modulator(name: 'serotonin', amount: 0.1)
32
+ expect(result[:success]).to be true
33
+ end
34
+ end
35
+
36
+ describe '#suppress_modulator' do
37
+ it 'returns success true' do
38
+ result = client.suppress_modulator(name: :serotonin, amount: 0.1)
39
+ expect(result[:success]).to be true
40
+ end
41
+
42
+ it 'decreases level' do
43
+ result = client.suppress_modulator(name: :serotonin, amount: 0.1)
44
+ expect(result[:level]).to be < 0.5
45
+ end
46
+
47
+ it 'returns error for unknown modulator' do
48
+ result = client.suppress_modulator(name: :adrenaline, amount: 0.1)
49
+ expect(result[:success]).to be false
50
+ end
51
+ end
52
+
53
+ describe '#modulator_level' do
54
+ it 'returns current level' do
55
+ result = client.modulator_level(name: :norepinephrine)
56
+ expect(result[:success]).to be true
57
+ expect(result[:level]).to eq(0.5)
58
+ end
59
+
60
+ it 'returns error for unknown modulator' do
61
+ result = client.modulator_level(name: :unknown)
62
+ expect(result[:success]).to be false
63
+ end
64
+ end
65
+
66
+ describe '#all_modulator_levels' do
67
+ it 'returns all four modulators' do
68
+ result = client.all_modulator_levels
69
+ expect(result[:success]).to be true
70
+ expect(result[:levels].keys).to contain_exactly(:dopamine, :serotonin, :norepinephrine, :acetylcholine)
71
+ end
72
+
73
+ it 'reflects changes after boost' do
74
+ client.boost_modulator(name: :dopamine, amount: 0.2)
75
+ result = client.all_modulator_levels
76
+ expect(result[:levels][:dopamine]).to be > 0.5
77
+ end
78
+ end
79
+
80
+ describe '#cognitive_influence' do
81
+ it 'returns all six cognitive properties' do
82
+ result = client.cognitive_influence
83
+ expect(result[:success]).to be true
84
+ influences = result[:influences]
85
+ expect(influences).to have_key(:learning_rate_modifier)
86
+ expect(influences).to have_key(:attention_precision)
87
+ expect(influences).to have_key(:exploration_bias)
88
+ expect(influences).to have_key(:patience_factor)
89
+ expect(influences).to have_key(:memory_encoding_strength)
90
+ expect(influences).to have_key(:arousal_level)
91
+ end
92
+ end
93
+
94
+ describe '#is_optimal' do
95
+ it 'returns true at default level' do
96
+ result = client.is_optimal(name: :dopamine)
97
+ expect(result[:success]).to be true
98
+ expect(result[:optimal]).to be true
99
+ end
100
+
101
+ it 'returns false when out of range' do
102
+ client.boost_modulator(name: :dopamine, amount: 0.5)
103
+ result = client.is_optimal(name: :dopamine)
104
+ expect(result[:optimal]).to be false
105
+ end
106
+
107
+ it 'includes range string' do
108
+ result = client.is_optimal(name: :serotonin)
109
+ expect(result[:range]).to be_a(String)
110
+ end
111
+
112
+ it 'returns error for unknown modulator' do
113
+ result = client.is_optimal(name: :cortisol)
114
+ expect(result[:success]).to be false
115
+ end
116
+ end
117
+
118
+ describe '#system_balance' do
119
+ it 'returns fully_balanced at default' do
120
+ result = client.system_balance
121
+ expect(result[:success]).to be true
122
+ expect(result[:status]).to eq(:fully_balanced)
123
+ expect(result[:score]).to eq(1.0)
124
+ end
125
+
126
+ it 'returns imbalanced when all are out of range' do
127
+ client.boost_modulator(name: :dopamine, amount: 0.5)
128
+ client.boost_modulator(name: :norepinephrine, amount: 0.5)
129
+ client.suppress_modulator(name: :serotonin, amount: 0.4)
130
+ client.suppress_modulator(name: :acetylcholine, amount: 0.4)
131
+ result = client.system_balance
132
+ expect(result[:score]).to be < 1.0
133
+ end
134
+
135
+ it 'returns state for each modulator' do
136
+ result = client.system_balance
137
+ expect(result[:states].keys).to contain_exactly(:dopamine, :serotonin, :norepinephrine, :acetylcholine)
138
+ end
139
+ end
140
+
141
+ describe '#modulator_history' do
142
+ it 'returns empty events initially' do
143
+ result = client.modulator_history(name: :dopamine)
144
+ expect(result[:success]).to be true
145
+ expect(result[:events]).to be_empty
146
+ end
147
+
148
+ it 'returns events after changes' do
149
+ client.boost_modulator(name: :dopamine, amount: 0.1)
150
+ client.suppress_modulator(name: :dopamine, amount: 0.05)
151
+ result = client.modulator_history(name: :dopamine)
152
+ expect(result[:events].size).to eq(2)
153
+ end
154
+
155
+ it 'respects the limit parameter' do
156
+ 10.times { client.boost_modulator(name: :dopamine, amount: 0.001) }
157
+ result = client.modulator_history(name: :dopamine, limit: 3)
158
+ expect(result[:events].size).to eq(3)
159
+ end
160
+
161
+ it 'returns error for unknown modulator' do
162
+ result = client.modulator_history(name: :unknown)
163
+ expect(result[:success]).to be false
164
+ end
165
+ end
166
+
167
+ describe '#update_neuromodulation' do
168
+ it 'returns success' do
169
+ result = client.update_neuromodulation
170
+ expect(result[:success]).to be true
171
+ expect(result[:action]).to eq(:drift_tick)
172
+ end
173
+
174
+ it 'returns current levels after drift' do
175
+ result = client.update_neuromodulation
176
+ expect(result[:levels].keys).to contain_exactly(:dopamine, :serotonin, :norepinephrine, :acetylcholine)
177
+ end
178
+
179
+ it 'nudges boosted modulators toward baseline' do
180
+ client.boost_modulator(name: :dopamine, amount: 0.3)
181
+ level_after_boost = client.modulator_level(name: :dopamine)[:level]
182
+ client.update_neuromodulation
183
+ level_after_tick = client.modulator_level(name: :dopamine)[:level]
184
+ expect(level_after_tick).to be < level_after_boost
185
+ end
186
+ end
187
+
188
+ describe '#neuromodulation_stats' do
189
+ it 'returns full system snapshot' do
190
+ result = client.neuromodulation_stats
191
+ expect(result[:success]).to be true
192
+ expect(result[:system]).to have_key(:modulators)
193
+ expect(result[:system]).to have_key(:influences)
194
+ expect(result[:system]).to have_key(:balance)
195
+ end
196
+ end
197
+ 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/neuromodulation'
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-neuromodulation
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: Neuromodulatory system modeling dopamine, serotonin, norepinephrine,
27
+ and acetylcholine pathways for brain-modeled agentic AI
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - lex-neuromodulation.gemspec
36
+ - lib/legion/extensions/neuromodulation.rb
37
+ - lib/legion/extensions/neuromodulation/actors/drift.rb
38
+ - lib/legion/extensions/neuromodulation/client.rb
39
+ - lib/legion/extensions/neuromodulation/helpers/constants.rb
40
+ - lib/legion/extensions/neuromodulation/helpers/modulator.rb
41
+ - lib/legion/extensions/neuromodulation/helpers/modulator_system.rb
42
+ - lib/legion/extensions/neuromodulation/runners/neuromodulation.rb
43
+ - lib/legion/extensions/neuromodulation/version.rb
44
+ - spec/legion/extensions/neuromodulation/client_spec.rb
45
+ - spec/legion/extensions/neuromodulation/helpers/constants_spec.rb
46
+ - spec/legion/extensions/neuromodulation/helpers/modulator_spec.rb
47
+ - spec/legion/extensions/neuromodulation/helpers/modulator_system_spec.rb
48
+ - spec/legion/extensions/neuromodulation/runners/neuromodulation_spec.rb
49
+ - spec/spec_helper.rb
50
+ homepage: https://github.com/LegionIO/lex-neuromodulation
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ homepage_uri: https://github.com/LegionIO/lex-neuromodulation
55
+ source_code_uri: https://github.com/LegionIO/lex-neuromodulation
56
+ documentation_uri: https://github.com/LegionIO/lex-neuromodulation
57
+ changelog_uri: https://github.com/LegionIO/lex-neuromodulation
58
+ bug_tracker_uri: https://github.com/LegionIO/lex-neuromodulation/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 Neuromodulation
77
+ test_files: []