lex-attention-switching 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: ba17254c5d16fcc1f2d51382f1e19329a63a59aa3a0efe4ca92020399c92cce3
4
+ data.tar.gz: 754942a7882a83c4fdaf587ed0cb51e064976959df5bb5b7872c3a2d6a80e766
5
+ SHA512:
6
+ metadata.gz: 8694f9c9d584430f5a067236d00ce44de7c68a03cac09ae213596bb4ee45a5cbfb3b0d2ed1629a4dcbed66c727bfcc69e279137e6594c7b8d63efecf7622b4a2
7
+ data.tar.gz: 95268bed20891af1165812d9c4a7fd09867c3071d4ed5beb3eb7f0961874dac587e7122b0f745741008a93a8b2aa6d10d3d6903467ac63c51c2bcccb35e19ae4
@@ -0,0 +1,16 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ ci:
9
+ uses: LegionIO/.github/.github/workflows/ci.yml@main
10
+
11
+ release:
12
+ needs: ci
13
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
14
+ uses: LegionIO/.github/.github/workflows/release.yml@main
15
+ secrets:
16
+ rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ *.gem
10
+ Gemfile.lock
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,37 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 3.4
4
+
5
+ Style/Documentation:
6
+ Enabled: false
7
+
8
+ Naming/PredicateMethod:
9
+ Enabled: false
10
+
11
+ Naming/PredicatePrefix:
12
+ Enabled: false
13
+
14
+ Metrics/ClassLength:
15
+ Max: 150
16
+
17
+ Metrics/MethodLength:
18
+ Max: 25
19
+
20
+ Metrics/AbcSize:
21
+ Max: 25
22
+
23
+ Metrics/ParameterLists:
24
+ Max: 8
25
+ MaxOptionalParameters: 8
26
+
27
+ Layout/HashAlignment:
28
+ EnforcedColonStyle: table
29
+ EnforcedHashRocketStyle: table
30
+
31
+ Metrics/BlockLength:
32
+ Exclude:
33
+ - 'spec/**/*'
34
+
35
+ Style/OneClassPerFile:
36
+ Exclude:
37
+ - 'spec/spec_helper.rb'
data/CLAUDE.md ADDED
@@ -0,0 +1,94 @@
1
+ # lex-attention-switching
2
+
3
+ **Level 3 Documentation**
4
+ - **Parent**: `/Users/miverso2/rubymine/legion/extensions-agentic/CLAUDE.md`
5
+ - **Grandparent**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
6
+
7
+ ## Purpose
8
+
9
+ Models the cognitive cost of switching between tasks including residual activation, warmup time, context restoration, and practice effects. Based on task-switching research showing that switching tasks incurs a real performance cost — residual activation from the previous task persists and interferes with the new one.
10
+
11
+ ## Gem Info
12
+
13
+ - **Gem name**: `lex-attention-switching`
14
+ - **Version**: `0.1.0`
15
+ - **Module**: `Legion::Extensions::AttentionSwitching`
16
+ - **Ruby**: `>= 3.4`
17
+ - **License**: MIT
18
+
19
+ ## File Structure
20
+
21
+ ```
22
+ lib/legion/extensions/attention_switching/
23
+ attention_switching.rb # Main extension module
24
+ version.rb # VERSION = '0.1.0'
25
+ client.rb # Client wrapper
26
+ helpers/
27
+ constants.rb # Switch costs, residual decay, warmup rate, task types, labels
28
+ task_set.rb # TaskSet value object (readiness, residual, practice count)
29
+ switch_event.rb # SwitchEvent value object
30
+ switching_engine.rb # SwitchingEngine — manages tasks, switch history, cost modeling
31
+ runners/
32
+ attention_switching.rb # Runner module with 10 public methods
33
+ spec/
34
+ (spec files)
35
+ ```
36
+
37
+ ## Key Constants
38
+
39
+ ```ruby
40
+ MAX_TASK_SETS = 100
41
+ MAX_SWITCH_EVENTS = 500
42
+ DEFAULT_SWITCH_COST = 0.3
43
+ RESIDUAL_DECAY_RATE = 0.1 # residual activation decays per tick
44
+ WARMUP_RATE = 0.15 # readiness increases per warmup call
45
+ CONTEXT_RESTORATION_COST = 0.2 # penalty when returning to a previous task
46
+ PRACTICE_REDUCTION = 0.01 # switch cost reduces with practice
47
+ HIGH_COST_THRESHOLD = 0.6
48
+ LOW_COST_THRESHOLD = 0.2
49
+ READY_THRESHOLD = 0.8
50
+ TASK_SET_TYPES = %i[analytical creative social procedural perceptual linguistic spatial emotional]
51
+ COST_LABELS = { (0.8..) => :prohibitive, ... (..0.2) => :negligible }
52
+ READINESS_LABELS = { (0.8..) => :fully_ready, ... (..0.2) => :unprepared }
53
+ RESIDUAL_LABELS = { (0.8..) => :overwhelming, ... (..0.2) => :negligible }
54
+ ```
55
+
56
+ ## Runners
57
+
58
+ ### `Runners::AttentionSwitching`
59
+
60
+ Methods accept optional `engine:` parameter (defaults to `@default_engine`), allowing test injection.
61
+
62
+ - `register_task(name:, task_type: :analytical, complexity: 0.5, engine: nil)` — register a task set with type and complexity
63
+ - `switch_to(task_id:, engine: nil)` — switch to a task; computes switch cost including residual and context restoration; records SwitchEvent
64
+ - `warmup(engine: nil)` — warm up the active task (increases readiness)
65
+ - `decay_residuals(engine: nil)` — decay residual activation for all tasks with residual
66
+ - `active_task(engine: nil)` — returns the currently active task
67
+ - `residual_tasks(engine: nil)` — tasks with non-negligible residual activation
68
+ - `recent_switches(limit: 10, engine: nil)` — recent switch events
69
+ - `average_switch_cost(engine: nil)` — average cost across all recorded switches
70
+ - `switch_cost_between(from_id:, to_id:, engine: nil)` — historical average cost for a specific pair
71
+ - `switching_report(engine: nil)` — comprehensive report
72
+ - `status(engine: nil)` — full state hash
73
+
74
+ ## Helpers
75
+
76
+ ### `Helpers::SwitchingEngine`
77
+ Core engine. `switch_to` computes cost as: `base_cost - (practice_count * PRACTICE_REDUCTION) + context_restoration_penalty`. Records SwitchEvent. Sets residual on the departed task. `decay_all_residuals!` reduces all tasks' residual by `RESIDUAL_DECAY_RATE`.
78
+
79
+ ### `Helpers::TaskSet`
80
+ Value object: name, task_type, complexity, readiness (0–1), residual_activation (0–1), practice_count, is_active.
81
+
82
+ ### `Helpers::SwitchEvent`
83
+ Value object: from_task, to_task, cost, timestamp.
84
+
85
+ ## Integration Points
86
+
87
+ No actor defined — callers must invoke `decay_residuals` periodically. Integrates with lex-tick mode switching: when lex-cortex changes cognitive modes (dormant → full_active), the switching cost models how long it takes to reach full cognitive readiness. `warmup` models the ramp-up time for new task contexts. `switch_cost_between` informs scheduling decisions about task sequencing.
88
+
89
+ ## Development Notes
90
+
91
+ - `engine:` parameter pattern allows passing a test double without monkey-patching
92
+ - Context restoration cost applies when switching to a task previously active in the same session (residual > 0)
93
+ - Practice reduces cost over repeated switches between the same pair — models procedural learning
94
+ - `RESIDUAL_DECAY_RATE = 0.1` means full decay requires ~10 calls to `decay_residuals`
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem 'rspec', '~> 3.13'
9
+ gem 'rubocop', '~> 1.75'
10
+ gem 'rubocop-rspec'
11
+ end
12
+
13
+ gem 'legion-gaia', path: '../../legion-gaia'
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # lex-attention-switching
2
+
3
+ Attention task-switching cost modeling for LegionIO — residual activation, warmup time, context restoration, and practice effects.
4
+
5
+ ## What It Does
6
+
7
+ Models the real cognitive cost of switching between tasks. When an agent switches from one task type to another, residual activation from the previous task persists and interferes with the new one. Readiness for the new task must build up through warmup. Repeated switching between the same pair reduces the cost through practice. Returning to a previously active task incurs an additional context restoration cost.
8
+
9
+ ## Core Concept: Switch Cost
10
+
11
+ ```ruby
12
+ switch_cost = base_cost - (practice_count * 0.01) + context_restoration_if_returning
13
+ # e.g., first switch: 0.30, tenth switch between same pair: 0.20
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```ruby
19
+ client = Legion::Extensions::AttentionSwitching::Client.new
20
+
21
+ # Register task types
22
+ analytical = client.register_task(name: :code_review, task_type: :analytical, complexity: 0.7)
23
+ social = client.register_task(name: :team_meeting, task_type: :social, complexity: 0.4)
24
+
25
+ # Switch tasks
26
+ result = client.switch_to(task_id: social[:task][:id])
27
+ # => { cost: 0.3, cost_label: :moderate, residual: 0.3, readiness: 0.1 }
28
+
29
+ # Warm up for the new task
30
+ client.warmup
31
+ client.warmup # readiness increases
32
+
33
+ # Check for lingering residual from the previous task
34
+ client.residual_tasks
35
+ # => { tasks: [{ name: :code_review, residual_activation: 0.2 }] }
36
+
37
+ # Decay residuals between ticks
38
+ client.decay_residuals
39
+
40
+ # Historical cost between specific tasks
41
+ client.switch_cost_between(from_id: analytical[:task][:id], to_id: social[:task][:id])
42
+ ```
43
+
44
+ ## Integration
45
+
46
+ Wire into lex-tick mode transitions to model cognitive warmup time when switching between dormant and full_active modes. Use `average_switch_cost` to inform task scheduling: avoid frequent task-type switches when processing budget is limited.
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ bundle install
52
+ bundle exec rspec
53
+ bundle exec rubocop
54
+ ```
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/attention_switching/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-attention-switching'
7
+ spec.version = Legion::Extensions::AttentionSwitching::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'Attention task-switching cost modeling for LegionIO'
12
+ spec.description = 'Models the cognitive cost of switching between tasks including residual activation, ' \
13
+ 'warmup time, context restoration, and practice effects.'
14
+ spec.homepage = 'https://github.com/LegionIO/lex-attention-switching'
15
+ spec.license = 'MIT'
16
+
17
+ spec.required_ruby_version = '>= 3.4'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-attention-switching'
21
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-attention-switching/blob/master/README.md'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-attention-switching/blob/master/CHANGELOG.md'
23
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-attention-switching/issues'
24
+ spec.metadata['rubygems_mfa_required'] = 'true'
25
+
26
+ spec.files = Dir.chdir(__dir__) do
27
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
28
+ end
29
+ spec.require_paths = ['lib']
30
+ spec.add_development_dependency 'legion-gaia'
31
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module AttentionSwitching
6
+ class Client
7
+ include Runners::AttentionSwitching
8
+
9
+ def initialize(engine: nil)
10
+ @default_engine = engine || Helpers::SwitchingEngine.new
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module AttentionSwitching
6
+ module Helpers
7
+ module Constants
8
+ # Limits
9
+ MAX_TASK_SETS = 100
10
+ MAX_SWITCH_EVENTS = 500
11
+
12
+ # Switch dynamics
13
+ DEFAULT_SWITCH_COST = 0.3
14
+ RESIDUAL_DECAY_RATE = 0.1
15
+ WARMUP_RATE = 0.15
16
+ CONTEXT_RESTORATION_COST = 0.2
17
+ PRACTICE_REDUCTION = 0.01
18
+
19
+ # Thresholds
20
+ HIGH_COST_THRESHOLD = 0.6
21
+ LOW_COST_THRESHOLD = 0.2
22
+ READY_THRESHOLD = 0.8
23
+
24
+ # Task set types
25
+ TASK_SET_TYPES = %i[
26
+ analytical creative social procedural
27
+ perceptual linguistic spatial emotional
28
+ ].freeze
29
+
30
+ # Switch cost labels
31
+ COST_LABELS = {
32
+ (0.8..) => :prohibitive,
33
+ (0.6...0.8) => :high,
34
+ (0.4...0.6) => :moderate,
35
+ (0.2...0.4) => :low,
36
+ (..0.2) => :negligible
37
+ }.freeze
38
+
39
+ # Readiness labels
40
+ READINESS_LABELS = {
41
+ (0.8..) => :fully_ready,
42
+ (0.6...0.8) => :mostly_ready,
43
+ (0.4...0.6) => :warming_up,
44
+ (0.2...0.4) => :loading,
45
+ (..0.2) => :unprepared
46
+ }.freeze
47
+
48
+ # Residual activation labels
49
+ RESIDUAL_LABELS = {
50
+ (0.8..) => :overwhelming,
51
+ (0.6...0.8) => :strong,
52
+ (0.4...0.6) => :moderate,
53
+ (0.2...0.4) => :fading,
54
+ (..0.2) => :negligible
55
+ }.freeze
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module AttentionSwitching
8
+ module Helpers
9
+ class SwitchEvent
10
+ include Constants
11
+
12
+ attr_reader :id, :from_task_id, :to_task_id, :switch_cost,
13
+ :residual_interference, :warmup_needed, :created_at
14
+
15
+ def initialize(from_task_id:, to_task_id:, switch_cost:, residual_interference:, warmup_needed:)
16
+ @id = SecureRandom.uuid
17
+ @from_task_id = from_task_id
18
+ @to_task_id = to_task_id
19
+ @switch_cost = switch_cost.to_f.clamp(0.0, 1.0).round(10)
20
+ @residual_interference = residual_interference.to_f.clamp(0.0, 1.0).round(10)
21
+ @warmup_needed = warmup_needed.to_f.clamp(0.0, 1.0).round(10)
22
+ @created_at = Time.now.utc
23
+ end
24
+
25
+ def costly?
26
+ @switch_cost >= HIGH_COST_THRESHOLD
27
+ end
28
+
29
+ def cheap?
30
+ @switch_cost <= LOW_COST_THRESHOLD
31
+ end
32
+
33
+ def cost_label
34
+ match = COST_LABELS.find { |range, _| range.cover?(@switch_cost) }
35
+ match ? match.last : :negligible
36
+ end
37
+
38
+ def to_h
39
+ {
40
+ id: @id,
41
+ from_task_id: @from_task_id,
42
+ to_task_id: @to_task_id,
43
+ switch_cost: @switch_cost,
44
+ cost_label: cost_label,
45
+ costly: costly?,
46
+ cheap: cheap?,
47
+ residual_interference: @residual_interference,
48
+ warmup_needed: @warmup_needed,
49
+ created_at: @created_at
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module AttentionSwitching
6
+ module Helpers
7
+ class SwitchingEngine
8
+ include Constants
9
+
10
+ def initialize
11
+ @task_sets = {}
12
+ @switch_events = []
13
+ @active_task_id = nil
14
+ end
15
+
16
+ def register_task(name:, task_type: :analytical, complexity: 0.5)
17
+ prune_tasks_if_needed
18
+ task = TaskSet.new(name: name, task_type: task_type, complexity: complexity)
19
+ @task_sets[task.id] = task
20
+ task
21
+ end
22
+
23
+ def activate_task(task_id:)
24
+ task = @task_sets[task_id]
25
+ return nil unless task
26
+
27
+ task.activate!
28
+ @active_task_id = task_id
29
+ task
30
+ end
31
+
32
+ def switch_to(task_id:)
33
+ target = @task_sets[task_id]
34
+ return nil unless target
35
+
36
+ current = @task_sets[@active_task_id]
37
+ event = if current && @active_task_id != task_id
38
+ perform_switch(current, target)
39
+ else
40
+ activate_task(task_id: task_id)
41
+ nil
42
+ end
43
+
44
+ @active_task_id = task_id
45
+ { task: target.to_h, switch_event: event&.to_h }
46
+ end
47
+
48
+ def warmup_active
49
+ task = @task_sets[@active_task_id]
50
+ return nil unless task
51
+
52
+ task.warmup!
53
+ end
54
+
55
+ def decay_all_residuals!
56
+ @task_sets.each_value(&:decay_residual!)
57
+ { tasks_decayed: @task_sets.size }
58
+ end
59
+
60
+ def active_task
61
+ @task_sets[@active_task_id]
62
+ end
63
+
64
+ def tasks_with_residual
65
+ @task_sets.values.select(&:has_residual?)
66
+ end
67
+
68
+ def recent_switches(limit: 10)
69
+ @switch_events.last(limit)
70
+ end
71
+
72
+ def average_switch_cost
73
+ return DEFAULT_SWITCH_COST if @switch_events.empty?
74
+
75
+ costs = @switch_events.map(&:switch_cost)
76
+ (costs.sum / costs.size).round(10)
77
+ end
78
+
79
+ def costly_switches
80
+ @switch_events.select(&:costly?)
81
+ end
82
+
83
+ def cheap_switches
84
+ @switch_events.select(&:cheap?)
85
+ end
86
+
87
+ def switch_cost_between(from_id:, to_id:)
88
+ relevant = @switch_events.select { |e| e.from_task_id == from_id && e.to_task_id == to_id }
89
+ return nil if relevant.empty?
90
+
91
+ costs = relevant.map(&:switch_cost)
92
+ (costs.sum / costs.size).round(10)
93
+ end
94
+
95
+ def most_costly_pair
96
+ return nil if @switch_events.empty?
97
+
98
+ @switch_events.max_by(&:switch_cost)
99
+ end
100
+
101
+ def switching_report
102
+ {
103
+ total_tasks: @task_sets.size,
104
+ total_switches: @switch_events.size,
105
+ active_task: active_task&.to_h,
106
+ average_switch_cost: average_switch_cost,
107
+ costly_count: costly_switches.size,
108
+ cheap_count: cheap_switches.size,
109
+ residual_count: tasks_with_residual.size,
110
+ recent_switches: recent_switches(limit: 5).map(&:to_h)
111
+ }
112
+ end
113
+
114
+ def to_h
115
+ {
116
+ total_tasks: @task_sets.size,
117
+ total_switches: @switch_events.size,
118
+ active_task_id: @active_task_id,
119
+ average_switch_cost: average_switch_cost,
120
+ residual_count: tasks_with_residual.size
121
+ }
122
+ end
123
+
124
+ private
125
+
126
+ def perform_switch(current, target)
127
+ current.deactivate!
128
+ cost = compute_switch_cost(current, target)
129
+ warmup = ((1.0 - target.readiness) * target.complexity).round(10)
130
+ target.activate!
131
+
132
+ event = SwitchEvent.new(
133
+ from_task_id: current.id,
134
+ to_task_id: target.id,
135
+ switch_cost: cost,
136
+ residual_interference: current.residual_activation,
137
+ warmup_needed: warmup
138
+ )
139
+ prune_events_if_needed
140
+ @switch_events << event
141
+ event
142
+ end
143
+
144
+ def compute_switch_cost(current, target)
145
+ type_cost = current.task_type == target.task_type ? 0.0 : 0.15
146
+ complexity_cost = ((current.complexity - target.complexity).abs * 0.3).round(10)
147
+ context_cost = CONTEXT_RESTORATION_COST * target.complexity
148
+ practice_bonus = [target.activation_count * PRACTICE_REDUCTION, 0.2].min
149
+
150
+ (DEFAULT_SWITCH_COST + type_cost + complexity_cost +
151
+ (current.residual_activation * 0.2) + context_cost - practice_bonus).clamp(0.0, 1.0).round(10)
152
+ end
153
+
154
+ def prune_tasks_if_needed
155
+ return if @task_sets.size < MAX_TASK_SETS
156
+
157
+ least_used = @task_sets.values.min_by(&:activation_count)
158
+ @task_sets.delete(least_used.id) if least_used
159
+ end
160
+
161
+ def prune_events_if_needed
162
+ @switch_events.shift while @switch_events.size >= MAX_SWITCH_EVENTS
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module AttentionSwitching
8
+ module Helpers
9
+ class TaskSet
10
+ include Constants
11
+
12
+ attr_reader :id, :name, :task_type, :complexity, :readiness,
13
+ :residual_activation, :activation_count, :created_at
14
+
15
+ def initialize(name:, task_type: :analytical, complexity: 0.5)
16
+ @id = SecureRandom.uuid
17
+ @name = name.to_s
18
+ @task_type = task_type.to_sym
19
+ @complexity = complexity.to_f.clamp(0.0, 1.0).round(10)
20
+ @readiness = 0.0
21
+ @residual_activation = 0.0
22
+ @activation_count = 0
23
+ @created_at = Time.now.utc
24
+ end
25
+
26
+ def activate!
27
+ @readiness = 1.0
28
+ @residual_activation = 0.0
29
+ @activation_count += 1
30
+ self
31
+ end
32
+
33
+ def deactivate!
34
+ @residual_activation = @readiness
35
+ @readiness = 0.0
36
+ self
37
+ end
38
+
39
+ def warmup!(amount: WARMUP_RATE)
40
+ @readiness = (@readiness + amount).clamp(0.0, 1.0).round(10)
41
+ self
42
+ end
43
+
44
+ def decay_residual!
45
+ @residual_activation = (@residual_activation - RESIDUAL_DECAY_RATE).clamp(0.0, 1.0).round(10)
46
+ self
47
+ end
48
+
49
+ def ready?
50
+ @readiness >= READY_THRESHOLD
51
+ end
52
+
53
+ def has_residual?
54
+ @residual_activation > 0.1
55
+ end
56
+
57
+ def readiness_label
58
+ match = READINESS_LABELS.find { |range, _| range.cover?(@readiness) }
59
+ match ? match.last : :unprepared
60
+ end
61
+
62
+ def residual_label
63
+ match = RESIDUAL_LABELS.find { |range, _| range.cover?(@residual_activation) }
64
+ match ? match.last : :negligible
65
+ end
66
+
67
+ def to_h
68
+ {
69
+ id: @id,
70
+ name: @name,
71
+ task_type: @task_type,
72
+ complexity: @complexity,
73
+ readiness: @readiness,
74
+ readiness_label: readiness_label,
75
+ residual_activation: @residual_activation,
76
+ residual_label: residual_label,
77
+ ready: ready?,
78
+ has_residual: has_residual?,
79
+ activation_count: @activation_count,
80
+ created_at: @created_at
81
+ }
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module AttentionSwitching
6
+ module Runners
7
+ module AttentionSwitching
8
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
9
+
10
+ def register_task(name:, task_type: :analytical, complexity: 0.5, engine: nil, **)
11
+ eng = engine || default_engine
12
+ task = eng.register_task(name: name, task_type: task_type, complexity: complexity)
13
+ { success: true, task: task.to_h }
14
+ end
15
+
16
+ def switch_to(task_id:, engine: nil, **)
17
+ eng = engine || default_engine
18
+ result = eng.switch_to(task_id: task_id)
19
+ return { success: false, error: 'task not found' } unless result
20
+
21
+ { success: true, **result }
22
+ end
23
+
24
+ def warmup(engine: nil, **)
25
+ eng = engine || default_engine
26
+ task = eng.warmup_active
27
+ return { success: false, error: 'no active task' } unless task
28
+
29
+ { success: true, task: task.to_h }
30
+ end
31
+
32
+ def decay_residuals(engine: nil, **)
33
+ eng = engine || default_engine
34
+ result = eng.decay_all_residuals!
35
+ { success: true, **result }
36
+ end
37
+
38
+ def active_task(engine: nil, **)
39
+ eng = engine || default_engine
40
+ task = eng.active_task
41
+ return { success: false, error: 'no active task' } unless task
42
+
43
+ { success: true, task: task.to_h }
44
+ end
45
+
46
+ def residual_tasks(engine: nil, **)
47
+ eng = engine || default_engine
48
+ { success: true, tasks: eng.tasks_with_residual.map(&:to_h) }
49
+ end
50
+
51
+ def recent_switches(limit: 10, engine: nil, **)
52
+ eng = engine || default_engine
53
+ { success: true, switches: eng.recent_switches(limit: limit).map(&:to_h) }
54
+ end
55
+
56
+ def average_switch_cost(engine: nil, **)
57
+ eng = engine || default_engine
58
+ { success: true, average_cost: eng.average_switch_cost }
59
+ end
60
+
61
+ def switch_cost_between(from_id:, to_id:, engine: nil, **)
62
+ eng = engine || default_engine
63
+ cost = eng.switch_cost_between(from_id: from_id, to_id: to_id)
64
+ return { success: false, error: 'no switches found for this pair' } unless cost
65
+
66
+ { success: true, cost: cost }
67
+ end
68
+
69
+ def switching_report(engine: nil, **)
70
+ eng = engine || default_engine
71
+ { success: true, report: eng.switching_report }
72
+ end
73
+
74
+ def status(engine: nil, **)
75
+ eng = engine || default_engine
76
+ { success: true, **eng.to_h }
77
+ end
78
+
79
+ private
80
+
81
+ def default_engine
82
+ @default_engine ||= Helpers::SwitchingEngine.new
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module AttentionSwitching
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'attention_switching/version'
4
+ require_relative 'attention_switching/helpers/constants'
5
+ require_relative 'attention_switching/helpers/task_set'
6
+ require_relative 'attention_switching/helpers/switch_event'
7
+ require_relative 'attention_switching/helpers/switching_engine'
8
+ require_relative 'attention_switching/runners/attention_switching'
9
+ require_relative 'attention_switching/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module AttentionSwitching
14
+ extend Legion::Extensions::Core if defined?(Legion::Extensions::Core)
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-attention-switching
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: Models the cognitive cost of switching between tasks including residual
27
+ activation, warmup time, context restoration, and practice effects.
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".github/workflows/ci.yml"
35
+ - ".gitignore"
36
+ - ".rspec"
37
+ - ".rubocop.yml"
38
+ - CLAUDE.md
39
+ - Gemfile
40
+ - README.md
41
+ - lex-attention-switching.gemspec
42
+ - lib/legion/extensions/attention_switching.rb
43
+ - lib/legion/extensions/attention_switching/client.rb
44
+ - lib/legion/extensions/attention_switching/helpers/constants.rb
45
+ - lib/legion/extensions/attention_switching/helpers/switch_event.rb
46
+ - lib/legion/extensions/attention_switching/helpers/switching_engine.rb
47
+ - lib/legion/extensions/attention_switching/helpers/task_set.rb
48
+ - lib/legion/extensions/attention_switching/runners/attention_switching.rb
49
+ - lib/legion/extensions/attention_switching/version.rb
50
+ homepage: https://github.com/LegionIO/lex-attention-switching
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ homepage_uri: https://github.com/LegionIO/lex-attention-switching
55
+ source_code_uri: https://github.com/LegionIO/lex-attention-switching
56
+ documentation_uri: https://github.com/LegionIO/lex-attention-switching/blob/master/README.md
57
+ changelog_uri: https://github.com/LegionIO/lex-attention-switching/blob/master/CHANGELOG.md
58
+ bug_tracker_uri: https://github.com/LegionIO/lex-attention-switching/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: Attention task-switching cost modeling for LegionIO
77
+ test_files: []