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
@@ -1,37 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "erb"
4
- require "forwardable"
5
- require "roast/workflow/context_path_resolver"
6
-
7
3
  module Roast
8
4
  module Workflow
9
5
  class BaseStep
10
- extend Forwardable
11
-
12
- attr_accessor :model, :print_response, :json, :params, :resource, :coerce_to
6
+ attr_accessor :model, :print_response, :json, :params, :resource, :coerce_to, :available_tools
13
7
  attr_reader :workflow, :name, :context_path
14
8
 
15
- def_delegator :workflow, :append_to_final_output
16
- def_delegator :workflow, :chat_completion
17
- def_delegator :workflow, :transcript
9
+ delegate :append_to_final_output, :transcript, to: :workflow
10
+ delegate_missing_to :workflow
18
11
 
19
12
  # TODO: is this really the model we want to default to, and is this the right place to set it?
20
13
  def initialize(workflow, model: "anthropic:claude-opus-4", name: nil, context_path: nil)
21
14
  @workflow = workflow
22
15
  @model = model
23
- @name = name || self.class.name.underscore.split("/").last
16
+ @name = normalize_name(name)
24
17
  @context_path = context_path || ContextPathResolver.resolve(self.class)
25
18
  @print_response = false
26
19
  @json = false
27
20
  @params = {}
28
21
  @coerce_to = nil
22
+ @available_tools = nil
29
23
  @resource = workflow.resource if workflow.respond_to?(:resource)
30
24
  end
31
25
 
32
26
  def call
33
27
  prompt(read_sidecar_prompt)
34
- result = chat_completion(print_response:, json:, params:)
28
+ result = chat_completion(print_response:, json:, params:, available_tools:)
35
29
 
36
30
  # Apply coercion if configured
37
31
  apply_coercion(result)
@@ -39,25 +33,17 @@ module Roast
39
33
 
40
34
  protected
41
35
 
42
- def chat_completion(print_response: nil, json: nil, params: nil)
36
+ def chat_completion(print_response: nil, json: nil, params: nil, available_tools: nil)
43
37
  # Use instance variables as defaults if parameters are not provided
44
38
  print_response = @print_response if print_response.nil?
45
39
  json = @json if json.nil?
46
40
  params = @params if params.nil?
41
+ available_tools = @available_tools if available_tools.nil?
47
42
 
48
- workflow.chat_completion(openai: workflow.openai? && model, model: model, json:, params:).tap do |result|
49
- process_output(result, print_response:)
50
-
51
- begin
52
- if json
53
- return nil if result.strip.empty? # Explicitly handle empty string
43
+ result = workflow.chat_completion(openai: workflow.openai? && model, model: model, json:, params:, available_tools:)
44
+ process_output(result, print_response:)
54
45
 
55
- return JSON.parse(result)
56
- end
57
- rescue JSON::ParserError
58
- # If JSON parsing fails, leave it as a string
59
- end
60
- end
46
+ result
61
47
  end
62
48
 
63
49
  def prompt(text)
@@ -79,8 +65,20 @@ module Roast
79
65
  def process_output(response, print_response:)
80
66
  output_path = File.join(context_path, "output.txt")
81
67
  if File.exist?(output_path) && print_response
82
- # TODO: use the workflow binding or the step?
83
- append_to_final_output(ERB.new(File.read(output_path), trim_mode: "-").result(binding))
68
+ # Deep wrap the response for template access
69
+ template_response = deep_wrap_for_templates(response)
70
+
71
+ # Debug output
72
+ if template_response.is_a?(DotAccessHash) && template_response.recommendations&.is_a?(Array)
73
+ $stderr.puts "DEBUG: recommendations array has #{template_response.recommendations.size} items"
74
+ $stderr.puts "DEBUG: first item class: #{template_response.recommendations.first.class}" if template_response.recommendations.first
75
+ end
76
+
77
+ # Create a binding that includes the wrapped response
78
+ template_binding = binding
79
+ template_binding.local_variable_set(:response, template_response)
80
+
81
+ append_to_final_output(ERB.new(File.read(output_path), trim_mode: "-").result(template_binding))
84
82
  elsif print_response
85
83
  append_to_final_output(response)
86
84
  end
@@ -88,6 +86,35 @@ module Roast
88
86
 
89
87
  private
90
88
 
89
+ def normalize_name(name)
90
+ return name if name.is_a?(Roast::ValueObjects::StepName)
91
+
92
+ name_value = name || self.class.name.underscore.split("/").last
93
+ Roast::ValueObjects::StepName.new(name_value)
94
+ end
95
+
96
+ # Deep wrap response for ERB templates
97
+ # This creates a new structure where:
98
+ # - Hashes are wrapped in DotAccessHash
99
+ # - Arrays are cloned with their Hash elements wrapped
100
+ def deep_wrap_for_templates(obj)
101
+ case obj
102
+ when Hash
103
+ # Convert the hash to a new hash with wrapped values
104
+ wrapped_hash = {}
105
+ obj.each do |key, value|
106
+ wrapped_hash[key] = deep_wrap_for_templates(value)
107
+ end
108
+ DotAccessHash.new(wrapped_hash)
109
+ when Array
110
+ # Create a new array with wrapped elements
111
+ # This allows the template to use dot notation on array elements
112
+ obj.map { |item| deep_wrap_for_templates(item) }
113
+ else
114
+ obj
115
+ end
116
+ end
117
+
91
118
  def apply_coercion(result)
92
119
  case @coerce_to
93
120
  when :boolean
@@ -97,7 +124,6 @@ module Roast
97
124
  !!result
98
125
  when :llm_boolean
99
126
  # Use LLM boolean coercer for natural language responses
100
- require "roast/workflow/llm_boolean_coercer"
101
127
  LlmBooleanCoercer.coerce(result)
102
128
  when :iterable
103
129
  # Ensure result is iterable
@@ -1,12 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "raix/chat_completion"
4
- require "raix/function_dispatch"
5
-
6
- require "roast/workflow/context_path_resolver"
7
- require "roast/workflow/dot_access_hash"
8
- require "roast/workflow/output_manager"
9
-
10
3
  module Roast
11
4
  module Workflow
12
5
  class BaseWorkflow
@@ -23,12 +16,15 @@ module Roast
23
16
  :session_name,
24
17
  :session_timestamp,
25
18
  :model,
26
- :workflow_configuration
19
+ :workflow_configuration,
20
+ :storage_type,
21
+ :context_management_config
27
22
 
28
- attr_reader :pre_processing_data
23
+ attr_reader :pre_processing_data, :context_manager
29
24
 
30
25
  delegate :api_provider, :openai?, to: :workflow_configuration, allow_nil: true
31
26
  delegate :output, :output=, :append_to_final_output, :final_output, to: :output_manager
27
+ delegate_missing_to :output
32
28
 
33
29
  def initialize(file = nil, name: nil, context_path: nil, resource: nil, session_name: nil, workflow_configuration: nil, pre_processing_data: nil)
34
30
  @file = file
@@ -42,6 +38,8 @@ module Roast
42
38
 
43
39
  # Initialize managers
44
40
  @output_manager = OutputManager.new
41
+ @context_manager = ContextManager.new
42
+ @context_management_config = {}
45
43
 
46
44
  # Setup prompt and handlers
47
45
  read_sidecar_prompt.then do |prompt|
@@ -59,29 +57,55 @@ module Roast
59
57
  step_model = kwargs[:model]
60
58
 
61
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
+
62
72
  ActiveSupport::Notifications.instrument("roast.chat_completion.start", {
63
73
  model: model,
64
74
  parameters: kwargs.except(:openai, :model),
65
75
  })
66
76
 
77
+ # Clear any previous response
78
+ Thread.current[:chat_completion_response] = nil
79
+
67
80
  # Call the parent module's chat_completion
68
81
  # skip model because it is read directly from the model method
69
82
  result = super(**kwargs.except(:model))
70
83
  execution_time = Time.now - start_time
71
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
+
72
95
  ActiveSupport::Notifications.instrument("roast.chat_completion.complete", {
73
96
  success: true,
74
97
  model: model,
75
98
  parameters: kwargs.except(:openai, :model),
76
99
  execution_time: execution_time,
77
100
  response_size: result.to_s.length,
101
+ token_usage: token_usage,
78
102
  })
79
103
  result
80
104
  end
81
105
  rescue Faraday::ResourceNotFound => e
82
106
  execution_time = Time.now - start_time
83
107
  message = e.response.dig(:body, "error", "message") || e.message
84
- error = Roast::ResourceNotFoundError.new(message)
108
+ error = Roast::Errors::ResourceNotFoundError.new(message)
85
109
  error.set_backtrace(e.backtrace)
86
110
  log_and_raise_error(error, message, step_model || model, kwargs, execution_time)
87
111
  rescue => e
@@ -104,19 +128,6 @@ module Roast
104
128
  # Expose output manager for state management
105
129
  attr_reader :output_manager
106
130
 
107
- # Allow direct access to output values without 'output.' prefix
108
- def method_missing(method_name, *args, &block)
109
- if output.respond_to?(method_name)
110
- output.send(method_name, *args, &block)
111
- else
112
- super
113
- end
114
- end
115
-
116
- def respond_to_missing?(method_name, include_private = false)
117
- output.respond_to?(method_name) || super
118
- end
119
-
120
131
  private
121
132
 
122
133
  def log_and_raise_error(error, message, model, params, execution_time)
@@ -134,6 +145,15 @@ module Roast
134
145
  def read_sidecar_prompt
135
146
  Roast::Helpers::PromptLoader.load_prompt(self, file)
136
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
137
157
  end
138
158
  end
139
159
  end
@@ -24,7 +24,6 @@ module Roast
24
24
  raise WorkflowExecutor::ConfigurationError, "Missing 'when' clauses in case configuration" unless when_clauses
25
25
 
26
26
  # Create and execute a CaseStep
27
- require "roast/workflow/case_step" unless defined?(Roast::Workflow::CaseStep)
28
27
  case_step = CaseStep.new(
29
28
  @workflow,
30
29
  config: case_config,
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/base_step"
4
- require "roast/workflow/expression_evaluator"
5
- require "roast/workflow/interpolator"
6
-
7
3
  module Roast
8
4
  module Workflow
9
5
  class CaseStep < BaseStep
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "English"
4
-
5
3
  module Roast
6
4
  module Workflow
7
5
  class CommandExecutor
@@ -24,7 +24,6 @@ module Roast
24
24
  raise WorkflowExecutor::ConfigurationError, "Missing 'then' steps in conditional configuration" unless then_steps
25
25
 
26
26
  # Create and execute a ConditionalStep
27
- require "roast/workflow/conditional_step" unless defined?(Roast::Workflow::ConditionalStep)
28
27
  conditional_step = ConditionalStep.new(
29
28
  @workflow,
30
29
  config: conditional_config,
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/base_step"
4
- require "roast/workflow/expression_evaluator"
5
- require "roast/workflow/interpolator"
6
-
7
3
  module Roast
8
4
  module Workflow
9
5
  class ConditionalStep < BaseStep
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/api_configuration"
4
- require "roast/workflow/configuration_loader"
5
- require "roast/workflow/resource_resolver"
6
- require "roast/workflow/step_finder"
7
-
8
3
  module Roast
9
4
  module Workflow
10
5
  # Encapsulates workflow configuration data and provides structured access
@@ -12,7 +7,7 @@ module Roast
12
7
  class Configuration
13
8
  MCPTool = Struct.new(:name, :config, :only, :except, keyword_init: true)
14
9
 
15
- 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
16
11
  attr_accessor :target
17
12
 
18
13
  delegate :api_provider, :openrouter?, :openai?, :uri_base, to: :api_configuration
@@ -37,6 +32,7 @@ module Roast
37
32
  @mcp_tools = ConfigurationLoader.extract_mcp_tools(@config_hash)
38
33
  @function_configs = ConfigurationLoader.extract_functions(@config_hash)
39
34
  @model = ConfigurationLoader.extract_model(@config_hash)
35
+ @context_management = ConfigurationLoader.extract_context_management(@config_hash)
40
36
 
41
37
  # Initialize components
42
38
  @api_configuration = ApiConfiguration.new(@config_hash)
@@ -45,8 +41,6 @@ module Roast
45
41
  # Process target and resource
46
42
  @target = ConfigurationLoader.extract_target(@config_hash, options)
47
43
  process_resource
48
-
49
- mark_last_step_for_output
50
44
  end
51
45
 
52
46
  def context_path
@@ -98,65 +92,9 @@ module Roast
98
92
  attr_reader :api_configuration
99
93
 
100
94
  def process_resource
101
- if defined?(Roast::Resources)
102
- @resource = ResourceResolver.resolve(@target, context_path)
103
- # Update target with processed value for backward compatibility
104
- @target = @resource.value if has_target?
105
- end
106
- end
107
-
108
- def mark_last_step_for_output
109
- return if @steps.empty?
110
-
111
- last_step = find_last_executable_step(@steps.last)
112
- return unless last_step
113
-
114
- # Get the step name/key
115
- step_key = extract_step_key(last_step)
116
- return unless step_key
117
-
118
- # Ensure config exists for this step
119
- @config_hash[step_key] ||= {}
120
-
121
- # Only set print_response if not already explicitly configured
122
- @config_hash[step_key]["print_response"] = true unless @config_hash[step_key].key?("print_response")
123
- end
124
-
125
- def find_last_executable_step(step)
126
- case step
127
- when String
128
- step
129
- when Hash
130
- # Check if it's a special step type (if, unless, each, repeat, case)
131
- if step.key?("if") || step.key?("unless")
132
- # For conditional steps, try to find the last step in the "then" branch
133
- then_steps = step["then"] || step["steps"]
134
- find_last_executable_step(then_steps.last) if then_steps&.any?
135
- elsif step.key?("each") || step.key?("repeat")
136
- # For iteration steps, we can't reliably determine the last step
137
- nil
138
- elsif step.key?("case")
139
- # For case steps, we can't reliably determine the last step
140
- nil
141
- elsif step.size == 1
142
- # Regular hash step with variable assignment
143
- step
144
- end
145
- when Array
146
- # For parallel steps, we can't determine a single "last" step
147
- nil
148
- else
149
- step
150
- end
151
- end
152
-
153
- def extract_step_key(step)
154
- case step
155
- when String
156
- step
157
- when Hash
158
- step.keys.first
159
- end
95
+ @resource = ResourceResolver.resolve(@target, context_path)
96
+ # Update target with processed value for backward compatibility
97
+ @target = @resource.value if has_target?
160
98
  end
161
99
  end
162
100
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
-
5
3
  module Roast
6
4
  module Workflow
7
5
  # Handles loading and parsing of workflow configuration files
@@ -10,7 +8,7 @@ module Roast
10
8
  # Load configuration from a YAML file
11
9
  # @param workflow_path [String] Path to the workflow YAML file
12
10
  # @return [Hash] The parsed configuration hash
13
- def load(workflow_path)
11
+ def load(workflow_path, options = {})
14
12
  validate_path!(workflow_path)
15
13
 
16
14
  # Load shared.yml if it exists one level above
@@ -25,6 +23,18 @@ module Roast
25
23
  end
26
24
 
27
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
+
28
38
  config_hash = YAML.load(yaml_content, aliases: true)
29
39
 
30
40
  validate_config!(config_hash)
@@ -134,6 +144,30 @@ module Roast
134
144
  options[:target] || config_hash["target"]
135
145
  end
136
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
+
137
171
  private
138
172
 
139
173
  def validate_path!(workflow_path)
@@ -145,6 +179,32 @@ module Roast
145
179
  def validate_config!(config_hash)
146
180
  raise ArgumentError, "Invalid workflow configuration" unless config_hash.is_a?(Hash)
147
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
148
208
  end
149
209
  end
150
210
  end
@@ -1,17 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/workflow/configuration"
4
- require "roast/workflow/workflow_initializer"
5
- require "roast/workflow/workflow_runner"
6
-
7
3
  module Roast
8
4
  module Workflow
9
5
  class ConfigurationParser
10
- extend Forwardable
11
-
12
6
  attr_reader :configuration, :options, :files, :current_workflow
13
7
 
14
- def_delegator :current_workflow, :output
8
+ delegate :output, to: :current_workflow
15
9
 
16
10
  def initialize(workflow_path, files = [], options = {})
17
11
  @configuration = Configuration.new(workflow_path, options)
@@ -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
@@ -9,7 +9,7 @@ module Roast
9
9
 
10
10
  def [](key)
11
11
  value = @hash[key.to_sym] || @hash[key.to_s]
12
- value.is_a?(Hash) ? DotAccessHash.new(value) : value
12
+ wrap_value(value)
13
13
  end
14
14
 
15
15
  def []=(key, value)
@@ -193,6 +193,21 @@ module Roast
193
193
  end
194
194
 
195
195
  alias_method :member?, :has_key?
196
+
197
+ private
198
+
199
+ def wrap_value(value)
200
+ case value
201
+ when Hash
202
+ DotAccessHash.new(value)
203
+ when Array
204
+ # Don't create a new array - return the original array
205
+ # Only wrap Hash elements within the array when needed
206
+ value
207
+ else
208
+ value
209
+ end
210
+ end
196
211
  end
197
212
  end
198
213
  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 = {
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "roast/helpers/logger"
4
- require "roast/workflow/command_executor"
5
-
6
3
  module Roast
7
4
  module Workflow
8
5
  # Handles error logging and instrumentation for workflow execution