roast-ai 0.4.0 → 0.4.2

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/.github/workflows/ci.yaml +2 -2
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +103 -0
  5. data/CLAUDE.md +55 -9
  6. data/Gemfile.lock +19 -10
  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/rb_test_runner +1 -1
  45. data/examples/grading/workflow.png +0 -0
  46. data/examples/interpolation/workflow.png +0 -0
  47. data/examples/interpolation/workflow.yml +1 -1
  48. data/examples/iteration/workflow.png +0 -0
  49. data/examples/json_handling/workflow.png +0 -0
  50. data/examples/mcp/database_workflow.png +0 -0
  51. data/examples/mcp/env_demo/workflow.png +0 -0
  52. data/examples/mcp/filesystem_demo/workflow.png +0 -0
  53. data/examples/mcp/github_workflow.png +0 -0
  54. data/examples/mcp/multi_mcp_workflow.png +0 -0
  55. data/examples/mcp/workflow.png +0 -0
  56. data/examples/no_model_fallback/README.md +17 -0
  57. data/examples/no_model_fallback/analyze_file/prompt.md +1 -0
  58. data/examples/no_model_fallback/analyze_patterns/prompt.md +27 -0
  59. data/examples/no_model_fallback/generate_report_for_md/prompt.md +10 -0
  60. data/examples/no_model_fallback/generate_report_for_rb/prompt.md +3 -0
  61. data/examples/no_model_fallback/sample.rb +42 -0
  62. data/examples/no_model_fallback/workflow.yml +19 -0
  63. data/examples/openrouter_example/workflow.png +0 -0
  64. data/examples/pre_post_processing/workflow.png +0 -0
  65. data/examples/rspec_to_minitest/workflow.png +0 -0
  66. data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
  67. data/examples/shared_config/shared.png +0 -0
  68. data/examples/single_target_prepost/workflow.png +0 -0
  69. data/examples/smart_coercion_defaults/workflow.png +0 -0
  70. data/examples/step_configuration/workflow.png +0 -0
  71. data/examples/swarm_example.yml +25 -0
  72. data/examples/tool_config_example/workflow.png +0 -0
  73. data/examples/user_input/funny_name/workflow.png +0 -0
  74. data/examples/user_input/simple_input_demo/workflow.png +0 -0
  75. data/examples/user_input/survey_workflow.png +0 -0
  76. data/examples/user_input/workflow.png +0 -0
  77. data/examples/workflow_generator/workflow.png +0 -0
  78. data/lib/roast/errors.rb +3 -0
  79. data/lib/roast/helpers/timeout_handler.rb +91 -0
  80. data/lib/roast/services/context_threshold_checker.rb +42 -0
  81. data/lib/roast/services/token_counting_service.rb +44 -0
  82. data/lib/roast/tools/apply_diff.rb +128 -0
  83. data/lib/roast/tools/bash.rb +15 -9
  84. data/lib/roast/tools/cmd.rb +32 -12
  85. data/lib/roast/tools/coding_agent.rb +65 -10
  86. data/lib/roast/tools/context_summarizer.rb +108 -0
  87. data/lib/roast/tools/swarm.rb +124 -0
  88. data/lib/roast/version.rb +1 -1
  89. data/lib/roast/workflow/agent_step.rb +9 -2
  90. data/lib/roast/workflow/base_iteration_step.rb +3 -2
  91. data/lib/roast/workflow/base_workflow.rb +41 -2
  92. data/lib/roast/workflow/command_executor.rb +3 -1
  93. data/lib/roast/workflow/configuration.rb +2 -1
  94. data/lib/roast/workflow/configuration_loader.rb +63 -1
  95. data/lib/roast/workflow/configuration_parser.rb +2 -0
  96. data/lib/roast/workflow/context_manager.rb +89 -0
  97. data/lib/roast/workflow/each_step.rb +1 -1
  98. data/lib/roast/workflow/input_step.rb +2 -0
  99. data/lib/roast/workflow/interpolator.rb +23 -1
  100. data/lib/roast/workflow/output_handler.rb +1 -1
  101. data/lib/roast/workflow/repeat_step.rb +1 -1
  102. data/lib/roast/workflow/replay_handler.rb +1 -1
  103. data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
  104. data/lib/roast/workflow/state_manager.rb +2 -2
  105. data/lib/roast/workflow/state_repository_factory.rb +36 -0
  106. data/lib/roast/workflow/step_completion_reporter.rb +27 -0
  107. data/lib/roast/workflow/step_executor_coordinator.rb +19 -18
  108. data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
  109. data/lib/roast/workflow/step_loader.rb +1 -1
  110. data/lib/roast/workflow/step_name_extractor.rb +84 -0
  111. data/lib/roast/workflow/validation_command.rb +197 -0
  112. data/lib/roast/workflow/validators/base_validator.rb +44 -0
  113. data/lib/roast/workflow/validators/dependency_validator.rb +223 -0
  114. data/lib/roast/workflow/validators/linting_validator.rb +113 -0
  115. data/lib/roast/workflow/validators/schema_validator.rb +90 -0
  116. data/lib/roast/workflow/validators/step_collector.rb +57 -0
  117. data/lib/roast/workflow/validators/validation_orchestrator.rb +52 -0
  118. data/lib/roast/workflow/workflow_executor.rb +11 -4
  119. data/lib/roast/workflow/workflow_initializer.rb +80 -0
  120. data/lib/roast/workflow/workflow_runner.rb +6 -0
  121. data/lib/roast/workflow_diagram_generator.rb +298 -0
  122. data/lib/roast.rb +158 -0
  123. data/roast.gemspec +4 -1
  124. data/schema/workflow.json +77 -1
  125. metadata +129 -1
@@ -19,10 +19,16 @@ module Roast
19
19
  base.class_eval do
20
20
  function(
21
21
  :coding_agent,
22
- "AI-powered coding agent that runs Claude Code CLI with the given prompt",
22
+ "AI-powered coding agent that runs an instance of the Claude Code agent with the given prompt. If the agent is iterating on previous work, set continue to true.",
23
23
  prompt: { type: "string", description: "The prompt to send to Claude Code" },
24
+ include_context_summary: { type: "boolean", description: "Whether to set a summary of the current workflow context as system directive (default: false)", required: false },
25
+ continue: { type: "boolean", description: "Whether to continue where the previous coding agent left off or start with a fresh context (default: false, start fresh)", required: false },
24
26
  ) do |params|
25
- Roast::Tools::CodingAgent.call(params[:prompt])
27
+ Roast::Tools::CodingAgent.call(
28
+ params[:prompt],
29
+ include_context_summary: params[:include_context_summary].presence || false,
30
+ continue: params[:continue].presence || false,
31
+ )
26
32
  end
27
33
  end
28
34
  end
@@ -33,9 +39,9 @@ module Roast
33
39
  end
34
40
  end
35
41
 
36
- def call(prompt)
42
+ def call(prompt, include_context_summary: false, continue: false)
37
43
  Roast::Helpers::Logger.info("🤖 Running CodingAgent\n")
38
- run_claude_code(prompt)
44
+ run_claude_code(prompt, include_context_summary:, continue:)
39
45
  rescue StandardError => e
40
46
  "Error running CodingAgent: #{e.message}".tap do |error_message|
41
47
  Roast::Helpers::Logger.error(error_message + "\n")
@@ -45,7 +51,7 @@ module Roast
45
51
 
46
52
  private
47
53
 
48
- def run_claude_code(prompt)
54
+ def run_claude_code(prompt, include_context_summary:, continue:)
49
55
  Roast::Helpers::Logger.debug("🤖 Executing Claude Code CLI with prompt: #{prompt}\n")
50
56
 
51
57
  # Create a temporary file with a unique name
@@ -55,14 +61,21 @@ module Roast
55
61
  temp_file = Tempfile.new(["claude_prompt_#{timestamp}_#{pid}_#{random_id}", ".txt"])
56
62
 
57
63
  begin
64
+ # Prepare the final prompt with context summary if requested
65
+ final_prompt = prepare_prompt(prompt, include_context_summary)
66
+
58
67
  # Write the prompt to the file
59
- temp_file.write(prompt)
68
+ temp_file.write(final_prompt)
60
69
  temp_file.close
61
70
 
71
+ # Build the command with continue option if specified
72
+ base_command = claude_code_command
73
+ command_to_run = build_command(base_command, continue:)
74
+
62
75
  # Run Claude Code CLI using the temp file as input with streaming output
63
- expect_json_output = claude_code_command.include?("--output-format stream-json") ||
64
- claude_code_command.include?("--output-format json")
65
- command = "cat #{temp_file.path} | #{claude_code_command}"
76
+ expect_json_output = command_to_run.include?("--output-format stream-json") ||
77
+ command_to_run.include?("--output-format json")
78
+ command = "cat #{temp_file.path} | #{command_to_run}"
66
79
  result = ""
67
80
 
68
81
  Open3.popen3(command) do |stdin, stdout, stderr, wait_thread|
@@ -134,7 +147,49 @@ module Roast
134
147
  end
135
148
 
136
149
  def claude_code_command
137
- CodingAgent.configured_command || ENV["CLAUDE_CODE_COMMAND"] || "claude -p --verbose --output-format stream-json"
150
+ CodingAgent.configured_command || ENV["CLAUDE_CODE_COMMAND"] || "claude -p --verbose --output-format stream-json --dangerously-skip-permissions"
151
+ end
152
+
153
+ def build_command(base_command, continue:)
154
+ return base_command unless continue
155
+
156
+ # Add --continue flag to the command
157
+ # If the command already has flags, insert --continue after 'claude'
158
+ if base_command.start_with?("claude ")
159
+ base_command.sub("claude ", "claude --continue ")
160
+ else
161
+ # Fallback for non-standard commands
162
+ "#{base_command} --continue"
163
+ end
164
+ end
165
+
166
+ def prepare_prompt(prompt, include_context_summary)
167
+ return prompt unless include_context_summary
168
+
169
+ context_summary = generate_context_summary(prompt)
170
+ return prompt if context_summary.blank? || context_summary == "No relevant information found in the workflow context."
171
+
172
+ # Prepend context summary as a system directive
173
+ <<~PROMPT
174
+ <system>
175
+ #{context_summary}
176
+ </system>
177
+
178
+ #{prompt}
179
+ PROMPT
180
+ end
181
+
182
+ def generate_context_summary(agent_prompt)
183
+ # Access the current workflow context if available
184
+ workflow_context = Thread.current[:workflow_context]
185
+ return unless workflow_context
186
+
187
+ # Use ContextSummarizer to generate an intelligent summary
188
+ summarizer = ContextSummarizer.new
189
+ summarizer.generate_summary(workflow_context, agent_prompt)
190
+ rescue => e
191
+ Roast::Helpers::Logger.debug("Failed to generate context summary: #{e.message}\n")
192
+ nil
138
193
  end
139
194
  end
140
195
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Tools
5
+ class ContextSummarizer
6
+ include Raix::ChatCompletion
7
+
8
+ attr_reader :model
9
+
10
+ def initialize(model: "o4-mini")
11
+ @model = model
12
+ end
13
+
14
+ # Generate an intelligent summary of the workflow context
15
+ # tailored to what the agent needs to know for its upcoming task
16
+ #
17
+ # @param workflow_context [Object] The workflow context from Thread.current
18
+ # @param agent_prompt [String] The prompt the agent is about to execute
19
+ # @return [String, nil] The generated summary or nil if generation fails
20
+ def generate_summary(workflow_context, agent_prompt)
21
+ return unless workflow_context&.workflow
22
+
23
+ context_data = build_context_data(workflow_context.workflow)
24
+ summary_prompt = build_summary_prompt(context_data, agent_prompt)
25
+
26
+ # Use our own transcript for the summary generation
27
+ self.transcript = []
28
+ prompt(summary_prompt)
29
+
30
+ result = chat_completion
31
+ result&.strip
32
+ rescue => e
33
+ Roast::Helpers::Logger.debug("Failed to generate LLM context summary: #{e.message}\n")
34
+ nil
35
+ end
36
+
37
+ private
38
+
39
+ def build_context_data(workflow)
40
+ data = {}
41
+
42
+ # Add workflow description if available
43
+ if workflow.config && workflow.config["description"]
44
+ data[:workflow_description] = workflow.config["description"]
45
+ end
46
+
47
+ # Add step outputs if available
48
+ if workflow.output && !workflow.output.empty?
49
+ data[:step_outputs] = workflow.output.map do |step_name, output|
50
+ # Include full output for context generation
51
+ { step: step_name, output: output }
52
+ end
53
+ end
54
+
55
+ # Add current working directory
56
+ data[:working_directory] = Dir.pwd
57
+
58
+ # Add workflow name if available
59
+ if workflow.respond_to?(:name)
60
+ data[:workflow_name] = workflow.name
61
+ end
62
+
63
+ data
64
+ end
65
+
66
+ def build_summary_prompt(context_data, agent_prompt)
67
+ prompt_parts = []
68
+
69
+ prompt_parts << "You are preparing a context summary for an AI coding agent (Claude Code) that is about to perform a task."
70
+ prompt_parts << "\nThe agent's upcoming task is:"
71
+ prompt_parts << "```"
72
+ prompt_parts << agent_prompt
73
+ prompt_parts << "```"
74
+
75
+ prompt_parts << "\nBased on the following workflow context, provide a concise summary of ONLY the information that would be relevant for the agent to complete this specific task."
76
+
77
+ if context_data[:workflow_description]
78
+ prompt_parts << "\nWorkflow Description: #{context_data[:workflow_description]}"
79
+ end
80
+
81
+ if context_data[:workflow_name]
82
+ prompt_parts << "\nWorkflow Name: #{context_data[:workflow_name]}"
83
+ end
84
+
85
+ if context_data[:working_directory]
86
+ prompt_parts << "\nWorking Directory: #{context_data[:working_directory]}"
87
+ end
88
+
89
+ if context_data[:step_outputs] && !context_data[:step_outputs].empty?
90
+ prompt_parts << "\nPrevious Step Outputs:"
91
+ context_data[:step_outputs].each do |step_data|
92
+ prompt_parts << "\n### Step: #{step_data[:step]}"
93
+ prompt_parts << "Output: #{step_data[:output]}"
94
+ end
95
+ end
96
+
97
+ prompt_parts << "\n\nGenerate a brief context summary that:"
98
+ prompt_parts << "1. Focuses ONLY on information relevant to the agent's upcoming task"
99
+ prompt_parts << "2. Highlights key findings, decisions, or outputs the agent should be aware of"
100
+ prompt_parts << "3. Is concise and actionable (aim for 3-5 sentences)"
101
+ prompt_parts << "4. Does not repeat information that would be obvious from the agent's prompt"
102
+ prompt_parts << "\nIf there is no relevant context for this task, respond with 'No relevant information found in the workflow context.'"
103
+
104
+ prompt_parts.join("\n")
105
+ end
106
+ end
107
+ end
108
+ end
@@ -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.2"
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
@@ -14,6 +14,8 @@ module Roast
14
14
  end
15
15
  end
16
16
 
17
+ BASH_COMMAND_REGEX = /^\$\((.*)\)$/m
18
+
17
19
  def initialize(logger: nil)
18
20
  @logger = logger || NullLogger.new
19
21
  end
@@ -44,7 +46,7 @@ module Roast
44
46
  private
45
47
 
46
48
  def extract_command(command_string)
47
- match = command_string.strip.match(/^\$\((.*)\)$/)
49
+ match = command_string.strip.match(BASH_COMMAND_REGEX)
48
50
  raise ArgumentError, "Invalid command format. Expected $(command), got: #{command_string}" unless match
49
51
 
50
52
  match[1]
@@ -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
@@ -38,6 +38,8 @@ module Roast
38
38
  else
39
39
  @workflow_runner.run_targetless
40
40
  end
41
+ rescue Roast::Errors::ExitEarly
42
+ $stderr.puts "Exiting workflow early."
41
43
  ensure
42
44
  execution_time = Time.now - start_time
43
45