roast-ai 0.4.9 → 0.5.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 (194) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/docs/write-comments.md +36 -0
  3. data/.github/CODEOWNERS +1 -1
  4. data/.github/workflows/ci.yaml +10 -6
  5. data/.gitignore +0 -1
  6. data/.rubocop.yml +7 -1
  7. data/CLAUDE.md +2 -2
  8. data/CONTRIBUTING.md +2 -0
  9. data/Gemfile +18 -18
  10. data/Gemfile.lock +46 -57
  11. data/README.md +118 -1432
  12. data/README_LEGACY.md +1464 -0
  13. data/Rakefile +39 -4
  14. data/dev.yml +29 -0
  15. data/dsl/agent_sessions.rb +20 -0
  16. data/dsl/async_cogs.rb +49 -0
  17. data/dsl/async_cogs_complex.rb +67 -0
  18. data/dsl/call.rb +44 -0
  19. data/dsl/collect_from.rb +72 -0
  20. data/dsl/demo/Gemfile +4 -0
  21. data/dsl/demo/Gemfile.lock +120 -0
  22. data/dsl/demo/cogs/local.rb +15 -0
  23. data/dsl/demo/simple_external_cog.rb +17 -0
  24. data/dsl/json_output.rb +28 -0
  25. data/dsl/map.rb +55 -0
  26. data/dsl/map_reduce.rb +37 -0
  27. data/dsl/map_with_index.rb +49 -0
  28. data/dsl/next_break.rb +40 -0
  29. data/dsl/next_break_parallel.rb +44 -0
  30. data/dsl/outputs.rb +39 -0
  31. data/dsl/outputs_bang.rb +36 -0
  32. data/dsl/parallel_map.rb +37 -0
  33. data/dsl/plugin-gem-example/.gitignore +8 -0
  34. data/dsl/plugin-gem-example/Gemfile +13 -0
  35. data/dsl/plugin-gem-example/Gemfile.lock +178 -0
  36. data/dsl/plugin-gem-example/lib/other.rb +17 -0
  37. data/dsl/plugin-gem-example/lib/plugin_gem_example.rb +5 -0
  38. data/dsl/plugin-gem-example/lib/simple.rb +15 -0
  39. data/dsl/plugin-gem-example/lib/version.rb +10 -0
  40. data/dsl/plugin-gem-example/plugin-gem-example.gemspec +28 -0
  41. data/dsl/prompts/simple_prompt.md.erb +3 -0
  42. data/dsl/prototype.rb +10 -4
  43. data/dsl/repeat_loop_results.rb +53 -0
  44. data/dsl/ruby_cog.rb +72 -0
  45. data/dsl/simple_agent.rb +18 -0
  46. data/dsl/simple_chat.rb +26 -0
  47. data/dsl/simple_repeat.rb +29 -0
  48. data/dsl/skip.rb +36 -0
  49. data/dsl/step_communication.rb +10 -5
  50. data/dsl/targets_and_params.rb +57 -0
  51. data/dsl/temperature.rb +17 -0
  52. data/dsl/temporary_directory.rb +22 -0
  53. data/dsl/tutorial/01_your_first_workflow/README.md +179 -0
  54. data/dsl/tutorial/01_your_first_workflow/configured_chat.rb +33 -0
  55. data/dsl/tutorial/01_your_first_workflow/hello.rb +23 -0
  56. data/dsl/tutorial/02_chaining_cogs/README.md +310 -0
  57. data/dsl/tutorial/02_chaining_cogs/code_review.rb +104 -0
  58. data/dsl/tutorial/02_chaining_cogs/session_resumption.rb +92 -0
  59. data/dsl/tutorial/02_chaining_cogs/simple_chain.rb +84 -0
  60. data/dsl/tutorial/03_targets_and_params/README.md +230 -0
  61. data/dsl/tutorial/03_targets_and_params/multiple_targets.rb +65 -0
  62. data/dsl/tutorial/03_targets_and_params/single_target.rb +65 -0
  63. data/dsl/tutorial/04_configuration_options/README.md +209 -0
  64. data/dsl/tutorial/04_configuration_options/control_display_and_temperature.rb +104 -0
  65. data/dsl/tutorial/04_configuration_options/simple_config.rb +68 -0
  66. data/dsl/tutorial/05_control_flow/README.md +156 -0
  67. data/dsl/tutorial/05_control_flow/conditional_execution.rb +62 -0
  68. data/dsl/tutorial/05_control_flow/handling_failures.rb +77 -0
  69. data/dsl/tutorial/06_reusable_scopes/README.md +172 -0
  70. data/dsl/tutorial/06_reusable_scopes/accessing_scope_outputs.rb +126 -0
  71. data/dsl/tutorial/06_reusable_scopes/basic_scope.rb +63 -0
  72. data/dsl/tutorial/06_reusable_scopes/parameterized_scope.rb +78 -0
  73. data/dsl/tutorial/07_processing_collections/README.md +152 -0
  74. data/dsl/tutorial/07_processing_collections/basic_map.rb +70 -0
  75. data/dsl/tutorial/07_processing_collections/parallel_map.rb +74 -0
  76. data/dsl/tutorial/08_iterative_workflows/README.md +231 -0
  77. data/dsl/tutorial/08_iterative_workflows/basic_repeat.rb +57 -0
  78. data/dsl/tutorial/08_iterative_workflows/conditional_break.rb +57 -0
  79. data/dsl/tutorial/09_async_cogs/README.md +197 -0
  80. data/dsl/tutorial/09_async_cogs/basic_async.rb +38 -0
  81. data/dsl/tutorial/README.md +222 -0
  82. data/dsl/working_directory.rb +16 -0
  83. data/exe/roast +1 -1
  84. data/internal/documentation/architectural-notes.md +115 -0
  85. data/internal/documentation/doc-comments-external.md +686 -0
  86. data/internal/documentation/doc-comments-internal.md +342 -0
  87. data/internal/documentation/doc-comments.md +211 -0
  88. data/lib/roast/dsl/cog/config.rb +280 -4
  89. data/lib/roast/dsl/cog/input.rb +73 -0
  90. data/lib/roast/dsl/cog/output.rb +313 -0
  91. data/lib/roast/dsl/cog/registry.rb +71 -0
  92. data/lib/roast/dsl/cog/stack.rb +3 -2
  93. data/lib/roast/dsl/cog/store.rb +11 -8
  94. data/lib/roast/dsl/cog.rb +108 -31
  95. data/lib/roast/dsl/cog_input_context.rb +44 -0
  96. data/lib/roast/dsl/cog_input_manager.rb +156 -0
  97. data/lib/roast/dsl/cogs/agent/config.rb +465 -0
  98. data/lib/roast/dsl/cogs/agent/input.rb +81 -0
  99. data/lib/roast/dsl/cogs/agent/output.rb +59 -0
  100. data/lib/roast/dsl/cogs/agent/provider.rb +51 -0
  101. data/lib/roast/dsl/cogs/agent/providers/claude/claude_invocation.rb +185 -0
  102. data/lib/roast/dsl/cogs/agent/providers/claude/message.rb +73 -0
  103. data/lib/roast/dsl/cogs/agent/providers/claude/messages/assistant_message.rb +36 -0
  104. data/lib/roast/dsl/cogs/agent/providers/claude/messages/result_message.rb +61 -0
  105. data/lib/roast/dsl/cogs/agent/providers/claude/messages/system_message.rb +47 -0
  106. data/lib/roast/dsl/cogs/agent/providers/claude/messages/text_message.rb +36 -0
  107. data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_result_message.rb +47 -0
  108. data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_use_message.rb +46 -0
  109. data/lib/roast/dsl/cogs/agent/providers/claude/messages/unknown_message.rb +27 -0
  110. data/lib/roast/dsl/cogs/agent/providers/claude/messages/user_message.rb +37 -0
  111. data/lib/roast/dsl/cogs/agent/providers/claude/tool_result.rb +51 -0
  112. data/lib/roast/dsl/cogs/agent/providers/claude/tool_use.rb +48 -0
  113. data/lib/roast/dsl/cogs/agent/providers/claude.rb +31 -0
  114. data/lib/roast/dsl/cogs/agent/stats.rb +92 -0
  115. data/lib/roast/dsl/cogs/agent/usage.rb +62 -0
  116. data/lib/roast/dsl/cogs/agent.rb +75 -0
  117. data/lib/roast/dsl/cogs/chat/config.rb +453 -0
  118. data/lib/roast/dsl/cogs/chat/input.rb +92 -0
  119. data/lib/roast/dsl/cogs/chat/output.rb +64 -0
  120. data/lib/roast/dsl/cogs/chat/session.rb +68 -0
  121. data/lib/roast/dsl/cogs/chat.rb +81 -0
  122. data/lib/roast/dsl/cogs/cmd.rb +291 -27
  123. data/lib/roast/dsl/cogs/ruby.rb +171 -0
  124. data/lib/roast/dsl/command_runner.rb +191 -0
  125. data/lib/roast/dsl/config_context.rb +2 -47
  126. data/lib/roast/dsl/config_manager.rb +143 -0
  127. data/lib/roast/dsl/control_flow.rb +41 -0
  128. data/lib/roast/dsl/execution_context.rb +9 -0
  129. data/lib/roast/dsl/execution_manager.rb +267 -0
  130. data/lib/roast/dsl/nil_assertions.rb +23 -0
  131. data/lib/roast/dsl/system_cog/params.rb +32 -0
  132. data/lib/roast/dsl/system_cog.rb +36 -0
  133. data/lib/roast/dsl/system_cogs/call.rb +162 -0
  134. data/lib/roast/dsl/system_cogs/map.rb +448 -0
  135. data/lib/roast/dsl/system_cogs/repeat.rb +242 -0
  136. data/lib/roast/dsl/workflow.rb +123 -0
  137. data/lib/roast/dsl/workflow_context.rb +20 -0
  138. data/lib/roast/dsl/workflow_params.rb +24 -0
  139. data/lib/roast/sorbet_runtime_stub.rb +154 -0
  140. data/lib/roast/tools/apply_diff.rb +1 -3
  141. data/lib/roast/tools/cmd.rb +4 -3
  142. data/lib/roast/tools/read_file.rb +1 -1
  143. data/lib/roast/tools/update_files.rb +1 -1
  144. data/lib/roast/tools/write_file.rb +1 -1
  145. data/lib/roast/version.rb +1 -1
  146. data/lib/roast/workflow/base_workflow.rb +4 -0
  147. data/lib/roast/workflow/step_loader.rb +14 -2
  148. data/lib/roast-ai.rb +4 -0
  149. data/lib/roast.rb +60 -22
  150. data/{roast.gemspec → roast-ai.gemspec} +10 -13
  151. data/sorbet/config +1 -0
  152. data/sorbet/rbi/gems/async@2.34.0.rbi +1577 -0
  153. data/sorbet/rbi/gems/cli-kit@5.2.0.rbi +2063 -0
  154. data/sorbet/rbi/gems/{cli-ui@2.3.0.rbi → cli-ui@2.7.0-6bdefd1d06305e5d6ae312ac76f9c88f88658dda.rbi} +1418 -1013
  155. data/sorbet/rbi/gems/console@1.34.2.rbi +1193 -0
  156. data/sorbet/rbi/gems/fiber-annotation@0.2.0.rbi +50 -0
  157. data/sorbet/rbi/gems/fiber-local@1.1.0.rbi +35 -0
  158. data/sorbet/rbi/gems/fiber-storage@1.0.1.rbi +41 -0
  159. data/sorbet/rbi/gems/io-event@1.14.0.rbi +724 -0
  160. data/sorbet/rbi/gems/marcel@1.1.0.rbi +239 -0
  161. data/sorbet/rbi/gems/metrics@0.15.0.rbi +9 -0
  162. data/sorbet/rbi/gems/ruby_llm@1.8.2.rbi +5703 -0
  163. data/sorbet/rbi/gems/traces@0.18.2.rbi +9 -0
  164. data/sorbet/rbi/shims/lib/roast/dsl/cog_input_context.rbi +1197 -0
  165. data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +314 -2
  166. data/sorbet/rbi/shims/lib/roast/dsl/execution_context.rbi +498 -0
  167. data/sorbet/tapioca/config.yml +6 -0
  168. data/sorbet/tapioca/require.rb +2 -0
  169. metadata +198 -34
  170. data/dsl/less_simple.rb +0 -112
  171. data/dsl/simple.rb +0 -8
  172. data/lib/roast/dsl/cog_execution_context.rb +0 -29
  173. data/lib/roast/dsl/cogs/graph.rb +0 -53
  174. data/lib/roast/dsl/cogs.rb +0 -65
  175. data/lib/roast/dsl/executor.rb +0 -82
  176. data/lib/roast/dsl/workflow_execution_context.rb +0 -47
  177. data/sorbet/rbi/gems/cgi@0.5.0.rbi +0 -2961
  178. data/sorbet/rbi/gems/claude_swarm@0.1.19.rbi +0 -568
  179. data/sorbet/rbi/gems/cli-kit@5.0.1.rbi +0 -1991
  180. data/sorbet/rbi/gems/dry-configurable@1.3.0.rbi +0 -672
  181. data/sorbet/rbi/gems/dry-core@1.1.0.rbi +0 -1894
  182. data/sorbet/rbi/gems/dry-inflector@1.2.0.rbi +0 -659
  183. data/sorbet/rbi/gems/dry-initializer@3.2.0.rbi +0 -781
  184. data/sorbet/rbi/gems/dry-logic@1.6.0.rbi +0 -1127
  185. data/sorbet/rbi/gems/dry-schema@1.14.1.rbi +0 -3727
  186. data/sorbet/rbi/gems/dry-types@1.8.3.rbi +0 -3969
  187. data/sorbet/rbi/gems/fast-mcp-annotations@1.5.3.rbi +0 -1588
  188. data/sorbet/rbi/gems/mime-types-data@3.2025.0617.rbi +0 -136
  189. data/sorbet/rbi/gems/mime-types@3.7.0.rbi +0 -1342
  190. data/sorbet/rbi/gems/rack@2.2.18.rbi +0 -5659
  191. data/sorbet/rbi/gems/rbs-inline@0.12.0.rbi +0 -2170
  192. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
  193. data/sorbet/rbi/gems/yard@0.9.37.rbi +0 -18492
  194. data/sorbet/rbi/shims/lib/roast/dsl/workflow_execution_context.rbi +0 -11
@@ -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,
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
@@ -3,52 +3,7 @@
3
3
 
4
4
  module Roast
5
5
  module DSL
6
- class ConfigContext
7
- def initialize(cogs, config_proc)
8
- @cogs = cogs
9
- @executor_scoped_configs = {}
10
- @cog_scoped_configs = {}
11
- @config_proc = config_proc
12
- end
13
-
14
- def fetch_merged_config(cog_class, name = nil)
15
- # All configs have an entry, even if it's empty.
16
- configs = fetch_execution_scope(cog_class)
17
- instance_configs = fetch_cog_config(cog_class, name) unless name.nil?
18
- configs = configs.merge(instance_configs) if instance_configs
19
- configs
20
- end
21
-
22
- def prepare!
23
- bind_default_cogs
24
- instance_eval(&@config_proc)
25
- end
26
-
27
- #: () -> void
28
- def bind_default_cogs
29
- bind_cog(Cogs::Cmd, :cmd)
30
- end
31
-
32
- def fetch_cog_config(cog_class, name)
33
- @cog_scoped_configs[cog_class][name]
34
- end
35
-
36
- def fetch_or_create_cog_config(cog_class, name)
37
- @cog_scoped_configs[cog_class][name] = cog_class.config_class.new unless @cog_scoped_configs.key?(name)
38
- @cog_scoped_configs[cog_class][name]
39
- end
40
-
41
- def fetch_execution_scope(cog_class)
42
- @executor_scoped_configs[cog_class]
43
- end
44
-
45
- def bind_cog(cog_class, method_name)
46
- @cog_scoped_configs[cog_class] = {}
47
- @executor_scoped_configs[cog_class] = cog_class.config_class.new
48
- instance_eval do
49
- define_singleton_method(method_name, &cog_class.on_config)
50
- end
51
- end
52
- end
6
+ # Context in which the `config` blocks of a workflow definition are evaluated
7
+ class ConfigContext; end
53
8
  end
54
9
  end
@@ -0,0 +1,143 @@
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
+ class IllegalCogNameError < ConfigManagerError; end
11
+
12
+ #: (Cog::Registry, Array[^() -> void]) -> void
13
+ def initialize(cog_registry, config_procs)
14
+ @cog_registry = cog_registry
15
+ @config_procs = config_procs
16
+ @config_context = ConfigContext.new #: ConfigContext
17
+ @global_config = Cog::Config.new #: Cog::Config
18
+ @general_configs = {} #: Hash[singleton(Cog), Cog::Config]
19
+ @regexp_scoped_configs = {} #: Hash[singleton(Cog), Hash[Regexp, Cog::Config]]
20
+ @name_scoped_configs = {} #: Hash[singleton(Cog), Hash[Symbol, Cog::Config]]
21
+ end
22
+
23
+ #: () -> void
24
+ def prepare!
25
+ raise ConfigManagerAlreadyPreparedError if preparing? || prepared?
26
+
27
+ @preparing = true
28
+ bind_global
29
+ bind_registered_cogs
30
+ @config_procs.each { |cp| @config_context.instance_eval(&cp) }
31
+ @prepared = true
32
+ end
33
+
34
+ #: () -> bool
35
+ def preparing?
36
+ @preparing ||= false
37
+ end
38
+
39
+ #: () -> bool
40
+ def prepared?
41
+ @prepared ||= false
42
+ end
43
+
44
+ #: (singleton(Cog), ?Symbol?) -> Cog::Config
45
+ def config_for(cog_class, name = nil)
46
+ raise ConfigManagerNotPreparedError unless prepared?
47
+
48
+ # All cogs will always have a config; empty by default if the cog was never explicitly configured
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!
59
+ config
60
+ end
61
+
62
+ private
63
+
64
+ #: (singleton(Cog)) -> Cog::Config
65
+ def fetch_general_config(cog_class)
66
+ @general_configs[cog_class] ||= cog_class.config_class.new
67
+ end
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
+
75
+ #: (singleton(Cog), Symbol) -> Cog::Config
76
+ def fetch_name_scoped_config(cog_class, name)
77
+ name_scoped_configs_for_cog = @name_scoped_configs[cog_class] ||= {}
78
+ name_scoped_configs_for_cog[name] ||= cog_class.config_class.new
79
+ end
80
+
81
+ #: () -> void
82
+ def bind_registered_cogs
83
+ @cog_registry.cogs.each { |cog_method_name, cog_class| bind_cog(cog_method_name, cog_class) }
84
+ end
85
+
86
+ #: (Symbol, singleton(Cog)) -> void
87
+ def bind_cog(cog_method_name, cog_class)
88
+ on_config_method = method(:on_config)
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)
91
+ end
92
+ @config_context.instance_eval do
93
+ raise IllegalCogNameError, cog_method_name if respond_to?(cog_method_name, true)
94
+
95
+ define_singleton_method(cog_method_name, cog_method)
96
+ end
97
+ end
98
+
99
+ #: (singleton(Cog), (Symbol | Regexp)?, ^() -> void ) -> void
100
+ def on_config(cog_class, cog_name_or_pattern, cog_config_proc)
101
+ # Called when the cog method is invoked in the workflow's 'config' block.
102
+ # This allows configuration parameters to be set for the cog generally or for a specific named instance
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
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)
114
+ else
115
+ raise ArgumentError, "Invalid type '#{cog_name_or_pattern.class}' for cog_name_or_pattern"
116
+ end
117
+
118
+ # NOTE: Sorbet expects the proc passed to instance_exec to be declared as taking an argument
119
+ # but our cog_config_proc does not get an argument
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
140
+ end
141
+ end
142
+ end
143
+ 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
@@ -0,0 +1,9 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ # Context in which the `execute` blocks of a workflow definition are evaluated
7
+ class ExecutionContext; end
8
+ end
9
+ end