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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yaml +2 -2
- data/.gitignore +1 -0
- data/CHANGELOG.md +85 -0
- data/CLAUDE.md +106 -9
- data/Gemfile +4 -1
- data/Gemfile.lock +70 -16
- data/README.md +159 -8
- data/bin/console +1 -0
- data/bin/roast +1 -1
- data/claude-swarm.yml +210 -0
- data/docs/AGENT_STEPS.md +288 -0
- 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/README.md +75 -0
- data/examples/agent_workflow/apply_refactorings/prompt.md +22 -0
- data/examples/agent_workflow/identify_code_smells/prompt.md +15 -0
- data/examples/agent_workflow/summarize_improvements/prompt.md +18 -0
- data/examples/agent_workflow/workflow.png +0 -0
- data/examples/agent_workflow/workflow.yml +16 -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/README.md +42 -0
- data/examples/available_tools_demo/analyze_files/prompt.md +6 -0
- data/examples/available_tools_demo/explore_directory/prompt.md +6 -0
- data/examples/available_tools_demo/workflow.png +0 -0
- data/examples/available_tools_demo/workflow.yml +32 -0
- data/examples/available_tools_demo/write_summary/prompt.md +6 -0
- data/examples/bash_prototyping/api_testing.png +0 -0
- data/examples/bash_prototyping/system_analysis.png +0 -0
- data/examples/case_when/detect_language/prompt.md +2 -2
- 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/run_coverage.rb +0 -2
- data/examples/grading/workflow.png +0 -0
- data/examples/interpolation/workflow.png +0 -0
- data/examples/interpolation/workflow.yml +1 -1
- data/examples/iteration/analyze_complexity/prompt.md +2 -2
- data/examples/iteration/generate_recommendations/prompt.md +2 -2
- data/examples/iteration/implement_fix/prompt.md +2 -2
- data/examples/iteration/prioritize_issues/prompt.md +1 -1
- data/examples/iteration/prompts/analyze_file.md +2 -2
- data/examples/iteration/prompts/generate_summary.md +1 -1
- data/examples/iteration/prompts/update_report.md +3 -3
- data/examples/iteration/prompts/write_report.md +3 -3
- data/examples/iteration/read_file/prompt.md +2 -2
- data/examples/iteration/select_next_issue/prompt.md +2 -2
- data/examples/iteration/update_fix_count/prompt.md +4 -4
- data/examples/iteration/verify_fix/prompt.md +3 -3
- data/examples/iteration/workflow.png +0 -0
- data/examples/json_handling/workflow.png +0 -0
- data/examples/mcp/README.md +3 -3
- data/examples/mcp/analyze_changes/prompt.md +1 -1
- data/examples/mcp/database_workflow.png +0 -0
- data/examples/mcp/database_workflow.yml +1 -1
- data/examples/mcp/env_demo/workflow.png +0 -0
- data/examples/mcp/fetch_pr_context/prompt.md +1 -1
- data/examples/mcp/filesystem_demo/workflow.png +0 -0
- data/examples/mcp/github_workflow.png +0 -0
- data/examples/mcp/github_workflow.yml +1 -1
- data/examples/mcp/multi_mcp_workflow.png +0 -0
- data/examples/mcp/post_review/prompt.md +1 -1
- 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/analyze_test_file/prompt.md +1 -1
- data/examples/pre_post_processing/improve_test_coverage/prompt.md +1 -1
- data/examples/pre_post_processing/optimize_test_performance/prompt.md +1 -1
- data/examples/pre_post_processing/post_processing/aggregate_metrics/prompt.md +2 -2
- data/examples/pre_post_processing/post_processing/generate_summary_report/prompt.md +1 -1
- data/examples/pre_post_processing/pre_processing/setup_test_environment/prompt.md +1 -1
- data/examples/pre_post_processing/validate_changes/prompt.md +2 -2
- 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/README.md +90 -0
- data/examples/user_input/funny_name/create_backstory/prompt.md +10 -0
- data/examples/user_input/funny_name/workflow.png +0 -0
- data/examples/user_input/funny_name/workflow.yml +26 -0
- data/examples/user_input/generate_summary/prompt.md +11 -0
- data/examples/user_input/simple_input_demo/workflow.png +0 -0
- data/examples/user_input/simple_input_demo/workflow.yml +35 -0
- data/examples/user_input/survey_workflow.png +0 -0
- data/examples/user_input/survey_workflow.yml +71 -0
- data/examples/user_input/welcome_message/prompt.md +3 -0
- data/examples/user_input/workflow.png +0 -0
- data/examples/user_input/workflow.yml +73 -0
- data/examples/workflow_generator/create_workflow_files/prompt.md +1 -1
- data/examples/workflow_generator/workflow.png +0 -0
- data/lib/roast/errors.rb +6 -4
- data/lib/roast/helpers/function_caching_interceptor.rb +0 -2
- data/lib/roast/helpers/logger.rb +12 -35
- data/lib/roast/helpers/minitest_coverage_runner.rb +0 -1
- data/lib/roast/helpers/prompt_loader.rb +0 -2
- data/lib/roast/helpers/timeout_handler.rb +91 -0
- data/lib/roast/resources/api_resource.rb +0 -4
- data/lib/roast/resources/url_resource.rb +0 -3
- data/lib/roast/resources.rb +0 -8
- 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/ask_user.rb +0 -2
- data/lib/roast/tools/bash.rb +12 -9
- data/lib/roast/tools/cmd.rb +29 -12
- data/lib/roast/tools/coding_agent.rb +65 -17
- data/lib/roast/tools/context_summarizer.rb +108 -0
- data/lib/roast/tools/grep.rb +0 -3
- data/lib/roast/tools/helpers/coding_agent_message_formatter.rb +1 -4
- data/lib/roast/tools/read_file.rb +0 -2
- data/lib/roast/tools/search_file.rb +0 -2
- data/lib/roast/tools/swarm.rb +124 -0
- data/lib/roast/tools/update_files.rb +0 -4
- data/lib/roast/tools/write_file.rb +0 -3
- data/lib/roast/tools.rb +0 -13
- data/lib/roast/value_objects/step_name.rb +14 -3
- data/lib/roast/value_objects/workflow_path.rb +0 -2
- data/lib/roast/value_objects.rb +4 -4
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/agent_step.rb +33 -0
- data/lib/roast/workflow/api_configuration.rb +0 -4
- data/lib/roast/workflow/base_iteration_step.rb +3 -6
- data/lib/roast/workflow/base_step.rb +54 -28
- data/lib/roast/workflow/base_workflow.rb +43 -23
- data/lib/roast/workflow/case_executor.rb +0 -1
- data/lib/roast/workflow/case_step.rb +0 -4
- data/lib/roast/workflow/command_executor.rb +0 -2
- data/lib/roast/workflow/conditional_executor.rb +0 -1
- data/lib/roast/workflow/conditional_step.rb +0 -4
- data/lib/roast/workflow/configuration.rb +5 -67
- data/lib/roast/workflow/configuration_loader.rb +63 -3
- data/lib/roast/workflow/configuration_parser.rb +1 -7
- data/lib/roast/workflow/context_manager.rb +89 -0
- data/lib/roast/workflow/dot_access_hash.rb +16 -1
- data/lib/roast/workflow/each_step.rb +1 -1
- data/lib/roast/workflow/error_handler.rb +0 -3
- data/lib/roast/workflow/expression_evaluator.rb +0 -3
- data/lib/roast/workflow/file_state_repository.rb +0 -5
- data/lib/roast/workflow/input_executor.rb +41 -0
- data/lib/roast/workflow/input_step.rb +163 -0
- data/lib/roast/workflow/iteration_executor.rb +0 -2
- data/lib/roast/workflow/output_handler.rb +1 -3
- data/lib/roast/workflow/output_manager.rb +0 -2
- data/lib/roast/workflow/repeat_step.rb +1 -1
- data/lib/roast/workflow/replay_handler.rb +1 -4
- data/lib/roast/workflow/resource_resolver.rb +0 -3
- data/lib/roast/workflow/session_manager.rb +0 -3
- data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
- data/lib/roast/workflow/state_manager.rb +2 -4
- 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 +48 -24
- data/lib/roast/workflow/step_executor_factory.rb +0 -5
- data/lib/roast/workflow/step_executor_registry.rb +1 -4
- data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
- data/lib/roast/workflow/step_executors/hash_step_executor.rb +0 -3
- data/lib/roast/workflow/step_executors/parallel_step_executor.rb +0 -3
- data/lib/roast/workflow/step_executors/string_step_executor.rb +0 -2
- data/lib/roast/workflow/step_factory.rb +56 -0
- data/lib/roast/workflow/step_loader.rb +31 -17
- data/lib/roast/workflow/step_name_extractor.rb +84 -0
- data/lib/roast/workflow/step_orchestrator.rb +3 -2
- data/lib/roast/workflow/step_type_resolver.rb +28 -1
- data/lib/roast/workflow/validation_command.rb +197 -0
- data/lib/roast/workflow/validator.rb +0 -4
- 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 -20
- data/lib/roast/workflow/workflow_initializer.rb +1 -8
- data/lib/roast/workflow/workflow_runner.rb +6 -7
- data/lib/roast/workflow.rb +0 -15
- data/lib/roast/workflow_diagram_generator.rb +298 -0
- data/lib/roast.rb +212 -10
- data/roast.gemspec +4 -2
- data/schema/workflow.json +123 -1
- metadata +143 -6
- data/lib/roast/helpers.rb +0 -12
data/lib/roast/tools/cmd.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "English"
|
4
4
|
require "roast/helpers/logger"
|
5
|
+
require "roast/helpers/timeout_handler"
|
5
6
|
|
6
7
|
module Roast
|
7
8
|
module Tools
|
@@ -65,6 +66,11 @@ module Roast
|
|
65
66
|
description: "Arguments to pass to the #{command} command",
|
66
67
|
required: false,
|
67
68
|
},
|
69
|
+
timeout: {
|
70
|
+
type: "integer",
|
71
|
+
description: "Timeout in seconds (optional, default: 30)",
|
72
|
+
required: false,
|
73
|
+
},
|
68
74
|
) do |params|
|
69
75
|
full_command = if params[:args].nil? || params[:args].empty?
|
70
76
|
command
|
@@ -72,7 +78,7 @@ module Roast
|
|
72
78
|
"#{command} #{params[:args]}"
|
73
79
|
end
|
74
80
|
|
75
|
-
Roast::Tools::Cmd.execute_allowed_command(full_command, command)
|
81
|
+
Roast::Tools::Cmd.execute_allowed_command(full_command, command, params[:timeout])
|
76
82
|
end
|
77
83
|
end
|
78
84
|
end
|
@@ -83,15 +89,16 @@ module Roast
|
|
83
89
|
end
|
84
90
|
end
|
85
91
|
|
86
|
-
def execute_allowed_command(full_command, command_prefix)
|
92
|
+
def execute_allowed_command(full_command, command_prefix, timeout = 30)
|
87
93
|
Roast::Helpers::Logger.info("🔧 Running command: #{full_command}\n")
|
88
|
-
|
94
|
+
|
95
|
+
execute_command(full_command, command_prefix, timeout)
|
89
96
|
rescue StandardError => e
|
90
97
|
handle_error(e)
|
91
98
|
end
|
92
99
|
|
93
100
|
# Legacy method for backward compatibility
|
94
|
-
def call(command, config = {})
|
101
|
+
def call(command, config = {}, timeout: 30)
|
95
102
|
Roast::Helpers::Logger.info("🔧 Running command: #{command}\n")
|
96
103
|
|
97
104
|
allowed_commands = config[CONFIG_ALLOWED_COMMANDS] || DEFAULT_ALLOWED_COMMANDS
|
@@ -99,7 +106,8 @@ module Roast
|
|
99
106
|
return validation_result unless validation_result.nil?
|
100
107
|
|
101
108
|
command_prefix = command.split(" ").first
|
102
|
-
|
109
|
+
|
110
|
+
execute_command(command, command_prefix, timeout)
|
103
111
|
rescue StandardError => e
|
104
112
|
handle_error(e)
|
105
113
|
end
|
@@ -130,16 +138,25 @@ module Roast
|
|
130
138
|
configuration&.tool_config("Roast::Tools::Cmd") || {}
|
131
139
|
end
|
132
140
|
|
133
|
-
def execute_command(command, command_prefix)
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
141
|
+
def execute_command(command, command_prefix, timeout)
|
142
|
+
timeout = Roast::Helpers::TimeoutHandler.validate_timeout(timeout)
|
143
|
+
|
144
|
+
full_command = if command_prefix == "dev"
|
145
|
+
"bash -l -c '#{command.gsub("'", "\\'")}'"
|
138
146
|
else
|
139
|
-
|
147
|
+
command
|
140
148
|
end
|
141
149
|
|
142
|
-
|
150
|
+
result, exit_status = Roast::Helpers::TimeoutHandler.call(
|
151
|
+
full_command,
|
152
|
+
timeout: timeout,
|
153
|
+
working_directory: Dir.pwd,
|
154
|
+
)
|
155
|
+
|
156
|
+
format_output(command, result, exit_status)
|
157
|
+
rescue Timeout::Error => e
|
158
|
+
Roast::Helpers::Logger.error(e.message + "\n")
|
159
|
+
e.message
|
143
160
|
end
|
144
161
|
|
145
162
|
def format_output(command, result, exit_status)
|
@@ -1,12 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roast/helpers/logger"
|
4
|
-
require "roast/tools/helpers/coding_agent_message_formatter"
|
5
|
-
require "json"
|
6
|
-
require "open3"
|
7
|
-
require "tempfile"
|
8
|
-
require "securerandom"
|
9
|
-
|
10
3
|
module Roast
|
11
4
|
module Tools
|
12
5
|
module CodingAgent
|
@@ -26,10 +19,16 @@ module Roast
|
|
26
19
|
base.class_eval do
|
27
20
|
function(
|
28
21
|
:coding_agent,
|
29
|
-
"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.",
|
30
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 },
|
31
26
|
) do |params|
|
32
|
-
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
|
+
)
|
33
32
|
end
|
34
33
|
end
|
35
34
|
end
|
@@ -40,9 +39,9 @@ module Roast
|
|
40
39
|
end
|
41
40
|
end
|
42
41
|
|
43
|
-
def call(prompt)
|
42
|
+
def call(prompt, include_context_summary: false, continue: false)
|
44
43
|
Roast::Helpers::Logger.info("🤖 Running CodingAgent\n")
|
45
|
-
run_claude_code(prompt)
|
44
|
+
run_claude_code(prompt, include_context_summary:, continue:)
|
46
45
|
rescue StandardError => e
|
47
46
|
"Error running CodingAgent: #{e.message}".tap do |error_message|
|
48
47
|
Roast::Helpers::Logger.error(error_message + "\n")
|
@@ -52,7 +51,7 @@ module Roast
|
|
52
51
|
|
53
52
|
private
|
54
53
|
|
55
|
-
def run_claude_code(prompt)
|
54
|
+
def run_claude_code(prompt, include_context_summary:, continue:)
|
56
55
|
Roast::Helpers::Logger.debug("🤖 Executing Claude Code CLI with prompt: #{prompt}\n")
|
57
56
|
|
58
57
|
# Create a temporary file with a unique name
|
@@ -62,14 +61,21 @@ module Roast
|
|
62
61
|
temp_file = Tempfile.new(["claude_prompt_#{timestamp}_#{pid}_#{random_id}", ".txt"])
|
63
62
|
|
64
63
|
begin
|
64
|
+
# Prepare the final prompt with context summary if requested
|
65
|
+
final_prompt = prepare_prompt(prompt, include_context_summary)
|
66
|
+
|
65
67
|
# Write the prompt to the file
|
66
|
-
temp_file.write(
|
68
|
+
temp_file.write(final_prompt)
|
67
69
|
temp_file.close
|
68
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
|
+
|
69
75
|
# Run Claude Code CLI using the temp file as input with streaming output
|
70
|
-
expect_json_output =
|
71
|
-
|
72
|
-
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}"
|
73
79
|
result = ""
|
74
80
|
|
75
81
|
Open3.popen3(command) do |stdin, stdout, stderr, wait_thread|
|
@@ -110,7 +116,7 @@ module Roast
|
|
110
116
|
def handle_intermediate_message(json)
|
111
117
|
case json["type"]
|
112
118
|
when "assistant", "user"
|
113
|
-
CodingAgentMessageFormatter.format_messages(json).each(&method(:log_message))
|
119
|
+
Roast::Tools::Helpers::CodingAgentMessageFormatter.format_messages(json).each(&method(:log_message))
|
114
120
|
when "result", "system"
|
115
121
|
# Ignore these message types
|
116
122
|
else
|
@@ -143,6 +149,48 @@ module Roast
|
|
143
149
|
def claude_code_command
|
144
150
|
CodingAgent.configured_command || ENV["CLAUDE_CODE_COMMAND"] || "claude -p --verbose --output-format stream-json"
|
145
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
|
193
|
+
end
|
146
194
|
end
|
147
195
|
end
|
148
196
|
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
|
data/lib/roast/tools/grep.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roast/helpers/logger"
|
4
|
-
|
5
3
|
module Roast
|
6
4
|
module Tools
|
7
5
|
module Grep
|
@@ -30,7 +28,6 @@ module Roast
|
|
30
28
|
Roast::Helpers::Logger.info("🔍 Grepping for string: #{string}\n")
|
31
29
|
|
32
30
|
# Use Open3 to safely pass the string as an argument, avoiding shell injection
|
33
|
-
require "open3"
|
34
31
|
cmd = ["rg", "-C", "4", "--trim", "--color=never", "--heading", "-F", "--", string, "."]
|
35
32
|
stdout, _stderr, _status = Open3.capture3(*cmd)
|
36
33
|
|
@@ -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/tools.rb
CHANGED
@@ -1,18 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "English"
|
4
|
-
require "fileutils"
|
5
|
-
|
6
|
-
require "roast/tools/ask_user"
|
7
|
-
require "roast/tools/bash"
|
8
|
-
require "roast/tools/cmd"
|
9
|
-
require "roast/tools/coding_agent"
|
10
|
-
require "roast/tools/grep"
|
11
|
-
require "roast/tools/read_file"
|
12
|
-
require "roast/tools/search_file"
|
13
|
-
require "roast/tools/update_files"
|
14
|
-
require "roast/tools/write_file"
|
15
|
-
|
16
3
|
module Roast
|
17
4
|
module Tools
|
18
5
|
extend self
|
@@ -24,12 +24,23 @@ module Roast
|
|
24
24
|
@value
|
25
25
|
end
|
26
26
|
|
27
|
+
# Enables implicit conversion to String
|
28
|
+
alias_method :to_str, :to_s
|
29
|
+
|
27
30
|
def ==(other)
|
28
|
-
|
31
|
+
case other
|
32
|
+
when StepName
|
33
|
+
value == other.value
|
34
|
+
when String
|
35
|
+
value == other
|
36
|
+
else
|
37
|
+
false
|
38
|
+
end
|
39
|
+
end
|
29
40
|
|
30
|
-
|
41
|
+
def eql?(other)
|
42
|
+
other.is_a?(StepName) && value == other.value
|
31
43
|
end
|
32
|
-
alias_method :eql?, :==
|
33
44
|
|
34
45
|
def hash
|
35
46
|
[self.class, @value].hash
|
data/lib/roast/value_objects.rb
CHANGED
data/lib/roast/version.rb
CHANGED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Workflow
|
5
|
+
class AgentStep < BaseStep
|
6
|
+
def call
|
7
|
+
# For inline prompts (detected by plain text step names), use the name as the prompt
|
8
|
+
# For file-based steps, load from the prompt file
|
9
|
+
prompt_content = if name.plain_text?
|
10
|
+
name.to_s
|
11
|
+
else
|
12
|
+
read_sidecar_prompt
|
13
|
+
end
|
14
|
+
|
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)
|
24
|
+
|
25
|
+
# Process output if print_response is enabled
|
26
|
+
process_output(result, print_response:)
|
27
|
+
|
28
|
+
# Apply coercion if configured
|
29
|
+
apply_coercion(result)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -1,9 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roast/factories/api_provider_factory"
|
4
|
-
require "roast/workflow/resource_resolver"
|
5
|
-
require "roast/value_objects/uri_base"
|
6
|
-
|
7
3
|
module Roast
|
8
4
|
module Workflow
|
9
5
|
# Handles API-related configuration including tokens and providers
|
@@ -1,9 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roast/workflow/expression_utils"
|
4
|
-
require "roast/workflow/llm_boolean_coercer"
|
5
|
-
require "roast/workflow/workflow_executor"
|
6
|
-
|
7
3
|
module Roast
|
8
4
|
module Workflow
|
9
5
|
# Base class for iteration steps (RepeatStep and EachStep)
|
@@ -69,10 +65,11 @@ module Roast
|
|
69
65
|
executor ||= WorkflowExecutor.new(context, config_hash, context_path)
|
70
66
|
results = []
|
71
67
|
|
72
|
-
steps.
|
68
|
+
steps.each_with_index do |step, index|
|
69
|
+
is_last_step = (index == steps.length - 1)
|
73
70
|
result = case step
|
74
71
|
when String
|
75
|
-
executor.execute_step(step)
|
72
|
+
executor.execute_step(step, is_last_step:)
|
76
73
|
when Hash, Array
|
77
74
|
executor.execute_steps([step])
|
78
75
|
end
|