roast-ai 0.4.0 → 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/CHANGELOG.md +65 -0
- data/CLAUDE.md +55 -9
- data/Gemfile +1 -0
- data/Gemfile.lock +8 -1
- 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/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/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 +64 -9
- 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/configuration.rb +2 -1
- data/lib/roast/workflow/configuration_loader.rb +63 -1
- data/lib/roast/workflow/context_manager.rb +89 -0
- data/lib/roast/workflow/each_step.rb +1 -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_runner.rb +6 -0
- data/lib/roast/workflow_diagram_generator.rb +298 -0
- data/lib/roast.rb +157 -0
- data/roast.gemspec +2 -1
- data/schema/workflow.json +77 -1
- metadata +101 -1
@@ -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/bash.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "English"
|
4
|
+
require "roast/helpers/logger"
|
5
|
+
require "roast/helpers/timeout_handler"
|
6
|
+
|
3
7
|
module Roast
|
4
8
|
module Tools
|
5
9
|
module Bash
|
@@ -12,14 +16,15 @@ module Roast
|
|
12
16
|
:bash,
|
13
17
|
"Execute any bash command without restrictions. ⚠️ WARNING: Use only in trusted environments!",
|
14
18
|
command: { type: "string", description: "The bash command to execute" },
|
19
|
+
timeout: { type: "integer", description: "Timeout in seconds (optional, default: 30)", required: false },
|
15
20
|
) do |params|
|
16
|
-
Roast::Tools::Bash.call(params[:command])
|
21
|
+
Roast::Tools::Bash.call(params[:command], timeout: params[:timeout])
|
17
22
|
end
|
18
23
|
end
|
19
24
|
end
|
20
25
|
end
|
21
26
|
|
22
|
-
def call(command)
|
27
|
+
def call(command, timeout: 30)
|
23
28
|
Roast::Helpers::Logger.info("🚀 Executing bash command: #{command}\n")
|
24
29
|
|
25
30
|
# Show warning unless explicitly disabled
|
@@ -27,15 +32,16 @@ module Roast
|
|
27
32
|
Roast::Helpers::Logger.warn("⚠️ WARNING: Unrestricted bash execution - use with caution!\n")
|
28
33
|
end
|
29
34
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
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
|
+
)
|
37
40
|
|
38
41
|
format_output(command, result, exit_status)
|
42
|
+
rescue Timeout::Error => e
|
43
|
+
Roast::Helpers::Logger.error(e.message + "\n")
|
44
|
+
e.message
|
39
45
|
rescue StandardError => e
|
40
46
|
handle_error(e)
|
41
47
|
end
|
data/lib/roast/tools/cmd.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "English"
|
4
|
+
require "roast/helpers/logger"
|
5
|
+
require "roast/helpers/timeout_handler"
|
6
|
+
|
3
7
|
module Roast
|
4
8
|
module Tools
|
5
9
|
module Cmd
|
@@ -62,6 +66,11 @@ module Roast
|
|
62
66
|
description: "Arguments to pass to the #{command} command",
|
63
67
|
required: false,
|
64
68
|
},
|
69
|
+
timeout: {
|
70
|
+
type: "integer",
|
71
|
+
description: "Timeout in seconds (optional, default: 30)",
|
72
|
+
required: false,
|
73
|
+
},
|
65
74
|
) do |params|
|
66
75
|
full_command = if params[:args].nil? || params[:args].empty?
|
67
76
|
command
|
@@ -69,7 +78,7 @@ module Roast
|
|
69
78
|
"#{command} #{params[:args]}"
|
70
79
|
end
|
71
80
|
|
72
|
-
Roast::Tools::Cmd.execute_allowed_command(full_command, command)
|
81
|
+
Roast::Tools::Cmd.execute_allowed_command(full_command, command, params[:timeout])
|
73
82
|
end
|
74
83
|
end
|
75
84
|
end
|
@@ -80,15 +89,16 @@ module Roast
|
|
80
89
|
end
|
81
90
|
end
|
82
91
|
|
83
|
-
def execute_allowed_command(full_command, command_prefix)
|
92
|
+
def execute_allowed_command(full_command, command_prefix, timeout = 30)
|
84
93
|
Roast::Helpers::Logger.info("🔧 Running command: #{full_command}\n")
|
85
|
-
|
94
|
+
|
95
|
+
execute_command(full_command, command_prefix, timeout)
|
86
96
|
rescue StandardError => e
|
87
97
|
handle_error(e)
|
88
98
|
end
|
89
99
|
|
90
100
|
# Legacy method for backward compatibility
|
91
|
-
def call(command, config = {})
|
101
|
+
def call(command, config = {}, timeout: 30)
|
92
102
|
Roast::Helpers::Logger.info("🔧 Running command: #{command}\n")
|
93
103
|
|
94
104
|
allowed_commands = config[CONFIG_ALLOWED_COMMANDS] || DEFAULT_ALLOWED_COMMANDS
|
@@ -96,7 +106,8 @@ module Roast
|
|
96
106
|
return validation_result unless validation_result.nil?
|
97
107
|
|
98
108
|
command_prefix = command.split(" ").first
|
99
|
-
|
109
|
+
|
110
|
+
execute_command(command, command_prefix, timeout)
|
100
111
|
rescue StandardError => e
|
101
112
|
handle_error(e)
|
102
113
|
end
|
@@ -127,16 +138,25 @@ module Roast
|
|
127
138
|
configuration&.tool_config("Roast::Tools::Cmd") || {}
|
128
139
|
end
|
129
140
|
|
130
|
-
def execute_command(command, command_prefix)
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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("'", "\\'")}'"
|
135
146
|
else
|
136
|
-
|
147
|
+
command
|
137
148
|
end
|
138
149
|
|
139
|
-
|
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
|
140
160
|
end
|
141
161
|
|
142
162
|
def format_output(command, result, exit_status)
|
@@ -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|
|
@@ -136,6 +149,48 @@ module Roast
|
|
136
149
|
def claude_code_command
|
137
150
|
CodingAgent.configured_command || ENV["CLAUDE_CODE_COMMAND"] || "claude -p --verbose --output-format stream-json"
|
138
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
|
139
194
|
end
|
140
195
|
end
|
141
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
|