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.
Files changed (216) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +2 -2
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +85 -0
  5. data/CLAUDE.md +106 -9
  6. data/Gemfile +4 -1
  7. data/Gemfile.lock +70 -16
  8. data/README.md +159 -8
  9. data/bin/console +1 -0
  10. data/bin/roast +1 -1
  11. data/claude-swarm.yml +210 -0
  12. data/docs/AGENT_STEPS.md +288 -0
  13. data/docs/VALIDATION.md +178 -0
  14. data/examples/agent_continue/add_documentation/prompt.md +5 -0
  15. data/examples/agent_continue/add_error_handling/prompt.md +5 -0
  16. data/examples/agent_continue/analyze_codebase/prompt.md +7 -0
  17. data/examples/agent_continue/combined_workflow.yml +24 -0
  18. data/examples/agent_continue/continue_adding_features/prompt.md +4 -0
  19. data/examples/agent_continue/create_integration_tests/prompt.md +3 -0
  20. data/examples/agent_continue/document_with_context/prompt.md +5 -0
  21. data/examples/agent_continue/explore_api/prompt.md +6 -0
  22. data/examples/agent_continue/implement_client/prompt.md +6 -0
  23. data/examples/agent_continue/inline_workflow.yml +20 -0
  24. data/examples/agent_continue/refactor_code/prompt.md +2 -0
  25. data/examples/agent_continue/verify_changes/prompt.md +6 -0
  26. data/examples/agent_continue/workflow.yml +27 -0
  27. data/examples/agent_workflow/README.md +75 -0
  28. data/examples/agent_workflow/apply_refactorings/prompt.md +22 -0
  29. data/examples/agent_workflow/identify_code_smells/prompt.md +15 -0
  30. data/examples/agent_workflow/summarize_improvements/prompt.md +18 -0
  31. data/examples/agent_workflow/workflow.png +0 -0
  32. data/examples/agent_workflow/workflow.yml +16 -0
  33. data/examples/api_workflow/workflow.png +0 -0
  34. data/examples/apply_diff_demo/README.md +58 -0
  35. data/examples/apply_diff_demo/apply_simple_change/prompt.md +13 -0
  36. data/examples/apply_diff_demo/create_sample_file/prompt.md +11 -0
  37. data/examples/apply_diff_demo/workflow.yml +24 -0
  38. data/examples/available_tools_demo/README.md +42 -0
  39. data/examples/available_tools_demo/analyze_files/prompt.md +6 -0
  40. data/examples/available_tools_demo/explore_directory/prompt.md +6 -0
  41. data/examples/available_tools_demo/workflow.png +0 -0
  42. data/examples/available_tools_demo/workflow.yml +32 -0
  43. data/examples/available_tools_demo/write_summary/prompt.md +6 -0
  44. data/examples/bash_prototyping/api_testing.png +0 -0
  45. data/examples/bash_prototyping/system_analysis.png +0 -0
  46. data/examples/case_when/detect_language/prompt.md +2 -2
  47. data/examples/case_when/workflow.png +0 -0
  48. data/examples/cmd/basic_workflow.png +0 -0
  49. data/examples/cmd/dev_workflow.png +0 -0
  50. data/examples/cmd/explorer_workflow.png +0 -0
  51. data/examples/conditional/simple_workflow.png +0 -0
  52. data/examples/conditional/workflow.png +0 -0
  53. data/examples/context_management_demo/README.md +43 -0
  54. data/examples/context_management_demo/workflow.yml +42 -0
  55. data/examples/direct_coerce_syntax/workflow.png +0 -0
  56. data/examples/dot_notation/workflow.png +0 -0
  57. data/examples/exit_on_error/workflow.png +0 -0
  58. data/examples/grading/run_coverage.rb +0 -2
  59. data/examples/grading/workflow.png +0 -0
  60. data/examples/interpolation/workflow.png +0 -0
  61. data/examples/interpolation/workflow.yml +1 -1
  62. data/examples/iteration/analyze_complexity/prompt.md +2 -2
  63. data/examples/iteration/generate_recommendations/prompt.md +2 -2
  64. data/examples/iteration/implement_fix/prompt.md +2 -2
  65. data/examples/iteration/prioritize_issues/prompt.md +1 -1
  66. data/examples/iteration/prompts/analyze_file.md +2 -2
  67. data/examples/iteration/prompts/generate_summary.md +1 -1
  68. data/examples/iteration/prompts/update_report.md +3 -3
  69. data/examples/iteration/prompts/write_report.md +3 -3
  70. data/examples/iteration/read_file/prompt.md +2 -2
  71. data/examples/iteration/select_next_issue/prompt.md +2 -2
  72. data/examples/iteration/update_fix_count/prompt.md +4 -4
  73. data/examples/iteration/verify_fix/prompt.md +3 -3
  74. data/examples/iteration/workflow.png +0 -0
  75. data/examples/json_handling/workflow.png +0 -0
  76. data/examples/mcp/README.md +3 -3
  77. data/examples/mcp/analyze_changes/prompt.md +1 -1
  78. data/examples/mcp/database_workflow.png +0 -0
  79. data/examples/mcp/database_workflow.yml +1 -1
  80. data/examples/mcp/env_demo/workflow.png +0 -0
  81. data/examples/mcp/fetch_pr_context/prompt.md +1 -1
  82. data/examples/mcp/filesystem_demo/workflow.png +0 -0
  83. data/examples/mcp/github_workflow.png +0 -0
  84. data/examples/mcp/github_workflow.yml +1 -1
  85. data/examples/mcp/multi_mcp_workflow.png +0 -0
  86. data/examples/mcp/post_review/prompt.md +1 -1
  87. data/examples/mcp/workflow.png +0 -0
  88. data/examples/no_model_fallback/README.md +17 -0
  89. data/examples/no_model_fallback/analyze_file/prompt.md +1 -0
  90. data/examples/no_model_fallback/analyze_patterns/prompt.md +27 -0
  91. data/examples/no_model_fallback/generate_report_for_md/prompt.md +10 -0
  92. data/examples/no_model_fallback/generate_report_for_rb/prompt.md +3 -0
  93. data/examples/no_model_fallback/sample.rb +42 -0
  94. data/examples/no_model_fallback/workflow.yml +19 -0
  95. data/examples/openrouter_example/workflow.png +0 -0
  96. data/examples/pre_post_processing/analyze_test_file/prompt.md +1 -1
  97. data/examples/pre_post_processing/improve_test_coverage/prompt.md +1 -1
  98. data/examples/pre_post_processing/optimize_test_performance/prompt.md +1 -1
  99. data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +2 -2
  100. data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +1 -1
  101. data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +1 -1
  102. data/examples/pre_post_processing/validate_changes/prompt.md +2 -2
  103. data/examples/pre_post_processing/workflow.png +0 -0
  104. data/examples/rspec_to_minitest/workflow.png +0 -0
  105. data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
  106. data/examples/shared_config/shared.png +0 -0
  107. data/examples/single_target_prepost/workflow.png +0 -0
  108. data/examples/smart_coercion_defaults/workflow.png +0 -0
  109. data/examples/step_configuration/workflow.png +0 -0
  110. data/examples/swarm_example.yml +25 -0
  111. data/examples/tool_config_example/workflow.png +0 -0
  112. data/examples/user_input/README.md +90 -0
  113. data/examples/user_input/funny_name/create_backstory/prompt.md +10 -0
  114. data/examples/user_input/funny_name/workflow.png +0 -0
  115. data/examples/user_input/funny_name/workflow.yml +26 -0
  116. data/examples/user_input/generate_summary/prompt.md +11 -0
  117. data/examples/user_input/simple_input_demo/workflow.png +0 -0
  118. data/examples/user_input/simple_input_demo/workflow.yml +35 -0
  119. data/examples/user_input/survey_workflow.png +0 -0
  120. data/examples/user_input/survey_workflow.yml +71 -0
  121. data/examples/user_input/welcome_message/prompt.md +3 -0
  122. data/examples/user_input/workflow.png +0 -0
  123. data/examples/user_input/workflow.yml +73 -0
  124. data/examples/workflow_generator/create_workflow_files/prompt.md +1 -1
  125. data/examples/workflow_generator/workflow.png +0 -0
  126. data/lib/roast/errors.rb +6 -4
  127. data/lib/roast/helpers/function_caching_interceptor.rb +0 -2
  128. data/lib/roast/helpers/logger.rb +12 -35
  129. data/lib/roast/helpers/minitest_coverage_runner.rb +0 -1
  130. data/lib/roast/helpers/prompt_loader.rb +0 -2
  131. data/lib/roast/helpers/timeout_handler.rb +91 -0
  132. data/lib/roast/resources/api_resource.rb +0 -4
  133. data/lib/roast/resources/url_resource.rb +0 -3
  134. data/lib/roast/resources.rb +0 -8
  135. data/lib/roast/services/context_threshold_checker.rb +42 -0
  136. data/lib/roast/services/token_counting_service.rb +44 -0
  137. data/lib/roast/tools/apply_diff.rb +128 -0
  138. data/lib/roast/tools/ask_user.rb +0 -2
  139. data/lib/roast/tools/bash.rb +12 -9
  140. data/lib/roast/tools/cmd.rb +29 -12
  141. data/lib/roast/tools/coding_agent.rb +65 -17
  142. data/lib/roast/tools/context_summarizer.rb +108 -0
  143. data/lib/roast/tools/grep.rb +0 -3
  144. data/lib/roast/tools/helpers/coding_agent_message_formatter.rb +1 -4
  145. data/lib/roast/tools/read_file.rb +0 -2
  146. data/lib/roast/tools/search_file.rb +0 -2
  147. data/lib/roast/tools/swarm.rb +124 -0
  148. data/lib/roast/tools/update_files.rb +0 -4
  149. data/lib/roast/tools/write_file.rb +0 -3
  150. data/lib/roast/tools.rb +0 -13
  151. data/lib/roast/value_objects/step_name.rb +14 -3
  152. data/lib/roast/value_objects/workflow_path.rb +0 -2
  153. data/lib/roast/value_objects.rb +4 -4
  154. data/lib/roast/version.rb +1 -1
  155. data/lib/roast/workflow/agent_step.rb +33 -0
  156. data/lib/roast/workflow/api_configuration.rb +0 -4
  157. data/lib/roast/workflow/base_iteration_step.rb +3 -6
  158. data/lib/roast/workflow/base_step.rb +54 -28
  159. data/lib/roast/workflow/base_workflow.rb +43 -23
  160. data/lib/roast/workflow/case_executor.rb +0 -1
  161. data/lib/roast/workflow/case_step.rb +0 -4
  162. data/lib/roast/workflow/command_executor.rb +0 -2
  163. data/lib/roast/workflow/conditional_executor.rb +0 -1
  164. data/lib/roast/workflow/conditional_step.rb +0 -4
  165. data/lib/roast/workflow/configuration.rb +5 -67
  166. data/lib/roast/workflow/configuration_loader.rb +63 -3
  167. data/lib/roast/workflow/configuration_parser.rb +1 -7
  168. data/lib/roast/workflow/context_manager.rb +89 -0
  169. data/lib/roast/workflow/dot_access_hash.rb +16 -1
  170. data/lib/roast/workflow/each_step.rb +1 -1
  171. data/lib/roast/workflow/error_handler.rb +0 -3
  172. data/lib/roast/workflow/expression_evaluator.rb +0 -3
  173. data/lib/roast/workflow/file_state_repository.rb +0 -5
  174. data/lib/roast/workflow/input_executor.rb +41 -0
  175. data/lib/roast/workflow/input_step.rb +163 -0
  176. data/lib/roast/workflow/iteration_executor.rb +0 -2
  177. data/lib/roast/workflow/output_handler.rb +1 -3
  178. data/lib/roast/workflow/output_manager.rb +0 -2
  179. data/lib/roast/workflow/repeat_step.rb +1 -1
  180. data/lib/roast/workflow/replay_handler.rb +1 -4
  181. data/lib/roast/workflow/resource_resolver.rb +0 -3
  182. data/lib/roast/workflow/session_manager.rb +0 -3
  183. data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
  184. data/lib/roast/workflow/state_manager.rb +2 -4
  185. data/lib/roast/workflow/state_repository_factory.rb +36 -0
  186. data/lib/roast/workflow/step_completion_reporter.rb +27 -0
  187. data/lib/roast/workflow/step_executor_coordinator.rb +48 -24
  188. data/lib/roast/workflow/step_executor_factory.rb +0 -5
  189. data/lib/roast/workflow/step_executor_registry.rb +1 -4
  190. data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
  191. data/lib/roast/workflow/step_executors/hash_step_executor.rb +0 -3
  192. data/lib/roast/workflow/step_executors/parallel_step_executor.rb +0 -3
  193. data/lib/roast/workflow/step_executors/string_step_executor.rb +0 -2
  194. data/lib/roast/workflow/step_factory.rb +56 -0
  195. data/lib/roast/workflow/step_loader.rb +31 -17
  196. data/lib/roast/workflow/step_name_extractor.rb +84 -0
  197. data/lib/roast/workflow/step_orchestrator.rb +3 -2
  198. data/lib/roast/workflow/step_type_resolver.rb +28 -1
  199. data/lib/roast/workflow/validation_command.rb +197 -0
  200. data/lib/roast/workflow/validator.rb +0 -4
  201. data/lib/roast/workflow/validators/base_validator.rb +44 -0
  202. data/lib/roast/workflow/validators/dependency_validator.rb +223 -0
  203. data/lib/roast/workflow/validators/linting_validator.rb +113 -0
  204. data/lib/roast/workflow/validators/schema_validator.rb +90 -0
  205. data/lib/roast/workflow/validators/step_collector.rb +57 -0
  206. data/lib/roast/workflow/validators/validation_orchestrator.rb +52 -0
  207. data/lib/roast/workflow/workflow_executor.rb +11 -20
  208. data/lib/roast/workflow/workflow_initializer.rb +1 -8
  209. data/lib/roast/workflow/workflow_runner.rb +6 -7
  210. data/lib/roast/workflow.rb +0 -15
  211. data/lib/roast/workflow_diagram_generator.rb +298 -0
  212. data/lib/roast.rb +212 -10
  213. data/roast.gemspec +4 -2
  214. data/schema/workflow.json +123 -1
  215. metadata +143 -6
  216. 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.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:)
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 execute_glob_step(step)
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 execute_hash_step(step)
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
- result = execute(interpolated_command, { exit_on_error: exit_on_error, step_key: interpolated_name })
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: 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: 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
- step_orchestrator.execute_step(step, exit_on_error: exit_on_error, step_key: step_key)
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
- # 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)
@@ -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
@@ -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,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 = "openai/gpt-4o-mini"
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 = 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
@@ -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
- 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