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
@@ -0,0 +1,73 @@
|
|
1
|
+
name: interactive_deployment
|
2
|
+
description: Interactive deployment workflow with user confirmations
|
3
|
+
model: gpt-4o
|
4
|
+
|
5
|
+
steps:
|
6
|
+
# Collect deployment information
|
7
|
+
- input:
|
8
|
+
prompt: "Which environment do you want to deploy to?"
|
9
|
+
name: environment
|
10
|
+
type: choice
|
11
|
+
options:
|
12
|
+
- development
|
13
|
+
- staging
|
14
|
+
- production
|
15
|
+
required: true
|
16
|
+
|
17
|
+
- input:
|
18
|
+
prompt: "Enter the deployment tag/version:"
|
19
|
+
name: deploy_tag
|
20
|
+
required: true
|
21
|
+
default: "latest"
|
22
|
+
|
23
|
+
# Show deployment plan
|
24
|
+
- bash:
|
25
|
+
command: |
|
26
|
+
echo "==================================="
|
27
|
+
echo " DEPLOYMENT PLAN"
|
28
|
+
echo "==================================="
|
29
|
+
echo "Environment: {{workflow.output.environment}}"
|
30
|
+
echo "Version: {{workflow.output.deploy_tag}}"
|
31
|
+
echo "Timestamp: {{Date.today}}"
|
32
|
+
echo "==================================="
|
33
|
+
|
34
|
+
# Confirm deployment
|
35
|
+
- input:
|
36
|
+
prompt: "Deploy {{workflow.output.deploy_tag}} to {{workflow.output.environment}}?"
|
37
|
+
type: boolean
|
38
|
+
default: false
|
39
|
+
name: confirm_deploy
|
40
|
+
|
41
|
+
# Execute deployment if confirmed
|
42
|
+
- if: "{{workflow.output.confirm_deploy}}"
|
43
|
+
then:
|
44
|
+
- bash:
|
45
|
+
command: echo "🚀 Starting deployment to {{workflow.output.environment}}..."
|
46
|
+
|
47
|
+
# Simulate deployment steps
|
48
|
+
- bash:
|
49
|
+
command: |
|
50
|
+
echo "🚀 Deploying version {{workflow.output.deploy_tag}} to {{workflow.output.environment}}"
|
51
|
+
echo "📦 Pulling Docker image: myapp:{{workflow.output.deploy_tag}}"
|
52
|
+
sleep 2
|
53
|
+
echo "🔄 Rolling out to {{workflow.output.environment}} cluster"
|
54
|
+
sleep 2
|
55
|
+
echo "✅ Deployment completed successfully!"
|
56
|
+
|
57
|
+
# Post-deployment verification
|
58
|
+
- input:
|
59
|
+
prompt: "Run smoke tests?"
|
60
|
+
type: boolean
|
61
|
+
default: true
|
62
|
+
name: run_tests
|
63
|
+
|
64
|
+
- if: "{{workflow.output.run_tests}}"
|
65
|
+
then:
|
66
|
+
- bash:
|
67
|
+
command: |
|
68
|
+
echo "🧪 Running smoke tests..."
|
69
|
+
sleep 1
|
70
|
+
echo "✅ All tests passed!"
|
71
|
+
else:
|
72
|
+
- bash:
|
73
|
+
command: echo "❌ Deployment cancelled by user."
|
@@ -15,7 +15,7 @@ Extract the workflow name from the user input JSON and create the workflow in th
|
|
15
15
|
|
16
16
|
Steps to complete:
|
17
17
|
|
18
|
-
1. **Create the main directory**: Use Cmd to create the "
|
18
|
+
1. **Create the main directory**: Use Cmd to create the "<%= workflow_name %>" directory
|
19
19
|
2. **Create step directories**: Create subdirectories for each workflow step
|
20
20
|
3. **Create workflow.yml**: Write the main workflow configuration file
|
21
21
|
4. **Create step prompt files**: Write each step's prompt.md file
|
Binary file
|
data/lib/roast/errors.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Roast
|
4
|
-
|
5
|
-
|
4
|
+
module Errors
|
5
|
+
# Custom error for API resource not found (404) responses
|
6
|
+
class ResourceNotFoundError < StandardError; end
|
6
7
|
|
7
|
-
|
8
|
-
|
8
|
+
# Custom error for when API authentication fails
|
9
|
+
class AuthenticationError < StandardError; end
|
10
|
+
end
|
9
11
|
end
|
data/lib/roast/helpers/logger.rb
CHANGED
@@ -1,54 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "logger"
|
4
|
-
require "forwardable"
|
5
|
-
|
6
3
|
module Roast
|
7
4
|
module Helpers
|
8
5
|
# Central logger for the Roast application
|
9
6
|
class Logger
|
10
|
-
extend Forwardable
|
11
7
|
VALID_LOG_LEVELS = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"].freeze
|
12
8
|
|
13
|
-
|
14
|
-
|
15
|
-
# Delegate info and warn methods to the underlying logger
|
16
|
-
def_delegators :logger, :info, :warn
|
17
|
-
|
18
|
-
# Create a specialized debug method that ensures proper functionality
|
19
|
-
def debug(message)
|
20
|
-
logger.debug(message)
|
21
|
-
end
|
22
|
-
|
23
|
-
def error(message)
|
24
|
-
# Add any custom error handling logic here
|
25
|
-
logger.error(message)
|
26
|
-
end
|
27
|
-
|
28
|
-
def fatal(message)
|
29
|
-
# Add any custom fatal error handling logic here
|
30
|
-
logger.fatal(message)
|
31
|
-
end
|
32
|
-
|
33
|
-
def initialize(stdout: $stdout, log_level: ENV["ROAST_LOG_LEVEL"] || "INFO")
|
34
|
-
@log_level = validate_log_level(log_level)
|
35
|
-
@logger = create_logger(stdout)
|
36
|
-
end
|
9
|
+
delegate_missing_to :@logger
|
37
10
|
|
38
|
-
|
39
|
-
@log_level = validate_log_level(level)
|
40
|
-
logger.level = ::Logger.const_get(@log_level)
|
41
|
-
end
|
11
|
+
attr_reader :logger
|
42
12
|
|
43
13
|
class << self
|
44
|
-
|
14
|
+
delegate_missing_to :instance
|
45
15
|
|
46
16
|
def instance
|
47
17
|
@instance ||= new
|
48
18
|
end
|
49
19
|
|
50
|
-
#
|
51
|
-
|
20
|
+
# Override Kernel#warn to ensure proper delegation
|
21
|
+
def warn(*args)
|
22
|
+
instance.warn(*args)
|
23
|
+
end
|
52
24
|
|
53
25
|
# For testing purposes
|
54
26
|
def reset
|
@@ -58,6 +30,11 @@ module Roast
|
|
58
30
|
|
59
31
|
private
|
60
32
|
|
33
|
+
def initialize(stdout: $stdout, log_level: ENV["ROAST_LOG_LEVEL"] || "INFO")
|
34
|
+
@log_level = validate_log_level(log_level)
|
35
|
+
@logger = create_logger(stdout)
|
36
|
+
end
|
37
|
+
|
61
38
|
def validate_log_level(level)
|
62
39
|
level_str = level.to_s.upcase
|
63
40
|
unless VALID_LOG_LEVELS.include?(level_str)
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timeout"
|
4
|
+
require "open3"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Helpers
|
8
|
+
# Shared timeout handling logic for command-based tools
|
9
|
+
#
|
10
|
+
# This class provides centralized timeout functionality for executing shell commands
|
11
|
+
# with proper process management and resource cleanup.
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# output, status = TimeoutHandler.call("echo hello", timeout: 5)
|
15
|
+
#
|
16
|
+
# @example With custom working directory
|
17
|
+
# output, status = TimeoutHandler.call("pwd", timeout: 10, working_directory: "/tmp")
|
18
|
+
class TimeoutHandler
|
19
|
+
DEFAULT_TIMEOUT = 30
|
20
|
+
MAX_TIMEOUT = 300
|
21
|
+
|
22
|
+
class << self
|
23
|
+
# Execute a command with timeout using Open3 with proper process cleanup
|
24
|
+
# @param command [String] The command to execute
|
25
|
+
# @param timeout [Integer] Timeout in seconds
|
26
|
+
# @param working_directory [String] Directory to execute in (default: Dir.pwd)
|
27
|
+
# @return [Array<String, Integer>] [output, exit_status]
|
28
|
+
# @raise [Timeout::Error] When command exceeds timeout duration
|
29
|
+
def call(command, timeout: DEFAULT_TIMEOUT, working_directory: Dir.pwd)
|
30
|
+
timeout = validate_timeout(timeout)
|
31
|
+
output = ""
|
32
|
+
exit_status = nil
|
33
|
+
wait_thr = nil
|
34
|
+
|
35
|
+
begin
|
36
|
+
Timeout.timeout(timeout) do
|
37
|
+
stdin, stdout, stderr, wait_thr = Open3.popen3(command, chdir: working_directory)
|
38
|
+
stdin.close # Prevent hanging on stdin-waiting commands
|
39
|
+
output = stdout.read + stderr.read
|
40
|
+
wait_thr.join
|
41
|
+
exit_status = wait_thr.value.exitstatus
|
42
|
+
|
43
|
+
[stdout, stderr].each(&:close)
|
44
|
+
end
|
45
|
+
rescue Timeout::Error
|
46
|
+
# Clean up any remaining processes to prevent zombies
|
47
|
+
cleanup_process(wait_thr) if wait_thr&.alive?
|
48
|
+
raise Timeout::Error, "Command '#{command}' in '#{working_directory}' timed out after #{timeout} seconds"
|
49
|
+
end
|
50
|
+
|
51
|
+
[output, exit_status]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Validate and normalize timeout value
|
55
|
+
# @param timeout [Integer, nil] Raw timeout value
|
56
|
+
# @return [Integer] Validated timeout between 1 and MAX_TIMEOUT
|
57
|
+
def validate_timeout(timeout)
|
58
|
+
return DEFAULT_TIMEOUT if timeout.nil? || timeout <= 0
|
59
|
+
|
60
|
+
[timeout, MAX_TIMEOUT].min
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Clean up process on timeout to prevent zombie processes
|
66
|
+
# @param wait_thr [Process::Waiter] The process thread to clean up
|
67
|
+
def cleanup_process(wait_thr)
|
68
|
+
return unless wait_thr&.alive?
|
69
|
+
|
70
|
+
pid = wait_thr.pid
|
71
|
+
# First try graceful termination
|
72
|
+
Process.kill("TERM", pid)
|
73
|
+
sleep(0.1)
|
74
|
+
|
75
|
+
# Force kill if still alive
|
76
|
+
if wait_thr.alive?
|
77
|
+
Process.kill("KILL", pid)
|
78
|
+
end
|
79
|
+
rescue Errno::ESRCH
|
80
|
+
# Process already terminated, which is fine
|
81
|
+
rescue Errno::EPERM
|
82
|
+
# Permission denied - process may be owned by different user
|
83
|
+
Roast::Helpers::Logger.debug("Could not kill process #{pid}: Permission denied")
|
84
|
+
rescue => e
|
85
|
+
# Catch any other unexpected errors during cleanup
|
86
|
+
Roast::Helpers::Logger.debug("Unexpected error during process cleanup: #{e.message}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/roast/resources.rb
CHANGED
@@ -1,13 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roast/resources/base_resource"
|
4
|
-
require "roast/resources/file_resource"
|
5
|
-
require "roast/resources/directory_resource"
|
6
|
-
require "roast/resources/url_resource"
|
7
|
-
require "roast/resources/api_resource"
|
8
|
-
require "roast/resources/none_resource"
|
9
|
-
require "uri"
|
10
|
-
|
11
3
|
module Roast
|
12
4
|
# The Resources module contains classes for handling different types of resources
|
13
5
|
# that workflows can operate on. Each resource type implements a common interface.
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Services
|
5
|
+
class ContextThresholdChecker
|
6
|
+
# Default max tokens if not specified (128k for GPT-4)
|
7
|
+
DEFAULT_MAX_TOKENS = 128_000
|
8
|
+
|
9
|
+
# Warning threshold as percentage of compaction threshold
|
10
|
+
WARNING_THRESHOLD_RATIO = 0.9
|
11
|
+
|
12
|
+
# Critical threshold as percentage of max tokens
|
13
|
+
CRITICAL_THRESHOLD_RATIO = 0.95
|
14
|
+
|
15
|
+
def should_compact?(token_count, threshold, max_tokens)
|
16
|
+
max_tokens ||= DEFAULT_MAX_TOKENS
|
17
|
+
token_count >= (max_tokens * threshold)
|
18
|
+
end
|
19
|
+
|
20
|
+
def check_warning_threshold(token_count, compaction_threshold, max_tokens)
|
21
|
+
max_tokens ||= DEFAULT_MAX_TOKENS
|
22
|
+
percentage_used = (token_count.to_f / max_tokens * 100).round
|
23
|
+
|
24
|
+
if token_count >= (max_tokens * CRITICAL_THRESHOLD_RATIO)
|
25
|
+
{
|
26
|
+
level: :critical,
|
27
|
+
percentage_used: percentage_used,
|
28
|
+
tokens_used: token_count,
|
29
|
+
max_tokens: max_tokens,
|
30
|
+
}
|
31
|
+
elsif token_count >= (max_tokens * compaction_threshold * WARNING_THRESHOLD_RATIO)
|
32
|
+
{
|
33
|
+
level: :approaching_limit,
|
34
|
+
percentage_used: percentage_used,
|
35
|
+
tokens_used: token_count,
|
36
|
+
max_tokens: max_tokens,
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Services
|
5
|
+
class TokenCountingService
|
6
|
+
# Approximate character-to-token ratio for English text
|
7
|
+
# Based on OpenAI's rule of thumb: ~4 characters per token
|
8
|
+
CHARS_PER_TOKEN = 4.0
|
9
|
+
|
10
|
+
# Base token overhead for message structure
|
11
|
+
MESSAGE_OVERHEAD_TOKENS = 3
|
12
|
+
|
13
|
+
def count_messages(messages)
|
14
|
+
return 0 if messages.nil? || messages.empty?
|
15
|
+
|
16
|
+
messages.sum do |message|
|
17
|
+
count_message(message)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def count_message(message)
|
24
|
+
return 0 if message.nil?
|
25
|
+
|
26
|
+
role_tokens = estimate_tokens(message[:role].to_s)
|
27
|
+
content_tokens = estimate_tokens(message[:content].to_s)
|
28
|
+
|
29
|
+
# Don't add overhead for empty messages
|
30
|
+
return 0 if role_tokens == 0 && content_tokens == 0
|
31
|
+
|
32
|
+
# Add overhead for message structure and special tokens
|
33
|
+
role_tokens + content_tokens + MESSAGE_OVERHEAD_TOKENS
|
34
|
+
end
|
35
|
+
|
36
|
+
def estimate_tokens(text)
|
37
|
+
return 0 if text.nil? || text.empty?
|
38
|
+
|
39
|
+
# Simple character-based estimation
|
40
|
+
(text.length / CHARS_PER_TOKEN).ceil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cli/ui"
|
4
|
+
|
5
|
+
module Roast
|
6
|
+
module Tools
|
7
|
+
module ApplyDiff
|
8
|
+
extend self
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def included(base)
|
12
|
+
base.class_eval do
|
13
|
+
function(
|
14
|
+
:apply_diff,
|
15
|
+
"Show a diff to the user and apply changes based on their yes/no response",
|
16
|
+
file_path: { type: "string", description: "Path to the file to modify" },
|
17
|
+
old_content: { type: "string", description: "The current content to be replaced" },
|
18
|
+
new_content: { type: "string", description: "The new content to replace with" },
|
19
|
+
description: { type: "string", description: "Optional description of the change", required: false },
|
20
|
+
) do |params|
|
21
|
+
Roast::Tools::ApplyDiff.call(
|
22
|
+
params[:file_path],
|
23
|
+
params[:old_content],
|
24
|
+
params[:new_content],
|
25
|
+
params[:description],
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(file_path, old_content, new_content, description = nil)
|
33
|
+
unless File.exist?(file_path)
|
34
|
+
error_msg = "File not found: #{file_path}"
|
35
|
+
Roast::Helpers::Logger.error(error_msg + "\n")
|
36
|
+
return error_msg
|
37
|
+
end
|
38
|
+
|
39
|
+
current_content = File.read(file_path)
|
40
|
+
unless current_content.include?(old_content)
|
41
|
+
error_msg = "Old content not found in file: #{file_path}"
|
42
|
+
Roast::Helpers::Logger.error(error_msg + "\n")
|
43
|
+
return error_msg
|
44
|
+
end
|
45
|
+
|
46
|
+
# Show the diff
|
47
|
+
show_diff(file_path, old_content, new_content, description)
|
48
|
+
|
49
|
+
# Ask for confirmation
|
50
|
+
prompt_text = "Apply this change? (y/n)"
|
51
|
+
response = ::CLI::UI::Prompt.ask(prompt_text)
|
52
|
+
|
53
|
+
if response.to_s.downcase.start_with?("y")
|
54
|
+
# Apply the change
|
55
|
+
updated_content = current_content.gsub(old_content, new_content)
|
56
|
+
File.write(file_path, updated_content)
|
57
|
+
|
58
|
+
success_msg = "✅ Changes applied to #{file_path}"
|
59
|
+
Roast::Helpers::Logger.info(success_msg + "\n")
|
60
|
+
success_msg
|
61
|
+
else
|
62
|
+
cancel_msg = "❌ Changes cancelled for #{file_path}"
|
63
|
+
Roast::Helpers::Logger.info(cancel_msg + "\n")
|
64
|
+
cancel_msg
|
65
|
+
end
|
66
|
+
rescue StandardError => e
|
67
|
+
error_message = "Error applying diff: #{e.message}"
|
68
|
+
Roast::Helpers::Logger.error(error_message + "\n")
|
69
|
+
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
70
|
+
error_message
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def show_diff(file_path, old_content, new_content, description)
|
76
|
+
require "tmpdir"
|
77
|
+
|
78
|
+
Roast::Helpers::Logger.info("📝 Proposed change for #{file_path}:\n")
|
79
|
+
|
80
|
+
if description
|
81
|
+
Roast::Helpers::Logger.info("Description: #{description}\n\n")
|
82
|
+
end
|
83
|
+
|
84
|
+
# Create temporary files for git diff
|
85
|
+
Dir.mktmpdir do |tmpdir|
|
86
|
+
# Write current content with old_content replaced by new_content
|
87
|
+
current_content = File.read(file_path)
|
88
|
+
updated_content = current_content.gsub(old_content, new_content)
|
89
|
+
|
90
|
+
# Create temp file with the proposed changes
|
91
|
+
temp_file = File.join(tmpdir, File.basename(file_path))
|
92
|
+
File.write(temp_file, updated_content)
|
93
|
+
|
94
|
+
# Run git diff
|
95
|
+
diff_output = %x(git diff --no-index --no-prefix "#{file_path}" "#{temp_file}" 2>/dev/null)
|
96
|
+
|
97
|
+
if diff_output.empty?
|
98
|
+
Roast::Helpers::Logger.info("No differences found (files are identical)\n")
|
99
|
+
else
|
100
|
+
# Clean up the diff output - remove temp file paths and use relative paths with colors
|
101
|
+
cleaned_diff = diff_output.lines.map do |line|
|
102
|
+
case line
|
103
|
+
when /^diff --git /
|
104
|
+
::CLI::UI.fmt("{{bold:diff --git a/#{file_path} b/#{file_path}}}")
|
105
|
+
when /^--- /
|
106
|
+
::CLI::UI.fmt("{{red:--- a/#{file_path}}}")
|
107
|
+
when /^\+\+\+ /
|
108
|
+
::CLI::UI.fmt("{{green:+++ b/#{file_path}}}")
|
109
|
+
when /^@@/
|
110
|
+
::CLI::UI.fmt("{{cyan:#{line.chomp}}}")
|
111
|
+
when /^-/
|
112
|
+
::CLI::UI.fmt("{{red:#{line.chomp}}}")
|
113
|
+
when /^\+/
|
114
|
+
::CLI::UI.fmt("{{green:#{line.chomp}}}")
|
115
|
+
else
|
116
|
+
line.chomp
|
117
|
+
end
|
118
|
+
end.join("\n")
|
119
|
+
|
120
|
+
Roast::Helpers::Logger.info("#{cleaned_diff}\n")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
Roast::Helpers::Logger.info("\n")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/roast/tools/ask_user.rb
CHANGED
data/lib/roast/tools/bash.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
|
@@ -15,14 +16,15 @@ module Roast
|
|
15
16
|
:bash,
|
16
17
|
"Execute any bash command without restrictions. ⚠️ WARNING: Use only in trusted environments!",
|
17
18
|
command: { type: "string", description: "The bash command to execute" },
|
19
|
+
timeout: { type: "integer", description: "Timeout in seconds (optional, default: 30)", required: false },
|
18
20
|
) do |params|
|
19
|
-
Roast::Tools::Bash.call(params[:command])
|
21
|
+
Roast::Tools::Bash.call(params[:command], timeout: params[:timeout])
|
20
22
|
end
|
21
23
|
end
|
22
24
|
end
|
23
25
|
end
|
24
26
|
|
25
|
-
def call(command)
|
27
|
+
def call(command, timeout: 30)
|
26
28
|
Roast::Helpers::Logger.info("🚀 Executing bash command: #{command}\n")
|
27
29
|
|
28
30
|
# Show warning unless explicitly disabled
|
@@ -30,15 +32,16 @@ module Roast
|
|
30
32
|
Roast::Helpers::Logger.warn("⚠️ WARNING: Unrestricted bash execution - use with caution!\n")
|
31
33
|
end
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
exit_status = $CHILD_STATUS.exitstatus
|
35
|
+
result, exit_status = Roast::Helpers::TimeoutHandler.call(
|
36
|
+
"#{command} 2>&1",
|
37
|
+
timeout: timeout,
|
38
|
+
working_directory: Dir.pwd,
|
39
|
+
)
|
40
40
|
|
41
41
|
format_output(command, result, exit_status)
|
42
|
+
rescue Timeout::Error => e
|
43
|
+
Roast::Helpers::Logger.error(e.message + "\n")
|
44
|
+
e.message
|
42
45
|
rescue StandardError => e
|
43
46
|
handle_error(e)
|
44
47
|
end
|