roast-ai 0.4.0 → 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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +2 -2
  3. data/CHANGELOG.md +65 -0
  4. data/CLAUDE.md +55 -9
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +8 -1
  7. data/README.md +69 -3
  8. data/bin/console +1 -0
  9. data/docs/AGENT_STEPS.md +33 -9
  10. data/docs/VALIDATION.md +178 -0
  11. data/examples/agent_continue/add_documentation/prompt.md +5 -0
  12. data/examples/agent_continue/add_error_handling/prompt.md +5 -0
  13. data/examples/agent_continue/analyze_codebase/prompt.md +7 -0
  14. data/examples/agent_continue/combined_workflow.yml +24 -0
  15. data/examples/agent_continue/continue_adding_features/prompt.md +4 -0
  16. data/examples/agent_continue/create_integration_tests/prompt.md +3 -0
  17. data/examples/agent_continue/document_with_context/prompt.md +5 -0
  18. data/examples/agent_continue/explore_api/prompt.md +6 -0
  19. data/examples/agent_continue/implement_client/prompt.md +6 -0
  20. data/examples/agent_continue/inline_workflow.yml +20 -0
  21. data/examples/agent_continue/refactor_code/prompt.md +2 -0
  22. data/examples/agent_continue/verify_changes/prompt.md +6 -0
  23. data/examples/agent_continue/workflow.yml +27 -0
  24. data/examples/agent_workflow/workflow.png +0 -0
  25. data/examples/api_workflow/workflow.png +0 -0
  26. data/examples/apply_diff_demo/README.md +58 -0
  27. data/examples/apply_diff_demo/apply_simple_change/prompt.md +13 -0
  28. data/examples/apply_diff_demo/create_sample_file/prompt.md +11 -0
  29. data/examples/apply_diff_demo/workflow.yml +24 -0
  30. data/examples/available_tools_demo/workflow.png +0 -0
  31. data/examples/bash_prototyping/api_testing.png +0 -0
  32. data/examples/bash_prototyping/system_analysis.png +0 -0
  33. data/examples/case_when/workflow.png +0 -0
  34. data/examples/cmd/basic_workflow.png +0 -0
  35. data/examples/cmd/dev_workflow.png +0 -0
  36. data/examples/cmd/explorer_workflow.png +0 -0
  37. data/examples/conditional/simple_workflow.png +0 -0
  38. data/examples/conditional/workflow.png +0 -0
  39. data/examples/context_management_demo/README.md +43 -0
  40. data/examples/context_management_demo/workflow.yml +42 -0
  41. data/examples/direct_coerce_syntax/workflow.png +0 -0
  42. data/examples/dot_notation/workflow.png +0 -0
  43. data/examples/exit_on_error/workflow.png +0 -0
  44. data/examples/grading/workflow.png +0 -0
  45. data/examples/interpolation/workflow.png +0 -0
  46. data/examples/interpolation/workflow.yml +1 -1
  47. data/examples/iteration/workflow.png +0 -0
  48. data/examples/json_handling/workflow.png +0 -0
  49. data/examples/mcp/database_workflow.png +0 -0
  50. data/examples/mcp/env_demo/workflow.png +0 -0
  51. data/examples/mcp/filesystem_demo/workflow.png +0 -0
  52. data/examples/mcp/github_workflow.png +0 -0
  53. data/examples/mcp/multi_mcp_workflow.png +0 -0
  54. data/examples/mcp/workflow.png +0 -0
  55. data/examples/no_model_fallback/README.md +17 -0
  56. data/examples/no_model_fallback/analyze_file/prompt.md +1 -0
  57. data/examples/no_model_fallback/analyze_patterns/prompt.md +27 -0
  58. data/examples/no_model_fallback/generate_report_for_md/prompt.md +10 -0
  59. data/examples/no_model_fallback/generate_report_for_rb/prompt.md +3 -0
  60. data/examples/no_model_fallback/sample.rb +42 -0
  61. data/examples/no_model_fallback/workflow.yml +19 -0
  62. data/examples/openrouter_example/workflow.png +0 -0
  63. data/examples/pre_post_processing/workflow.png +0 -0
  64. data/examples/rspec_to_minitest/workflow.png +0 -0
  65. data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
  66. data/examples/shared_config/shared.png +0 -0
  67. data/examples/single_target_prepost/workflow.png +0 -0
  68. data/examples/smart_coercion_defaults/workflow.png +0 -0
  69. data/examples/step_configuration/workflow.png +0 -0
  70. data/examples/swarm_example.yml +25 -0
  71. data/examples/tool_config_example/workflow.png +0 -0
  72. data/examples/user_input/funny_name/workflow.png +0 -0
  73. data/examples/user_input/simple_input_demo/workflow.png +0 -0
  74. data/examples/user_input/survey_workflow.png +0 -0
  75. data/examples/user_input/workflow.png +0 -0
  76. data/examples/workflow_generator/workflow.png +0 -0
  77. data/lib/roast/helpers/timeout_handler.rb +91 -0
  78. data/lib/roast/services/context_threshold_checker.rb +42 -0
  79. data/lib/roast/services/token_counting_service.rb +44 -0
  80. data/lib/roast/tools/apply_diff.rb +128 -0
  81. data/lib/roast/tools/bash.rb +15 -9
  82. data/lib/roast/tools/cmd.rb +32 -12
  83. data/lib/roast/tools/coding_agent.rb +64 -9
  84. data/lib/roast/tools/context_summarizer.rb +108 -0
  85. data/lib/roast/tools/swarm.rb +124 -0
  86. data/lib/roast/version.rb +1 -1
  87. data/lib/roast/workflow/agent_step.rb +9 -2
  88. data/lib/roast/workflow/base_iteration_step.rb +3 -2
  89. data/lib/roast/workflow/base_workflow.rb +41 -2
  90. data/lib/roast/workflow/configuration.rb +2 -1
  91. data/lib/roast/workflow/configuration_loader.rb +63 -1
  92. data/lib/roast/workflow/context_manager.rb +89 -0
  93. data/lib/roast/workflow/each_step.rb +1 -1
  94. data/lib/roast/workflow/output_handler.rb +1 -1
  95. data/lib/roast/workflow/repeat_step.rb +1 -1
  96. data/lib/roast/workflow/replay_handler.rb +1 -1
  97. data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
  98. data/lib/roast/workflow/state_manager.rb +2 -2
  99. data/lib/roast/workflow/state_repository_factory.rb +36 -0
  100. data/lib/roast/workflow/step_completion_reporter.rb +27 -0
  101. data/lib/roast/workflow/step_executor_coordinator.rb +19 -18
  102. data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
  103. data/lib/roast/workflow/step_loader.rb +1 -1
  104. data/lib/roast/workflow/step_name_extractor.rb +84 -0
  105. data/lib/roast/workflow/validation_command.rb +197 -0
  106. data/lib/roast/workflow/validators/base_validator.rb +44 -0
  107. data/lib/roast/workflow/validators/dependency_validator.rb +223 -0
  108. data/lib/roast/workflow/validators/linting_validator.rb +113 -0
  109. data/lib/roast/workflow/validators/schema_validator.rb +90 -0
  110. data/lib/roast/workflow/validators/step_collector.rb +57 -0
  111. data/lib/roast/workflow/validators/validation_orchestrator.rb +52 -0
  112. data/lib/roast/workflow/workflow_executor.rb +11 -4
  113. data/lib/roast/workflow/workflow_runner.rb +6 -0
  114. data/lib/roast/workflow_diagram_generator.rb +298 -0
  115. data/lib/roast.rb +157 -0
  116. data/roast.gemspec +2 -1
  117. data/schema/workflow.json +77 -1
  118. metadata +101 -1
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ module Roast
6
+ module Tools
7
+ module Swarm
8
+ extend self
9
+
10
+ DEFAULT_CONFIG_PATHS = [
11
+ ".swarm.yml",
12
+ "swarm.yml",
13
+ ".swarm/config.yml",
14
+ ].freeze
15
+
16
+ CONFIG_PATH_KEY = "path"
17
+ private_constant :DEFAULT_CONFIG_PATHS, :CONFIG_PATH_KEY
18
+
19
+ class << self
20
+ def included(base)
21
+ base.class_eval do
22
+ function(
23
+ :swarm,
24
+ "Execute Claude Swarm to orchestrate multiple Claude Code instances",
25
+ prompt: {
26
+ type: "string",
27
+ description: "The prompt to send to the swarm agents",
28
+ required: true,
29
+ },
30
+ path: {
31
+ type: "string",
32
+ description: "Path to the swarm configuration file (optional)",
33
+ required: false,
34
+ },
35
+ ) do |params|
36
+ Roast::Tools::Swarm.call(params[:prompt], params[:path])
37
+ end
38
+ end
39
+ end
40
+
41
+ def post_configuration_setup(base, config = {})
42
+ @tool_config = config
43
+ end
44
+
45
+ attr_reader :tool_config
46
+ end
47
+
48
+ def call(prompt, step_path = nil)
49
+ config_path = determine_config_path(step_path)
50
+
51
+ if config_path.nil?
52
+ return "Error: No swarm configuration file found. Please create a .swarm.yml file or specify a path."
53
+ end
54
+
55
+ unless File.exist?(config_path)
56
+ return "Error: Swarm configuration file not found at: #{config_path}"
57
+ end
58
+
59
+ Roast::Helpers::Logger.info("🐝 Running Claude Swarm with config: #{config_path}\n")
60
+
61
+ execute_swarm(prompt, config_path)
62
+ rescue StandardError => e
63
+ handle_error(e)
64
+ end
65
+
66
+ private
67
+
68
+ def determine_config_path(step_path)
69
+ # Priority: step-level path > tool-level path > default locations
70
+
71
+ # 1. Check step-level path
72
+ return step_path if step_path
73
+
74
+ # 2. Check tool-level path from configuration
75
+ if tool_config && tool_config[CONFIG_PATH_KEY]
76
+ return tool_config[CONFIG_PATH_KEY]
77
+ end
78
+
79
+ # 3. Check default locations
80
+ DEFAULT_CONFIG_PATHS.find { |path| File.exist?(path) }
81
+ end
82
+
83
+ def execute_swarm(prompt, config_path)
84
+ # Build the swarm command with proper escaping
85
+ command = build_swarm_command(prompt, config_path)
86
+
87
+ result = ""
88
+
89
+ # Execute the command directly with the prompt included
90
+ IO.popen(command, err: [:child, :out]) do |io|
91
+ result = io.read
92
+ end
93
+
94
+ exit_status = $CHILD_STATUS.exitstatus
95
+
96
+ format_output(command, result, exit_status)
97
+ end
98
+
99
+ def build_swarm_command(prompt, config_path)
100
+ # Build the claude-swarm command with properly escaped arguments
101
+ [
102
+ "claude-swarm",
103
+ "--config",
104
+ config_path,
105
+ "--prompt",
106
+ prompt,
107
+ ].shelljoin
108
+ end
109
+
110
+ def format_output(command, result, exit_status)
111
+ "Command: #{command}\n" \
112
+ "Exit status: #{exit_status}\n" \
113
+ "Output:\n#{result}"
114
+ end
115
+
116
+ def handle_error(error)
117
+ error_message = "Error running swarm: #{error.message}"
118
+ Roast::Helpers::Logger.error("#{error_message}\n")
119
+ Roast::Helpers::Logger.debug("#{error.backtrace.join("\n")}\n") if ENV["DEBUG"]
120
+ error_message
121
+ end
122
+ end
123
+ end
124
+ end
data/lib/roast/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Roast
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -12,8 +12,15 @@ module Roast
12
12
  read_sidecar_prompt
13
13
  end
14
14
 
15
- # Call CodingAgent directly with the prompt content
16
- result = Roast::Tools::CodingAgent.call(prompt_content)
15
+ # Extract agent-specific configuration from workflow config
16
+ step_config = workflow.config[name.to_s] || {}
17
+ agent_options = {
18
+ include_context_summary: step_config.fetch("include_context_summary", false),
19
+ continue: step_config.fetch("continue", false),
20
+ }
21
+
22
+ # Call CodingAgent directly with the prompt content and options
23
+ result = Roast::Tools::CodingAgent.call(prompt_content, **agent_options)
17
24
 
18
25
  # Process output if print_response is enabled
19
26
  process_output(result, print_response:)
@@ -65,10 +65,11 @@ module Roast
65
65
  executor ||= WorkflowExecutor.new(context, config_hash, context_path)
66
66
  results = []
67
67
 
68
- steps.each do |step|
68
+ steps.each_with_index do |step, index|
69
+ is_last_step = (index == steps.length - 1)
69
70
  result = case step
70
71
  when String
71
- executor.execute_step(step)
72
+ executor.execute_step(step, is_last_step:)
72
73
  when Hash, Array
73
74
  executor.execute_steps([step])
74
75
  end
@@ -16,9 +16,11 @@ module Roast
16
16
  :session_name,
17
17
  :session_timestamp,
18
18
  :model,
19
- :workflow_configuration
19
+ :workflow_configuration,
20
+ :storage_type,
21
+ :context_management_config
20
22
 
21
- attr_reader :pre_processing_data
23
+ attr_reader :pre_processing_data, :context_manager
22
24
 
23
25
  delegate :api_provider, :openai?, to: :workflow_configuration, allow_nil: true
24
26
  delegate :output, :output=, :append_to_final_output, :final_output, to: :output_manager
@@ -36,6 +38,8 @@ module Roast
36
38
 
37
39
  # Initialize managers
38
40
  @output_manager = OutputManager.new
41
+ @context_manager = ContextManager.new
42
+ @context_management_config = {}
39
43
 
40
44
  # Setup prompt and handlers
41
45
  read_sidecar_prompt.then do |prompt|
@@ -53,22 +57,48 @@ module Roast
53
57
  step_model = kwargs[:model]
54
58
 
55
59
  with_model(step_model) do
60
+ # Configure context manager if needed
61
+ if @context_management_config.any?
62
+ @context_manager.configure(@context_management_config)
63
+ end
64
+
65
+ # Track token usage before API call
66
+ messages = kwargs[:messages] || transcript.flatten.compact
67
+ if @context_management_config[:enabled]
68
+ @context_manager.track_usage(messages)
69
+ @context_manager.check_warnings
70
+ end
71
+
56
72
  ActiveSupport::Notifications.instrument("roast.chat_completion.start", {
57
73
  model: model,
58
74
  parameters: kwargs.except(:openai, :model),
59
75
  })
60
76
 
77
+ # Clear any previous response
78
+ Thread.current[:chat_completion_response] = nil
79
+
61
80
  # Call the parent module's chat_completion
62
81
  # skip model because it is read directly from the model method
63
82
  result = super(**kwargs.except(:model))
64
83
  execution_time = Time.now - start_time
65
84
 
85
+ # Extract token usage from the raw response stored by Raix
86
+ raw_response = Thread.current[:chat_completion_response]
87
+ token_usage = extract_token_usage(raw_response) if raw_response
88
+
89
+ # Update context manager with actual token usage if available
90
+ if token_usage && @context_management_config[:enabled]
91
+ actual_total = token_usage.dig("total_tokens") || token_usage.dig(:total_tokens)
92
+ @context_manager.update_with_actual_usage(actual_total) if actual_total
93
+ end
94
+
66
95
  ActiveSupport::Notifications.instrument("roast.chat_completion.complete", {
67
96
  success: true,
68
97
  model: model,
69
98
  parameters: kwargs.except(:openai, :model),
70
99
  execution_time: execution_time,
71
100
  response_size: result.to_s.length,
101
+ token_usage: token_usage,
72
102
  })
73
103
  result
74
104
  end
@@ -115,6 +145,15 @@ module Roast
115
145
  def read_sidecar_prompt
116
146
  Roast::Helpers::PromptLoader.load_prompt(self, file)
117
147
  end
148
+
149
+ def extract_token_usage(result)
150
+ # Token usage is typically in the response metadata
151
+ # This depends on the API provider's response format
152
+ return unless result.is_a?(Hash) || result.respond_to?(:to_h)
153
+
154
+ result_hash = result.is_a?(Hash) ? result : result.to_h
155
+ result_hash.dig("usage") || result_hash.dig(:usage)
156
+ end
118
157
  end
119
158
  end
120
159
  end
@@ -7,7 +7,7 @@ module Roast
7
7
  class Configuration
8
8
  MCPTool = Struct.new(:name, :config, :only, :except, keyword_init: true)
9
9
 
10
- attr_reader :config_hash, :workflow_path, :name, :steps, :pre_processing, :post_processing, :tools, :tool_configs, :mcp_tools, :function_configs, :model, :resource
10
+ attr_reader :config_hash, :workflow_path, :name, :steps, :pre_processing, :post_processing, :tools, :tool_configs, :mcp_tools, :function_configs, :model, :resource, :context_management
11
11
  attr_accessor :target
12
12
 
13
13
  delegate :api_provider, :openrouter?, :openai?, :uri_base, to: :api_configuration
@@ -32,6 +32,7 @@ module Roast
32
32
  @mcp_tools = ConfigurationLoader.extract_mcp_tools(@config_hash)
33
33
  @function_configs = ConfigurationLoader.extract_functions(@config_hash)
34
34
  @model = ConfigurationLoader.extract_model(@config_hash)
35
+ @context_management = ConfigurationLoader.extract_context_management(@config_hash)
35
36
 
36
37
  # Initialize components
37
38
  @api_configuration = ApiConfiguration.new(@config_hash)
@@ -8,7 +8,7 @@ module Roast
8
8
  # Load configuration from a YAML file
9
9
  # @param workflow_path [String] Path to the workflow YAML file
10
10
  # @return [Hash] The parsed configuration hash
11
- def load(workflow_path)
11
+ def load(workflow_path, options = {})
12
12
  validate_path!(workflow_path)
13
13
 
14
14
  # Load shared.yml if it exists one level above
@@ -23,6 +23,18 @@ module Roast
23
23
  end
24
24
 
25
25
  yaml_content += File.read(workflow_path)
26
+
27
+ # Use comprehensive validation if requested
28
+ if options[:comprehensive_validation]
29
+ validator = Validators::ValidationOrchestrator.new(yaml_content, workflow_path)
30
+ unless validator.valid?
31
+ raise_validation_errors(validator)
32
+ end
33
+
34
+ # Show warnings if any
35
+ display_warnings(validator.warnings) if validator.warnings.any?
36
+ end
37
+
26
38
  config_hash = YAML.load(yaml_content, aliases: true)
27
39
 
28
40
  validate_config!(config_hash)
@@ -132,6 +144,30 @@ module Roast
132
144
  options[:target] || config_hash["target"]
133
145
  end
134
146
 
147
+ # Extract context management configuration
148
+ # @param config_hash [Hash] The configuration hash
149
+ # @return [Hash] The context management configuration with defaults
150
+ def extract_context_management(config_hash)
151
+ default_config = {
152
+ enabled: true,
153
+ strategy: "auto",
154
+ threshold: 0.8,
155
+ max_tokens: nil,
156
+ retain_steps: [],
157
+ }
158
+
159
+ return default_config unless config_hash["context_management"].is_a?(Hash)
160
+
161
+ config = config_hash["context_management"]
162
+ {
163
+ enabled: config.fetch("enabled", default_config[:enabled]),
164
+ strategy: config.fetch("strategy", default_config[:strategy]),
165
+ threshold: config.fetch("threshold", default_config[:threshold]),
166
+ max_tokens: config["max_tokens"],
167
+ retain_steps: config.fetch("retain_steps", default_config[:retain_steps]),
168
+ }
169
+ end
170
+
135
171
  private
136
172
 
137
173
  def validate_path!(workflow_path)
@@ -143,6 +179,32 @@ module Roast
143
179
  def validate_config!(config_hash)
144
180
  raise ArgumentError, "Invalid workflow configuration" unless config_hash.is_a?(Hash)
145
181
  end
182
+
183
+ def raise_validation_errors(validator)
184
+ error_messages = validator.errors.map do |error|
185
+ message = "• #{error[:message]}"
186
+ message += " (#{error[:suggestion]})" if error[:suggestion]
187
+ message
188
+ end.join("\n")
189
+
190
+ raise CLI::Kit::Abort, <<~ERROR
191
+ Workflow validation failed with #{validator.errors.size} error(s):
192
+
193
+ #{error_messages}
194
+ ERROR
195
+ end
196
+
197
+ def display_warnings(warnings)
198
+ return if warnings.empty?
199
+
200
+ ::CLI::UI::Frame.open("Validation Warnings", color: :yellow) do
201
+ warnings.each do |warning|
202
+ puts ::CLI::UI.fmt("{{yellow:#{warning[:message]}}}")
203
+ puts ::CLI::UI.fmt(" {{gray:→ #{warning[:suggestion]}}}") if warning[:suggestion]
204
+ puts
205
+ end
206
+ end
207
+ end
146
208
  end
147
209
  end
148
210
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Workflow
5
+ class ContextManager
6
+ attr_reader :total_tokens
7
+
8
+ def initialize(token_counter: nil, threshold_checker: nil)
9
+ @token_counter = token_counter || Services::TokenCountingService.new
10
+ @threshold_checker = threshold_checker || Services::ContextThresholdChecker.new
11
+ @total_tokens = 0
12
+ @message_count = 0
13
+ @config = default_config
14
+ @last_actual_update = nil
15
+ @estimated_tokens_since_update = 0
16
+ end
17
+
18
+ def configure(config)
19
+ @config = default_config.merge(config)
20
+ end
21
+
22
+ def track_usage(messages)
23
+ current_tokens = @token_counter.count_messages(messages)
24
+ @total_tokens += current_tokens
25
+ @message_count += messages.size
26
+
27
+ {
28
+ current_tokens: current_tokens,
29
+ total_tokens: @total_tokens,
30
+ }
31
+ end
32
+
33
+ def should_compact?(token_count = @total_tokens)
34
+ return false unless @config[:enabled]
35
+
36
+ @threshold_checker.should_compact?(
37
+ token_count,
38
+ @config[:threshold],
39
+ @config[:max_tokens],
40
+ )
41
+ end
42
+
43
+ def check_warnings(token_count = @total_tokens)
44
+ return unless @config[:enabled]
45
+
46
+ warning = @threshold_checker.check_warning_threshold(
47
+ token_count,
48
+ @config[:threshold],
49
+ @config[:max_tokens],
50
+ )
51
+
52
+ if warning
53
+ ActiveSupport::Notifications.instrument("roast.context_warning", warning)
54
+ end
55
+ end
56
+
57
+ def reset
58
+ @total_tokens = 0
59
+ @message_count = 0
60
+ end
61
+
62
+ def statistics
63
+ {
64
+ total_tokens: @total_tokens,
65
+ message_count: @message_count,
66
+ average_tokens_per_message: @message_count > 0 ? @total_tokens / @message_count : 0,
67
+ }
68
+ end
69
+
70
+ def update_with_actual_usage(actual_total)
71
+ return unless actual_total && actual_total > 0
72
+
73
+ @total_tokens = actual_total
74
+ @last_actual_update = Time.now
75
+ @estimated_tokens_since_update = 0
76
+ end
77
+
78
+ private
79
+
80
+ def default_config
81
+ {
82
+ enabled: true,
83
+ threshold: 0.8,
84
+ max_tokens: nil, # Will use default from threshold checker
85
+ }
86
+ end
87
+ end
88
+ end
89
+ end
@@ -65,7 +65,7 @@ module Roast
65
65
  end
66
66
 
67
67
  def save_iteration_state(index, item)
68
- state_repository = FileStateRepository.new
68
+ state_repository = StateRepositoryFactory.create(workflow.storage_type)
69
69
 
70
70
  # Save the current iteration state
71
71
  state_data = {
@@ -11,7 +11,7 @@ module Roast
11
11
  final_output = workflow.final_output.to_s
12
12
  return if final_output.empty?
13
13
 
14
- state_repository = FileStateRepository.new
14
+ state_repository = StateRepositoryFactory.create(workflow.storage_type)
15
15
  output_file = state_repository.save_final_output(workflow, final_output)
16
16
  $stderr.puts "Final output saved to: #{output_file}" if output_file
17
17
  rescue => e
@@ -55,7 +55,7 @@ module Roast
55
55
  private
56
56
 
57
57
  def save_iteration_state(iteration)
58
- state_repository = FileStateRepository.new
58
+ state_repository = StateRepositoryFactory.create(workflow.storage_type)
59
59
 
60
60
  # Save the current iteration count in the state
61
61
  state_data = {
@@ -9,7 +9,7 @@ module Roast
9
9
 
10
10
  def initialize(workflow, state_repository: nil)
11
11
  @workflow = workflow
12
- @state_repository = state_repository || FileStateRepository.new
12
+ @state_repository = state_repository || StateRepositoryFactory.create(workflow.storage_type)
13
13
  @processed = false
14
14
  end
15
15