lex-planning 0.1.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 +7 -0
- data/lib/legion/extensions/planning/client.rb +23 -0
- data/lib/legion/extensions/planning/helpers/constants.rb +22 -0
- data/lib/legion/extensions/planning/helpers/plan.rb +106 -0
- data/lib/legion/extensions/planning/helpers/plan_step.rb +78 -0
- data/lib/legion/extensions/planning/helpers/plan_store.rb +157 -0
- data/lib/legion/extensions/planning/runners/planning.rb +119 -0
- data/lib/legion/extensions/planning/version.rb +9 -0
- data/lib/legion/extensions/planning.rb +17 -0
- metadata +65 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5d3a843a1b4b2613a45996d9c12206f3a0f9387fb55a53688a09ccc254503c09
|
|
4
|
+
data.tar.gz: bc73bac9c97162ce4e471daa009242797d64b3a939de353f8a1395c96e04904f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 427f9cbf65397f9520b22f883bd0e406e9a26b4ecc483b11ae5ccfdb3205151bef3a5611da724b0fdb1ef204861d7b78defd4f5e1c924c1e7ccce49daa9c9e40
|
|
7
|
+
data.tar.gz: c31c83212f3d078206d6fcd714571c40b7313b4aecf534acc902677e4d566207a1f6fd238defdf3f9a103376891ea9d3fd6e4a5960acca308b270e5ae7d85d78
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/planning/helpers/constants'
|
|
4
|
+
require 'legion/extensions/planning/helpers/plan_step'
|
|
5
|
+
require 'legion/extensions/planning/helpers/plan'
|
|
6
|
+
require 'legion/extensions/planning/helpers/plan_store'
|
|
7
|
+
require 'legion/extensions/planning/runners/planning'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module Planning
|
|
12
|
+
class Client
|
|
13
|
+
include Runners::Planning
|
|
14
|
+
|
|
15
|
+
attr_reader :plan_store
|
|
16
|
+
|
|
17
|
+
def initialize(plan_store: nil, **)
|
|
18
|
+
@plan_store = plan_store || Helpers::PlanStore.new
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Planning
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
PLAN_STATUSES = %i[forming active executing completed failed abandoned].freeze
|
|
9
|
+
STEP_STATUSES = %i[pending active completed failed skipped blocked].freeze
|
|
10
|
+
PRIORITIES = { critical: 1.0, high: 0.75, medium: 0.5, low: 0.25 }.freeze
|
|
11
|
+
MAX_PLANS = 50
|
|
12
|
+
MAX_STEPS_PER_PLAN = 100
|
|
13
|
+
MAX_CONTINGENCIES = 20
|
|
14
|
+
REPLAN_LIMIT = 3
|
|
15
|
+
STALE_PLAN_THRESHOLD = 3600
|
|
16
|
+
COMPLETION_THRESHOLD = 0.95
|
|
17
|
+
PLANNING_HORIZON = 10
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Planning
|
|
8
|
+
module Helpers
|
|
9
|
+
class Plan
|
|
10
|
+
attr_reader :id, :goal, :description, :priority, :steps, :contingencies,
|
|
11
|
+
:parent_plan_id, :created_at, :replan_count
|
|
12
|
+
attr_accessor :status, :updated_at
|
|
13
|
+
|
|
14
|
+
def initialize(goal:, steps: [], priority: :medium, contingencies: {}, parent_plan_id: nil, description: nil, **)
|
|
15
|
+
@id = SecureRandom.uuid
|
|
16
|
+
@goal = goal
|
|
17
|
+
@description = description
|
|
18
|
+
@priority = priority
|
|
19
|
+
@status = :forming
|
|
20
|
+
@steps = steps.dup
|
|
21
|
+
@contingencies = contingencies.dup
|
|
22
|
+
@parent_plan_id = parent_plan_id
|
|
23
|
+
@created_at = Time.now.utc
|
|
24
|
+
@updated_at = Time.now.utc
|
|
25
|
+
@replan_count = 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def progress
|
|
29
|
+
return 0.0 if @steps.empty?
|
|
30
|
+
|
|
31
|
+
done = @steps.count { |s| %i[completed skipped].include?(s.status) }
|
|
32
|
+
done.to_f / @steps.size
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def active_step
|
|
36
|
+
@steps.find { |s| s.status == :active }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def completed_step_ids
|
|
40
|
+
@steps.select { |s| %i[completed skipped].include?(s.status) }.map(&:id)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def advance!(step_id, result: nil)
|
|
44
|
+
step = @steps.find { |s| s.id == step_id }
|
|
45
|
+
return nil unless step
|
|
46
|
+
|
|
47
|
+
step.complete!(result: result)
|
|
48
|
+
@updated_at = Time.now.utc
|
|
49
|
+
@status = :completed if progress >= Constants::COMPLETION_THRESHOLD
|
|
50
|
+
step
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def fail_step!(step_id, reason: nil)
|
|
54
|
+
step = @steps.find { |s| s.id == step_id }
|
|
55
|
+
return nil unless step
|
|
56
|
+
|
|
57
|
+
step.fail!(reason: reason)
|
|
58
|
+
@updated_at = Time.now.utc
|
|
59
|
+
step
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def complete?
|
|
63
|
+
@status == :completed || progress >= Constants::COMPLETION_THRESHOLD
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def failed?
|
|
67
|
+
@status == :failed
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def stale?
|
|
71
|
+
(Time.now.utc - @updated_at) > Constants::STALE_PLAN_THRESHOLD
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def increment_replan!
|
|
75
|
+
@replan_count += 1
|
|
76
|
+
@updated_at = Time.now.utc
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def replace_remaining_steps!(new_steps)
|
|
80
|
+
pending = @steps.reject { |s| %i[completed skipped failed].include?(s.status) }
|
|
81
|
+
pending.each { |s| @steps.delete(s) }
|
|
82
|
+
new_steps.each { |s| @steps << s }
|
|
83
|
+
@updated_at = Time.now.utc
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def to_h
|
|
87
|
+
{
|
|
88
|
+
id: @id,
|
|
89
|
+
goal: @goal,
|
|
90
|
+
description: @description,
|
|
91
|
+
priority: @priority,
|
|
92
|
+
status: @status,
|
|
93
|
+
progress: progress.round(4),
|
|
94
|
+
steps: @steps.map(&:to_h),
|
|
95
|
+
contingencies: @contingencies,
|
|
96
|
+
parent_plan_id: @parent_plan_id,
|
|
97
|
+
replan_count: @replan_count,
|
|
98
|
+
created_at: @created_at,
|
|
99
|
+
updated_at: @updated_at
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Planning
|
|
8
|
+
module Helpers
|
|
9
|
+
class PlanStep
|
|
10
|
+
attr_reader :id, :action, :description, :depends_on, :estimated_effort,
|
|
11
|
+
:started_at, :completed_at, :result
|
|
12
|
+
attr_accessor :status, :actual_effort
|
|
13
|
+
|
|
14
|
+
def initialize(action:, description: nil, depends_on: [], estimated_effort: 1, **)
|
|
15
|
+
@id = SecureRandom.uuid
|
|
16
|
+
@action = action
|
|
17
|
+
@description = description
|
|
18
|
+
@status = :pending
|
|
19
|
+
@depends_on = Array(depends_on).dup
|
|
20
|
+
@estimated_effort = estimated_effort
|
|
21
|
+
@actual_effort = nil
|
|
22
|
+
@result = nil
|
|
23
|
+
@started_at = nil
|
|
24
|
+
@completed_at = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def ready?(completed_step_ids)
|
|
28
|
+
return false if %i[completed failed skipped].include?(@status)
|
|
29
|
+
|
|
30
|
+
@depends_on.all? { |dep_id| completed_step_ids.include?(dep_id) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def duration
|
|
34
|
+
return nil unless @started_at && @completed_at
|
|
35
|
+
|
|
36
|
+
@completed_at - @started_at
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def blocked?
|
|
40
|
+
@status == :blocked
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def complete!(result: nil)
|
|
44
|
+
@status = :completed
|
|
45
|
+
@result = result
|
|
46
|
+
@completed_at = Time.now.utc
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def fail!(reason: nil)
|
|
50
|
+
@status = :failed
|
|
51
|
+
@result = { reason: reason }
|
|
52
|
+
@completed_at = Time.now.utc
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def start!
|
|
56
|
+
@status = :active
|
|
57
|
+
@started_at = Time.now.utc
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_h
|
|
61
|
+
{
|
|
62
|
+
id: @id,
|
|
63
|
+
action: @action,
|
|
64
|
+
description: @description,
|
|
65
|
+
status: @status,
|
|
66
|
+
depends_on: @depends_on,
|
|
67
|
+
estimated_effort: @estimated_effort,
|
|
68
|
+
actual_effort: @actual_effort,
|
|
69
|
+
result: @result,
|
|
70
|
+
started_at: @started_at,
|
|
71
|
+
completed_at: @completed_at
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Planning
|
|
6
|
+
module Helpers
|
|
7
|
+
class PlanStore
|
|
8
|
+
attr_reader :plans, :plan_history
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@plans = {}
|
|
12
|
+
@plan_history = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create_plan(goal:, steps: [], priority: :medium, contingencies: {}, parent_plan_id: nil, **)
|
|
16
|
+
step_objects = steps.map do |s|
|
|
17
|
+
s.is_a?(PlanStep) ? s : PlanStep.new(**s)
|
|
18
|
+
end
|
|
19
|
+
plan = Plan.new(
|
|
20
|
+
goal: goal,
|
|
21
|
+
steps: step_objects,
|
|
22
|
+
priority: priority,
|
|
23
|
+
contingencies: contingencies,
|
|
24
|
+
parent_plan_id: parent_plan_id
|
|
25
|
+
)
|
|
26
|
+
plan.status = :active
|
|
27
|
+
@plans[plan.id] = plan
|
|
28
|
+
trim_history
|
|
29
|
+
plan
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def find_plan(plan_id)
|
|
33
|
+
@plans[plan_id]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def advance_step(plan_id:, step_id:, result: {})
|
|
37
|
+
plan = @plans[plan_id]
|
|
38
|
+
return { error: 'plan not found' } unless plan
|
|
39
|
+
|
|
40
|
+
step = plan.advance!(step_id, result: result)
|
|
41
|
+
return { error: 'step not found' } unless step
|
|
42
|
+
|
|
43
|
+
if plan.complete?
|
|
44
|
+
plan.status = :completed
|
|
45
|
+
archive_plan(plan_id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
{ success: true, step_id: step_id, plan_progress: plan.progress.round(4), plan_status: plan.status }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fail_step(plan_id:, step_id:, reason: nil)
|
|
52
|
+
plan = @plans[plan_id]
|
|
53
|
+
return { error: 'plan not found' } unless plan
|
|
54
|
+
|
|
55
|
+
step = plan.fail_step!(step_id, reason: reason)
|
|
56
|
+
return { error: 'step not found' } unless step
|
|
57
|
+
|
|
58
|
+
contingency = plan.contingencies[step.action] || plan.contingencies[step_id]
|
|
59
|
+
{ success: true, step_id: step_id, contingency: contingency, plan_status: plan.status }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def replan(plan_id:, new_steps:, reason: nil)
|
|
63
|
+
plan = @plans[plan_id]
|
|
64
|
+
return { error: 'plan not found' } unless plan
|
|
65
|
+
return { error: 'replan limit reached' } if plan.replan_count >= Constants::REPLAN_LIMIT
|
|
66
|
+
|
|
67
|
+
step_objects = new_steps.map do |s|
|
|
68
|
+
s.is_a?(PlanStep) ? s : PlanStep.new(**s)
|
|
69
|
+
end
|
|
70
|
+
plan.increment_replan!
|
|
71
|
+
plan.replace_remaining_steps!(step_objects)
|
|
72
|
+
plan.status = :active
|
|
73
|
+
|
|
74
|
+
{ success: true, plan_id: plan_id, replan_count: plan.replan_count, reason: reason }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def abandon_plan(plan_id:, reason: nil)
|
|
78
|
+
plan = @plans[plan_id]
|
|
79
|
+
return { error: 'plan not found' } unless plan
|
|
80
|
+
|
|
81
|
+
plan.status = :abandoned
|
|
82
|
+
archive_plan(plan_id)
|
|
83
|
+
{ success: true, plan_id: plan_id, reason: reason }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def active_plans
|
|
87
|
+
@plans.values.select { |p| %i[active executing].include?(p.status) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def completed_plans(limit: 10)
|
|
91
|
+
@plan_history.last(limit)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def plan_progress(plan_id)
|
|
95
|
+
plan = @plans[plan_id] || @plan_history.find { |p| p.id == plan_id }
|
|
96
|
+
return { error: 'plan not found' } unless plan
|
|
97
|
+
|
|
98
|
+
ready_ids = plan.completed_step_ids
|
|
99
|
+
{
|
|
100
|
+
plan_id: plan_id,
|
|
101
|
+
goal: plan.goal,
|
|
102
|
+
status: plan.status,
|
|
103
|
+
progress: plan.progress.round(4),
|
|
104
|
+
total_steps: plan.steps.size,
|
|
105
|
+
completed: plan.steps.count { |s| s.status == :completed },
|
|
106
|
+
failed: plan.steps.count { |s| s.status == :failed },
|
|
107
|
+
pending: plan.steps.count { |s| s.status == :pending },
|
|
108
|
+
blocked: plan.steps.count { |s| s.status == :blocked },
|
|
109
|
+
next_ready: plan.steps.select { |s| s.status == :pending && s.ready?(ready_ids) }.map(&:id),
|
|
110
|
+
replan_count: plan.replan_count,
|
|
111
|
+
stale: plan.stale?
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def plans_by_priority
|
|
116
|
+
priority_value = ->(p) { Constants::PRIORITIES.fetch(p.priority, 0.5) }
|
|
117
|
+
@plans.values.sort_by { |p| -priority_value.call(p) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def stats
|
|
121
|
+
all = @plans.values
|
|
122
|
+
total = all.size + @plan_history.size
|
|
123
|
+
by_status = Constants::PLAN_STATUSES.to_h { |s| [s, all.count { |p| p.status == s }] }
|
|
124
|
+
replanned = all.count { |p| p.replan_count.positive? }
|
|
125
|
+
avg_progress = all.empty? ? 0.0 : (all.sum(&:progress) / all.size).round(4)
|
|
126
|
+
replan_rate = total.zero? ? 0.0 : (replanned.to_f / [all.size, 1].max).round(4)
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
active_plan_count: all.size,
|
|
130
|
+
archived_plan_count: @plan_history.size,
|
|
131
|
+
by_status: by_status,
|
|
132
|
+
avg_progress: avg_progress,
|
|
133
|
+
replan_rate: replan_rate
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def trim_history
|
|
138
|
+
return unless @plans.size > Constants::MAX_PLANS
|
|
139
|
+
|
|
140
|
+
oldest_id = @plans.keys.min_by { |k| @plans[k].created_at }
|
|
141
|
+
archive_plan(oldest_id)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def archive_plan(plan_id)
|
|
147
|
+
plan = @plans.delete(plan_id)
|
|
148
|
+
return unless plan
|
|
149
|
+
|
|
150
|
+
@plan_history << plan
|
|
151
|
+
@plan_history.shift while @plan_history.size > Constants::MAX_PLANS
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Planning
|
|
6
|
+
module Runners
|
|
7
|
+
module Planning
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def update_planning(tick_results: {}, **)
|
|
12
|
+
check_stale_plans
|
|
13
|
+
advance_ready_steps(tick_results)
|
|
14
|
+
|
|
15
|
+
active = plan_store.active_plans
|
|
16
|
+
Legion::Logging.debug "[planning] active_plans=#{active.size} stats=#{plan_store.stats[:by_status]}"
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
active_plans: active.size,
|
|
20
|
+
total_steps: active.sum { |p| p.steps.size },
|
|
21
|
+
stale_checked: true
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_plan(goal:, steps: [], priority: :medium, contingencies: {}, parent_plan_id: nil, **)
|
|
26
|
+
steps_data = steps.map { |s| s.is_a?(Hash) ? s : s.to_h }
|
|
27
|
+
plan = plan_store.create_plan(
|
|
28
|
+
goal: goal,
|
|
29
|
+
steps: steps_data,
|
|
30
|
+
priority: priority,
|
|
31
|
+
contingencies: contingencies,
|
|
32
|
+
parent_plan_id: parent_plan_id
|
|
33
|
+
)
|
|
34
|
+
Legion::Logging.info "[planning] created plan=#{plan.id} goal=#{goal} steps=#{plan.steps.size}"
|
|
35
|
+
{ success: true, plan_id: plan.id, goal: goal, steps: plan.steps.size }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def advance_plan(plan_id:, step_id:, result: {}, **)
|
|
39
|
+
outcome = plan_store.advance_step(plan_id: plan_id, step_id: step_id, result: result)
|
|
40
|
+
Legion::Logging.debug "[planning] advance plan=#{plan_id} step=#{step_id} outcome=#{outcome[:plan_status]}"
|
|
41
|
+
outcome
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def fail_plan_step(plan_id:, step_id:, reason: nil, **)
|
|
45
|
+
outcome = plan_store.fail_step(plan_id: plan_id, step_id: step_id, reason: reason)
|
|
46
|
+
Legion::Logging.warn "[planning] step failed plan=#{plan_id} step=#{step_id} reason=#{reason}"
|
|
47
|
+
outcome
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def replan(plan_id:, new_steps: [], reason: nil, **)
|
|
51
|
+
steps_data = new_steps.map { |s| s.is_a?(Hash) ? s : s.to_h }
|
|
52
|
+
outcome = plan_store.replan(plan_id: plan_id, new_steps: steps_data, reason: reason)
|
|
53
|
+
if outcome[:success]
|
|
54
|
+
Legion::Logging.info "[planning] replan plan=#{plan_id} count=#{outcome[:replan_count]} reason=#{reason}"
|
|
55
|
+
else
|
|
56
|
+
Legion::Logging.warn "[planning] replan rejected plan=#{plan_id} reason=#{outcome[:error]}"
|
|
57
|
+
end
|
|
58
|
+
outcome
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def abandon_plan(plan_id:, reason: nil, **)
|
|
62
|
+
outcome = plan_store.abandon_plan(plan_id: plan_id, reason: reason)
|
|
63
|
+
Legion::Logging.info "[planning] abandoned plan=#{plan_id} reason=#{reason}"
|
|
64
|
+
outcome
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def plan_status(plan_id:, **)
|
|
68
|
+
outcome = plan_store.plan_progress(plan_id)
|
|
69
|
+
Legion::Logging.debug "[planning] status plan=#{plan_id} progress=#{outcome[:progress]}"
|
|
70
|
+
outcome
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def active_plans(**)
|
|
74
|
+
plans = plan_store.active_plans
|
|
75
|
+
Legion::Logging.debug "[planning] active_plans=#{plans.size}"
|
|
76
|
+
{
|
|
77
|
+
plans: plans.map(&:to_h),
|
|
78
|
+
count: plans.size
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def planning_stats(**)
|
|
83
|
+
stats = plan_store.stats
|
|
84
|
+
Legion::Logging.debug "[planning] stats=#{stats}"
|
|
85
|
+
stats
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def plan_store
|
|
91
|
+
@plan_store ||= Helpers::PlanStore.new
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def check_stale_plans
|
|
95
|
+
plan_store.active_plans.each do |plan|
|
|
96
|
+
Legion::Logging.warn "[planning] stale plan detected plan=#{plan.id} goal=#{plan.goal}" if plan.stale?
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def advance_ready_steps(tick_results)
|
|
101
|
+
completed_actions = tick_results.dig(:action_selection, :completed_actions)
|
|
102
|
+
return unless completed_actions.is_a?(Array)
|
|
103
|
+
|
|
104
|
+
plan_store.active_plans.each do |plan|
|
|
105
|
+
ready_ids = plan.completed_step_ids
|
|
106
|
+
plan.steps.each do |step|
|
|
107
|
+
next unless step.status == :pending && step.ready?(ready_ids)
|
|
108
|
+
next unless completed_actions.any? { |a| a[:action] == step.action }
|
|
109
|
+
|
|
110
|
+
result = completed_actions.find { |a| a[:action] == step.action }
|
|
111
|
+
plan_store.advance_step(plan_id: plan.id, step_id: step.id, result: result)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/planning/version'
|
|
4
|
+
require 'legion/extensions/planning/helpers/constants'
|
|
5
|
+
require 'legion/extensions/planning/helpers/plan_step'
|
|
6
|
+
require 'legion/extensions/planning/helpers/plan'
|
|
7
|
+
require 'legion/extensions/planning/helpers/plan_store'
|
|
8
|
+
require 'legion/extensions/planning/runners/planning'
|
|
9
|
+
require 'legion/extensions/planning/client'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module Planning
|
|
14
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-planning
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Matthew Iverson
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: legion-gaia
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: 'Models the prefrontal cortex planning function: plan trees, progress
|
|
27
|
+
tracking, contingencies, re-planning'
|
|
28
|
+
email:
|
|
29
|
+
- matt@legionIO.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- lib/legion/extensions/planning.rb
|
|
35
|
+
- lib/legion/extensions/planning/client.rb
|
|
36
|
+
- lib/legion/extensions/planning/helpers/constants.rb
|
|
37
|
+
- lib/legion/extensions/planning/helpers/plan.rb
|
|
38
|
+
- lib/legion/extensions/planning/helpers/plan_step.rb
|
|
39
|
+
- lib/legion/extensions/planning/helpers/plan_store.rb
|
|
40
|
+
- lib/legion/extensions/planning/runners/planning.rb
|
|
41
|
+
- lib/legion/extensions/planning/version.rb
|
|
42
|
+
homepage: https://github.com/LegionIO/lex-planning
|
|
43
|
+
licenses:
|
|
44
|
+
- MIT
|
|
45
|
+
metadata:
|
|
46
|
+
rubygems_mfa_required: 'true'
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.4'
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubygems_version: 3.6.9
|
|
62
|
+
specification_version: 4
|
|
63
|
+
summary: Hierarchical goal decomposition and plan formation for LegionIO cognitive
|
|
64
|
+
agents
|
|
65
|
+
test_files: []
|