lex-default-mode-network 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-default-mode-network.gemspec +31 -0
- data/lib/legion/extensions/default_mode_network/actors/idle.rb +41 -0
- data/lib/legion/extensions/default_mode_network/client.rb +24 -0
- data/lib/legion/extensions/default_mode_network/helpers/constants.rb +49 -0
- data/lib/legion/extensions/default_mode_network/helpers/dmn_engine.rb +217 -0
- data/lib/legion/extensions/default_mode_network/helpers/wandering_thought.rb +56 -0
- data/lib/legion/extensions/default_mode_network/runners/default_mode_network.rb +118 -0
- data/lib/legion/extensions/default_mode_network/version.rb +9 -0
- data/lib/legion/extensions/default_mode_network.rb +17 -0
- data/spec/legion/extensions/default_mode_network/client_spec.rb +69 -0
- data/spec/legion/extensions/default_mode_network/helpers/constants_spec.rb +76 -0
- data/spec/legion/extensions/default_mode_network/helpers/dmn_engine_spec.rb +321 -0
- data/spec/legion/extensions/default_mode_network/helpers/wandering_thought_spec.rb +145 -0
- data/spec/legion/extensions/default_mode_network/runners/default_mode_network_spec.rb +269 -0
- data/spec/spec_helper.rb +20 -0
- metadata +78 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 281cdccc190e505af09438971b70bed9cff9b95e1ff851fe3b298cc3ef7b3997
|
|
4
|
+
data.tar.gz: 45b4583ac9323704568ce7962fa9b79279c36681e14daf84d84e27693414331c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b1b4db33bd397bd34da5e157d7342e04368be38e4667be51d025d5205eed0b710cf3953d799d7970a5d2e400d2616832714cc5b7dcbec675b825e81155a1f7ba
|
|
7
|
+
data.tar.gz: d5c1e14574ddc8be3e61a6b51981be75be108d12652c386cdbdc29bb8090183fd1490bc80792a9fd0190a22fc42167d362381b799e85ca976cf6380b9b66a6ca
|
data/Gemfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/default_mode_network/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-default-mode-network'
|
|
7
|
+
spec.version = Legion::Extensions::DefaultModeNetwork::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Default Mode Network'
|
|
12
|
+
spec.description = 'Raichle (2001) Default Mode Network for brain-modeled agentic AI — ' \
|
|
13
|
+
'resting-state activation for self-referential processing, mind-wandering, ' \
|
|
14
|
+
'spontaneous planning, and social replay when the agent is not task-focused.'
|
|
15
|
+
spec.homepage = 'https://github.com/LegionIO/lex-default-mode-network'
|
|
16
|
+
spec.license = 'MIT'
|
|
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-default-mode-network'
|
|
21
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-default-mode-network'
|
|
22
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-default-mode-network'
|
|
23
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-default-mode-network/issues'
|
|
24
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
25
|
+
|
|
26
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
27
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-default-mode-network.gemspec Gemfile]
|
|
28
|
+
end
|
|
29
|
+
spec.require_paths = ['lib']
|
|
30
|
+
spec.add_development_dependency 'legion-gaia'
|
|
31
|
+
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 DefaultModeNetwork
|
|
8
|
+
module Actor
|
|
9
|
+
class Idle < Legion::Extensions::Actors::Every
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::DefaultModeNetwork::Runners::DefaultModeNetwork
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'update_dmn'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def time
|
|
19
|
+
30
|
|
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,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/default_mode_network/helpers/constants'
|
|
4
|
+
require 'legion/extensions/default_mode_network/helpers/wandering_thought'
|
|
5
|
+
require 'legion/extensions/default_mode_network/helpers/dmn_engine'
|
|
6
|
+
require 'legion/extensions/default_mode_network/runners/default_mode_network'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module DefaultModeNetwork
|
|
11
|
+
class Client
|
|
12
|
+
include Runners::DefaultModeNetwork
|
|
13
|
+
|
|
14
|
+
def initialize(dmn_engine: nil, **)
|
|
15
|
+
@dmn_engine = dmn_engine || Helpers::DmnEngine.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :dmn_engine
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module DefaultModeNetwork
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
# Idle time thresholds
|
|
9
|
+
IDLE_THRESHOLD = 30 # seconds before DMN activates
|
|
10
|
+
DEEP_IDLE_THRESHOLD = 300 # seconds before deep idle mode
|
|
11
|
+
|
|
12
|
+
# Thought storage limits
|
|
13
|
+
MAX_WANDERING_THOUGHTS = 100
|
|
14
|
+
MAX_THOUGHT_HISTORY = 200
|
|
15
|
+
MAX_ASSOCIATION_CHAIN = 5 # max hops in a wandering chain
|
|
16
|
+
|
|
17
|
+
# Salience parameters
|
|
18
|
+
THOUGHT_SALIENCE_FLOOR = 0.05
|
|
19
|
+
THOUGHT_DECAY = 0.01
|
|
20
|
+
DEFAULT_SALIENCE = 0.3
|
|
21
|
+
SALIENCE_ALPHA = 0.1
|
|
22
|
+
|
|
23
|
+
# Probabilistic thought-type selection (must sum to <= 1.0)
|
|
24
|
+
SELF_REFERENTIAL_PROBABILITY = 0.3
|
|
25
|
+
SOCIAL_REPLAY_PROBABILITY = 0.2
|
|
26
|
+
PLANNING_PROBABILITY = 0.2
|
|
27
|
+
WANDERING_PROBABILITY = 0.3
|
|
28
|
+
|
|
29
|
+
# Activity mode labels
|
|
30
|
+
ACTIVITY_LABELS = {
|
|
31
|
+
active: :task_focused,
|
|
32
|
+
transitioning: :shifting,
|
|
33
|
+
idle: :daydreaming,
|
|
34
|
+
deep_idle: :deep_reflection
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
# Salience quality labels — range-keyed hash
|
|
38
|
+
SALIENCE_LABELS = {
|
|
39
|
+
(0.8..) => :breakthrough,
|
|
40
|
+
(0.6...0.8) => :significant,
|
|
41
|
+
(0.4...0.6) => :notable,
|
|
42
|
+
(0.2...0.4) => :passing,
|
|
43
|
+
(..0.2) => :fleeting
|
|
44
|
+
}.freeze
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module DefaultModeNetwork
|
|
6
|
+
module Helpers
|
|
7
|
+
class DmnEngine
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :mode, :last_stimulus_at, :thoughts, :thought_history, :wandering_seeds
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@mode = :active
|
|
14
|
+
@last_stimulus_at = Time.now.utc
|
|
15
|
+
@thoughts = []
|
|
16
|
+
@thought_history = []
|
|
17
|
+
@wandering_seeds = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# --- Stimulus / Mode ---
|
|
21
|
+
|
|
22
|
+
def register_stimulus(source: nil)
|
|
23
|
+
@last_stimulus_at = Time.now.utc
|
|
24
|
+
previous = @mode
|
|
25
|
+
@mode = :active
|
|
26
|
+
{ previous_mode: previous, current_mode: @mode, source: source, at: @last_stimulus_at }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def tick_mode
|
|
30
|
+
elapsed = idle_duration
|
|
31
|
+
previous = @mode
|
|
32
|
+
|
|
33
|
+
@mode = if elapsed >= Constants::DEEP_IDLE_THRESHOLD
|
|
34
|
+
:deep_idle
|
|
35
|
+
elsif elapsed >= Constants::IDLE_THRESHOLD
|
|
36
|
+
:idle
|
|
37
|
+
elsif @mode == :active && elapsed.positive?
|
|
38
|
+
:transitioning
|
|
39
|
+
else
|
|
40
|
+
@mode
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
{ previous_mode: previous, current_mode: @mode, idle_duration: elapsed.round(2) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def idle_duration
|
|
47
|
+
Time.now.utc - @last_stimulus_at
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# --- Thought Generation ---
|
|
51
|
+
|
|
52
|
+
def generate_thought
|
|
53
|
+
roll = rand
|
|
54
|
+
thought = if roll < Constants::SELF_REFERENTIAL_PROBABILITY
|
|
55
|
+
self_reflect
|
|
56
|
+
elsif roll < Constants::SELF_REFERENTIAL_PROBABILITY + Constants::SOCIAL_REPLAY_PROBABILITY
|
|
57
|
+
social_replay(interaction: random_seed(:social))
|
|
58
|
+
elsif roll < Constants::SELF_REFERENTIAL_PROBABILITY +
|
|
59
|
+
Constants::SOCIAL_REPLAY_PROBABILITY +
|
|
60
|
+
Constants::PLANNING_PROBABILITY
|
|
61
|
+
plan_spontaneously(goal: random_seed(:goal))
|
|
62
|
+
else
|
|
63
|
+
wander(seed: random_seed(:concept))
|
|
64
|
+
end
|
|
65
|
+
store_thought(thought)
|
|
66
|
+
thought
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self_reflect(topic: nil)
|
|
70
|
+
topics = %i[identity values goals capabilities limitations purpose growth]
|
|
71
|
+
chosen_topic = topic || topics.sample
|
|
72
|
+
chain = build_association_chain(chosen_topic, Constants::MAX_ASSOCIATION_CHAIN)
|
|
73
|
+
|
|
74
|
+
WanderingThought.new(
|
|
75
|
+
seed: chosen_topic,
|
|
76
|
+
association_chain: chain,
|
|
77
|
+
domain: :self,
|
|
78
|
+
thought_type: :self_referential,
|
|
79
|
+
salience: rand(0.3..0.7)
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def social_replay(interaction: nil)
|
|
84
|
+
seed = interaction || :recent_interaction
|
|
85
|
+
chain = build_association_chain(seed, Constants::MAX_ASSOCIATION_CHAIN)
|
|
86
|
+
|
|
87
|
+
WanderingThought.new(
|
|
88
|
+
seed: seed,
|
|
89
|
+
association_chain: chain,
|
|
90
|
+
domain: :social,
|
|
91
|
+
thought_type: :social_replay,
|
|
92
|
+
salience: rand(0.2..0.6)
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def plan_spontaneously(goal: nil)
|
|
97
|
+
seed = goal || :unresolved_objective
|
|
98
|
+
chain = build_association_chain(seed, Constants::MAX_ASSOCIATION_CHAIN)
|
|
99
|
+
|
|
100
|
+
WanderingThought.new(
|
|
101
|
+
seed: seed,
|
|
102
|
+
association_chain: chain,
|
|
103
|
+
domain: :planning,
|
|
104
|
+
thought_type: :spontaneous_plan,
|
|
105
|
+
salience: rand(0.3..0.8)
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def wander(seed: nil)
|
|
110
|
+
anchor = seed || random_seed(:concept)
|
|
111
|
+
chain = build_association_chain(anchor, Constants::MAX_ASSOCIATION_CHAIN)
|
|
112
|
+
|
|
113
|
+
WanderingThought.new(
|
|
114
|
+
seed: anchor,
|
|
115
|
+
association_chain: chain,
|
|
116
|
+
domain: :associative,
|
|
117
|
+
thought_type: :wandering,
|
|
118
|
+
salience: rand(0.1..0.5)
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# --- Retrieval ---
|
|
123
|
+
|
|
124
|
+
def salient_thoughts(count: 5)
|
|
125
|
+
@thoughts.sort_by { |t| -t.salience }.first(count)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def thoughts_of_type(type:)
|
|
129
|
+
@thoughts.select { |t| t.thought_type == type.to_sym }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# --- Lifecycle ---
|
|
133
|
+
|
|
134
|
+
def decay_all
|
|
135
|
+
@thoughts.each(&:decay)
|
|
136
|
+
faded = @thoughts.select(&:faded?)
|
|
137
|
+
faded.each { |t| archive_thought(t) }
|
|
138
|
+
@thoughts.reject!(&:faded?)
|
|
139
|
+
faded.size
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def thought_count
|
|
143
|
+
@thoughts.size
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def to_h
|
|
147
|
+
{
|
|
148
|
+
mode: @mode,
|
|
149
|
+
mode_label: Constants::ACTIVITY_LABELS[@mode],
|
|
150
|
+
idle_duration: idle_duration.round(2),
|
|
151
|
+
thought_count: @thoughts.size,
|
|
152
|
+
history_count: @thought_history.size,
|
|
153
|
+
last_stimulus_at: @last_stimulus_at
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def store_thought(thought)
|
|
160
|
+
@thoughts << thought
|
|
161
|
+
prune_thoughts if @thoughts.size > Constants::MAX_WANDERING_THOUGHTS
|
|
162
|
+
thought
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def archive_thought(thought)
|
|
166
|
+
@thought_history << thought.to_h
|
|
167
|
+
@thought_history.shift while @thought_history.size > Constants::MAX_THOUGHT_HISTORY
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def prune_thoughts
|
|
171
|
+
@thoughts.sort_by! { |t| -t.salience }
|
|
172
|
+
@thoughts.pop while @thoughts.size > Constants::MAX_WANDERING_THOUGHTS
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def build_association_chain(seed, max_hops)
|
|
176
|
+
chain = [seed]
|
|
177
|
+
max_hops.times do
|
|
178
|
+
next_concept = derive_association(chain.last)
|
|
179
|
+
break if chain.include?(next_concept)
|
|
180
|
+
|
|
181
|
+
chain << next_concept
|
|
182
|
+
end
|
|
183
|
+
chain
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Lightweight deterministic-ish association for standalone use
|
|
187
|
+
def derive_association(concept)
|
|
188
|
+
associations = {
|
|
189
|
+
identity: :values,
|
|
190
|
+
values: :purpose,
|
|
191
|
+
purpose: :goals,
|
|
192
|
+
goals: :capabilities,
|
|
193
|
+
capabilities: :limitations,
|
|
194
|
+
limitations: :growth,
|
|
195
|
+
growth: :identity,
|
|
196
|
+
social: :empathy,
|
|
197
|
+
empathy: :trust,
|
|
198
|
+
trust: :collaboration,
|
|
199
|
+
collaboration: :outcome,
|
|
200
|
+
outcome: :reflection
|
|
201
|
+
}
|
|
202
|
+
associations.fetch(concept.to_sym, :"#{concept}_context")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def random_seed(category)
|
|
206
|
+
pools = {
|
|
207
|
+
social: %i[recent_conversation last_request team_interaction feedback_given feedback_received],
|
|
208
|
+
goal: %i[pending_task unresolved_objective upcoming_deadline open_question improvement_opportunity],
|
|
209
|
+
concept: %i[efficiency creativity curiosity uncertainty possibility connection meaning pattern]
|
|
210
|
+
}
|
|
211
|
+
pools.fetch(category, pools[:concept]).sample
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module DefaultModeNetwork
|
|
6
|
+
module Helpers
|
|
7
|
+
class WanderingThought
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :id, :seed, :association_chain, :domain, :thought_type, :created_at
|
|
11
|
+
attr_accessor :salience
|
|
12
|
+
|
|
13
|
+
def initialize(seed:, association_chain:, domain:, thought_type:, salience: Constants::DEFAULT_SALIENCE)
|
|
14
|
+
@id = SecureRandom.uuid
|
|
15
|
+
@seed = seed
|
|
16
|
+
@association_chain = Array(association_chain)
|
|
17
|
+
@domain = domain
|
|
18
|
+
@thought_type = thought_type.to_sym
|
|
19
|
+
@salience = salience.to_f.clamp(0.0, 1.0)
|
|
20
|
+
@created_at = Time.now.utc
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def boost_salience(amount = Constants::SALIENCE_ALPHA)
|
|
24
|
+
@salience = [@salience + amount.to_f, 1.0].min
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def decay
|
|
28
|
+
@salience = [@salience - Constants::THOUGHT_DECAY, Constants::THOUGHT_SALIENCE_FLOOR].max
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def faded?
|
|
32
|
+
@salience <= Constants::THOUGHT_SALIENCE_FLOOR
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def label
|
|
36
|
+
Constants::SALIENCE_LABELS.each { |range, lbl| return lbl if range.cover?(@salience) }
|
|
37
|
+
:fleeting
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
id: @id,
|
|
43
|
+
seed: @seed,
|
|
44
|
+
association_chain: @association_chain,
|
|
45
|
+
domain: @domain,
|
|
46
|
+
thought_type: @thought_type,
|
|
47
|
+
salience: @salience.round(4),
|
|
48
|
+
label: label,
|
|
49
|
+
created_at: @created_at
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module DefaultModeNetwork
|
|
6
|
+
module Runners
|
|
7
|
+
module DefaultModeNetwork
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def register_external_stimulus(source: nil, **)
|
|
12
|
+
Legion::Logging.debug "[dmn] register_stimulus: source=#{source}"
|
|
13
|
+
result = dmn_engine.register_stimulus(source: source)
|
|
14
|
+
{
|
|
15
|
+
success: true,
|
|
16
|
+
previous_mode: result[:previous_mode],
|
|
17
|
+
current_mode: result[:current_mode],
|
|
18
|
+
source: result[:source]
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate_idle_thought(**)
|
|
23
|
+
Legion::Logging.debug "[dmn] generate_idle_thought: mode=#{dmn_engine.mode}"
|
|
24
|
+
thought = dmn_engine.generate_thought
|
|
25
|
+
Legion::Logging.debug "[dmn] thought_generated: type=#{thought.thought_type} salience=#{thought.salience.round(2)} label=#{thought.label}"
|
|
26
|
+
{ success: true, thought: thought.to_h }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def trigger_self_reflection(**)
|
|
30
|
+
Legion::Logging.debug '[dmn] trigger_self_reflection'
|
|
31
|
+
thought = dmn_engine.self_reflect
|
|
32
|
+
dmn_engine.send(:store_thought, thought)
|
|
33
|
+
Legion::Logging.debug "[dmn] self_reflect: topic=#{thought.seed} salience=#{thought.salience.round(2)}"
|
|
34
|
+
{ success: true, thought: thought.to_h }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def trigger_social_replay(interaction: nil, **)
|
|
38
|
+
Legion::Logging.debug "[dmn] trigger_social_replay: interaction=#{interaction}"
|
|
39
|
+
thought = dmn_engine.social_replay(interaction: interaction)
|
|
40
|
+
dmn_engine.send(:store_thought, thought)
|
|
41
|
+
Legion::Logging.debug "[dmn] social_replay: seed=#{thought.seed} salience=#{thought.salience.round(2)}"
|
|
42
|
+
{ success: true, thought: thought.to_h }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def trigger_spontaneous_plan(goal: nil, **)
|
|
46
|
+
Legion::Logging.debug "[dmn] trigger_spontaneous_plan: goal=#{goal}"
|
|
47
|
+
thought = dmn_engine.plan_spontaneously(goal: goal)
|
|
48
|
+
dmn_engine.send(:store_thought, thought)
|
|
49
|
+
Legion::Logging.debug "[dmn] spontaneous_plan: goal=#{thought.seed} salience=#{thought.salience.round(2)}"
|
|
50
|
+
{ success: true, thought: thought.to_h }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def trigger_wandering(seed: nil, **)
|
|
54
|
+
Legion::Logging.debug "[dmn] trigger_wandering: seed=#{seed}"
|
|
55
|
+
thought = dmn_engine.wander(seed: seed)
|
|
56
|
+
dmn_engine.send(:store_thought, thought)
|
|
57
|
+
Legion::Logging.debug "[dmn] wandering: seed=#{thought.seed} chain_length=#{thought.association_chain.size}"
|
|
58
|
+
{ success: true, thought: thought.to_h }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def salient_thoughts(count: 5, **)
|
|
62
|
+
count = count.to_i
|
|
63
|
+
Legion::Logging.debug "[dmn] salient_thoughts: count=#{count}"
|
|
64
|
+
thoughts = dmn_engine.salient_thoughts(count: count)
|
|
65
|
+
{ success: true, thoughts: thoughts.map(&:to_h), count: thoughts.size }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def dmn_mode_status(**)
|
|
69
|
+
Legion::Logging.debug "[dmn] dmn_mode_status: mode=#{dmn_engine.mode}"
|
|
70
|
+
mode = dmn_engine.mode
|
|
71
|
+
mode_label = Helpers::Constants::ACTIVITY_LABELS[mode]
|
|
72
|
+
idle_secs = dmn_engine.idle_duration.round(2)
|
|
73
|
+
{
|
|
74
|
+
success: true,
|
|
75
|
+
mode: mode,
|
|
76
|
+
mode_label: mode_label,
|
|
77
|
+
idle_duration: idle_secs,
|
|
78
|
+
thought_count: dmn_engine.thought_count
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def update_dmn(**)
|
|
83
|
+
Legion::Logging.debug '[dmn] update_dmn: tick'
|
|
84
|
+
tick_result = dmn_engine.tick_mode
|
|
85
|
+
faded_count = dmn_engine.decay_all
|
|
86
|
+
thought = nil
|
|
87
|
+
|
|
88
|
+
if %i[idle deep_idle].include?(dmn_engine.mode)
|
|
89
|
+
thought = dmn_engine.generate_thought
|
|
90
|
+
Legion::Logging.debug "[dmn] idle_thought: type=#{thought.thought_type} salience=#{thought.salience.round(2)}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
Legion::Logging.debug "[dmn] update_dmn: mode=#{tick_result[:current_mode]} faded=#{faded_count} thoughts=#{dmn_engine.thought_count}"
|
|
94
|
+
{
|
|
95
|
+
success: true,
|
|
96
|
+
mode: tick_result[:current_mode],
|
|
97
|
+
previous_mode: tick_result[:previous_mode],
|
|
98
|
+
faded_count: faded_count,
|
|
99
|
+
thought_count: dmn_engine.thought_count,
|
|
100
|
+
new_thought: thought&.to_h
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def dmn_stats(**)
|
|
105
|
+
Legion::Logging.debug '[dmn] dmn_stats'
|
|
106
|
+
{ success: true, stats: dmn_engine.to_h }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def dmn_engine
|
|
112
|
+
@dmn_engine ||= Helpers::DmnEngine.new
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'legion/extensions/default_mode_network/version'
|
|
5
|
+
require 'legion/extensions/default_mode_network/helpers/constants'
|
|
6
|
+
require 'legion/extensions/default_mode_network/helpers/wandering_thought'
|
|
7
|
+
require 'legion/extensions/default_mode_network/helpers/dmn_engine'
|
|
8
|
+
require 'legion/extensions/default_mode_network/runners/default_mode_network'
|
|
9
|
+
require 'legion/extensions/default_mode_network/client'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module DefaultModeNetwork
|
|
14
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::DefaultModeNetwork::Client do
|
|
4
|
+
subject(:client) { described_class.new }
|
|
5
|
+
|
|
6
|
+
it 'includes Runners::DefaultModeNetwork' do
|
|
7
|
+
expect(described_class.ancestors).to include(
|
|
8
|
+
Legion::Extensions::DefaultModeNetwork::Runners::DefaultModeNetwork
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'responds to all runner methods' do
|
|
13
|
+
%i[
|
|
14
|
+
register_external_stimulus
|
|
15
|
+
generate_idle_thought
|
|
16
|
+
trigger_self_reflection
|
|
17
|
+
trigger_social_replay
|
|
18
|
+
trigger_spontaneous_plan
|
|
19
|
+
trigger_wandering
|
|
20
|
+
salient_thoughts
|
|
21
|
+
dmn_mode_status
|
|
22
|
+
update_dmn
|
|
23
|
+
dmn_stats
|
|
24
|
+
].each do |method|
|
|
25
|
+
expect(client).to respond_to(method)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'accepts a custom dmn_engine' do
|
|
30
|
+
engine = Legion::Extensions::DefaultModeNetwork::Helpers::DmnEngine.new
|
|
31
|
+
c = described_class.new(dmn_engine: engine)
|
|
32
|
+
expect(c).to respond_to(:dmn_stats)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe 'full DMN lifecycle scenario' do
|
|
36
|
+
it 'goes from active through idle and generates thoughts' do
|
|
37
|
+
# Start active — register stimulus
|
|
38
|
+
stimulus_result = client.register_external_stimulus(source: :test_harness)
|
|
39
|
+
expect(stimulus_result[:current_mode]).to eq(:active)
|
|
40
|
+
|
|
41
|
+
# Explicitly trigger various thought types
|
|
42
|
+
self_thought = client.trigger_self_reflection
|
|
43
|
+
social_thought = client.trigger_social_replay(interaction: :pair_programming)
|
|
44
|
+
plan_thought = client.trigger_spontaneous_plan(goal: :optimize_query)
|
|
45
|
+
wander_thought = client.trigger_wandering(seed: :pattern)
|
|
46
|
+
|
|
47
|
+
expect(self_thought[:thought][:thought_type]).to eq(:self_referential)
|
|
48
|
+
expect(social_thought[:thought][:thought_type]).to eq(:social_replay)
|
|
49
|
+
expect(plan_thought[:thought][:thought_type]).to eq(:spontaneous_plan)
|
|
50
|
+
expect(wander_thought[:thought][:thought_type]).to eq(:wandering)
|
|
51
|
+
|
|
52
|
+
# Check salient thoughts includes our injected thoughts
|
|
53
|
+
salient = client.salient_thoughts(count: 10)
|
|
54
|
+
expect(salient[:count]).to eq(4)
|
|
55
|
+
|
|
56
|
+
# Run several update ticks — thoughts decay
|
|
57
|
+
5.times { client.update_dmn }
|
|
58
|
+
|
|
59
|
+
# Stats should still be coherent
|
|
60
|
+
stats = client.dmn_stats
|
|
61
|
+
expect(stats[:stats][:thought_count]).to be_a(Integer)
|
|
62
|
+
|
|
63
|
+
# Mode status
|
|
64
|
+
status = client.dmn_mode_status
|
|
65
|
+
expect(status[:success]).to be true
|
|
66
|
+
expect(status[:idle_duration]).to be >= 0
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|