roast-ai 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yaml +2 -2
- data/.gitignore +1 -0
- data/CHANGELOG.md +85 -0
- data/CLAUDE.md +106 -9
- data/Gemfile +4 -1
- data/Gemfile.lock +70 -16
- data/README.md +159 -8
- data/bin/console +1 -0
- data/bin/roast +1 -1
- data/claude-swarm.yml +210 -0
- data/docs/AGENT_STEPS.md +288 -0
- data/docs/VALIDATION.md +178 -0
- data/examples/agent_continue/add_documentation/prompt.md +5 -0
- data/examples/agent_continue/add_error_handling/prompt.md +5 -0
- data/examples/agent_continue/analyze_codebase/prompt.md +7 -0
- data/examples/agent_continue/combined_workflow.yml +24 -0
- data/examples/agent_continue/continue_adding_features/prompt.md +4 -0
- data/examples/agent_continue/create_integration_tests/prompt.md +3 -0
- data/examples/agent_continue/document_with_context/prompt.md +5 -0
- data/examples/agent_continue/explore_api/prompt.md +6 -0
- data/examples/agent_continue/implement_client/prompt.md +6 -0
- data/examples/agent_continue/inline_workflow.yml +20 -0
- data/examples/agent_continue/refactor_code/prompt.md +2 -0
- data/examples/agent_continue/verify_changes/prompt.md +6 -0
- data/examples/agent_continue/workflow.yml +27 -0
- data/examples/agent_workflow/README.md +75 -0
- data/examples/agent_workflow/apply_refactorings/prompt.md +22 -0
- data/examples/agent_workflow/identify_code_smells/prompt.md +15 -0
- data/examples/agent_workflow/summarize_improvements/prompt.md +18 -0
- data/examples/agent_workflow/workflow.png +0 -0
- data/examples/agent_workflow/workflow.yml +16 -0
- data/examples/api_workflow/workflow.png +0 -0
- data/examples/apply_diff_demo/README.md +58 -0
- data/examples/apply_diff_demo/apply_simple_change/prompt.md +13 -0
- data/examples/apply_diff_demo/create_sample_file/prompt.md +11 -0
- data/examples/apply_diff_demo/workflow.yml +24 -0
- data/examples/available_tools_demo/README.md +42 -0
- data/examples/available_tools_demo/analyze_files/prompt.md +6 -0
- data/examples/available_tools_demo/explore_directory/prompt.md +6 -0
- data/examples/available_tools_demo/workflow.png +0 -0
- data/examples/available_tools_demo/workflow.yml +32 -0
- data/examples/available_tools_demo/write_summary/prompt.md +6 -0
- data/examples/bash_prototyping/api_testing.png +0 -0
- data/examples/bash_prototyping/system_analysis.png +0 -0
- data/examples/case_when/detect_language/prompt.md +2 -2
- data/examples/case_when/workflow.png +0 -0
- data/examples/cmd/basic_workflow.png +0 -0
- data/examples/cmd/dev_workflow.png +0 -0
- data/examples/cmd/explorer_workflow.png +0 -0
- data/examples/conditional/simple_workflow.png +0 -0
- data/examples/conditional/workflow.png +0 -0
- data/examples/context_management_demo/README.md +43 -0
- data/examples/context_management_demo/workflow.yml +42 -0
- data/examples/direct_coerce_syntax/workflow.png +0 -0
- data/examples/dot_notation/workflow.png +0 -0
- data/examples/exit_on_error/workflow.png +0 -0
- data/examples/grading/run_coverage.rb +0 -2
- data/examples/grading/workflow.png +0 -0
- data/examples/interpolation/workflow.png +0 -0
- data/examples/interpolation/workflow.yml +1 -1
- data/examples/iteration/analyze_complexity/prompt.md +2 -2
- data/examples/iteration/generate_recommendations/prompt.md +2 -2
- data/examples/iteration/implement_fix/prompt.md +2 -2
- data/examples/iteration/prioritize_issues/prompt.md +1 -1
- data/examples/iteration/prompts/analyze_file.md +2 -2
- data/examples/iteration/prompts/generate_summary.md +1 -1
- data/examples/iteration/prompts/update_report.md +3 -3
- data/examples/iteration/prompts/write_report.md +3 -3
- data/examples/iteration/read_file/prompt.md +2 -2
- data/examples/iteration/select_next_issue/prompt.md +2 -2
- data/examples/iteration/update_fix_count/prompt.md +4 -4
- data/examples/iteration/verify_fix/prompt.md +3 -3
- data/examples/iteration/workflow.png +0 -0
- data/examples/json_handling/workflow.png +0 -0
- data/examples/mcp/README.md +3 -3
- data/examples/mcp/analyze_changes/prompt.md +1 -1
- data/examples/mcp/database_workflow.png +0 -0
- data/examples/mcp/database_workflow.yml +1 -1
- data/examples/mcp/env_demo/workflow.png +0 -0
- data/examples/mcp/fetch_pr_context/prompt.md +1 -1
- data/examples/mcp/filesystem_demo/workflow.png +0 -0
- data/examples/mcp/github_workflow.png +0 -0
- data/examples/mcp/github_workflow.yml +1 -1
- data/examples/mcp/multi_mcp_workflow.png +0 -0
- data/examples/mcp/post_review/prompt.md +1 -1
- data/examples/mcp/workflow.png +0 -0
- data/examples/no_model_fallback/README.md +17 -0
- data/examples/no_model_fallback/analyze_file/prompt.md +1 -0
- data/examples/no_model_fallback/analyze_patterns/prompt.md +27 -0
- data/examples/no_model_fallback/generate_report_for_md/prompt.md +10 -0
- data/examples/no_model_fallback/generate_report_for_rb/prompt.md +3 -0
- data/examples/no_model_fallback/sample.rb +42 -0
- data/examples/no_model_fallback/workflow.yml +19 -0
- data/examples/openrouter_example/workflow.png +0 -0
- data/examples/pre_post_processing/analyze_test_file/prompt.md +1 -1
- data/examples/pre_post_processing/improve_test_coverage/prompt.md +1 -1
- data/examples/pre_post_processing/optimize_test_performance/prompt.md +1 -1
- data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +2 -2
- data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +1 -1
- data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +1 -1
- data/examples/pre_post_processing/validate_changes/prompt.md +2 -2
- data/examples/pre_post_processing/workflow.png +0 -0
- data/examples/rspec_to_minitest/workflow.png +0 -0
- data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
- data/examples/shared_config/shared.png +0 -0
- data/examples/single_target_prepost/workflow.png +0 -0
- data/examples/smart_coercion_defaults/workflow.png +0 -0
- data/examples/step_configuration/workflow.png +0 -0
- data/examples/swarm_example.yml +25 -0
- data/examples/tool_config_example/workflow.png +0 -0
- data/examples/user_input/README.md +90 -0
- data/examples/user_input/funny_name/create_backstory/prompt.md +10 -0
- data/examples/user_input/funny_name/workflow.png +0 -0
- data/examples/user_input/funny_name/workflow.yml +26 -0
- data/examples/user_input/generate_summary/prompt.md +11 -0
- data/examples/user_input/simple_input_demo/workflow.png +0 -0
- data/examples/user_input/simple_input_demo/workflow.yml +35 -0
- data/examples/user_input/survey_workflow.png +0 -0
- data/examples/user_input/survey_workflow.yml +71 -0
- data/examples/user_input/welcome_message/prompt.md +3 -0
- data/examples/user_input/workflow.png +0 -0
- data/examples/user_input/workflow.yml +73 -0
- data/examples/workflow_generator/create_workflow_files/prompt.md +1 -1
- data/examples/workflow_generator/workflow.png +0 -0
- data/lib/roast/errors.rb +6 -4
- data/lib/roast/helpers/function_caching_interceptor.rb +0 -2
- data/lib/roast/helpers/logger.rb +12 -35
- data/lib/roast/helpers/minitest_coverage_runner.rb +0 -1
- data/lib/roast/helpers/prompt_loader.rb +0 -2
- data/lib/roast/helpers/timeout_handler.rb +91 -0
- data/lib/roast/resources/api_resource.rb +0 -4
- data/lib/roast/resources/url_resource.rb +0 -3
- data/lib/roast/resources.rb +0 -8
- data/lib/roast/services/context_threshold_checker.rb +42 -0
- data/lib/roast/services/token_counting_service.rb +44 -0
- data/lib/roast/tools/apply_diff.rb +128 -0
- data/lib/roast/tools/ask_user.rb +0 -2
- data/lib/roast/tools/bash.rb +12 -9
- data/lib/roast/tools/cmd.rb +29 -12
- data/lib/roast/tools/coding_agent.rb +65 -17
- data/lib/roast/tools/context_summarizer.rb +108 -0
- data/lib/roast/tools/grep.rb +0 -3
- data/lib/roast/tools/helpers/coding_agent_message_formatter.rb +1 -4
- data/lib/roast/tools/read_file.rb +0 -2
- data/lib/roast/tools/search_file.rb +0 -2
- data/lib/roast/tools/swarm.rb +124 -0
- data/lib/roast/tools/update_files.rb +0 -4
- data/lib/roast/tools/write_file.rb +0 -3
- data/lib/roast/tools.rb +0 -13
- data/lib/roast/value_objects/step_name.rb +14 -3
- data/lib/roast/value_objects/workflow_path.rb +0 -2
- data/lib/roast/value_objects.rb +4 -4
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/agent_step.rb +33 -0
- data/lib/roast/workflow/api_configuration.rb +0 -4
- data/lib/roast/workflow/base_iteration_step.rb +3 -6
- data/lib/roast/workflow/base_step.rb +54 -28
- data/lib/roast/workflow/base_workflow.rb +43 -23
- data/lib/roast/workflow/case_executor.rb +0 -1
- data/lib/roast/workflow/case_step.rb +0 -4
- data/lib/roast/workflow/command_executor.rb +0 -2
- data/lib/roast/workflow/conditional_executor.rb +0 -1
- data/lib/roast/workflow/conditional_step.rb +0 -4
- data/lib/roast/workflow/configuration.rb +5 -67
- data/lib/roast/workflow/configuration_loader.rb +63 -3
- data/lib/roast/workflow/configuration_parser.rb +1 -7
- data/lib/roast/workflow/context_manager.rb +89 -0
- data/lib/roast/workflow/dot_access_hash.rb +16 -1
- data/lib/roast/workflow/each_step.rb +1 -1
- data/lib/roast/workflow/error_handler.rb +0 -3
- data/lib/roast/workflow/expression_evaluator.rb +0 -3
- data/lib/roast/workflow/file_state_repository.rb +0 -5
- data/lib/roast/workflow/input_executor.rb +41 -0
- data/lib/roast/workflow/input_step.rb +163 -0
- data/lib/roast/workflow/iteration_executor.rb +0 -2
- data/lib/roast/workflow/output_handler.rb +1 -3
- data/lib/roast/workflow/output_manager.rb +0 -2
- data/lib/roast/workflow/repeat_step.rb +1 -1
- data/lib/roast/workflow/replay_handler.rb +1 -4
- data/lib/roast/workflow/resource_resolver.rb +0 -3
- data/lib/roast/workflow/session_manager.rb +0 -3
- data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
- data/lib/roast/workflow/state_manager.rb +2 -4
- data/lib/roast/workflow/state_repository_factory.rb +36 -0
- data/lib/roast/workflow/step_completion_reporter.rb +27 -0
- data/lib/roast/workflow/step_executor_coordinator.rb +48 -24
- data/lib/roast/workflow/step_executor_factory.rb +0 -5
- data/lib/roast/workflow/step_executor_registry.rb +1 -4
- data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
- data/lib/roast/workflow/step_executors/hash_step_executor.rb +0 -3
- data/lib/roast/workflow/step_executors/parallel_step_executor.rb +0 -3
- data/lib/roast/workflow/step_executors/string_step_executor.rb +0 -2
- data/lib/roast/workflow/step_factory.rb +56 -0
- data/lib/roast/workflow/step_loader.rb +31 -17
- data/lib/roast/workflow/step_name_extractor.rb +84 -0
- data/lib/roast/workflow/step_orchestrator.rb +3 -2
- data/lib/roast/workflow/step_type_resolver.rb +28 -1
- data/lib/roast/workflow/validation_command.rb +197 -0
- data/lib/roast/workflow/validator.rb +0 -4
- data/lib/roast/workflow/validators/base_validator.rb +44 -0
- data/lib/roast/workflow/validators/dependency_validator.rb +223 -0
- data/lib/roast/workflow/validators/linting_validator.rb +113 -0
- data/lib/roast/workflow/validators/schema_validator.rb +90 -0
- data/lib/roast/workflow/validators/step_collector.rb +57 -0
- data/lib/roast/workflow/validators/validation_orchestrator.rb +52 -0
- data/lib/roast/workflow/workflow_executor.rb +11 -20
- data/lib/roast/workflow/workflow_initializer.rb +1 -8
- data/lib/roast/workflow/workflow_runner.rb +6 -7
- data/lib/roast/workflow.rb +0 -15
- data/lib/roast/workflow_diagram_generator.rb +298 -0
- data/lib/roast.rb +212 -10
- data/roast.gemspec +4 -2
- data/schema/workflow.json +123 -1
- metadata +143 -6
- data/lib/roast/helpers.rb +0 -12
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Factory for creating the appropriate StateRepository implementation
|
6
|
+
class StateRepositoryFactory
|
7
|
+
class << self
|
8
|
+
def create(type = nil)
|
9
|
+
type ||= default_type
|
10
|
+
|
11
|
+
case type.to_s
|
12
|
+
when "sqlite"
|
13
|
+
# Lazy load the SQLite repository only when needed
|
14
|
+
Roast::Workflow::SqliteStateRepository.new
|
15
|
+
when "file", "filesystem"
|
16
|
+
Roast::Workflow::FileStateRepository.new
|
17
|
+
else
|
18
|
+
raise ArgumentError, "Unknown state repository type: #{type}. Valid types are: sqlite, file"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def default_type
|
25
|
+
# Check environment variable first (for backwards compatibility)
|
26
|
+
if ENV["ROAST_STATE_STORAGE"]
|
27
|
+
ENV["ROAST_STATE_STORAGE"].downcase
|
28
|
+
else
|
29
|
+
# Default to SQLite for better functionality
|
30
|
+
"sqlite"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Reports step completion with token consumption information
|
6
|
+
class StepCompletionReporter
|
7
|
+
def initialize(output: $stderr)
|
8
|
+
@output = output
|
9
|
+
end
|
10
|
+
|
11
|
+
def report(step_name, tokens_consumed, total_tokens)
|
12
|
+
formatted_consumed = number_with_delimiter(tokens_consumed)
|
13
|
+
formatted_total = number_with_delimiter(total_tokens)
|
14
|
+
|
15
|
+
@output.puts "✓ Complete: #{step_name} (consumed #{formatted_consumed} tokens, total #{formatted_total})"
|
16
|
+
@output.puts
|
17
|
+
@output.puts
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def number_with_delimiter(number)
|
23
|
+
number.to_s.gsub(/(\d)(?=(\d{3})+(?!\d))/, '\\1,')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,10 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roast/workflow/case_executor"
|
4
|
-
require "roast/workflow/conditional_executor"
|
5
|
-
require "roast/workflow/step_executor_factory"
|
6
|
-
require "roast/workflow/step_type_resolver"
|
7
|
-
|
8
3
|
module Roast
|
9
4
|
module Workflow
|
10
5
|
# Coordinates the execution of different types of steps
|
@@ -29,20 +24,21 @@ module Roast
|
|
29
24
|
|
30
25
|
# Execute a list of steps
|
31
26
|
def execute_steps(workflow_steps)
|
32
|
-
workflow_steps.
|
27
|
+
workflow_steps.each_with_index do |step, index|
|
28
|
+
is_last_step = (index == workflow_steps.length - 1)
|
33
29
|
case step
|
34
30
|
when Hash
|
35
|
-
execute(step)
|
31
|
+
execute(step, is_last_step:)
|
36
32
|
when Array
|
37
|
-
execute(step)
|
33
|
+
execute(step, is_last_step:)
|
38
34
|
when String
|
39
|
-
execute(step)
|
35
|
+
execute(step, is_last_step:)
|
40
36
|
# Handle pause after string steps
|
41
37
|
if @context.workflow.pause_step_name == step
|
42
38
|
Kernel.binding.irb # rubocop:disable Lint/Debugger
|
43
39
|
end
|
44
40
|
else
|
45
|
-
step_orchestrator.execute_step(step)
|
41
|
+
step_orchestrator.execute_step(step, is_last_step: is_last_step)
|
46
42
|
end
|
47
43
|
end
|
48
44
|
end
|
@@ -63,16 +59,20 @@ module Roast
|
|
63
59
|
when StepTypeResolver::COMMAND_STEP
|
64
60
|
# Command steps should also go through interpolation
|
65
61
|
execute_string_step(step, options)
|
62
|
+
when StepTypeResolver::AGENT_STEP
|
63
|
+
execute_agent_step(step, options)
|
66
64
|
when StepTypeResolver::GLOB_STEP
|
67
|
-
execute_glob_step(step)
|
65
|
+
execute_glob_step(step, options)
|
68
66
|
when StepTypeResolver::ITERATION_STEP
|
69
|
-
execute_iteration_step(step)
|
67
|
+
execute_iteration_step(step, options)
|
70
68
|
when StepTypeResolver::CONDITIONAL_STEP
|
71
|
-
execute_conditional_step(step)
|
69
|
+
execute_conditional_step(step, options)
|
72
70
|
when StepTypeResolver::CASE_STEP
|
73
|
-
execute_case_step(step)
|
71
|
+
execute_case_step(step, options)
|
72
|
+
when StepTypeResolver::INPUT_STEP
|
73
|
+
execute_input_step(step, options)
|
74
74
|
when StepTypeResolver::HASH_STEP
|
75
|
-
execute_hash_step(step)
|
75
|
+
execute_hash_step(step, options)
|
76
76
|
when StepTypeResolver::PARALLEL_STEP
|
77
77
|
# Use factory for parallel steps
|
78
78
|
executor = StepExecutorFactory.for(step, workflow_executor)
|
@@ -117,6 +117,15 @@ module Roast
|
|
117
117
|
)
|
118
118
|
end
|
119
119
|
|
120
|
+
def input_executor
|
121
|
+
@input_executor ||= dependencies[:input_executor] || InputExecutor.new(
|
122
|
+
context.workflow,
|
123
|
+
context.context_path,
|
124
|
+
dependencies[:state_manager] || dependencies[:workflow_executor].state_manager,
|
125
|
+
workflow_executor,
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
120
129
|
def step_orchestrator
|
121
130
|
dependencies[:step_orchestrator]
|
122
131
|
end
|
@@ -171,11 +180,20 @@ module Roast
|
|
171
180
|
end
|
172
181
|
end
|
173
182
|
|
174
|
-
def
|
183
|
+
def execute_agent_step(step, options = {})
|
184
|
+
# Extract the step name without the ^ prefix
|
185
|
+
step_name = StepTypeResolver.extract_name(step)
|
186
|
+
|
187
|
+
# Load and execute the agent step
|
188
|
+
exit_on_error = options.fetch(:exit_on_error, context.exit_on_error?(step))
|
189
|
+
step_orchestrator.execute_step(step_name, exit_on_error:, step_key: options[:step_key], agent_type: :coding_agent)
|
190
|
+
end
|
191
|
+
|
192
|
+
def execute_glob_step(step, options = {})
|
175
193
|
Dir.glob(step).join("\n")
|
176
194
|
end
|
177
195
|
|
178
|
-
def execute_iteration_step(step)
|
196
|
+
def execute_iteration_step(step, options = {})
|
179
197
|
name = step.keys.first
|
180
198
|
command = step[name]
|
181
199
|
|
@@ -188,15 +206,19 @@ module Roast
|
|
188
206
|
end
|
189
207
|
end
|
190
208
|
|
191
|
-
def execute_conditional_step(step)
|
209
|
+
def execute_conditional_step(step, options = {})
|
192
210
|
conditional_executor.execute_conditional(step)
|
193
211
|
end
|
194
212
|
|
195
|
-
def execute_case_step(step)
|
213
|
+
def execute_case_step(step, options = {})
|
196
214
|
case_executor.execute_case(step)
|
197
215
|
end
|
198
216
|
|
199
|
-
def
|
217
|
+
def execute_input_step(step, options = {})
|
218
|
+
input_executor.execute_input(step["input"])
|
219
|
+
end
|
220
|
+
|
221
|
+
def execute_hash_step(step, options = {})
|
200
222
|
name, command = step.to_a.flatten
|
201
223
|
interpolated_name = interpolator.interpolate(name)
|
202
224
|
|
@@ -208,7 +230,8 @@ module Roast
|
|
208
230
|
|
209
231
|
# Execute the command directly using the appropriate executor
|
210
232
|
# Pass the original key name for configuration lookup
|
211
|
-
|
233
|
+
# Merge options to preserve is_last_step
|
234
|
+
result = execute(interpolated_command, { exit_on_error:, step_key: interpolated_name }.merge(options))
|
212
235
|
context.workflow.output[interpolated_name] = result
|
213
236
|
result
|
214
237
|
end
|
@@ -225,17 +248,18 @@ module Roast
|
|
225
248
|
if StepTypeResolver.command_step?(interpolated_step)
|
226
249
|
# Command step - execute directly, preserving any passed options
|
227
250
|
exit_on_error = options.fetch(:exit_on_error, true)
|
228
|
-
execute_command_step(interpolated_step, { exit_on_error:
|
251
|
+
execute_command_step(interpolated_step, { exit_on_error: })
|
229
252
|
else
|
230
253
|
exit_on_error = options.fetch(:exit_on_error, context.exit_on_error?(step))
|
231
|
-
execute_standard_step(interpolated_step, options.merge(exit_on_error:
|
254
|
+
execute_standard_step(interpolated_step, options.merge(exit_on_error:))
|
232
255
|
end
|
233
256
|
end
|
234
257
|
|
235
258
|
def execute_standard_step(step, options)
|
236
259
|
exit_on_error = options.fetch(:exit_on_error, true)
|
237
260
|
step_key = options[:step_key]
|
238
|
-
|
261
|
+
is_last_step = options[:is_last_step]
|
262
|
+
step_orchestrator.execute_step(step, exit_on_error:, step_key:, is_last_step:)
|
239
263
|
end
|
240
264
|
|
241
265
|
def validate_each_step!(step)
|
@@ -1,10 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roast/workflow/step_executor_registry"
|
4
|
-
require "roast/workflow/step_executors/hash_step_executor"
|
5
|
-
require "roast/workflow/step_executors/parallel_step_executor"
|
6
|
-
require "roast/workflow/step_executors/string_step_executor"
|
7
|
-
|
8
3
|
module Roast
|
9
4
|
module Workflow
|
10
5
|
# Factory for creating step executors - now delegates to registry
|
@@ -43,10 +43,7 @@ module Roast
|
|
43
43
|
def clear!
|
44
44
|
@executors.clear
|
45
45
|
@type_matchers.clear
|
46
|
-
|
47
|
-
if defined?(StepExecutorFactory)
|
48
|
-
StepExecutorFactory.instance_variable_set(:@defaults_registered, false)
|
49
|
-
end
|
46
|
+
StepExecutorFactory.instance_variable_set(:@defaults_registered, false)
|
50
47
|
end
|
51
48
|
|
52
49
|
# Get all registered executors (useful for debugging)
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Decorator that adds token consumption reporting to step execution
|
6
|
+
class StepExecutorWithReporting
|
7
|
+
def initialize(base_executor, context, output: $stderr)
|
8
|
+
@base_executor = base_executor
|
9
|
+
@context = context
|
10
|
+
@reporter = StepCompletionReporter.new(output: output)
|
11
|
+
@name_extractor = StepNameExtractor.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(step, options = {})
|
15
|
+
# Track tokens before execution
|
16
|
+
tokens_before = @context.workflow.context_manager&.total_tokens || 0
|
17
|
+
|
18
|
+
# Execute the step
|
19
|
+
result = @base_executor.execute(step, options)
|
20
|
+
|
21
|
+
# Report token consumption after successful execution
|
22
|
+
tokens_after = @context.workflow.context_manager&.total_tokens || 0
|
23
|
+
tokens_consumed = tokens_after - tokens_before
|
24
|
+
|
25
|
+
step_type = StepTypeResolver.resolve(step, @context)
|
26
|
+
step_name = @name_extractor.extract(step, step_type)
|
27
|
+
@reporter.report(step_name, tokens_consumed, tokens_after)
|
28
|
+
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
# Override execute_steps to ensure reporting happens for each step
|
33
|
+
def execute_steps(workflow_steps)
|
34
|
+
workflow_steps.each_with_index do |step, index|
|
35
|
+
is_last_step = (index == workflow_steps.length - 1)
|
36
|
+
case step
|
37
|
+
when Hash
|
38
|
+
execute(step, is_last_step:)
|
39
|
+
when Array
|
40
|
+
execute(step, is_last_step:)
|
41
|
+
when String
|
42
|
+
execute(step, is_last_step:)
|
43
|
+
# Handle pause after string steps
|
44
|
+
if @context.workflow.pause_step_name == step
|
45
|
+
Kernel.binding.irb # rubocop:disable Lint/Debugger
|
46
|
+
end
|
47
|
+
else
|
48
|
+
# For other types, delegate to base executor
|
49
|
+
execute(step, is_last_step:)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Delegate all other methods to the base executor
|
55
|
+
def method_missing(method, *args, **kwargs, &block)
|
56
|
+
if @base_executor.respond_to?(method)
|
57
|
+
@base_executor.send(method, *args, **kwargs, &block)
|
58
|
+
else
|
59
|
+
super
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def respond_to_missing?(method, include_private = false)
|
64
|
+
@base_executor.respond_to?(method, include_private) || super
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Factory for creating step instances based on step characteristics
|
6
|
+
class StepFactory
|
7
|
+
class << self
|
8
|
+
# Create a step instance based on the step type and characteristics
|
9
|
+
#
|
10
|
+
# @param workflow [BaseWorkflow] The workflow instance
|
11
|
+
# @param step_name [String, StepName] The name of the step
|
12
|
+
# @param options [Hash] Additional options for step creation
|
13
|
+
# @return [BaseStep] The appropriate step instance
|
14
|
+
def create(workflow, step_name, options = {})
|
15
|
+
name = normalize_step_name(step_name)
|
16
|
+
|
17
|
+
# Determine the step class based on characteristics
|
18
|
+
step_class = determine_step_class(name, options)
|
19
|
+
|
20
|
+
# Create the step instance with appropriate parameters
|
21
|
+
build_step_instance(step_class, workflow, name, options)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def normalize_step_name(step_name)
|
27
|
+
step_name.is_a?(Roast::ValueObjects::StepName) ? step_name : Roast::ValueObjects::StepName.new(step_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def determine_step_class(name, options)
|
31
|
+
# Check if this is an agent step (indicated by special processing needs)
|
32
|
+
if options[:agent_type] == :coding_agent
|
33
|
+
Roast::Workflow::AgentStep
|
34
|
+
elsif name.plain_text?
|
35
|
+
# Plain text steps are always prompt steps
|
36
|
+
options[:agent_type] == :coding_agent ? Roast::Workflow::AgentStep : Roast::Workflow::PromptStep
|
37
|
+
else
|
38
|
+
# Default to BaseStep for directory-based steps
|
39
|
+
Roast::Workflow::BaseStep
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_step_instance(step_class, workflow, name, options)
|
44
|
+
step_params = {
|
45
|
+
name:,
|
46
|
+
}
|
47
|
+
|
48
|
+
# Add context path if provided
|
49
|
+
step_params[:context_path] = options[:context_path] if options[:context_path]
|
50
|
+
|
51
|
+
step_class.new(workflow, **step_params)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -1,15 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roast/value_objects/step_name"
|
4
|
-
require "roast/workflow/workflow_context"
|
5
|
-
require "roast/workflow/base_step"
|
6
|
-
require "roast/workflow/prompt_step"
|
7
|
-
|
8
3
|
module Roast
|
9
4
|
module Workflow
|
10
5
|
# Handles loading and instantiation of workflow steps
|
11
6
|
class StepLoader
|
12
|
-
DEFAULT_MODEL = "
|
7
|
+
DEFAULT_MODEL = "gpt-4o-mini"
|
13
8
|
|
14
9
|
# Custom exception classes
|
15
10
|
class StepLoaderError < StandardError
|
@@ -47,8 +42,9 @@ module Roast
|
|
47
42
|
#
|
48
43
|
# @param step_name [String, StepName] The name of the step to load
|
49
44
|
# @param step_key [String] The configuration key for the step (optional)
|
45
|
+
# @param options [Hash] Additional options for step loading
|
50
46
|
# @return [BaseStep] The loaded step instance
|
51
|
-
def load(step_name, step_key: nil)
|
47
|
+
def load(step_name, step_key: nil, is_last_step: nil, **options)
|
52
48
|
name = step_name.is_a?(Roast::ValueObjects::StepName) ? step_name : Roast::ValueObjects::StepName.new(step_name)
|
53
49
|
|
54
50
|
# Get step config for per-step path
|
@@ -57,17 +53,17 @@ module Roast
|
|
57
53
|
|
58
54
|
# First check for a prompt step (contains spaces)
|
59
55
|
if name.plain_text?
|
60
|
-
step =
|
56
|
+
step = StepFactory.create(workflow, name, options)
|
61
57
|
# Use step_key for configuration if provided, otherwise use name
|
62
58
|
config_key = step_key || name.to_s
|
63
|
-
configure_step(step, config_key)
|
59
|
+
configure_step(step, config_key, is_last_step:)
|
64
60
|
return step
|
65
61
|
end
|
66
62
|
|
67
63
|
# Look for Ruby file in various locations
|
68
64
|
step_file_path = find_step_file(name.to_s, per_step_path)
|
69
65
|
if step_file_path
|
70
|
-
return load_ruby_step(step_file_path, name.to_s)
|
66
|
+
return load_ruby_step(step_file_path, name.to_s, is_last_step:)
|
71
67
|
end
|
72
68
|
|
73
69
|
# Look for step directory
|
@@ -76,7 +72,10 @@ module Roast
|
|
76
72
|
raise StepNotFoundError.new("Step directory or file not found: #{name}", step_name: name.to_s)
|
77
73
|
end
|
78
74
|
|
79
|
-
|
75
|
+
# Use factory to create the appropriate step instance
|
76
|
+
step = StepFactory.create(workflow, name, options.merge(context_path: step_directory))
|
77
|
+
configure_step(step, name.to_s, is_last_step:)
|
78
|
+
step
|
80
79
|
end
|
81
80
|
|
82
81
|
private
|
@@ -141,7 +140,7 @@ module Roast
|
|
141
140
|
end
|
142
141
|
|
143
142
|
# Load a Ruby step from a file
|
144
|
-
def load_ruby_step(file_path, step_name)
|
143
|
+
def load_ruby_step(file_path, step_name, is_last_step: nil)
|
145
144
|
$stderr.puts "Requiring step file: #{file_path}"
|
146
145
|
|
147
146
|
begin
|
@@ -154,18 +153,24 @@ module Roast
|
|
154
153
|
|
155
154
|
step_class = step_name.classify.constantize
|
156
155
|
context = File.dirname(file_path)
|
157
|
-
|
156
|
+
# For Ruby steps, we instantiate the specific class directly
|
157
|
+
# Convert step_name to StepName value object
|
158
|
+
step_name_obj = Roast::ValueObjects::StepName.new(step_name)
|
159
|
+
step = step_class.new(workflow, name: step_name_obj, context_path: context)
|
160
|
+
configure_step(step, step_name, is_last_step:)
|
161
|
+
step
|
158
162
|
end
|
159
163
|
|
160
164
|
# Create and configure a step instance
|
161
|
-
def create_step_instance(step_class, step_name, context_path)
|
162
|
-
|
163
|
-
|
165
|
+
def create_step_instance(step_class, step_name, context_path, options = {})
|
166
|
+
is_last_step = options[:is_last_step]
|
167
|
+
step = StepFactory.create(workflow, step_name, options.merge(context_path: context_path))
|
168
|
+
configure_step(step, step_name, is_last_step:)
|
164
169
|
step
|
165
170
|
end
|
166
171
|
|
167
172
|
# Configure a step instance with settings from config_hash
|
168
|
-
def configure_step(step, step_name)
|
173
|
+
def configure_step(step, step_name, is_last_step: nil)
|
169
174
|
step_config = config_hash[step_name]
|
170
175
|
|
171
176
|
# Always set the model
|
@@ -176,6 +181,11 @@ module Roast
|
|
176
181
|
|
177
182
|
# Apply additional configuration if present
|
178
183
|
apply_step_configuration(step, step_config) if step_config.present?
|
184
|
+
|
185
|
+
# Set print_response to true for the last step if not already configured
|
186
|
+
if is_last_step && !step_config&.key?("print_response")
|
187
|
+
step.print_response = true
|
188
|
+
end
|
179
189
|
end
|
180
190
|
|
181
191
|
# Determine which model to use for the step
|
@@ -189,6 +199,10 @@ module Roast
|
|
189
199
|
step.json = step_config["json"] if step_config.key?("json")
|
190
200
|
step.params = step_config["params"] if step_config.key?("params")
|
191
201
|
step.coerce_to = step_config["coerce_to"].to_sym if step_config.key?("coerce_to")
|
202
|
+
|
203
|
+
if step_config.key?("available_tools")
|
204
|
+
step.available_tools = step_config["available_tools"]
|
205
|
+
end
|
192
206
|
end
|
193
207
|
end
|
194
208
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
# Extracts human-readable names from various step types
|
6
|
+
class StepNameExtractor
|
7
|
+
def extract(step, step_type)
|
8
|
+
case step_type
|
9
|
+
when StepTypeResolver::COMMAND_STEP
|
10
|
+
extract_command_name(step)
|
11
|
+
when StepTypeResolver::HASH_STEP
|
12
|
+
extract_hash_step_name(step)
|
13
|
+
when StepTypeResolver::ITERATION_STEP
|
14
|
+
extract_iteration_step_name(step)
|
15
|
+
when StepTypeResolver::CONDITIONAL_STEP
|
16
|
+
extract_conditional_step_name(step)
|
17
|
+
when StepTypeResolver::CASE_STEP
|
18
|
+
"case"
|
19
|
+
when StepTypeResolver::INPUT_STEP
|
20
|
+
"input"
|
21
|
+
when StepTypeResolver::AGENT_STEP
|
22
|
+
StepTypeResolver.extract_name(step)
|
23
|
+
when StepTypeResolver::STRING_STEP
|
24
|
+
step.to_s
|
25
|
+
else
|
26
|
+
step.to_s
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def extract_command_name(step)
|
33
|
+
cmd = step.to_s.strip
|
34
|
+
cmd.length > 20 ? "#{cmd[0..19]}..." : cmd
|
35
|
+
end
|
36
|
+
|
37
|
+
def extract_hash_step_name(step)
|
38
|
+
key, value = step.to_a.first
|
39
|
+
|
40
|
+
# Check if this looks like an inline prompt (key is similar to sanitized value)
|
41
|
+
if value.is_a?(String)
|
42
|
+
# Get first non-empty line
|
43
|
+
first_line = value.lines.map(&:strip).find { |line| !line.empty? } || ""
|
44
|
+
|
45
|
+
# If key looks like it was auto-generated from the content, use truncated content
|
46
|
+
sanitized = first_line.downcase.gsub(/[^a-z0-9_]/, "_").squeeze("_").gsub(/^_|_$/, "")
|
47
|
+
if key.to_s == sanitized || key.to_s.start_with?(sanitized[0..15])
|
48
|
+
# This is likely an inline prompt
|
49
|
+
first_line.length > 20 ? "#{first_line[0..19]}..." : first_line
|
50
|
+
else
|
51
|
+
# This is a labeled step
|
52
|
+
key.to_s
|
53
|
+
end
|
54
|
+
else
|
55
|
+
key.to_s
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def extract_iteration_step_name(step)
|
60
|
+
if step.key?("each")
|
61
|
+
items = step["each"]
|
62
|
+
count = items.respond_to?(:size) ? items.size : "?"
|
63
|
+
"each (#{count} items)"
|
64
|
+
elsif step.key?("repeat")
|
65
|
+
config = step["repeat"]
|
66
|
+
times = config.is_a?(Hash) ? config["times"] || "?" : config
|
67
|
+
"repeat (#{times} times)"
|
68
|
+
else
|
69
|
+
"iteration"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def extract_conditional_step_name(step)
|
74
|
+
if step.key?("if")
|
75
|
+
"if"
|
76
|
+
elsif step.key?("unless")
|
77
|
+
"unless"
|
78
|
+
else
|
79
|
+
"conditional"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -22,7 +22,7 @@ module Roast
|
|
22
22
|
@workflow_executor = workflow_executor
|
23
23
|
end
|
24
24
|
|
25
|
-
def execute_step(name, exit_on_error: true, step_key: nil)
|
25
|
+
def execute_step(name, exit_on_error: true, step_key: nil, **options)
|
26
26
|
resource_type = @workflow.respond_to?(:resource) ? @workflow.resource&.type : nil
|
27
27
|
|
28
28
|
@error_handler.with_error_handling(name, resource_type: resource_type) do
|
@@ -30,7 +30,8 @@ module Roast
|
|
30
30
|
|
31
31
|
# Use step_key for loading if provided, otherwise use name
|
32
32
|
load_key = step_key || name
|
33
|
-
|
33
|
+
is_last_step = options[:is_last_step]
|
34
|
+
step_object = @step_loader.load(name, step_key: load_key, is_last_step:, **options)
|
34
35
|
step_result = step_object.call
|
35
36
|
|
36
37
|
# Store result in workflow output
|