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
data/lib/roast/dsl/cog/config.rb
CHANGED
|
@@ -1,30 +1,306 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: true
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
module Roast
|
|
5
5
|
module DSL
|
|
6
6
|
class Cog
|
|
7
|
+
# Base configuration class for all cogs
|
|
8
|
+
#
|
|
9
|
+
# Provides common configuration methods and utilities for cog behavior.
|
|
10
|
+
# Cogs extend this class to define their own configuration options using either
|
|
11
|
+
# the `field` class method for simple fields or custom methods for complex configuration.
|
|
7
12
|
class Config
|
|
13
|
+
# Parent class for all configuration-related errors
|
|
14
|
+
class ConfigError < Roast::Error; end
|
|
15
|
+
|
|
16
|
+
# Raised when a configuration value is invalid or missing
|
|
17
|
+
class InvalidConfigError < ConfigError; end
|
|
18
|
+
|
|
19
|
+
# Validate that the config instance has all required parameters set in an acceptable manner
|
|
20
|
+
#
|
|
21
|
+
# Inheriting cogs should implement this method for their config class if validation is desired.
|
|
22
|
+
# This method is called after configuration is complete to ensure all required values are present
|
|
23
|
+
# and valid.
|
|
24
|
+
#
|
|
25
|
+
#: () -> void
|
|
26
|
+
def validate!; end
|
|
27
|
+
|
|
28
|
+
# The internal hash storing all configuration values
|
|
29
|
+
#
|
|
30
|
+
#: Hash[Symbol, untyped]
|
|
8
31
|
attr_reader :values
|
|
9
32
|
|
|
33
|
+
#: (?Hash[Symbol, untyped]) -> void
|
|
10
34
|
def initialize(initial = {})
|
|
11
35
|
@values = initial
|
|
12
36
|
end
|
|
13
37
|
|
|
38
|
+
# Merge another config object into this one, returning a new config instance
|
|
39
|
+
#
|
|
40
|
+
# Creates a new config object with values from both this config and the provided config.
|
|
41
|
+
# Values from the provided config take precedence over values from this config.
|
|
42
|
+
#
|
|
43
|
+
# #### See Also
|
|
44
|
+
# - `values`
|
|
45
|
+
#
|
|
46
|
+
#: (Cog::Config) -> Cog::Config
|
|
14
47
|
def merge(config_object)
|
|
15
48
|
self.class.new(values.merge(config_object.values))
|
|
16
49
|
end
|
|
17
50
|
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
51
|
+
# Set a configuration value using hash-style syntax
|
|
52
|
+
#
|
|
53
|
+
# This method provides basic key-value storage for cog configuration.
|
|
54
|
+
# All standard Roast cogs use imperative setter methods for config values.
|
|
55
|
+
# It is recommended that custom cogs implement their own config classes with similar methods
|
|
56
|
+
# for a more structured interface, but this hash-style syntax is provided for simple cases.
|
|
57
|
+
#
|
|
58
|
+
# #### See Also
|
|
59
|
+
# - `[]`
|
|
60
|
+
#
|
|
61
|
+
#: (Symbol, untyped) -> void
|
|
21
62
|
def []=(key, value)
|
|
22
63
|
@values[key] = value
|
|
23
64
|
end
|
|
24
65
|
|
|
66
|
+
# Get a configuration value using hash-style syntax
|
|
67
|
+
#
|
|
68
|
+
# This method provides basic key-value retrieval for cog configuration.
|
|
69
|
+
# All standard Roast cogs use imperative setter methods for config values.
|
|
70
|
+
# It is recommended that custom cogs implement their own config classes with similar methods
|
|
71
|
+
# for a more structured interface, but this hash-style syntax is provided for simple cases.
|
|
72
|
+
#
|
|
73
|
+
# #### See Also
|
|
74
|
+
# - `[]=`
|
|
75
|
+
#
|
|
76
|
+
#: (Symbol) -> untyped
|
|
25
77
|
def [](key)
|
|
26
78
|
@values[key]
|
|
27
79
|
end
|
|
80
|
+
|
|
81
|
+
class << self
|
|
82
|
+
# Define a configuration field with simple, out-of-the-box getter/setter behavior
|
|
83
|
+
# and default value handling
|
|
84
|
+
#
|
|
85
|
+
# #### Generated Methods
|
|
86
|
+
# This method creates two methods for a configuration field:
|
|
87
|
+
# 1. A dual-purpose method (`key`) that gets the value when called without arguments,
|
|
88
|
+
# or sets the value when called with an argument.
|
|
89
|
+
# 2. A bang method (`use_default_#{key}!`) that explicitly resets the field to its default value.
|
|
90
|
+
#
|
|
91
|
+
# When getting a value without arguments, the configured value is returned if set,
|
|
92
|
+
# otherwise the default value is returned.
|
|
93
|
+
# When setting a value with an argument, the validator block is applied if provided.
|
|
94
|
+
#
|
|
95
|
+
# #### Validation
|
|
96
|
+
#
|
|
97
|
+
# This method accepts an optional `validator` block that will be called with the new value
|
|
98
|
+
# when the field's setter method is invoked. The validator should raise an exception if the
|
|
99
|
+
# provided value is not valid. It's return value will be used as the new config value.
|
|
100
|
+
# This allows the validator to coerce an value into a standard form if desired.
|
|
101
|
+
#
|
|
102
|
+
# ##### See Also
|
|
103
|
+
# - `Cog::Config#validate!` - validates the config object as a whole, after all values have been set
|
|
104
|
+
#
|
|
105
|
+
# #### Parameters
|
|
106
|
+
# - `key` - The name of the configuration field
|
|
107
|
+
# - `default` - The default value for this field
|
|
108
|
+
# - `validator` - Optional block that validates and/or transforms the value before storing it
|
|
109
|
+
#
|
|
110
|
+
#: [T] (Symbol, T) ?{(T) -> T} -> void
|
|
111
|
+
def field(key, default, &validator)
|
|
112
|
+
default = default #: as untyped
|
|
113
|
+
|
|
114
|
+
define_method(key) do |*args|
|
|
115
|
+
if args.empty?
|
|
116
|
+
# with no args, return the configured value, or the default
|
|
117
|
+
@values[key] || default.deep_dup
|
|
118
|
+
else
|
|
119
|
+
# with an argument, set the configured value
|
|
120
|
+
new_value = args.first
|
|
121
|
+
@values[key] = validator ? validator.call(new_value) : new_value
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
define_method("use_default_#{key}!".to_sym) do
|
|
126
|
+
# explicitly set the configured value to the default
|
|
127
|
+
@values[key] = default.deep_dup
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Configure the cog to run asynchronously in the background
|
|
133
|
+
#
|
|
134
|
+
# When configured to run asynchronously, the cog will execute in the background
|
|
135
|
+
# and the next cog in the workflow will be able to start immediately without waiting
|
|
136
|
+
# for this cog to complete.
|
|
137
|
+
#
|
|
138
|
+
# If this cog has started running, attempts to access its output from another cog will
|
|
139
|
+
# block until this cog completes.
|
|
140
|
+
# If this cog has not yet started, attempts to access its output from another cog will
|
|
141
|
+
# fail in the same way that accessing the output of a synchronous cog that has not yet
|
|
142
|
+
# run would fail.
|
|
143
|
+
#
|
|
144
|
+
# The workflow will not complete until all asynchronous cogs have completed (or failed).
|
|
145
|
+
#
|
|
146
|
+
# #### Inverse Methods
|
|
147
|
+
# - `no_async!`
|
|
148
|
+
# - `sync!`
|
|
149
|
+
#
|
|
150
|
+
# #### See Also
|
|
151
|
+
# - `async?`
|
|
152
|
+
#
|
|
153
|
+
#: () -> void
|
|
154
|
+
def async!
|
|
155
|
+
@values[:async] = true
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Configure the cog __not__ to run asynchronously
|
|
159
|
+
#
|
|
160
|
+
# When configured not to run asynchronously, the cog will execute synchronously
|
|
161
|
+
# and the next cog in the workflow will wait for this cog to complete before starting.
|
|
162
|
+
#
|
|
163
|
+
# #### Alias Methods
|
|
164
|
+
# - `no_async!`
|
|
165
|
+
# - `sync!`
|
|
166
|
+
#
|
|
167
|
+
# #### Inverse Methods
|
|
168
|
+
# - `async!`
|
|
169
|
+
#
|
|
170
|
+
# #### See Also
|
|
171
|
+
# - `async?`
|
|
172
|
+
#
|
|
173
|
+
#: () -> void
|
|
174
|
+
def no_async!
|
|
175
|
+
@values[:async] = false
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Check if the cog is configured to run asynchronously
|
|
179
|
+
#
|
|
180
|
+
# #### See Also
|
|
181
|
+
# - `async!`
|
|
182
|
+
# - `no_async!`
|
|
183
|
+
# - `sync!`
|
|
184
|
+
#
|
|
185
|
+
#: () -> bool
|
|
186
|
+
def async?
|
|
187
|
+
!!@values[:async]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Configure the cog to abort the workflow immediately if it fails to complete successfully
|
|
191
|
+
#
|
|
192
|
+
# Enabled by default.
|
|
193
|
+
#
|
|
194
|
+
# #### Inverse Methods
|
|
195
|
+
# - `continue_on_failure!`
|
|
196
|
+
# - `no_abort_on_failure!`
|
|
197
|
+
#
|
|
198
|
+
# #### See Also
|
|
199
|
+
# - `abort_on_failure?`
|
|
200
|
+
#
|
|
201
|
+
#: () -> void
|
|
202
|
+
def abort_on_failure!
|
|
203
|
+
@values[:abort_on_failure] = true
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Configure the cog __not__ to abort the workflow if it fails to complete successfully
|
|
207
|
+
#
|
|
208
|
+
# When a cog is configured not to abort on failure, the workflow will continue to run subsequent cogs
|
|
209
|
+
# even if a cog fails. However, attempts to access that cog's output from another cog will fail.
|
|
210
|
+
#
|
|
211
|
+
# #### Alias Methods
|
|
212
|
+
# - `continue_on_failure!`
|
|
213
|
+
#
|
|
214
|
+
# #### Inverse Methods
|
|
215
|
+
# - `abort_on_failure!`
|
|
216
|
+
#
|
|
217
|
+
# #### See Also
|
|
218
|
+
# - `abort_on_failure?`
|
|
219
|
+
#
|
|
220
|
+
#: () -> void
|
|
221
|
+
def no_abort_on_failure!
|
|
222
|
+
@values[:abort_on_failure] = false
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Check if the cog is configured to abort the workflow immediately on failure
|
|
226
|
+
#
|
|
227
|
+
# #### See Also
|
|
228
|
+
# - `abort_on_failure!`
|
|
229
|
+
# - `continue_on_failure!`
|
|
230
|
+
# - `no_abort_on_failure!`
|
|
231
|
+
#
|
|
232
|
+
#: () -> bool
|
|
233
|
+
def abort_on_failure?
|
|
234
|
+
@values[:abort_on_failure] ||= true
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Configure the cog to run external commands in the specified working directory
|
|
238
|
+
#
|
|
239
|
+
# The directory given can be relative or absolute.
|
|
240
|
+
# If relative, it will be understood in relation to the directory from which Roast is invoked.
|
|
241
|
+
#
|
|
242
|
+
# ---
|
|
243
|
+
#
|
|
244
|
+
# __Important Note__: this configuration option only applies to external commands invoked by a cog
|
|
245
|
+
# It does not affect the working directory in which Roast is running.
|
|
246
|
+
#
|
|
247
|
+
# ---
|
|
248
|
+
#
|
|
249
|
+
# #### See Also
|
|
250
|
+
# - `use_current_working_directory!`
|
|
251
|
+
# - `valid_working_directory`
|
|
252
|
+
#
|
|
253
|
+
#: (String) -> void
|
|
254
|
+
def working_directory(directory)
|
|
255
|
+
@values[:working_directory] = directory
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Configure the cog to run in the directory from which Roast is invoked
|
|
259
|
+
#
|
|
260
|
+
# ---
|
|
261
|
+
#
|
|
262
|
+
# __Important Note__: this configuration option only applies to external commands invoked by a cog
|
|
263
|
+
# It does not affect the working directory in which Roast is running.
|
|
264
|
+
#
|
|
265
|
+
# ---
|
|
266
|
+
#
|
|
267
|
+
# #### See Also
|
|
268
|
+
# - `working_directory`
|
|
269
|
+
# - `valid_working_directory`
|
|
270
|
+
#
|
|
271
|
+
#: () -> void
|
|
272
|
+
def use_current_working_directory!
|
|
273
|
+
@values[:working_directory] = nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Get the validated, configured value for the working directory path in which the cog should run
|
|
277
|
+
#
|
|
278
|
+
# A value of `nil` means to use the current working directory.
|
|
279
|
+
# This method will raise an `InvalidConfigError` if the path does not exist or is not a directory.
|
|
280
|
+
#
|
|
281
|
+
# ---
|
|
282
|
+
#
|
|
283
|
+
# __Important Note__: this configuration option only applies to external commands invoked by a cog
|
|
284
|
+
# It does not affect the working directory in which Roast is running.
|
|
285
|
+
#
|
|
286
|
+
# ---
|
|
287
|
+
#
|
|
288
|
+
# #### See Also
|
|
289
|
+
# - `working_directory`
|
|
290
|
+
# - `use_current_working_directory!`
|
|
291
|
+
#
|
|
292
|
+
#: () -> Pathname?
|
|
293
|
+
def valid_working_directory
|
|
294
|
+
path = Pathname.new(@values[:working_directory]).expand_path if @values[:working_directory]
|
|
295
|
+
return unless path
|
|
296
|
+
raise InvalidConfigError, "working directory '#{path}' does not exist'" unless path.exist?
|
|
297
|
+
raise InvalidConfigError, "working directory '#{path}' is not a directory'" unless path.directory?
|
|
298
|
+
|
|
299
|
+
path
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
alias_method(:continue_on_failure!, :no_abort_on_failure!)
|
|
303
|
+
alias_method(:sync!, :no_async!)
|
|
28
304
|
end
|
|
29
305
|
end
|
|
30
306
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module DSL
|
|
6
|
+
class Cog
|
|
7
|
+
# Abstract parent class for inputs provided to a cog when it runs
|
|
8
|
+
#
|
|
9
|
+
# Cogs extend this class to define their own input types that specify what data the cog needs to execute.
|
|
10
|
+
# Input classes must be instantiatable with a no-argument constructor and expose methods to incrementally
|
|
11
|
+
# set their values.
|
|
12
|
+
#
|
|
13
|
+
# The input lifecycle:
|
|
14
|
+
# 1. An input instance is created when the cog is invoked
|
|
15
|
+
# 2. The `validate!` method is called to see if all required parameters are set correctly (errors are swallowed)
|
|
16
|
+
# 3. If validation fails, the `coerce` method is called with the return value from the input block (if provided)
|
|
17
|
+
# 4. The `validate!` method is called again to ensure all required parameters are set correctly (errors are raised)
|
|
18
|
+
# 4. The validated input is passed to the cog's `execute` method
|
|
19
|
+
class Input
|
|
20
|
+
# Parent class for all errors raised by the Roast::DSL::Input class
|
|
21
|
+
class InputError < Roast::Error; end
|
|
22
|
+
|
|
23
|
+
# Raised when validation fails on a cog's input object.
|
|
24
|
+
class InvalidInputError < InputError; end
|
|
25
|
+
|
|
26
|
+
# Validate that the input instance has all required parameters set in an acceptable manner
|
|
27
|
+
#
|
|
28
|
+
# Subclasses must implement this method to verify that the input is in a valid state before
|
|
29
|
+
# the cog executes. This method should raise an `InvalidInputError` if the input is not valid.
|
|
30
|
+
#
|
|
31
|
+
# #### See Also
|
|
32
|
+
# - `coerce`
|
|
33
|
+
#
|
|
34
|
+
#: () -> void
|
|
35
|
+
def validate!
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Use the value returned from the cog's input block to coerce the input to a valid state
|
|
40
|
+
#
|
|
41
|
+
# Subclasses may implement this method to automatically configure the input based on the return
|
|
42
|
+
# value from the input block. This is optional; if not implemented, the default behavior is to
|
|
43
|
+
# do nothing.
|
|
44
|
+
#
|
|
45
|
+
# #### See Also
|
|
46
|
+
# - `validate!`
|
|
47
|
+
#
|
|
48
|
+
#: (untyped) -> void
|
|
49
|
+
def coerce(input_return_value)
|
|
50
|
+
@coerce_ran = true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Determine whether the input's coerce method has already been attempted
|
|
56
|
+
#
|
|
57
|
+
# This can be useful for validate! to adapt its behaviour based on whether it is being called the first
|
|
58
|
+
# or second time.
|
|
59
|
+
#
|
|
60
|
+
# For instance, if an input has an attribute than can legitimately be `nil`, but the cog
|
|
61
|
+
# still wants to attempt coercion if the attribute is not set to a non-`nil` value initially, `validate!`
|
|
62
|
+
# can be implemented to raise `InvalidInputError` if the attribute is `nil` and `coerce_ran?` is `false`,
|
|
63
|
+
# but not to raise if `coerce_ran?` is `true`, to allow the input to be ultimately validated with a `nil`
|
|
64
|
+
# value for that attribute.
|
|
65
|
+
#
|
|
66
|
+
#: () -> bool
|
|
67
|
+
def coerce_ran?
|
|
68
|
+
@coerce_ran ||= false
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module DSL
|
|
6
|
+
class Cog
|
|
7
|
+
# Generic output from running a cog.
|
|
8
|
+
# Cogs should extend this class with their own output types.
|
|
9
|
+
class Output
|
|
10
|
+
# @requires_ancestor: Roast::DSL::Cog::Output
|
|
11
|
+
module WithJson
|
|
12
|
+
# Get parsed JSON from the output, raising an error if parsing fails
|
|
13
|
+
#
|
|
14
|
+
# This method attempts to parse JSON from the output text using multiple fallback strategies,
|
|
15
|
+
# including extracting from code blocks and JSON-like patterns. If the input is nil or empty,
|
|
16
|
+
# an empty hash is returned.
|
|
17
|
+
#
|
|
18
|
+
# #### See Also
|
|
19
|
+
# - `json`
|
|
20
|
+
#
|
|
21
|
+
#: () -> Hash[Symbol, untyped]
|
|
22
|
+
def json!
|
|
23
|
+
input = raw_text
|
|
24
|
+
return {} if input.nil? || input.strip.empty?
|
|
25
|
+
|
|
26
|
+
@json ||= parse_json_with_fallbacks(input)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get parsed JSON from the output, returning nil if parsing fails
|
|
30
|
+
#
|
|
31
|
+
# This method provides a safe alternative to `json!` that returns `nil` instead of raising
|
|
32
|
+
# an error when JSON parsing fails.
|
|
33
|
+
#
|
|
34
|
+
# #### See Also
|
|
35
|
+
# - `json!`
|
|
36
|
+
#
|
|
37
|
+
#: () -> Hash[Symbol, untyped]?
|
|
38
|
+
def json
|
|
39
|
+
json!
|
|
40
|
+
rescue JSON::ParserError
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Cogs should implement this method to provide the text value that should be parsed to provide the 'json' attribute
|
|
47
|
+
#
|
|
48
|
+
#: () -> String?
|
|
49
|
+
def raw_text
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Try parsing JSON from various possible formats in priority order
|
|
54
|
+
#
|
|
55
|
+
#: (String) -> Hash[Symbol, untyped]
|
|
56
|
+
def parse_json_with_fallbacks(input)
|
|
57
|
+
candidates = extract_json_candidates(input)
|
|
58
|
+
candidates.each do |candidate|
|
|
59
|
+
return JSON.parse(candidate.strip, symbolize_names: true)
|
|
60
|
+
rescue JSON::ParserError, TypeError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
raise JSON::ParserError, "Could not parse JSON from input:\n---\n#{input}\n---"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Extract potential JSON strings in priority order
|
|
67
|
+
#
|
|
68
|
+
#: (String) -> Array[String]
|
|
69
|
+
def extract_json_candidates(input)
|
|
70
|
+
[
|
|
71
|
+
input.strip, # 1. Entire input
|
|
72
|
+
*extract_code_blocks(input, "json").reverse, # 2. ```json blocks (last first)
|
|
73
|
+
*extract_code_blocks(input, nil).reverse, # 3. ``` blocks (last first)
|
|
74
|
+
*extract_code_blocks(input, :any).reverse, # 4. ```type blocks (last first)
|
|
75
|
+
*extract_json_like_blocks(input), # 5. { } or [ ] blocks (longest first)
|
|
76
|
+
].compact.uniq
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Extract code blocks with optional language specifier
|
|
80
|
+
# language can be: String (exact match), nil (no language), :any (any language except json/nil)
|
|
81
|
+
#
|
|
82
|
+
#: (String, String | Symbol | nil) -> Array[String]
|
|
83
|
+
def extract_code_blocks(input, language)
|
|
84
|
+
blocks = []
|
|
85
|
+
parts = input.split("```")
|
|
86
|
+
|
|
87
|
+
# Process pairs of splits (opening ``` and closing ```)
|
|
88
|
+
(1...parts.length).step(2) do |i|
|
|
89
|
+
block_with_header = parts[i]
|
|
90
|
+
next unless block_with_header
|
|
91
|
+
|
|
92
|
+
lines = block_with_header.lines
|
|
93
|
+
first_line = lines.first&.strip || ""
|
|
94
|
+
content = (lines[1..] || []).join
|
|
95
|
+
|
|
96
|
+
case language
|
|
97
|
+
when String
|
|
98
|
+
blocks << content if first_line == language
|
|
99
|
+
when nil
|
|
100
|
+
blocks << content if first_line.empty?
|
|
101
|
+
when :any
|
|
102
|
+
blocks << content if !first_line.empty? && first_line != "json"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
blocks
|
|
107
|
+
rescue
|
|
108
|
+
[]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Extract blocks that look like JSON objects or arrays
|
|
112
|
+
#
|
|
113
|
+
#: (String) -> Array[String]
|
|
114
|
+
def extract_json_like_blocks(input)
|
|
115
|
+
blocks = []
|
|
116
|
+
|
|
117
|
+
# Find all potential JSON blocks starting with { or [ and ending with } or ]
|
|
118
|
+
input.scan(/^[ \t]*([{\[].*?[}\]])[ \t]*$/m) do |match|
|
|
119
|
+
blocks << match[0]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Also try to find JSON anywhere in the text (not just at line boundaries)
|
|
123
|
+
input.scan(/([{\[](?:[^{}\[\]]|(?:\{(?:[^{}]|\{[^{}]*\})*\})|(?:\[(?:[^\[\]]|\[[^\[\]]*\])*\]))*[}\]])/m) do |match|
|
|
124
|
+
blocks << match[0]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Sort by length (longest first) and deduplicate
|
|
128
|
+
blocks.uniq.sort_by { |b| -b.length }
|
|
129
|
+
end
|
|
130
|
+
rescue
|
|
131
|
+
[]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @requires_ancestor: Roast::DSL::Cog::Output
|
|
135
|
+
module WithNumber
|
|
136
|
+
# Get parsed float from the output, raising an error if parsing fails
|
|
137
|
+
#
|
|
138
|
+
# This method attempts to parse a float from the output text using multiple permissive fallback
|
|
139
|
+
# strategies to extract a substring that looks like a number.
|
|
140
|
+
#
|
|
141
|
+
# Raises `ArgumentError` if output text does not contain any value that can be parsed as a number.
|
|
142
|
+
#
|
|
143
|
+
# #### See Also
|
|
144
|
+
# - `float`
|
|
145
|
+
# - `integer!`
|
|
146
|
+
# - `integer`
|
|
147
|
+
#
|
|
148
|
+
#: () -> Float
|
|
149
|
+
def float!
|
|
150
|
+
@float ||= parse_number_with_fallbacks(raw_text || "")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Get parsed float from the output, returning nil if parsing fails
|
|
154
|
+
#
|
|
155
|
+
# This method attempts to parse a float from the output text using multiple permissive fallback
|
|
156
|
+
# strategies to extract a substring that looks like a number.
|
|
157
|
+
#
|
|
158
|
+
# Returns `nil` if output text does not contain any value that can be parsed as a number.
|
|
159
|
+
#
|
|
160
|
+
# #### See Also
|
|
161
|
+
# - `float!`
|
|
162
|
+
# - `integer!`
|
|
163
|
+
# - `integer`
|
|
164
|
+
#
|
|
165
|
+
#: () -> Float?
|
|
166
|
+
def float
|
|
167
|
+
float!
|
|
168
|
+
rescue ArgumentError
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Get parsed integer from the output, raising an error if parsing fails
|
|
173
|
+
#
|
|
174
|
+
# This method attempts to parse an integer from the output text using multiple permissive fallback
|
|
175
|
+
# strategies to extract a substring that looks like a number. This method will attempt to parse
|
|
176
|
+
# and round a floating point value; it will not strictly match only integers in the source text.
|
|
177
|
+
#
|
|
178
|
+
# Raises `ArgumentError` if output text does not contain any value that can be parsed as a number.
|
|
179
|
+
#
|
|
180
|
+
# #### See Also
|
|
181
|
+
# - `integer`
|
|
182
|
+
# - `float!`
|
|
183
|
+
# - `float`
|
|
184
|
+
#
|
|
185
|
+
#: () -> Integer
|
|
186
|
+
def integer!
|
|
187
|
+
@integer ||= float!.round
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Get parsed integer from the output, returning nil if parsing fails
|
|
191
|
+
#
|
|
192
|
+
# This method attempts to parse an integer from the output text using multiple permissive fallback
|
|
193
|
+
# strategies to extract a substring that looks like a number. This method will attempt to parse
|
|
194
|
+
# and round a floating point value; it will not strictly match only integers in the source text.
|
|
195
|
+
#
|
|
196
|
+
# Returns `nil` if output text does not contain any value that can be parsed as a number.
|
|
197
|
+
#
|
|
198
|
+
# #### See Also
|
|
199
|
+
# - `integer!`
|
|
200
|
+
# - `float!`
|
|
201
|
+
# - `float`
|
|
202
|
+
#
|
|
203
|
+
#: () -> Integer?
|
|
204
|
+
def integer
|
|
205
|
+
integer!
|
|
206
|
+
rescue ArgumentError
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
private
|
|
211
|
+
|
|
212
|
+
# Try parsing a number from various possible formats in priority order
|
|
213
|
+
#
|
|
214
|
+
#: (String) -> Float
|
|
215
|
+
def parse_number_with_fallbacks(input)
|
|
216
|
+
candidates = extract_number_candidates(input)
|
|
217
|
+
candidates.each do |candidate|
|
|
218
|
+
normalized = normalize_number_string(candidate)
|
|
219
|
+
next if normalized.nil?
|
|
220
|
+
|
|
221
|
+
return Float(normalized)
|
|
222
|
+
rescue ArgumentError, TypeError
|
|
223
|
+
next
|
|
224
|
+
end
|
|
225
|
+
raise ArgumentError, "Could not parse number from input:\n---\n#{input}\n---"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Extract potential number strings in priority order
|
|
229
|
+
#
|
|
230
|
+
#: (String) -> Array[String]
|
|
231
|
+
def extract_number_candidates(input)
|
|
232
|
+
candidates = []
|
|
233
|
+
|
|
234
|
+
# 1. Try the entire string
|
|
235
|
+
candidates << input.strip
|
|
236
|
+
|
|
237
|
+
# 2. Try each line from bottom up (with whitespace stripped)
|
|
238
|
+
lines = input.lines.map(&:strip).reject(&:empty?)
|
|
239
|
+
candidates.concat(lines.reverse)
|
|
240
|
+
|
|
241
|
+
# 3. Try to extract numbers from each line from bottom up
|
|
242
|
+
lines.reverse.each do |line|
|
|
243
|
+
# Look for numbers with various separators, formats, and currency symbols (very permissive)
|
|
244
|
+
# Matches: 123, 1,234, 1_234, 1 234, 1.23, -1.23, 1.23e10, 1.23e-10
|
|
245
|
+
matches = line.scan(/-?[\d\s$¢£€¥.,_]+(?:[eE][+-]?\d+)?/) #: as Array[String]
|
|
246
|
+
candidates.concat(matches.map(&:strip))
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
candidates.compact.uniq
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Normalize a number string by removing separators and currency codes and validating format
|
|
253
|
+
#
|
|
254
|
+
#: (String) -> String?
|
|
255
|
+
def normalize_number_string(raw)
|
|
256
|
+
# Remove common digit separators and currency codes
|
|
257
|
+
normalized = raw.strip.gsub(/[\s$¢£€¥,_]/, "")
|
|
258
|
+
|
|
259
|
+
# Validate it looks like a number (optional minus, digits, optional decimal, optional scientific notation)
|
|
260
|
+
normalized if normalized.match?(/\A-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?\z/)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# @requires_ancestor: Roast::DSL::Cog::Output
|
|
265
|
+
module WithText
|
|
266
|
+
# Get the output as a single string with surrounding whitespace removed
|
|
267
|
+
#
|
|
268
|
+
# This method returns the text output with leading and trailing whitespace stripped.
|
|
269
|
+
#
|
|
270
|
+
# #### See Also
|
|
271
|
+
# - `lines`
|
|
272
|
+
#
|
|
273
|
+
#: () -> String
|
|
274
|
+
def text
|
|
275
|
+
raw_text.strip
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Get the output as an array of lines with each line's whitespace stripped
|
|
279
|
+
#
|
|
280
|
+
# This method splits the output into individual lines and removes leading and trailing
|
|
281
|
+
# whitespace from each line.
|
|
282
|
+
#
|
|
283
|
+
# #### See Also
|
|
284
|
+
# - `text`
|
|
285
|
+
#
|
|
286
|
+
#: () -> Array[String]
|
|
287
|
+
def lines
|
|
288
|
+
raw_text.lines.map(&:strip)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
private
|
|
292
|
+
|
|
293
|
+
# Cogs should implement this method to provide the text value of their output
|
|
294
|
+
#
|
|
295
|
+
#: () -> String
|
|
296
|
+
def raw_text
|
|
297
|
+
raise NotImplementedError
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
private
|
|
302
|
+
|
|
303
|
+
# Cogs should implement this method to provide the text value that should be parsed to provide the
|
|
304
|
+
# the values produced by the `WithText`, `WithJson`, and `WithNumber` modules.
|
|
305
|
+
#
|
|
306
|
+
#: () -> String?
|
|
307
|
+
def raw_text
|
|
308
|
+
raise NotImplementedError
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|