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.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +2 -2
  3. data/CHANGELOG.md +65 -0
  4. data/CLAUDE.md +55 -9
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +8 -1
  7. data/README.md +69 -3
  8. data/bin/console +1 -0
  9. data/docs/AGENT_STEPS.md +33 -9
  10. data/docs/VALIDATION.md +178 -0
  11. data/examples/agent_continue/add_documentation/prompt.md +5 -0
  12. data/examples/agent_continue/add_error_handling/prompt.md +5 -0
  13. data/examples/agent_continue/analyze_codebase/prompt.md +7 -0
  14. data/examples/agent_continue/combined_workflow.yml +24 -0
  15. data/examples/agent_continue/continue_adding_features/prompt.md +4 -0
  16. data/examples/agent_continue/create_integration_tests/prompt.md +3 -0
  17. data/examples/agent_continue/document_with_context/prompt.md +5 -0
  18. data/examples/agent_continue/explore_api/prompt.md +6 -0
  19. data/examples/agent_continue/implement_client/prompt.md +6 -0
  20. data/examples/agent_continue/inline_workflow.yml +20 -0
  21. data/examples/agent_continue/refactor_code/prompt.md +2 -0
  22. data/examples/agent_continue/verify_changes/prompt.md +6 -0
  23. data/examples/agent_continue/workflow.yml +27 -0
  24. data/examples/agent_workflow/workflow.png +0 -0
  25. data/examples/api_workflow/workflow.png +0 -0
  26. data/examples/apply_diff_demo/README.md +58 -0
  27. data/examples/apply_diff_demo/apply_simple_change/prompt.md +13 -0
  28. data/examples/apply_diff_demo/create_sample_file/prompt.md +11 -0
  29. data/examples/apply_diff_demo/workflow.yml +24 -0
  30. data/examples/available_tools_demo/workflow.png +0 -0
  31. data/examples/bash_prototyping/api_testing.png +0 -0
  32. data/examples/bash_prototyping/system_analysis.png +0 -0
  33. data/examples/case_when/workflow.png +0 -0
  34. data/examples/cmd/basic_workflow.png +0 -0
  35. data/examples/cmd/dev_workflow.png +0 -0
  36. data/examples/cmd/explorer_workflow.png +0 -0
  37. data/examples/conditional/simple_workflow.png +0 -0
  38. data/examples/conditional/workflow.png +0 -0
  39. data/examples/context_management_demo/README.md +43 -0
  40. data/examples/context_management_demo/workflow.yml +42 -0
  41. data/examples/direct_coerce_syntax/workflow.png +0 -0
  42. data/examples/dot_notation/workflow.png +0 -0
  43. data/examples/exit_on_error/workflow.png +0 -0
  44. data/examples/grading/workflow.png +0 -0
  45. data/examples/interpolation/workflow.png +0 -0
  46. data/examples/interpolation/workflow.yml +1 -1
  47. data/examples/iteration/workflow.png +0 -0
  48. data/examples/json_handling/workflow.png +0 -0
  49. data/examples/mcp/database_workflow.png +0 -0
  50. data/examples/mcp/env_demo/workflow.png +0 -0
  51. data/examples/mcp/filesystem_demo/workflow.png +0 -0
  52. data/examples/mcp/github_workflow.png +0 -0
  53. data/examples/mcp/multi_mcp_workflow.png +0 -0
  54. data/examples/mcp/workflow.png +0 -0
  55. data/examples/no_model_fallback/README.md +17 -0
  56. data/examples/no_model_fallback/analyze_file/prompt.md +1 -0
  57. data/examples/no_model_fallback/analyze_patterns/prompt.md +27 -0
  58. data/examples/no_model_fallback/generate_report_for_md/prompt.md +10 -0
  59. data/examples/no_model_fallback/generate_report_for_rb/prompt.md +3 -0
  60. data/examples/no_model_fallback/sample.rb +42 -0
  61. data/examples/no_model_fallback/workflow.yml +19 -0
  62. data/examples/openrouter_example/workflow.png +0 -0
  63. data/examples/pre_post_processing/workflow.png +0 -0
  64. data/examples/rspec_to_minitest/workflow.png +0 -0
  65. data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
  66. data/examples/shared_config/shared.png +0 -0
  67. data/examples/single_target_prepost/workflow.png +0 -0
  68. data/examples/smart_coercion_defaults/workflow.png +0 -0
  69. data/examples/step_configuration/workflow.png +0 -0
  70. data/examples/swarm_example.yml +25 -0
  71. data/examples/tool_config_example/workflow.png +0 -0
  72. data/examples/user_input/funny_name/workflow.png +0 -0
  73. data/examples/user_input/simple_input_demo/workflow.png +0 -0
  74. data/examples/user_input/survey_workflow.png +0 -0
  75. data/examples/user_input/workflow.png +0 -0
  76. data/examples/workflow_generator/workflow.png +0 -0
  77. data/lib/roast/helpers/timeout_handler.rb +91 -0
  78. data/lib/roast/services/context_threshold_checker.rb +42 -0
  79. data/lib/roast/services/token_counting_service.rb +44 -0
  80. data/lib/roast/tools/apply_diff.rb +128 -0
  81. data/lib/roast/tools/bash.rb +15 -9
  82. data/lib/roast/tools/cmd.rb +32 -12
  83. data/lib/roast/tools/coding_agent.rb +64 -9
  84. data/lib/roast/tools/context_summarizer.rb +108 -0
  85. data/lib/roast/tools/swarm.rb +124 -0
  86. data/lib/roast/version.rb +1 -1
  87. data/lib/roast/workflow/agent_step.rb +9 -2
  88. data/lib/roast/workflow/base_iteration_step.rb +3 -2
  89. data/lib/roast/workflow/base_workflow.rb +41 -2
  90. data/lib/roast/workflow/configuration.rb +2 -1
  91. data/lib/roast/workflow/configuration_loader.rb +63 -1
  92. data/lib/roast/workflow/context_manager.rb +89 -0
  93. data/lib/roast/workflow/each_step.rb +1 -1
  94. data/lib/roast/workflow/output_handler.rb +1 -1
  95. data/lib/roast/workflow/repeat_step.rb +1 -1
  96. data/lib/roast/workflow/replay_handler.rb +1 -1
  97. data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
  98. data/lib/roast/workflow/state_manager.rb +2 -2
  99. data/lib/roast/workflow/state_repository_factory.rb +36 -0
  100. data/lib/roast/workflow/step_completion_reporter.rb +27 -0
  101. data/lib/roast/workflow/step_executor_coordinator.rb +19 -18
  102. data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
  103. data/lib/roast/workflow/step_loader.rb +1 -1
  104. data/lib/roast/workflow/step_name_extractor.rb +84 -0
  105. data/lib/roast/workflow/validation_command.rb +197 -0
  106. data/lib/roast/workflow/validators/base_validator.rb +44 -0
  107. data/lib/roast/workflow/validators/dependency_validator.rb +223 -0
  108. data/lib/roast/workflow/validators/linting_validator.rb +113 -0
  109. data/lib/roast/workflow/validators/schema_validator.rb +90 -0
  110. data/lib/roast/workflow/validators/step_collector.rb +57 -0
  111. data/lib/roast/workflow/validators/validation_orchestrator.rb +52 -0
  112. data/lib/roast/workflow/workflow_executor.rb +11 -4
  113. data/lib/roast/workflow/workflow_runner.rb +6 -0
  114. data/lib/roast/workflow_diagram_generator.rb +298 -0
  115. data/lib/roast.rb +157 -0
  116. data/roast.gemspec +2 -1
  117. data/schema/workflow.json +77 -1
  118. 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
@@ -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
- # Execute the command without any restrictions
31
- result = ""
32
- IO.popen("#{command} 2>&1", chdir: Dir.pwd) do |io|
33
- result = io.read
34
- end
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
@@ -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
- execute_command(full_command, command_prefix)
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
- execute_command(command, command_prefix)
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
- result = if command_prefix == "dev"
132
- # Use bash -l -c to ensure we get a login shell with all environment variables
133
- full_command = "bash -l -c '#{command.gsub("'", "\\'")}'"
134
- IO.popen(full_command, chdir: Dir.pwd, &:read)
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
- IO.popen(command, chdir: Dir.pwd, &:read)
147
+ command
137
148
  end
138
149
 
139
- format_output(command, result, $CHILD_STATUS.exitstatus)
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 CLI with the given prompt",
22
+ "AI-powered coding agent that runs an instance of the Claude Code agent with the given prompt. If the agent is iterating on previous work, set continue to true.",
23
23
  prompt: { type: "string", description: "The prompt to send to Claude Code" },
24
+ include_context_summary: { type: "boolean", description: "Whether to set a summary of the current workflow context as system directive (default: false)", required: false },
25
+ continue: { type: "boolean", description: "Whether to continue where the previous coding agent left off or start with a fresh context (default: false, start fresh)", required: false },
24
26
  ) do |params|
25
- Roast::Tools::CodingAgent.call(params[:prompt])
27
+ Roast::Tools::CodingAgent.call(
28
+ params[:prompt],
29
+ include_context_summary: params[:include_context_summary].presence || false,
30
+ continue: params[:continue].presence || false,
31
+ )
26
32
  end
27
33
  end
28
34
  end
@@ -33,9 +39,9 @@ module Roast
33
39
  end
34
40
  end
35
41
 
36
- def call(prompt)
42
+ def call(prompt, include_context_summary: false, continue: false)
37
43
  Roast::Helpers::Logger.info("🤖 Running CodingAgent\n")
38
- run_claude_code(prompt)
44
+ run_claude_code(prompt, include_context_summary:, continue:)
39
45
  rescue StandardError => e
40
46
  "Error running CodingAgent: #{e.message}".tap do |error_message|
41
47
  Roast::Helpers::Logger.error(error_message + "\n")
@@ -45,7 +51,7 @@ module Roast
45
51
 
46
52
  private
47
53
 
48
- def run_claude_code(prompt)
54
+ def run_claude_code(prompt, include_context_summary:, continue:)
49
55
  Roast::Helpers::Logger.debug("🤖 Executing Claude Code CLI with prompt: #{prompt}\n")
50
56
 
51
57
  # Create a temporary file with a unique name
@@ -55,14 +61,21 @@ module Roast
55
61
  temp_file = Tempfile.new(["claude_prompt_#{timestamp}_#{pid}_#{random_id}", ".txt"])
56
62
 
57
63
  begin
64
+ # Prepare the final prompt with context summary if requested
65
+ final_prompt = prepare_prompt(prompt, include_context_summary)
66
+
58
67
  # Write the prompt to the file
59
- temp_file.write(prompt)
68
+ temp_file.write(final_prompt)
60
69
  temp_file.close
61
70
 
71
+ # Build the command with continue option if specified
72
+ base_command = claude_code_command
73
+ command_to_run = build_command(base_command, continue:)
74
+
62
75
  # Run Claude Code CLI using the temp file as input with streaming output
63
- expect_json_output = claude_code_command.include?("--output-format stream-json") ||
64
- claude_code_command.include?("--output-format json")
65
- command = "cat #{temp_file.path} | #{claude_code_command}"
76
+ expect_json_output = command_to_run.include?("--output-format stream-json") ||
77
+ command_to_run.include?("--output-format json")
78
+ command = "cat #{temp_file.path} | #{command_to_run}"
66
79
  result = ""
67
80
 
68
81
  Open3.popen3(command) do |stdin, stdout, stderr, wait_thread|
@@ -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