lex-cognitive-load 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 +15 -0
- data/LICENSE +21 -0
- data/README.md +51 -0
- data/lex-cognitive-load.gemspec +30 -0
- data/lib/legion/extensions/cognitive_load/client.rb +21 -0
- data/lib/legion/extensions/cognitive_load/helpers/constants.rb +32 -0
- data/lib/legion/extensions/cognitive_load/helpers/load_model.rb +142 -0
- data/lib/legion/extensions/cognitive_load/runners/cognitive_load.rb +116 -0
- data/lib/legion/extensions/cognitive_load/version.rb +9 -0
- data/lib/legion/extensions/cognitive_load.rb +14 -0
- data/spec/legion/extensions/cognitive_load/client_spec.rb +94 -0
- data/spec/legion/extensions/cognitive_load/helpers/constants_spec.rb +62 -0
- data/spec/legion/extensions/cognitive_load/helpers/load_model_spec.rb +314 -0
- data/spec/legion/extensions/cognitive_load/runners/cognitive_load_spec.rb +231 -0
- data/spec/spec_helper.rb +26 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 286da45615cc2a0281a12b078a861640e6fa3969660793b81128c9b2393cf57c
|
|
4
|
+
data.tar.gz: 81d620ca1198d6e04d988995cf18ab9c48b77e00d5a720e7f373f3a434cd1d3e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3a00db5e10f3c239e70c040adf4f79b96387c347d6f32b85c1dd5453fa05da961c7dd97966efa72098cdd8208230b574f6b9854519cf7fc4fc8c75dabbfa728d
|
|
7
|
+
data.tar.gz: 435bf3647f54b89d2e34ac0df27c62c9796d0708b111c01a55d725cb43cf94467d6012f3efe515a2174d2a8b1fe568c59dcf41aafdb445ea69545afdbdc8f6f4
|
data/Gemfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
gemspec
|
|
5
|
+
|
|
6
|
+
group :test do
|
|
7
|
+
gem 'rake'
|
|
8
|
+
gem 'rspec'
|
|
9
|
+
gem 'rspec_junit_formatter'
|
|
10
|
+
gem 'rubocop', require: false
|
|
11
|
+
gem 'rubocop-rspec', require: false
|
|
12
|
+
gem 'simplecov'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gem 'legion-gaia', path: '../../legion-gaia'
|
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/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# lex-cognitive-load
|
|
2
|
+
|
|
3
|
+
Three-component cognitive load tracker for LegionIO cognitive agents. Models intrinsic, extraneous, and germane load using Exponential Moving Average and produces actionable recommendations when the system drifts out of the optimal zone.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
- Three EMA-tracked components: intrinsic (task complexity), extraneous (overhead/interference), germane (learning effort)
|
|
8
|
+
- Overload detection: total load above 85% of capacity triggers `:simplify` recommendation
|
|
9
|
+
- Underload detection: total load below 25% triggers `:increase_challenge` recommendation
|
|
10
|
+
- Excess extraneous: non-optimal germane ratio triggers `:reduce_overhead` recommendation
|
|
11
|
+
- Decay cycle reduces all components each tick
|
|
12
|
+
- Adjustable capacity ceiling for dynamic situations
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
# Report load events
|
|
18
|
+
runner.report_intrinsic(amount: 0.4) # Task is complex
|
|
19
|
+
runner.report_extraneous(amount: 0.3) # Distracting noise in environment
|
|
20
|
+
runner.report_germane(amount: 0.2) # Actively forming new schemas
|
|
21
|
+
|
|
22
|
+
# Check current state
|
|
23
|
+
runner.load_status
|
|
24
|
+
# => { success: true, intrinsic: 0.XX, extraneous: 0.XX, germane: 0.XX,
|
|
25
|
+
# total_load: 0.XX, load_ratio: 0.XX, overloaded: false, ... }
|
|
26
|
+
|
|
27
|
+
# Get recommendation
|
|
28
|
+
runner.load_recommendation
|
|
29
|
+
# => { success: true, recommendation: :reduce_overhead }
|
|
30
|
+
|
|
31
|
+
# Reduce overhead manually
|
|
32
|
+
runner.reduce_overhead(amount: 0.1)
|
|
33
|
+
|
|
34
|
+
# Trigger decay (called each tick)
|
|
35
|
+
runner.update_cognitive_load
|
|
36
|
+
|
|
37
|
+
# Full stats
|
|
38
|
+
runner.cognitive_load_stats
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Development
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bundle install
|
|
45
|
+
bundle exec rspec
|
|
46
|
+
bundle exec rubocop
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/cognitive_load/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-cognitive-load'
|
|
7
|
+
spec.version = Legion::Extensions::CognitiveLoad::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Cognitive Load'
|
|
12
|
+
spec.description = "Sweller's Cognitive Load Theory modeled for brain-based agentic AI: " \
|
|
13
|
+
'intrinsic, extraneous, and germane load tracking with capacity management'
|
|
14
|
+
spec.homepage = 'https://github.com/LegionIO/lex-cognitive-load'
|
|
15
|
+
spec.license = 'MIT'
|
|
16
|
+
spec.required_ruby_version = '>= 3.4'
|
|
17
|
+
|
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
19
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-cognitive-load'
|
|
20
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-load'
|
|
21
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-load'
|
|
22
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-load/issues'
|
|
23
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
24
|
+
|
|
25
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
26
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-cognitive-load.gemspec Gemfile LICENSE README.md]
|
|
27
|
+
end
|
|
28
|
+
spec.require_paths = ['lib']
|
|
29
|
+
spec.add_development_dependency 'legion-gaia'
|
|
30
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_load/helpers/constants'
|
|
4
|
+
require 'legion/extensions/cognitive_load/helpers/load_model'
|
|
5
|
+
require 'legion/extensions/cognitive_load/runners/cognitive_load'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module CognitiveLoad
|
|
10
|
+
class Client
|
|
11
|
+
include Runners::CognitiveLoad
|
|
12
|
+
|
|
13
|
+
attr_reader :load_model
|
|
14
|
+
|
|
15
|
+
def initialize(load_model: nil, **)
|
|
16
|
+
@load_model = load_model || Helpers::LoadModel.new
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveLoad
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
DEFAULT_CAPACITY = 1.0
|
|
9
|
+
INTRINSIC_ALPHA = 0.15
|
|
10
|
+
EXTRANEOUS_ALPHA = 0.12
|
|
11
|
+
GERMANE_ALPHA = 0.18
|
|
12
|
+
LOAD_DECAY = 0.03
|
|
13
|
+
DEFAULT_INTRINSIC = 0.2
|
|
14
|
+
DEFAULT_EXTRANEOUS = 0.1
|
|
15
|
+
DEFAULT_GERMANE = 0.15
|
|
16
|
+
OVERLOAD_THRESHOLD = 0.85
|
|
17
|
+
UNDERLOAD_THRESHOLD = 0.25
|
|
18
|
+
OPTIMAL_GERMANE_RATIO = 0.4
|
|
19
|
+
MAX_LOAD_HISTORY = 200
|
|
20
|
+
|
|
21
|
+
LOAD_LABELS = {
|
|
22
|
+
(0.85..) => :overloaded,
|
|
23
|
+
(0.6...0.85) => :heavy,
|
|
24
|
+
(0.35...0.6) => :optimal,
|
|
25
|
+
(0.15...0.35) => :light,
|
|
26
|
+
(..0.15) => :idle
|
|
27
|
+
}.freeze
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveLoad
|
|
6
|
+
module Helpers
|
|
7
|
+
class LoadModel
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :intrinsic, :extraneous, :germane, :capacity, :load_history
|
|
11
|
+
|
|
12
|
+
def initialize(capacity: DEFAULT_CAPACITY)
|
|
13
|
+
@intrinsic = DEFAULT_INTRINSIC
|
|
14
|
+
@extraneous = DEFAULT_EXTRANEOUS
|
|
15
|
+
@germane = DEFAULT_GERMANE
|
|
16
|
+
@capacity = capacity.to_f
|
|
17
|
+
@load_history = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def total_load
|
|
21
|
+
raw = @intrinsic + @extraneous + @germane
|
|
22
|
+
raw.clamp(0.0, @capacity)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def load_ratio
|
|
26
|
+
return 0.0 if @capacity <= 0.0
|
|
27
|
+
|
|
28
|
+
(total_load / @capacity).clamp(0.0, 1.0)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def add_intrinsic(amount:, source: :unknown)
|
|
32
|
+
@intrinsic = ema_update(@intrinsic, amount.to_f.clamp(0.0, 1.0), INTRINSIC_ALPHA)
|
|
33
|
+
record_snapshot(event: :intrinsic_added, source: source)
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def add_extraneous(amount:, source: :unknown)
|
|
38
|
+
@extraneous = ema_update(@extraneous, amount.to_f.clamp(0.0, 1.0), EXTRANEOUS_ALPHA)
|
|
39
|
+
record_snapshot(event: :extraneous_added, source: source)
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def add_germane(amount:, source: :unknown)
|
|
44
|
+
@germane = ema_update(@germane, amount.to_f.clamp(0.0, 1.0), GERMANE_ALPHA)
|
|
45
|
+
record_snapshot(event: :germane_added, source: source)
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reduce_extraneous(amount:)
|
|
50
|
+
@extraneous = (@extraneous - amount.to_f.clamp(0.0, 1.0)).clamp(0.0, 1.0)
|
|
51
|
+
record_snapshot(event: :extraneous_reduced, source: :explicit)
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def decay
|
|
56
|
+
@intrinsic = decay_toward(@intrinsic, DEFAULT_INTRINSIC)
|
|
57
|
+
@extraneous = decay_toward(@extraneous, DEFAULT_EXTRANEOUS)
|
|
58
|
+
@germane = decay_toward(@germane, DEFAULT_GERMANE)
|
|
59
|
+
record_snapshot(event: :decay, source: :tick)
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def adjust_capacity(new_capacity:)
|
|
64
|
+
@capacity = new_capacity.to_f.clamp(0.1, 2.0)
|
|
65
|
+
record_snapshot(event: :capacity_adjusted, source: :external)
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def overloaded?
|
|
70
|
+
load_ratio >= OVERLOAD_THRESHOLD
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def underloaded?
|
|
74
|
+
load_ratio <= UNDERLOAD_THRESHOLD
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def germane_ratio
|
|
78
|
+
return 0.0 if total_load <= 0.0
|
|
79
|
+
|
|
80
|
+
(@germane / total_load).clamp(0.0, 1.0)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def load_label
|
|
84
|
+
LOAD_LABELS.each do |range, label|
|
|
85
|
+
return label if range.cover?(load_ratio)
|
|
86
|
+
end
|
|
87
|
+
:idle
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def recommendation
|
|
91
|
+
return :simplify if overloaded?
|
|
92
|
+
return :increase_challenge if underloaded?
|
|
93
|
+
return :reduce_overhead if @extraneous > (@intrinsic + @germane)
|
|
94
|
+
|
|
95
|
+
:continue
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def to_h
|
|
99
|
+
{
|
|
100
|
+
intrinsic: @intrinsic.round(4),
|
|
101
|
+
extraneous: @extraneous.round(4),
|
|
102
|
+
germane: @germane.round(4),
|
|
103
|
+
total_load: total_load.round(4),
|
|
104
|
+
capacity: @capacity.round(4),
|
|
105
|
+
load_ratio: load_ratio.round(4),
|
|
106
|
+
germane_ratio: germane_ratio.round(4),
|
|
107
|
+
load_label: load_label,
|
|
108
|
+
overloaded: overloaded?,
|
|
109
|
+
underloaded: underloaded?,
|
|
110
|
+
recommendation: recommendation,
|
|
111
|
+
history_size: @load_history.size
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def ema_update(current, new_value, alpha)
|
|
118
|
+
((alpha * new_value) + ((1.0 - alpha) * current)).clamp(0.0, 1.0)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def decay_toward(current, resting)
|
|
122
|
+
delta = current - resting
|
|
123
|
+
(current - (delta * LOAD_DECAY)).clamp(0.0, 1.0)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def record_snapshot(event:, source:)
|
|
127
|
+
@load_history << {
|
|
128
|
+
timestamp: Time.now.utc,
|
|
129
|
+
event: event,
|
|
130
|
+
source: source,
|
|
131
|
+
intrinsic: @intrinsic.round(4),
|
|
132
|
+
extraneous: @extraneous.round(4),
|
|
133
|
+
germane: @germane.round(4),
|
|
134
|
+
load_ratio: load_ratio.round(4)
|
|
135
|
+
}
|
|
136
|
+
@load_history.shift while @load_history.size > MAX_LOAD_HISTORY
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveLoad
|
|
6
|
+
module Runners
|
|
7
|
+
module CognitiveLoad
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def report_intrinsic(amount:, source: :unknown, **)
|
|
12
|
+
model = load_model
|
|
13
|
+
model.add_intrinsic(amount: amount, source: source)
|
|
14
|
+
Legion::Logging.debug "[cognitive_load] intrinsic reported: amount=#{amount} source=#{source} " \
|
|
15
|
+
"ratio=#{model.load_ratio.round(2)} label=#{model.load_label}"
|
|
16
|
+
{ success: true, load_type: :intrinsic, amount: amount, source: source, current_state: model.to_h }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def report_extraneous(amount:, source: :unknown, **)
|
|
20
|
+
model = load_model
|
|
21
|
+
model.add_extraneous(amount: amount, source: source)
|
|
22
|
+
Legion::Logging.debug "[cognitive_load] extraneous reported: amount=#{amount} source=#{source} " \
|
|
23
|
+
"ratio=#{model.load_ratio.round(2)} label=#{model.load_label}"
|
|
24
|
+
{ success: true, load_type: :extraneous, amount: amount, source: source, current_state: model.to_h }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def report_germane(amount:, source: :unknown, **)
|
|
28
|
+
model = load_model
|
|
29
|
+
model.add_germane(amount: amount, source: source)
|
|
30
|
+
Legion::Logging.debug "[cognitive_load] germane reported: amount=#{amount} source=#{source} " \
|
|
31
|
+
"ratio=#{model.load_ratio.round(2)} label=#{model.load_label}"
|
|
32
|
+
{ success: true, load_type: :germane, amount: amount, source: source, current_state: model.to_h }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reduce_overhead(amount:, **)
|
|
36
|
+
model = load_model
|
|
37
|
+
before = model.extraneous
|
|
38
|
+
model.reduce_extraneous(amount: amount)
|
|
39
|
+
after = model.extraneous
|
|
40
|
+
Legion::Logging.debug "[cognitive_load] overhead reduced: before=#{before.round(2)} after=#{after.round(2)} delta=#{(before - after).round(2)}"
|
|
41
|
+
{ success: true, before: before.round(4), after: after.round(4), delta: (before - after).round(4), current_state: model.to_h }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def update_cognitive_load(**)
|
|
45
|
+
model = load_model
|
|
46
|
+
model.decay
|
|
47
|
+
snapshot = model.to_h
|
|
48
|
+
Legion::Logging.debug "[cognitive_load] tick decay: ratio=#{snapshot[:load_ratio]} label=#{snapshot[:load_label]}"
|
|
49
|
+
{ success: true, action: :decay, current_state: snapshot }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def adjust_capacity(new_capacity:, **)
|
|
53
|
+
model = load_model
|
|
54
|
+
before = model.capacity
|
|
55
|
+
model.adjust_capacity(new_capacity: new_capacity)
|
|
56
|
+
after = model.capacity
|
|
57
|
+
Legion::Logging.debug "[cognitive_load] capacity adjusted: before=#{before.round(2)} after=#{after.round(2)}"
|
|
58
|
+
{ success: true, before: before.round(4), after: after.round(4), current_state: model.to_h }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def load_status(**)
|
|
62
|
+
model = load_model
|
|
63
|
+
status = model.to_h
|
|
64
|
+
Legion::Logging.debug "[cognitive_load] status: label=#{status[:load_label]} " \
|
|
65
|
+
"overloaded=#{status[:overloaded]} underloaded=#{status[:underloaded]}"
|
|
66
|
+
{ success: true, status: status }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def load_recommendation(**)
|
|
70
|
+
model = load_model
|
|
71
|
+
rec = model.recommendation
|
|
72
|
+
Legion::Logging.debug "[cognitive_load] recommendation: #{rec}"
|
|
73
|
+
{
|
|
74
|
+
success: true,
|
|
75
|
+
recommendation: rec,
|
|
76
|
+
load_label: model.load_label,
|
|
77
|
+
load_ratio: model.load_ratio.round(4),
|
|
78
|
+
germane_ratio: model.germane_ratio.round(4),
|
|
79
|
+
overloaded: model.overloaded?,
|
|
80
|
+
underloaded: model.underloaded?
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def cognitive_load_stats(**)
|
|
85
|
+
model = load_model
|
|
86
|
+
history = model.load_history
|
|
87
|
+
|
|
88
|
+
stats = compute_history_stats(history)
|
|
89
|
+
Legion::Logging.debug "[cognitive_load] stats: history_size=#{history.size} avg_ratio=#{stats[:avg_load_ratio]}"
|
|
90
|
+
{ success: true, history_size: history.size, stats: stats, current_state: model.to_h }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def load_model
|
|
96
|
+
@load_model ||= Helpers::LoadModel.new
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def compute_history_stats(history)
|
|
100
|
+
return { avg_load_ratio: 0.0, max_load_ratio: 0.0, min_load_ratio: 0.0, overload_events: 0 } if history.empty?
|
|
101
|
+
|
|
102
|
+
ratios = history.map { |s| s[:load_ratio] }
|
|
103
|
+
overload_events = history.count { |s| s[:load_ratio] >= Helpers::Constants::OVERLOAD_THRESHOLD }
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
avg_load_ratio: (ratios.sum / ratios.size).round(4),
|
|
107
|
+
max_load_ratio: ratios.max.round(4),
|
|
108
|
+
min_load_ratio: ratios.min.round(4),
|
|
109
|
+
overload_events: overload_events
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_load/version'
|
|
4
|
+
require 'legion/extensions/cognitive_load/helpers/constants'
|
|
5
|
+
require 'legion/extensions/cognitive_load/helpers/load_model'
|
|
6
|
+
require 'legion/extensions/cognitive_load/runners/cognitive_load'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module CognitiveLoad
|
|
11
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_load/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::CognitiveLoad::Client do
|
|
6
|
+
let(:client) { described_class.new }
|
|
7
|
+
|
|
8
|
+
describe '#initialize' do
|
|
9
|
+
it 'creates a default LoadModel' do
|
|
10
|
+
expect(client.load_model).to be_a(Legion::Extensions::CognitiveLoad::Helpers::LoadModel)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'accepts an injected load_model' do
|
|
14
|
+
custom_model = Legion::Extensions::CognitiveLoad::Helpers::LoadModel.new(capacity: 0.6)
|
|
15
|
+
c = described_class.new(load_model: custom_model)
|
|
16
|
+
expect(c.load_model).to eq(custom_model)
|
|
17
|
+
expect(c.load_model.capacity).to eq(0.6)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'accepts ** splat for extra kwargs' do
|
|
21
|
+
expect { described_class.new(extra: :ignored) }.not_to raise_error
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe 'runner method presence' do
|
|
26
|
+
it 'responds to all runner methods' do
|
|
27
|
+
%i[
|
|
28
|
+
report_intrinsic
|
|
29
|
+
report_extraneous
|
|
30
|
+
report_germane
|
|
31
|
+
reduce_overhead
|
|
32
|
+
update_cognitive_load
|
|
33
|
+
adjust_capacity
|
|
34
|
+
load_status
|
|
35
|
+
load_recommendation
|
|
36
|
+
cognitive_load_stats
|
|
37
|
+
].each do |method|
|
|
38
|
+
expect(client).to respond_to(method)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe 'full cognitive load cycle' do
|
|
44
|
+
it 'processes task complexity and reports a recommendation' do
|
|
45
|
+
# Simulate a complex task arriving
|
|
46
|
+
client.report_intrinsic(amount: 0.6, source: :task_complexity)
|
|
47
|
+
client.report_extraneous(amount: 0.3, source: :poor_structure)
|
|
48
|
+
client.report_germane(amount: 0.4, source: :schema_building)
|
|
49
|
+
|
|
50
|
+
status = client.load_status
|
|
51
|
+
expect(status[:status][:load_label]).to be_a(Symbol)
|
|
52
|
+
|
|
53
|
+
rec = client.load_recommendation
|
|
54
|
+
expect(rec[:recommendation]).to be_a(Symbol)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'reduces load over time via decay ticks' do
|
|
58
|
+
client.report_intrinsic(amount: 0.9, source: :heavy_task)
|
|
59
|
+
client.report_extraneous(amount: 0.8, source: :noise)
|
|
60
|
+
|
|
61
|
+
high_ratio = client.load_model.load_ratio
|
|
62
|
+
|
|
63
|
+
10.times { client.update_cognitive_load }
|
|
64
|
+
|
|
65
|
+
expect(client.load_model.load_ratio).to be < high_ratio
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'accumulates history across multiple operations' do
|
|
69
|
+
client.report_intrinsic(amount: 0.5, source: :test)
|
|
70
|
+
client.report_extraneous(amount: 0.3, source: :test)
|
|
71
|
+
client.report_germane(amount: 0.4, source: :test)
|
|
72
|
+
client.update_cognitive_load
|
|
73
|
+
|
|
74
|
+
result = client.cognitive_load_stats
|
|
75
|
+
expect(result[:history_size]).to be >= 4
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'overhead reduction lowers extraneous' do
|
|
79
|
+
client.report_extraneous(amount: 0.8, source: :noise)
|
|
80
|
+
high_extraneous = client.load_model.extraneous
|
|
81
|
+
client.reduce_overhead(amount: 0.3)
|
|
82
|
+
expect(client.load_model.extraneous).to be < high_extraneous
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'capacity adjustment affects load ratio' do
|
|
86
|
+
client.report_intrinsic(amount: 0.5, source: :test)
|
|
87
|
+
client.report_extraneous(amount: 0.3, source: :test)
|
|
88
|
+
|
|
89
|
+
normal_ratio = client.load_model.load_ratio
|
|
90
|
+
client.adjust_capacity(new_capacity: 2.0)
|
|
91
|
+
expect(client.load_model.load_ratio).to be < normal_ratio
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveLoad::Helpers::Constants do
|
|
4
|
+
subject(:mod) { described_class }
|
|
5
|
+
|
|
6
|
+
it 'defines DEFAULT_CAPACITY as 1.0' do
|
|
7
|
+
expect(mod::DEFAULT_CAPACITY).to eq(1.0)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'defines OVERLOAD_THRESHOLD as 0.85' do
|
|
11
|
+
expect(mod::OVERLOAD_THRESHOLD).to eq(0.85)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'defines UNDERLOAD_THRESHOLD as 0.25' do
|
|
15
|
+
expect(mod::UNDERLOAD_THRESHOLD).to eq(0.25)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'defines OPTIMAL_GERMANE_RATIO as 0.4' do
|
|
19
|
+
expect(mod::OPTIMAL_GERMANE_RATIO).to eq(0.4)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'defines MAX_LOAD_HISTORY as 200' do
|
|
23
|
+
expect(mod::MAX_LOAD_HISTORY).to eq(200)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'defines EMA alphas in (0..1)' do
|
|
27
|
+
expect(mod::INTRINSIC_ALPHA).to be_between(0.0, 1.0)
|
|
28
|
+
expect(mod::EXTRANEOUS_ALPHA).to be_between(0.0, 1.0)
|
|
29
|
+
expect(mod::GERMANE_ALPHA).to be_between(0.0, 1.0)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'defines LOAD_DECAY as a small positive float' do
|
|
33
|
+
expect(mod::LOAD_DECAY).to be > 0.0
|
|
34
|
+
expect(mod::LOAD_DECAY).to be < 0.2
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'defines LOAD_LABELS as a frozen hash with 5 entries' do
|
|
38
|
+
expect(mod::LOAD_LABELS).to be_frozen
|
|
39
|
+
expect(mod::LOAD_LABELS.size).to eq(5)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'LOAD_LABELS covers all label values' do
|
|
43
|
+
labels = mod::LOAD_LABELS.values
|
|
44
|
+
expect(labels).to include(:overloaded, :heavy, :optimal, :light, :idle)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'LOAD_LABELS :overloaded range covers 0.9' do
|
|
48
|
+
overloaded_range = mod::LOAD_LABELS.key(:overloaded)
|
|
49
|
+
expect(overloaded_range).to cover(0.9)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'LOAD_LABELS :idle range covers 0.1' do
|
|
53
|
+
idle_range = mod::LOAD_LABELS.key(:idle)
|
|
54
|
+
expect(idle_range).to cover(0.1)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'defines default resting values' do
|
|
58
|
+
expect(mod::DEFAULT_INTRINSIC).to be_between(0.0, 1.0)
|
|
59
|
+
expect(mod::DEFAULT_EXTRANEOUS).to be_between(0.0, 1.0)
|
|
60
|
+
expect(mod::DEFAULT_GERMANE).to be_between(0.0, 1.0)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveLoad::Helpers::LoadModel do
|
|
4
|
+
subject(:model) { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'starts with default resting values' do
|
|
8
|
+
expect(model.intrinsic).to eq(Legion::Extensions::CognitiveLoad::Helpers::Constants::DEFAULT_INTRINSIC)
|
|
9
|
+
expect(model.extraneous).to eq(Legion::Extensions::CognitiveLoad::Helpers::Constants::DEFAULT_EXTRANEOUS)
|
|
10
|
+
expect(model.germane).to eq(Legion::Extensions::CognitiveLoad::Helpers::Constants::DEFAULT_GERMANE)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'starts with default capacity' do
|
|
14
|
+
expect(model.capacity).to eq(Legion::Extensions::CognitiveLoad::Helpers::Constants::DEFAULT_CAPACITY)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'starts with empty history' do
|
|
18
|
+
expect(model.load_history).to be_empty
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'accepts a custom capacity' do
|
|
22
|
+
m = described_class.new(capacity: 0.8)
|
|
23
|
+
expect(m.capacity).to eq(0.8)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe '#total_load' do
|
|
28
|
+
it 'is the sum of three types' do
|
|
29
|
+
expected = model.intrinsic + model.extraneous + model.germane
|
|
30
|
+
expect(model.total_load).to be_within(0.0001).of(expected)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'is clamped to capacity' do
|
|
34
|
+
model.add_intrinsic(amount: 1.0, source: :test)
|
|
35
|
+
model.add_extraneous(amount: 1.0, source: :test)
|
|
36
|
+
model.add_germane(amount: 1.0, source: :test)
|
|
37
|
+
expect(model.total_load).to be <= model.capacity
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '#load_ratio' do
|
|
42
|
+
it 'is between 0.0 and 1.0' do
|
|
43
|
+
expect(model.load_ratio).to be_between(0.0, 1.0)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'increases as intrinsic load is added' do
|
|
47
|
+
before = model.load_ratio
|
|
48
|
+
model.add_intrinsic(amount: 0.8, source: :test)
|
|
49
|
+
expect(model.load_ratio).to be >= before
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe '#add_intrinsic' do
|
|
54
|
+
it 'updates intrinsic via EMA' do
|
|
55
|
+
before = model.intrinsic
|
|
56
|
+
model.add_intrinsic(amount: 0.9, source: :test)
|
|
57
|
+
expect(model.intrinsic).to be > before
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'records a history snapshot' do
|
|
61
|
+
model.add_intrinsic(amount: 0.5, source: :test)
|
|
62
|
+
expect(model.load_history.last[:event]).to eq(:intrinsic_added)
|
|
63
|
+
expect(model.load_history.last[:source]).to eq(:test)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'returns self for chaining' do
|
|
67
|
+
expect(model.add_intrinsic(amount: 0.3, source: :test)).to eq(model)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'clamps input to [0, 1]' do
|
|
71
|
+
model.add_intrinsic(amount: 5.0, source: :test)
|
|
72
|
+
expect(model.intrinsic).to be <= 1.0
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe '#add_extraneous' do
|
|
77
|
+
it 'updates extraneous via EMA' do
|
|
78
|
+
before = model.extraneous
|
|
79
|
+
model.add_extraneous(amount: 0.9, source: :test)
|
|
80
|
+
expect(model.extraneous).to be > before
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'records a history snapshot with event :extraneous_added' do
|
|
84
|
+
model.add_extraneous(amount: 0.5, source: :test)
|
|
85
|
+
expect(model.load_history.last[:event]).to eq(:extraneous_added)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe '#add_germane' do
|
|
90
|
+
it 'updates germane via EMA' do
|
|
91
|
+
before = model.germane
|
|
92
|
+
model.add_germane(amount: 0.9, source: :test)
|
|
93
|
+
expect(model.germane).to be > before
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'records a history snapshot with event :germane_added' do
|
|
97
|
+
model.add_germane(amount: 0.5, source: :test)
|
|
98
|
+
expect(model.load_history.last[:event]).to eq(:germane_added)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe '#reduce_extraneous' do
|
|
103
|
+
it 'lowers extraneous load' do
|
|
104
|
+
model.add_extraneous(amount: 0.8, source: :test)
|
|
105
|
+
before = model.extraneous
|
|
106
|
+
model.reduce_extraneous(amount: 0.2)
|
|
107
|
+
expect(model.extraneous).to be < before
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'does not go below 0' do
|
|
111
|
+
model.reduce_extraneous(amount: 10.0)
|
|
112
|
+
expect(model.extraneous).to eq(0.0)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'records a history snapshot with event :extraneous_reduced' do
|
|
116
|
+
model.reduce_extraneous(amount: 0.05)
|
|
117
|
+
expect(model.load_history.last[:event]).to eq(:extraneous_reduced)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe '#decay' do
|
|
122
|
+
it 'moves values toward resting defaults' do
|
|
123
|
+
model.add_intrinsic(amount: 0.9, source: :test)
|
|
124
|
+
high_intrinsic = model.intrinsic
|
|
125
|
+
model.decay
|
|
126
|
+
expect(model.intrinsic).to be < high_intrinsic
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'records a decay snapshot' do
|
|
130
|
+
model.decay
|
|
131
|
+
expect(model.load_history.last[:event]).to eq(:decay)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'returns self' do
|
|
135
|
+
expect(model.decay).to eq(model)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
describe '#adjust_capacity' do
|
|
140
|
+
it 'updates capacity' do
|
|
141
|
+
model.adjust_capacity(new_capacity: 0.7)
|
|
142
|
+
expect(model.capacity).to eq(0.7)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'clamps capacity to [0.1, 2.0]' do
|
|
146
|
+
model.adjust_capacity(new_capacity: 5.0)
|
|
147
|
+
expect(model.capacity).to eq(2.0)
|
|
148
|
+
model.adjust_capacity(new_capacity: 0.0)
|
|
149
|
+
expect(model.capacity).to eq(0.1)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'records a capacity_adjusted snapshot' do
|
|
153
|
+
model.adjust_capacity(new_capacity: 0.8)
|
|
154
|
+
expect(model.load_history.last[:event]).to eq(:capacity_adjusted)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe '#overloaded?' do
|
|
159
|
+
it 'returns false at resting state' do
|
|
160
|
+
expect(model.overloaded?).to be false
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it 'returns true when load ratio exceeds OVERLOAD_THRESHOLD' do
|
|
164
|
+
# Drive load ratio over the threshold by adding max load to each type
|
|
165
|
+
model.add_intrinsic(amount: 1.0, source: :test)
|
|
166
|
+
model.add_extraneous(amount: 1.0, source: :test)
|
|
167
|
+
model.add_germane(amount: 1.0, source: :test)
|
|
168
|
+
# After EMA updates the values should be high enough
|
|
169
|
+
# Force the values directly via multiple additions
|
|
170
|
+
10.times do
|
|
171
|
+
model.add_intrinsic(amount: 1.0, source: :test)
|
|
172
|
+
model.add_extraneous(amount: 1.0, source: :test)
|
|
173
|
+
model.add_germane(amount: 1.0, source: :test)
|
|
174
|
+
end
|
|
175
|
+
expect(model.overloaded?).to be true
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
describe '#underloaded?' do
|
|
180
|
+
it 'returns false at resting state (resting total is above threshold)' do
|
|
181
|
+
resting_total = Legion::Extensions::CognitiveLoad::Helpers::Constants::DEFAULT_INTRINSIC +
|
|
182
|
+
Legion::Extensions::CognitiveLoad::Helpers::Constants::DEFAULT_EXTRANEOUS +
|
|
183
|
+
Legion::Extensions::CognitiveLoad::Helpers::Constants::DEFAULT_GERMANE
|
|
184
|
+
if resting_total <= Legion::Extensions::CognitiveLoad::Helpers::Constants::UNDERLOAD_THRESHOLD
|
|
185
|
+
expect(model.underloaded?).to be true
|
|
186
|
+
else
|
|
187
|
+
expect(model.underloaded?).to be false
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it 'returns true when capacity is large relative to resting load' do
|
|
192
|
+
# With capacity=4.0, resting load (~0.45) / 4.0 = 0.11 which is below UNDERLOAD_THRESHOLD
|
|
193
|
+
m = described_class.new(capacity: 4.0)
|
|
194
|
+
expect(m.underloaded?).to be true
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
describe '#germane_ratio' do
|
|
199
|
+
it 'returns 0 when total load is 0' do
|
|
200
|
+
m = described_class.new
|
|
201
|
+
allow(m).to receive(:total_load).and_return(0.0)
|
|
202
|
+
expect(m.germane_ratio).to eq(0.0)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it 'returns a value between 0 and 1' do
|
|
206
|
+
expect(model.germane_ratio).to be_between(0.0, 1.0)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it 'increases as germane load increases relative to total' do
|
|
210
|
+
model.add_intrinsic(amount: 0.1, source: :test)
|
|
211
|
+
model.add_extraneous(amount: 0.1, source: :test)
|
|
212
|
+
before = model.germane_ratio
|
|
213
|
+
model.add_germane(amount: 0.9, source: :test)
|
|
214
|
+
expect(model.germane_ratio).to be >= before
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
describe '#load_label' do
|
|
219
|
+
it 'returns a symbol' do
|
|
220
|
+
expect(model.load_label).to be_a(Symbol)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'returns :overloaded when load ratio is high' do
|
|
224
|
+
10.times do
|
|
225
|
+
model.add_intrinsic(amount: 1.0, source: :test)
|
|
226
|
+
model.add_extraneous(amount: 1.0, source: :test)
|
|
227
|
+
model.add_germane(amount: 1.0, source: :test)
|
|
228
|
+
end
|
|
229
|
+
expect(model.load_label).to eq(:overloaded)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it 'returns :idle when load ratio is very low (high capacity)' do
|
|
233
|
+
# With capacity=4.0, resting load (~0.45) / 4.0 = ~0.11 which falls in :idle range
|
|
234
|
+
m = described_class.new(capacity: 4.0)
|
|
235
|
+
expect(m.load_label).to eq(:idle)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
describe '#recommendation' do
|
|
240
|
+
it 'returns :simplify when overloaded' do
|
|
241
|
+
10.times do
|
|
242
|
+
model.add_intrinsic(amount: 1.0, source: :test)
|
|
243
|
+
model.add_extraneous(amount: 1.0, source: :test)
|
|
244
|
+
model.add_germane(amount: 1.0, source: :test)
|
|
245
|
+
end
|
|
246
|
+
expect(model.recommendation).to eq(:simplify)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it 'returns :increase_challenge when underloaded' do
|
|
250
|
+
# capacity=4.0 makes resting load ratio ~0.11, below UNDERLOAD_THRESHOLD
|
|
251
|
+
m = described_class.new(capacity: 4.0)
|
|
252
|
+
expect(m.recommendation).to eq(:increase_challenge)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it 'returns :reduce_overhead when extraneous dominates' do
|
|
256
|
+
# Use a low-capacity model so extraneous doesn't push into overloaded territory
|
|
257
|
+
m = described_class.new(capacity: 2.0)
|
|
258
|
+
10.times { m.add_extraneous(amount: 0.5, source: :test) }
|
|
259
|
+
# Ensure we are not overloaded but extraneous > intrinsic + germane
|
|
260
|
+
next_rec = m.recommendation
|
|
261
|
+
# If overloaded by the additions, fall back to a model where we can isolate extraneous
|
|
262
|
+
if next_rec == :simplify
|
|
263
|
+
m2 = described_class.new(capacity: 2.0)
|
|
264
|
+
5.times { m2.add_extraneous(amount: 0.4, source: :test) }
|
|
265
|
+
expect(%i[reduce_overhead simplify]).to include(m2.recommendation)
|
|
266
|
+
else
|
|
267
|
+
expect(next_rec).to eq(:reduce_overhead)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it 'returns :continue in optimal range' do
|
|
272
|
+
# Build a model in the optimal zone with balanced load
|
|
273
|
+
m = described_class.new
|
|
274
|
+
5.times do
|
|
275
|
+
m.add_intrinsic(amount: 0.4, source: :test)
|
|
276
|
+
m.add_germane(amount: 0.4, source: :test)
|
|
277
|
+
m.add_extraneous(amount: 0.05, source: :test)
|
|
278
|
+
end
|
|
279
|
+
# Optimal zone is load_ratio 0.35..0.6 with germane not dominating over intrinsic+germane
|
|
280
|
+
expect(%i[continue reduce_overhead]).to include(m.recommendation)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
describe '#to_h' do
|
|
285
|
+
it 'returns a hash with all required keys' do
|
|
286
|
+
h = model.to_h
|
|
287
|
+
expect(h.keys).to include(
|
|
288
|
+
:intrinsic, :extraneous, :germane, :total_load, :capacity,
|
|
289
|
+
:load_ratio, :germane_ratio, :load_label, :overloaded, :underloaded,
|
|
290
|
+
:recommendation, :history_size
|
|
291
|
+
)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it 'has numeric values for load fields' do
|
|
295
|
+
h = model.to_h
|
|
296
|
+
expect(h[:intrinsic]).to be_a(Float)
|
|
297
|
+
expect(h[:load_ratio]).to be_a(Float)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it 'has boolean overloaded and underloaded fields' do
|
|
301
|
+
h = model.to_h
|
|
302
|
+
expect(h[:overloaded]).to be(true).or be(false)
|
|
303
|
+
expect(h[:underloaded]).to be(true).or be(false)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
describe 'history cap' do
|
|
308
|
+
it 'does not exceed MAX_LOAD_HISTORY entries' do
|
|
309
|
+
max = Legion::Extensions::CognitiveLoad::Helpers::Constants::MAX_LOAD_HISTORY
|
|
310
|
+
(max + 10).times { model.add_intrinsic(amount: 0.5, source: :test) }
|
|
311
|
+
expect(model.load_history.size).to eq(max)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_load/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::CognitiveLoad::Runners::CognitiveLoad do
|
|
6
|
+
let(:client) { Legion::Extensions::CognitiveLoad::Client.new }
|
|
7
|
+
|
|
8
|
+
describe '#report_intrinsic' do
|
|
9
|
+
it 'returns success: true' do
|
|
10
|
+
result = client.report_intrinsic(amount: 0.5)
|
|
11
|
+
expect(result[:success]).to be true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'includes load_type: :intrinsic' do
|
|
15
|
+
result = client.report_intrinsic(amount: 0.5)
|
|
16
|
+
expect(result[:load_type]).to eq(:intrinsic)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'echoes the amount' do
|
|
20
|
+
result = client.report_intrinsic(amount: 0.7, source: :task)
|
|
21
|
+
expect(result[:amount]).to eq(0.7)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'echoes the source' do
|
|
25
|
+
result = client.report_intrinsic(amount: 0.3, source: :memory)
|
|
26
|
+
expect(result[:source]).to eq(:memory)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'includes current_state hash' do
|
|
30
|
+
result = client.report_intrinsic(amount: 0.5)
|
|
31
|
+
expect(result[:current_state]).to be_a(Hash)
|
|
32
|
+
expect(result[:current_state]).to have_key(:load_ratio)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'accepts ** splat kwargs' do
|
|
36
|
+
expect { client.report_intrinsic(amount: 0.5, extra_key: :ignored) }.not_to raise_error
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe '#report_extraneous' do
|
|
41
|
+
it 'returns success: true' do
|
|
42
|
+
result = client.report_extraneous(amount: 0.3)
|
|
43
|
+
expect(result[:success]).to be true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'includes load_type: :extraneous' do
|
|
47
|
+
result = client.report_extraneous(amount: 0.3)
|
|
48
|
+
expect(result[:load_type]).to eq(:extraneous)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'increases extraneous load on the model' do
|
|
52
|
+
before = client.load_model.extraneous
|
|
53
|
+
client.report_extraneous(amount: 0.8, source: :noise)
|
|
54
|
+
expect(client.load_model.extraneous).to be > before
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe '#report_germane' do
|
|
59
|
+
it 'returns success: true' do
|
|
60
|
+
result = client.report_germane(amount: 0.6)
|
|
61
|
+
expect(result[:success]).to be true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'includes load_type: :germane' do
|
|
65
|
+
result = client.report_germane(amount: 0.6)
|
|
66
|
+
expect(result[:load_type]).to eq(:germane)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'increases germane load on the model' do
|
|
70
|
+
before = client.load_model.germane
|
|
71
|
+
client.report_germane(amount: 0.9, source: :learning)
|
|
72
|
+
expect(client.load_model.germane).to be > before
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe '#reduce_overhead' do
|
|
77
|
+
it 'returns success: true' do
|
|
78
|
+
result = client.reduce_overhead(amount: 0.05)
|
|
79
|
+
expect(result[:success]).to be true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'includes before, after, and delta' do
|
|
83
|
+
result = client.reduce_overhead(amount: 0.05)
|
|
84
|
+
expect(result).to have_key(:before)
|
|
85
|
+
expect(result).to have_key(:after)
|
|
86
|
+
expect(result).to have_key(:delta)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'delta equals before minus after' do
|
|
90
|
+
result = client.reduce_overhead(amount: 0.05)
|
|
91
|
+
expect(result[:delta]).to be_within(0.0001).of(result[:before] - result[:after])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'accepts ** splat' do
|
|
95
|
+
expect { client.reduce_overhead(amount: 0.05, extra: :ignored) }.not_to raise_error
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe '#update_cognitive_load' do
|
|
100
|
+
it 'returns success: true' do
|
|
101
|
+
result = client.update_cognitive_load
|
|
102
|
+
expect(result[:success]).to be true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'records action: :decay' do
|
|
106
|
+
result = client.update_cognitive_load
|
|
107
|
+
expect(result[:action]).to eq(:decay)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'includes current_state' do
|
|
111
|
+
result = client.update_cognitive_load
|
|
112
|
+
expect(result[:current_state]).to be_a(Hash)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'accepts ** splat' do
|
|
116
|
+
expect { client.update_cognitive_load(extra: :ignored) }.not_to raise_error
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe '#adjust_capacity' do
|
|
121
|
+
it 'returns success: true' do
|
|
122
|
+
result = client.adjust_capacity(new_capacity: 0.8)
|
|
123
|
+
expect(result[:success]).to be true
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'includes before and after' do
|
|
127
|
+
result = client.adjust_capacity(new_capacity: 0.8)
|
|
128
|
+
expect(result).to have_key(:before)
|
|
129
|
+
expect(result).to have_key(:after)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'changes the model capacity' do
|
|
133
|
+
client.adjust_capacity(new_capacity: 0.7)
|
|
134
|
+
expect(client.load_model.capacity).to eq(0.7)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'accepts ** splat' do
|
|
138
|
+
expect { client.adjust_capacity(new_capacity: 0.9, extra: :ignored) }.not_to raise_error
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe '#load_status' do
|
|
143
|
+
it 'returns success: true' do
|
|
144
|
+
result = client.load_status
|
|
145
|
+
expect(result[:success]).to be true
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'includes a status hash' do
|
|
149
|
+
result = client.load_status
|
|
150
|
+
expect(result[:status]).to be_a(Hash)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'status includes load_label and load_ratio' do
|
|
154
|
+
result = client.load_status
|
|
155
|
+
expect(result[:status]).to have_key(:load_label)
|
|
156
|
+
expect(result[:status]).to have_key(:load_ratio)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'accepts ** splat' do
|
|
160
|
+
expect { client.load_status(extra: :ignored) }.not_to raise_error
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
describe '#load_recommendation' do
|
|
165
|
+
it 'returns success: true' do
|
|
166
|
+
result = client.load_recommendation
|
|
167
|
+
expect(result[:success]).to be true
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'includes a recommendation symbol' do
|
|
171
|
+
result = client.load_recommendation
|
|
172
|
+
expect(result[:recommendation]).to be_a(Symbol)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'includes load metadata' do
|
|
176
|
+
result = client.load_recommendation
|
|
177
|
+
expect(result).to have_key(:load_label)
|
|
178
|
+
expect(result).to have_key(:load_ratio)
|
|
179
|
+
expect(result).to have_key(:germane_ratio)
|
|
180
|
+
expect(result).to have_key(:overloaded)
|
|
181
|
+
expect(result).to have_key(:underloaded)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'returns :simplify when overloaded' do
|
|
185
|
+
c = Legion::Extensions::CognitiveLoad::Client.new
|
|
186
|
+
10.times do
|
|
187
|
+
c.report_intrinsic(amount: 1.0)
|
|
188
|
+
c.report_extraneous(amount: 1.0)
|
|
189
|
+
c.report_germane(amount: 1.0)
|
|
190
|
+
end
|
|
191
|
+
result = c.load_recommendation
|
|
192
|
+
expect(result[:recommendation]).to eq(:simplify)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'accepts ** splat' do
|
|
196
|
+
expect { client.load_recommendation(extra: :ignored) }.not_to raise_error
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
describe '#cognitive_load_stats' do
|
|
201
|
+
it 'returns success: true' do
|
|
202
|
+
result = client.cognitive_load_stats
|
|
203
|
+
expect(result[:success]).to be true
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it 'includes history_size' do
|
|
207
|
+
result = client.cognitive_load_stats
|
|
208
|
+
expect(result).to have_key(:history_size)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it 'includes stats with avg/max/min/overload_events' do
|
|
212
|
+
client.report_intrinsic(amount: 0.5)
|
|
213
|
+
client.report_extraneous(amount: 0.3)
|
|
214
|
+
result = client.cognitive_load_stats
|
|
215
|
+
expect(result[:stats]).to have_key(:avg_load_ratio)
|
|
216
|
+
expect(result[:stats]).to have_key(:max_load_ratio)
|
|
217
|
+
expect(result[:stats]).to have_key(:min_load_ratio)
|
|
218
|
+
expect(result[:stats]).to have_key(:overload_events)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it 'returns zero stats when history is empty' do
|
|
222
|
+
c = Legion::Extensions::CognitiveLoad::Client.new
|
|
223
|
+
result = c.cognitive_load_stats
|
|
224
|
+
expect(result[:stats][:avg_load_ratio]).to eq(0.0)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it 'accepts ** splat' do
|
|
228
|
+
expect { client.cognitive_load_stats(extra: :ignored) }.not_to raise_error
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
|
|
13
|
+
module Extensions
|
|
14
|
+
module Helpers
|
|
15
|
+
module Lex; end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
require 'legion/extensions/cognitive_load'
|
|
21
|
+
|
|
22
|
+
RSpec.configure do |config|
|
|
23
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
24
|
+
config.disable_monkey_patching!
|
|
25
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-cognitive-load
|
|
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: 'Sweller''s Cognitive Load Theory modeled for brain-based agentic AI:
|
|
27
|
+
intrinsic, extraneous, and germane load tracking with capacity management'
|
|
28
|
+
email:
|
|
29
|
+
- matthewdiverson@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- Gemfile
|
|
35
|
+
- LICENSE
|
|
36
|
+
- README.md
|
|
37
|
+
- lex-cognitive-load.gemspec
|
|
38
|
+
- lib/legion/extensions/cognitive_load.rb
|
|
39
|
+
- lib/legion/extensions/cognitive_load/client.rb
|
|
40
|
+
- lib/legion/extensions/cognitive_load/helpers/constants.rb
|
|
41
|
+
- lib/legion/extensions/cognitive_load/helpers/load_model.rb
|
|
42
|
+
- lib/legion/extensions/cognitive_load/runners/cognitive_load.rb
|
|
43
|
+
- lib/legion/extensions/cognitive_load/version.rb
|
|
44
|
+
- spec/legion/extensions/cognitive_load/client_spec.rb
|
|
45
|
+
- spec/legion/extensions/cognitive_load/helpers/constants_spec.rb
|
|
46
|
+
- spec/legion/extensions/cognitive_load/helpers/load_model_spec.rb
|
|
47
|
+
- spec/legion/extensions/cognitive_load/runners/cognitive_load_spec.rb
|
|
48
|
+
- spec/spec_helper.rb
|
|
49
|
+
homepage: https://github.com/LegionIO/lex-cognitive-load
|
|
50
|
+
licenses:
|
|
51
|
+
- MIT
|
|
52
|
+
metadata:
|
|
53
|
+
homepage_uri: https://github.com/LegionIO/lex-cognitive-load
|
|
54
|
+
source_code_uri: https://github.com/LegionIO/lex-cognitive-load
|
|
55
|
+
documentation_uri: https://github.com/LegionIO/lex-cognitive-load
|
|
56
|
+
changelog_uri: https://github.com/LegionIO/lex-cognitive-load
|
|
57
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-cognitive-load/issues
|
|
58
|
+
rubygems_mfa_required: 'true'
|
|
59
|
+
rdoc_options: []
|
|
60
|
+
require_paths:
|
|
61
|
+
- lib
|
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3.4'
|
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0'
|
|
72
|
+
requirements: []
|
|
73
|
+
rubygems_version: 3.6.9
|
|
74
|
+
specification_version: 4
|
|
75
|
+
summary: LEX Cognitive Load
|
|
76
|
+
test_files: []
|