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 +7 -0
- data/.github/workflows/ci.yml +16 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +37 -0
- data/CLAUDE.md +94 -0
- data/Gemfile +13 -0
- data/README.md +58 -0
- data/lex-attention-switching.gemspec +31 -0
- data/lib/legion/extensions/attention_switching/client.rb +15 -0
- data/lib/legion/extensions/attention_switching/helpers/constants.rb +60 -0
- data/lib/legion/extensions/attention_switching/helpers/switch_event.rb +56 -0
- data/lib/legion/extensions/attention_switching/helpers/switching_engine.rb +168 -0
- data/lib/legion/extensions/attention_switching/helpers/task_set.rb +87 -0
- data/lib/legion/extensions/attention_switching/runners/attention_switching.rb +88 -0
- data/lib/legion/extensions/attention_switching/version.rb +9 -0
- data/lib/legion/extensions/attention_switching.rb +17 -0
- metadata +77 -0
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
data/.rspec
ADDED
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
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,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: []
|