roast-ai 0.4.10 → 0.5.1
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/.claude/commands/docs/write-comments.md +36 -0
- data/.github/CODEOWNERS +1 -1
- data/.github/workflows/ci.yaml +10 -6
- data/.gitignore +0 -1
- data/.rubocop.yml +7 -1
- data/.ruby-version +1 -1
- data/CLAUDE.md +2 -2
- data/CONTRIBUTING.md +2 -0
- data/Gemfile +19 -18
- data/Gemfile.lock +35 -58
- data/README.md +118 -1432
- data/README_LEGACY.md +1464 -0
- data/Rakefile +39 -4
- data/dev.yml +29 -0
- data/dsl/agent_sessions.rb +20 -0
- data/dsl/async_cogs.rb +49 -0
- data/dsl/async_cogs_complex.rb +67 -0
- data/dsl/call.rb +44 -0
- data/dsl/collect_from.rb +72 -0
- data/dsl/json_output.rb +28 -0
- data/dsl/map.rb +55 -0
- data/dsl/map_reduce.rb +37 -0
- data/dsl/map_with_index.rb +49 -0
- data/dsl/next_break.rb +45 -0
- data/dsl/next_break_parallel.rb +44 -0
- data/dsl/outputs.rb +39 -0
- data/dsl/outputs_bang.rb +36 -0
- data/dsl/parallel_map.rb +37 -0
- data/dsl/prompts/simple_prompt.md.erb +3 -0
- data/dsl/prototype.rb +5 -7
- data/dsl/repeat_loop_results.rb +53 -0
- data/dsl/ruby_cog.rb +72 -0
- data/dsl/simple_agent.rb +18 -0
- data/dsl/simple_chat.rb +15 -1
- data/dsl/simple_repeat.rb +29 -0
- data/dsl/skip.rb +36 -0
- data/dsl/step_communication.rb +2 -3
- data/dsl/targets_and_params.rb +57 -0
- data/dsl/temperature.rb +17 -0
- data/dsl/temporary_directory.rb +22 -0
- data/dsl/tutorial/01_your_first_workflow/README.md +179 -0
- data/dsl/tutorial/01_your_first_workflow/configured_chat.rb +33 -0
- data/dsl/tutorial/01_your_first_workflow/hello.rb +23 -0
- data/dsl/tutorial/02_chaining_cogs/README.md +310 -0
- data/dsl/tutorial/02_chaining_cogs/code_review.rb +104 -0
- data/dsl/tutorial/02_chaining_cogs/session_resumption.rb +92 -0
- data/dsl/tutorial/02_chaining_cogs/simple_chain.rb +84 -0
- data/dsl/tutorial/03_targets_and_params/README.md +230 -0
- data/dsl/tutorial/03_targets_and_params/multiple_targets.rb +65 -0
- data/dsl/tutorial/03_targets_and_params/single_target.rb +65 -0
- data/dsl/tutorial/04_configuration_options/README.md +209 -0
- data/dsl/tutorial/04_configuration_options/control_display_and_temperature.rb +104 -0
- data/dsl/tutorial/04_configuration_options/simple_config.rb +68 -0
- data/dsl/tutorial/05_control_flow/README.md +156 -0
- data/dsl/tutorial/05_control_flow/conditional_execution.rb +62 -0
- data/dsl/tutorial/05_control_flow/handling_failures.rb +77 -0
- data/dsl/tutorial/06_reusable_scopes/README.md +172 -0
- data/dsl/tutorial/06_reusable_scopes/accessing_scope_outputs.rb +126 -0
- data/dsl/tutorial/06_reusable_scopes/basic_scope.rb +63 -0
- data/dsl/tutorial/06_reusable_scopes/parameterized_scope.rb +78 -0
- data/dsl/tutorial/07_processing_collections/README.md +152 -0
- data/dsl/tutorial/07_processing_collections/basic_map.rb +70 -0
- data/dsl/tutorial/07_processing_collections/parallel_map.rb +74 -0
- data/dsl/tutorial/08_iterative_workflows/README.md +231 -0
- data/dsl/tutorial/08_iterative_workflows/basic_repeat.rb +57 -0
- data/dsl/tutorial/08_iterative_workflows/conditional_break.rb +57 -0
- data/dsl/tutorial/09_async_cogs/README.md +197 -0
- data/dsl/tutorial/09_async_cogs/basic_async.rb +38 -0
- data/dsl/tutorial/README.md +222 -0
- data/dsl/working_directory.rb +16 -0
- data/exe/roast +1 -1
- data/internal/documentation/architectural-notes.md +115 -0
- data/internal/documentation/doc-comments-external.md +686 -0
- data/internal/documentation/doc-comments-internal.md +342 -0
- data/internal/documentation/doc-comments.md +211 -0
- data/lib/roast/dsl/cog/config.rb +274 -3
- data/lib/roast/dsl/cog/input.rb +53 -10
- data/lib/roast/dsl/cog/output.rb +297 -8
- data/lib/roast/dsl/cog/registry.rb +35 -3
- data/lib/roast/dsl/cog/stack.rb +1 -1
- data/lib/roast/dsl/cog/store.rb +5 -5
- data/lib/roast/dsl/cog.rb +70 -14
- data/lib/roast/dsl/cog_input_context.rb +36 -1
- data/lib/roast/dsl/cog_input_manager.rb +116 -7
- data/lib/roast/dsl/cogs/agent/config.rb +465 -0
- data/lib/roast/dsl/cogs/agent/input.rb +81 -0
- data/lib/roast/dsl/cogs/agent/output.rb +59 -0
- data/lib/roast/dsl/cogs/agent/provider.rb +51 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/claude_invocation.rb +185 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/message.rb +73 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/assistant_message.rb +36 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/result_message.rb +61 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/system_message.rb +47 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/text_message.rb +36 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_result_message.rb +47 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_use_message.rb +46 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/unknown_message.rb +27 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/user_message.rb +37 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/tool_result.rb +51 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/tool_use.rb +48 -0
- data/lib/roast/dsl/cogs/agent/providers/claude.rb +31 -0
- data/lib/roast/dsl/cogs/agent/stats.rb +92 -0
- data/lib/roast/dsl/cogs/agent/usage.rb +62 -0
- data/lib/roast/dsl/cogs/agent.rb +75 -0
- data/lib/roast/dsl/cogs/chat/config.rb +453 -0
- data/lib/roast/dsl/cogs/chat/input.rb +92 -0
- data/lib/roast/dsl/cogs/chat/output.rb +64 -0
- data/lib/roast/dsl/cogs/chat/session.rb +68 -0
- data/lib/roast/dsl/cogs/chat.rb +59 -56
- data/lib/roast/dsl/cogs/cmd.rb +251 -61
- data/lib/roast/dsl/cogs/ruby.rb +171 -0
- data/lib/roast/dsl/command_runner.rb +191 -0
- data/lib/roast/dsl/config_manager.rb +58 -11
- data/lib/roast/dsl/control_flow.rb +41 -0
- data/lib/roast/dsl/execution_manager.rb +162 -32
- data/lib/roast/dsl/nil_assertions.rb +23 -0
- data/lib/roast/dsl/system_cog/params.rb +32 -0
- data/lib/roast/dsl/system_cog.rb +36 -0
- data/lib/roast/dsl/system_cogs/call.rb +163 -0
- data/lib/roast/dsl/system_cogs/map.rb +454 -0
- data/lib/roast/dsl/system_cogs/repeat.rb +242 -0
- data/lib/roast/dsl/workflow.rb +26 -16
- data/lib/roast/dsl/workflow_context.rb +20 -0
- data/lib/roast/dsl/workflow_params.rb +24 -0
- data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
- data/lib/roast/sorbet_runtime_stub.rb +154 -0
- data/lib/roast/tools/apply_diff.rb +1 -3
- data/lib/roast/tools/cmd.rb +4 -3
- data/lib/roast/tools/read_file.rb +1 -1
- data/lib/roast/tools/update_files.rb +1 -1
- data/lib/roast/tools/write_file.rb +1 -1
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/base_workflow.rb +4 -0
- data/lib/roast/workflow/step_loader.rb +14 -2
- data/lib/roast-ai.rb +4 -0
- data/lib/roast.rb +58 -21
- data/{roast.gemspec → roast-ai.gemspec} +9 -13
- data/sorbet/rbi/gems/async@2.34.0.rbi +1577 -0
- data/sorbet/rbi/gems/cli-kit@5.2.0.rbi +2063 -0
- data/sorbet/rbi/gems/{cli-ui@2.3.0.rbi → cli-ui@2.7.0-6bdefd1d06305e5d6ae312ac76f9c88f88658dda.rbi} +1418 -1013
- data/sorbet/rbi/gems/console@1.34.2.rbi +1193 -0
- data/sorbet/rbi/gems/fiber-annotation@0.2.0.rbi +50 -0
- data/sorbet/rbi/gems/fiber-local@1.1.0.rbi +35 -0
- data/sorbet/rbi/gems/fiber-storage@1.0.1.rbi +41 -0
- data/sorbet/rbi/gems/io-event@1.14.0.rbi +724 -0
- data/sorbet/rbi/gems/metrics@0.15.0.rbi +9 -0
- data/sorbet/rbi/gems/traces@0.18.2.rbi +9 -0
- data/sorbet/rbi/shims/lib/roast/dsl/cog_input_context.rbi +1185 -5
- data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +311 -5
- data/sorbet/rbi/shims/lib/roast/dsl/execution_context.rbi +486 -5
- data/sorbet/tapioca/config.yml +6 -0
- data/sorbet/tapioca/require.rb +2 -0
- metadata +157 -30
- data/dsl/less_simple.rb +0 -112
- data/dsl/scoped_executors.rb +0 -28
- data/dsl/simple.rb +0 -8
- data/lib/roast/dsl/cogs/execute.rb +0 -46
- data/lib/roast/dsl/cogs/graph.rb +0 -53
- data/sorbet/rbi/gems/cgi@0.5.0.rbi +0 -2961
- data/sorbet/rbi/gems/claude_swarm@0.1.19.rbi +0 -568
- data/sorbet/rbi/gems/cli-kit@5.0.1.rbi +0 -1991
- data/sorbet/rbi/gems/dry-configurable@1.3.0.rbi +0 -672
- data/sorbet/rbi/gems/dry-core@1.1.0.rbi +0 -1894
- data/sorbet/rbi/gems/dry-inflector@1.2.0.rbi +0 -659
- data/sorbet/rbi/gems/dry-initializer@3.2.0.rbi +0 -781
- data/sorbet/rbi/gems/dry-logic@1.6.0.rbi +0 -1127
- data/sorbet/rbi/gems/dry-schema@1.14.1.rbi +0 -3727
- data/sorbet/rbi/gems/dry-types@1.8.3.rbi +0 -3969
- data/sorbet/rbi/gems/fast-mcp-annotations@1.5.3.rbi +0 -1588
- data/sorbet/rbi/gems/mime-types-data@3.2025.0617.rbi +0 -136
- data/sorbet/rbi/gems/mime-types@3.7.0.rbi +0 -1342
- data/sorbet/rbi/gems/rack@2.2.19.rbi +0 -5676
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
- data/sorbet/rbi/gems/yard@0.9.37.rbi +0 -18492
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module DSL
|
|
6
|
+
# The canonical way to execute shell commands in Roast.
|
|
7
|
+
#
|
|
8
|
+
# CommandRunner is the standard command execution interface for DSL cogs
|
|
9
|
+
# and should be used for all command invocations in this project.
|
|
10
|
+
#
|
|
11
|
+
# Features:
|
|
12
|
+
# - Separate stdout/stderr capture (using Async fibers for concurrency)
|
|
13
|
+
# - Line-by-line streaming callbacks for custom handling
|
|
14
|
+
# - Optional timeout support with automatic process cleanup
|
|
15
|
+
# - Direct command execution (no shell by default for safety)
|
|
16
|
+
#
|
|
17
|
+
# Note: Currently executes commands directly without shell features.
|
|
18
|
+
# Shell support (pipes, redirects, etc.) will be added in a future version.
|
|
19
|
+
class CommandRunner
|
|
20
|
+
class CommandRunnerError < StandardError; end
|
|
21
|
+
|
|
22
|
+
class NoCommandProvidedError < CommandRunnerError; end
|
|
23
|
+
|
|
24
|
+
class TimeoutError < CommandRunnerError; end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Execute a command with optional stream handlers
|
|
28
|
+
#
|
|
29
|
+
# @param args [Array<String>] Command and arguments as an array
|
|
30
|
+
# @param timeout [Integer, nil] Timeout in seconds (default: nil, no timeout)
|
|
31
|
+
# @param stdout_handler [Proc, nil] Called for each stdout line
|
|
32
|
+
# @param stderr_handler [Proc, nil] Called for each stderr line
|
|
33
|
+
# @return [Array<String, String, Process::Status>] stdout, stderr, status
|
|
34
|
+
#
|
|
35
|
+
# @example Basic usage
|
|
36
|
+
# stdout, stderr, status = CommandRunner.execute(["echo", "hello"])
|
|
37
|
+
#
|
|
38
|
+
# @example With handlers for streaming output
|
|
39
|
+
# CommandRunner.execute(
|
|
40
|
+
# ["ls", "-la"],
|
|
41
|
+
# stdout_handler: ->(line) { puts "[OUT] #{line}" }
|
|
42
|
+
# )
|
|
43
|
+
#
|
|
44
|
+
# @example With explicit timeout
|
|
45
|
+
# CommandRunner.execute(["sleep", "5"], timeout: 2) # Will timeout after 2 seconds
|
|
46
|
+
#: (
|
|
47
|
+
#| Array[String],
|
|
48
|
+
#| ?working_directory: (Pathname | String)?,
|
|
49
|
+
#| ?timeout: (Integer | Float)?,
|
|
50
|
+
#| ?stdin_content: String?,
|
|
51
|
+
#| ?stdout_handler: (^(String) -> void)?,
|
|
52
|
+
#| ?stderr_handler: (^(String) -> void)?,
|
|
53
|
+
#| ) -> [String, String, Process::Status]
|
|
54
|
+
def execute(
|
|
55
|
+
args,
|
|
56
|
+
working_directory: nil,
|
|
57
|
+
timeout: nil,
|
|
58
|
+
stdin_content: nil,
|
|
59
|
+
stdout_handler: nil,
|
|
60
|
+
stderr_handler: nil
|
|
61
|
+
)
|
|
62
|
+
args.compact!
|
|
63
|
+
raise NoCommandProvidedError if args.blank?
|
|
64
|
+
|
|
65
|
+
stdin, stdout, stderr, wait_thread = Open3 #: as untyped
|
|
66
|
+
.popen3(
|
|
67
|
+
{ "PWD" => working_directory&.to_s }.compact,
|
|
68
|
+
*args.map(&:to_s),
|
|
69
|
+
{ chdir: working_directory }.compact,
|
|
70
|
+
)
|
|
71
|
+
stdin.puts stdin_content if stdin_content.present?
|
|
72
|
+
stdin.close
|
|
73
|
+
pid = wait_thread.pid
|
|
74
|
+
|
|
75
|
+
# If timeout is specified, start a timer in a separate fiber
|
|
76
|
+
timeout_task = if timeout
|
|
77
|
+
Async do |task|
|
|
78
|
+
task.annotate("CommandRunner Timeout Monitor")
|
|
79
|
+
sleep(timeout)
|
|
80
|
+
kill_process(pid) if pid
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Read stdout and stderr concurrently
|
|
85
|
+
stdout_content, stderr_content = Sync do |sync_task|
|
|
86
|
+
sync_task.annotate("CommandRunner Process Handler")
|
|
87
|
+
stdout_task = Async do |task|
|
|
88
|
+
task.annotate("CommandRunner Standard Output Reader")
|
|
89
|
+
buffer = "" #: String
|
|
90
|
+
stdout.each_line do |line|
|
|
91
|
+
buffer += line
|
|
92
|
+
begin
|
|
93
|
+
stdout_handler&.call(line)
|
|
94
|
+
rescue => e
|
|
95
|
+
Roast::Helpers::Logger.debug("stdout_handler raised: #{e.class} - #{e.message}")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
buffer
|
|
99
|
+
rescue IOError
|
|
100
|
+
buffer
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
stderr_task = Async do |task|
|
|
104
|
+
task.annotate("CommandRunner Standard Error Reader")
|
|
105
|
+
buffer = "" #: String
|
|
106
|
+
stderr.each_line do |line|
|
|
107
|
+
buffer += line
|
|
108
|
+
begin
|
|
109
|
+
stderr_handler&.call(line)
|
|
110
|
+
rescue => e
|
|
111
|
+
Roast::Helpers::Logger.debug("stderr_handler raised: #{e.class} - #{e.message}")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
buffer
|
|
115
|
+
rescue IOError
|
|
116
|
+
buffer
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
[stdout_task.wait, stderr_task.wait]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Wait for the process to complete
|
|
123
|
+
status = wait_thread.value
|
|
124
|
+
|
|
125
|
+
# Cancel the timeout task if it's still running
|
|
126
|
+
timeout_task&.stop
|
|
127
|
+
|
|
128
|
+
# Check if the process was killed due to timeout
|
|
129
|
+
if timeout && status.signaled? && (status.termsig == 15 || status.termsig == 9)
|
|
130
|
+
raise TimeoutError, "Command timed out after #{timeout} seconds"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
[stdout_content, stderr_content, status]
|
|
134
|
+
ensure
|
|
135
|
+
# Clean up resources
|
|
136
|
+
begin
|
|
137
|
+
[stdout, stderr].compact.each(&:close)
|
|
138
|
+
rescue
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
# If we haven't waited for the process yet, kill it
|
|
142
|
+
if pid && wait_thread&.alive?
|
|
143
|
+
Async do |task|
|
|
144
|
+
task.annotate("CommandRunner Process Killer")
|
|
145
|
+
kill_process(pid)
|
|
146
|
+
end.wait
|
|
147
|
+
wait_thread.join(1) # Give it a second to finish
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
#: (Integer) -> void
|
|
154
|
+
def kill_process(pid)
|
|
155
|
+
return unless process_running?(pid)
|
|
156
|
+
|
|
157
|
+
# First try TERM signal
|
|
158
|
+
Process.kill("TERM", pid)
|
|
159
|
+
|
|
160
|
+
# Give process a short time to terminate gracefully
|
|
161
|
+
5.times do
|
|
162
|
+
sleep(0.02)
|
|
163
|
+
return unless process_running?(pid)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# If still running, use KILL signal
|
|
167
|
+
Process.kill("KILL", pid) if process_running?(pid)
|
|
168
|
+
|
|
169
|
+
# Also try to kill the process group to ensure child processes are killed
|
|
170
|
+
begin
|
|
171
|
+
Process.kill("-KILL", pid)
|
|
172
|
+
rescue
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
rescue Errno::ESRCH
|
|
176
|
+
# Process already terminated
|
|
177
|
+
rescue Errno::EPERM
|
|
178
|
+
Roast::Helpers::Logger.debug("Could not kill process #{pid}: Permission denied")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
#: (Integer) -> bool
|
|
182
|
+
def process_running?(pid)
|
|
183
|
+
Process.getpgid(pid)
|
|
184
|
+
true
|
|
185
|
+
rescue Errno::ESRCH
|
|
186
|
+
false
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -7,13 +7,16 @@ module Roast
|
|
|
7
7
|
class ConfigManagerError < Roast::Error; end
|
|
8
8
|
class ConfigManagerNotPreparedError < ConfigManagerError; end
|
|
9
9
|
class ConfigManagerAlreadyPreparedError < ConfigManagerError; end
|
|
10
|
+
class IllegalCogNameError < ConfigManagerError; end
|
|
10
11
|
|
|
11
12
|
#: (Cog::Registry, Array[^() -> void]) -> void
|
|
12
13
|
def initialize(cog_registry, config_procs)
|
|
13
14
|
@cog_registry = cog_registry
|
|
14
15
|
@config_procs = config_procs
|
|
15
16
|
@config_context = ConfigContext.new #: ConfigContext
|
|
17
|
+
@global_config = Cog::Config.new #: Cog::Config
|
|
16
18
|
@general_configs = {} #: Hash[singleton(Cog), Cog::Config]
|
|
19
|
+
@regexp_scoped_configs = {} #: Hash[singleton(Cog), Hash[Regexp, Cog::Config]]
|
|
17
20
|
@name_scoped_configs = {} #: Hash[singleton(Cog), Hash[Symbol, Cog::Config]]
|
|
18
21
|
end
|
|
19
22
|
|
|
@@ -22,6 +25,7 @@ module Roast
|
|
|
22
25
|
raise ConfigManagerAlreadyPreparedError if preparing? || prepared?
|
|
23
26
|
|
|
24
27
|
@preparing = true
|
|
28
|
+
bind_global
|
|
25
29
|
bind_registered_cogs
|
|
26
30
|
@config_procs.each { |cp| @config_context.instance_eval(&cp) }
|
|
27
31
|
@prepared = true
|
|
@@ -42,9 +46,16 @@ module Roast
|
|
|
42
46
|
raise ConfigManagerNotPreparedError unless prepared?
|
|
43
47
|
|
|
44
48
|
# All cogs will always have a config; empty by default if the cog was never explicitly configured
|
|
45
|
-
config =
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
config = cog_class.config_class.new(@global_config.instance_variable_get(:@values).deep_dup)
|
|
50
|
+
config = config.merge(fetch_general_config(cog_class))
|
|
51
|
+
@regexp_scoped_configs.fetch(cog_class, {}).select do |pattern, _|
|
|
52
|
+
pattern.match?(name.to_s) unless name.nil?
|
|
53
|
+
end.values.each { |cfg| config = config.merge(cfg) }
|
|
54
|
+
unless name.nil?
|
|
55
|
+
name_scoped_config = fetch_name_scoped_config(cog_class, name)
|
|
56
|
+
config = config.merge(name_scoped_config)
|
|
57
|
+
end
|
|
58
|
+
config.validate!
|
|
48
59
|
config
|
|
49
60
|
end
|
|
50
61
|
|
|
@@ -55,6 +66,12 @@ module Roast
|
|
|
55
66
|
@general_configs[cog_class] ||= cog_class.config_class.new
|
|
56
67
|
end
|
|
57
68
|
|
|
69
|
+
#: (singleton(Cog), Regexp) -> Cog::Config
|
|
70
|
+
def fetch_regexp_scoped_config(cog_class, pattern)
|
|
71
|
+
regexp_scoped_configs_for_cog = @regexp_scoped_configs[cog_class] ||= {}
|
|
72
|
+
regexp_scoped_configs_for_cog[pattern] ||= cog_class.config_class.new
|
|
73
|
+
end
|
|
74
|
+
|
|
58
75
|
#: (singleton(Cog), Symbol) -> Cog::Config
|
|
59
76
|
def fetch_name_scoped_config(cog_class, name)
|
|
60
77
|
name_scoped_configs_for_cog = @name_scoped_configs[cog_class] ||= {}
|
|
@@ -69,27 +86,57 @@ module Roast
|
|
|
69
86
|
#: (Symbol, singleton(Cog)) -> void
|
|
70
87
|
def bind_cog(cog_method_name, cog_class)
|
|
71
88
|
on_config_method = method(:on_config)
|
|
72
|
-
cog_method = proc do |
|
|
73
|
-
on_config_method.call(cog_class,
|
|
89
|
+
cog_method = proc do |cog_name_or_pattern = nil, &cog_config_proc|
|
|
90
|
+
on_config_method.call(cog_class, cog_name_or_pattern, cog_config_proc)
|
|
74
91
|
end
|
|
75
92
|
@config_context.instance_eval do
|
|
93
|
+
raise IllegalCogNameError, cog_method_name if respond_to?(cog_method_name, true)
|
|
94
|
+
|
|
76
95
|
define_singleton_method(cog_method_name, cog_method)
|
|
77
96
|
end
|
|
78
97
|
end
|
|
79
98
|
|
|
80
|
-
#: (singleton(Cog), Symbol)
|
|
81
|
-
def on_config(cog_class,
|
|
99
|
+
#: (singleton(Cog), (Symbol | Regexp)?, ^() -> void ) -> void
|
|
100
|
+
def on_config(cog_class, cog_name_or_pattern, cog_config_proc)
|
|
82
101
|
# Called when the cog method is invoked in the workflow's 'config' block.
|
|
83
102
|
# This allows configuration parameters to be set for the cog generally or for a specific named instance
|
|
84
|
-
|
|
103
|
+
|
|
104
|
+
# NOTE: cast to untyped is to intentional handling the 'unreachable' else case here.
|
|
105
|
+
# This method takes user input directly so additional validation with a clearer exception message will be helpful
|
|
106
|
+
cog_name_or_pattern = cog_name_or_pattern #: untyped
|
|
107
|
+
config_object = case cog_name_or_pattern
|
|
108
|
+
when NilClass
|
|
85
109
|
fetch_general_config(cog_class)
|
|
110
|
+
when Regexp
|
|
111
|
+
fetch_regexp_scoped_config(cog_class, cog_name_or_pattern)
|
|
112
|
+
when Symbol
|
|
113
|
+
fetch_name_scoped_config(cog_class, cog_name_or_pattern)
|
|
86
114
|
else
|
|
87
|
-
|
|
115
|
+
raise ArgumentError, "Invalid type '#{cog_name_or_pattern.class}' for cog_name_or_pattern"
|
|
88
116
|
end
|
|
117
|
+
|
|
89
118
|
# NOTE: Sorbet expects the proc passed to instance_exec to be declared as taking an argument
|
|
90
119
|
# but our cog_config_proc does not get an argument
|
|
91
|
-
|
|
92
|
-
config_object
|
|
120
|
+
cog_config_proc = cog_config_proc #: as ^(untyped) -> void
|
|
121
|
+
config_object.instance_exec(&cog_config_proc) if cog_config_proc
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def bind_global
|
|
126
|
+
on_global_method = method(:on_global)
|
|
127
|
+
method_to_bind = proc do |&global_proc|
|
|
128
|
+
on_global_method.call(global_proc)
|
|
129
|
+
end
|
|
130
|
+
@config_context.instance_eval do
|
|
131
|
+
define_singleton_method(:global, method_to_bind)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
#: (^() -> void ) -> void
|
|
136
|
+
def on_global(global_config_proc)
|
|
137
|
+
global_config_proc = global_config_proc #: as ^(untyped) -> void
|
|
138
|
+
@global_config.instance_exec(&global_config_proc) if global_config_proc
|
|
139
|
+
nil
|
|
93
140
|
end
|
|
94
141
|
end
|
|
95
142
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module DSL
|
|
6
|
+
# Errors that can be raised to control workflow execution
|
|
7
|
+
module ControlFlow
|
|
8
|
+
class Base < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Raised in a cog's input block or execute method to terminate the cog and mark it as 'skipped'
|
|
11
|
+
# without triggering any failure handling
|
|
12
|
+
class SkipCog < Base; end
|
|
13
|
+
|
|
14
|
+
# Raised in a cog's input block or execute method to terminate the cog and mark it as 'failed'
|
|
15
|
+
# without terminating the workflow. The workflow may abort anyway if the cog is configured to abort the
|
|
16
|
+
# workflow on failure.
|
|
17
|
+
class FailCog < Base; end
|
|
18
|
+
|
|
19
|
+
# Raised in a cog's input block or execute method to terminate the current loop iteration
|
|
20
|
+
# and start the next iteration immediately. The current cog will be marked as 'skipped',
|
|
21
|
+
# and every subsequent cog in the current iteration will not run. Any async cogs currently running in the
|
|
22
|
+
# current scope will be stopped.
|
|
23
|
+
#
|
|
24
|
+
# If this exception is raised outside of a loop (e.g, via the `call` cog, or in the top-level executor),
|
|
25
|
+
# this exception will just terminate that executor as described above without starting a 'next' iteration.
|
|
26
|
+
class Next < Base; end
|
|
27
|
+
|
|
28
|
+
# Raised in a cog's input block or execute method to terminate the current loop iteration immediately
|
|
29
|
+
# and skip all subsequent loop iterations. The current cog will be marked as 'skipped',
|
|
30
|
+
# and every subsequent cog in the current iteration will not run. Any async cogs currently running in the
|
|
31
|
+
# current scope will be stopped.
|
|
32
|
+
#
|
|
33
|
+
# If this exception is raised outside of a loop (e.g, via the `call` cog, or in the top-level executor),
|
|
34
|
+
# this exception will just terminate that executor as described above without starting a 'next' iteration.
|
|
35
|
+
#
|
|
36
|
+
# If this exception is raised inside a `map`, this exception will prevent any subsequent executor invocations
|
|
37
|
+
# within that map and will stop any async invocations running in parallel.
|
|
38
|
+
class Break < Base; end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -5,23 +5,61 @@ module Roast
|
|
|
5
5
|
module DSL
|
|
6
6
|
# Context in which the `execute` block of a workflow is evaluated
|
|
7
7
|
class ExecutionManager
|
|
8
|
+
include SystemCogs::Call::Manager
|
|
9
|
+
include SystemCogs::Map::Manager
|
|
10
|
+
include SystemCogs::Repeat::Manager
|
|
11
|
+
|
|
8
12
|
class ExecutionManagerError < Roast::Error; end
|
|
13
|
+
|
|
9
14
|
class ExecutionManagerNotPreparedError < ExecutionManagerError; end
|
|
15
|
+
|
|
10
16
|
class ExecutionManagerAlreadyPreparedError < ExecutionManagerError; end
|
|
17
|
+
|
|
11
18
|
class ExecutionManagerCurrentlyRunningError < ExecutionManagerError; end
|
|
19
|
+
|
|
12
20
|
class ExecutionScopeDoesNotExistError < ExecutionManagerError; end
|
|
21
|
+
|
|
13
22
|
class ExecutionScopeNotSpecifiedError < ExecutionManagerError; end
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
class IllegalCogNameError < ExecutionManagerError; end
|
|
25
|
+
|
|
26
|
+
class OutputsAlreadyDefinedError < ExecutionManagerError; end
|
|
27
|
+
|
|
28
|
+
#: untyped
|
|
29
|
+
attr_reader :final_output
|
|
30
|
+
|
|
31
|
+
#: (
|
|
32
|
+
#| Cog::Registry,
|
|
33
|
+
#| ConfigManager,
|
|
34
|
+
#| Hash[Symbol?, Array[^() -> void]],
|
|
35
|
+
#| WorkflowContext,
|
|
36
|
+
#| ?scope: Symbol?,
|
|
37
|
+
#| ?scope_value: untyped?,
|
|
38
|
+
#| ?scope_index: Integer
|
|
39
|
+
#| ) -> void
|
|
40
|
+
def initialize(
|
|
41
|
+
cog_registry,
|
|
42
|
+
config_manager,
|
|
43
|
+
all_execution_procs,
|
|
44
|
+
workflow_context,
|
|
45
|
+
scope: nil,
|
|
46
|
+
scope_value: nil,
|
|
47
|
+
scope_index: 0
|
|
48
|
+
)
|
|
17
49
|
@cog_registry = cog_registry
|
|
18
50
|
@config_manager = config_manager
|
|
19
51
|
@all_execution_procs = all_execution_procs
|
|
52
|
+
@workflow_context = workflow_context
|
|
20
53
|
@scope = scope
|
|
54
|
+
@scope_value = scope_value
|
|
55
|
+
@scope_index = scope_index
|
|
21
56
|
@cogs = Cog::Store.new #: Cog::Store
|
|
22
57
|
@cog_stack = Cog::Stack.new #: Cog::Stack
|
|
23
58
|
@execution_context = ExecutionContext.new #: ExecutionContext
|
|
24
|
-
@cog_input_manager = CogInputManager.new(@cog_registry, @cogs) #: CogInputManager
|
|
59
|
+
@cog_input_manager = CogInputManager.new(@cog_registry, @cogs, @workflow_context) #: CogInputManager
|
|
60
|
+
@barrier = Async::Barrier.new #: Async::Barrier
|
|
61
|
+
@final_output = nil #: untyped
|
|
62
|
+
@final_output_computed = false #: bool
|
|
25
63
|
end
|
|
26
64
|
|
|
27
65
|
#: () -> void
|
|
@@ -29,6 +67,7 @@ module Roast
|
|
|
29
67
|
raise ExecutionManagerAlreadyPreparedError if preparing? || prepared?
|
|
30
68
|
|
|
31
69
|
@preparing = true
|
|
70
|
+
bind_outputs
|
|
32
71
|
bind_registered_cogs
|
|
33
72
|
my_execution_procs.each { |ep| @execution_context.instance_eval(&ep) }
|
|
34
73
|
@prepared = true
|
|
@@ -39,13 +78,33 @@ module Roast
|
|
|
39
78
|
raise ExecutionManagerCurrentlyRunningError if running?
|
|
40
79
|
|
|
41
80
|
@running = true
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
81
|
+
Sync do |sync_task|
|
|
82
|
+
sync_task.annotate("ExecutionManager #{@scope}")
|
|
83
|
+
@cog_stack.each do |cog|
|
|
84
|
+
cog_config = @config_manager.config_for(cog.class, cog.name)
|
|
85
|
+
cog_task = cog.run!(
|
|
86
|
+
@barrier,
|
|
87
|
+
cog_config.deep_dup,
|
|
88
|
+
cog_input_context,
|
|
89
|
+
@scope_value.deep_dup,
|
|
90
|
+
@scope_index,
|
|
91
|
+
)
|
|
92
|
+
cog_task.wait unless cog_config.async?
|
|
93
|
+
end
|
|
94
|
+
# Wait on the tasks in their completion order, so that an exception in a task will be raised as soon as it occurs
|
|
95
|
+
# noinspection RubyArgCount
|
|
96
|
+
@barrier.wait { |task| wait_for_task_with_exception_handling(task) }
|
|
97
|
+
compute_final_output # eagerly compute the final output (so it, too, can 'break!' subsequent executions in a loop)
|
|
98
|
+
ensure
|
|
99
|
+
@barrier.stop
|
|
100
|
+
compute_final_output
|
|
101
|
+
@running = false
|
|
47
102
|
end
|
|
48
|
-
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
#: () -> void
|
|
106
|
+
def stop!
|
|
107
|
+
@barrier.stop
|
|
49
108
|
end
|
|
50
109
|
|
|
51
110
|
#: () -> bool
|
|
@@ -64,7 +123,7 @@ module Roast
|
|
|
64
123
|
end
|
|
65
124
|
|
|
66
125
|
#: () -> CogInputContext
|
|
67
|
-
def
|
|
126
|
+
def cog_input_context
|
|
68
127
|
raise ExecutionManagerNotPreparedError unless prepared?
|
|
69
128
|
|
|
70
129
|
@cog_input_manager.context
|
|
@@ -72,17 +131,32 @@ module Roast
|
|
|
72
131
|
|
|
73
132
|
private
|
|
74
133
|
|
|
134
|
+
#: (Async::Task) -> void
|
|
135
|
+
def wait_for_task_with_exception_handling(task)
|
|
136
|
+
task.wait
|
|
137
|
+
rescue ControlFlow::Next
|
|
138
|
+
# TODO: do something with the message passed to next!
|
|
139
|
+
@barrier.stop
|
|
140
|
+
rescue ControlFlow::Break => e
|
|
141
|
+
@barrier.stop
|
|
142
|
+
compute_final_output # make sure the final output is always computed, even if the iteration is broken
|
|
143
|
+
raise e
|
|
144
|
+
rescue StandardError => e
|
|
145
|
+
@barrier.stop
|
|
146
|
+
raise e
|
|
147
|
+
end
|
|
148
|
+
|
|
75
149
|
#: () -> Array[^() -> void]
|
|
76
150
|
def my_execution_procs
|
|
77
|
-
raise ExecutionScopeDoesNotExistError unless @all_execution_procs.key?(@scope)
|
|
151
|
+
raise ExecutionScopeDoesNotExistError, @scope unless @all_execution_procs.key?(@scope)
|
|
78
152
|
|
|
79
153
|
@all_execution_procs[@scope] || []
|
|
80
154
|
end
|
|
81
155
|
|
|
82
|
-
#: (
|
|
83
|
-
def add_cog_instance(
|
|
84
|
-
@cogs.insert(
|
|
85
|
-
@cog_stack.push(
|
|
156
|
+
#: (Cog) -> void
|
|
157
|
+
def add_cog_instance(cog)
|
|
158
|
+
@cogs.insert(cog)
|
|
159
|
+
@cog_stack.push(cog)
|
|
86
160
|
end
|
|
87
161
|
|
|
88
162
|
# TODO: add typing for output
|
|
@@ -99,38 +173,94 @@ module Roast
|
|
|
99
173
|
#: (Symbol, singleton(Cog)) -> void
|
|
100
174
|
def bind_cog(cog_method_name, cog_class)
|
|
101
175
|
on_execute_method = method(:on_execute)
|
|
102
|
-
cog_method = proc do
|
|
103
|
-
on_execute_method.call(cog_class,
|
|
176
|
+
cog_method = proc do |*args, **kwargs, &cog_input_proc|
|
|
177
|
+
on_execute_method.call(cog_class, args, kwargs, cog_input_proc)
|
|
104
178
|
end
|
|
105
179
|
@execution_context.instance_eval do
|
|
180
|
+
raise IllegalCogNameError, cog_method_name if respond_to?(cog_method_name, true)
|
|
181
|
+
|
|
106
182
|
define_singleton_method(cog_method_name, cog_method)
|
|
107
183
|
end
|
|
108
184
|
end
|
|
109
185
|
|
|
110
|
-
#: (singleton(Cog), Symbol
|
|
111
|
-
def on_execute(cog_class,
|
|
186
|
+
#: (singleton(Cog), Array[untyped], Hash[Symbol, untyped], ^(Cog::Input) -> untyped) -> void
|
|
187
|
+
def on_execute(cog_class, cog_args, cog_kwargs, cog_input_proc)
|
|
112
188
|
# Called when the cog method is invoked in the workflow's 'execute' block.
|
|
113
189
|
# This creates the cog instance and prepares it for execution.
|
|
114
|
-
|
|
115
|
-
|
|
190
|
+
if cog_class <= SystemCog
|
|
191
|
+
untyped_cog_class = cog_class #: as untyped // to remove warning about splats of unknown length
|
|
192
|
+
cog_params = untyped_cog_class.params_class.new(*cog_args, **cog_kwargs)
|
|
193
|
+
cog_instance = if cog_class == SystemCogs::Call
|
|
194
|
+
create_call_system_cog(cog_params, cog_input_proc)
|
|
195
|
+
elsif cog_class == SystemCogs::Map
|
|
196
|
+
create_map_system_cog(cog_params, cog_input_proc)
|
|
197
|
+
elsif cog_class == SystemCogs::Repeat
|
|
198
|
+
create_repeat_system_cog(cog_params, cog_input_proc)
|
|
199
|
+
else
|
|
200
|
+
raise NotImplementedError, "No system cog manager defined for #{cog_class}"
|
|
201
|
+
end
|
|
116
202
|
else
|
|
117
|
-
|
|
203
|
+
cog_name = Array.wrap(cog_args).shift || Cog.generate_fallback_name
|
|
204
|
+
cog_instance = cog_class.new(cog_name, cog_input_proc)
|
|
118
205
|
end
|
|
119
|
-
add_cog_instance(
|
|
206
|
+
add_cog_instance(cog_instance)
|
|
120
207
|
end
|
|
121
208
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
209
|
+
def bind_outputs
|
|
210
|
+
on_outputs_method = method(:on_outputs)
|
|
211
|
+
on_outputs_bang_method = method(:on_outputs!)
|
|
212
|
+
method_to_bind = proc { |&outputs_proc| on_outputs_method.call(outputs_proc) }
|
|
213
|
+
bang_method_to_bind = proc { |&outputs_proc| on_outputs_bang_method.call(outputs_proc) }
|
|
214
|
+
@execution_context.instance_eval do
|
|
215
|
+
define_singleton_method(:outputs, method_to_bind)
|
|
216
|
+
define_singleton_method(:outputs!, bang_method_to_bind)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
#: (^(untyped, Integer) -> untyped) -> void
|
|
221
|
+
def on_outputs(outputs)
|
|
222
|
+
raise OutputsAlreadyDefinedError if @outputs || @outputs_bang
|
|
223
|
+
|
|
224
|
+
@outputs = outputs
|
|
225
|
+
end
|
|
126
226
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
227
|
+
#: (^(untyped, Integer) -> untyped) -> void
|
|
228
|
+
def on_outputs!(outputs)
|
|
229
|
+
raise OutputsAlreadyDefinedError if @outputs || @outputs_bang
|
|
230
|
+
|
|
231
|
+
@outputs_bang = outputs
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
#: () -> untyped
|
|
235
|
+
def compute_final_output
|
|
236
|
+
return if @final_output_computed
|
|
237
|
+
|
|
238
|
+
@final_output_computed = true
|
|
239
|
+
outputs_proc = @outputs_bang || @outputs
|
|
240
|
+
|
|
241
|
+
@final_output = if outputs_proc
|
|
242
|
+
@cog_input_manager.context.instance_exec(@scope_value, @scope_index, &outputs_proc)
|
|
243
|
+
else
|
|
244
|
+
last_cog_name = @cog_stack.last&.name
|
|
245
|
+
raise CogInputManager::CogDoesNotExistError, "no cogs defined in scope" unless last_cog_name
|
|
130
246
|
|
|
131
|
-
|
|
247
|
+
@cog_input_manager.send(:cog_output, last_cog_name)
|
|
132
248
|
end
|
|
133
|
-
|
|
249
|
+
rescue ControlFlow::SkipCog, ControlFlow::Next
|
|
250
|
+
# TODO: do something with the message passed to the control flow statement
|
|
251
|
+
# Swallow skip! and next! control flow statements in the outputs block
|
|
252
|
+
# Calling these will just make the final output `nil`.
|
|
253
|
+
# (As will calling `break!`, but it gets handled elsewhere.)
|
|
254
|
+
# Calling `fail!` inside `outputs` should actually raise an exception.
|
|
255
|
+
rescue CogInputManager::CogNotYetRunError, CogInputManager::CogSkippedError, CogInputManager::CogStoppedError => e
|
|
256
|
+
# Attempting to accessing a cog that was skipped, stopped, or did not run from inside an `outputs` block
|
|
257
|
+
# is more likely to happen when the user `break!`s from a loop. Allowing this access not to result in an
|
|
258
|
+
# exception getting raised immediately will reduce boilerplate code needed to check if the loop was broken
|
|
259
|
+
# and return nil or some fallback value if it was, and the normal outputs value otherwise.
|
|
260
|
+
#
|
|
261
|
+
# Using `outputs` to define the scope's outputs will swallow these exceptions.
|
|
262
|
+
# Using `outputs!` instead will cause the exceptions to be raised.
|
|
263
|
+
raise e if @outputs_bang.present?
|
|
134
264
|
end
|
|
135
265
|
end
|
|
136
266
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Kernel
|
|
5
|
+
#: -> self
|
|
6
|
+
def not_nil!
|
|
7
|
+
self
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class NilClass
|
|
12
|
+
# @override
|
|
13
|
+
#: -> bot
|
|
14
|
+
def not_nil!
|
|
15
|
+
raise UnexpectedNilError
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class UnexpectedNilError < StandardError
|
|
20
|
+
def initialize(message = "Unexpected nil value encountered.")
|
|
21
|
+
super(message)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module DSL
|
|
6
|
+
class SystemCog
|
|
7
|
+
# Parameters for system cogs set at workflow evaluation time
|
|
8
|
+
#
|
|
9
|
+
# Params are used to provide limited evaluation-time parameterization to system cogs,
|
|
10
|
+
# such as the name of the execute scope to be invoked by a `call`, `map`, a, `repeat` cog.
|
|
11
|
+
#
|
|
12
|
+
# System cogs also accept input at execution time, just like regular cogs.
|
|
13
|
+
class Params
|
|
14
|
+
# The name identifier for this system cog instance
|
|
15
|
+
#
|
|
16
|
+
# Used to reference this cog's output. Auto-generated if not provided.
|
|
17
|
+
#
|
|
18
|
+
#: Symbol
|
|
19
|
+
attr_reader :name
|
|
20
|
+
|
|
21
|
+
# Initialize parameters with the cog name
|
|
22
|
+
#
|
|
23
|
+
# Subclasses should define their own `initialize` accepting specific parameters.
|
|
24
|
+
#
|
|
25
|
+
#: (Symbol?) -> void
|
|
26
|
+
def initialize(name)
|
|
27
|
+
@name = name || Cog.generate_fallback_name
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|