Class: RubyLLM::Toolbox::Tools::EditFile

Inherits:
Base
  • Object
show all
Defined in:
lib/ruby_llm/toolbox/tools/edit_file.rb

Overview

EXEC. The core editing primitive: replace an exact substring in a file.

By default old_string must match EXACTLY ONCE — if it’s missing the edit fails, and if it’s ambiguous (appears more than once) the edit also fails rather than guessing. Set replace_all to change every occurrence. This is the same contract coding agents rely on from a str_replace tool: it makes edits deterministic and refuses to silently do the wrong thing.

Constant Summary collapse

MAX_BYTES =
10 * 1024 * 1024

Instance Attribute Summary

Attributes inherited from Base

#config

Instance Method Summary collapse

Methods inherited from Base

#call, exec_tool!, exec_tool?, #initialize, #name

Constructor Details

This class inherits a constructor from RubyLLM::Toolbox::Base

Instance Method Details

#execute(path:, old_string:, new_string:, replace_all: false) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/ruby_llm/toolbox/tools/edit_file.rb', line 39

def execute(path:, old_string:, new_string:, replace_all: false)
  old_s = old_string.to_s
  new_s = new_string.to_s
  return error("old_string must not be empty", code: :empty_match) if old_s.empty?
  return error("old_string and new_string are identical; nothing to do", code: :no_change) if old_s == new_s

  jail = Safety::PathJail.new(config.fs_root)
  real = jail.resolve(path)
  return error("not a file: #{path}", code: :not_a_file) unless File.file?(real)
  return error("file too large (> #{MAX_BYTES} bytes)", code: :too_large) if File.size(real) > MAX_BYTES

  original = File.read(real).scrub
  count = original.scan(old_string_regexp(old_s)).size
  return error("old_string not found in #{path}", code: :not_found) if count.zero?
  if count > 1 && !replace_all
    return error("old_string is ambiguous: #{count} matches in #{path}. " \
                 "Add surrounding context to make it unique, or set replace_all.",
                 code: :ambiguous)
  end

  updated = if replace_all
              original.gsub(old_s) { new_s }
            else
              original.sub(old_s) { new_s }
            end
  File.write(real, updated)

  line = original[0...original.index(old_s)].count("\n") + 1
  "Edited #{path} (#{count} replacement#{count == 1 ? '' : 's'}, " \
    "#{original.bytesize} -> #{updated.bytesize} bytes, first change at line #{line})"
rescue Safety::PathJail::Jailbreak => e
  error(e.message, code: :path_denied)
end