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 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+
11
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module DefaultModeNetwork
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ 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