roast-ai 0.3.1 → 0.4.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.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +20 -0
  4. data/CLAUDE.md +52 -1
  5. data/Gemfile +3 -1
  6. data/Gemfile.lock +63 -16
  7. data/README.md +90 -5
  8. data/bin/roast +1 -1
  9. data/claude-swarm.yml +210 -0
  10. data/docs/AGENT_STEPS.md +264 -0
  11. data/examples/agent_workflow/README.md +75 -0
  12. data/examples/agent_workflow/apply_refactorings/prompt.md +22 -0
  13. data/examples/agent_workflow/identify_code_smells/prompt.md +15 -0
  14. data/examples/agent_workflow/summarize_improvements/prompt.md +18 -0
  15. data/examples/agent_workflow/workflow.yml +16 -0
  16. data/examples/available_tools_demo/README.md +42 -0
  17. data/examples/available_tools_demo/analyze_files/prompt.md +6 -0
  18. data/examples/available_tools_demo/explore_directory/prompt.md +6 -0
  19. data/examples/available_tools_demo/workflow.yml +32 -0
  20. data/examples/available_tools_demo/write_summary/prompt.md +6 -0
  21. data/examples/case_when/detect_language/prompt.md +2 -2
  22. data/examples/grading/run_coverage.rb +0 -2
  23. data/examples/iteration/analyze_complexity/prompt.md +2 -2
  24. data/examples/iteration/generate_recommendations/prompt.md +2 -2
  25. data/examples/iteration/implement_fix/prompt.md +2 -2
  26. data/examples/iteration/prioritize_issues/prompt.md +1 -1
  27. data/examples/iteration/prompts/analyze_file.md +2 -2
  28. data/examples/iteration/prompts/generate_summary.md +1 -1
  29. data/examples/iteration/prompts/update_report.md +3 -3
  30. data/examples/iteration/prompts/write_report.md +3 -3
  31. data/examples/iteration/read_file/prompt.md +2 -2
  32. data/examples/iteration/select_next_issue/prompt.md +2 -2
  33. data/examples/iteration/update_fix_count/prompt.md +4 -4
  34. data/examples/iteration/verify_fix/prompt.md +3 -3
  35. data/examples/mcp/README.md +3 -3
  36. data/examples/mcp/analyze_changes/prompt.md +1 -1
  37. data/examples/mcp/database_workflow.yml +1 -1
  38. data/examples/mcp/fetch_pr_context/prompt.md +1 -1
  39. data/examples/mcp/github_workflow.yml +1 -1
  40. data/examples/mcp/post_review/prompt.md +1 -1
  41. data/examples/pre_post_processing/analyze_test_file/prompt.md +1 -1
  42. data/examples/pre_post_processing/improve_test_coverage/prompt.md +1 -1
  43. data/examples/pre_post_processing/optimize_test_performance/prompt.md +1 -1
  44. data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +2 -2
  45. data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +1 -1
  46. data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +1 -1
  47. data/examples/pre_post_processing/validate_changes/prompt.md +2 -2
  48. data/examples/user_input/README.md +90 -0
  49. data/examples/user_input/funny_name/create_backstory/prompt.md +10 -0
  50. data/examples/user_input/funny_name/workflow.yml +26 -0
  51. data/examples/user_input/generate_summary/prompt.md +11 -0
  52. data/examples/user_input/simple_input_demo/workflow.yml +35 -0
  53. data/examples/user_input/survey_workflow.yml +71 -0
  54. data/examples/user_input/welcome_message/prompt.md +3 -0
  55. data/examples/user_input/workflow.yml +73 -0
  56. data/examples/workflow_generator/create_workflow_files/prompt.md +1 -1
  57. data/lib/roast/errors.rb +6 -4
  58. data/lib/roast/helpers/function_caching_interceptor.rb +0 -2
  59. data/lib/roast/helpers/logger.rb +12 -35
  60. data/lib/roast/helpers/minitest_coverage_runner.rb +0 -1
  61. data/lib/roast/helpers/prompt_loader.rb +0 -2
  62. data/lib/roast/resources/api_resource.rb +0 -4
  63. data/lib/roast/resources/url_resource.rb +0 -3
  64. data/lib/roast/resources.rb +0 -8
  65. data/lib/roast/tools/ask_user.rb +0 -2
  66. data/lib/roast/tools/bash.rb +0 -3
  67. data/lib/roast/tools/cmd.rb +0 -3
  68. data/lib/roast/tools/coding_agent.rb +1 -8
  69. data/lib/roast/tools/grep.rb +0 -3
  70. data/lib/roast/tools/helpers/coding_agent_message_formatter.rb +1 -4
  71. data/lib/roast/tools/read_file.rb +0 -2
  72. data/lib/roast/tools/search_file.rb +0 -2
  73. data/lib/roast/tools/update_files.rb +0 -4
  74. data/lib/roast/tools/write_file.rb +0 -3
  75. data/lib/roast/tools.rb +0 -13
  76. data/lib/roast/value_objects/step_name.rb +14 -3
  77. data/lib/roast/value_objects/workflow_path.rb +0 -2
  78. data/lib/roast/value_objects.rb +4 -4
  79. data/lib/roast/version.rb +1 -1
  80. data/lib/roast/workflow/agent_step.rb +26 -0
  81. data/lib/roast/workflow/api_configuration.rb +0 -4
  82. data/lib/roast/workflow/base_iteration_step.rb +0 -4
  83. data/lib/roast/workflow/base_step.rb +54 -28
  84. data/lib/roast/workflow/base_workflow.rb +2 -21
  85. data/lib/roast/workflow/case_executor.rb +0 -1
  86. data/lib/roast/workflow/case_step.rb +0 -4
  87. data/lib/roast/workflow/command_executor.rb +0 -2
  88. data/lib/roast/workflow/conditional_executor.rb +0 -1
  89. data/lib/roast/workflow/conditional_step.rb +0 -4
  90. data/lib/roast/workflow/configuration.rb +3 -66
  91. data/lib/roast/workflow/configuration_loader.rb +0 -2
  92. data/lib/roast/workflow/configuration_parser.rb +1 -7
  93. data/lib/roast/workflow/dot_access_hash.rb +16 -1
  94. data/lib/roast/workflow/error_handler.rb +0 -3
  95. data/lib/roast/workflow/expression_evaluator.rb +0 -3
  96. data/lib/roast/workflow/file_state_repository.rb +0 -5
  97. data/lib/roast/workflow/input_executor.rb +41 -0
  98. data/lib/roast/workflow/input_step.rb +163 -0
  99. data/lib/roast/workflow/iteration_executor.rb +0 -2
  100. data/lib/roast/workflow/output_handler.rb +0 -2
  101. data/lib/roast/workflow/output_manager.rb +0 -2
  102. data/lib/roast/workflow/replay_handler.rb +0 -3
  103. data/lib/roast/workflow/resource_resolver.rb +0 -3
  104. data/lib/roast/workflow/session_manager.rb +0 -3
  105. data/lib/roast/workflow/state_manager.rb +0 -2
  106. data/lib/roast/workflow/step_executor_coordinator.rb +34 -11
  107. data/lib/roast/workflow/step_executor_factory.rb +0 -5
  108. data/lib/roast/workflow/step_executor_registry.rb +1 -4
  109. data/lib/roast/workflow/step_executors/hash_step_executor.rb +0 -3
  110. data/lib/roast/workflow/step_executors/parallel_step_executor.rb +0 -3
  111. data/lib/roast/workflow/step_executors/string_step_executor.rb +0 -2
  112. data/lib/roast/workflow/step_factory.rb +56 -0
  113. data/lib/roast/workflow/step_loader.rb +30 -16
  114. data/lib/roast/workflow/step_orchestrator.rb +3 -2
  115. data/lib/roast/workflow/step_type_resolver.rb +28 -1
  116. data/lib/roast/workflow/validator.rb +0 -4
  117. data/lib/roast/workflow/workflow_executor.rb +0 -16
  118. data/lib/roast/workflow/workflow_initializer.rb +1 -8
  119. data/lib/roast/workflow/workflow_runner.rb +0 -7
  120. data/lib/roast/workflow.rb +0 -15
  121. data/lib/roast.rb +55 -10
  122. data/roast.gemspec +2 -1
  123. data/schema/workflow.json +46 -0
  124. metadata +43 -6
  125. data/lib/roast/helpers.rb +0 -12
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "fileutils"
5
- require "roast/workflow/session_manager"
6
- require "roast/workflow/state_repository"
7
-
8
3
  module Roast
9
4
  module Workflow
10
5
  # File-based implementation of StateRepository
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ # Handles execution of input steps
6
+ class InputExecutor
7
+ def initialize(workflow, context_path, state_manager, workflow_executor = nil)
8
+ @workflow = workflow
9
+ @context_path = context_path
10
+ @state_manager = state_manager
11
+ @workflow_executor = workflow_executor
12
+ end
13
+
14
+ def execute_input(input_config)
15
+ # Interpolate the prompt if workflow executor is available
16
+ if @workflow_executor && input_config["prompt"]
17
+ interpolated_config = input_config.dup
18
+ interpolated_config["prompt"] = @workflow_executor.interpolate(input_config["prompt"])
19
+ else
20
+ interpolated_config = input_config
21
+ end
22
+
23
+ # Create and execute an InputStep
24
+ input_step = InputStep.new(
25
+ @workflow,
26
+ config: interpolated_config,
27
+ name: input_config["name"] || "input_#{Time.now.to_i}",
28
+ context_path: @context_path,
29
+ )
30
+
31
+ result = input_step.call
32
+
33
+ # Store in 'previous' for conditional checks
34
+ @workflow.output["previous"] = result
35
+ @state_manager.save_state("previous", result)
36
+
37
+ result
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ module Roast
6
+ module Workflow
7
+ class InputStep < BaseStep
8
+ attr_reader :prompt_text, :type, :required, :default, :timeout, :options, :step_name
9
+
10
+ def initialize(workflow, config:, **kwargs)
11
+ super(workflow, **kwargs)
12
+ parse_config(config)
13
+ end
14
+
15
+ def call
16
+ # Get user input based on the configured type
17
+ result = case type
18
+ when "boolean"
19
+ prompt_boolean
20
+ when "choice"
21
+ prompt_choice
22
+ when "password"
23
+ prompt_password
24
+ else
25
+ prompt_text_input
26
+ end
27
+
28
+ # Store the result in workflow state if a name was provided
29
+ store_in_state(result) if step_name
30
+
31
+ result
32
+ rescue Timeout::Error
33
+ handle_timeout
34
+ end
35
+
36
+ private
37
+
38
+ def parse_config(config)
39
+ @prompt_text = config["prompt"] || raise_config_error("Missing 'prompt' in input configuration")
40
+ @step_name = config["name"]
41
+ @type = config["type"] || "text"
42
+ @required = config.fetch("required", false)
43
+ @default = config["default"]
44
+ @timeout = config["timeout"]
45
+ @options = config["options"]
46
+
47
+ validate_config
48
+ end
49
+
50
+ def validate_config
51
+ if type == "choice" && options.nil?
52
+ raise_config_error("Missing 'options' for choice type input")
53
+ end
54
+
55
+ if type == "boolean" && default && ![true, false, "true", "false", "yes", "no"].include?(default)
56
+ raise_config_error("Invalid default value for boolean type: #{default}")
57
+ end
58
+ end
59
+
60
+ def prompt_text_input
61
+ loop do
62
+ result = if timeout
63
+ with_timeout { ::CLI::UI.ask(prompt_text, default: default) }
64
+ else
65
+ ::CLI::UI.ask(prompt_text, default: default)
66
+ end
67
+
68
+ if required && result.to_s.strip.empty?
69
+ puts ::CLI::UI.fmt("{{red:This field is required. Please provide a value.}}")
70
+ next
71
+ end
72
+
73
+ return result
74
+ end
75
+ end
76
+
77
+ def prompt_boolean
78
+ if timeout
79
+ with_timeout { ::CLI::UI.confirm(prompt_text, default: boolean_default) }
80
+ else
81
+ ::CLI::UI.confirm(prompt_text, default: boolean_default)
82
+ end
83
+ end
84
+
85
+ def prompt_choice
86
+ if timeout
87
+ with_timeout { ::CLI::UI.ask(prompt_text, options: options, default: default) }
88
+ else
89
+ ::CLI::UI.ask(prompt_text, options: options, default: default)
90
+ end
91
+ end
92
+
93
+ def prompt_password
94
+ require "io/console"
95
+
96
+ loop do
97
+ result = if timeout
98
+ with_timeout { prompt_password_with_echo_off }
99
+ else
100
+ prompt_password_with_echo_off
101
+ end
102
+
103
+ if required && result.to_s.strip.empty?
104
+ puts ::CLI::UI.fmt("{{red:This field is required. Please provide a value.}}")
105
+ next
106
+ end
107
+
108
+ return result
109
+ end
110
+ end
111
+
112
+ def prompt_password_with_echo_off
113
+ ::CLI::UI.with_frame_color(:blue) do
114
+ print("🔒 #{prompt_text} ")
115
+
116
+ password = if $stdin.tty?
117
+ # Use noecho for TTY environments
118
+ $stdin.noecho { $stdin.gets }.chomp
119
+ else
120
+ # Fall back to regular input for non-TTY environments
121
+ warn("[WARNING] Password will be visible (not running in TTY)")
122
+ $stdin.gets.chomp
123
+ end
124
+
125
+ puts # Add newline after password input
126
+ password
127
+ end
128
+ end
129
+
130
+ def boolean_default
131
+ case default
132
+ when true, "true", "yes"
133
+ true
134
+ when false, "false", "no"
135
+ false
136
+ end
137
+ end
138
+
139
+ def with_timeout(&block)
140
+ Timeout.timeout(timeout, &block)
141
+ end
142
+
143
+ def handle_timeout
144
+ puts ::CLI::UI.fmt("{{yellow:Input timed out after #{timeout} seconds}}")
145
+
146
+ if default
147
+ puts ::CLI::UI.fmt("{{yellow:Using default value: #{default}}}")
148
+ default
149
+ elsif required
150
+ raise_config_error("Required input timed out with no default value")
151
+ end
152
+ end
153
+
154
+ def store_in_state(value)
155
+ workflow.output[step_name] = value
156
+ end
157
+
158
+ def raise_config_error(message)
159
+ raise WorkflowExecutor::ConfigurationError, message
160
+ end
161
+ end
162
+ end
163
+ end
@@ -24,7 +24,6 @@ module Roast
24
24
  raise WorkflowExecutor::ConfigurationError, "Missing 'until' condition in repeat configuration" unless until_condition
25
25
 
26
26
  # Create and execute a RepeatStep
27
- require "roast/workflow/repeat_step" unless defined?(RepeatStep)
28
27
  repeat_step = RepeatStep.new(
29
28
  @workflow,
30
29
  steps: steps,
@@ -64,7 +63,6 @@ module Roast
64
63
  raise WorkflowExecutor::ConfigurationError, "Missing 'steps' in each configuration" unless steps
65
64
 
66
65
  # Create and execute an EachStep
67
- require "roast/workflow/each_step" unless defined?(EachStep)
68
66
  each_step = EachStep.new(
69
67
  @workflow,
70
68
  collection_expr: collection_expr,
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/file_state_repository"
4
-
5
3
  module Roast
6
4
  module Workflow
7
5
  # Handles output operations for workflows including saving final output and results
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/dot_access_hash"
4
-
5
3
  module Roast
6
4
  module Workflow
7
5
  # Manages workflow output, including both the key-value output hash
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/step_finder"
4
- require "roast/workflow/file_state_repository"
5
-
6
3
  module Roast
7
4
  module Workflow
8
5
  # Handles replay functionality for workflows
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require "roast/resources"
5
-
6
3
  module Roast
7
4
  module Workflow
8
5
  # Handles resource resolution and target processing
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fileutils"
4
- require "digest"
5
-
6
3
  module Roast
7
4
  module Workflow
8
5
  # Manages session creation, timestamping, and directory management
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/file_state_repository"
4
-
5
3
  module Roast
6
4
  module Workflow
7
5
  # Manages workflow state persistence and restoration
@@ -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.each do |step|
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: is_last_step)
36
32
  when Array
37
- execute(step)
33
+ execute(step, is_last_step: is_last_step)
38
34
  when String
39
- execute(step)
35
+ execute(step, is_last_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,6 +59,8 @@ 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
65
  execute_glob_step(step)
68
66
  when StepTypeResolver::ITERATION_STEP
@@ -71,6 +69,8 @@ module Roast
71
69
  execute_conditional_step(step)
72
70
  when StepTypeResolver::CASE_STEP
73
71
  execute_case_step(step)
72
+ when StepTypeResolver::INPUT_STEP
73
+ execute_input_step(step)
74
74
  when StepTypeResolver::HASH_STEP
75
75
  execute_hash_step(step)
76
76
  when StepTypeResolver::PARALLEL_STEP
@@ -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,6 +180,15 @@ module Roast
171
180
  end
172
181
  end
173
182
 
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
+
174
192
  def execute_glob_step(step)
175
193
  Dir.glob(step).join("\n")
176
194
  end
@@ -196,6 +214,10 @@ module Roast
196
214
  case_executor.execute_case(step)
197
215
  end
198
216
 
217
+ def execute_input_step(step)
218
+ input_executor.execute_input(step["input"])
219
+ end
220
+
199
221
  def execute_hash_step(step)
200
222
  name, command = step.to_a.flatten
201
223
  interpolated_name = interpolator.interpolate(name)
@@ -235,7 +257,8 @@ module Roast
235
257
  def execute_standard_step(step, options)
236
258
  exit_on_error = options.fetch(:exit_on_error, true)
237
259
  step_key = options[:step_key]
238
- step_orchestrator.execute_step(step, exit_on_error: exit_on_error, step_key: step_key)
260
+ is_last_step = options[:is_last_step]
261
+ step_orchestrator.execute_step(step, exit_on_error:, step_key:, is_last_step:)
239
262
  end
240
263
 
241
264
  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
- # Reset the factory's defaults flag if it's defined
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)
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/step_executors/base_step_executor"
4
- require "roast/workflow/step_runner"
5
-
6
3
  module Roast
7
4
  module Workflow
8
5
  module StepExecutors
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/step_executors/base_step_executor"
4
- require "roast/workflow/step_runner"
5
-
6
3
  module Roast
7
4
  module Workflow
8
5
  module StepExecutors
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/step_executors/base_step_executor"
4
-
5
3
  module Roast
6
4
  module Workflow
7
5
  module StepExecutors
@@ -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,10 +1,5 @@
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
@@ -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 = Roast::Workflow::PromptStep.new(workflow, name: name.to_s)
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
- create_step_instance(Roast::Workflow::BaseStep, name.to_s, step_directory)
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
- create_step_instance(step_class, step_name, context)
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
- step = step_class.new(workflow, name: step_name, context_path: context_path)
163
- configure_step(step, step_name)
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
@@ -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
- step_object = @step_loader.load(name, step_key: load_key)
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