lex-agentic-executive 0.1.12 → 0.2.0
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 +13 -0
- data/lib/legion/extensions/agentic/executive/goal_management/client.rb +23 -0
- data/lib/legion/extensions/agentic/executive/goal_management/helpers/decomposer.rb +107 -0
- data/lib/legion/extensions/agentic/executive/goal_management/helpers/feedback_listener.rb +63 -0
- data/lib/legion/extensions/agentic/executive/goal_management/helpers/goal.rb +41 -12
- data/lib/legion/extensions/agentic/executive/goal_management/helpers/goal_engine.rb +99 -10
- data/lib/legion/extensions/agentic/executive/goal_management/helpers/goal_persistence.rb +145 -0
- data/lib/legion/extensions/agentic/executive/goal_management/helpers/task_dispatcher.rb +109 -0
- data/lib/legion/extensions/agentic/executive/goal_management/runners/goal_management.rb +96 -36
- data/lib/legion/extensions/agentic/executive/goal_management.rb +3 -0
- data/lib/legion/extensions/agentic/executive/version.rb +1 -1
- data/lib/legion/extensions/agentic/executive/volition/client.rb +3 -1
- data/lib/legion/extensions/agentic/executive/volition/helpers/goal_bridge.rb +86 -0
- data/lib/legion/extensions/agentic/executive/volition/runners/volition.rb +13 -6
- data/lib/legion/extensions/agentic/executive/volition.rb +1 -0
- data/spec/integration/autonomous_goal_pipeline_spec.rb +107 -0
- data/spec/legion/extensions/agentic/executive/goal_management/helpers/decomposer_spec.rb +152 -0
- data/spec/legion/extensions/agentic/executive/goal_management/helpers/feedback_listener_spec.rb +104 -0
- data/spec/legion/extensions/agentic/executive/goal_management/helpers/goal_engine_spec.rb +32 -0
- data/spec/legion/extensions/agentic/executive/goal_management/helpers/goal_persistence_spec.rb +119 -0
- data/spec/legion/extensions/agentic/executive/goal_management/helpers/task_dispatcher_spec.rb +176 -0
- data/spec/legion/extensions/agentic/executive/volition/helpers/goal_bridge_spec.rb +58 -0
- data/spec/legion/extensions/agentic/executive/volition/runners/volition_spec.rb +26 -0
- metadata +12 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Agentic
|
|
6
|
+
module Executive
|
|
7
|
+
module GoalManagement
|
|
8
|
+
module Helpers
|
|
9
|
+
class TaskDispatcher
|
|
10
|
+
DOMAIN_RUNNERS = {
|
|
11
|
+
safety: { runner_class: 'Legion::Extensions::MindGrowth::Runners::Monitor',
|
|
12
|
+
function: :health_check,
|
|
13
|
+
args_builder: ->(goal) { { extension: goal[:domain].to_s } } },
|
|
14
|
+
cognition: { runner_class: 'Legion::Extensions::MindGrowth::Runners::Analyzer',
|
|
15
|
+
function: :recommend_priorities,
|
|
16
|
+
args_builder: ->(_goal) { { existing_extensions: [] } } },
|
|
17
|
+
perception: { client_class: 'Legion::Extensions::Agentic::Learning::Curiosity::Client',
|
|
18
|
+
function: :detect_gaps,
|
|
19
|
+
args_builder: ->(_goal) { { prior_results: {} } } },
|
|
20
|
+
introspection: { client_class: 'Legion::Extensions::Agentic::Executive::Volition::Client',
|
|
21
|
+
function: :volition_status,
|
|
22
|
+
args_builder: ->(_goal) { {} } }
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
def dispatch_goal(goal:)
|
|
26
|
+
domain = goal[:domain]&.to_sym
|
|
27
|
+
mapping = DOMAIN_RUNNERS[domain]
|
|
28
|
+
|
|
29
|
+
return unroutable(goal, domain) unless mapping
|
|
30
|
+
return unroutable_not_loaded(goal, domain) unless runner_class_loaded?(mapping)
|
|
31
|
+
|
|
32
|
+
execute_dispatch(goal: goal, mapping: mapping)
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
log.error "[task_dispatcher] dispatch error goal=#{goal[:id]} #{e.message}"
|
|
35
|
+
{ dispatched: false, error: e.message, goal_id: goal[:id] }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def runner_class_loaded?(mapping)
|
|
41
|
+
class_name = mapping[:runner_class] || mapping[:client_class]
|
|
42
|
+
Kernel.const_get(class_name)
|
|
43
|
+
true
|
|
44
|
+
rescue NameError => e
|
|
45
|
+
log.debug "[task_dispatcher] runner not loaded: #{e.message}"
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def execute_dispatch(goal:, mapping:)
|
|
50
|
+
function = mapping[:function]
|
|
51
|
+
args = mapping[:args_builder].call(goal)
|
|
52
|
+
|
|
53
|
+
result = if mapping.key?(:client_class)
|
|
54
|
+
dispatch_via_client(mapping[:client_class], function, args)
|
|
55
|
+
else
|
|
56
|
+
dispatch_via_runner(mapping[:runner_class], function, args)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
success = result[:status] == 'task.completed'
|
|
60
|
+
task_id = result[:task_id]
|
|
61
|
+
runner_key = mapping[:runner_class] || mapping[:client_class]
|
|
62
|
+
|
|
63
|
+
log.info "[task_dispatcher] dispatched goal=#{goal[:id]} runner=#{runner_key} function=#{function}"
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
dispatched: true,
|
|
67
|
+
success: success,
|
|
68
|
+
task_id: task_id,
|
|
69
|
+
runner_mapping: runner_key,
|
|
70
|
+
result: result,
|
|
71
|
+
goal_id: goal[:id]
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def dispatch_via_runner(runner_class, function, args)
|
|
76
|
+
Legion::Runner.run(
|
|
77
|
+
runner_class: runner_class,
|
|
78
|
+
function: function,
|
|
79
|
+
args: args,
|
|
80
|
+
generate_task: true,
|
|
81
|
+
check_subtask: true
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def dispatch_via_client(client_class, function, args)
|
|
86
|
+
client = Kernel.const_get(client_class).new
|
|
87
|
+
client.send(function, **args)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def log
|
|
91
|
+
Legion::Logging
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def unroutable(goal, domain)
|
|
95
|
+
log.debug "[task_dispatcher] no runner for domain=#{domain} goal=#{goal[:id]}"
|
|
96
|
+
{ dispatched: false, reason: :no_runner, domain: domain, goal_id: goal[:id] }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def unroutable_not_loaded(goal, domain)
|
|
100
|
+
log.debug "[task_dispatcher] runner not loaded for domain=#{domain} goal=#{goal[:id]}"
|
|
101
|
+
{ dispatched: false, reason: :runner_not_loaded, domain: domain, goal_id: goal[:id] }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -7,151 +7,203 @@ module Legion
|
|
|
7
7
|
module GoalManagement
|
|
8
8
|
module Runners
|
|
9
9
|
module GoalManagement
|
|
10
|
-
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
11
|
-
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
11
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
12
12
|
|
|
13
13
|
def add_goal(content:, parent_id: nil, domain: :general,
|
|
14
14
|
priority: Helpers::Constants::DEFAULT_PRIORITY, deadline: nil, **)
|
|
15
|
-
|
|
15
|
+
log.debug "[goal_management] runner add_goal domain=#{domain}"
|
|
16
16
|
engine.add_goal(content: content, parent_id: parent_id, domain: domain,
|
|
17
17
|
priority: priority, deadline: deadline)
|
|
18
18
|
rescue StandardError => e
|
|
19
|
-
|
|
19
|
+
log.error "[goal_management] add_goal error: #{e.message}"
|
|
20
20
|
{ success: false, error: e.message }
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def decompose_goal(goal_id:, sub_goals:, **)
|
|
24
|
-
|
|
24
|
+
log.debug "[goal_management] runner decompose_goal parent=#{goal_id}"
|
|
25
25
|
engine.decompose(goal_id: goal_id, sub_goals: sub_goals)
|
|
26
26
|
rescue StandardError => e
|
|
27
|
-
|
|
27
|
+
log.error "[goal_management] decompose_goal error: #{e.message}"
|
|
28
|
+
{ success: false, error: e.message }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def auto_decompose_goal(goal_id:, strategy: :heuristic, **)
|
|
32
|
+
goal_data = engine.goals[goal_id]
|
|
33
|
+
return { success: false, error: :not_found } unless goal_data
|
|
34
|
+
|
|
35
|
+
decomp = Helpers::Decomposer.decompose(goal: goal_data.to_h, strategy: strategy)
|
|
36
|
+
return decomp unless decomp[:success]
|
|
37
|
+
|
|
38
|
+
result = engine.decompose(goal_id: goal_id, sub_goals: decomp[:sub_goals])
|
|
39
|
+
result.merge(strategy_used: decomp[:strategy_used])
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
log.error "[goal_management] auto_decompose_goal error: #{e.message}"
|
|
28
42
|
{ success: false, error: e.message }
|
|
29
43
|
end
|
|
30
44
|
|
|
31
45
|
def activate_goal(goal_id:, **)
|
|
32
|
-
|
|
46
|
+
log.debug "[goal_management] runner activate_goal id=#{goal_id}"
|
|
33
47
|
engine.activate_goal(goal_id: goal_id)
|
|
34
48
|
rescue StandardError => e
|
|
35
|
-
|
|
49
|
+
log.error "[goal_management] activate_goal error: #{e.message}"
|
|
36
50
|
{ success: false, error: e.message }
|
|
37
51
|
end
|
|
38
52
|
|
|
39
53
|
def complete_goal(goal_id:, **)
|
|
40
|
-
|
|
54
|
+
log.debug "[goal_management] runner complete_goal id=#{goal_id}"
|
|
41
55
|
engine.complete_goal(goal_id: goal_id)
|
|
42
56
|
rescue StandardError => e
|
|
43
|
-
|
|
57
|
+
log.error "[goal_management] complete_goal error: #{e.message}"
|
|
44
58
|
{ success: false, error: e.message }
|
|
45
59
|
end
|
|
46
60
|
|
|
47
61
|
def abandon_goal(goal_id:, **)
|
|
48
|
-
|
|
62
|
+
log.debug "[goal_management] runner abandon_goal id=#{goal_id}"
|
|
49
63
|
engine.abandon_goal(goal_id: goal_id)
|
|
50
64
|
rescue StandardError => e
|
|
51
|
-
|
|
65
|
+
log.error "[goal_management] abandon_goal error: #{e.message}"
|
|
52
66
|
{ success: false, error: e.message }
|
|
53
67
|
end
|
|
54
68
|
|
|
55
69
|
def block_goal(goal_id:, **)
|
|
56
|
-
|
|
70
|
+
log.debug "[goal_management] runner block_goal id=#{goal_id}"
|
|
57
71
|
engine.block_goal(goal_id: goal_id)
|
|
58
72
|
rescue StandardError => e
|
|
59
|
-
|
|
73
|
+
log.error "[goal_management] block_goal error: #{e.message}"
|
|
60
74
|
{ success: false, error: e.message }
|
|
61
75
|
end
|
|
62
76
|
|
|
63
77
|
def unblock_goal(goal_id:, **)
|
|
64
|
-
|
|
78
|
+
log.debug "[goal_management] runner unblock_goal id=#{goal_id}"
|
|
65
79
|
engine.unblock_goal(goal_id: goal_id)
|
|
66
80
|
rescue StandardError => e
|
|
67
|
-
|
|
81
|
+
log.error "[goal_management] unblock_goal error: #{e.message}"
|
|
68
82
|
{ success: false, error: e.message }
|
|
69
83
|
end
|
|
70
84
|
|
|
71
85
|
def advance_goal_progress(goal_id:, amount:, **)
|
|
72
|
-
|
|
86
|
+
log.debug "[goal_management] runner advance_goal_progress id=#{goal_id} amount=#{amount}"
|
|
73
87
|
engine.advance_progress(goal_id: goal_id, amount: amount)
|
|
74
88
|
rescue StandardError => e
|
|
75
|
-
|
|
89
|
+
log.error "[goal_management] advance_goal_progress error: #{e.message}"
|
|
76
90
|
{ success: false, error: e.message }
|
|
77
91
|
end
|
|
78
92
|
|
|
79
93
|
def detect_goal_conflicts(goal_id:, **)
|
|
80
|
-
|
|
94
|
+
log.debug "[goal_management] runner detect_goal_conflicts id=#{goal_id}"
|
|
81
95
|
engine.detect_conflicts(goal_id: goal_id)
|
|
82
96
|
rescue StandardError => e
|
|
83
|
-
|
|
97
|
+
log.error "[goal_management] detect_goal_conflicts error: #{e.message}"
|
|
84
98
|
{ success: false, error: e.message }
|
|
85
99
|
end
|
|
86
100
|
|
|
87
101
|
def list_active_goals(**)
|
|
88
102
|
goals = engine.active_goals
|
|
89
|
-
|
|
103
|
+
log.debug "[goal_management] list_active_goals count=#{goals.size}"
|
|
90
104
|
{ success: true, goals: goals.map(&:to_h), count: goals.size }
|
|
91
105
|
rescue StandardError => e
|
|
92
|
-
|
|
106
|
+
log.error "[goal_management] list_active_goals error: #{e.message}"
|
|
93
107
|
{ success: false, error: e.message }
|
|
94
108
|
end
|
|
95
109
|
|
|
96
110
|
def list_blocked_goals(**)
|
|
97
111
|
goals = engine.blocked_goals
|
|
98
|
-
|
|
112
|
+
log.debug "[goal_management] list_blocked_goals count=#{goals.size}"
|
|
99
113
|
{ success: true, goals: goals.map(&:to_h), count: goals.size }
|
|
100
114
|
rescue StandardError => e
|
|
101
|
-
|
|
115
|
+
log.error "[goal_management] list_blocked_goals error: #{e.message}"
|
|
102
116
|
{ success: false, error: e.message }
|
|
103
117
|
end
|
|
104
118
|
|
|
105
119
|
def list_overdue_goals(**)
|
|
106
120
|
goals = engine.overdue_goals
|
|
107
|
-
|
|
121
|
+
log.debug "[goal_management] list_overdue_goals count=#{goals.size}"
|
|
108
122
|
{ success: true, goals: goals.map(&:to_h), count: goals.size }
|
|
109
123
|
rescue StandardError => e
|
|
110
|
-
|
|
124
|
+
log.error "[goal_management] list_overdue_goals error: #{e.message}"
|
|
111
125
|
{ success: false, error: e.message }
|
|
112
126
|
end
|
|
113
127
|
|
|
114
128
|
def list_completed_goals(**)
|
|
115
129
|
goals = engine.completed_goals
|
|
116
|
-
|
|
130
|
+
log.debug "[goal_management] list_completed_goals count=#{goals.size}"
|
|
117
131
|
{ success: true, goals: goals.map(&:to_h), count: goals.size }
|
|
118
132
|
rescue StandardError => e
|
|
119
|
-
|
|
133
|
+
log.error "[goal_management] list_completed_goals error: #{e.message}"
|
|
120
134
|
{ success: false, error: e.message }
|
|
121
135
|
end
|
|
122
136
|
|
|
123
137
|
def get_goal_tree(goal_id:, **)
|
|
124
|
-
|
|
138
|
+
log.debug "[goal_management] runner get_goal_tree id=#{goal_id}"
|
|
125
139
|
engine.goal_tree(goal_id: goal_id)
|
|
126
140
|
rescue StandardError => e
|
|
127
|
-
|
|
141
|
+
log.error "[goal_management] get_goal_tree error: #{e.message}"
|
|
128
142
|
{ success: false, error: e.message }
|
|
129
143
|
end
|
|
130
144
|
|
|
131
145
|
def highest_priority_goals(limit: 5, **)
|
|
132
146
|
goals = engine.highest_priority(limit: limit)
|
|
133
|
-
|
|
147
|
+
log.debug "[goal_management] highest_priority_goals limit=#{limit} count=#{goals.size}"
|
|
134
148
|
{ success: true, goals: goals.map(&:to_h), count: goals.size }
|
|
135
149
|
rescue StandardError => e
|
|
136
|
-
|
|
150
|
+
log.error "[goal_management] highest_priority_goals error: #{e.message}"
|
|
137
151
|
{ success: false, error: e.message }
|
|
138
152
|
end
|
|
139
153
|
|
|
140
154
|
def decay_priorities(**)
|
|
141
155
|
result = engine.decay_all_priorities!
|
|
142
|
-
|
|
156
|
+
log.debug "[goal_management] decay_priorities decayed=#{result[:decayed]}"
|
|
143
157
|
result.merge(success: true)
|
|
144
158
|
rescue StandardError => e
|
|
145
|
-
|
|
159
|
+
log.error "[goal_management] decay_priorities error: #{e.message}"
|
|
146
160
|
{ success: false, error: e.message }
|
|
147
161
|
end
|
|
148
162
|
|
|
149
163
|
def goal_status(**)
|
|
150
164
|
report = engine.goal_report
|
|
151
|
-
|
|
165
|
+
log.debug "[goal_management] goal_status total=#{report[:total]}"
|
|
152
166
|
{ success: true }.merge(report)
|
|
153
167
|
rescue StandardError => e
|
|
154
|
-
|
|
168
|
+
log.error "[goal_management] goal_status error: #{e.message}"
|
|
169
|
+
{ success: false, error: e.message }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def dispatch_leaf_goals(**)
|
|
173
|
+
dispatcher = Helpers::TaskDispatcher.new
|
|
174
|
+
engine.goals.each_value do |g|
|
|
175
|
+
engine.activate_goal(goal_id: g.id) if g.leaf? && g.status == :proposed
|
|
176
|
+
end
|
|
177
|
+
leaves = engine.active_goals.select(&:leaf?)
|
|
178
|
+
results = leaves.reject(&:task_id).map do |goal|
|
|
179
|
+
dispatch = dispatcher.dispatch_goal(goal: goal.to_h)
|
|
180
|
+
if dispatch[:dispatched] && dispatch[:task_id]
|
|
181
|
+
engine.assign_task_to_goal(goal_id: goal.id,
|
|
182
|
+
task_id: dispatch[:task_id],
|
|
183
|
+
runner_mapping: dispatch[:runner_mapping])
|
|
184
|
+
end
|
|
185
|
+
{ goal_id: goal.id, dispatch: dispatch }
|
|
186
|
+
end
|
|
187
|
+
dispatched_count = results.count { |r| r[:dispatch][:dispatched] }
|
|
188
|
+
log.debug "[goal_management] dispatch_leaf_goals dispatched=#{dispatched_count} total=#{results.size}"
|
|
189
|
+
{ success: true, dispatched: dispatched_count, total: results.size, results: results }
|
|
190
|
+
rescue StandardError => e
|
|
191
|
+
log.error "[goal_management] dispatch_leaf_goals error: #{e.message}"
|
|
192
|
+
{ success: false, error: e.message }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def update_goal_from_task(task_id:, status:, result: nil, **)
|
|
196
|
+
engine.update_from_task_event(task_id: task_id, status: status, result: result)
|
|
197
|
+
rescue StandardError => e
|
|
198
|
+
log.error "[goal_management] update_goal_from_task error: #{e.message}"
|
|
199
|
+
{ success: false, error: e.message }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def start_feedback_listener(**)
|
|
203
|
+
feedback_listener.start_listening
|
|
204
|
+
{ success: true, listening: feedback_listener.listening? }
|
|
205
|
+
rescue StandardError => e
|
|
206
|
+
log.error "[goal_management] start_feedback_listener error: #{e.message}"
|
|
155
207
|
{ success: false, error: e.message }
|
|
156
208
|
end
|
|
157
209
|
|
|
@@ -160,6 +212,14 @@ module Legion
|
|
|
160
212
|
def engine
|
|
161
213
|
@engine ||= Helpers::GoalEngine.new
|
|
162
214
|
end
|
|
215
|
+
|
|
216
|
+
def feedback_listener
|
|
217
|
+
@feedback_listener ||= Helpers::FeedbackListener.new(engine: engine)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def log
|
|
221
|
+
Legion::Logging
|
|
222
|
+
end
|
|
163
223
|
end
|
|
164
224
|
end
|
|
165
225
|
end
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
require 'legion/extensions/agentic/executive/goal_management/version'
|
|
4
4
|
require 'legion/extensions/agentic/executive/goal_management/helpers/constants'
|
|
5
5
|
require 'legion/extensions/agentic/executive/goal_management/helpers/goal'
|
|
6
|
+
require 'legion/extensions/agentic/executive/goal_management/helpers/goal_persistence'
|
|
6
7
|
require 'legion/extensions/agentic/executive/goal_management/helpers/goal_engine'
|
|
8
|
+
require 'legion/extensions/agentic/executive/goal_management/helpers/task_dispatcher'
|
|
9
|
+
require 'legion/extensions/agentic/executive/goal_management/helpers/decomposer'
|
|
7
10
|
require 'legion/extensions/agentic/executive/goal_management/runners/goal_management'
|
|
8
11
|
require 'legion/extensions/agentic/executive/goal_management/client'
|
|
9
12
|
|
|
@@ -4,6 +4,7 @@ require 'legion/extensions/agentic/executive/volition/helpers/constants'
|
|
|
4
4
|
require 'legion/extensions/agentic/executive/volition/helpers/intention'
|
|
5
5
|
require 'legion/extensions/agentic/executive/volition/helpers/intention_stack'
|
|
6
6
|
require 'legion/extensions/agentic/executive/volition/helpers/drive_synthesizer'
|
|
7
|
+
require 'legion/extensions/agentic/executive/volition/helpers/goal_bridge'
|
|
7
8
|
require 'legion/extensions/agentic/executive/volition/runners/volition'
|
|
8
9
|
|
|
9
10
|
module Legion
|
|
@@ -16,8 +17,9 @@ module Legion
|
|
|
16
17
|
|
|
17
18
|
attr_reader :intention_stack
|
|
18
19
|
|
|
19
|
-
def initialize(stack: nil, **)
|
|
20
|
+
def initialize(stack: nil, goal_bridge: nil, **)
|
|
20
21
|
@intention_stack = stack || Helpers::IntentionStack.new
|
|
22
|
+
@goal_bridge = goal_bridge
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/agentic/executive/goal_management/helpers/goal_persistence'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Agentic
|
|
8
|
+
module Executive
|
|
9
|
+
module Volition
|
|
10
|
+
module Helpers
|
|
11
|
+
class GoalBridge
|
|
12
|
+
def initialize(goal_client:, persistence: nil)
|
|
13
|
+
@goal_client = goal_client
|
|
14
|
+
@persistence = persistence || default_persistence
|
|
15
|
+
@bridged_intentions = @persistence&.load_bridge_state || {}
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def bridge_intentions(intentions)
|
|
20
|
+
result = { bridged: 0, skipped: 0, duplicates: 0, goal_ids: [], errors: [] }
|
|
21
|
+
return result if intentions.nil? || intentions.empty?
|
|
22
|
+
|
|
23
|
+
intentions.each do |intention|
|
|
24
|
+
outcome = bridge_one(intention)
|
|
25
|
+
case outcome[:status]
|
|
26
|
+
when :bridged
|
|
27
|
+
result[:bridged] += 1
|
|
28
|
+
result[:goal_ids] << outcome[:goal_id]
|
|
29
|
+
when :skipped then result[:skipped] += 1
|
|
30
|
+
when :duplicate then result[:duplicates] += 1
|
|
31
|
+
when :error then result[:errors] << outcome[:error]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
persist_bridge_state
|
|
36
|
+
log.info "[goal_bridge] bridged #{result[:bridged]} intentions to goals"
|
|
37
|
+
result
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def log
|
|
43
|
+
Legion::Logging
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def default_persistence
|
|
47
|
+
GoalManagement::Helpers::GoalPersistence.new
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
log.error "GoalBridge: #{e.message}"
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def persist_bridge_state
|
|
54
|
+
@persistence&.save_bridge_state(@bridged_intentions)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def bridge_one(intention)
|
|
58
|
+
return { status: :skipped } unless intention[:state] == :active
|
|
59
|
+
|
|
60
|
+
intent_key = "#{intention[:drive]}:#{intention[:domain]}:#{intention[:goal]}"
|
|
61
|
+
@mutex.synchronize do
|
|
62
|
+
return { status: :duplicate } if @bridged_intentions.key?(intent_key)
|
|
63
|
+
|
|
64
|
+
goal_result = @goal_client.add_goal(
|
|
65
|
+
content: intention[:goal],
|
|
66
|
+
parent_id: nil,
|
|
67
|
+
domain: intention[:domain],
|
|
68
|
+
priority: intention[:salience],
|
|
69
|
+
deadline: nil
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if goal_result[:success]
|
|
73
|
+
@bridged_intentions[intent_key] = goal_result[:goal][:id]
|
|
74
|
+
{ status: :bridged, goal_id: goal_result[:goal][:id] }
|
|
75
|
+
else
|
|
76
|
+
{ status: :error, error: goal_result[:error] }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -17,10 +17,10 @@ module Legion
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
new_intentions = Helpers::DriveSynthesizer.generate_intentions(drives, cognitive_state: cognitive_state)
|
|
20
|
-
|
|
20
|
+
pushed_intentions = []
|
|
21
21
|
new_intentions.each do |intention|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
outcome = intention_stack.push(intention)
|
|
23
|
+
pushed_intentions << intention if outcome == :pushed
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
expired = intention_stack.decay_all
|
|
@@ -28,18 +28,25 @@ module Legion
|
|
|
28
28
|
current = intention_stack.top
|
|
29
29
|
proactive = evaluate_proactive_outreach(tick_results, bond_state)
|
|
30
30
|
|
|
31
|
-
log.debug "[volition] drives=#{format_drives(drives)} pushed=#{
|
|
31
|
+
log.debug "[volition] drives=#{format_drives(drives)} pushed=#{pushed_intentions.size} expired=#{expired} " \
|
|
32
32
|
"active=#{intention_stack.active_count} top=#{current&.dig(:goal)}"
|
|
33
33
|
|
|
34
|
-
{
|
|
34
|
+
result = {
|
|
35
35
|
drives: drives,
|
|
36
36
|
dominant_drive: dominant,
|
|
37
|
-
new_intentions:
|
|
37
|
+
new_intentions: pushed_intentions.map { |i| format_intention(i) },
|
|
38
38
|
expired: expired,
|
|
39
39
|
active_intentions: intention_stack.active_count,
|
|
40
40
|
current_intention: format_intention(current),
|
|
41
41
|
proactive_outreach: proactive
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
if defined?(@goal_bridge) && @goal_bridge
|
|
45
|
+
bridge_result = @goal_bridge.bridge_intentions(pushed_intentions)
|
|
46
|
+
result[:bridge_result] = bridge_result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
result
|
|
43
50
|
end
|
|
44
51
|
|
|
45
52
|
def current_intention(**)
|
|
@@ -5,6 +5,7 @@ require 'legion/extensions/agentic/executive/volition/helpers/constants'
|
|
|
5
5
|
require 'legion/extensions/agentic/executive/volition/helpers/intention'
|
|
6
6
|
require 'legion/extensions/agentic/executive/volition/helpers/intention_stack'
|
|
7
7
|
require 'legion/extensions/agentic/executive/volition/helpers/drive_synthesizer'
|
|
8
|
+
require 'legion/extensions/agentic/executive/volition/helpers/goal_bridge'
|
|
8
9
|
require 'legion/extensions/agentic/executive/volition/runners/volition'
|
|
9
10
|
require 'legion/extensions/agentic/executive/volition/client'
|
|
10
11
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'Autonomous Goal Pipeline Integration' do
|
|
6
|
+
let(:goal_client) { Legion::Extensions::Agentic::Executive::GoalManagement::Client.new }
|
|
7
|
+
let(:bridge) do
|
|
8
|
+
Legion::Extensions::Agentic::Executive::Volition::Helpers::GoalBridge.new(
|
|
9
|
+
goal_client: goal_client
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
let(:volition_client) do
|
|
13
|
+
Legion::Extensions::Agentic::Executive::Volition::Client.new(
|
|
14
|
+
stack: Legion::Extensions::Agentic::Executive::Volition::Helpers::IntentionStack.new,
|
|
15
|
+
goal_bridge: bridge
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
let(:tick_results) do
|
|
20
|
+
{ working_memory_integration: { curiosity: { intensity: 0.9, count: 5 } } }
|
|
21
|
+
end
|
|
22
|
+
let(:cognitive_state) do
|
|
23
|
+
{ health: 0.6, pending_goals: 0, arousal: 0.5, gut: { signal: :explore },
|
|
24
|
+
confidence: 0.4, pending_questions: 3, peer_interactions: 0,
|
|
25
|
+
trust: { avg_composite: 0.5 } }
|
|
26
|
+
end
|
|
27
|
+
let(:bond_state) { {} }
|
|
28
|
+
|
|
29
|
+
it 'flows from intention through goal to dispatch-ready state' do
|
|
30
|
+
# Step 1: Form intentions (triggers goal bridge)
|
|
31
|
+
volition_result = volition_client.form_intentions(
|
|
32
|
+
tick_results: tick_results, cognitive_state: cognitive_state, bond_state: bond_state
|
|
33
|
+
)
|
|
34
|
+
expect(volition_result[:bridge_result][:bridged]).to be >= 1
|
|
35
|
+
|
|
36
|
+
# Step 2: Verify goals were created
|
|
37
|
+
goal_ids = volition_result[:bridge_result][:goal_ids]
|
|
38
|
+
expect(goal_ids).not_to be_empty
|
|
39
|
+
|
|
40
|
+
# Step 3: Auto-decompose the first goal
|
|
41
|
+
first_goal_id = goal_ids.first
|
|
42
|
+
goal_client.activate_goal(goal_id: first_goal_id)
|
|
43
|
+
decomp = goal_client.auto_decompose_goal(goal_id: first_goal_id, strategy: :heuristic)
|
|
44
|
+
expect(decomp[:success]).to be true
|
|
45
|
+
|
|
46
|
+
# Step 4: Verify goal tree has children
|
|
47
|
+
tree = goal_client.get_goal_tree(goal_id: first_goal_id)
|
|
48
|
+
expect(tree[:tree]).not_to be_nil
|
|
49
|
+
|
|
50
|
+
# Step 5: Check goal status shows goals exist
|
|
51
|
+
status = goal_client.goal_status
|
|
52
|
+
expect(status[:total]).to be >= 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'updates goal on simulated task completion' do
|
|
56
|
+
# Create and activate a leaf goal with task assignment
|
|
57
|
+
add_result = goal_client.add_goal(
|
|
58
|
+
content: 'simple leaf goal', domain: :general, priority: 0.5,
|
|
59
|
+
parent_id: nil, deadline: nil
|
|
60
|
+
)
|
|
61
|
+
goal_id = add_result[:goal][:id]
|
|
62
|
+
goal_client.activate_goal(goal_id: goal_id)
|
|
63
|
+
|
|
64
|
+
engine = goal_client.send(:engine)
|
|
65
|
+
goal = engine.goals[goal_id]
|
|
66
|
+
goal.assign_task!(task_id: 'task-sim-001', runner_mapping: { runner_class: 'Test', function: :test })
|
|
67
|
+
|
|
68
|
+
# Simulate task completion
|
|
69
|
+
update = engine.update_from_task_event(task_id: 'task-sim-001', status: 'task.completed')
|
|
70
|
+
expect(update[:found]).to be true
|
|
71
|
+
expect(update[:new_status]).to eq(:completed)
|
|
72
|
+
expect(goal.progress).to eq(1.0)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'blocks goal on simulated task failure' do
|
|
76
|
+
add_result = goal_client.add_goal(
|
|
77
|
+
content: 'failing goal', domain: :general, priority: 0.5,
|
|
78
|
+
parent_id: nil, deadline: nil
|
|
79
|
+
)
|
|
80
|
+
goal_id = add_result[:goal][:id]
|
|
81
|
+
goal_client.activate_goal(goal_id: goal_id)
|
|
82
|
+
|
|
83
|
+
engine = goal_client.send(:engine)
|
|
84
|
+
goal = engine.goals[goal_id]
|
|
85
|
+
goal.assign_task!(task_id: 'task-fail-001', runner_mapping: { runner_class: 'Test', function: :test })
|
|
86
|
+
|
|
87
|
+
update = engine.update_from_task_event(task_id: 'task-fail-001', status: 'task.exception')
|
|
88
|
+
expect(update[:found]).to be true
|
|
89
|
+
expect(update[:new_status]).to eq(:blocked)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'reprioritizes goals on urgency signal' do
|
|
93
|
+
goal_client.add_goal(content: 'safety issue', domain: :safety, priority: 0.4, parent_id: nil, deadline: nil)
|
|
94
|
+
goal_client.add_goal(content: 'general task', domain: :general, priority: 0.4, parent_id: nil, deadline: nil)
|
|
95
|
+
|
|
96
|
+
# Activate both
|
|
97
|
+
engine = goal_client.send(:engine)
|
|
98
|
+
engine.goals.each_value(&:activate!)
|
|
99
|
+
|
|
100
|
+
result = engine.reprioritize!(signal: { domain: :safety, urgency: :critical })
|
|
101
|
+
expect(result[:adjusted]).to eq(1)
|
|
102
|
+
|
|
103
|
+
safety = engine.goals.values.find { |g| g.domain == :safety }
|
|
104
|
+
general = engine.goals.values.find { |g| g.domain == :general }
|
|
105
|
+
expect(safety.priority).to be > general.priority
|
|
106
|
+
end
|
|
107
|
+
end
|