lex-global-workspace 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 +11 -0
- data/lex-global-workspace.gemspec +32 -0
- data/lib/legion/extensions/global_workspace/actors/competition.rb +41 -0
- data/lib/legion/extensions/global_workspace/client.rb +25 -0
- data/lib/legion/extensions/global_workspace/helpers/broadcast.rb +58 -0
- data/lib/legion/extensions/global_workspace/helpers/competitor.rb +55 -0
- data/lib/legion/extensions/global_workspace/helpers/constants.rb +61 -0
- data/lib/legion/extensions/global_workspace/helpers/workspace.rb +184 -0
- data/lib/legion/extensions/global_workspace/runners/global_workspace.rb +100 -0
- data/lib/legion/extensions/global_workspace/version.rb +9 -0
- data/lib/legion/extensions/global_workspace.rb +17 -0
- data/spec/legion/extensions/global_workspace/client_spec.rb +49 -0
- data/spec/legion/extensions/global_workspace/helpers/broadcast_spec.rb +83 -0
- data/spec/legion/extensions/global_workspace/helpers/competitor_spec.rb +84 -0
- data/spec/legion/extensions/global_workspace/helpers/workspace_spec.rb +174 -0
- data/spec/legion/extensions/global_workspace/runners/global_workspace_spec.rb +118 -0
- data/spec/spec_helper.rb +20 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7e43f7f4fe9651e9509cdc5b2c92212b617571f81996118c258bf91f4226c2a7
|
|
4
|
+
data.tar.gz: a9a08a51d92c11f4d1480e50b6768202f498c7ec179d9e589104d7aa3bb9ffa5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: aa1b26d8aed0354075e509ea803f819b344d60c70686399c9277e12083b1b059671d36b4d11511710633d2d8bd6111ae726605c96109141c7bb3fc0fc4a77b43
|
|
7
|
+
data.tar.gz: 363e6dc2064a884834078c146f1310ef266714dc61295e12d3bca341a6c2f730dd5cb8ccae5cdbe6e2be86e401c4b454b22f4b30ce96534031c137fafe881099
|
data/Gemfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/global_workspace/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-global-workspace'
|
|
7
|
+
spec.version = Legion::Extensions::GlobalWorkspace::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Global Workspace'
|
|
12
|
+
spec.description = "Baars' Global Workspace Theory for brain-modeled agentic AI — information " \
|
|
13
|
+
'competes for access to a limited-capacity workspace; winners are broadcast to ' \
|
|
14
|
+
'all subscribed cognitive subsystems, implementing a computational model of ' \
|
|
15
|
+
'conscious access and attentional bottleneck.'
|
|
16
|
+
spec.homepage = 'https://github.com/LegionIO/lex-global-workspace'
|
|
17
|
+
spec.license = 'MIT'
|
|
18
|
+
spec.required_ruby_version = '>= 3.4'
|
|
19
|
+
|
|
20
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
21
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-global-workspace'
|
|
22
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-global-workspace'
|
|
23
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-global-workspace'
|
|
24
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-global-workspace/issues'
|
|
25
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
26
|
+
|
|
27
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
28
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-global-workspace.gemspec Gemfile]
|
|
29
|
+
end
|
|
30
|
+
spec.require_paths = ['lib']
|
|
31
|
+
spec.add_development_dependency 'legion-gaia'
|
|
32
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/actors/every'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module GlobalWorkspace
|
|
8
|
+
module Actor
|
|
9
|
+
class Competition < Legion::Extensions::Actors::Every
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::GlobalWorkspace::Runners::GlobalWorkspace
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'update_global_workspace'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def time
|
|
19
|
+
2
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run_now?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def use_runner?
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def check_subtask?
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def generate_task?
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/global_workspace/helpers/constants'
|
|
4
|
+
require 'legion/extensions/global_workspace/helpers/broadcast'
|
|
5
|
+
require 'legion/extensions/global_workspace/helpers/competitor'
|
|
6
|
+
require 'legion/extensions/global_workspace/helpers/workspace'
|
|
7
|
+
require 'legion/extensions/global_workspace/runners/global_workspace'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module GlobalWorkspace
|
|
12
|
+
class Client
|
|
13
|
+
include Runners::GlobalWorkspace
|
|
14
|
+
|
|
15
|
+
def initialize(workspace: nil, **)
|
|
16
|
+
@workspace = workspace || Helpers::Workspace.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :workspace
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module GlobalWorkspace
|
|
6
|
+
module Helpers
|
|
7
|
+
class Broadcast
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :content, :source, :domain, :salience, :coalition,
|
|
11
|
+
:broadcast_at, :received_by
|
|
12
|
+
|
|
13
|
+
def initialize(content:, source:, domain:, salience:, coalition: [])
|
|
14
|
+
@content = content
|
|
15
|
+
@source = source
|
|
16
|
+
@domain = domain
|
|
17
|
+
@salience = salience.to_f.clamp(0.0, 1.0)
|
|
18
|
+
@coalition = Array(coalition).first(MAX_COALITION_SIZE)
|
|
19
|
+
@broadcast_at = Time.now.utc
|
|
20
|
+
@received_by = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def acknowledge(subscriber_id)
|
|
24
|
+
@received_by << subscriber_id unless @received_by.include?(subscriber_id)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def expired?
|
|
28
|
+
(Time.now.utc - @broadcast_at) > BROADCAST_TTL
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def age
|
|
32
|
+
Time.now.utc - @broadcast_at
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def label
|
|
36
|
+
SALIENCE_LABELS.each { |range, lbl| return lbl if range.cover?(@salience) }
|
|
37
|
+
:subliminal
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
content: @content,
|
|
43
|
+
source: @source,
|
|
44
|
+
domain: @domain,
|
|
45
|
+
salience: @salience.round(4),
|
|
46
|
+
label: label,
|
|
47
|
+
coalition: @coalition,
|
|
48
|
+
broadcast_at: @broadcast_at,
|
|
49
|
+
age: age.round(2),
|
|
50
|
+
received_by: @received_by.dup,
|
|
51
|
+
expired: expired?
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module GlobalWorkspace
|
|
6
|
+
module Helpers
|
|
7
|
+
class Competitor
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :content, :source, :domain, :coalition, :submitted_at
|
|
11
|
+
attr_accessor :salience, :urgency
|
|
12
|
+
|
|
13
|
+
def initialize(content:, source:, domain:, salience:, coalition: [])
|
|
14
|
+
@content = content
|
|
15
|
+
@source = source
|
|
16
|
+
@domain = domain
|
|
17
|
+
@salience = salience.to_f.clamp(0.0, 1.0)
|
|
18
|
+
@coalition = Array(coalition).first(MAX_COALITION_SIZE)
|
|
19
|
+
@urgency = 0.0
|
|
20
|
+
@submitted_at = Time.now.utc
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def effective_salience
|
|
24
|
+
(@salience + @urgency).clamp(0.0, 1.0)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def decay
|
|
28
|
+
@salience = [@salience - SALIENCE_DECAY, 0.0].max
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def boost_urgency
|
|
32
|
+
@urgency = [@urgency + URGENCY_BOOST, MAX_URGENCY].min
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def below_threshold?
|
|
36
|
+
effective_salience < COMPETITION_THRESHOLD
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
content: @content,
|
|
42
|
+
source: @source,
|
|
43
|
+
domain: @domain,
|
|
44
|
+
salience: @salience.round(4),
|
|
45
|
+
urgency: @urgency.round(4),
|
|
46
|
+
effective_salience: effective_salience.round(4),
|
|
47
|
+
coalition: @coalition,
|
|
48
|
+
submitted_at: @submitted_at
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module GlobalWorkspace
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
# Maximum items that can compete for the workspace in one cycle
|
|
9
|
+
MAX_COMPETITORS = 50
|
|
10
|
+
|
|
11
|
+
# Maximum broadcasts retained in history
|
|
12
|
+
MAX_BROADCAST_HISTORY = 200
|
|
13
|
+
|
|
14
|
+
# Maximum number of coalition members per broadcast
|
|
15
|
+
MAX_COALITION_SIZE = 10
|
|
16
|
+
|
|
17
|
+
# Minimum salience to enter competition
|
|
18
|
+
COMPETITION_THRESHOLD = 0.2
|
|
19
|
+
|
|
20
|
+
# Winner takes all — top competitor must exceed runner-up by this margin
|
|
21
|
+
DOMINANCE_MARGIN = 0.05
|
|
22
|
+
|
|
23
|
+
# How long a broadcast remains "conscious" (seconds)
|
|
24
|
+
BROADCAST_TTL = 10
|
|
25
|
+
|
|
26
|
+
# Salience decay per tick for waiting competitors
|
|
27
|
+
SALIENCE_DECAY = 0.02
|
|
28
|
+
|
|
29
|
+
# Urgency boost per tick for items that keep losing competition
|
|
30
|
+
URGENCY_BOOST = 0.01
|
|
31
|
+
|
|
32
|
+
# Maximum urgency accumulation
|
|
33
|
+
MAX_URGENCY = 0.5
|
|
34
|
+
|
|
35
|
+
# EMA alpha for workspace utilization tracking
|
|
36
|
+
UTILIZATION_ALPHA = 0.1
|
|
37
|
+
|
|
38
|
+
# Maximum registered subscribers
|
|
39
|
+
MAX_SUBSCRIBERS = 50
|
|
40
|
+
|
|
41
|
+
# Labels for workspace state
|
|
42
|
+
WORKSPACE_STATE_LABELS = {
|
|
43
|
+
broadcasting: 'actively broadcasting content',
|
|
44
|
+
idle: 'workspace empty, awaiting input',
|
|
45
|
+
contention: 'multiple items competing for access',
|
|
46
|
+
saturated: 'high utilization, processing backlog'
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
# Labels for broadcast salience
|
|
50
|
+
SALIENCE_LABELS = {
|
|
51
|
+
(0.8..) => :dominant,
|
|
52
|
+
(0.6...0.8) => :salient,
|
|
53
|
+
(0.4...0.6) => :moderate,
|
|
54
|
+
(0.2...0.4) => :marginal,
|
|
55
|
+
(..0.2) => :subliminal
|
|
56
|
+
}.freeze
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module GlobalWorkspace
|
|
6
|
+
module Helpers
|
|
7
|
+
class Workspace
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :competitors, :broadcast_history, :subscribers, :current_broadcast, :utilization
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@competitors = []
|
|
14
|
+
@broadcast_history = []
|
|
15
|
+
@subscribers = {} # id => { name:, domains: [] }
|
|
16
|
+
@current_broadcast = nil
|
|
17
|
+
@utilization = 0.0 # EMA of workspace usage
|
|
18
|
+
@competition_count = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# --- Subscriber Management ---
|
|
22
|
+
|
|
23
|
+
def register_subscriber(id:, name:, domains: [])
|
|
24
|
+
return false if @subscribers.size >= MAX_SUBSCRIBERS && !@subscribers.key?(id)
|
|
25
|
+
|
|
26
|
+
@subscribers[id] = { name: name, domains: Array(domains), registered_at: Time.now.utc }
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def unregister_subscriber(id:)
|
|
31
|
+
!@subscribers.delete(id).nil?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# --- Competition ---
|
|
35
|
+
|
|
36
|
+
def submit(content:, source:, domain:, salience:, coalition: [])
|
|
37
|
+
return nil if salience.to_f < COMPETITION_THRESHOLD
|
|
38
|
+
|
|
39
|
+
@competitors.shift while @competitors.size >= MAX_COMPETITORS
|
|
40
|
+
competitor = Competitor.new(
|
|
41
|
+
content: content,
|
|
42
|
+
source: source,
|
|
43
|
+
domain: domain,
|
|
44
|
+
salience: salience,
|
|
45
|
+
coalition: coalition
|
|
46
|
+
)
|
|
47
|
+
@competitors << competitor
|
|
48
|
+
competitor
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def compete
|
|
52
|
+
@competition_count += 1
|
|
53
|
+
expire_current_broadcast
|
|
54
|
+
prune_weak_competitors
|
|
55
|
+
|
|
56
|
+
return nil if @competitors.empty?
|
|
57
|
+
|
|
58
|
+
sorted = @competitors.sort_by { |c| -c.effective_salience }
|
|
59
|
+
winner = sorted.first
|
|
60
|
+
|
|
61
|
+
if sorted.size > 1
|
|
62
|
+
runner_up = sorted[1]
|
|
63
|
+
unless (winner.effective_salience - runner_up.effective_salience) >= DOMINANCE_MARGIN
|
|
64
|
+
boost_losers
|
|
65
|
+
update_utilization(busy: true)
|
|
66
|
+
return nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@competitors.delete(winner)
|
|
71
|
+
broadcast = create_broadcast(winner)
|
|
72
|
+
@current_broadcast = broadcast
|
|
73
|
+
|
|
74
|
+
boost_losers
|
|
75
|
+
update_utilization(busy: true)
|
|
76
|
+
|
|
77
|
+
broadcast
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# --- Query ---
|
|
81
|
+
|
|
82
|
+
def conscious?(content)
|
|
83
|
+
return false unless @current_broadcast && !@current_broadcast.expired?
|
|
84
|
+
|
|
85
|
+
@current_broadcast.content == content
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def current_content
|
|
89
|
+
return nil unless @current_broadcast && !@current_broadcast.expired?
|
|
90
|
+
|
|
91
|
+
@current_broadcast.to_h
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def acknowledge(subscriber_id:)
|
|
95
|
+
return false unless @current_broadcast && !@current_broadcast.expired?
|
|
96
|
+
|
|
97
|
+
@current_broadcast.acknowledge(subscriber_id)
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# --- Workspace State ---
|
|
102
|
+
|
|
103
|
+
def state
|
|
104
|
+
if @current_broadcast && !@current_broadcast.expired?
|
|
105
|
+
:broadcasting
|
|
106
|
+
elsif @competitors.size > 1
|
|
107
|
+
:contention
|
|
108
|
+
elsif @utilization > 0.7
|
|
109
|
+
:saturated
|
|
110
|
+
else
|
|
111
|
+
:idle
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def subscriber_count
|
|
116
|
+
@subscribers.size
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def competitor_count
|
|
120
|
+
@competitors.size
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# --- Tick / Maintenance ---
|
|
124
|
+
|
|
125
|
+
def tick
|
|
126
|
+
expire_current_broadcast
|
|
127
|
+
decay_competitors
|
|
128
|
+
prune_weak_competitors
|
|
129
|
+
update_utilization(busy: !@competitors.empty? || (@current_broadcast && !@current_broadcast.expired?))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def to_h
|
|
133
|
+
{
|
|
134
|
+
state: state,
|
|
135
|
+
state_label: WORKSPACE_STATE_LABELS[state],
|
|
136
|
+
subscribers: @subscribers.size,
|
|
137
|
+
competitors: @competitors.size,
|
|
138
|
+
broadcast_history: @broadcast_history.size,
|
|
139
|
+
utilization: @utilization.round(4),
|
|
140
|
+
competitions: @competition_count,
|
|
141
|
+
current_broadcast: @current_broadcast&.expired? == false ? @current_broadcast.to_h : nil
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def create_broadcast(winner)
|
|
148
|
+
broadcast = Broadcast.new(
|
|
149
|
+
content: winner.content,
|
|
150
|
+
source: winner.source,
|
|
151
|
+
domain: winner.domain,
|
|
152
|
+
salience: winner.effective_salience,
|
|
153
|
+
coalition: winner.coalition
|
|
154
|
+
)
|
|
155
|
+
@broadcast_history << broadcast
|
|
156
|
+
@broadcast_history.shift while @broadcast_history.size > MAX_BROADCAST_HISTORY
|
|
157
|
+
broadcast
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def expire_current_broadcast
|
|
161
|
+
@current_broadcast = nil if @current_broadcast&.expired?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def prune_weak_competitors
|
|
165
|
+
@competitors.reject!(&:below_threshold?)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def decay_competitors
|
|
169
|
+
@competitors.each(&:decay)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def boost_losers
|
|
173
|
+
@competitors.each(&:boost_urgency)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def update_utilization(busy:)
|
|
177
|
+
sample = busy ? 1.0 : 0.0
|
|
178
|
+
@utilization += (UTILIZATION_ALPHA * (sample - @utilization))
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module GlobalWorkspace
|
|
6
|
+
module Runners
|
|
7
|
+
module GlobalWorkspace
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def submit_for_broadcast(content:, source:, domain:, salience:, coalition: [], **)
|
|
12
|
+
Legion::Logging.debug "[global_workspace] submit: source=#{source} domain=#{domain} salience=#{salience}"
|
|
13
|
+
competitor = workspace.submit(
|
|
14
|
+
content: content,
|
|
15
|
+
source: source,
|
|
16
|
+
domain: domain,
|
|
17
|
+
salience: salience,
|
|
18
|
+
coalition: coalition
|
|
19
|
+
)
|
|
20
|
+
if competitor
|
|
21
|
+
{ success: true, effective_salience: competitor.effective_salience.round(4),
|
|
22
|
+
competitors: workspace.competitor_count }
|
|
23
|
+
else
|
|
24
|
+
{ success: false, reason: :below_threshold, competitors: workspace.competitor_count }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run_competition(**)
|
|
29
|
+
Legion::Logging.debug '[global_workspace] run_competition'
|
|
30
|
+
broadcast = workspace.compete
|
|
31
|
+
if broadcast
|
|
32
|
+
{
|
|
33
|
+
success: true,
|
|
34
|
+
broadcast: broadcast.to_h,
|
|
35
|
+
state: workspace.state,
|
|
36
|
+
remaining: workspace.competitor_count
|
|
37
|
+
}
|
|
38
|
+
else
|
|
39
|
+
{ success: false, reason: :no_winner, state: workspace.state, competitors: workspace.competitor_count }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def register_subscriber(id:, name:, domains: [], **)
|
|
44
|
+
Legion::Logging.debug "[global_workspace] register_subscriber: id=#{id} name=#{name}"
|
|
45
|
+
registered = workspace.register_subscriber(id: id, name: name, domains: domains)
|
|
46
|
+
{ success: registered, subscriber_count: workspace.subscriber_count }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def unregister_subscriber(id:, **)
|
|
50
|
+
Legion::Logging.debug "[global_workspace] unregister_subscriber: id=#{id}"
|
|
51
|
+
removed = workspace.unregister_subscriber(id: id)
|
|
52
|
+
{ success: removed, subscriber_count: workspace.subscriber_count }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def acknowledge_broadcast(subscriber_id:, **)
|
|
56
|
+
Legion::Logging.debug "[global_workspace] acknowledge: subscriber=#{subscriber_id}"
|
|
57
|
+
ack = workspace.acknowledge(subscriber_id: subscriber_id)
|
|
58
|
+
{ success: ack }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def query_consciousness(content:, **)
|
|
62
|
+
is_conscious = workspace.conscious?(content)
|
|
63
|
+
Legion::Logging.debug "[global_workspace] conscious?(#{content}): #{is_conscious}"
|
|
64
|
+
{ success: true, conscious: is_conscious }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def current_broadcast(**)
|
|
68
|
+
content = workspace.current_content
|
|
69
|
+
Legion::Logging.debug "[global_workspace] current_broadcast: #{content ? 'active' : 'none'}"
|
|
70
|
+
{ success: true, broadcast: content }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def broadcast_history(limit: 10, **)
|
|
74
|
+
history = workspace.broadcast_history.last(limit.to_i).map(&:to_h)
|
|
75
|
+
Legion::Logging.debug "[global_workspace] history: #{history.size} entries"
|
|
76
|
+
{ success: true, history: history, total: workspace.broadcast_history.size }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def update_global_workspace(**)
|
|
80
|
+
Legion::Logging.debug '[global_workspace] tick'
|
|
81
|
+
workspace.tick
|
|
82
|
+
{ success: true, state: workspace.state, competitors: workspace.competitor_count,
|
|
83
|
+
utilization: workspace.utilization.round(4) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def workspace_stats(**)
|
|
87
|
+
Legion::Logging.debug '[global_workspace] stats'
|
|
88
|
+
{ success: true, stats: workspace.to_h }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def workspace
|
|
94
|
+
@workspace ||= Helpers::Workspace.new
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/global_workspace/version'
|
|
4
|
+
require 'legion/extensions/global_workspace/helpers/constants'
|
|
5
|
+
require 'legion/extensions/global_workspace/helpers/broadcast'
|
|
6
|
+
require 'legion/extensions/global_workspace/helpers/competitor'
|
|
7
|
+
require 'legion/extensions/global_workspace/helpers/workspace'
|
|
8
|
+
require 'legion/extensions/global_workspace/runners/global_workspace'
|
|
9
|
+
require 'legion/extensions/global_workspace/client'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module GlobalWorkspace
|
|
14
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::GlobalWorkspace::Client do
|
|
4
|
+
subject(:client) { described_class.new }
|
|
5
|
+
|
|
6
|
+
it 'includes Runners::GlobalWorkspace' do
|
|
7
|
+
expect(described_class.ancestors).to include(Legion::Extensions::GlobalWorkspace::Runners::GlobalWorkspace)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'supports full conscious access lifecycle' do
|
|
11
|
+
# Register subscribers (cognitive subsystems)
|
|
12
|
+
client.register_subscriber(id: :memory, name: 'lex-memory', domains: [:all])
|
|
13
|
+
client.register_subscriber(id: :emotion, name: 'lex-emotion', domains: [:safety])
|
|
14
|
+
client.register_subscriber(id: :planning, name: 'lex-planning', domains: [:cognition])
|
|
15
|
+
|
|
16
|
+
# Multiple subsystems submit content for conscious access
|
|
17
|
+
client.submit_for_broadcast(content: 'threat_nearby', source: :emotion, domain: :safety, salience: 0.85)
|
|
18
|
+
client.submit_for_broadcast(content: 'plan_step_3', source: :planning, domain: :cognition, salience: 0.5)
|
|
19
|
+
client.submit_for_broadcast(content: 'memory_trace', source: :memory, domain: :recall, salience: 0.3)
|
|
20
|
+
|
|
21
|
+
# Competition: emotion wins (highest salience, clear margin)
|
|
22
|
+
competition = client.run_competition
|
|
23
|
+
expect(competition[:success]).to be true
|
|
24
|
+
expect(competition[:broadcast][:content]).to eq('threat_nearby')
|
|
25
|
+
|
|
26
|
+
# Verify consciousness
|
|
27
|
+
conscious = client.query_consciousness(content: 'threat_nearby')
|
|
28
|
+
expect(conscious[:conscious]).to be true
|
|
29
|
+
|
|
30
|
+
# Subscribers acknowledge receipt
|
|
31
|
+
client.acknowledge_broadcast(subscriber_id: :memory)
|
|
32
|
+
client.acknowledge_broadcast(subscriber_id: :emotion)
|
|
33
|
+
|
|
34
|
+
# Check current state
|
|
35
|
+
current = client.current_broadcast
|
|
36
|
+
expect(current[:broadcast][:received_by]).to include(:memory, :emotion)
|
|
37
|
+
|
|
38
|
+
# Tick maintenance
|
|
39
|
+
client.update_global_workspace
|
|
40
|
+
|
|
41
|
+
# History preserved
|
|
42
|
+
history = client.broadcast_history(limit: 10)
|
|
43
|
+
expect(history[:total]).to eq(1)
|
|
44
|
+
|
|
45
|
+
# Stats
|
|
46
|
+
stats = client.workspace_stats
|
|
47
|
+
expect(stats[:stats][:subscribers]).to eq(3)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::GlobalWorkspace::Helpers::Broadcast do
|
|
4
|
+
subject(:broadcast) do
|
|
5
|
+
described_class.new(content: 'threat_detected', source: :emotion, domain: :safety, salience: 0.9,
|
|
6
|
+
coalition: %i[fear attention])
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
let(:constants) { Legion::Extensions::GlobalWorkspace::Helpers::Constants }
|
|
10
|
+
|
|
11
|
+
describe '#initialize' do
|
|
12
|
+
it 'sets attributes' do
|
|
13
|
+
expect(broadcast.content).to eq('threat_detected')
|
|
14
|
+
expect(broadcast.source).to eq(:emotion)
|
|
15
|
+
expect(broadcast.domain).to eq(:safety)
|
|
16
|
+
expect(broadcast.salience).to eq(0.9)
|
|
17
|
+
expect(broadcast.coalition).to eq(%i[fear attention])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'clamps salience' do
|
|
21
|
+
high = described_class.new(content: :x, source: :s, domain: :d, salience: 1.5)
|
|
22
|
+
expect(high.salience).to eq(1.0)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'records broadcast_at' do
|
|
26
|
+
expect(broadcast.broadcast_at).to be_a(Time)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'starts with empty received_by' do
|
|
30
|
+
expect(broadcast.received_by).to be_empty
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'limits coalition size' do
|
|
34
|
+
big = described_class.new(content: :x, source: :s, domain: :d, salience: 0.5,
|
|
35
|
+
coalition: (1..20).to_a)
|
|
36
|
+
expect(big.coalition.size).to eq(constants::MAX_COALITION_SIZE)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe '#acknowledge' do
|
|
41
|
+
it 'adds subscriber to received_by' do
|
|
42
|
+
broadcast.acknowledge(:memory)
|
|
43
|
+
expect(broadcast.received_by).to include(:memory)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'does not duplicate' do
|
|
47
|
+
broadcast.acknowledge(:memory)
|
|
48
|
+
broadcast.acknowledge(:memory)
|
|
49
|
+
expect(broadcast.received_by.size).to eq(1)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe '#expired?' do
|
|
54
|
+
it 'returns false when fresh' do
|
|
55
|
+
expect(broadcast.expired?).to be false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe '#age' do
|
|
60
|
+
it 'returns elapsed time' do
|
|
61
|
+
expect(broadcast.age).to be >= 0.0
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe '#label' do
|
|
66
|
+
it 'returns :dominant for high salience' do
|
|
67
|
+
expect(broadcast.label).to eq(:dominant)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'returns :subliminal for low salience' do
|
|
71
|
+
low = described_class.new(content: :x, source: :s, domain: :d, salience: 0.1)
|
|
72
|
+
expect(low.label).to eq(:subliminal)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe '#to_h' do
|
|
77
|
+
it 'returns hash with all fields' do
|
|
78
|
+
h = broadcast.to_h
|
|
79
|
+
expect(h).to include(:content, :source, :domain, :salience, :label, :coalition, :broadcast_at, :age,
|
|
80
|
+
:received_by, :expired)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::GlobalWorkspace::Helpers::Competitor do
|
|
4
|
+
subject(:competitor) do
|
|
5
|
+
described_class.new(content: 'idea', source: :cortex, domain: :cognition, salience: 0.6, coalition: [:attention])
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
let(:constants) { Legion::Extensions::GlobalWorkspace::Helpers::Constants }
|
|
9
|
+
|
|
10
|
+
describe '#initialize' do
|
|
11
|
+
it 'sets attributes' do
|
|
12
|
+
expect(competitor.content).to eq('idea')
|
|
13
|
+
expect(competitor.source).to eq(:cortex)
|
|
14
|
+
expect(competitor.salience).to eq(0.6)
|
|
15
|
+
expect(competitor.urgency).to eq(0.0)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'clamps salience' do
|
|
19
|
+
high = described_class.new(content: :x, source: :s, domain: :d, salience: 2.0)
|
|
20
|
+
expect(high.salience).to eq(1.0)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '#effective_salience' do
|
|
25
|
+
it 'equals salience when no urgency' do
|
|
26
|
+
expect(competitor.effective_salience).to eq(0.6)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'includes urgency' do
|
|
30
|
+
competitor.urgency = 0.2
|
|
31
|
+
expect(competitor.effective_salience).to eq(0.8)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'clamps to 1.0' do
|
|
35
|
+
competitor.salience = 0.9
|
|
36
|
+
competitor.urgency = 0.3
|
|
37
|
+
expect(competitor.effective_salience).to eq(1.0)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '#decay' do
|
|
42
|
+
it 'reduces salience' do
|
|
43
|
+
before = competitor.salience
|
|
44
|
+
competitor.decay
|
|
45
|
+
expect(competitor.salience).to eq(before - constants::SALIENCE_DECAY)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'does not go below 0' do
|
|
49
|
+
competitor.salience = 0.01
|
|
50
|
+
competitor.decay
|
|
51
|
+
expect(competitor.salience).to eq(0.0)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '#boost_urgency' do
|
|
56
|
+
it 'increases urgency' do
|
|
57
|
+
competitor.boost_urgency
|
|
58
|
+
expect(competitor.urgency).to eq(constants::URGENCY_BOOST)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'caps at MAX_URGENCY' do
|
|
62
|
+
50.times { competitor.boost_urgency }
|
|
63
|
+
expect(competitor.urgency).to eq(constants::MAX_URGENCY)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe '#below_threshold?' do
|
|
68
|
+
it 'returns false when above threshold' do
|
|
69
|
+
expect(competitor.below_threshold?).to be false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'returns true when below threshold' do
|
|
73
|
+
competitor.salience = 0.1
|
|
74
|
+
expect(competitor.below_threshold?).to be true
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe '#to_h' do
|
|
79
|
+
it 'returns hash with all fields' do
|
|
80
|
+
h = competitor.to_h
|
|
81
|
+
expect(h).to include(:content, :source, :domain, :salience, :urgency, :effective_salience, :coalition)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::GlobalWorkspace::Helpers::Workspace do
|
|
4
|
+
subject(:ws) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:constants) { Legion::Extensions::GlobalWorkspace::Helpers::Constants }
|
|
7
|
+
|
|
8
|
+
describe '#register_subscriber / #unregister_subscriber' do
|
|
9
|
+
it 'registers a subscriber' do
|
|
10
|
+
expect(ws.register_subscriber(id: :memory, name: 'lex-memory', domains: [:all])).to be true
|
|
11
|
+
expect(ws.subscriber_count).to eq(1)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'unregisters a subscriber' do
|
|
15
|
+
ws.register_subscriber(id: :memory, name: 'lex-memory')
|
|
16
|
+
expect(ws.unregister_subscriber(id: :memory)).to be true
|
|
17
|
+
expect(ws.subscriber_count).to eq(0)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'returns false for unknown unregister' do
|
|
21
|
+
expect(ws.unregister_subscriber(id: :unknown)).to be false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'limits subscribers' do
|
|
25
|
+
constants::MAX_SUBSCRIBERS.times { |i| ws.register_subscriber(id: "sub_#{i}", name: "s#{i}") }
|
|
26
|
+
expect(ws.register_subscriber(id: :overflow, name: 'over')).to be false
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe '#submit' do
|
|
31
|
+
it 'adds a competitor' do
|
|
32
|
+
comp = ws.submit(content: 'idea', source: :cortex, domain: :cognition, salience: 0.6)
|
|
33
|
+
expect(comp).to be_a(Legion::Extensions::GlobalWorkspace::Helpers::Competitor)
|
|
34
|
+
expect(ws.competitor_count).to eq(1)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'rejects below-threshold submissions' do
|
|
38
|
+
result = ws.submit(content: 'weak', source: :s, domain: :d, salience: 0.1)
|
|
39
|
+
expect(result).to be_nil
|
|
40
|
+
expect(ws.competitor_count).to eq(0)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'limits competitor queue' do
|
|
44
|
+
constants::MAX_COMPETITORS.times { |i| ws.submit(content: "c_#{i}", source: :s, domain: :d, salience: 0.5) }
|
|
45
|
+
ws.submit(content: :overflow, source: :s, domain: :d, salience: 0.5)
|
|
46
|
+
expect(ws.competitor_count).to eq(constants::MAX_COMPETITORS)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#compete' do
|
|
51
|
+
it 'returns nil with no competitors' do
|
|
52
|
+
expect(ws.compete).to be_nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'broadcasts the dominant competitor' do
|
|
56
|
+
ws.submit(content: 'strong', source: :emotion, domain: :safety, salience: 0.9)
|
|
57
|
+
ws.submit(content: 'weak', source: :cortex, domain: :cognition, salience: 0.3)
|
|
58
|
+
broadcast = ws.compete
|
|
59
|
+
expect(broadcast).to be_a(Legion::Extensions::GlobalWorkspace::Helpers::Broadcast)
|
|
60
|
+
expect(broadcast.content).to eq('strong')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'returns nil when no clear winner (within margin)' do
|
|
64
|
+
ws.submit(content: 'a', source: :s, domain: :d, salience: 0.5)
|
|
65
|
+
ws.submit(content: 'b', source: :s, domain: :d, salience: 0.5)
|
|
66
|
+
result = ws.compete
|
|
67
|
+
expect(result).to be_nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'boosts urgency of losers' do
|
|
71
|
+
ws.submit(content: 'a', source: :s, domain: :d, salience: 0.5)
|
|
72
|
+
ws.submit(content: 'b', source: :s, domain: :d, salience: 0.5)
|
|
73
|
+
ws.compete
|
|
74
|
+
expect(ws.competitors.first.urgency).to be > 0
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'removes winner from competitors' do
|
|
78
|
+
ws.submit(content: 'winner', source: :s, domain: :d, salience: 0.9)
|
|
79
|
+
ws.compete
|
|
80
|
+
expect(ws.competitor_count).to eq(0)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'adds to broadcast history' do
|
|
84
|
+
ws.submit(content: 'winner', source: :s, domain: :d, salience: 0.9)
|
|
85
|
+
ws.compete
|
|
86
|
+
expect(ws.broadcast_history.size).to eq(1)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe '#conscious?' do
|
|
91
|
+
it 'returns true for current broadcast content' do
|
|
92
|
+
ws.submit(content: 'idea', source: :s, domain: :d, salience: 0.9)
|
|
93
|
+
ws.compete
|
|
94
|
+
expect(ws.conscious?('idea')).to be true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns false when no broadcast' do
|
|
98
|
+
expect(ws.conscious?('anything')).to be false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'returns false for non-matching content' do
|
|
102
|
+
ws.submit(content: 'idea', source: :s, domain: :d, salience: 0.9)
|
|
103
|
+
ws.compete
|
|
104
|
+
expect(ws.conscious?('other')).to be false
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe '#current_content' do
|
|
109
|
+
it 'returns broadcast hash when active' do
|
|
110
|
+
ws.submit(content: 'idea', source: :cortex, domain: :cognition, salience: 0.9)
|
|
111
|
+
ws.compete
|
|
112
|
+
content = ws.current_content
|
|
113
|
+
expect(content[:content]).to eq('idea')
|
|
114
|
+
expect(content[:source]).to eq(:cortex)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'returns nil when no active broadcast' do
|
|
118
|
+
expect(ws.current_content).to be_nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe '#acknowledge' do
|
|
123
|
+
it 'acknowledges current broadcast' do
|
|
124
|
+
ws.submit(content: 'idea', source: :s, domain: :d, salience: 0.9)
|
|
125
|
+
ws.compete
|
|
126
|
+
expect(ws.acknowledge(subscriber_id: :memory)).to be true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'returns false when no broadcast' do
|
|
130
|
+
expect(ws.acknowledge(subscriber_id: :memory)).to be false
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
describe '#state' do
|
|
135
|
+
it 'returns :idle when empty' do
|
|
136
|
+
expect(ws.state).to eq(:idle)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'returns :broadcasting when broadcast active' do
|
|
140
|
+
ws.submit(content: 'idea', source: :s, domain: :d, salience: 0.9)
|
|
141
|
+
ws.compete
|
|
142
|
+
expect(ws.state).to eq(:broadcasting)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'returns :contention when multiple competitors' do
|
|
146
|
+
ws.submit(content: 'a', source: :s, domain: :d, salience: 0.5)
|
|
147
|
+
ws.submit(content: 'b', source: :s, domain: :d, salience: 0.5)
|
|
148
|
+
expect(ws.state).to eq(:contention)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
describe '#tick' do
|
|
153
|
+
it 'decays competitors' do
|
|
154
|
+
ws.submit(content: 'idea', source: :s, domain: :d, salience: 0.5)
|
|
155
|
+
before = ws.competitors.first.salience
|
|
156
|
+
ws.tick
|
|
157
|
+
expect(ws.competitors.first&.salience || 0).to be <= before
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'prunes weak competitors after decay' do
|
|
161
|
+
ws.submit(content: 'weak', source: :s, domain: :d, salience: constants::COMPETITION_THRESHOLD + 0.01)
|
|
162
|
+
ws.tick
|
|
163
|
+
expect(ws.competitor_count).to eq(0)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
describe '#to_h' do
|
|
168
|
+
it 'returns stats hash' do
|
|
169
|
+
h = ws.to_h
|
|
170
|
+
expect(h).to include(:state, :state_label, :subscribers, :competitors, :broadcast_history, :utilization,
|
|
171
|
+
:competitions)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::GlobalWorkspace::Runners::GlobalWorkspace do
|
|
4
|
+
let(:client) { Legion::Extensions::GlobalWorkspace::Client.new }
|
|
5
|
+
|
|
6
|
+
describe '#submit_for_broadcast' do
|
|
7
|
+
it 'submits content for competition' do
|
|
8
|
+
result = client.submit_for_broadcast(content: 'idea', source: :cortex, domain: :cognition, salience: 0.7)
|
|
9
|
+
expect(result[:success]).to be true
|
|
10
|
+
expect(result[:effective_salience]).to be > 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'rejects below-threshold content' do
|
|
14
|
+
result = client.submit_for_broadcast(content: 'weak', source: :s, domain: :d, salience: 0.1)
|
|
15
|
+
expect(result[:success]).to be false
|
|
16
|
+
expect(result[:reason]).to eq(:below_threshold)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe '#run_competition' do
|
|
21
|
+
it 'broadcasts winning content' do
|
|
22
|
+
client.submit_for_broadcast(content: 'strong', source: :emotion, domain: :safety, salience: 0.9)
|
|
23
|
+
result = client.run_competition
|
|
24
|
+
expect(result[:success]).to be true
|
|
25
|
+
expect(result[:broadcast][:content]).to eq('strong')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'returns no_winner when no competitors' do
|
|
29
|
+
result = client.run_competition
|
|
30
|
+
expect(result[:success]).to be false
|
|
31
|
+
expect(result[:reason]).to eq(:no_winner)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe '#register_subscriber / #unregister_subscriber' do
|
|
36
|
+
it 'registers a subscriber' do
|
|
37
|
+
result = client.register_subscriber(id: :memory, name: 'lex-memory', domains: [:all])
|
|
38
|
+
expect(result[:success]).to be true
|
|
39
|
+
expect(result[:subscriber_count]).to eq(1)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'unregisters a subscriber' do
|
|
43
|
+
client.register_subscriber(id: :memory, name: 'lex-memory')
|
|
44
|
+
result = client.unregister_subscriber(id: :memory)
|
|
45
|
+
expect(result[:success]).to be true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe '#acknowledge_broadcast' do
|
|
50
|
+
it 'acknowledges active broadcast' do
|
|
51
|
+
client.submit_for_broadcast(content: 'idea', source: :s, domain: :d, salience: 0.9)
|
|
52
|
+
client.run_competition
|
|
53
|
+
result = client.acknowledge_broadcast(subscriber_id: :memory)
|
|
54
|
+
expect(result[:success]).to be true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'returns false when no broadcast' do
|
|
58
|
+
result = client.acknowledge_broadcast(subscriber_id: :memory)
|
|
59
|
+
expect(result[:success]).to be false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe '#query_consciousness' do
|
|
64
|
+
it 'returns conscious: true for broadcasted content' do
|
|
65
|
+
client.submit_for_broadcast(content: 'idea', source: :s, domain: :d, salience: 0.9)
|
|
66
|
+
client.run_competition
|
|
67
|
+
result = client.query_consciousness(content: 'idea')
|
|
68
|
+
expect(result[:conscious]).to be true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'returns conscious: false for non-broadcasted content' do
|
|
72
|
+
result = client.query_consciousness(content: 'nothing')
|
|
73
|
+
expect(result[:conscious]).to be false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '#current_broadcast' do
|
|
78
|
+
it 'returns broadcast when active' do
|
|
79
|
+
client.submit_for_broadcast(content: 'idea', source: :s, domain: :d, salience: 0.9)
|
|
80
|
+
client.run_competition
|
|
81
|
+
result = client.current_broadcast
|
|
82
|
+
expect(result[:broadcast]).not_to be_nil
|
|
83
|
+
expect(result[:broadcast][:content]).to eq('idea')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'returns nil when no active broadcast' do
|
|
87
|
+
result = client.current_broadcast
|
|
88
|
+
expect(result[:broadcast]).to be_nil
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe '#broadcast_history' do
|
|
93
|
+
it 'returns history' do
|
|
94
|
+
client.submit_for_broadcast(content: 'idea', source: :s, domain: :d, salience: 0.9)
|
|
95
|
+
client.run_competition
|
|
96
|
+
result = client.broadcast_history(limit: 5)
|
|
97
|
+
expect(result[:success]).to be true
|
|
98
|
+
expect(result[:history].size).to eq(1)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe '#update_global_workspace' do
|
|
103
|
+
it 'runs tick maintenance' do
|
|
104
|
+
result = client.update_global_workspace
|
|
105
|
+
expect(result[:success]).to be true
|
|
106
|
+
expect(result).to have_key(:state)
|
|
107
|
+
expect(result).to have_key(:utilization)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
describe '#workspace_stats' do
|
|
112
|
+
it 'returns stats' do
|
|
113
|
+
result = client.workspace_stats
|
|
114
|
+
expect(result[:success]).to be true
|
|
115
|
+
expect(result[:stats]).to include(:state, :subscribers, :competitors, :utilization)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Logging
|
|
7
|
+
def self.debug(_msg); end
|
|
8
|
+
def self.info(_msg); end
|
|
9
|
+
def self.warn(_msg); end
|
|
10
|
+
def self.error(_msg); end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require 'legion/extensions/global_workspace'
|
|
15
|
+
|
|
16
|
+
RSpec.configure do |config|
|
|
17
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
18
|
+
config.disable_monkey_patching!
|
|
19
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-global-workspace
|
|
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: Baars' Global Workspace Theory for brain-modeled agentic AI — information
|
|
27
|
+
competes for access to a limited-capacity workspace; winners are broadcast to all
|
|
28
|
+
subscribed cognitive subsystems, implementing a computational model of conscious
|
|
29
|
+
access and attentional bottleneck.
|
|
30
|
+
email:
|
|
31
|
+
- matthewdiverson@gmail.com
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- Gemfile
|
|
37
|
+
- lex-global-workspace.gemspec
|
|
38
|
+
- lib/legion/extensions/global_workspace.rb
|
|
39
|
+
- lib/legion/extensions/global_workspace/actors/competition.rb
|
|
40
|
+
- lib/legion/extensions/global_workspace/client.rb
|
|
41
|
+
- lib/legion/extensions/global_workspace/helpers/broadcast.rb
|
|
42
|
+
- lib/legion/extensions/global_workspace/helpers/competitor.rb
|
|
43
|
+
- lib/legion/extensions/global_workspace/helpers/constants.rb
|
|
44
|
+
- lib/legion/extensions/global_workspace/helpers/workspace.rb
|
|
45
|
+
- lib/legion/extensions/global_workspace/runners/global_workspace.rb
|
|
46
|
+
- lib/legion/extensions/global_workspace/version.rb
|
|
47
|
+
- spec/legion/extensions/global_workspace/client_spec.rb
|
|
48
|
+
- spec/legion/extensions/global_workspace/helpers/broadcast_spec.rb
|
|
49
|
+
- spec/legion/extensions/global_workspace/helpers/competitor_spec.rb
|
|
50
|
+
- spec/legion/extensions/global_workspace/helpers/workspace_spec.rb
|
|
51
|
+
- spec/legion/extensions/global_workspace/runners/global_workspace_spec.rb
|
|
52
|
+
- spec/spec_helper.rb
|
|
53
|
+
homepage: https://github.com/LegionIO/lex-global-workspace
|
|
54
|
+
licenses:
|
|
55
|
+
- MIT
|
|
56
|
+
metadata:
|
|
57
|
+
homepage_uri: https://github.com/LegionIO/lex-global-workspace
|
|
58
|
+
source_code_uri: https://github.com/LegionIO/lex-global-workspace
|
|
59
|
+
documentation_uri: https://github.com/LegionIO/lex-global-workspace
|
|
60
|
+
changelog_uri: https://github.com/LegionIO/lex-global-workspace
|
|
61
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-global-workspace/issues
|
|
62
|
+
rubygems_mfa_required: 'true'
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '3.4'
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 3.6.9
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: LEX Global Workspace
|
|
80
|
+
test_files: []
|