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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yaml +2 -2
- data/.gitignore +1 -0
- data/CHANGELOG.md +103 -0
- data/CLAUDE.md +55 -9
- data/Gemfile.lock +19 -10
- data/README.md +69 -3
- data/bin/console +1 -0
- data/docs/AGENT_STEPS.md +33 -9
- data/docs/VALIDATION.md +178 -0
- data/examples/agent_continue/add_documentation/prompt.md +5 -0
- data/examples/agent_continue/add_error_handling/prompt.md +5 -0
- data/examples/agent_continue/analyze_codebase/prompt.md +7 -0
- data/examples/agent_continue/combined_workflow.yml +24 -0
- data/examples/agent_continue/continue_adding_features/prompt.md +4 -0
- data/examples/agent_continue/create_integration_tests/prompt.md +3 -0
- data/examples/agent_continue/document_with_context/prompt.md +5 -0
- data/examples/agent_continue/explore_api/prompt.md +6 -0
- data/examples/agent_continue/implement_client/prompt.md +6 -0
- data/examples/agent_continue/inline_workflow.yml +20 -0
- data/examples/agent_continue/refactor_code/prompt.md +2 -0
- data/examples/agent_continue/verify_changes/prompt.md +6 -0
- data/examples/agent_continue/workflow.yml +27 -0
- data/examples/agent_workflow/workflow.png +0 -0
- data/examples/api_workflow/workflow.png +0 -0
- data/examples/apply_diff_demo/README.md +58 -0
- data/examples/apply_diff_demo/apply_simple_change/prompt.md +13 -0
- data/examples/apply_diff_demo/create_sample_file/prompt.md +11 -0
- data/examples/apply_diff_demo/workflow.yml +24 -0
- data/examples/available_tools_demo/workflow.png +0 -0
- data/examples/bash_prototyping/api_testing.png +0 -0
- data/examples/bash_prototyping/system_analysis.png +0 -0
- data/examples/case_when/workflow.png +0 -0
- data/examples/cmd/basic_workflow.png +0 -0
- data/examples/cmd/dev_workflow.png +0 -0
- data/examples/cmd/explorer_workflow.png +0 -0
- data/examples/conditional/simple_workflow.png +0 -0
- data/examples/conditional/workflow.png +0 -0
- data/examples/context_management_demo/README.md +43 -0
- data/examples/context_management_demo/workflow.yml +42 -0
- data/examples/direct_coerce_syntax/workflow.png +0 -0
- data/examples/dot_notation/workflow.png +0 -0
- data/examples/exit_on_error/workflow.png +0 -0
- data/examples/grading/rb_test_runner +1 -1
- data/examples/grading/workflow.png +0 -0
- data/examples/interpolation/workflow.png +0 -0
- data/examples/interpolation/workflow.yml +1 -1
- data/examples/iteration/workflow.png +0 -0
- data/examples/json_handling/workflow.png +0 -0
- data/examples/mcp/database_workflow.png +0 -0
- data/examples/mcp/env_demo/workflow.png +0 -0
- data/examples/mcp/filesystem_demo/workflow.png +0 -0
- data/examples/mcp/github_workflow.png +0 -0
- data/examples/mcp/multi_mcp_workflow.png +0 -0
- data/examples/mcp/workflow.png +0 -0
- data/examples/no_model_fallback/README.md +17 -0
- data/examples/no_model_fallback/analyze_file/prompt.md +1 -0
- data/examples/no_model_fallback/analyze_patterns/prompt.md +27 -0
- data/examples/no_model_fallback/generate_report_for_md/prompt.md +10 -0
- data/examples/no_model_fallback/generate_report_for_rb/prompt.md +3 -0
- data/examples/no_model_fallback/sample.rb +42 -0
- data/examples/no_model_fallback/workflow.yml +19 -0
- data/examples/openrouter_example/workflow.png +0 -0
- data/examples/pre_post_processing/workflow.png +0 -0
- data/examples/rspec_to_minitest/workflow.png +0 -0
- data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
- data/examples/shared_config/shared.png +0 -0
- data/examples/single_target_prepost/workflow.png +0 -0
- data/examples/smart_coercion_defaults/workflow.png +0 -0
- data/examples/step_configuration/workflow.png +0 -0
- data/examples/swarm_example.yml +25 -0
- data/examples/tool_config_example/workflow.png +0 -0
- data/examples/user_input/funny_name/workflow.png +0 -0
- data/examples/user_input/simple_input_demo/workflow.png +0 -0
- data/examples/user_input/survey_workflow.png +0 -0
- data/examples/user_input/workflow.png +0 -0
- data/examples/workflow_generator/workflow.png +0 -0
- data/lib/roast/errors.rb +3 -0
- data/lib/roast/helpers/timeout_handler.rb +91 -0
- data/lib/roast/services/context_threshold_checker.rb +42 -0
- data/lib/roast/services/token_counting_service.rb +44 -0
- data/lib/roast/tools/apply_diff.rb +128 -0
- data/lib/roast/tools/bash.rb +15 -9
- data/lib/roast/tools/cmd.rb +32 -12
- data/lib/roast/tools/coding_agent.rb +65 -10
- data/lib/roast/tools/context_summarizer.rb +108 -0
- data/lib/roast/tools/swarm.rb +124 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/agent_step.rb +9 -2
- data/lib/roast/workflow/base_iteration_step.rb +3 -2
- data/lib/roast/workflow/base_workflow.rb +41 -2
- data/lib/roast/workflow/command_executor.rb +3 -1
- data/lib/roast/workflow/configuration.rb +2 -1
- data/lib/roast/workflow/configuration_loader.rb +63 -1
- data/lib/roast/workflow/configuration_parser.rb +2 -0
- data/lib/roast/workflow/context_manager.rb +89 -0
- data/lib/roast/workflow/each_step.rb +1 -1
- data/lib/roast/workflow/input_step.rb +2 -0
- data/lib/roast/workflow/interpolator.rb +23 -1
- data/lib/roast/workflow/output_handler.rb +1 -1
- data/lib/roast/workflow/repeat_step.rb +1 -1
- data/lib/roast/workflow/replay_handler.rb +1 -1
- data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
- data/lib/roast/workflow/state_manager.rb +2 -2
- data/lib/roast/workflow/state_repository_factory.rb +36 -0
- data/lib/roast/workflow/step_completion_reporter.rb +27 -0
- data/lib/roast/workflow/step_executor_coordinator.rb +19 -18
- data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
- data/lib/roast/workflow/step_loader.rb +1 -1
- data/lib/roast/workflow/step_name_extractor.rb +84 -0
- data/lib/roast/workflow/validation_command.rb +197 -0
- data/lib/roast/workflow/validators/base_validator.rb +44 -0
- data/lib/roast/workflow/validators/dependency_validator.rb +223 -0
- data/lib/roast/workflow/validators/linting_validator.rb +113 -0
- data/lib/roast/workflow/validators/schema_validator.rb +90 -0
- data/lib/roast/workflow/validators/step_collector.rb +57 -0
- data/lib/roast/workflow/validators/validation_orchestrator.rb +52 -0
- data/lib/roast/workflow/workflow_executor.rb +11 -4
- data/lib/roast/workflow/workflow_initializer.rb +80 -0
- data/lib/roast/workflow/workflow_runner.rb +6 -0
- data/lib/roast/workflow_diagram_generator.rb +298 -0
- data/lib/roast.rb +158 -0
- data/roast.gemspec +4 -1
- data/schema/workflow.json +77 -1
- 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
|
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(
|
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(
|
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 =
|
64
|
-
|
65
|
-
command = "cat #{temp_file.path} | #{
|
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
@@ -12,8 +12,15 @@ module Roast
|
|
12
12
|
read_sidecar_prompt
|
13
13
|
end
|
14
14
|
|
15
|
-
#
|
16
|
-
|
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.
|
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
|