lex-tick 0.1.12 → 0.1.13
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/lib/legion/extensions/tick/actors/tick.rb +16 -0
- data/lib/legion/extensions/tick/helpers/state.rb +6 -4
- data/lib/legion/extensions/tick/runners/orchestrator.rb +28 -12
- data/lib/legion/extensions/tick/version.rb +1 -1
- data/spec/legion/extensions/tick/actors/tick_spec.rb +43 -3
- data/spec/legion/extensions/tick/helpers/state_spec.rb +17 -0
- data/spec/legion/extensions/tick/runners/orchestrator_spec.rb +38 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c8a9b49e730209a458c58291781bc1973ce091b09363f5d530475632bccf177
|
|
4
|
+
data.tar.gz: 27e6ad3c2d0690b7258171affd31e43514090d824d66a153b16a1e57faa09f66
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bd517d24eb257869fb775c52b7dd5448fdd140ad72899441edffe6ebd118888c91258b10052128f579b7dc6a747ad02721e897b76fb0db0c85fcdc3ec7d12368
|
|
7
|
+
data.tar.gz: d303fd69ff1ad994460a7531f32553cc80a14855cfbb0c3eb20b94c50a894b32a860de5dbdb217310c650d5b6ab9f33abdd7d86ba82e56e634fc2d7e462dfa30
|
|
@@ -19,10 +19,14 @@ module Legion
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def runner_class
|
|
22
|
+
return Legion::Gaia if gaia_heartbeat_available?
|
|
23
|
+
|
|
22
24
|
Legion::Extensions::Tick::Runners::Orchestrator
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
def runner_function
|
|
28
|
+
return 'heartbeat' if gaia_heartbeat_available?
|
|
29
|
+
|
|
26
30
|
'execute_tick'
|
|
27
31
|
end
|
|
28
32
|
|
|
@@ -51,11 +55,23 @@ module Legion
|
|
|
51
55
|
end
|
|
52
56
|
|
|
53
57
|
def args
|
|
58
|
+
return {} if gaia_heartbeat_available?
|
|
59
|
+
|
|
54
60
|
{ signals: [], phase_handlers: {} }
|
|
55
61
|
end
|
|
56
62
|
|
|
57
63
|
private
|
|
58
64
|
|
|
65
|
+
def gaia_heartbeat_available?
|
|
66
|
+
return false unless defined?(Legion::Gaia)
|
|
67
|
+
return false unless Legion::Gaia.respond_to?(:started?) && Legion::Gaia.started?
|
|
68
|
+
|
|
69
|
+
!Legion::Gaia.respond_to?(:router_mode?) || !Legion::Gaia.router_mode?
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
log.debug "gaia_heartbeat_available? check failed: #{e.message}"
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
|
|
59
75
|
def apply_initial_jitter
|
|
60
76
|
return unless Helpers::Jitter.jitter_enabled?
|
|
61
77
|
|
|
@@ -18,9 +18,11 @@ module Legion
|
|
|
18
18
|
@mode_history = [{ mode: mode, at: Time.now.utc }]
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def record_signal(salience: 0.0)
|
|
21
|
+
def record_signal(salience: 0.0, source_type: nil)
|
|
22
22
|
@last_signal_at = Time.now.utc
|
|
23
|
-
|
|
23
|
+
return unless salience >= Constants::HIGH_SALIENCE_THRESHOLD || source_type == :human_direct
|
|
24
|
+
|
|
25
|
+
@last_high_salience_at = Time.now.utc
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def record_phase(phase, result)
|
|
@@ -43,13 +45,13 @@ module Legion
|
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
def seconds_since_signal
|
|
46
|
-
return
|
|
48
|
+
return 0.0 unless @last_signal_at
|
|
47
49
|
|
|
48
50
|
Time.now.utc - @last_signal_at
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
def seconds_since_high_salience
|
|
52
|
-
return
|
|
54
|
+
return 0.0 unless @last_high_salience_at
|
|
53
55
|
|
|
54
56
|
Time.now.utc - @last_high_salience_at
|
|
55
57
|
end
|
|
@@ -12,16 +12,16 @@ module Legion
|
|
|
12
12
|
false
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def execute_tick(signals: [], phase_handlers: {}, **)
|
|
15
|
+
def execute_tick(signals: [], phase_handlers: {}, **context)
|
|
16
16
|
if defined?(Legion::Telemetry::OpenInference)
|
|
17
17
|
state = tick_state
|
|
18
18
|
Legion::Telemetry::OpenInference.agent_span(
|
|
19
19
|
name: "tick-#{state.tick_count + 1}", mode: state.mode,
|
|
20
20
|
phase_count: Helpers::Constants.phases_for_mode(state.mode).size,
|
|
21
21
|
budget_ms: (Helpers::Constants.tick_budget(state.mode) * 1000).round
|
|
22
|
-
) { |_span| execute_tick_impl(signals: signals, phase_handlers: phase_handlers) }
|
|
22
|
+
) { |_span| execute_tick_impl(signals: signals, phase_handlers: phase_handlers, **context) }
|
|
23
23
|
else
|
|
24
|
-
execute_tick_impl(signals: signals, phase_handlers: phase_handlers)
|
|
24
|
+
execute_tick_impl(signals: signals, phase_handlers: phase_handlers, **context)
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -68,7 +68,7 @@ module Legion
|
|
|
68
68
|
:sentinel
|
|
69
69
|
end
|
|
70
70
|
when :full_active
|
|
71
|
-
if state
|
|
71
|
+
if full_active_cooldown_elapsed(state) >= Helpers::Constants::ACTIVE_TIMEOUT
|
|
72
72
|
:sentinel
|
|
73
73
|
else
|
|
74
74
|
:full_active
|
|
@@ -104,22 +104,28 @@ module Legion
|
|
|
104
104
|
|
|
105
105
|
private
|
|
106
106
|
|
|
107
|
-
def execute_tick_impl(signals:, phase_handlers
|
|
107
|
+
def execute_tick_impl(signals:, phase_handlers:, **context)
|
|
108
108
|
state = tick_state
|
|
109
109
|
state.increment_tick
|
|
110
110
|
|
|
111
111
|
max_salience = signals.map { |s| s.is_a?(Hash) ? (s[:salience] || 0.0) : 0.0 }.max || 0.0
|
|
112
|
-
|
|
112
|
+
has_human = signals.any? { |s| s.is_a?(Hash) && s[:source_type] == :human_direct }
|
|
113
|
+
state.record_signal(salience: max_salience, source_type: (has_human ? :human_direct : nil)) unless signals.empty?
|
|
113
114
|
|
|
114
115
|
log.debug "[tick] ##{state.tick_count} starting | mode=#{state.mode} signals=#{signals.size} max_salience=#{max_salience.round(2)}"
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
log.info "[tick] mode transition: #{transition[:previous_mode]} -> #{transition[:new_mode]} (#{transition[:reason]})" if transition[:transitioned]
|
|
117
|
+
evaluate_mode_transition(signals: signals)
|
|
118
118
|
|
|
119
119
|
phases = Helpers::Constants.phases_for_mode(state.mode)
|
|
120
120
|
budget = Helpers::Constants.tick_budget(state.mode)
|
|
121
121
|
start_time = Time.now.utc
|
|
122
|
-
ctx = {
|
|
122
|
+
ctx = {
|
|
123
|
+
budget: budget,
|
|
124
|
+
start_time: start_time,
|
|
125
|
+
phase_handlers: phase_handlers,
|
|
126
|
+
signals: signals,
|
|
127
|
+
context: context
|
|
128
|
+
}
|
|
123
129
|
results = run_phases(phases, state, ctx)
|
|
124
130
|
|
|
125
131
|
total_elapsed = Time.now.utc - start_time
|
|
@@ -148,16 +154,16 @@ module Legion
|
|
|
148
154
|
break
|
|
149
155
|
end
|
|
150
156
|
|
|
151
|
-
result = run_single_phase(phase, ctx[:phase_handlers][phase], state, ctx[:signals], results)
|
|
157
|
+
result = run_single_phase(phase, ctx[:phase_handlers][phase], state, ctx[:signals], results, **ctx[:context])
|
|
152
158
|
state.record_phase(phase, result)
|
|
153
159
|
results[phase] = result
|
|
154
160
|
end
|
|
155
161
|
results
|
|
156
162
|
end
|
|
157
163
|
|
|
158
|
-
def run_single_phase(phase, handler, state, signals, results)
|
|
164
|
+
def run_single_phase(phase, handler, state, signals, results, **context)
|
|
159
165
|
phase_start = Time.now.utc
|
|
160
|
-
result = handler ? handler.call(state: state, signals: signals, prior_results: results) : { status: :no_handler }
|
|
166
|
+
result = handler ? handler.call(state: state, signals: signals, prior_results: results, **context) : { status: :no_handler }
|
|
161
167
|
phase_elapsed = ((Time.now.utc - phase_start) * 1000).round(1)
|
|
162
168
|
status = result.is_a?(Hash) ? (result[:status] || :ok) : :ok
|
|
163
169
|
log.debug "[tick] ##{state.tick_count} phase=#{phase} status=#{status} (#{phase_elapsed}ms)"
|
|
@@ -171,6 +177,16 @@ module Legion
|
|
|
171
177
|
"elapsed=#{(total_elapsed * 1000).round(1)}ms#{skipped_suffix}"
|
|
172
178
|
end
|
|
173
179
|
|
|
180
|
+
def full_active_cooldown_elapsed(state)
|
|
181
|
+
if state.last_high_salience_at
|
|
182
|
+
state.seconds_since_high_salience
|
|
183
|
+
elsif state.last_signal_at
|
|
184
|
+
state.seconds_since_signal
|
|
185
|
+
else
|
|
186
|
+
Helpers::Constants::ACTIVE_TIMEOUT
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
174
190
|
def tick_state
|
|
175
191
|
@tick_state ||= Helpers::State.new
|
|
176
192
|
end
|
|
@@ -58,15 +58,45 @@ RSpec.describe Legion::Extensions::Tick::Actor::Tick do
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
describe '#runner_class' do
|
|
61
|
-
it 'returns the Orchestrator module' do
|
|
61
|
+
it 'returns the Orchestrator module by default' do
|
|
62
|
+
expect(actor.runner_class).to eq(Legion::Extensions::Tick::Runners::Orchestrator)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'returns Legion::Gaia when Gaia is running in agent mode' do
|
|
66
|
+
stub_const('Legion::Gaia', Module.new do
|
|
67
|
+
def self.started? = true
|
|
68
|
+
|
|
69
|
+
def self.router_mode? = false
|
|
70
|
+
end)
|
|
71
|
+
|
|
72
|
+
expect(actor.runner_class).to eq(Legion::Gaia)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'falls back to the Orchestrator module when Gaia is running in router mode' do
|
|
76
|
+
stub_const('Legion::Gaia', Module.new do
|
|
77
|
+
def self.started? = true
|
|
78
|
+
|
|
79
|
+
def self.router_mode? = true
|
|
80
|
+
end)
|
|
81
|
+
|
|
62
82
|
expect(actor.runner_class).to eq(Legion::Extensions::Tick::Runners::Orchestrator)
|
|
63
83
|
end
|
|
64
84
|
end
|
|
65
85
|
|
|
66
86
|
describe '#runner_function' do
|
|
67
|
-
it 'returns execute_tick' do
|
|
87
|
+
it 'returns execute_tick by default' do
|
|
68
88
|
expect(actor.runner_function).to eq('execute_tick')
|
|
69
89
|
end
|
|
90
|
+
|
|
91
|
+
it 'returns heartbeat when Gaia is running in agent mode' do
|
|
92
|
+
stub_const('Legion::Gaia', Module.new do
|
|
93
|
+
def self.started? = true
|
|
94
|
+
|
|
95
|
+
def self.router_mode? = false
|
|
96
|
+
end)
|
|
97
|
+
|
|
98
|
+
expect(actor.runner_function).to eq('heartbeat')
|
|
99
|
+
end
|
|
70
100
|
end
|
|
71
101
|
|
|
72
102
|
describe '#time' do
|
|
@@ -100,9 +130,19 @@ RSpec.describe Legion::Extensions::Tick::Actor::Tick do
|
|
|
100
130
|
end
|
|
101
131
|
|
|
102
132
|
describe '#args' do
|
|
103
|
-
it 'returns a hash with empty signals and phase_handlers' do
|
|
133
|
+
it 'returns a hash with empty signals and phase_handlers by default' do
|
|
104
134
|
expect(actor.args).to eq({ signals: [], phase_handlers: {} })
|
|
105
135
|
end
|
|
136
|
+
|
|
137
|
+
it 'returns empty args when Gaia is running in agent mode' do
|
|
138
|
+
stub_const('Legion::Gaia', Module.new do
|
|
139
|
+
def self.started? = true
|
|
140
|
+
|
|
141
|
+
def self.router_mode? = false
|
|
142
|
+
end)
|
|
143
|
+
|
|
144
|
+
expect(actor.args).to eq({})
|
|
145
|
+
end
|
|
106
146
|
end
|
|
107
147
|
|
|
108
148
|
describe 'initial jitter behavior' do
|
|
@@ -28,6 +28,11 @@ RSpec.describe Legion::Extensions::Tick::Helpers::State do
|
|
|
28
28
|
state.record_signal(salience: 0.3)
|
|
29
29
|
expect(state.last_high_salience_at).to be_nil
|
|
30
30
|
end
|
|
31
|
+
|
|
32
|
+
it 'updates last_high_salience_at for human direct signals' do
|
|
33
|
+
state.record_signal(salience: 0.3, source_type: :human_direct)
|
|
34
|
+
expect(state.last_high_salience_at).not_to be_nil
|
|
35
|
+
end
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
describe '#increment_tick' do
|
|
@@ -43,6 +48,18 @@ RSpec.describe Legion::Extensions::Tick::Helpers::State do
|
|
|
43
48
|
end
|
|
44
49
|
end
|
|
45
50
|
|
|
51
|
+
describe '#seconds_since_signal' do
|
|
52
|
+
it 'treats a fresh boot as recently active' do
|
|
53
|
+
expect(state.seconds_since_signal).to eq(0.0)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe '#seconds_since_high_salience' do
|
|
58
|
+
it 'treats missing high-salience history as recent, not infinite' do
|
|
59
|
+
expect(state.seconds_since_high_salience).to eq(0.0)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
46
63
|
describe '#transition_to' do
|
|
47
64
|
it 'changes mode' do
|
|
48
65
|
state.transition_to(:sentinel)
|
|
@@ -40,6 +40,23 @@ RSpec.describe Legion::Extensions::Tick::Runners::Orchestrator do
|
|
|
40
40
|
expect(handler_called).to be true
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
it 'passes extra execution context through to phase handlers' do
|
|
44
|
+
seen = nil
|
|
45
|
+
handlers = {
|
|
46
|
+
memory_consolidation: lambda { |partner_observations: nil, **|
|
|
47
|
+
seen = partner_observations
|
|
48
|
+
{ status: :ok }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
client.execute_tick(
|
|
53
|
+
phase_handlers: handlers,
|
|
54
|
+
partner_observations: [{ identity: 'partner-1' }]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
expect(seen).to eq([{ identity: 'partner-1' }])
|
|
58
|
+
end
|
|
59
|
+
|
|
43
60
|
it 'reports no_handler for unhandled phases' do
|
|
44
61
|
result = client.execute_tick
|
|
45
62
|
expect(result[:results][:memory_consolidation][:status]).to eq(:no_handler)
|
|
@@ -86,6 +103,12 @@ RSpec.describe Legion::Extensions::Tick::Runners::Orchestrator do
|
|
|
86
103
|
expect(result[:transitioned]).to be false
|
|
87
104
|
end
|
|
88
105
|
|
|
106
|
+
it 'does not enter dormant_active immediately on fresh boot' do
|
|
107
|
+
result = client.evaluate_mode_transition
|
|
108
|
+
expect(result[:transitioned]).to be false
|
|
109
|
+
expect(result[:current_mode]).to eq(:dormant)
|
|
110
|
+
end
|
|
111
|
+
|
|
89
112
|
context 'dormant_active transitions' do
|
|
90
113
|
it 'transitions dormant -> dormant_active after DREAM_IDLE_THRESHOLD with no signals' do
|
|
91
114
|
state = client.send(:tick_state)
|
|
@@ -135,6 +158,21 @@ RSpec.describe Legion::Extensions::Tick::Runners::Orchestrator do
|
|
|
135
158
|
expect(result[:new_mode]).to eq(:dormant_active)
|
|
136
159
|
end
|
|
137
160
|
end
|
|
161
|
+
|
|
162
|
+
context 'full_active cooldown' do
|
|
163
|
+
it 'demotes full_active after ACTIVE_TIMEOUT when only human-direct signal history exists' do
|
|
164
|
+
client.set_mode(mode: :full_active)
|
|
165
|
+
state = client.send(:tick_state)
|
|
166
|
+
allow(state).to receive(:last_high_salience_at).and_return(nil)
|
|
167
|
+
allow(state).to receive(:last_signal_at).and_return(Time.now.utc - 301)
|
|
168
|
+
allow(state).to receive(:seconds_since_signal).and_return(301.0)
|
|
169
|
+
|
|
170
|
+
result = client.evaluate_mode_transition
|
|
171
|
+
|
|
172
|
+
expect(result[:transitioned]).to be true
|
|
173
|
+
expect(result[:new_mode]).to eq(:sentinel)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
138
176
|
end
|
|
139
177
|
|
|
140
178
|
describe '#set_mode' do
|