lex-agentic-executive 0.1.11 → 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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +1 -1
  4. data/lib/legion/extensions/agentic/executive/goal_management/client.rb +23 -0
  5. data/lib/legion/extensions/agentic/executive/goal_management/helpers/decomposer.rb +107 -0
  6. data/lib/legion/extensions/agentic/executive/goal_management/helpers/feedback_listener.rb +63 -0
  7. data/lib/legion/extensions/agentic/executive/goal_management/helpers/goal.rb +41 -12
  8. data/lib/legion/extensions/agentic/executive/goal_management/helpers/goal_engine.rb +99 -10
  9. data/lib/legion/extensions/agentic/executive/goal_management/helpers/goal_persistence.rb +145 -0
  10. data/lib/legion/extensions/agentic/executive/goal_management/helpers/task_dispatcher.rb +109 -0
  11. data/lib/legion/extensions/agentic/executive/goal_management/runners/goal_management.rb +96 -36
  12. data/lib/legion/extensions/agentic/executive/goal_management.rb +3 -0
  13. data/lib/legion/extensions/agentic/executive/version.rb +1 -1
  14. data/lib/legion/extensions/agentic/executive/volition/client.rb +3 -1
  15. data/lib/legion/extensions/agentic/executive/volition/helpers/goal_bridge.rb +86 -0
  16. data/lib/legion/extensions/agentic/executive/volition/runners/volition.rb +13 -6
  17. data/lib/legion/extensions/agentic/executive/volition.rb +1 -0
  18. data/lib/legion/extensions/agentic/executive.rb +12 -0
  19. data/spec/integration/autonomous_goal_pipeline_spec.rb +107 -0
  20. data/spec/legion/extensions/agentic/executive/goal_management/helpers/decomposer_spec.rb +152 -0
  21. data/spec/legion/extensions/agentic/executive/goal_management/helpers/feedback_listener_spec.rb +104 -0
  22. data/spec/legion/extensions/agentic/executive/goal_management/helpers/goal_engine_spec.rb +32 -0
  23. data/spec/legion/extensions/agentic/executive/goal_management/helpers/goal_persistence_spec.rb +119 -0
  24. data/spec/legion/extensions/agentic/executive/goal_management/helpers/task_dispatcher_spec.rb +176 -0
  25. data/spec/legion/extensions/agentic/executive/volition/helpers/goal_bridge_spec.rb +58 -0
  26. data/spec/legion/extensions/agentic/executive/volition/runners/volition_spec.rb +26 -0
  27. metadata +12 -1
@@ -0,0 +1,145 @@
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 GoalPersistence
10
+ include Legion::Cache::Helper if defined?(Legion::Cache::Helper)
11
+ include Legion::JSON::Helper if defined?(Legion::JSON::Helper)
12
+
13
+ GOAL_TTL = 86_400
14
+ INDEX_KEY_SUFFIX = ':index'
15
+ BRIDGE_KEY_SUFFIX = ':bridge'
16
+
17
+ def initialize(namespace: 'legion_goals')
18
+ @namespace = namespace
19
+ end
20
+
21
+ def save_goal(goal_hash)
22
+ return false unless persistence_available?
23
+
24
+ cache_set(goal_key(goal_hash[:id]), json_dump(goal_hash), ttl: GOAL_TTL)
25
+ update_index(goal_hash[:id], :add)
26
+ true
27
+ rescue StandardError => e
28
+ log.error "[goal_persistence] save_goal failed: #{e.message}"
29
+ false
30
+ end
31
+
32
+ def load_goal(id)
33
+ return nil unless persistence_available?
34
+
35
+ raw = cache_get(goal_key(id))
36
+ return nil unless raw
37
+
38
+ json_load(raw)
39
+ rescue StandardError => e
40
+ log.error "[goal_persistence] load_goal failed: #{e.message}"
41
+ nil
42
+ end
43
+
44
+ def delete_goal(id)
45
+ return false unless persistence_available?
46
+
47
+ cache_delete(goal_key(id))
48
+ update_index(id, :remove)
49
+ true
50
+ rescue StandardError => e
51
+ log.error "[goal_persistence] delete_goal failed: #{e.message}"
52
+ false
53
+ end
54
+
55
+ def save_all(goals_hash)
56
+ return false unless persistence_available?
57
+
58
+ goals_hash.each_value { |g| save_goal(g.is_a?(Hash) ? g : g.to_h) }
59
+ true
60
+ rescue StandardError => e
61
+ log.error "[goal_persistence] save_all failed: #{e.message}"
62
+ false
63
+ end
64
+
65
+ def load_all
66
+ return {} unless persistence_available?
67
+
68
+ ids = load_index
69
+ return {} if ids.empty?
70
+
71
+ goals = ids.each_with_object({}) do |id, result|
72
+ goal = load_goal(id)
73
+ result[id] = goal if goal
74
+ end
75
+ log.info "[goal_persistence] rehydrated #{goals.size} goals from cache"
76
+ goals
77
+ rescue StandardError => e
78
+ log.error "[goal_persistence] load_all failed: #{e.message}"
79
+ {}
80
+ end
81
+
82
+ def save_bridge_state(bridge_hash)
83
+ return false unless persistence_available?
84
+
85
+ cache_set(bridge_key, json_dump(bridge_hash), ttl: GOAL_TTL)
86
+ true
87
+ rescue StandardError => e
88
+ log.error "[goal_persistence] save_bridge_state failed: #{e.message}"
89
+ false
90
+ end
91
+
92
+ def load_bridge_state
93
+ return {} unless persistence_available?
94
+
95
+ raw = cache_get(bridge_key)
96
+ return {} unless raw
97
+
98
+ json_load(raw)
99
+ rescue StandardError => e
100
+ log.error "[goal_persistence] load_bridge_state failed: #{e.message}"
101
+ {}
102
+ end
103
+
104
+ private
105
+
106
+ def log
107
+ Legion::Logging
108
+ end
109
+
110
+ def persistence_available?
111
+ respond_to?(:cache_connected?) && cache_connected?
112
+ rescue StandardError => e
113
+ log.debug "[goal_persistence] cache not available: #{e.message}"
114
+ false
115
+ end
116
+
117
+ def goal_key(id) = "#{@namespace}:goal:#{id}"
118
+ def index_key = "#{@namespace}#{INDEX_KEY_SUFFIX}"
119
+ def bridge_key = "#{@namespace}#{BRIDGE_KEY_SUFFIX}"
120
+
121
+ def update_index(id, operation)
122
+ ids = load_index
123
+ case operation
124
+ when :add then ids << id unless ids.include?(id)
125
+ when :remove then ids.delete(id)
126
+ end
127
+ cache_set(index_key, json_dump(ids), ttl: GOAL_TTL)
128
+ end
129
+
130
+ def load_index
131
+ raw = cache_get(index_key)
132
+ return [] unless raw
133
+
134
+ json_load(raw)
135
+ rescue StandardError => e
136
+ log.error "[goal_persistence] load_index failed: #{e.message}"
137
+ []
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -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
- Legion::Logging.debug "[goal_management] runner add_goal domain=#{domain}"
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
- Legion::Logging.error "[goal_management] add_goal error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] runner decompose_goal parent=#{goal_id}"
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
- Legion::Logging.error "[goal_management] decompose_goal error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] runner activate_goal id=#{goal_id}"
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
- Legion::Logging.error "[goal_management] activate_goal error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] runner complete_goal id=#{goal_id}"
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
- Legion::Logging.error "[goal_management] complete_goal error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] runner abandon_goal id=#{goal_id}"
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
- Legion::Logging.error "[goal_management] abandon_goal error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] runner block_goal id=#{goal_id}"
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
- Legion::Logging.error "[goal_management] block_goal error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] runner unblock_goal id=#{goal_id}"
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
- Legion::Logging.error "[goal_management] unblock_goal error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] runner advance_goal_progress id=#{goal_id} amount=#{amount}"
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
- Legion::Logging.error "[goal_management] advance_goal_progress error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] runner detect_goal_conflicts id=#{goal_id}"
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
- Legion::Logging.error "[goal_management] detect_goal_conflicts error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] list_active_goals count=#{goals.size}"
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
- Legion::Logging.error "[goal_management] list_active_goals error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] list_blocked_goals count=#{goals.size}"
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
- Legion::Logging.error "[goal_management] list_blocked_goals error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] list_overdue_goals count=#{goals.size}"
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
- Legion::Logging.error "[goal_management] list_overdue_goals error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] list_completed_goals count=#{goals.size}"
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
- Legion::Logging.error "[goal_management] list_completed_goals error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] runner get_goal_tree id=#{goal_id}"
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
- Legion::Logging.error "[goal_management] get_goal_tree error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] highest_priority_goals limit=#{limit} count=#{goals.size}"
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
- Legion::Logging.error "[goal_management] highest_priority_goals error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] decay_priorities decayed=#{result[:decayed]}"
156
+ log.debug "[goal_management] decay_priorities decayed=#{result[:decayed]}"
143
157
  result.merge(success: true)
144
158
  rescue StandardError => e
145
- Legion::Logging.error "[goal_management] decay_priorities error: #{e.message}"
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
- Legion::Logging.debug "[goal_management] goal_status total=#{report[:total]}"
165
+ log.debug "[goal_management] goal_status total=#{report[:total]}"
152
166
  { success: true }.merge(report)
153
167
  rescue StandardError => e
154
- Legion::Logging.error "[goal_management] goal_status error: #{e.message}"
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,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Executive
7
- VERSION = '0.1.11'
7
+ VERSION = '0.2.0'
8
8
  end
9
9
  end
10
10
  end
@@ -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