lex-arousal 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 +10 -0
- data/LICENSE +21 -0
- data/lex-arousal.gemspec +29 -0
- data/lib/legion/extensions/arousal/client.rb +23 -0
- data/lib/legion/extensions/arousal/helpers/arousal_model.rb +80 -0
- data/lib/legion/extensions/arousal/helpers/constants.rb +40 -0
- data/lib/legion/extensions/arousal/runners/arousal.rb +115 -0
- data/lib/legion/extensions/arousal/version.rb +9 -0
- data/lib/legion/extensions/arousal.rb +14 -0
- data/spec/legion/extensions/arousal/client_spec.rb +42 -0
- data/spec/legion/extensions/arousal/helpers/arousal_model_spec.rb +160 -0
- data/spec/legion/extensions/arousal/helpers/constants_spec.rb +61 -0
- data/spec/legion/extensions/arousal/runners/arousal_spec.rb +137 -0
- data/spec/spec_helper.rb +20 -0
- metadata +74 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cd18506c2aa666a76b1df8c348a61119fd1871041b5d06e9f8e38a8c25104596
|
|
4
|
+
data.tar.gz: f8b1dd784ef606eac38de02072e7815dba1d4e79151248e230430edcb94f380f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 944c6c7317069df58999a0e6e4a95d4ec9be4b31cfd5f42ae232c6abb463382acc475223e4480b700b806e4d5651fb65c93999909b7250fc670c73243ebc4f3f
|
|
7
|
+
data.tar.gz: 960732fa65fdce73e209030ed3b603b21ac5f5a6a3b090e9a17248aab7b544b45ac67bb64788676324f8191548dbfacc27298eb2ff7758e5e0e859692572bd91
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Iverson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/lex-arousal.gemspec
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/arousal/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-arousal'
|
|
7
|
+
spec.version = Legion::Extensions::Arousal::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Arousal'
|
|
12
|
+
spec.description = 'Yerkes-Dodson arousal regulation for brain-modeled agentic AI'
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-arousal'
|
|
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-arousal'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-arousal'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-arousal'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-arousal/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-arousal.gemspec Gemfile LICENSE]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
spec.add_development_dependency 'legion-gaia'
|
|
29
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/arousal/helpers/constants'
|
|
4
|
+
require 'legion/extensions/arousal/helpers/arousal_model'
|
|
5
|
+
require 'legion/extensions/arousal/runners/arousal'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module Arousal
|
|
10
|
+
class Client
|
|
11
|
+
include Runners::Arousal
|
|
12
|
+
|
|
13
|
+
def initialize(**)
|
|
14
|
+
@arousal_model = Helpers::ArousalModel.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :arousal_model
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Arousal
|
|
6
|
+
module Helpers
|
|
7
|
+
class ArousalModel
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :arousal, :arousal_history, :performance_last
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@arousal = DEFAULT_AROUSAL
|
|
14
|
+
@arousal_history = []
|
|
15
|
+
@performance_last = 0.0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def stimulate(amount:, source: :unknown)
|
|
19
|
+
boost = amount || BOOST_FACTOR
|
|
20
|
+
raw = @arousal + boost.to_f.clamp(0.0, 1.0)
|
|
21
|
+
update_arousal(raw, source: source)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def calm(amount:)
|
|
25
|
+
reduction = amount || CALM_FACTOR
|
|
26
|
+
raw = @arousal - reduction.to_f.clamp(0.0, 1.0)
|
|
27
|
+
update_arousal(raw, source: :calm)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def decay
|
|
31
|
+
delta = (@arousal - DEFAULT_AROUSAL) * DECAY_RATE
|
|
32
|
+
raw = @arousal - delta
|
|
33
|
+
update_arousal(raw, source: :decay)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def performance(task_complexity: :moderate)
|
|
37
|
+
optimal = optimal_for(task_complexity)
|
|
38
|
+
diff = @arousal - optimal
|
|
39
|
+
@performance_last = Math.exp(-PERFORMANCE_SENSITIVITY * (diff**2))
|
|
40
|
+
@performance_last
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def arousal_label
|
|
44
|
+
AROUSAL_LABELS.each do |range, label|
|
|
45
|
+
return label if range.cover?(@arousal)
|
|
46
|
+
end
|
|
47
|
+
:dormant
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def optimal_for(complexity)
|
|
51
|
+
TASK_COMPLEXITIES.fetch(complexity, OPTIMAL_AROUSAL_DEFAULT)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_h
|
|
55
|
+
{
|
|
56
|
+
arousal: @arousal,
|
|
57
|
+
label: arousal_label,
|
|
58
|
+
performance_last: @performance_last,
|
|
59
|
+
history_size: @arousal_history.size
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def update_arousal(raw, source: :unknown)
|
|
66
|
+
clamped = raw.clamp(AROUSAL_FLOOR, AROUSAL_CEILING)
|
|
67
|
+
@arousal = (AROUSAL_ALPHA * clamped) + ((1.0 - AROUSAL_ALPHA) * @arousal)
|
|
68
|
+
record_history(source)
|
|
69
|
+
@arousal
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def record_history(source)
|
|
73
|
+
@arousal_history << { arousal: @arousal, source: source, at: Time.now.utc }
|
|
74
|
+
@arousal_history.shift while @arousal_history.size > MAX_AROUSAL_HISTORY
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Arousal
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
DEFAULT_AROUSAL = 0.3
|
|
9
|
+
AROUSAL_ALPHA = 0.15
|
|
10
|
+
DECAY_RATE = 0.05
|
|
11
|
+
OPTIMAL_AROUSAL_SIMPLE = 0.7
|
|
12
|
+
OPTIMAL_AROUSAL_COMPLEX = 0.4
|
|
13
|
+
OPTIMAL_AROUSAL_DEFAULT = 0.5
|
|
14
|
+
PERFORMANCE_SENSITIVITY = 4.0
|
|
15
|
+
BOOST_FACTOR = 0.2
|
|
16
|
+
CALM_FACTOR = 0.15
|
|
17
|
+
AROUSAL_FLOOR = 0.0
|
|
18
|
+
AROUSAL_CEILING = 1.0
|
|
19
|
+
MAX_AROUSAL_HISTORY = 200
|
|
20
|
+
|
|
21
|
+
TASK_COMPLEXITIES = {
|
|
22
|
+
trivial: 0.8,
|
|
23
|
+
simple: 0.7,
|
|
24
|
+
moderate: 0.5,
|
|
25
|
+
complex: 0.4,
|
|
26
|
+
extreme: 0.3
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
AROUSAL_LABELS = {
|
|
30
|
+
(0.8..) => :panic,
|
|
31
|
+
(0.6...0.8) => :high,
|
|
32
|
+
(0.4...0.6) => :optimal,
|
|
33
|
+
(0.2...0.4) => :low,
|
|
34
|
+
(..0.2) => :dormant
|
|
35
|
+
}.freeze
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Arousal
|
|
6
|
+
module Runners
|
|
7
|
+
module Arousal
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def stimulate(amount: nil, source: :unknown, **)
|
|
12
|
+
model = arousal_model
|
|
13
|
+
amount ||= Helpers::Constants::BOOST_FACTOR
|
|
14
|
+
new_level = model.stimulate(amount: amount, source: source)
|
|
15
|
+
Legion::Logging.debug "[arousal] stimulate: source=#{source} amount=#{amount.round(2)} level=#{new_level.round(3)}"
|
|
16
|
+
{
|
|
17
|
+
success: true,
|
|
18
|
+
arousal: new_level,
|
|
19
|
+
label: model.arousal_label,
|
|
20
|
+
source: source
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def calm(amount: nil, **)
|
|
25
|
+
model = arousal_model
|
|
26
|
+
amount ||= Helpers::Constants::CALM_FACTOR
|
|
27
|
+
new_level = model.calm(amount: amount)
|
|
28
|
+
Legion::Logging.debug "[arousal] calm: amount=#{amount.round(2)} level=#{new_level.round(3)}"
|
|
29
|
+
{
|
|
30
|
+
success: true,
|
|
31
|
+
arousal: new_level,
|
|
32
|
+
label: model.arousal_label
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def update_arousal(**)
|
|
37
|
+
model = arousal_model
|
|
38
|
+
model.decay
|
|
39
|
+
perf = model.performance
|
|
40
|
+
Legion::Logging.debug "[arousal] update: level=#{model.arousal.round(3)} label=#{model.arousal_label} perf=#{perf.round(3)}"
|
|
41
|
+
{
|
|
42
|
+
success: true,
|
|
43
|
+
arousal: model.arousal,
|
|
44
|
+
label: model.arousal_label,
|
|
45
|
+
performance: perf
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def check_performance(task_complexity: :moderate, **)
|
|
50
|
+
model = arousal_model
|
|
51
|
+
perf = model.performance(task_complexity: task_complexity)
|
|
52
|
+
optimal = model.optimal_for(task_complexity)
|
|
53
|
+
msg = "[arousal] performance: complexity=#{task_complexity} " \
|
|
54
|
+
"arousal=#{model.arousal.round(3)} optimal=#{optimal} perf=#{perf.round(3)}"
|
|
55
|
+
Legion::Logging.debug msg
|
|
56
|
+
{
|
|
57
|
+
success: true,
|
|
58
|
+
performance: perf,
|
|
59
|
+
arousal: model.arousal,
|
|
60
|
+
optimal_arousal: optimal,
|
|
61
|
+
task_complexity: task_complexity
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def arousal_status(**)
|
|
66
|
+
model = arousal_model
|
|
67
|
+
perf = model.performance
|
|
68
|
+
Legion::Logging.debug "[arousal] status: level=#{model.arousal.round(3)} label=#{model.arousal_label}"
|
|
69
|
+
{
|
|
70
|
+
success: true,
|
|
71
|
+
arousal: model.arousal,
|
|
72
|
+
label: model.arousal_label,
|
|
73
|
+
performance: perf,
|
|
74
|
+
history_size: model.arousal_history.size
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def arousal_guidance(task_complexity: :moderate, **)
|
|
79
|
+
model = arousal_model
|
|
80
|
+
current = model.arousal
|
|
81
|
+
optimal = model.optimal_for(task_complexity)
|
|
82
|
+
perf = model.performance(task_complexity: task_complexity)
|
|
83
|
+
guidance = compute_guidance(current, optimal)
|
|
84
|
+
Legion::Logging.debug "[arousal] guidance: complexity=#{task_complexity} current=#{current.round(3)} optimal=#{optimal} guidance=#{guidance}"
|
|
85
|
+
{
|
|
86
|
+
success: true,
|
|
87
|
+
guidance: guidance,
|
|
88
|
+
arousal: current,
|
|
89
|
+
optimal_arousal: optimal,
|
|
90
|
+
performance: perf,
|
|
91
|
+
task_complexity: task_complexity
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def arousal_model
|
|
98
|
+
@arousal_model ||= Helpers::ArousalModel.new
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def compute_guidance(current, optimal)
|
|
102
|
+
gap = current - optimal
|
|
103
|
+
if gap > 0.15
|
|
104
|
+
:throttle
|
|
105
|
+
elsif gap < -0.15
|
|
106
|
+
:boost
|
|
107
|
+
else
|
|
108
|
+
:maintain
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/arousal/version'
|
|
4
|
+
require 'legion/extensions/arousal/helpers/constants'
|
|
5
|
+
require 'legion/extensions/arousal/helpers/arousal_model'
|
|
6
|
+
require 'legion/extensions/arousal/runners/arousal'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module Arousal
|
|
11
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/arousal/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Arousal::Client do
|
|
6
|
+
let(:client) { described_class.new }
|
|
7
|
+
|
|
8
|
+
it 'responds to all runner methods' do
|
|
9
|
+
expect(client).to respond_to(:stimulate)
|
|
10
|
+
expect(client).to respond_to(:calm)
|
|
11
|
+
expect(client).to respond_to(:update_arousal)
|
|
12
|
+
expect(client).to respond_to(:check_performance)
|
|
13
|
+
expect(client).to respond_to(:arousal_status)
|
|
14
|
+
expect(client).to respond_to(:arousal_guidance)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'runs a full arousal cycle' do
|
|
18
|
+
client.stimulate(amount: 0.3, source: :external)
|
|
19
|
+
status = client.arousal_status
|
|
20
|
+
expect(status[:arousal]).to be > Legion::Extensions::Arousal::Helpers::Constants::DEFAULT_AROUSAL
|
|
21
|
+
expect(status[:label]).to be_a(Symbol)
|
|
22
|
+
|
|
23
|
+
client.calm(amount: 0.1)
|
|
24
|
+
calmed = client.arousal_status
|
|
25
|
+
expect(calmed[:arousal]).to be_a(Float)
|
|
26
|
+
|
|
27
|
+
guidance = client.arousal_guidance(task_complexity: :moderate)
|
|
28
|
+
expect(guidance[:guidance]).to be_a(Symbol)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'persists state across calls' do
|
|
32
|
+
client.stimulate(amount: 0.5, source: :test)
|
|
33
|
+
client.stimulate(amount: 0.2, source: :test)
|
|
34
|
+
status = client.arousal_status
|
|
35
|
+
expect(status[:history_size]).to be >= 2
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'returns higher performance near the optimal arousal for a given complexity' do
|
|
39
|
+
perf_moderate = client.check_performance(task_complexity: :moderate)
|
|
40
|
+
expect(perf_moderate[:performance]).to be_between(0.0, 1.0)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Arousal::Helpers::ArousalModel do
|
|
4
|
+
let(:model) { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'starts at the default arousal level' do
|
|
8
|
+
expect(model.arousal).to be_within(0.001).of(Legion::Extensions::Arousal::Helpers::Constants::DEFAULT_AROUSAL)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'starts with empty history' do
|
|
12
|
+
expect(model.arousal_history).to be_empty
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'starts with zero performance' do
|
|
16
|
+
expect(model.performance_last).to eq(0.0)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe '#stimulate' do
|
|
21
|
+
it 'increases arousal' do
|
|
22
|
+
before = model.arousal
|
|
23
|
+
model.stimulate(amount: 0.3, source: :test)
|
|
24
|
+
expect(model.arousal).to be > before
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'records the source in history' do
|
|
28
|
+
model.stimulate(amount: 0.2, source: :external)
|
|
29
|
+
expect(model.arousal_history.last[:source]).to eq(:external)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'clamps arousal at ceiling' do
|
|
33
|
+
10.times { model.stimulate(amount: 1.0, source: :test) }
|
|
34
|
+
expect(model.arousal).to be <= 1.0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'returns the new arousal level' do
|
|
38
|
+
result = model.stimulate(amount: 0.2, source: :test)
|
|
39
|
+
expect(result).to eq(model.arousal)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe '#calm' do
|
|
44
|
+
before { model.stimulate(amount: 0.5, source: :setup) }
|
|
45
|
+
|
|
46
|
+
it 'decreases arousal' do
|
|
47
|
+
before = model.arousal
|
|
48
|
+
model.calm(amount: 0.2)
|
|
49
|
+
expect(model.arousal).to be < before
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'clamps arousal at floor' do
|
|
53
|
+
10.times { model.calm(amount: 1.0) }
|
|
54
|
+
expect(model.arousal).to be >= 0.0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'returns the new arousal level' do
|
|
58
|
+
result = model.calm(amount: 0.1)
|
|
59
|
+
expect(result).to eq(model.arousal)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe '#decay' do
|
|
64
|
+
it 'moves arousal toward the default resting level from above' do
|
|
65
|
+
model.stimulate(amount: 0.5, source: :setup)
|
|
66
|
+
raised = model.arousal
|
|
67
|
+
model.decay
|
|
68
|
+
# After stimulating above default, decay should lower it (move toward default)
|
|
69
|
+
expect(model.arousal).to be < raised
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'records a decay entry in history' do
|
|
73
|
+
model.decay
|
|
74
|
+
expect(model.arousal_history.last[:source]).to eq(:decay)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe '#performance' do
|
|
79
|
+
it 'returns a value between 0 and 1' do
|
|
80
|
+
perf = model.performance
|
|
81
|
+
expect(perf).to be >= 0.0
|
|
82
|
+
expect(perf).to be <= 1.0
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'returns highest performance near the optimal arousal' do
|
|
86
|
+
model.stimulate(amount: 0.2, source: :test)
|
|
87
|
+
perf_near_optimal = model.performance(task_complexity: :moderate)
|
|
88
|
+
|
|
89
|
+
model2 = described_class.new
|
|
90
|
+
10.times { model2.stimulate(amount: 0.8, source: :test) }
|
|
91
|
+
perf_far_from_optimal = model2.performance(task_complexity: :moderate)
|
|
92
|
+
|
|
93
|
+
expect(perf_near_optimal).to be >= perf_far_from_optimal
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'uses complexity-specific optimal for simple tasks' do
|
|
97
|
+
perf_simple = model.performance(task_complexity: :simple)
|
|
98
|
+
expect(perf_simple).to be_a(Float)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'updates performance_last' do
|
|
102
|
+
model.performance(task_complexity: :moderate)
|
|
103
|
+
expect(model.performance_last).to be > 0.0
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe '#arousal_label' do
|
|
108
|
+
it 'returns :dormant for near-zero arousal' do
|
|
109
|
+
10.times { model.calm(amount: 1.0) }
|
|
110
|
+
expect(model.arousal_label).to eq(:dormant)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'returns :panic for high arousal' do
|
|
114
|
+
10.times { model.stimulate(amount: 1.0, source: :test) }
|
|
115
|
+
expect(model.arousal_label).to eq(:panic)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'returns a symbol for any arousal level' do
|
|
119
|
+
expect(model.arousal_label).to be_a(Symbol)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
describe '#optimal_for' do
|
|
124
|
+
it 'returns the correct optimal for each complexity' do
|
|
125
|
+
constants = Legion::Extensions::Arousal::Helpers::Constants
|
|
126
|
+
expect(model.optimal_for(:trivial)).to eq(constants::TASK_COMPLEXITIES[:trivial])
|
|
127
|
+
expect(model.optimal_for(:moderate)).to eq(constants::TASK_COMPLEXITIES[:moderate])
|
|
128
|
+
expect(model.optimal_for(:extreme)).to eq(constants::TASK_COMPLEXITIES[:extreme])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'returns the default when complexity is unknown' do
|
|
132
|
+
expect(model.optimal_for(:unknown)).to eq(Legion::Extensions::Arousal::Helpers::Constants::OPTIMAL_AROUSAL_DEFAULT)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe '#arousal_history cap' do
|
|
137
|
+
it 'does not exceed MAX_AROUSAL_HISTORY entries' do
|
|
138
|
+
max = Legion::Extensions::Arousal::Helpers::Constants::MAX_AROUSAL_HISTORY
|
|
139
|
+
(max + 10).times { model.stimulate(amount: 0.01, source: :test) }
|
|
140
|
+
expect(model.arousal_history.size).to eq(max)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
describe '#to_h' do
|
|
145
|
+
it 'includes arousal, label, performance_last, and history_size' do
|
|
146
|
+
h = model.to_h
|
|
147
|
+
expect(h).to have_key(:arousal)
|
|
148
|
+
expect(h).to have_key(:label)
|
|
149
|
+
expect(h).to have_key(:performance_last)
|
|
150
|
+
expect(h).to have_key(:history_size)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'reflects the current state' do
|
|
154
|
+
model.stimulate(amount: 0.2, source: :test)
|
|
155
|
+
h = model.to_h
|
|
156
|
+
expect(h[:arousal]).to eq(model.arousal)
|
|
157
|
+
expect(h[:history_size]).to eq(model.arousal_history.size)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Arousal::Helpers::Constants do
|
|
4
|
+
describe 'DEFAULT_AROUSAL' do
|
|
5
|
+
it 'is 0.3' do
|
|
6
|
+
expect(described_class::DEFAULT_AROUSAL).to eq(0.3)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe 'AROUSAL_FLOOR and AROUSAL_CEILING' do
|
|
11
|
+
it 'floor is 0.0' do
|
|
12
|
+
expect(described_class::AROUSAL_FLOOR).to eq(0.0)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'ceiling is 1.0' do
|
|
16
|
+
expect(described_class::AROUSAL_CEILING).to eq(1.0)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe 'TASK_COMPLEXITIES' do
|
|
21
|
+
it 'includes all expected complexity levels' do
|
|
22
|
+
expect(described_class::TASK_COMPLEXITIES.keys).to contain_exactly(:trivial, :simple, :moderate, :complex, :extreme)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'trivial has the highest optimal arousal' do
|
|
26
|
+
expect(described_class::TASK_COMPLEXITIES[:trivial]).to be > described_class::TASK_COMPLEXITIES[:extreme]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'extreme has the lowest optimal arousal' do
|
|
30
|
+
expect(described_class::TASK_COMPLEXITIES[:extreme]).to eq(0.3)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe 'AROUSAL_LABELS' do
|
|
35
|
+
it 'covers the full 0.0..1.0 range' do
|
|
36
|
+
test_values = [0.0, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0]
|
|
37
|
+
test_values.each do |v|
|
|
38
|
+
matched = described_class::AROUSAL_LABELS.any? { |range, _| range.cover?(v) }
|
|
39
|
+
expect(matched).to be(true), "Expected #{v} to match a label range"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'maps values >= 0.8 to :panic' do
|
|
44
|
+
range, label = described_class::AROUSAL_LABELS.find { |r, _| r.cover?(0.9) }
|
|
45
|
+
expect(label).to eq(:panic)
|
|
46
|
+
expect(range).to cover(0.9)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'maps values in 0.4..0.6 to :optimal' do
|
|
50
|
+
range, label = described_class::AROUSAL_LABELS.find { |r, _| r.cover?(0.5) }
|
|
51
|
+
expect(label).to eq(:optimal)
|
|
52
|
+
expect(range).to cover(0.5)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe 'OPTIMAL_AROUSAL_SIMPLE and OPTIMAL_AROUSAL_COMPLEX' do
|
|
57
|
+
it 'simple optimal is higher than complex optimal' do
|
|
58
|
+
expect(described_class::OPTIMAL_AROUSAL_SIMPLE).to be > described_class::OPTIMAL_AROUSAL_COMPLEX
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/arousal/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Arousal::Runners::Arousal do
|
|
6
|
+
let(:client) { Legion::Extensions::Arousal::Client.new }
|
|
7
|
+
|
|
8
|
+
describe '#stimulate' do
|
|
9
|
+
it 'returns success: true' do
|
|
10
|
+
result = client.stimulate
|
|
11
|
+
expect(result[:success]).to be(true)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'returns the new arousal level' do
|
|
15
|
+
result = client.stimulate(amount: 0.2, source: :test)
|
|
16
|
+
expect(result[:arousal]).to be_a(Float)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'returns a label' do
|
|
20
|
+
result = client.stimulate
|
|
21
|
+
expect(result[:label]).to be_a(Symbol)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'returns the source' do
|
|
25
|
+
result = client.stimulate(source: :external_event)
|
|
26
|
+
expect(result[:source]).to eq(:external_event)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'increases arousal above default' do
|
|
30
|
+
result = client.stimulate(amount: 0.4)
|
|
31
|
+
expect(result[:arousal]).to be > Legion::Extensions::Arousal::Helpers::Constants::DEFAULT_AROUSAL
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe '#calm' do
|
|
36
|
+
before { client.stimulate(amount: 0.5) }
|
|
37
|
+
|
|
38
|
+
it 'returns success: true' do
|
|
39
|
+
expect(client.calm[:success]).to be(true)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'returns the new arousal level' do
|
|
43
|
+
result = client.calm(amount: 0.1)
|
|
44
|
+
expect(result[:arousal]).to be_a(Float)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'returns a label' do
|
|
48
|
+
expect(client.calm[:label]).to be_a(Symbol)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '#update_arousal' do
|
|
53
|
+
it 'returns success: true' do
|
|
54
|
+
expect(client.update_arousal[:success]).to be(true)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'returns arousal, label, and performance' do
|
|
58
|
+
result = client.update_arousal
|
|
59
|
+
expect(result).to have_key(:arousal)
|
|
60
|
+
expect(result).to have_key(:label)
|
|
61
|
+
expect(result).to have_key(:performance)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'returns performance between 0 and 1' do
|
|
65
|
+
result = client.update_arousal
|
|
66
|
+
expect(result[:performance]).to be_between(0.0, 1.0)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#check_performance' do
|
|
71
|
+
it 'returns success: true' do
|
|
72
|
+
expect(client.check_performance[:success]).to be(true)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'returns performance, arousal, optimal_arousal, and task_complexity' do
|
|
76
|
+
result = client.check_performance(task_complexity: :simple)
|
|
77
|
+
expect(result).to have_key(:performance)
|
|
78
|
+
expect(result).to have_key(:arousal)
|
|
79
|
+
expect(result).to have_key(:optimal_arousal)
|
|
80
|
+
expect(result[:task_complexity]).to eq(:simple)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'reflects the optimal arousal for the given complexity' do
|
|
84
|
+
result_simple = client.check_performance(task_complexity: :simple)
|
|
85
|
+
result_extreme = client.check_performance(task_complexity: :extreme)
|
|
86
|
+
expect(result_simple[:optimal_arousal]).to be > result_extreme[:optimal_arousal]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe '#arousal_status' do
|
|
91
|
+
it 'returns success: true' do
|
|
92
|
+
expect(client.arousal_status[:success]).to be(true)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'returns arousal, label, performance, and history_size' do
|
|
96
|
+
result = client.arousal_status
|
|
97
|
+
expect(result).to have_key(:arousal)
|
|
98
|
+
expect(result).to have_key(:label)
|
|
99
|
+
expect(result).to have_key(:performance)
|
|
100
|
+
expect(result).to have_key(:history_size)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
describe '#arousal_guidance' do
|
|
105
|
+
it 'returns success: true' do
|
|
106
|
+
expect(client.arousal_guidance[:success]).to be(true)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'returns guidance as a symbol' do
|
|
110
|
+
result = client.arousal_guidance
|
|
111
|
+
expect(result[:guidance]).to be_a(Symbol)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'returns :throttle when arousal is well above optimal' do
|
|
115
|
+
5.times { client.stimulate(amount: 1.0) }
|
|
116
|
+
result = client.arousal_guidance(task_complexity: :extreme)
|
|
117
|
+
expect(result[:guidance]).to eq(:throttle)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'returns :boost when arousal is well below optimal' do
|
|
121
|
+
10.times { client.calm(amount: 1.0) }
|
|
122
|
+
result = client.arousal_guidance(task_complexity: :trivial)
|
|
123
|
+
expect(result[:guidance]).to eq(:boost)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'returns :maintain when near optimal' do
|
|
127
|
+
result = client.arousal_guidance(task_complexity: :moderate)
|
|
128
|
+
expect(%i[maintain throttle boost]).to include(result[:guidance])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'includes optimal_arousal and task_complexity' do
|
|
132
|
+
result = client.arousal_guidance(task_complexity: :complex)
|
|
133
|
+
expect(result[:optimal_arousal]).to eq(0.4)
|
|
134
|
+
expect(result[:task_complexity]).to eq(:complex)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
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/arousal'
|
|
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,74 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-arousal
|
|
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: Yerkes-Dodson arousal regulation for brain-modeled agentic AI
|
|
27
|
+
email:
|
|
28
|
+
- matthewdiverson@gmail.com
|
|
29
|
+
executables: []
|
|
30
|
+
extensions: []
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- Gemfile
|
|
34
|
+
- LICENSE
|
|
35
|
+
- lex-arousal.gemspec
|
|
36
|
+
- lib/legion/extensions/arousal.rb
|
|
37
|
+
- lib/legion/extensions/arousal/client.rb
|
|
38
|
+
- lib/legion/extensions/arousal/helpers/arousal_model.rb
|
|
39
|
+
- lib/legion/extensions/arousal/helpers/constants.rb
|
|
40
|
+
- lib/legion/extensions/arousal/runners/arousal.rb
|
|
41
|
+
- lib/legion/extensions/arousal/version.rb
|
|
42
|
+
- spec/legion/extensions/arousal/client_spec.rb
|
|
43
|
+
- spec/legion/extensions/arousal/helpers/arousal_model_spec.rb
|
|
44
|
+
- spec/legion/extensions/arousal/helpers/constants_spec.rb
|
|
45
|
+
- spec/legion/extensions/arousal/runners/arousal_spec.rb
|
|
46
|
+
- spec/spec_helper.rb
|
|
47
|
+
homepage: https://github.com/LegionIO/lex-arousal
|
|
48
|
+
licenses:
|
|
49
|
+
- MIT
|
|
50
|
+
metadata:
|
|
51
|
+
homepage_uri: https://github.com/LegionIO/lex-arousal
|
|
52
|
+
source_code_uri: https://github.com/LegionIO/lex-arousal
|
|
53
|
+
documentation_uri: https://github.com/LegionIO/lex-arousal
|
|
54
|
+
changelog_uri: https://github.com/LegionIO/lex-arousal
|
|
55
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-arousal/issues
|
|
56
|
+
rubygems_mfa_required: 'true'
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '3.4'
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 3.6.9
|
|
72
|
+
specification_version: 4
|
|
73
|
+
summary: LEX Arousal
|
|
74
|
+
test_files: []
|