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.
Files changed (175) 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/.ruby-version +1 -1
  8. data/CLAUDE.md +2 -2
  9. data/CONTRIBUTING.md +2 -0
  10. data/Gemfile +19 -18
  11. data/Gemfile.lock +35 -58
  12. data/README.md +118 -1432
  13. data/README_LEGACY.md +1464 -0
  14. data/Rakefile +39 -4
  15. data/dev.yml +29 -0
  16. data/dsl/agent_sessions.rb +20 -0
  17. data/dsl/async_cogs.rb +49 -0
  18. data/dsl/async_cogs_complex.rb +67 -0
  19. data/dsl/call.rb +44 -0
  20. data/dsl/collect_from.rb +72 -0
  21. data/dsl/json_output.rb +28 -0
  22. data/dsl/map.rb +55 -0
  23. data/dsl/map_reduce.rb +37 -0
  24. data/dsl/map_with_index.rb +49 -0
  25. data/dsl/next_break.rb +45 -0
  26. data/dsl/next_break_parallel.rb +44 -0
  27. data/dsl/outputs.rb +39 -0
  28. data/dsl/outputs_bang.rb +36 -0
  29. data/dsl/parallel_map.rb +37 -0
  30. data/dsl/prompts/simple_prompt.md.erb +3 -0
  31. data/dsl/prototype.rb +5 -7
  32. data/dsl/repeat_loop_results.rb +53 -0
  33. data/dsl/ruby_cog.rb +72 -0
  34. data/dsl/simple_agent.rb +18 -0
  35. data/dsl/simple_chat.rb +15 -1
  36. data/dsl/simple_repeat.rb +29 -0
  37. data/dsl/skip.rb +36 -0
  38. data/dsl/step_communication.rb +2 -3
  39. data/dsl/targets_and_params.rb +57 -0
  40. data/dsl/temperature.rb +17 -0
  41. data/dsl/temporary_directory.rb +22 -0
  42. data/dsl/tutorial/01_your_first_workflow/README.md +179 -0
  43. data/dsl/tutorial/01_your_first_workflow/configured_chat.rb +33 -0
  44. data/dsl/tutorial/01_your_first_workflow/hello.rb +23 -0
  45. data/dsl/tutorial/02_chaining_cogs/README.md +310 -0
  46. data/dsl/tutorial/02_chaining_cogs/code_review.rb +104 -0
  47. data/dsl/tutorial/02_chaining_cogs/session_resumption.rb +92 -0
  48. data/dsl/tutorial/02_chaining_cogs/simple_chain.rb +84 -0
  49. data/dsl/tutorial/03_targets_and_params/README.md +230 -0
  50. data/dsl/tutorial/03_targets_and_params/multiple_targets.rb +65 -0
  51. data/dsl/tutorial/03_targets_and_params/single_target.rb +65 -0
  52. data/dsl/tutorial/04_configuration_options/README.md +209 -0
  53. data/dsl/tutorial/04_configuration_options/control_display_and_temperature.rb +104 -0
  54. data/dsl/tutorial/04_configuration_options/simple_config.rb +68 -0
  55. data/dsl/tutorial/05_control_flow/README.md +156 -0
  56. data/dsl/tutorial/05_control_flow/conditional_execution.rb +62 -0
  57. data/dsl/tutorial/05_control_flow/handling_failures.rb +77 -0
  58. data/dsl/tutorial/06_reusable_scopes/README.md +172 -0
  59. data/dsl/tutorial/06_reusable_scopes/accessing_scope_outputs.rb +126 -0
  60. data/dsl/tutorial/06_reusable_scopes/basic_scope.rb +63 -0
  61. data/dsl/tutorial/06_reusable_scopes/parameterized_scope.rb +78 -0
  62. data/dsl/tutorial/07_processing_collections/README.md +152 -0
  63. data/dsl/tutorial/07_processing_collections/basic_map.rb +70 -0
  64. data/dsl/tutorial/07_processing_collections/parallel_map.rb +74 -0
  65. data/dsl/tutorial/08_iterative_workflows/README.md +231 -0
  66. data/dsl/tutorial/08_iterative_workflows/basic_repeat.rb +57 -0
  67. data/dsl/tutorial/08_iterative_workflows/conditional_break.rb +57 -0
  68. data/dsl/tutorial/09_async_cogs/README.md +197 -0
  69. data/dsl/tutorial/09_async_cogs/basic_async.rb +38 -0
  70. data/dsl/tutorial/README.md +222 -0
  71. data/dsl/working_directory.rb +16 -0
  72. data/exe/roast +1 -1
  73. data/internal/documentation/architectural-notes.md +115 -0
  74. data/internal/documentation/doc-comments-external.md +686 -0
  75. data/internal/documentation/doc-comments-internal.md +342 -0
  76. data/internal/documentation/doc-comments.md +211 -0
  77. data/lib/roast/dsl/cog/config.rb +274 -3
  78. data/lib/roast/dsl/cog/input.rb +53 -10
  79. data/lib/roast/dsl/cog/output.rb +297 -8
  80. data/lib/roast/dsl/cog/registry.rb +35 -3
  81. data/lib/roast/dsl/cog/stack.rb +1 -1
  82. data/lib/roast/dsl/cog/store.rb +5 -5
  83. data/lib/roast/dsl/cog.rb +70 -14
  84. data/lib/roast/dsl/cog_input_context.rb +36 -1
  85. data/lib/roast/dsl/cog_input_manager.rb +116 -7
  86. data/lib/roast/dsl/cogs/agent/config.rb +465 -0
  87. data/lib/roast/dsl/cogs/agent/input.rb +81 -0
  88. data/lib/roast/dsl/cogs/agent/output.rb +59 -0
  89. data/lib/roast/dsl/cogs/agent/provider.rb +51 -0
  90. data/lib/roast/dsl/cogs/agent/providers/claude/claude_invocation.rb +185 -0
  91. data/lib/roast/dsl/cogs/agent/providers/claude/message.rb +73 -0
  92. data/lib/roast/dsl/cogs/agent/providers/claude/messages/assistant_message.rb +36 -0
  93. data/lib/roast/dsl/cogs/agent/providers/claude/messages/result_message.rb +61 -0
  94. data/lib/roast/dsl/cogs/agent/providers/claude/messages/system_message.rb +47 -0
  95. data/lib/roast/dsl/cogs/agent/providers/claude/messages/text_message.rb +36 -0
  96. data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_result_message.rb +47 -0
  97. data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_use_message.rb +46 -0
  98. data/lib/roast/dsl/cogs/agent/providers/claude/messages/unknown_message.rb +27 -0
  99. data/lib/roast/dsl/cogs/agent/providers/claude/messages/user_message.rb +37 -0
  100. data/lib/roast/dsl/cogs/agent/providers/claude/tool_result.rb +51 -0
  101. data/lib/roast/dsl/cogs/agent/providers/claude/tool_use.rb +48 -0
  102. data/lib/roast/dsl/cogs/agent/providers/claude.rb +31 -0
  103. data/lib/roast/dsl/cogs/agent/stats.rb +92 -0
  104. data/lib/roast/dsl/cogs/agent/usage.rb +62 -0
  105. data/lib/roast/dsl/cogs/agent.rb +75 -0
  106. data/lib/roast/dsl/cogs/chat/config.rb +453 -0
  107. data/lib/roast/dsl/cogs/chat/input.rb +92 -0
  108. data/lib/roast/dsl/cogs/chat/output.rb +64 -0
  109. data/lib/roast/dsl/cogs/chat/session.rb +68 -0
  110. data/lib/roast/dsl/cogs/chat.rb +59 -56
  111. data/lib/roast/dsl/cogs/cmd.rb +251 -61
  112. data/lib/roast/dsl/cogs/ruby.rb +171 -0
  113. data/lib/roast/dsl/command_runner.rb +191 -0
  114. data/lib/roast/dsl/config_manager.rb +58 -11
  115. data/lib/roast/dsl/control_flow.rb +41 -0
  116. data/lib/roast/dsl/execution_manager.rb +162 -32
  117. data/lib/roast/dsl/nil_assertions.rb +23 -0
  118. data/lib/roast/dsl/system_cog/params.rb +32 -0
  119. data/lib/roast/dsl/system_cog.rb +36 -0
  120. data/lib/roast/dsl/system_cogs/call.rb +163 -0
  121. data/lib/roast/dsl/system_cogs/map.rb +454 -0
  122. data/lib/roast/dsl/system_cogs/repeat.rb +242 -0
  123. data/lib/roast/dsl/workflow.rb +26 -16
  124. data/lib/roast/dsl/workflow_context.rb +20 -0
  125. data/lib/roast/dsl/workflow_params.rb +24 -0
  126. data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
  127. data/lib/roast/sorbet_runtime_stub.rb +154 -0
  128. data/lib/roast/tools/apply_diff.rb +1 -3
  129. data/lib/roast/tools/cmd.rb +4 -3
  130. data/lib/roast/tools/read_file.rb +1 -1
  131. data/lib/roast/tools/update_files.rb +1 -1
  132. data/lib/roast/tools/write_file.rb +1 -1
  133. data/lib/roast/version.rb +1 -1
  134. data/lib/roast/workflow/base_workflow.rb +4 -0
  135. data/lib/roast/workflow/step_loader.rb +14 -2
  136. data/lib/roast-ai.rb +4 -0
  137. data/lib/roast.rb +58 -21
  138. data/{roast.gemspec → roast-ai.gemspec} +9 -13
  139. data/sorbet/rbi/gems/async@2.34.0.rbi +1577 -0
  140. data/sorbet/rbi/gems/cli-kit@5.2.0.rbi +2063 -0
  141. data/sorbet/rbi/gems/{cli-ui@2.3.0.rbi → cli-ui@2.7.0-6bdefd1d06305e5d6ae312ac76f9c88f88658dda.rbi} +1418 -1013
  142. data/sorbet/rbi/gems/console@1.34.2.rbi +1193 -0
  143. data/sorbet/rbi/gems/fiber-annotation@0.2.0.rbi +50 -0
  144. data/sorbet/rbi/gems/fiber-local@1.1.0.rbi +35 -0
  145. data/sorbet/rbi/gems/fiber-storage@1.0.1.rbi +41 -0
  146. data/sorbet/rbi/gems/io-event@1.14.0.rbi +724 -0
  147. data/sorbet/rbi/gems/metrics@0.15.0.rbi +9 -0
  148. data/sorbet/rbi/gems/traces@0.18.2.rbi +9 -0
  149. data/sorbet/rbi/shims/lib/roast/dsl/cog_input_context.rbi +1185 -5
  150. data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +311 -5
  151. data/sorbet/rbi/shims/lib/roast/dsl/execution_context.rbi +486 -5
  152. data/sorbet/tapioca/config.yml +6 -0
  153. data/sorbet/tapioca/require.rb +2 -0
  154. metadata +157 -30
  155. data/dsl/less_simple.rb +0 -112
  156. data/dsl/scoped_executors.rb +0 -28
  157. data/dsl/simple.rb +0 -8
  158. data/lib/roast/dsl/cogs/execute.rb +0 -46
  159. data/lib/roast/dsl/cogs/graph.rb +0 -53
  160. data/sorbet/rbi/gems/cgi@0.5.0.rbi +0 -2961
  161. data/sorbet/rbi/gems/claude_swarm@0.1.19.rbi +0 -568
  162. data/sorbet/rbi/gems/cli-kit@5.0.1.rbi +0 -1991
  163. data/sorbet/rbi/gems/dry-configurable@1.3.0.rbi +0 -672
  164. data/sorbet/rbi/gems/dry-core@1.1.0.rbi +0 -1894
  165. data/sorbet/rbi/gems/dry-inflector@1.2.0.rbi +0 -659
  166. data/sorbet/rbi/gems/dry-initializer@3.2.0.rbi +0 -781
  167. data/sorbet/rbi/gems/dry-logic@1.6.0.rbi +0 -1127
  168. data/sorbet/rbi/gems/dry-schema@1.14.1.rbi +0 -3727
  169. data/sorbet/rbi/gems/dry-types@1.8.3.rbi +0 -3969
  170. data/sorbet/rbi/gems/fast-mcp-annotations@1.5.3.rbi +0 -1588
  171. data/sorbet/rbi/gems/mime-types-data@3.2025.0617.rbi +0 -136
  172. data/sorbet/rbi/gems/mime-types@3.7.0.rbi +0 -1342
  173. data/sorbet/rbi/gems/rack@2.2.19.rbi +0 -5676
  174. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
  175. data/sorbet/rbi/gems/yard@0.9.37.rbi +0 -18492
@@ -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,163 @@
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::Next, ControlFlow::Break
110
+ # treat `break!` like `next!` in a `call` invocation
111
+ # just end the execution early and return like normal
112
+ # TODO: maybe do something with the message passed to next! or break!
113
+ end
114
+ Output.new(em)
115
+ end
116
+ end
117
+ end
118
+
119
+ # @requires_ancestor: Roast::DSL::CogInputContext
120
+ module InputContext
121
+ # Retrieve the output from a `call` cog's execution scope
122
+ #
123
+ # Extracts the final output from the execution scope that was invoked by the call cog.
124
+ # When called without a block, returns the final output directly. When called with a block,
125
+ # executes the block in the context of the called scope's input context, receiving the final
126
+ # output as an argument.
127
+ #
128
+ # This allows you to access the results of a called scope and optionally transform them
129
+ # and/or access other outputs from within that scope.
130
+ #
131
+ # #### Usage
132
+ # ```ruby
133
+ # # Get the final output directly
134
+ # result = from(call!(:my_call))
135
+ #
136
+ # # Transform the output with a block
137
+ # transformed = from(call!(:my_call)) { |output| output.upcase }
138
+ #
139
+ # # Access other cog outputs from within the called scope
140
+ # inner_result = from(call!(:my_call)) { inner_cog!(:some_step) }
141
+ # ```
142
+ #
143
+ # #### See Also
144
+ # - `Roast::DSL::SystemCogs::Call::Output` - the output type from call cogs
145
+ #
146
+ # @rbs [T] (Roast::DSL::SystemCogs::Call::Output) {(untyped, untyped, Integer) -> T} -> T
147
+ # | (Roast::DSL::SystemCogs::Call::Output) -> untyped
148
+ def from(call_cog_output, &block)
149
+ em = call_cog_output.instance_variable_get(:@execution_manager)
150
+ raise CogInputContext::ContextNotFoundError if em.nil?
151
+
152
+ final_output = em.final_output
153
+ scope_value = em.instance_variable_get(:@scope_value).deep_dup
154
+ scope_index = em.instance_variable_get(:@scope_index)
155
+ return em.cog_input_context.instance_exec(final_output, scope_value, scope_index, &block) if block_given?
156
+
157
+ final_output
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,454 @@
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::Next
296
+ # TODO: do something with the message passed to next!
297
+ # proceed to next iteration
298
+ rescue ControlFlow::Break
299
+ # TODO: do something with the message passed to break!
300
+ break
301
+ end
302
+ ems.fill(nil, ems.length, input.items.length - ems.length)
303
+ Output.new(ems)
304
+ end
305
+
306
+ #: (Symbol, Map::Input, Integer?) -> Output
307
+ def execute_map_in_parallel(run, input, max_parallel_tasks)
308
+ barrier = Async::Barrier.new
309
+ semaphore = Async::Semaphore.new(max_parallel_tasks, parent: barrier) if max_parallel_tasks.present?
310
+ ems = {}
311
+ input.items.map.with_index do |item, index|
312
+ (semaphore || barrier).async(finished: false) do |task|
313
+ task.annotate("Map Invocation #{index + input.initial_index}")
314
+ ems[index] = em = create_execution_manager_for_map_item(run, item, index + input.initial_index)
315
+ em.prepare!
316
+ em.run!
317
+ rescue ControlFlow::Next
318
+ # TODO: do something with the message passed to next!
319
+ # proceed to next iteration
320
+ end
321
+ end #: Array[Async::Task]
322
+
323
+ # Wait on the tasks in their completion order, so that an exception in a task will be raised as soon as it occurs
324
+ # noinspection RubyArgCount
325
+ barrier.wait do |task|
326
+ task.wait
327
+ rescue ControlFlow::Break
328
+ # TODO: do something with the message passed to break!
329
+ barrier.stop
330
+ rescue StandardError => e
331
+ barrier.stop
332
+ raise e
333
+ end
334
+
335
+ Output.new((0...input.items.length).map { |idx| ems[idx] })
336
+ ensure
337
+ # noinspection RubyRedundantSafeNavigation
338
+ barrier&.stop
339
+ end
340
+ end
341
+
342
+ # @requires_ancestor: Roast::DSL::CogInputContext
343
+ module InputContext
344
+ # Collect the results from all `map` cog iterations into an array
345
+ #
346
+ # Extracts the final output from each iteration that ran. When called without a block,
347
+ # returns an array of the final outputs directly. When called with a block, executes
348
+ # the block in the context of each iteration's input context, receiving the final output,
349
+ # the original item value, and the iteration index as arguments.
350
+ #
351
+ # Iterations that did not run (due to `break!`) will be represented as `nil` in the
352
+ # returned array.
353
+ #
354
+ # #### Usage
355
+ # ```ruby
356
+ # # Get all final outputs directly
357
+ # results = collect(map!(:process_items))
358
+ #
359
+ # # Transform each output with access to the original item and index
360
+ # results = collect(map!(:process_items)) do |output, item, index|
361
+ # { item: item, result: output, position: index }
362
+ # end
363
+ #
364
+ # # Access other cog outputs from within each iteration
365
+ # results = collect(map!(:process_items)) do |output, item, index|
366
+ # inner_cog!(:some_step)
367
+ # end
368
+ # ```
369
+ #
370
+ # #### See Also
371
+ # - `reduce`
372
+ # - `Roast::DSL::SystemCogs::Map::Output`
373
+ #
374
+ # @rbs [T] (Roast::DSL::SystemCogs::Map::Output) {() -> T} -> Array[T]
375
+ # | (Roast::DSL::SystemCogs::Map::Output) -> Array[untyped]
376
+ def collect(map_cog_output, &block)
377
+ ems = map_cog_output.instance_variable_get(:@execution_managers)
378
+ raise CogInputContext::ContextNotFoundError if ems.nil?
379
+
380
+ return ems.map do |em|
381
+ next unless em
382
+
383
+ scope_value = em.instance_variable_get(:@scope_value)
384
+ scope_index = em.instance_variable_get(:@scope_index)
385
+ final_output = em.final_output
386
+ em.cog_input_context.instance_exec(final_output, scope_value, scope_index, &block)
387
+ end if block_given?
388
+
389
+ ems.map { |em| em&.final_output }
390
+ end
391
+
392
+ # Reduce the results from all `map` cog iterations to a single value
393
+ #
394
+ # Processes each iteration's output sequentially, combining them into an accumulator value.
395
+ # The block receives the current accumulator value, the final output from the iteration,
396
+ # the original item value, and the iteration index. The block should return the new
397
+ # accumulator value.
398
+ #
399
+ # If the block returns `nil`, the accumulator will __not__ be updated (preserving any
400
+ # previous non-nil value). This prevents accidental overwrites with `nil` values.
401
+ #
402
+ # Iterations that did not run (due to `break!`) are skipped.
403
+ #
404
+ # #### Usage
405
+ # ```ruby
406
+ # # Sum all outputs
407
+ # total = reduce(map!(:calculate_scores), 0) do |sum, output, item, index|
408
+ # sum + output
409
+ # end
410
+ #
411
+ # # Build a hash from outputs
412
+ # results = reduce(map!(:process_items), {}) do |hash, output, item, index|
413
+ # hash.merge(item => output)
414
+ # end
415
+ #
416
+ # # Collect with conditional accumulation
417
+ # valid_results = reduce(map!(:validate_items), []) do |acc, output, item, index|
418
+ # output.valid? ? acc + [output] : acc
419
+ # end
420
+ # ```
421
+ #
422
+ # #### See Also
423
+ # - `collect`
424
+ # - `Roast::DSL::SystemCogs::Map::Output`
425
+ #
426
+ #: [A] (Roast::DSL::SystemCogs::Map::Output, ?A?) {(A?, untyped) -> A} -> A?
427
+ def reduce(map_cog_output, initial_value = nil, &block)
428
+ ems = map_cog_output.instance_variable_get(:@execution_managers)
429
+ raise CogInputContext::ContextNotFoundError if ems.nil?
430
+
431
+ accumulator = initial_value
432
+ ems.compact.each do |em|
433
+ next unless em
434
+
435
+ scope_value = em.instance_variable_get(:@scope_value)
436
+ scope_index = em.instance_variable_get(:@scope_index)
437
+ final_output = em.final_output
438
+ new_accumulator = em.cog_input_context.instance_exec(accumulator, final_output, scope_value, scope_index, &block)
439
+ case new_accumulator
440
+ when nil
441
+ # do not overwrite a non-nil value in the accumulator with a nil value,
442
+ # even if one is returned from the block
443
+ else
444
+ accumulator = new_accumulator #: as A
445
+ end
446
+ end
447
+
448
+ accumulator
449
+ end
450
+ end
451
+ end
452
+ end
453
+ end
454
+ end