Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
**/.claude/settings.local.json

**/CLAUDE.local.md
.roast/
/.roast/

bin/_guard-core
bin/bundle
Expand Down
2 changes: 1 addition & 1 deletion lib/roast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
require "cli/ui"
require "raix"
require "thor"
require "roast/dot_roast"
require "roast/errors"
require "roast/helpers"
require "roast/initializers"
require "roast/resources"
require "roast/tools"
require "roast/version"
Expand Down
49 changes: 49 additions & 0 deletions lib/roast/dot_roast.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

require "fileutils"
require "roast/helpers/logger"

module Roast
module DotRoast
class << self
def root(starting_path = Dir.pwd, ending_path = File.dirname(Dir.home))
unless starting_path.start_with?(ending_path)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lack of this was a bug in the prev code, pwd wasn't under HOME, you'd end up hanging/looping forever.

Roast::Helpers::Logger.warn(<<~WARN)
Unexpected ending path when looking for .roast:
Starting path #{starting_path} is not a subdir of ending path #{ending_path}
Will check all the way up to root.
WARN

ending_path = "/"
end

candidate = starting_path
until candidate == ending_path
dot_roast_candidate = File.join(candidate, ".roast")
return dot_roast_candidate if Dir.exist?(dot_roast_candidate)

candidate = File.dirname(candidate)
end

File.join(starting_path, ".roast")
end

def ensure_subdir(subdir_name, gitignored: true)
subdir_path = File.join(root, subdir_name)
FileUtils.mkdir_p(subdir_path) unless File.directory?(subdir_path)

if gitignored
gitignore_path = File.join(subdir_path, ".gitignore")
File.write(gitignore_path, "*") unless File.exist?(gitignore_path)
end

subdir_path
end

def subdir_path(subdir_name)
path = File.join(root, subdir_name)
File.directory?(path) ? path : nil
end
end
end
end
2 changes: 1 addition & 1 deletion lib/roast/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
require "roast/helpers/path_resolver"
require "roast/helpers/prompt_loader"
require "roast/helpers/minitest_coverage_runner"
require "roast/helpers/function_caching_interceptor"
require "roast/helpers/function_cache"

module Roast
module Helpers
Expand Down
30 changes: 30 additions & 0 deletions lib/roast/helpers/function_cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

require "digest"
require "roast/dot_roast"

module Roast
module Helpers
module FunctionCache
autoload :Interceptor, "roast/helpers/function_cache/interceptor"

class << self
def for_workflow(workflow_name, workflow_path)
for_namespace(namespace_from_workflow(workflow_name, workflow_path))
end

def for_namespace(namespace)
cache_dir = Roast::DotRoast.ensure_subdir("cache")
cache_path = namespace.empty? ? cache_dir : File.join(cache_dir, namespace)
ActiveSupport::Cache::FileStore.new(cache_path)
end

def namespace_from_workflow(workflow_name, workflow_path)
sanitized_name = workflow_name.parameterize.underscore
workflow_path_sha = Digest::MD5.hexdigest(workflow_path).first(4)
"#{sanitized_name}_#{workflow_path_sha}"
end
end
end
end
end
90 changes: 90 additions & 0 deletions lib/roast/helpers/function_cache/interceptor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# frozen_string_literal: true

require "roast/helpers/logger"

module Roast
module Helpers
module FunctionCache
# Intercepts function dispatching to add caching capabilities
# This module wraps around Raix::FunctionDispatch to provide caching for tool functions
module Interceptor
def dispatch_tool_function(function_name, params)
start_time = Time.now

ActiveSupport::Notifications.instrument("roast.tool.execute", {
function_name: function_name,
params: params,
})

# Handle workflows with or without configuration
result = if !respond_to?(:workflow_configuration) || workflow_configuration.nil?
super(function_name, params)
else
function_config = if workflow_configuration.respond_to?(:function_config)
workflow_configuration.function_config(function_name)
else
{}
end

# Check if caching is enabled - handle both formats:
# 1. cache: true (boolean format)
# 2. cache: { enabled: true } (hash format)
cache_enabled = if function_config.is_a?(Hash)
cache_config = function_config["cache"]
if cache_config.is_a?(Hash)
cache_config["enabled"]
else
# Direct boolean value
cache_config
end
else
false
end

if cache_enabled
# Call the original function and pass in the cache
cache = Roast::Helpers::FunctionCache.for_workflow(workflow_configuration.name, workflow_configuration.workflow_path)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used configuration in the prev code instead of workflow_configuration which was silently failing to do any function caching, this fixes that.

super(function_name, params, cache:)
else
Roast::Helpers::Logger.debug("⚠️ Caching not enabled for #{function_name}")
super(function_name, params)
end
end

execution_time = Time.now - start_time

# Determine if caching was enabled for metrics
cache_enabled = if defined?(function_config) && function_config.is_a?(Hash)
cache_config = function_config["cache"]
if cache_config.is_a?(Hash)
cache_config["enabled"]
else
# Direct boolean value
cache_config
end
else
false
end

ActiveSupport::Notifications.instrument("roast.tool.complete", {
function_name: function_name,
execution_time: execution_time,
cache_enabled: cache_enabled,
})

result
rescue => e
execution_time = Time.now - start_time

ActiveSupport::Notifications.instrument("roast.tool.error", {
function_name: function_name,
error: e.class.name,
message: e.message,
execution_time: execution_time,
})
raise
end
end
end
end
end
87 changes: 0 additions & 87 deletions lib/roast/helpers/function_caching_interceptor.rb

This file was deleted.

39 changes: 0 additions & 39 deletions lib/roast/initializers.rb

This file was deleted.

10 changes: 0 additions & 10 deletions lib/roast/tools.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,6 @@ module Roast
module Tools
extend self

# Initialize cache and ensure .gitignore exists
cache_dir = File.join(Dir.pwd, ".roast", "cache")
FileUtils.mkdir_p(cache_dir) unless File.directory?(cache_dir)

# Add .gitignore to cache directory
gitignore_path = File.join(cache_dir, ".gitignore")
File.write(gitignore_path, "*") unless File.exist?(gitignore_path)

CACHE = ActiveSupport::Cache::FileStore.new(cache_dir)

def file_to_prompt(file)
<<~PROMPT
# #{file}
Expand Down
26 changes: 26 additions & 0 deletions lib/roast/workflow/initializers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

require "roast/dot_roast"

module Roast
module Workflow
class Initializers
class << self
def load_all
project_initializers = Roast::DotRoast.subdir_path("initializers")
return unless project_initializers

$stderr.puts "Loading project initializers from #{project_initializers}"
pattern = File.join(project_initializers, "**/*.rb")
Dir.glob(pattern, sort: true).each do |file|
$stderr.puts "Loading initializer: #{file}"
require file
end
rescue => e
puts "ERROR: Error loading initializers: #{e.message}"
Roast::Helpers::Logger.error("Error loading initializers: #{e.message}")
end
end
end
end
end
Loading