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 +4 -4
- data/CHANGELOG.md +12 -0
- data/lib/legion/extensions/agentic/executive/executive_function/helpers/executive_controller.rb +2 -1
- data/lib/legion/extensions/agentic/executive/goal_management/helpers/decomposer.rb +25 -6
- data/lib/legion/extensions/agentic/executive/goal_management/helpers/feedback_listener.rb +22 -2
- data/lib/legion/extensions/agentic/executive/version.rb +1 -1
- data/spec/legion/extensions/agentic/executive/goal_management/helpers/decomposer_spec.rb +15 -0
- data/spec/legion/extensions/agentic/executive/goal_management/helpers/feedback_listener_spec.rb +67 -0
- data/spec/legion/extensions/agentic/executive/goal_management/helpers/task_dispatcher_spec.rb +8 -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: 0ac4e5f2b4c752913d15a4d25240de021a5e7821b2b50be4dd474b1a51ab482f
|
|
4
|
+
data.tar.gz: 9c407b92da789b2f6a11c340b921d772f32fdc5617b968ed5a3dad01c680ce53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/legion/extensions/agentic/executive/executive_function/helpers/executive_controller.rb
CHANGED
|
@@ -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)) + (
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
93
|
-
domain: (sg
|
|
94
|
-
priority: (sg
|
|
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
|
|
@@ -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
|
data/spec/legion/extensions/agentic/executive/goal_management/helpers/feedback_listener_spec.rb
CHANGED
|
@@ -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
|
data/spec/legion/extensions/agentic/executive/goal_management/helpers/task_dispatcher_spec.rb
CHANGED
|
@@ -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')
|