roast-ai 0.4.10 → 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 +19 -18
- data/Gemfile.lock +35 -58
- 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/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/prompts/simple_prompt.md.erb +3 -0
- data/dsl/prototype.rb +5 -7
- 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 +15 -1
- data/dsl/simple_repeat.rb +29 -0
- data/dsl/skip.rb +36 -0
- data/dsl/step_communication.rb +2 -3
- 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 +274 -3
- data/lib/roast/dsl/cog/input.rb +53 -10
- data/lib/roast/dsl/cog/output.rb +297 -8
- data/lib/roast/dsl/cog/registry.rb +35 -3
- data/lib/roast/dsl/cog/stack.rb +1 -1
- data/lib/roast/dsl/cog/store.rb +5 -5
- data/lib/roast/dsl/cog.rb +70 -14
- data/lib/roast/dsl/cog_input_context.rb +36 -1
- data/lib/roast/dsl/cog_input_manager.rb +116 -7
- 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 +59 -56
- data/lib/roast/dsl/cogs/cmd.rb +248 -61
- data/lib/roast/dsl/cogs/ruby.rb +171 -0
- data/lib/roast/dsl/command_runner.rb +191 -0
- data/lib/roast/dsl/config_manager.rb +58 -11
- data/lib/roast/dsl/control_flow.rb +41 -0
- data/lib/roast/dsl/execution_manager.rb +162 -32
- 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 +26 -16
- 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 +58 -21
- data/{roast.gemspec → roast-ai.gemspec} +9 -13
- 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/metrics@0.15.0.rbi +9 -0
- data/sorbet/rbi/gems/traces@0.18.2.rbi +9 -0
- data/sorbet/rbi/shims/lib/roast/dsl/cog_input_context.rbi +1185 -5
- data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +311 -5
- data/sorbet/rbi/shims/lib/roast/dsl/execution_context.rbi +486 -5
- data/sorbet/tapioca/config.yml +6 -0
- data/sorbet/tapioca/require.rb +2 -0
- metadata +157 -30
- data/dsl/less_simple.rb +0 -112
- data/dsl/scoped_executors.rb +0 -28
- data/dsl/simple.rb +0 -8
- data/lib/roast/dsl/cogs/execute.rb +0 -46
- data/lib/roast/dsl/cogs/graph.rb +0 -53
- 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.19.rbi +0 -5676
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
- 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,162 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module DSL
|
|
6
|
+
module SystemCogs
|
|
7
|
+
# Call cog for invoking named execution scopes
|
|
8
|
+
#
|
|
9
|
+
# Executes a named execution scope (defined with `execute(:name)`) with a provided value
|
|
10
|
+
# and index. The scope runs independently and can access the value and index through
|
|
11
|
+
# special variables.
|
|
12
|
+
class Call < SystemCog
|
|
13
|
+
class Config < Cog::Config; end
|
|
14
|
+
|
|
15
|
+
# Parameters for the call system cog
|
|
16
|
+
class Params < SystemCog::Params
|
|
17
|
+
# The name of the execution scope to invoke
|
|
18
|
+
#
|
|
19
|
+
#: Symbol
|
|
20
|
+
attr_accessor :run
|
|
21
|
+
|
|
22
|
+
#: (?Symbol?, run: Symbol) -> void
|
|
23
|
+
def initialize(name = nil, run:)
|
|
24
|
+
super(name)
|
|
25
|
+
@run = run
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Input for the call system cog
|
|
30
|
+
#
|
|
31
|
+
# Provides the value and index to be passed to the execution scope. The scope
|
|
32
|
+
# can access these through the implicit variables available in the execution context.
|
|
33
|
+
class Input < Cog::Input
|
|
34
|
+
# The value to pass to the execution scope
|
|
35
|
+
#
|
|
36
|
+
# This value becomes available in the called scope and can be accessed by steps
|
|
37
|
+
# within that scope. Required.
|
|
38
|
+
#
|
|
39
|
+
#: untyped
|
|
40
|
+
attr_accessor :value
|
|
41
|
+
|
|
42
|
+
# The index value to pass to the execution scope
|
|
43
|
+
#
|
|
44
|
+
# Defaults to 0. Can be used to track position when calling scopes in a sequence.
|
|
45
|
+
#
|
|
46
|
+
# Integer
|
|
47
|
+
attr_accessor :index
|
|
48
|
+
|
|
49
|
+
#: () -> void
|
|
50
|
+
def initialize
|
|
51
|
+
super
|
|
52
|
+
@index = 0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Validate that required input values are present
|
|
56
|
+
#
|
|
57
|
+
#: () -> void
|
|
58
|
+
def validate!
|
|
59
|
+
raise Cog::Input::InvalidInputError, "'value' is required" if value.nil? && !coerce_ran?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Coerce the input from the return value of the input block
|
|
63
|
+
#
|
|
64
|
+
# Sets the value from the input block's return value if not already set directly.
|
|
65
|
+
#
|
|
66
|
+
def coerce(input_return_value)
|
|
67
|
+
super
|
|
68
|
+
@value = input_return_value unless @value.present?
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Output from running the `call` cog
|
|
73
|
+
#
|
|
74
|
+
# Contains the result from the called scope. Use the `from` method to retrieve
|
|
75
|
+
# the final output from the scope's execution.
|
|
76
|
+
#
|
|
77
|
+
# #### See Also
|
|
78
|
+
# - `Roast::DSL::CogInputContext#from` - retrieves output from a `call` cog
|
|
79
|
+
class Output < Cog::Output
|
|
80
|
+
#: (ExecutionManager) -> void
|
|
81
|
+
def initialize(execution_manager)
|
|
82
|
+
super()
|
|
83
|
+
@execution_manager = execution_manager
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @requires_ancestor: Roast::DSL::ExecutionManager
|
|
88
|
+
module Manager
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
#: (Params, ^(Cog::Input) -> untyped) -> SystemCogs::Call
|
|
92
|
+
def create_call_system_cog(params, input_proc)
|
|
93
|
+
SystemCogs::Call.new(params.name, input_proc) do |input|
|
|
94
|
+
input = input #: as Input
|
|
95
|
+
raise ExecutionManager::ExecutionScopeNotSpecifiedError unless params.run.present?
|
|
96
|
+
|
|
97
|
+
em = ExecutionManager.new(
|
|
98
|
+
@cog_registry,
|
|
99
|
+
@config_manager,
|
|
100
|
+
@all_execution_procs,
|
|
101
|
+
@workflow_context,
|
|
102
|
+
scope: params.run,
|
|
103
|
+
scope_value: input.value,
|
|
104
|
+
scope_index: input.index,
|
|
105
|
+
)
|
|
106
|
+
em.prepare!
|
|
107
|
+
begin
|
|
108
|
+
em.run!
|
|
109
|
+
rescue ControlFlow::Break
|
|
110
|
+
# treat `break!` like `next!` in a `call` invocation
|
|
111
|
+
# TODO: maybe do something with the message passed to break!
|
|
112
|
+
end
|
|
113
|
+
Output.new(em)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @requires_ancestor: Roast::DSL::CogInputContext
|
|
119
|
+
module InputContext
|
|
120
|
+
# Retrieve the output from a `call` cog's execution scope
|
|
121
|
+
#
|
|
122
|
+
# Extracts the final output from the execution scope that was invoked by the call cog.
|
|
123
|
+
# When called without a block, returns the final output directly. When called with a block,
|
|
124
|
+
# executes the block in the context of the called scope's input context, receiving the final
|
|
125
|
+
# output as an argument.
|
|
126
|
+
#
|
|
127
|
+
# This allows you to access the results of a called scope and optionally transform them
|
|
128
|
+
# and/or access other outputs from within that scope.
|
|
129
|
+
#
|
|
130
|
+
# #### Usage
|
|
131
|
+
# ```ruby
|
|
132
|
+
# # Get the final output directly
|
|
133
|
+
# result = from(call!(:my_call))
|
|
134
|
+
#
|
|
135
|
+
# # Transform the output with a block
|
|
136
|
+
# transformed = from(call!(:my_call)) { |output| output.upcase }
|
|
137
|
+
#
|
|
138
|
+
# # Access other cog outputs from within the called scope
|
|
139
|
+
# inner_result = from(call!(:my_call)) { inner_cog!(:some_step) }
|
|
140
|
+
# ```
|
|
141
|
+
#
|
|
142
|
+
# #### See Also
|
|
143
|
+
# - `Roast::DSL::SystemCogs::Call::Output` - the output type from call cogs
|
|
144
|
+
#
|
|
145
|
+
# @rbs [T] (Roast::DSL::SystemCogs::Call::Output) {(untyped, untyped, Integer) -> T} -> T
|
|
146
|
+
# | (Roast::DSL::SystemCogs::Call::Output) -> untyped
|
|
147
|
+
def from(call_cog_output, &block)
|
|
148
|
+
em = call_cog_output.instance_variable_get(:@execution_manager)
|
|
149
|
+
raise CogInputContext::ContextNotFoundError if em.nil?
|
|
150
|
+
|
|
151
|
+
final_output = em.final_output
|
|
152
|
+
scope_value = em.instance_variable_get(:@scope_value).deep_dup
|
|
153
|
+
scope_index = em.instance_variable_get(:@scope_index)
|
|
154
|
+
return em.cog_input_context.instance_exec(final_output, scope_value, scope_index, &block) if block_given?
|
|
155
|
+
|
|
156
|
+
final_output
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -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
|