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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3be4c9e415b8ecb5f4bf87248edbc9d25192c1ad57892ed975ef722e517bc95
4
- data.tar.gz: 4ddcbe7624bbab90a6fa463aa6c03cc36ae5ebe3efa9af121cc675fe3287e6cb
3
+ metadata.gz: 5c8a9b49e730209a458c58291781bc1973ce091b09363f5d530475632bccf177
4
+ data.tar.gz: 27e6ad3c2d0690b7258171affd31e43514090d824d66a153b16a1e57faa09f66
5
5
  SHA512:
6
- metadata.gz: 9461d3f7247953b00d7a08263185128834584bc798877a120df3453773d9c0556f32777420cb4c9a9132c333544907009a6d8076681e2cc5c962cbe5ab369649
7
- data.tar.gz: 379fbad5fccc9573de1a3b8ac0ccd660d918c86a5fc08858000b549cb341d925b98692a97e99eef4aab5a4ad13d73dcf31244725aef9b91edb13fc813dd76e23
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
- @last_high_salience_at = Time.now.utc if salience >= Constants::HIGH_SALIENCE_THRESHOLD
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 Float::INFINITY unless @last_signal_at
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 Float::INFINITY unless @last_high_salience_at
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.seconds_since_high_salience >= Helpers::Constants::ACTIVE_TIMEOUT
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
- state.record_signal(salience: max_salience) unless signals.empty?
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
- transition = evaluate_mode_transition(signals: signals)
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 = { budget: budget, start_time: start_time, phase_handlers: phase_handlers, signals: signals }
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
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Tick
6
- VERSION = '0.1.12'
6
+ VERSION = '0.1.13'
7
7
  end
8
8
  end
9
9
  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
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.12
4
+ version: 0.1.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity