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,267 @@
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
+ include SystemCogs::Call::Manager
9
+ include SystemCogs::Map::Manager
10
+ include SystemCogs::Repeat::Manager
11
+
12
+ class ExecutionManagerError < Roast::Error; end
13
+
14
+ class ExecutionManagerNotPreparedError < ExecutionManagerError; end
15
+
16
+ class ExecutionManagerAlreadyPreparedError < ExecutionManagerError; end
17
+
18
+ class ExecutionManagerCurrentlyRunningError < ExecutionManagerError; end
19
+
20
+ class ExecutionScopeDoesNotExistError < ExecutionManagerError; end
21
+
22
+ class ExecutionScopeNotSpecifiedError < ExecutionManagerError; end
23
+
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
+ )
49
+ @cog_registry = cog_registry
50
+ @config_manager = config_manager
51
+ @all_execution_procs = all_execution_procs
52
+ @workflow_context = workflow_context
53
+ @scope = scope
54
+ @scope_value = scope_value
55
+ @scope_index = scope_index
56
+ @cogs = Cog::Store.new #: Cog::Store
57
+ @cog_stack = Cog::Stack.new #: Cog::Stack
58
+ @execution_context = ExecutionContext.new #: ExecutionContext
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
63
+ end
64
+
65
+ #: () -> void
66
+ def prepare!
67
+ raise ExecutionManagerAlreadyPreparedError if preparing? || prepared?
68
+
69
+ @preparing = true
70
+ bind_outputs
71
+ bind_registered_cogs
72
+ my_execution_procs.each { |ep| @execution_context.instance_eval(&ep) }
73
+ @prepared = true
74
+ end
75
+
76
+ def run!
77
+ raise ExecutionManagerNotPreparedError unless prepared?
78
+ raise ExecutionManagerCurrentlyRunningError if running?
79
+
80
+ @running = true
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
102
+ end
103
+ end
104
+
105
+ #: () -> void
106
+ def stop!
107
+ @barrier.stop
108
+ end
109
+
110
+ #: () -> bool
111
+ def preparing?
112
+ @preparing ||= false
113
+ end
114
+
115
+ #: () -> bool
116
+ def prepared?
117
+ @prepared ||= false
118
+ end
119
+
120
+ #: () -> bool
121
+ def running?
122
+ @running ||= false
123
+ end
124
+
125
+ #: () -> CogInputContext
126
+ def cog_input_context
127
+ raise ExecutionManagerNotPreparedError unless prepared?
128
+
129
+ @cog_input_manager.context
130
+ end
131
+
132
+ private
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
+
149
+ #: () -> Array[^() -> void]
150
+ def my_execution_procs
151
+ raise ExecutionScopeDoesNotExistError, @scope unless @all_execution_procs.key?(@scope)
152
+
153
+ @all_execution_procs[@scope] || []
154
+ end
155
+
156
+ #: (Cog) -> void
157
+ def add_cog_instance(cog)
158
+ @cogs.insert(cog)
159
+ @cog_stack.push(cog)
160
+ end
161
+
162
+ # TODO: add typing for output
163
+ #: (Symbol) -> untyped
164
+ def output(name)
165
+ @cogs[name].output
166
+ end
167
+
168
+ #: () -> void
169
+ def bind_registered_cogs
170
+ @cog_registry.cogs.each { |cog_method_name, cog_class| bind_cog(cog_method_name, cog_class) }
171
+ end
172
+
173
+ #: (Symbol, singleton(Cog)) -> void
174
+ def bind_cog(cog_method_name, cog_class)
175
+ on_execute_method = method(:on_execute)
176
+ cog_method = proc do |*args, **kwargs, &cog_input_proc|
177
+ on_execute_method.call(cog_class, args, kwargs, cog_input_proc)
178
+ end
179
+ @execution_context.instance_eval do
180
+ raise IllegalCogNameError, cog_method_name if respond_to?(cog_method_name, true)
181
+
182
+ define_singleton_method(cog_method_name, cog_method)
183
+ end
184
+ end
185
+
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)
188
+ # Called when the cog method is invoked in the workflow's 'execute' block.
189
+ # This creates the cog instance and prepares it for execution.
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
202
+ else
203
+ cog_name = Array.wrap(cog_args).shift || Cog.generate_fallback_name
204
+ cog_instance = cog_class.new(cog_name, cog_input_proc)
205
+ end
206
+ add_cog_instance(cog_instance)
207
+ end
208
+
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
226
+
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
246
+
247
+ @cog_input_manager.send(:cog_output, last_cog_name)
248
+ end
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?
264
+ end
265
+ end
266
+ end
267
+ 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
@@ -0,0 +1,36 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ class SystemCog < Cog
7
+ class << self
8
+ #: () -> singleton(SystemCog::Params)
9
+ def params_class
10
+ @params_class ||= find_child_params_or_default
11
+ end
12
+
13
+ private
14
+
15
+ #: () -> singleton(SystemCog::Params)
16
+ def find_child_params_or_default
17
+ config_constant = "#{name}::Params"
18
+ const_defined?(config_constant) ? const_get(config_constant) : SystemCog::Params # rubocop:disable Sorbet/ConstantsFromStrings
19
+ end
20
+ end
21
+
22
+ #: (Symbol, ^(Cog::Input) -> untyped) { (Cog::Input, Cog::Config) -> Cog::Output } -> void
23
+ def initialize(name, cog_input_proc, &on_execute)
24
+ super(name, cog_input_proc)
25
+ @on_execute = on_execute
26
+ end
27
+
28
+ #: (Cog::Input) -> Cog::Output
29
+ def execute(input)
30
+ # The `on_execute` callback allows a system cog to pass its execution back to the ExecutionManager
31
+ # for special handling.
32
+ @on_execute.call(input, @config)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,162 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module SystemCogs
7
+ # Call cog for invoking named execution scopes
8
+ #
9
+ # Executes a named execution scope (defined with `execute(:name)`) with a provided value
10
+ # and index. The scope runs independently and can access the value and index through
11
+ # special variables.
12
+ class Call < SystemCog
13
+ class Config < Cog::Config; end
14
+
15
+ # Parameters for the call system cog
16
+ class Params < SystemCog::Params
17
+ # The name of the execution scope to invoke
18
+ #
19
+ #: Symbol
20
+ attr_accessor :run
21
+
22
+ #: (?Symbol?, run: Symbol) -> void
23
+ def initialize(name = nil, run:)
24
+ super(name)
25
+ @run = run
26
+ end
27
+ end
28
+
29
+ # Input for the call system cog
30
+ #
31
+ # Provides the value and index to be passed to the execution scope. The scope
32
+ # can access these through the implicit variables available in the execution context.
33
+ class Input < Cog::Input
34
+ # The value to pass to the execution scope
35
+ #
36
+ # This value becomes available in the called scope and can be accessed by steps
37
+ # within that scope. Required.
38
+ #
39
+ #: untyped
40
+ attr_accessor :value
41
+
42
+ # The index value to pass to the execution scope
43
+ #
44
+ # Defaults to 0. Can be used to track position when calling scopes in a sequence.
45
+ #
46
+ # Integer
47
+ attr_accessor :index
48
+
49
+ #: () -> void
50
+ def initialize
51
+ super
52
+ @index = 0
53
+ end
54
+
55
+ # Validate that required input values are present
56
+ #
57
+ #: () -> void
58
+ def validate!
59
+ raise Cog::Input::InvalidInputError, "'value' is required" if value.nil? && !coerce_ran?
60
+ end
61
+
62
+ # Coerce the input from the return value of the input block
63
+ #
64
+ # Sets the value from the input block's return value if not already set directly.
65
+ #
66
+ def coerce(input_return_value)
67
+ super
68
+ @value = input_return_value unless @value.present?
69
+ end
70
+ end
71
+
72
+ # Output from running the `call` cog
73
+ #
74
+ # Contains the result from the called scope. Use the `from` method to retrieve
75
+ # the final output from the scope's execution.
76
+ #
77
+ # #### See Also
78
+ # - `Roast::DSL::CogInputContext#from` - retrieves output from a `call` cog
79
+ class Output < Cog::Output
80
+ #: (ExecutionManager) -> void
81
+ def initialize(execution_manager)
82
+ super()
83
+ @execution_manager = execution_manager
84
+ end
85
+ end
86
+
87
+ # @requires_ancestor: Roast::DSL::ExecutionManager
88
+ module Manager
89
+ private
90
+
91
+ #: (Params, ^(Cog::Input) -> untyped) -> SystemCogs::Call
92
+ def create_call_system_cog(params, input_proc)
93
+ SystemCogs::Call.new(params.name, input_proc) do |input|
94
+ input = input #: as Input
95
+ raise ExecutionManager::ExecutionScopeNotSpecifiedError unless params.run.present?
96
+
97
+ em = ExecutionManager.new(
98
+ @cog_registry,
99
+ @config_manager,
100
+ @all_execution_procs,
101
+ @workflow_context,
102
+ scope: params.run,
103
+ scope_value: input.value,
104
+ scope_index: input.index,
105
+ )
106
+ em.prepare!
107
+ begin
108
+ em.run!
109
+ rescue ControlFlow::Break
110
+ # treat `break!` like `next!` in a `call` invocation
111
+ # TODO: maybe do something with the message passed to break!
112
+ end
113
+ Output.new(em)
114
+ end
115
+ end
116
+ end
117
+
118
+ # @requires_ancestor: Roast::DSL::CogInputContext
119
+ module InputContext
120
+ # Retrieve the output from a `call` cog's execution scope
121
+ #
122
+ # Extracts the final output from the execution scope that was invoked by the call cog.
123
+ # When called without a block, returns the final output directly. When called with a block,
124
+ # executes the block in the context of the called scope's input context, receiving the final
125
+ # output as an argument.
126
+ #
127
+ # This allows you to access the results of a called scope and optionally transform them
128
+ # and/or access other outputs from within that scope.
129
+ #
130
+ # #### Usage
131
+ # ```ruby
132
+ # # Get the final output directly
133
+ # result = from(call!(:my_call))
134
+ #
135
+ # # Transform the output with a block
136
+ # transformed = from(call!(:my_call)) { |output| output.upcase }
137
+ #
138
+ # # Access other cog outputs from within the called scope
139
+ # inner_result = from(call!(:my_call)) { inner_cog!(:some_step) }
140
+ # ```
141
+ #
142
+ # #### See Also
143
+ # - `Roast::DSL::SystemCogs::Call::Output` - the output type from call cogs
144
+ #
145
+ # @rbs [T] (Roast::DSL::SystemCogs::Call::Output) {(untyped, untyped, Integer) -> T} -> T
146
+ # | (Roast::DSL::SystemCogs::Call::Output) -> untyped
147
+ def from(call_cog_output, &block)
148
+ em = call_cog_output.instance_variable_get(:@execution_manager)
149
+ raise CogInputContext::ContextNotFoundError if em.nil?
150
+
151
+ final_output = em.final_output
152
+ scope_value = em.instance_variable_get(:@scope_value).deep_dup
153
+ scope_index = em.instance_variable_get(:@scope_index)
154
+ return em.cog_input_context.instance_exec(final_output, scope_value, scope_index, &block) if block_given?
155
+
156
+ final_output
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end