roast-ai 0.4.8 → 0.4.10
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/.gitignore +1 -0
- data/.rubocop.yml +1 -0
- data/Gemfile +6 -7
- data/Gemfile.lock +15 -3
- data/README.md +9 -5
- data/dsl/demo/Gemfile +4 -0
- data/dsl/demo/Gemfile.lock +120 -0
- data/dsl/demo/cogs/local.rb +15 -0
- data/dsl/demo/simple_external_cog.rb +17 -0
- data/dsl/less_simple.rb +112 -0
- data/dsl/plugin-gem-example/.gitignore +8 -0
- data/dsl/plugin-gem-example/Gemfile +13 -0
- data/dsl/plugin-gem-example/Gemfile.lock +178 -0
- data/dsl/plugin-gem-example/lib/other.rb +17 -0
- data/dsl/plugin-gem-example/lib/plugin_gem_example.rb +5 -0
- data/dsl/plugin-gem-example/lib/simple.rb +15 -0
- data/dsl/plugin-gem-example/lib/version.rb +10 -0
- data/dsl/plugin-gem-example/plugin-gem-example.gemspec +28 -0
- data/dsl/prototype.rb +25 -0
- data/dsl/scoped_executors.rb +28 -0
- data/dsl/simple.rb +5 -7
- data/dsl/simple_chat.rb +12 -0
- data/dsl/step_communication.rb +24 -0
- data/examples/grading/README.md +46 -0
- data/examples/grading/analyze_coverage/prompt.md +52 -0
- data/examples/grading/calculate_final_grade.rb +64 -0
- data/examples/grading/format_result.rb +61 -0
- data/examples/grading/generate_grades/prompt.md +105 -0
- data/examples/grading/generate_recommendations/output.txt +17 -0
- data/examples/grading/generate_recommendations/prompt.md +60 -0
- data/examples/grading/read_dependencies/prompt.md +15 -0
- data/examples/grading/verify_mocks_and_stubs/prompt.md +12 -0
- data/examples/grading/verify_test_helpers/prompt.md +53 -0
- data/examples/grading/workflow.md +5 -0
- data/examples/grading/workflow.yml +28 -0
- data/lib/roast/dsl/cog/config.rb +36 -0
- data/lib/roast/dsl/cog/input.rb +30 -0
- data/lib/roast/dsl/cog/output.rb +24 -0
- data/lib/roast/dsl/cog/registry.rb +39 -0
- data/lib/roast/dsl/cog/stack.rb +22 -0
- data/lib/roast/dsl/cog/store.rb +29 -0
- data/lib/roast/dsl/cog.rb +91 -0
- data/lib/roast/dsl/cog_input_context.rb +9 -0
- data/lib/roast/dsl/cog_input_manager.rb +47 -0
- data/lib/roast/dsl/cogs/chat.rb +78 -0
- data/lib/roast/dsl/cogs/cmd.rb +132 -0
- data/lib/roast/dsl/cogs/execute.rb +46 -0
- data/lib/roast/dsl/cogs/graph.rb +53 -0
- data/lib/roast/dsl/config_context.rb +9 -0
- data/lib/roast/dsl/config_manager.rb +96 -0
- data/lib/roast/dsl/execution_context.rb +9 -0
- data/lib/roast/dsl/execution_manager.rb +137 -0
- data/lib/roast/dsl/workflow.rb +113 -0
- data/lib/roast/error.rb +7 -0
- data/lib/roast/errors.rb +3 -3
- data/lib/roast/graph/edge.rb +25 -0
- data/lib/roast/graph/node.rb +40 -0
- data/lib/roast/graph/quantum_edge.rb +27 -0
- data/lib/roast/graph/threaded_exec.rb +93 -0
- data/lib/roast/graph.rb +233 -0
- data/lib/roast/resources/api_resource.rb +2 -2
- data/lib/roast/resources/url_resource.rb +2 -2
- data/lib/roast/tools/apply_diff.rb +1 -1
- data/lib/roast/tools/ask_user.rb +1 -1
- data/lib/roast/tools/bash.rb +1 -1
- data/lib/roast/tools/cmd.rb +2 -2
- data/lib/roast/tools/coding_agent.rb +2 -2
- data/lib/roast/tools/grep.rb +1 -1
- data/lib/roast/tools/read_file.rb +1 -1
- data/lib/roast/tools/search_file.rb +1 -1
- data/lib/roast/tools/swarm.rb +1 -1
- data/lib/roast/tools/update_files.rb +2 -2
- data/lib/roast/tools/write_file.rb +1 -1
- data/lib/roast/tools.rb +1 -1
- data/lib/roast/value_objects/api_token.rb +1 -1
- data/lib/roast/value_objects/uri_base.rb +1 -1
- data/lib/roast/value_objects/workflow_path.rb +1 -1
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/base_workflow.rb +38 -2
- data/lib/roast/workflow/command_executor.rb +1 -1
- data/lib/roast/workflow/configuration_loader.rb +1 -1
- data/lib/roast/workflow/error_handler.rb +1 -1
- data/lib/roast/workflow/step_executor_registry.rb +1 -1
- data/lib/roast/workflow/step_loader.rb +1 -1
- data/lib/roast/workflow/workflow_executor.rb +1 -1
- data/lib/roast.rb +4 -3
- data/roast.gemspec +1 -0
- data/sorbet/config +3 -0
- data/sorbet/rbi/annotations/.gitattributes +1 -0
- data/sorbet/rbi/annotations/activesupport.rbi +495 -0
- data/sorbet/rbi/annotations/faraday.rbi +17 -0
- data/sorbet/rbi/annotations/minitest.rbi +119 -0
- data/sorbet/rbi/annotations/mocha.rbi +34 -0
- data/sorbet/rbi/annotations/rainbow.rbi +269 -0
- data/sorbet/rbi/annotations/webmock.rbi +9 -0
- data/sorbet/rbi/gems/marcel@1.1.0.rbi +239 -0
- data/sorbet/rbi/gems/{rack@2.2.17.rbi → rack@2.2.19.rbi} +55 -38
- data/sorbet/rbi/gems/{rexml@3.4.1.rbi → rexml@3.4.2.rbi} +284 -239
- data/sorbet/rbi/gems/ruby_llm@1.8.2.rbi +5703 -0
- data/sorbet/rbi/shims/lib/roast/dsl/cog_input_context.rbi +17 -0
- data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +17 -0
- data/sorbet/rbi/shims/lib/roast/dsl/execution_context.rbi +17 -0
- data/sorbet/rbi/todo.rbi +7 -0
- metadata +84 -6
- data/lib/roast/dsl/executor.rb +0 -27
- data/package-lock.json +0 -6
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module DSL
|
|
6
|
+
class ConfigManager
|
|
7
|
+
class ConfigManagerError < Roast::Error; end
|
|
8
|
+
class ConfigManagerNotPreparedError < ConfigManagerError; end
|
|
9
|
+
class ConfigManagerAlreadyPreparedError < ConfigManagerError; end
|
|
10
|
+
|
|
11
|
+
#: (Cog::Registry, Array[^() -> void]) -> void
|
|
12
|
+
def initialize(cog_registry, config_procs)
|
|
13
|
+
@cog_registry = cog_registry
|
|
14
|
+
@config_procs = config_procs
|
|
15
|
+
@config_context = ConfigContext.new #: ConfigContext
|
|
16
|
+
@general_configs = {} #: Hash[singleton(Cog), Cog::Config]
|
|
17
|
+
@name_scoped_configs = {} #: Hash[singleton(Cog), Hash[Symbol, Cog::Config]]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#: () -> void
|
|
21
|
+
def prepare!
|
|
22
|
+
raise ConfigManagerAlreadyPreparedError if preparing? || prepared?
|
|
23
|
+
|
|
24
|
+
@preparing = true
|
|
25
|
+
bind_registered_cogs
|
|
26
|
+
@config_procs.each { |cp| @config_context.instance_eval(&cp) }
|
|
27
|
+
@prepared = true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#: () -> bool
|
|
31
|
+
def preparing?
|
|
32
|
+
@preparing ||= false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
#: () -> bool
|
|
36
|
+
def prepared?
|
|
37
|
+
@prepared ||= false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
#: (singleton(Cog), ?Symbol?) -> Cog::Config
|
|
41
|
+
def config_for(cog_class, name = nil)
|
|
42
|
+
raise ConfigManagerNotPreparedError unless prepared?
|
|
43
|
+
|
|
44
|
+
# All cogs will always have a config; empty by default if the cog was never explicitly configured
|
|
45
|
+
config = fetch_general_config(cog_class)
|
|
46
|
+
name_scoped_config = fetch_name_scoped_config(cog_class, name) unless name.nil?
|
|
47
|
+
config = config.merge(name_scoped_config) if name_scoped_config
|
|
48
|
+
config
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
#: (singleton(Cog)) -> Cog::Config
|
|
54
|
+
def fetch_general_config(cog_class)
|
|
55
|
+
@general_configs[cog_class] ||= cog_class.config_class.new
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
#: (singleton(Cog), Symbol) -> Cog::Config
|
|
59
|
+
def fetch_name_scoped_config(cog_class, name)
|
|
60
|
+
name_scoped_configs_for_cog = @name_scoped_configs[cog_class] ||= {}
|
|
61
|
+
name_scoped_configs_for_cog[name] ||= cog_class.config_class.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#: () -> void
|
|
65
|
+
def bind_registered_cogs
|
|
66
|
+
@cog_registry.cogs.each { |cog_method_name, cog_class| bind_cog(cog_method_name, cog_class) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
#: (Symbol, singleton(Cog)) -> void
|
|
70
|
+
def bind_cog(cog_method_name, cog_class)
|
|
71
|
+
on_config_method = method(:on_config)
|
|
72
|
+
cog_method = proc do |cog_name = nil, &cog_config_proc|
|
|
73
|
+
on_config_method.call(cog_class, cog_name, &cog_config_proc)
|
|
74
|
+
end
|
|
75
|
+
@config_context.instance_eval do
|
|
76
|
+
define_singleton_method(cog_method_name, cog_method)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#: (singleton(Cog), Symbol) { () -> void } -> void
|
|
81
|
+
def on_config(cog_class, cog_name, &cog_config_proc)
|
|
82
|
+
# Called when the cog method is invoked in the workflow's 'config' block.
|
|
83
|
+
# This allows configuration parameters to be set for the cog generally or for a specific named instance
|
|
84
|
+
config_object = if cog_name.nil?
|
|
85
|
+
fetch_general_config(cog_class)
|
|
86
|
+
else
|
|
87
|
+
fetch_name_scoped_config(cog_class, cog_name)
|
|
88
|
+
end
|
|
89
|
+
# NOTE: Sorbet expects the proc passed to instance_exec to be declared as taking an argument
|
|
90
|
+
# but our cog_config_proc does not get an argument
|
|
91
|
+
config_object.instance_exec(&T.unsafe(cog_config_proc)) if cog_config_proc
|
|
92
|
+
config_object
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module DSL
|
|
6
|
+
# Context in which the `execute` block of a workflow is evaluated
|
|
7
|
+
class ExecutionManager
|
|
8
|
+
class ExecutionManagerError < Roast::Error; end
|
|
9
|
+
class ExecutionManagerNotPreparedError < ExecutionManagerError; end
|
|
10
|
+
class ExecutionManagerAlreadyPreparedError < ExecutionManagerError; end
|
|
11
|
+
class ExecutionManagerCurrentlyRunningError < ExecutionManagerError; end
|
|
12
|
+
class ExecutionScopeDoesNotExistError < ExecutionManagerError; end
|
|
13
|
+
class ExecutionScopeNotSpecifiedError < ExecutionManagerError; end
|
|
14
|
+
|
|
15
|
+
#: (Cog::Registry, ConfigManager, Hash[Symbol?, Array[^() -> void]], ?Symbol?) -> void
|
|
16
|
+
def initialize(cog_registry, config_manager, all_execution_procs, scope = nil)
|
|
17
|
+
@cog_registry = cog_registry
|
|
18
|
+
@config_manager = config_manager
|
|
19
|
+
@all_execution_procs = all_execution_procs
|
|
20
|
+
@scope = scope
|
|
21
|
+
@cogs = Cog::Store.new #: Cog::Store
|
|
22
|
+
@cog_stack = Cog::Stack.new #: Cog::Stack
|
|
23
|
+
@execution_context = ExecutionContext.new #: ExecutionContext
|
|
24
|
+
@cog_input_manager = CogInputManager.new(@cog_registry, @cogs) #: CogInputManager
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#: () -> void
|
|
28
|
+
def prepare!
|
|
29
|
+
raise ExecutionManagerAlreadyPreparedError if preparing? || prepared?
|
|
30
|
+
|
|
31
|
+
@preparing = true
|
|
32
|
+
bind_registered_cogs
|
|
33
|
+
my_execution_procs.each { |ep| @execution_context.instance_eval(&ep) }
|
|
34
|
+
@prepared = true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def run!
|
|
38
|
+
raise ExecutionManagerNotPreparedError unless prepared?
|
|
39
|
+
raise ExecutionManagerCurrentlyRunningError if running?
|
|
40
|
+
|
|
41
|
+
@running = true
|
|
42
|
+
@cog_stack.map do |name, cog|
|
|
43
|
+
cog.run!(
|
|
44
|
+
@config_manager.config_for(cog.class, name.to_sym),
|
|
45
|
+
cog_input_manager,
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
@running = false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
#: () -> bool
|
|
52
|
+
def preparing?
|
|
53
|
+
@preparing ||= false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#: () -> bool
|
|
57
|
+
def prepared?
|
|
58
|
+
@prepared ||= false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#: () -> bool
|
|
62
|
+
def running?
|
|
63
|
+
@running ||= false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
#: () -> CogInputContext
|
|
67
|
+
def cog_input_manager
|
|
68
|
+
raise ExecutionManagerNotPreparedError unless prepared?
|
|
69
|
+
|
|
70
|
+
@cog_input_manager.context
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
#: () -> Array[^() -> void]
|
|
76
|
+
def my_execution_procs
|
|
77
|
+
raise ExecutionScopeDoesNotExistError unless @all_execution_procs.key?(@scope)
|
|
78
|
+
|
|
79
|
+
@all_execution_procs[@scope] || []
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
#: (Symbol, Cog) -> void
|
|
83
|
+
def add_cog_instance(name, cog)
|
|
84
|
+
@cogs.insert(name, cog)
|
|
85
|
+
@cog_stack.push([name, cog])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# TODO: add typing for output
|
|
89
|
+
#: (Symbol) -> untyped
|
|
90
|
+
def output(name)
|
|
91
|
+
@cogs[name].output
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
#: () -> void
|
|
95
|
+
def bind_registered_cogs
|
|
96
|
+
@cog_registry.cogs.each { |cog_method_name, cog_class| bind_cog(cog_method_name, cog_class) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
#: (Symbol, singleton(Cog)) -> void
|
|
100
|
+
def bind_cog(cog_method_name, cog_class)
|
|
101
|
+
on_execute_method = method(:on_execute)
|
|
102
|
+
cog_method = proc do |cog_name = Random.uuid, &cog_input_proc|
|
|
103
|
+
on_execute_method.call(cog_class, cog_name, &cog_input_proc)
|
|
104
|
+
end
|
|
105
|
+
@execution_context.instance_eval do
|
|
106
|
+
define_singleton_method(cog_method_name, cog_method)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
#: (singleton(Cog), Symbol) { (Cog::Input) -> untyped } -> void
|
|
111
|
+
def on_execute(cog_class, cog_name, &cog_input_proc)
|
|
112
|
+
# Called when the cog method is invoked in the workflow's 'execute' block.
|
|
113
|
+
# This creates the cog instance and prepares it for execution.
|
|
114
|
+
cog_instance = if cog_class == Cogs::Execute
|
|
115
|
+
create_special_execute_cog(cog_name, cog_input_proc)
|
|
116
|
+
else
|
|
117
|
+
cog_class.new(cog_name, cog_input_proc)
|
|
118
|
+
end
|
|
119
|
+
add_cog_instance(cog_name, cog_instance)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
#: (Symbol, ^(Cogs::Execute::Input) -> untyped) -> Cogs::Execute
|
|
123
|
+
def create_special_execute_cog(cog_name, cog_input_proc)
|
|
124
|
+
trigger = proc do |input|
|
|
125
|
+
raise ExecutionScopeNotSpecifiedError unless input.scope.present?
|
|
126
|
+
|
|
127
|
+
em = ExecutionManager.new(@cog_registry, @config_manager, @all_execution_procs, input.scope)
|
|
128
|
+
em.prepare!
|
|
129
|
+
em.run!
|
|
130
|
+
|
|
131
|
+
# TODO: collect the outputs of the cogs in the execution manager that just ran and do something with them
|
|
132
|
+
end
|
|
133
|
+
Cogs::Execute.new(cog_name, cog_input_proc, trigger)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module DSL
|
|
6
|
+
class Workflow
|
|
7
|
+
class WorkflowError < Roast::Error; end
|
|
8
|
+
class WorkflowNotPreparedError < WorkflowError; end
|
|
9
|
+
class WorkflowAlreadyPreparedError < WorkflowError; end
|
|
10
|
+
class WorkflowAlreadyStartedError < WorkflowError; end
|
|
11
|
+
class InvalidCogReference < WorkflowError; end
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
#: (String) -> void
|
|
15
|
+
def from_file(workflow_path)
|
|
16
|
+
workflow = new(workflow_path)
|
|
17
|
+
workflow.prepare!
|
|
18
|
+
workflow.start!
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#: (String) -> void
|
|
23
|
+
def initialize(workflow_path)
|
|
24
|
+
@workflow_path = Pathname.new(workflow_path)
|
|
25
|
+
@workflow_definition = File.read(workflow_path)
|
|
26
|
+
@cog_registry = Cog::Registry.new #: Cog::Registry
|
|
27
|
+
@config_procs = [] #: Array[^() -> void]
|
|
28
|
+
@execution_procs = { nil: [] } #: Hash[Symbol?, Array[^() -> void]]
|
|
29
|
+
@config_manager = nil #: ConfigManager?
|
|
30
|
+
@execution_manager = nil #: ExecutionManager?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#: () -> void
|
|
34
|
+
def prepare!
|
|
35
|
+
raise WorkflowAlreadyPreparedError if preparing? || prepared?
|
|
36
|
+
|
|
37
|
+
@preparing = true
|
|
38
|
+
extract_dsl_procs!
|
|
39
|
+
@config_manager = ConfigManager.new(@cog_registry, @config_procs)
|
|
40
|
+
@config_manager.prepare!
|
|
41
|
+
@execution_manager = ExecutionManager.new(@cog_registry, @config_manager, @execution_procs)
|
|
42
|
+
@execution_manager.prepare!
|
|
43
|
+
|
|
44
|
+
@prepared = true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#: () -> void
|
|
48
|
+
def start!
|
|
49
|
+
raise WorkflowNotPreparedError unless @config_manager.present? && @execution_manager.present?
|
|
50
|
+
raise WorkflowAlreadyStartedError if started? || completed?
|
|
51
|
+
|
|
52
|
+
@started = true
|
|
53
|
+
@execution_manager.run!
|
|
54
|
+
@completed = true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
#: () -> bool
|
|
58
|
+
def preparing?
|
|
59
|
+
@preparing ||= false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#: () -> bool
|
|
63
|
+
def prepared?
|
|
64
|
+
@prepared ||= false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
#: () -> bool
|
|
68
|
+
def started?
|
|
69
|
+
@started ||= false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#: () -> bool
|
|
73
|
+
def completed?
|
|
74
|
+
@completed ||= false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#: { () [self: ConfigContext] -> void } -> void
|
|
78
|
+
def config(&block)
|
|
79
|
+
@config_procs << block
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
#: (?Symbol?) { () [self: ExecutionContext] -> void } -> void
|
|
83
|
+
def execute(scope = nil, &block)
|
|
84
|
+
(@execution_procs[scope] ||= []) << block
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def use(cogs = [], from: nil)
|
|
88
|
+
require from if from
|
|
89
|
+
|
|
90
|
+
Array.wrap(cogs).each do |cog_name|
|
|
91
|
+
path = @workflow_path.realdirpath.dirname.join("cogs/#{cog_name}")
|
|
92
|
+
require path.to_s if from.nil?
|
|
93
|
+
|
|
94
|
+
cog_class_name = cog_name.camelize
|
|
95
|
+
raise InvalidCogReference, "Expected #{cog_class_name} class, not found in #{path}" unless Object.const_defined?(cog_class_name)
|
|
96
|
+
|
|
97
|
+
cog_class = cog_class_name.constantize # rubocop:disable Sorbet/ConstantsFromStrings
|
|
98
|
+
@cog_registry.use(cog_class)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# Evaluate the top-level workflow definition
|
|
105
|
+
# This collects the procs passed to `config` and `execute` calls in the workflow definition,
|
|
106
|
+
# but does not evaluate any of them individually yet.
|
|
107
|
+
#: () -> void
|
|
108
|
+
def extract_dsl_procs!
|
|
109
|
+
instance_eval(@workflow_definition)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/roast/error.rb
ADDED
data/lib/roast/errors.rb
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
module Roast
|
|
5
5
|
module Errors
|
|
6
6
|
# Custom error for API resource not found (404) responses
|
|
7
|
-
class ResourceNotFoundError <
|
|
7
|
+
class ResourceNotFoundError < Roast::Error; end
|
|
8
8
|
|
|
9
9
|
# Custom error for when API authentication fails
|
|
10
|
-
class AuthenticationError <
|
|
10
|
+
class AuthenticationError < Roast::Error; end
|
|
11
11
|
|
|
12
12
|
# Exit the app, for instance via Ctrl-C during an InputStep
|
|
13
|
-
class ExitEarly <
|
|
13
|
+
class ExitEarly < Roast::Error; end
|
|
14
14
|
end
|
|
15
15
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
class Graph
|
|
6
|
+
class Edge
|
|
7
|
+
#: (Node) -> void
|
|
8
|
+
attr_writer :join_node
|
|
9
|
+
|
|
10
|
+
attr_reader :from_node, :to_node
|
|
11
|
+
|
|
12
|
+
#: (Node, Node, ?proc: Proc?) -> void
|
|
13
|
+
def initialize(from_node, to_node, proc: nil)
|
|
14
|
+
@from_node = from_node
|
|
15
|
+
@to_node = to_node
|
|
16
|
+
@proc = proc # TODO: Shadowing proc builtin here
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
#: () -> String
|
|
20
|
+
def to_s
|
|
21
|
+
"#{@from_node} -> #{@to_node}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
class Graph
|
|
6
|
+
class Node
|
|
7
|
+
# class InvalidExecutableError < Roast::Error; end
|
|
8
|
+
|
|
9
|
+
attr_reader :name, :executable
|
|
10
|
+
|
|
11
|
+
#: (Symbol, ?executable: Proc | Graph | nil) -> void
|
|
12
|
+
def initialize(name, executable: nil)
|
|
13
|
+
@name = name
|
|
14
|
+
@executable = executable
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#: () -> T::Boolean
|
|
18
|
+
def subgraph?
|
|
19
|
+
@executable.is_a?(Graph)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#: () -> T::Boolean
|
|
23
|
+
def done?
|
|
24
|
+
@name == :DONE
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#: (Hash) -> void
|
|
28
|
+
def execute(state)
|
|
29
|
+
return if @executable.nil?
|
|
30
|
+
|
|
31
|
+
executable = @executable
|
|
32
|
+
if executable.is_a?(Proc)
|
|
33
|
+
executable.call(state)
|
|
34
|
+
elsif executable.is_a?(Graph)
|
|
35
|
+
executable.execute(state)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
class Graph
|
|
6
|
+
class QuantumEdge
|
|
7
|
+
#: (Roast::Graph::Node, Proc) -> void
|
|
8
|
+
def initialize(from_node, to_proc)
|
|
9
|
+
@from_node = from_node
|
|
10
|
+
@to_proc = to_proc
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
#: (Hash[untyped, untyped], Hash[Symbol, Roast::Graph::Node]) -> Array[Roast::Graph::Edge]
|
|
14
|
+
def collapse(state, nodes)
|
|
15
|
+
to_node_names = @to_proc.call(state)
|
|
16
|
+
to_node_names = to_node_names.is_a?(Array) ? to_node_names : [to_node_names]
|
|
17
|
+
|
|
18
|
+
to_node_names.map do |to_node_name|
|
|
19
|
+
to_node = nodes[to_node_name]
|
|
20
|
+
raise Error, "No node found with name #{to_node_name.inspect}" if to_node.nil?
|
|
21
|
+
|
|
22
|
+
Edge.new(@from_node, to_node)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
class Graph
|
|
6
|
+
class StateConflictError < Error; end
|
|
7
|
+
|
|
8
|
+
class ThreadedExec
|
|
9
|
+
def initialize(nodes, og_state)
|
|
10
|
+
@nodes = nodes
|
|
11
|
+
@og_state = og_state
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
#: () -> Hash[untyped, untyped]
|
|
15
|
+
def async_execute
|
|
16
|
+
states = threaded_execute(@nodes, @og_state)
|
|
17
|
+
merge_states!(@og_state, states)
|
|
18
|
+
@og_state
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns a hash of the new states for each node.
|
|
22
|
+
#: (Array[Node], Hash[untyped, untyped]) -> Hash[Symbol, Hash[untyped, untyped]]
|
|
23
|
+
def threaded_execute(nodes, og_state)
|
|
24
|
+
states = {}
|
|
25
|
+
threads = nodes.map do |current_node|
|
|
26
|
+
states[current_node.name] = og_state.dup
|
|
27
|
+
Thread.new do
|
|
28
|
+
current_node.execute(states[current_node.name])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
threads.map(&:value)
|
|
33
|
+
|
|
34
|
+
states
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#: (Hash, Hash) -> void
|
|
38
|
+
def merge_states!(orig_state, new_states)
|
|
39
|
+
orig_state.merge!(merge_new_states(orig_state, new_states))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# We merge all the new states together first so we can catch any keys that were modified by multiple threads.
|
|
43
|
+
#: (Hash[untyped, untyped], Hash[Symbol, Hash[untyped, untyped]]) -> Hash[untyped, untyped]
|
|
44
|
+
def merge_new_states(orig_state, new_states)
|
|
45
|
+
# We only care about what the threads have changes from the original state.
|
|
46
|
+
changed_states = changed_states(orig_state, new_states)
|
|
47
|
+
|
|
48
|
+
return {} if changed_states.empty?
|
|
49
|
+
|
|
50
|
+
# Grab some entry to be the one we merge all the other changes into.
|
|
51
|
+
base_node_name, base_changed_state = T.must(changed_states.shift)
|
|
52
|
+
base_node_name = T.cast(base_node_name, Symbol)
|
|
53
|
+
base_changed_state = T.cast(base_changed_state, T::Hash[T.untyped, T.untyped])
|
|
54
|
+
|
|
55
|
+
changed_states.each do |node_name, changed_state|
|
|
56
|
+
base_changed_state.merge!(changed_state) do |key, old_value, new_value|
|
|
57
|
+
raise_state_conflict(key, old_value, new_value, [base_node_name, node_name])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
base_changed_state
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#: (Hash, Hash[Symbol, Hash]) -> Hash[Symbol, Hash]
|
|
65
|
+
def changed_states(orig_state, new_states)
|
|
66
|
+
new_states.transform_values do |new_state|
|
|
67
|
+
changed_state_entries(orig_state, new_state)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Filter new states content to only include keys that were modified by the new states.
|
|
72
|
+
#: (Hash, Hash) -> Hash
|
|
73
|
+
def changed_state_entries(orig_state, new_state)
|
|
74
|
+
new_state.reject do |key, value|
|
|
75
|
+
orig_state[key] == value
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
#: (Symbol, untyped, untyped, Array[Symbol]) -> void
|
|
80
|
+
def raise_state_conflict(key, old_value, new_value, conflicting_nodes)
|
|
81
|
+
raise StateConflictError, <<~CONFLICT.chomp
|
|
82
|
+
Parallel nodes modified the same state key.
|
|
83
|
+
Conflicting nodes: #{conflicting_nodes.join(", ")}
|
|
84
|
+
Key: :#{key}
|
|
85
|
+
Old value:
|
|
86
|
+
#{old_value.inspect}
|
|
87
|
+
New value:
|
|
88
|
+
#{new_value.inspect}
|
|
89
|
+
CONFLICT
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|