lex-agentic-executive 0.2.0 → 0.2.2

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: aaa8f955fd551b961fd735e5bbf9a0dc33b54134b4d5e2ab1506221687aa3195
4
- data.tar.gz: 862042eb5a340dcacce8ec8f2c7cb5f2b9501640ca3d0f39e3307ab69c87d571
3
+ metadata.gz: 0ac4e5f2b4c752913d15a4d25240de021a5e7821b2b50be4dd474b1a51ab482f
4
+ data.tar.gz: 9c407b92da789b2f6a11c340b921d772f32fdc5617b968ed5a3dad01c680ce53
5
5
  SHA512:
6
- metadata.gz: 51cbce9e25c1edb78855eeb7304663855d5f3b4cfc0072b074476f16c230786ea9bd7dfff15c43f10a55558463642218835d3ed3e9f1fcc7a444fbcce1893a74
7
- data.tar.gz: 69f0e5e91e4f98c9b2a46d518301674bedf0bedf7ee04adb4a8b5117c450afedf0ebbe8dc2eef73143a6773e96df5bf2a13e6cdf0ab9c4a590b97c4af821559a
6
+ metadata.gz: 558348baa035852ac306d07d992a3fb8c65263e87bad2d359dfa3c6ab611a25c5c6477ab02cd5d6776f81cd5b5e648f6bcd5583a4984705ca52f587e41304f4d
7
+ data.tar.gz: 9c0eb7766904360b3c679c3357fb94006f24841f3eb392e9dcf7690e6282a4d65f67150b4cc204f0c67d7bafd67f1299ce5dcb2eccaed0e5ed50697d8b634fa8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.2] - 2026-05-08
4
+ ### Fixed
5
+ - `Decomposer#parse_sub_goals` now accesses JSON sub-goal hashes using string keys (with symbol fallback), preventing LLM-decomposed goals from getting empty content/default domain/0.5 priority.
6
+ - `ExecutiveController#common_ef_level` formula corrected — blends minimum effective capacity (weighted 0.6) with average (weighted 0.4) instead of computing a no-op `avg * 0.6 + avg * 0.4`.
7
+ - `FeedbackListener` tracks event handler references, enabling `stop_listening` and `restart_listening` to prevent duplicate handler accumulation on repeated start calls.
8
+
9
+ ## [0.2.1] - 2026-05-07
10
+
11
+ ### Fixed
12
+ - Goal decomposer LLM strategy now parses native `Legion::LLM.chat` hash responses without requiring a legacy chat session.
13
+ - Added regression coverage for cognition-domain dispatch to the MindGrowth analyzer.
14
+
3
15
  ## [0.2.0] - 2026-04-21
4
16
  ### Added
5
17
  - **Autonomous Goal-Setting Pipeline (G1-G5)** — GAIA can now convert intentions into goals, decompose them, dispatch execution, and receive feedback with no human intervention
@@ -72,8 +72,9 @@ module Legion
72
72
 
73
73
  def common_ef_level
74
74
  values = @components.values.map(&:effective_capacity)
75
+ min = values.min
75
76
  avg = values.sum / values.size.to_f
76
- (avg * (1.0 - COMMON_EF_WEIGHT)) + (avg * COMMON_EF_WEIGHT)
77
+ (avg * (1.0 - COMMON_EF_WEIGHT)) + (min * COMMON_EF_WEIGHT)
77
78
  end
78
79
 
79
80
  def can_inhibit?
@@ -48,9 +48,10 @@ module Legion
48
48
 
49
49
  prompt = build_decomposition_prompt(goal)
50
50
  response = Legion::LLM.chat(
51
- caller: { extension: 'lex-agentic-executive', operation: 'decompose' }
52
- ).ask(prompt)
53
- parse_sub_goals(response.content, goal[:domain])
51
+ message: prompt,
52
+ caller: { extension: 'lex-agentic-executive', operation: 'decompose' }
53
+ )
54
+ parse_sub_goals(extract_response_content(response, prompt), goal[:domain])
54
55
  rescue StandardError => e
55
56
  log.error "Decomposer#decompose_with_llm: #{e.message}"
56
57
  nil
@@ -83,21 +84,39 @@ module Legion
83
84
  end
84
85
 
85
86
  def parse_sub_goals(content, domain)
87
+ return nil unless content
88
+
86
89
  cleaned = content.gsub(/```(?:json)?\s*\n?/, '').strip
87
90
  data = json_load(cleaned)
88
91
  return nil unless data.is_a?(Array) && !data.empty?
89
92
 
90
93
  data.map do |sg|
91
94
  {
92
- content: sg[:content].to_s,
93
- domain: (sg[:domain] || domain).to_sym,
94
- priority: (sg[:priority] || 0.5).to_f.clamp(0.0, 1.0)
95
+ content: sg.fetch('content', sg.fetch(:content, '')).to_s,
96
+ domain: sg.fetch('domain', sg.fetch(:domain, domain)).to_sym,
97
+ priority: sg.fetch('priority', sg.fetch(:priority, 0.5)).to_f.clamp(0.0, 1.0)
95
98
  }
96
99
  end
97
100
  rescue StandardError => e
98
101
  log.error "Decomposer#parse_sub_goals: #{e.message}"
99
102
  nil
100
103
  end
104
+
105
+ def extract_response_content(response, prompt)
106
+ return response.strip if response.is_a?(String)
107
+ return response.content if response.respond_to?(:content)
108
+
109
+ if response.respond_to?(:ask)
110
+ asked = response.ask(prompt)
111
+ return extract_response_content(asked, prompt)
112
+ end
113
+
114
+ return nil unless response.is_a?(Hash)
115
+
116
+ response[:content] || response['content'] ||
117
+ response.dig(:message, :content) || response.dig('message', 'content') ||
118
+ response[:response] || response['response']
119
+ end
101
120
  end
102
121
  end
103
122
  end
@@ -10,6 +10,7 @@ module Legion
10
10
  def initialize(engine:)
11
11
  @engine = engine
12
12
  @listening = false
13
+ @handlers = []
13
14
  end
14
15
 
15
16
  def handle_task_event(task_id:, status:, result: nil)
@@ -26,7 +27,7 @@ module Legion
26
27
  return if @listening
27
28
  return unless defined?(Legion::Events)
28
29
 
29
- Legion::Events.on('task.completed') do |event|
30
+ handler_completed = Legion::Events.on('task.completed') do |event|
30
31
  handle_task_event(
31
32
  task_id: event[:task_id],
32
33
  status: event[:status] || 'task.completed',
@@ -34,7 +35,7 @@ module Legion
34
35
  )
35
36
  end
36
37
 
37
- Legion::Events.on('task.failed') do |event|
38
+ handler_failed = Legion::Events.on('task.failed') do |event|
38
39
  handle_task_event(
39
40
  task_id: event[:task_id],
40
41
  status: event[:status] || 'task.failed',
@@ -42,9 +43,28 @@ module Legion
42
43
  )
43
44
  end
44
45
 
46
+ @handlers << { event: 'task.completed', block: handler_completed }
47
+ @handlers << { event: 'task.failed', block: handler_failed }
45
48
  @listening = true
46
49
  end
47
50
 
51
+ def stop_listening
52
+ return unless @listening
53
+
54
+ @handlers.each do |entry|
55
+ Legion::Events.off(entry[:event], entry[:block])
56
+ rescue StandardError => e
57
+ log.debug "[feedback_listener] stop_listening cleanup failed: #{e.message}"
58
+ end
59
+ @handlers.clear
60
+ @listening = false
61
+ end
62
+
63
+ def restart_listening
64
+ stop_listening
65
+ start_listening
66
+ end
67
+
48
68
  def listening?
49
69
  @listening
50
70
  end
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Executive
7
- VERSION = '0.2.0'
7
+ VERSION = '0.2.2'
8
8
  end
9
9
  end
10
10
  end
@@ -33,6 +33,21 @@ RSpec.describe Legion::Extensions::Agentic::Executive::GoalManagement::Helpers::
33
33
  expect(result[:success]).to be true
34
34
  expect(result[:strategy_used]).to eq(:heuristic)
35
35
  end
36
+
37
+ it 'parses native hash responses from Legion::LLM.chat' do
38
+ llm = Module.new
39
+ llm.define_singleton_method(:chat) do |message:, **|
40
+ raise 'missing prompt' if message.to_s.empty?
41
+
42
+ { content: '[{"content":"inspect controls","domain":"safety","priority":0.8}]' }
43
+ end
44
+ stub_const('Legion::LLM', llm)
45
+
46
+ result = described_class.decompose(goal: goal_hash, strategy: :llm)
47
+
48
+ expect(result[:strategy_used]).to eq(:llm)
49
+ expect(result[:sub_goals].first[:content]).to eq('inspect controls')
50
+ end
36
51
  end
37
52
 
38
53
  context 'with default strategy' do
@@ -101,4 +101,71 @@ RSpec.describe Legion::Extensions::Agentic::Executive::GoalManagement::Helpers::
101
101
  expect(listener.listening?).to be false
102
102
  end
103
103
  end
104
+
105
+ describe '#stop_listening' do
106
+ it 'removes registered handlers and resets listening state' do
107
+ events_spy = Class.new do
108
+ attr_reader :registered, :removed
109
+
110
+ def initialize
111
+ @registered = []
112
+ @removed = []
113
+ end
114
+
115
+ def on(event, &block)
116
+ @registered << { event: event, block: block }
117
+ block
118
+ end
119
+
120
+ def off(event, block)
121
+ @removed << { event: event, block: block }
122
+ end
123
+ end.new
124
+ stub_const('Legion::Events', events_spy)
125
+ listener.start_listening
126
+ expect(listener.listening?).to be true
127
+
128
+ listener.stop_listening
129
+ expect(listener.listening?).to be false
130
+ expect(events_spy.removed.size).to eq(2)
131
+ removed_events = events_spy.removed.map { |r| r[:event] }
132
+ expect(removed_events).to include('task.completed')
133
+ expect(removed_events).to include('task.failed')
134
+ end
135
+
136
+ it 'does nothing when not listening' do
137
+ expect { listener.stop_listening }.not_to raise_error
138
+ expect(listener.listening?).to be false
139
+ end
140
+ end
141
+
142
+ describe '#restart_listening' do
143
+ it 'stops and starts listening' do
144
+ events_spy = Class.new do
145
+ attr_reader :registered, :removed
146
+
147
+ def initialize
148
+ @registered = []
149
+ @removed = []
150
+ end
151
+
152
+ def on(event, &block)
153
+ @registered << { event: event, block: block }
154
+ block
155
+ end
156
+
157
+ def off(event, block)
158
+ @removed << { event: event, block: block }
159
+ end
160
+ end.new
161
+ stub_const('Legion::Events', events_spy)
162
+ listener.start_listening
163
+
164
+ listener.restart_listening
165
+ expect(listener.listening?).to be true
166
+ # Original handlers removed, new ones registered
167
+ expect(events_spy.removed.size).to eq(2)
168
+ expect(events_spy.registered.size).to eq(4) # 2 original + 2 new
169
+ end
170
+ end
104
171
  end
@@ -80,6 +80,14 @@ RSpec.describe Legion::Extensions::Agentic::Executive::GoalManagement::Helpers::
80
80
  expect(result[:runner_mapping]).to eq('Legion::Extensions::MindGrowth::Runners::Monitor')
81
81
  end
82
82
 
83
+ it 'dispatches cognition goals to the current MindGrowth analyzer runner' do
84
+ stub_const('Legion::Extensions::MindGrowth::Runners::Analyzer', Module.new)
85
+ result = dispatcher.dispatch_goal(goal: goal_hash.merge(domain: :cognition))
86
+
87
+ expect(result[:dispatched]).to be true
88
+ expect(result[:runner_mapping]).to eq('Legion::Extensions::MindGrowth::Runners::Analyzer')
89
+ end
90
+
83
91
  it 'includes goal_id in the response' do
84
92
  result = dispatcher.dispatch_goal(goal: goal_hash)
85
93
  expect(result[:goal_id]).to eq('goal-123')
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-agentic-executive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity