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.
- checksums.yaml +4 -4
- data/.claude/commands/docs/write-comments.md +36 -0
- data/.github/CODEOWNERS +1 -1
- data/.github/workflows/ci.yaml +10 -6
- data/.gitignore +0 -1
- data/.rubocop.yml +7 -1
- data/CLAUDE.md +2 -2
- data/CONTRIBUTING.md +2 -0
- data/Gemfile +18 -18
- data/Gemfile.lock +46 -57
- data/README.md +118 -1432
- data/README_LEGACY.md +1464 -0
- data/Rakefile +39 -4
- data/dev.yml +29 -0
- data/dsl/agent_sessions.rb +20 -0
- data/dsl/async_cogs.rb +49 -0
- data/dsl/async_cogs_complex.rb +67 -0
- data/dsl/call.rb +44 -0
- data/dsl/collect_from.rb +72 -0
- data/dsl/demo/Gemfile +4 -0
- data/dsl/demo/Gemfile.lock +120 -0
- data/dsl/demo/cogs/local.rb +15 -0
- data/dsl/demo/simple_external_cog.rb +17 -0
- data/dsl/json_output.rb +28 -0
- data/dsl/map.rb +55 -0
- data/dsl/map_reduce.rb +37 -0
- data/dsl/map_with_index.rb +49 -0
- data/dsl/next_break.rb +40 -0
- data/dsl/next_break_parallel.rb +44 -0
- data/dsl/outputs.rb +39 -0
- data/dsl/outputs_bang.rb +36 -0
- data/dsl/parallel_map.rb +37 -0
- data/dsl/plugin-gem-example/.gitignore +8 -0
- data/dsl/plugin-gem-example/Gemfile +13 -0
- data/dsl/plugin-gem-example/Gemfile.lock +178 -0
- data/dsl/plugin-gem-example/lib/other.rb +17 -0
- data/dsl/plugin-gem-example/lib/plugin_gem_example.rb +5 -0
- data/dsl/plugin-gem-example/lib/simple.rb +15 -0
- data/dsl/plugin-gem-example/lib/version.rb +10 -0
- data/dsl/plugin-gem-example/plugin-gem-example.gemspec +28 -0
- data/dsl/prompts/simple_prompt.md.erb +3 -0
- data/dsl/prototype.rb +10 -4
- data/dsl/repeat_loop_results.rb +53 -0
- data/dsl/ruby_cog.rb +72 -0
- data/dsl/simple_agent.rb +18 -0
- data/dsl/simple_chat.rb +26 -0
- data/dsl/simple_repeat.rb +29 -0
- data/dsl/skip.rb +36 -0
- data/dsl/step_communication.rb +10 -5
- data/dsl/targets_and_params.rb +57 -0
- data/dsl/temperature.rb +17 -0
- data/dsl/temporary_directory.rb +22 -0
- data/dsl/tutorial/01_your_first_workflow/README.md +179 -0
- data/dsl/tutorial/01_your_first_workflow/configured_chat.rb +33 -0
- data/dsl/tutorial/01_your_first_workflow/hello.rb +23 -0
- data/dsl/tutorial/02_chaining_cogs/README.md +310 -0
- data/dsl/tutorial/02_chaining_cogs/code_review.rb +104 -0
- data/dsl/tutorial/02_chaining_cogs/session_resumption.rb +92 -0
- data/dsl/tutorial/02_chaining_cogs/simple_chain.rb +84 -0
- data/dsl/tutorial/03_targets_and_params/README.md +230 -0
- data/dsl/tutorial/03_targets_and_params/multiple_targets.rb +65 -0
- data/dsl/tutorial/03_targets_and_params/single_target.rb +65 -0
- data/dsl/tutorial/04_configuration_options/README.md +209 -0
- data/dsl/tutorial/04_configuration_options/control_display_and_temperature.rb +104 -0
- data/dsl/tutorial/04_configuration_options/simple_config.rb +68 -0
- data/dsl/tutorial/05_control_flow/README.md +156 -0
- data/dsl/tutorial/05_control_flow/conditional_execution.rb +62 -0
- data/dsl/tutorial/05_control_flow/handling_failures.rb +77 -0
- data/dsl/tutorial/06_reusable_scopes/README.md +172 -0
- data/dsl/tutorial/06_reusable_scopes/accessing_scope_outputs.rb +126 -0
- data/dsl/tutorial/06_reusable_scopes/basic_scope.rb +63 -0
- data/dsl/tutorial/06_reusable_scopes/parameterized_scope.rb +78 -0
- data/dsl/tutorial/07_processing_collections/README.md +152 -0
- data/dsl/tutorial/07_processing_collections/basic_map.rb +70 -0
- data/dsl/tutorial/07_processing_collections/parallel_map.rb +74 -0
- data/dsl/tutorial/08_iterative_workflows/README.md +231 -0
- data/dsl/tutorial/08_iterative_workflows/basic_repeat.rb +57 -0
- data/dsl/tutorial/08_iterative_workflows/conditional_break.rb +57 -0
- data/dsl/tutorial/09_async_cogs/README.md +197 -0
- data/dsl/tutorial/09_async_cogs/basic_async.rb +38 -0
- data/dsl/tutorial/README.md +222 -0
- data/dsl/working_directory.rb +16 -0
- data/exe/roast +1 -1
- data/internal/documentation/architectural-notes.md +115 -0
- data/internal/documentation/doc-comments-external.md +686 -0
- data/internal/documentation/doc-comments-internal.md +342 -0
- data/internal/documentation/doc-comments.md +211 -0
- data/lib/roast/dsl/cog/config.rb +280 -4
- data/lib/roast/dsl/cog/input.rb +73 -0
- data/lib/roast/dsl/cog/output.rb +313 -0
- data/lib/roast/dsl/cog/registry.rb +71 -0
- data/lib/roast/dsl/cog/stack.rb +3 -2
- data/lib/roast/dsl/cog/store.rb +11 -8
- data/lib/roast/dsl/cog.rb +108 -31
- data/lib/roast/dsl/cog_input_context.rb +44 -0
- data/lib/roast/dsl/cog_input_manager.rb +156 -0
- data/lib/roast/dsl/cogs/agent/config.rb +465 -0
- data/lib/roast/dsl/cogs/agent/input.rb +81 -0
- data/lib/roast/dsl/cogs/agent/output.rb +59 -0
- data/lib/roast/dsl/cogs/agent/provider.rb +51 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/claude_invocation.rb +185 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/message.rb +73 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/assistant_message.rb +36 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/result_message.rb +61 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/system_message.rb +47 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/text_message.rb +36 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_result_message.rb +47 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/tool_use_message.rb +46 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/unknown_message.rb +27 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/messages/user_message.rb +37 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/tool_result.rb +51 -0
- data/lib/roast/dsl/cogs/agent/providers/claude/tool_use.rb +48 -0
- data/lib/roast/dsl/cogs/agent/providers/claude.rb +31 -0
- data/lib/roast/dsl/cogs/agent/stats.rb +92 -0
- data/lib/roast/dsl/cogs/agent/usage.rb +62 -0
- data/lib/roast/dsl/cogs/agent.rb +75 -0
- data/lib/roast/dsl/cogs/chat/config.rb +453 -0
- data/lib/roast/dsl/cogs/chat/input.rb +92 -0
- data/lib/roast/dsl/cogs/chat/output.rb +64 -0
- data/lib/roast/dsl/cogs/chat/session.rb +68 -0
- data/lib/roast/dsl/cogs/chat.rb +81 -0
- data/lib/roast/dsl/cogs/cmd.rb +291 -27
- data/lib/roast/dsl/cogs/ruby.rb +171 -0
- data/lib/roast/dsl/command_runner.rb +191 -0
- data/lib/roast/dsl/config_context.rb +2 -47
- data/lib/roast/dsl/config_manager.rb +143 -0
- data/lib/roast/dsl/control_flow.rb +41 -0
- data/lib/roast/dsl/execution_context.rb +9 -0
- data/lib/roast/dsl/execution_manager.rb +267 -0
- data/lib/roast/dsl/nil_assertions.rb +23 -0
- data/lib/roast/dsl/system_cog/params.rb +32 -0
- data/lib/roast/dsl/system_cog.rb +36 -0
- data/lib/roast/dsl/system_cogs/call.rb +162 -0
- data/lib/roast/dsl/system_cogs/map.rb +448 -0
- data/lib/roast/dsl/system_cogs/repeat.rb +242 -0
- data/lib/roast/dsl/workflow.rb +123 -0
- data/lib/roast/dsl/workflow_context.rb +20 -0
- data/lib/roast/dsl/workflow_params.rb +24 -0
- data/lib/roast/sorbet_runtime_stub.rb +154 -0
- data/lib/roast/tools/apply_diff.rb +1 -3
- data/lib/roast/tools/cmd.rb +4 -3
- data/lib/roast/tools/read_file.rb +1 -1
- data/lib/roast/tools/update_files.rb +1 -1
- data/lib/roast/tools/write_file.rb +1 -1
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/base_workflow.rb +4 -0
- data/lib/roast/workflow/step_loader.rb +14 -2
- data/lib/roast-ai.rb +4 -0
- data/lib/roast.rb +60 -22
- data/{roast.gemspec → roast-ai.gemspec} +10 -13
- data/sorbet/config +1 -0
- data/sorbet/rbi/gems/async@2.34.0.rbi +1577 -0
- data/sorbet/rbi/gems/cli-kit@5.2.0.rbi +2063 -0
- data/sorbet/rbi/gems/{cli-ui@2.3.0.rbi → cli-ui@2.7.0-6bdefd1d06305e5d6ae312ac76f9c88f88658dda.rbi} +1418 -1013
- data/sorbet/rbi/gems/console@1.34.2.rbi +1193 -0
- data/sorbet/rbi/gems/fiber-annotation@0.2.0.rbi +50 -0
- data/sorbet/rbi/gems/fiber-local@1.1.0.rbi +35 -0
- data/sorbet/rbi/gems/fiber-storage@1.0.1.rbi +41 -0
- data/sorbet/rbi/gems/io-event@1.14.0.rbi +724 -0
- data/sorbet/rbi/gems/marcel@1.1.0.rbi +239 -0
- data/sorbet/rbi/gems/metrics@0.15.0.rbi +9 -0
- data/sorbet/rbi/gems/ruby_llm@1.8.2.rbi +5703 -0
- data/sorbet/rbi/gems/traces@0.18.2.rbi +9 -0
- data/sorbet/rbi/shims/lib/roast/dsl/cog_input_context.rbi +1197 -0
- data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +314 -2
- data/sorbet/rbi/shims/lib/roast/dsl/execution_context.rbi +498 -0
- data/sorbet/tapioca/config.yml +6 -0
- data/sorbet/tapioca/require.rb +2 -0
- metadata +198 -34
- data/dsl/less_simple.rb +0 -112
- data/dsl/simple.rb +0 -8
- data/lib/roast/dsl/cog_execution_context.rb +0 -29
- data/lib/roast/dsl/cogs/graph.rb +0 -53
- data/lib/roast/dsl/cogs.rb +0 -65
- data/lib/roast/dsl/executor.rb +0 -82
- data/lib/roast/dsl/workflow_execution_context.rb +0 -47
- data/sorbet/rbi/gems/cgi@0.5.0.rbi +0 -2961
- data/sorbet/rbi/gems/claude_swarm@0.1.19.rbi +0 -568
- data/sorbet/rbi/gems/cli-kit@5.0.1.rbi +0 -1991
- data/sorbet/rbi/gems/dry-configurable@1.3.0.rbi +0 -672
- data/sorbet/rbi/gems/dry-core@1.1.0.rbi +0 -1894
- data/sorbet/rbi/gems/dry-inflector@1.2.0.rbi +0 -659
- data/sorbet/rbi/gems/dry-initializer@3.2.0.rbi +0 -781
- data/sorbet/rbi/gems/dry-logic@1.6.0.rbi +0 -1127
- data/sorbet/rbi/gems/dry-schema@1.14.1.rbi +0 -3727
- data/sorbet/rbi/gems/dry-types@1.8.3.rbi +0 -3969
- data/sorbet/rbi/gems/fast-mcp-annotations@1.5.3.rbi +0 -1588
- data/sorbet/rbi/gems/mime-types-data@3.2025.0617.rbi +0 -136
- data/sorbet/rbi/gems/mime-types@3.7.0.rbi +0 -1342
- data/sorbet/rbi/gems/rack@2.2.18.rbi +0 -5659
- data/sorbet/rbi/gems/rbs-inline@0.12.0.rbi +0 -2170
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
- data/sorbet/rbi/gems/yard@0.9.37.rbi +0 -18492
- 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
|