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,448 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module SystemCogs
7
+ # Map cog for executing a scope over a collection of items
8
+ #
9
+ # Executes a named execution scope (defined with `execute(:name)`) for each item in a collection.
10
+ # Supports both serial and parallel execution modes. Each iteration receives the current item
11
+ # as its value and the iteration index.
12
+ class Map < SystemCog
13
+ # Parent class for all `map` cog output access errors
14
+ class MapOutputAccessError < Roast::Error; end
15
+
16
+ # Raised when attempting to access an iteration that did not run
17
+ #
18
+ # This can occur when a `break!` is called during iteration, preventing
19
+ # subsequent iterations from executing.
20
+ class MapIterationDidNotRunError < MapOutputAccessError; end
21
+
22
+ # Configuration for the `map` cog
23
+ class Config < Cog::Config
24
+ # Configure the cog to execute iterations in parallel with a maximum number of concurrent tasks
25
+ #
26
+ # Pass `0` to enable unlimited parallelism (no concurrency limit).
27
+ # Pass a positive integer to limit the number of iterations that can run concurrently.
28
+ #
29
+ # Default: serial execution (equivalent to `parallel(1)`)
30
+ #
31
+ # #### See Also
32
+ # - `parallel!`
33
+ # - `no_parallel!`
34
+ #
35
+ #: (Integer) -> void
36
+ def parallel(value)
37
+ # treat 0 as unlimited parallelism
38
+ @values[:parallel] = value > 0 ? value : nil
39
+ end
40
+
41
+ # Configure the cog to execute iterations in parallel with unlimited concurrency
42
+ #
43
+ # This removes any limit on the number of iterations that can run concurrently.
44
+ # All iterations will start simultaneously.
45
+ #
46
+ # #### See Also
47
+ # - `parallel`
48
+ # - `no_parallel!`
49
+ #
50
+ #: () -> void
51
+ def parallel!
52
+ @values[:parallel] = nil
53
+ end
54
+
55
+ # Configure the cog to execute iterations serially (one at a time)
56
+ #
57
+ # This is the default behavior. Iterations will run sequentially in order.
58
+ #
59
+ # #### See Also
60
+ # - `parallel`
61
+ # - `parallel!`
62
+ #
63
+ #: () -> void
64
+ def no_parallel!
65
+ @values[:parallel] = 1
66
+ end
67
+
68
+ # Validate the configuration
69
+ #
70
+ #: () -> void
71
+ def validate!
72
+ valid_parallel!
73
+ end
74
+
75
+ # Get the validated, configured parallelism limit
76
+ #
77
+ # Returns `nil` for unlimited parallelism, or an `Integer` for the maximum number
78
+ # of concurrent iterations. This method will raise an `InvalidConfigError` if the
79
+ # parallelism value is negative.
80
+ #
81
+ # #### See Also
82
+ # - `parallel`
83
+ # - `parallel!`
84
+ # - `no_parallel!`
85
+ #
86
+ #: () -> Integer?
87
+ def valid_parallel!
88
+ parallel = @values.fetch(:parallel, 1)
89
+ return if parallel.nil?
90
+ raise InvalidConfigError, "'parallel' must be >= 0 if specified" if parallel < 0
91
+
92
+ parallel
93
+ end
94
+ end
95
+
96
+ # Parameters for the `map` cog
97
+ class Params < SystemCog::Params
98
+ # The name of the execution scope to invoke for each item
99
+ #
100
+ #: Symbol
101
+ attr_accessor :run
102
+
103
+ # Initialize parameters with the cog name and execution scope
104
+ #
105
+ #: (?Symbol?, run: Symbol) -> void
106
+ def initialize(name = nil, run:)
107
+ super(name)
108
+ @run = run
109
+ end
110
+ end
111
+
112
+ # Input for the `map` cog
113
+ #
114
+ # Provides the collection of items to iterate over and an optional starting index.
115
+ # Each item will be passed to the execution scope along with its index.
116
+ class Input < Cog::Input
117
+ # The collection of items to iterate over
118
+ #
119
+ # This can be any enumerable collection. Each item will be passed as the value
120
+ # to the execution scope. Required.
121
+ #
122
+ #: Array[untyped]
123
+ attr_accessor :items
124
+
125
+ # The starting index for the first iteration
126
+ #
127
+ # Defaults to `0`. This affects the index value passed to each iteration but does
128
+ # not change which items are processed.
129
+ #
130
+ #: Integer
131
+ attr_accessor :initial_index
132
+
133
+ # Initialize the input with default values
134
+ #
135
+ #: () -> void
136
+ def initialize
137
+ super
138
+ @items = []
139
+ @initial_index = 0
140
+ end
141
+
142
+ # Validate that required input values are present
143
+ #
144
+ #: () -> void
145
+ def validate!
146
+ raise Cog::Input::InvalidInputError, "'items' is required" if items.nil?
147
+ raise Cog::Input::InvalidInputError if items.empty? && !coerce_ran?
148
+ end
149
+
150
+ # Coerce the input from the return value of the input block
151
+ #
152
+ # Sets the items from the input block's return value if not already set directly.
153
+ # Converts enumerable objects to arrays and wraps non-enumerable single values in an array.
154
+ #
155
+ #: (Array[untyped]) -> void
156
+ def coerce(input_return_value)
157
+ super
158
+ return if @items.present?
159
+
160
+ @items = input_return_value.respond_to?(:each) ? input_return_value.to_a : Array.wrap(input_return_value)
161
+ end
162
+ end
163
+
164
+ # Output from running the `map` cog
165
+ #
166
+ # Contains results from each iteration, allowing access to individual iteration outputs.
167
+ # Iterations that did not run (due to `break!`) will be `nil`.
168
+ #
169
+ # #### See Also
170
+ # - `Roast::DSL::CogInputContext#collect` - retrieves all iteration outputs as an array
171
+ # - `Roast::DSL::CogInputContext#reduce` - reduces iteration outputs to a single value
172
+ class Output < Cog::Output
173
+ # Initialize the output with results for each iteration
174
+ #
175
+ #: (Array[ExecutionManager?]) -> void
176
+ def initialize(execution_managers)
177
+ super()
178
+ @execution_managers = execution_managers
179
+ end
180
+
181
+ # Check if a specific iteration ran successfully
182
+ #
183
+ # Returns `true` if the iteration at the given index executed, `false` if it was
184
+ # skipped (e.g., due to `break!`). Supports negative indices to count from the end.
185
+ #
186
+ # #### See Also
187
+ # - `iteration`
188
+ #
189
+ #: (Integer) -> bool
190
+ def iteration?(index)
191
+ @execution_managers.fetch(index).present?
192
+ end
193
+
194
+ # Get the output from a specific iteration, in concert with `from`
195
+ #
196
+ # Returns a `Roast::DSL::SystemCogs::Call::Output` object for the iteration at the given index.
197
+ # Supports negative indices to count from the end (e.g., `-1` for the last iteration).
198
+ # Raises `MapIterationDidNotRunError` if the iteration did not run.
199
+ #
200
+ # Use `from` on the return value of this method, as for a single `call` cog invocation, to access the
201
+ # final output and individual cog outputs from the specified invocation.
202
+ #
203
+ # #### Usage
204
+ # ```ruby
205
+ # # Access a specific iteration
206
+ # result = from(map!(:process_items).iteration(2))
207
+ #
208
+ # # Access with negative index
209
+ # result = from(map!(:process_items).iteration(-1))
210
+ # ```
211
+ #
212
+ # #### See Also
213
+ # - `iteration?`
214
+ # - `first`
215
+ # - `last`
216
+ # - `Roast::DSL::CogInputContext#from`
217
+ #
218
+ #: (Integer) -> Call::Output
219
+ def iteration(index)
220
+ em = @execution_managers.fetch(index)
221
+ raise MapIterationDidNotRunError, index unless em.present?
222
+
223
+ Call::Output.new(em)
224
+ end
225
+
226
+ # Get the output from the first iteration
227
+ #
228
+ # Convenience method equivalent to `iteration(0)`. Raises `MapIterationDidNotRunError`
229
+ # if the first iteration did not run.
230
+ #
231
+ # #### See Also
232
+ # - `iteration`
233
+ # - `last`
234
+ #
235
+ #: () -> Call::Output
236
+ def first
237
+ iteration(0)
238
+ end
239
+
240
+ # Get the output from the last iteration that ran
241
+ #
242
+ # Convenience method equivalent to `iteration(-1)`. Raises `MapIterationDidNotRunError`
243
+ # if the last iteration did not run.
244
+ #
245
+ # #### See Also
246
+ # - `iteration`
247
+ # - `first`
248
+ #
249
+ #: () -> Call::Output
250
+ def last
251
+ iteration(-1)
252
+ end
253
+ end
254
+
255
+ # @requires_ancestor: Roast::DSL::ExecutionManager
256
+ module Manager
257
+ private
258
+
259
+ #: (Params, ^(Cog::Input) -> untyped) -> SystemCogs::Map
260
+ def create_map_system_cog(params, input_proc)
261
+ SystemCogs::Map.new(params.name, input_proc) do |input, config|
262
+ raise ExecutionManager::ExecutionScopeNotSpecifiedError unless params.run.present?
263
+
264
+ input = input #: as Input
265
+ config = config #: as Config
266
+ max_parallel_tasks = config.valid_parallel!
267
+ if max_parallel_tasks == 1
268
+ execute_map_in_series(params.run, input)
269
+ else
270
+ execute_map_in_parallel(params.run, input, max_parallel_tasks)
271
+ end
272
+ end
273
+ end
274
+
275
+ #: (Symbol, untyped, Integer) -> ExecutionManager
276
+ def create_execution_manager_for_map_item(scope, scope_value, scope_index)
277
+ ExecutionManager.new(
278
+ @cog_registry,
279
+ @config_manager,
280
+ @all_execution_procs,
281
+ @workflow_context,
282
+ scope:,
283
+ scope_value:,
284
+ scope_index:,
285
+ )
286
+ end
287
+
288
+ #: (Symbol, Map::Input) -> Output
289
+ def execute_map_in_series(run, input)
290
+ ems = []
291
+ input.items.each_with_index do |item, index|
292
+ ems << em = create_execution_manager_for_map_item(run, item, index + input.initial_index)
293
+ em.prepare!
294
+ em.run!
295
+ rescue ControlFlow::Break
296
+ # TODO: do something with the message passed to break!
297
+ break
298
+ end
299
+ ems.fill(nil, ems.length, input.items.length - ems.length)
300
+ Output.new(ems)
301
+ end
302
+
303
+ #: (Symbol, Map::Input, Integer?) -> Output
304
+ def execute_map_in_parallel(run, input, max_parallel_tasks)
305
+ barrier = Async::Barrier.new
306
+ semaphore = Async::Semaphore.new(max_parallel_tasks, parent: barrier) if max_parallel_tasks.present?
307
+ ems = {}
308
+ input.items.map.with_index do |item, index|
309
+ (semaphore || barrier).async(finished: false) do |task|
310
+ task.annotate("Map Invocation #{index + input.initial_index}")
311
+ ems[index] = em = create_execution_manager_for_map_item(run, item, index + input.initial_index)
312
+ em.prepare!
313
+ em.run!
314
+ end
315
+ end #: Array[Async::Task]
316
+
317
+ # Wait on the tasks in their completion order, so that an exception in a task will be raised as soon as it occurs
318
+ # noinspection RubyArgCount
319
+ barrier.wait do |task|
320
+ task.wait
321
+ rescue ControlFlow::Break
322
+ # TODO: do something with the message passed to break!
323
+ barrier.stop
324
+ rescue StandardError => e
325
+ barrier.stop
326
+ raise e
327
+ end
328
+
329
+ Output.new((0...input.items.length).map { |idx| ems[idx] })
330
+ ensure
331
+ # noinspection RubyRedundantSafeNavigation
332
+ barrier&.stop
333
+ end
334
+ end
335
+
336
+ # @requires_ancestor: Roast::DSL::CogInputContext
337
+ module InputContext
338
+ # Collect the results from all `map` cog iterations into an array
339
+ #
340
+ # Extracts the final output from each iteration that ran. When called without a block,
341
+ # returns an array of the final outputs directly. When called with a block, executes
342
+ # the block in the context of each iteration's input context, receiving the final output,
343
+ # the original item value, and the iteration index as arguments.
344
+ #
345
+ # Iterations that did not run (due to `break!`) will be represented as `nil` in the
346
+ # returned array.
347
+ #
348
+ # #### Usage
349
+ # ```ruby
350
+ # # Get all final outputs directly
351
+ # results = collect(map!(:process_items))
352
+ #
353
+ # # Transform each output with access to the original item and index
354
+ # results = collect(map!(:process_items)) do |output, item, index|
355
+ # { item: item, result: output, position: index }
356
+ # end
357
+ #
358
+ # # Access other cog outputs from within each iteration
359
+ # results = collect(map!(:process_items)) do |output, item, index|
360
+ # inner_cog!(:some_step)
361
+ # end
362
+ # ```
363
+ #
364
+ # #### See Also
365
+ # - `reduce`
366
+ # - `Roast::DSL::SystemCogs::Map::Output`
367
+ #
368
+ # @rbs [T] (Roast::DSL::SystemCogs::Map::Output) {() -> T} -> Array[T]
369
+ # | (Roast::DSL::SystemCogs::Map::Output) -> Array[untyped]
370
+ def collect(map_cog_output, &block)
371
+ ems = map_cog_output.instance_variable_get(:@execution_managers)
372
+ raise CogInputContext::ContextNotFoundError if ems.nil?
373
+
374
+ return ems.map do |em|
375
+ next unless em
376
+
377
+ scope_value = em.instance_variable_get(:@scope_value)
378
+ scope_index = em.instance_variable_get(:@scope_index)
379
+ final_output = em.final_output
380
+ em.cog_input_context.instance_exec(final_output, scope_value, scope_index, &block)
381
+ end if block_given?
382
+
383
+ ems.map { |em| em&.final_output }
384
+ end
385
+
386
+ # Reduce the results from all `map` cog iterations to a single value
387
+ #
388
+ # Processes each iteration's output sequentially, combining them into an accumulator value.
389
+ # The block receives the current accumulator value, the final output from the iteration,
390
+ # the original item value, and the iteration index. The block should return the new
391
+ # accumulator value.
392
+ #
393
+ # If the block returns `nil`, the accumulator will __not__ be updated (preserving any
394
+ # previous non-nil value). This prevents accidental overwrites with `nil` values.
395
+ #
396
+ # Iterations that did not run (due to `break!`) are skipped.
397
+ #
398
+ # #### Usage
399
+ # ```ruby
400
+ # # Sum all outputs
401
+ # total = reduce(map!(:calculate_scores), 0) do |sum, output, item, index|
402
+ # sum + output
403
+ # end
404
+ #
405
+ # # Build a hash from outputs
406
+ # results = reduce(map!(:process_items), {}) do |hash, output, item, index|
407
+ # hash.merge(item => output)
408
+ # end
409
+ #
410
+ # # Collect with conditional accumulation
411
+ # valid_results = reduce(map!(:validate_items), []) do |acc, output, item, index|
412
+ # output.valid? ? acc + [output] : acc
413
+ # end
414
+ # ```
415
+ #
416
+ # #### See Also
417
+ # - `collect`
418
+ # - `Roast::DSL::SystemCogs::Map::Output`
419
+ #
420
+ #: [A] (Roast::DSL::SystemCogs::Map::Output, ?A?) {(A?, untyped) -> A} -> A?
421
+ def reduce(map_cog_output, initial_value = nil, &block)
422
+ ems = map_cog_output.instance_variable_get(:@execution_managers)
423
+ raise CogInputContext::ContextNotFoundError if ems.nil?
424
+
425
+ accumulator = initial_value
426
+ ems.compact.each do |em|
427
+ next unless em
428
+
429
+ scope_value = em.instance_variable_get(:@scope_value)
430
+ scope_index = em.instance_variable_get(:@scope_index)
431
+ final_output = em.final_output
432
+ new_accumulator = em.cog_input_context.instance_exec(accumulator, final_output, scope_value, scope_index, &block)
433
+ case new_accumulator
434
+ when nil
435
+ # do not overwrite a non-nil value in the accumulator with a nil value,
436
+ # even if one is returned from the block
437
+ else
438
+ accumulator = new_accumulator #: as A
439
+ end
440
+ end
441
+
442
+ accumulator
443
+ end
444
+ end
445
+ end
446
+ end
447
+ end
448
+ end
@@ -0,0 +1,242 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Roast
5
+ module DSL
6
+ module SystemCogs
7
+ # Repeat cog for executing a scope multiple times in a loop
8
+ #
9
+ # Executes a named execution scope (defined with `execute(:name)`) repeatedly until
10
+ # a `break!` is called. The output from each iteration becomes the input value for
11
+ # the next iteration, allowing for iterative transformations.
12
+ class Repeat < SystemCog
13
+ # Configuration for the `repeat` cog
14
+ #
15
+ # Currently has no configuration options.
16
+ class Config < Cog::Config; end
17
+
18
+ # Parameters for the `repeat` cog
19
+ class Params < SystemCog::Params
20
+ # The name of the execution scope to invoke for each iteration
21
+ #
22
+ #: Symbol
23
+ attr_accessor :run
24
+
25
+ # Initialize parameters with the cog name and execution scope
26
+ #
27
+ #: (?Symbol?, run: Symbol) -> void
28
+ def initialize(name = nil, run:)
29
+ super(name)
30
+ @run = run
31
+ end
32
+ end
33
+
34
+ # Input for the `repeat` cog
35
+ #
36
+ # Provides the initial value to pass to the first iteration. Each subsequent iteration
37
+ # receives the output from the previous iteration as its value.
38
+ class Input < Cog::Input
39
+ # The initial value to pass to the first iteration
40
+ #
41
+ # This value will be passed to the execution scope on the first iteration. Subsequent
42
+ # iterations receive the output from the previous iteration. Required.
43
+ #
44
+ #: untyped
45
+ attr_accessor :value
46
+
47
+ # The starting index for the first iteration
48
+ #
49
+ # Defaults to `0`. This affects the index value passed to each iteration.
50
+ #
51
+ # Integer
52
+ attr_accessor :index
53
+
54
+ # The maximum number of iterations for which the loop may run
55
+ #
56
+ # Defaults to `nil`, meaning that no maximum iteration limit is applied
57
+ #
58
+ #: Integer?
59
+ attr_accessor :max_iterations
60
+
61
+ # Initialize the input with default values
62
+ #
63
+ #: () -> void
64
+ def initialize
65
+ super
66
+ @index = 0
67
+ end
68
+
69
+ # Validate that required input values are present
70
+ #
71
+ #: () -> void
72
+ def validate!
73
+ raise Cog::Input::InvalidInputError, "'value' is required" if value.nil? && !coerce_ran?
74
+ raise Cog::Input::InvalidInputError, "'max_iterations' must be >= 1 if present" if (max_iterations || 1) < 1
75
+ end
76
+
77
+ # Coerce the input from the return value of the input block
78
+ #
79
+ # Sets the value from the input block's return value if not already set directly.
80
+ #
81
+ #: (untyped) -> void
82
+ def coerce(input_return_value)
83
+ super
84
+ @value = input_return_value unless @value.present?
85
+ end
86
+ end
87
+
88
+ # Output from running the `repeat` cog
89
+ #
90
+ # Contains results from all iterations that ran. Provides access to the final value
91
+ # (output from the last iteration) as well as individual iteration results.
92
+ #
93
+ # #### See Also
94
+ # - `Roast::DSL::CogInputContext#collect` - retrieves all iteration outputs as an array (via `results`)
95
+ # - `Roast::DSL::CogInputContext#reduce` - reduces iteration outputs to a single value (via `results`)
96
+ class Output < Cog::Output
97
+ # Initialize the output with results for all iterations
98
+ #
99
+ #: (Array[ExecutionManager]) -> void
100
+ def initialize(execution_managers)
101
+ super()
102
+ @execution_managers = execution_managers
103
+ end
104
+
105
+ # Get the final output value from the last iteration
106
+ #
107
+ # This is the output from the last iteration before `break!` was called.
108
+ # Returns `nil` if no iterations ran.
109
+ #
110
+ # #### Usage
111
+ # ```ruby
112
+ # # Get the final result directly
113
+ # final = repeat!(:process)
114
+ # ```
115
+ #
116
+ # #### See Also
117
+ # - `last`
118
+ # - `results`
119
+ #
120
+ #: () -> untyped
121
+ def value
122
+ @execution_managers.last&.final_output
123
+ end
124
+
125
+ # Get the output from a specific iteration
126
+ #
127
+ # Returns a `Roast::DSL::SystemCogs::Call::Output` object for the iteration at the given index.
128
+ # Supports negative indices to count from the end (e.g., `-1` for the last iteration).
129
+ #
130
+ # #### Usage
131
+ # ```ruby
132
+ # # Access a specific iteration
133
+ # result = from(repeat!(:process).iteration(2))
134
+ #
135
+ # # Access with negative index
136
+ # result = from(repeat!(:process).iteration(-1))
137
+ # ```
138
+ #
139
+ # #### See Also
140
+ # - `first`
141
+ # - `last`
142
+ # - `value`
143
+ # - `Roast::DSL::CogInputContext#from`
144
+ #
145
+ #: (Integer) -> Call::Output
146
+ def iteration(index)
147
+ Call::Output.new(@execution_managers.fetch(index))
148
+ end
149
+
150
+ # Get the output from the first iteration
151
+ #
152
+ # Convenience method equivalent to `iteration(0)`.
153
+ #
154
+ # #### See Also
155
+ # - `iteration`
156
+ # - `last`
157
+ #
158
+ #: () -> Call::Output
159
+ def first
160
+ iteration(0)
161
+ end
162
+
163
+ # Get the output from the last iteration
164
+ #
165
+ # Convenience method equivalent to `iteration(-1)`. Returns the same value as
166
+ # calling `value`, but wrapped in a `Roast::DSL::SystemCogs::Call::Output` for use with `from`.
167
+ #
168
+ # #### See Also
169
+ # - `iteration`
170
+ # - `first`
171
+ # - `value`
172
+ #
173
+ #: () -> Call::Output
174
+ def last
175
+ iteration(-1)
176
+ end
177
+
178
+ # Get all iteration results as a `Roast::DSL::SystemCogs::Map::Output` object
179
+ #
180
+ # Returns a `Roast::DSL::SystemCogs::Map::Output` containing all iterations, which can be used with
181
+ # `collect` or `reduce` to process all iteration outputs.
182
+ #
183
+ # #### Usage
184
+ # ```ruby
185
+ # # Collect all iteration outputs
186
+ # all_results = collect(repeat!(:process).results)
187
+ #
188
+ # # Reduce all iteration outputs
189
+ # sum = reduce(repeat!(:process).results, 0) { |acc, output| acc + output }
190
+ # ```
191
+ #
192
+ # #### See Also
193
+ # - `value`
194
+ # - `Roast::DSL::CogInputContext#collect`
195
+ # - `Roast::DSL::CogInputContext#reduce`
196
+ # - `Roast::DSL::SystemCogs::Map::Output`
197
+ #
198
+ #: () -> Map::Output
199
+ def results
200
+ Map::Output.new(@execution_managers)
201
+ end
202
+ end
203
+
204
+ # @requires_ancestor: Roast::DSL::ExecutionManager
205
+ module Manager
206
+ private
207
+
208
+ #: (Params, ^(Cog::Input) -> untyped) -> SystemCogs::Repeat
209
+ def create_repeat_system_cog(params, input_proc)
210
+ SystemCogs::Repeat.new(params.name, input_proc) do |input|
211
+ input = input #: as Input
212
+ raise ExecutionManager::ExecutionScopeNotSpecifiedError unless params.run.present?
213
+
214
+ ems = [] #: Array[ExecutionManager]
215
+ scope_value = input.value.deep_dup
216
+ max_iterations = input.max_iterations
217
+ loop do
218
+ ems << em = ExecutionManager.new(
219
+ @cog_registry,
220
+ @config_manager,
221
+ @all_execution_procs,
222
+ @workflow_context,
223
+ scope: params.run,
224
+ scope_value: scope_value,
225
+ scope_index: ems.length,
226
+ )
227
+ em.prepare!
228
+ em.run!
229
+ scope_value = em.final_output
230
+ break if max_iterations.present? && ems.length >= max_iterations
231
+ rescue ControlFlow::Break
232
+ # TODO: do something with the message passed to break!
233
+ break
234
+ end
235
+ Output.new(ems)
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end