lex-tick 0.1.14 → 0.1.16
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 +4 -4
- data/lex-tick.gemspec +1 -1
- data/lib/legion/extensions/tick/helpers/constants.rb +6 -2
- data/lib/legion/extensions/tick/helpers/state.rb +19 -7
- data/lib/legion/extensions/tick/runners/orchestrator.rb +45 -6
- data/lib/legion/extensions/tick/version.rb +1 -1
- data/spec/legion/extensions/tick/helpers/constants_spec.rb +10 -2
- data/spec/legion/extensions/tick/helpers/state_spec.rb +14 -0
- data/spec/legion/extensions/tick/runners/orchestrator_spec.rb +74 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 94918234bd1b19854887b794867550d1a6252f74ea4ad10dd6009b6df2797c5b
|
|
4
|
+
data.tar.gz: df175d240f2a5816e9b7be4269aa12880051e1bbe5bdf6f761731cca585b0e10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 291b85fa4cfe572c00bb279026569c08691daf1233d7900ffe619e6c4f8d15b3cc9241d18f98aa8ef22715a0438a78a1239919181decf4463f8ab26e13c708cc
|
|
7
|
+
data.tar.gz: 56b3a74946173477cbd2d3d85a6b35ff15cc61472b9065bade72af63dcbdab5a8392ceaf50a54ca7e4397db01142cf6d4ee702786d60d938bbd7fa2ff541f954
|
data/lex-tick.gemspec
CHANGED
|
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
|
9
9
|
spec.email = ['matthewdiverson@gmail.com']
|
|
10
10
|
|
|
11
11
|
spec.summary = 'LEX Tick'
|
|
12
|
-
spec.description = 'Atomic cognitive processing cycle (
|
|
12
|
+
spec.description = 'Atomic cognitive processing cycle (16 phases, 4 modes, 10 dream phases) for brain-modeled agentic AI'
|
|
13
13
|
spec.homepage = 'https://github.com/LegionIO/lex-tick'
|
|
14
14
|
spec.license = 'MIT'
|
|
15
15
|
spec.required_ruby_version = '>= 3.4'
|
|
@@ -55,12 +55,16 @@ module Legion
|
|
|
55
55
|
SENTINEL_TIMEOUT = 3600 # seconds without any signal before demotion to dormant
|
|
56
56
|
DREAM_IDLE_THRESHOLD = 1800 # seconds dormant with no signal before entering dream cycle
|
|
57
57
|
SENTINEL_TO_DREAM_THRESHOLD = 600 # seconds sentinel with no signal before entering dream cycle
|
|
58
|
+
DREAM_BACKOFF_INTERVAL = 1800 # seconds after a completed dream before another dream cycle
|
|
58
59
|
MAX_TICK_DURATION = 5.0 # hard ceiling for full active tick (seconds)
|
|
60
|
+
DREAM_TICK_BUDGET = 5.0 # hard ceiling for dormant-active dream tick (seconds)
|
|
59
61
|
SENTINEL_TICK_BUDGET = 0.5 # time budget for sentinel tick
|
|
60
62
|
DORMANT_TICK_BUDGET = 0.2 # time budget for dormant tick
|
|
61
63
|
EMERGENCY_PROMOTION_BUDGET = 0.05 # max latency for emergency mode promotion
|
|
62
64
|
|
|
63
|
-
# Phase timing budgets (fraction of total tick time)
|
|
65
|
+
# Phase timing budgets (fraction of total tick time).
|
|
66
|
+
# Informational only — not enforced by run_phases, which uses the tick-level budget.
|
|
67
|
+
# Useful as a reference for callers that want to self-limit within a phase handler.
|
|
64
68
|
PHASE_BUDGETS = {
|
|
65
69
|
sensory_processing: 0.12,
|
|
66
70
|
emotional_evaluation: 0.08,
|
|
@@ -93,7 +97,7 @@ module Legion
|
|
|
93
97
|
def tick_budget(mode)
|
|
94
98
|
case mode
|
|
95
99
|
when :dormant then DORMANT_TICK_BUDGET
|
|
96
|
-
when :dormant_active then
|
|
100
|
+
when :dormant_active then DREAM_TICK_BUDGET
|
|
97
101
|
when :sentinel then SENTINEL_TICK_BUDGET
|
|
98
102
|
else MAX_TICK_DURATION
|
|
99
103
|
end
|
|
@@ -5,7 +5,7 @@ module Legion
|
|
|
5
5
|
module Tick
|
|
6
6
|
module Helpers
|
|
7
7
|
class State
|
|
8
|
-
attr_reader :mode, :tick_count, :last_signal_at, :last_high_salience_at,
|
|
8
|
+
attr_reader :mode, :tick_count, :last_signal_at, :last_high_salience_at, :last_dream_completed_at,
|
|
9
9
|
:phase_results, :current_phase, :mode_history
|
|
10
10
|
|
|
11
11
|
def initialize(mode: :dormant)
|
|
@@ -13,6 +13,7 @@ module Legion
|
|
|
13
13
|
@tick_count = 0
|
|
14
14
|
@last_signal_at = nil
|
|
15
15
|
@last_high_salience_at = nil
|
|
16
|
+
@last_dream_completed_at = nil
|
|
16
17
|
@phase_results = {}
|
|
17
18
|
@current_phase = nil
|
|
18
19
|
@mode_history = [{ mode: mode, at: Time.now.utc }]
|
|
@@ -44,6 +45,10 @@ module Legion
|
|
|
44
45
|
@mode_history.shift while @mode_history.size > 50
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
def record_dream_completed
|
|
49
|
+
@last_dream_completed_at = Time.now.utc
|
|
50
|
+
end
|
|
51
|
+
|
|
47
52
|
def seconds_since_signal
|
|
48
53
|
return 0.0 unless @last_signal_at
|
|
49
54
|
|
|
@@ -56,14 +61,21 @@ module Legion
|
|
|
56
61
|
Time.now.utc - @last_high_salience_at
|
|
57
62
|
end
|
|
58
63
|
|
|
64
|
+
def seconds_since_dream_completed
|
|
65
|
+
return Float::INFINITY unless @last_dream_completed_at
|
|
66
|
+
|
|
67
|
+
Time.now.utc - @last_dream_completed_at
|
|
68
|
+
end
|
|
69
|
+
|
|
59
70
|
def to_h
|
|
60
71
|
{
|
|
61
|
-
mode:
|
|
62
|
-
tick_count:
|
|
63
|
-
current_phase:
|
|
64
|
-
last_signal_at:
|
|
65
|
-
last_high_salience_at:
|
|
66
|
-
|
|
72
|
+
mode: @mode,
|
|
73
|
+
tick_count: @tick_count,
|
|
74
|
+
current_phase: @current_phase,
|
|
75
|
+
last_signal_at: @last_signal_at,
|
|
76
|
+
last_high_salience_at: @last_high_salience_at,
|
|
77
|
+
last_dream_completed_at: @last_dream_completed_at,
|
|
78
|
+
phases_completed: @phase_results.keys
|
|
67
79
|
}
|
|
68
80
|
end
|
|
69
81
|
end
|
|
@@ -25,7 +25,7 @@ module Legion
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def evaluate_mode_transition(signals: [], emergency: nil, dream_complete: false, **) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
28
|
+
def evaluate_mode_transition(signals: [], emergency: nil, dream_complete: false, **) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
29
29
|
state = tick_state
|
|
30
30
|
previous_mode = state.mode
|
|
31
31
|
|
|
@@ -44,7 +44,7 @@ module Legion
|
|
|
44
44
|
when :dormant
|
|
45
45
|
if signals.any?
|
|
46
46
|
:sentinel
|
|
47
|
-
elsif state.seconds_since_signal >= Helpers::Constants::DREAM_IDLE_THRESHOLD
|
|
47
|
+
elsif state.seconds_since_signal >= Helpers::Constants::DREAM_IDLE_THRESHOLD && dream_backoff_elapsed?(state)
|
|
48
48
|
:dormant_active
|
|
49
49
|
else
|
|
50
50
|
:dormant
|
|
@@ -53,6 +53,7 @@ module Legion
|
|
|
53
53
|
if max_salience >= Helpers::Constants::HIGH_SALIENCE_THRESHOLD || has_human
|
|
54
54
|
:sentinel
|
|
55
55
|
elsif dream_complete
|
|
56
|
+
state.record_dream_completed
|
|
56
57
|
:dormant
|
|
57
58
|
else
|
|
58
59
|
:dormant_active
|
|
@@ -60,10 +61,11 @@ module Legion
|
|
|
60
61
|
when :sentinel
|
|
61
62
|
if has_human || max_salience >= Helpers::Constants::HIGH_SALIENCE_THRESHOLD
|
|
62
63
|
:full_active
|
|
63
|
-
elsif state.seconds_since_signal >= Helpers::Constants::SENTINEL_TO_DREAM_THRESHOLD
|
|
64
|
-
:dormant_active
|
|
65
64
|
elsif state.seconds_since_signal >= Helpers::Constants::SENTINEL_TIMEOUT
|
|
66
65
|
:dormant
|
|
66
|
+
elsif state.seconds_since_signal >= Helpers::Constants::SENTINEL_TO_DREAM_THRESHOLD &&
|
|
67
|
+
dream_backoff_elapsed?(state)
|
|
68
|
+
:dormant_active
|
|
67
69
|
else
|
|
68
70
|
:sentinel
|
|
69
71
|
end
|
|
@@ -98,7 +100,9 @@ module Legion
|
|
|
98
100
|
|
|
99
101
|
previous = tick_state.mode
|
|
100
102
|
tick_state.transition_to(mode)
|
|
101
|
-
|
|
103
|
+
@mode_forced = true
|
|
104
|
+
@force_expires_at = Time.now.utc + Helpers::Constants.tick_budget(mode).clamp(0, Helpers::Constants::MAX_TICK_DURATION)
|
|
105
|
+
log.info "[tick] mode forced: #{previous} -> #{mode} (protected until #{@force_expires_at})"
|
|
102
106
|
{ mode: mode }
|
|
103
107
|
end
|
|
104
108
|
|
|
@@ -114,7 +118,15 @@ module Legion
|
|
|
114
118
|
|
|
115
119
|
log.debug "[tick] ##{state.tick_count} starting | mode=#{state.mode} signals=#{signals.size} max_salience=#{max_salience.round(2)}"
|
|
116
120
|
|
|
117
|
-
|
|
121
|
+
# Skip automatic mode evaluation for the one tick following a forced set_mode call.
|
|
122
|
+
# @mode_forced is set by set_mode and cleared here so the force lasts exactly one cycle.
|
|
123
|
+
if @mode_forced
|
|
124
|
+
log.debug "[tick] ##{state.tick_count} skipping mode evaluation (mode forced to #{state.mode})"
|
|
125
|
+
@mode_forced = false
|
|
126
|
+
@force_expires_at = nil
|
|
127
|
+
else
|
|
128
|
+
evaluate_mode_transition(signals: signals)
|
|
129
|
+
end
|
|
118
130
|
|
|
119
131
|
phases = Helpers::Constants.phases_for_mode(state.mode)
|
|
120
132
|
budget = Helpers::Constants.tick_budget(state.mode)
|
|
@@ -127,6 +139,7 @@ module Legion
|
|
|
127
139
|
context: context
|
|
128
140
|
}
|
|
129
141
|
results = run_phases(phases, state, ctx)
|
|
142
|
+
finish_dream_cycle(state, phases, results)
|
|
130
143
|
|
|
131
144
|
total_elapsed = Time.now.utc - start_time
|
|
132
145
|
skipped = phases - results.keys
|
|
@@ -177,6 +190,28 @@ module Legion
|
|
|
177
190
|
"elapsed=#{(total_elapsed * 1000).round(1)}ms#{skipped_suffix}"
|
|
178
191
|
end
|
|
179
192
|
|
|
193
|
+
def finish_dream_cycle(state, phases, results)
|
|
194
|
+
return unless state.mode == :dormant_active
|
|
195
|
+
return unless phases == Helpers::Constants::DREAM_PHASES
|
|
196
|
+
|
|
197
|
+
skipped = phases - results.keys
|
|
198
|
+
failed = failed_dream_phases(results)
|
|
199
|
+
state.record_dream_completed
|
|
200
|
+
state.transition_to(:dormant)
|
|
201
|
+
if skipped.empty? && failed.empty?
|
|
202
|
+
log.info '[tick] dream cycle complete; backing off to dormant'
|
|
203
|
+
else
|
|
204
|
+
log.info "[tick] dream cycle deferred; backing off to dormant skipped=#{skipped} failed=#{failed}"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def failed_dream_phases(results)
|
|
209
|
+
results.each_with_object([]) do |(phase, result), failed|
|
|
210
|
+
status = result.is_a?(Hash) ? result[:status] : nil
|
|
211
|
+
failed << phase if %i[error failed failure timeout].include?(status)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
180
215
|
def full_active_cooldown_elapsed(state)
|
|
181
216
|
if state.last_high_salience_at
|
|
182
217
|
state.seconds_since_high_salience
|
|
@@ -187,6 +222,10 @@ module Legion
|
|
|
187
222
|
end
|
|
188
223
|
end
|
|
189
224
|
|
|
225
|
+
def dream_backoff_elapsed?(state)
|
|
226
|
+
state.seconds_since_dream_completed >= Helpers::Constants::DREAM_BACKOFF_INTERVAL
|
|
227
|
+
end
|
|
228
|
+
|
|
190
229
|
def tick_state
|
|
191
230
|
@tick_state ||= Helpers::State.new
|
|
192
231
|
end
|
|
@@ -87,8 +87,8 @@ RSpec.describe Legion::Extensions::Tick::Helpers::Constants do
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
describe '.tick_budget' do
|
|
90
|
-
it 'returns
|
|
91
|
-
expect(described_class.tick_budget(:dormant_active)).to eq(
|
|
90
|
+
it 'returns DREAM_TICK_BUDGET for dormant_active' do
|
|
91
|
+
expect(described_class.tick_budget(:dormant_active)).to eq(described_class::DREAM_TICK_BUDGET)
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
|
|
@@ -100,5 +100,13 @@ RSpec.describe Legion::Extensions::Tick::Helpers::Constants do
|
|
|
100
100
|
it 'defines SENTINEL_TO_DREAM_THRESHOLD as 600' do
|
|
101
101
|
expect(described_class::SENTINEL_TO_DREAM_THRESHOLD).to eq(600)
|
|
102
102
|
end
|
|
103
|
+
|
|
104
|
+
it 'defines DREAM_BACKOFF_INTERVAL as 1800' do
|
|
105
|
+
expect(described_class::DREAM_BACKOFF_INTERVAL).to eq(1800)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'defines DREAM_TICK_BUDGET as finite' do
|
|
109
|
+
expect(described_class::DREAM_TICK_BUDGET).to eq(5.0)
|
|
110
|
+
end
|
|
103
111
|
end
|
|
104
112
|
end
|
|
@@ -60,6 +60,19 @@ RSpec.describe Legion::Extensions::Tick::Helpers::State do
|
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
describe '#record_dream_completed' do
|
|
64
|
+
it 'updates last_dream_completed_at' do
|
|
65
|
+
state.record_dream_completed
|
|
66
|
+
expect(state.last_dream_completed_at).not_to be_nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#seconds_since_dream_completed' do
|
|
71
|
+
it 'treats missing dream completion history as eligible for a dream cycle' do
|
|
72
|
+
expect(state.seconds_since_dream_completed).to eq(Float::INFINITY)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
63
76
|
describe '#transition_to' do
|
|
64
77
|
it 'changes mode' do
|
|
65
78
|
state.transition_to(:sentinel)
|
|
@@ -83,6 +96,7 @@ RSpec.describe Legion::Extensions::Tick::Helpers::State do
|
|
|
83
96
|
h = state.to_h
|
|
84
97
|
expect(h).to have_key(:mode)
|
|
85
98
|
expect(h).to have_key(:tick_count)
|
|
99
|
+
expect(h).to have_key(:last_dream_completed_at)
|
|
86
100
|
expect(h).to have_key(:phases_completed)
|
|
87
101
|
end
|
|
88
102
|
end
|
|
@@ -67,6 +67,56 @@ RSpec.describe Legion::Extensions::Tick::Runners::Orchestrator do
|
|
|
67
67
|
result = client.execute_tick(signals: [{ salience: 0.3 }])
|
|
68
68
|
expect(result[:mode]).to eq(:sentinel)
|
|
69
69
|
end
|
|
70
|
+
|
|
71
|
+
it 'completes one dormant_active dream cycle and backs off to dormant' do
|
|
72
|
+
client.set_mode(mode: :dormant_active)
|
|
73
|
+
|
|
74
|
+
result = client.execute_tick
|
|
75
|
+
|
|
76
|
+
expect(result[:phases_executed]).to eq(Legion::Extensions::Tick::Helpers::Constants::DREAM_PHASES)
|
|
77
|
+
expect(result[:mode]).to eq(:dormant)
|
|
78
|
+
expect(client.tick_status[:last_dream_completed_at]).not_to be_nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'does not immediately restart a completed dream cycle on the next heartbeat' do
|
|
82
|
+
state = client.send(:tick_state)
|
|
83
|
+
allow(state).to receive(:seconds_since_signal).and_return(1801.0)
|
|
84
|
+
|
|
85
|
+
client.set_mode(mode: :dormant_active)
|
|
86
|
+
client.execute_tick
|
|
87
|
+
result = client.execute_tick
|
|
88
|
+
|
|
89
|
+
expect(result[:mode]).to eq(:dormant)
|
|
90
|
+
expect(result[:phases_executed]).to eq([:memory_consolidation])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'backs off to dormant when a dream phase fails' do
|
|
94
|
+
client.set_mode(mode: :dormant_active)
|
|
95
|
+
|
|
96
|
+
result = client.execute_tick(
|
|
97
|
+
phase_handlers: {
|
|
98
|
+
dream_narration: ->(**) { { status: :error } }
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
expect(result[:mode]).to eq(:dormant)
|
|
103
|
+
expect(result[:results][:dream_narration][:status]).to eq(:error)
|
|
104
|
+
expect(client.tick_status[:last_dream_completed_at]).not_to be_nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'backs off to dormant when the dream tick budget is exhausted before all phases run' do
|
|
108
|
+
constants = Legion::Extensions::Tick::Helpers::Constants
|
|
109
|
+
allow(constants).to receive(:tick_budget).and_call_original
|
|
110
|
+
allow(constants).to receive(:tick_budget).with(:dormant_active).and_return(0.0)
|
|
111
|
+
|
|
112
|
+
client.set_mode(mode: :dormant_active)
|
|
113
|
+
result = client.execute_tick
|
|
114
|
+
|
|
115
|
+
expect(result[:mode]).to eq(:dormant)
|
|
116
|
+
expect(result[:phases_executed]).to be_empty
|
|
117
|
+
expect(result[:phases_skipped]).to eq(constants::DREAM_PHASES)
|
|
118
|
+
expect(client.tick_status[:last_dream_completed_at]).not_to be_nil
|
|
119
|
+
end
|
|
70
120
|
end
|
|
71
121
|
|
|
72
122
|
describe '#evaluate_mode_transition' do
|
|
@@ -138,6 +188,7 @@ RSpec.describe Legion::Extensions::Tick::Runners::Orchestrator do
|
|
|
138
188
|
result = client.evaluate_mode_transition(dream_complete: true)
|
|
139
189
|
expect(result[:transitioned]).to be true
|
|
140
190
|
expect(result[:new_mode]).to eq(:dormant)
|
|
191
|
+
expect(client.tick_status[:last_dream_completed_at]).not_to be_nil
|
|
141
192
|
end
|
|
142
193
|
|
|
143
194
|
it 'stays dormant_active when no signals and dream not complete' do
|
|
@@ -146,6 +197,29 @@ RSpec.describe Legion::Extensions::Tick::Runners::Orchestrator do
|
|
|
146
197
|
expect(result[:transitioned]).to be false
|
|
147
198
|
expect(result[:current_mode]).to eq(:dormant_active)
|
|
148
199
|
end
|
|
200
|
+
|
|
201
|
+
it 'keeps dormant backed off after a completed dream cycle' do
|
|
202
|
+
state = client.send(:tick_state)
|
|
203
|
+
state.record_dream_completed
|
|
204
|
+
allow(state).to receive(:seconds_since_signal).and_return(1801.0)
|
|
205
|
+
|
|
206
|
+
result = client.evaluate_mode_transition
|
|
207
|
+
|
|
208
|
+
expect(result[:transitioned]).to be false
|
|
209
|
+
expect(result[:current_mode]).to eq(:dormant)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'allows dormant_active again after dream backoff elapses' do
|
|
213
|
+
state = client.send(:tick_state)
|
|
214
|
+
state.record_dream_completed
|
|
215
|
+
allow(state).to receive(:seconds_since_signal).and_return(1801.0)
|
|
216
|
+
allow(state).to receive(:seconds_since_dream_completed).and_return(1801.0)
|
|
217
|
+
|
|
218
|
+
result = client.evaluate_mode_transition
|
|
219
|
+
|
|
220
|
+
expect(result[:transitioned]).to be true
|
|
221
|
+
expect(result[:new_mode]).to eq(:dormant_active)
|
|
222
|
+
end
|
|
149
223
|
end
|
|
150
224
|
|
|
151
225
|
context 'sentinel -> dormant_active' do
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-tick
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -107,8 +107,8 @@ dependencies:
|
|
|
107
107
|
- - ">="
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: 1.3.9
|
|
110
|
-
description: Atomic cognitive processing cycle (
|
|
111
|
-
agentic AI
|
|
110
|
+
description: Atomic cognitive processing cycle (16 phases, 4 modes, 10 dream phases)
|
|
111
|
+
for brain-modeled agentic AI
|
|
112
112
|
email:
|
|
113
113
|
- matthewdiverson@gmail.com
|
|
114
114
|
executables: []
|