Class: Rubino::Tools::MultiEditTool
- Inherits:
-
Base
- Object
- Base
- Rubino::Tools::MultiEditTool
show all
- Defined in:
- lib/rubino/tools/multi_edit_tool.rb
Overview
Applies an ordered list of exact string replacements to a single file in one transactional shot. If any edit fails (string not found, or non-unique without replace_all) the file is left untouched — the LLM gets a single error pointing at the offending edit index.
Each subsequent edit sees the result of prior edits in the same call, so you can rename A→B and then change a line that contains B.
Instance Attribute Summary
Attributes inherited from Base
#cancel_token, #read_tracker, #stream_chunk
Instance Method Summary
collapse
Methods inherited from Base
#cancellation_requested?, #config_key, #emit_chunk, #risky?, #to_tool_definition, workspace_root, workspace_roots
Instance Method Details
#call(arguments) ⇒ Object
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
# File 'lib/rubino/tools/multi_edit_tool.rb', line 53
def call(arguments)
file_path = arguments["file_path"] || arguments[:file_path]
edits = arguments["edits"] || arguments[:edits] || []
return "Error: file_path is required" if file_path.nil? || file_path.to_s.empty?
return "Error: edits must be a non-empty array" if !edits.is_a?(Array) || edits.empty?
expanded = File.expand_path(file_path)
return workspace_violation_message(file_path) unless within_workspace?(expanded)
return "Error: File not found: #{file_path}" unless File.exist?(expanded)
if (gate = read_gate_error(expanded, file_path, verb: "edits"))
return gate
end
content = File.read(expanded)
working = content.dup
applied_count = 0
edits.each_with_index do |edit, idx|
if cancellation_requested?
return "Cancelled before edit ##{idx + 1} — no changes written " \
"(multi_edit is atomic: stages in memory, writes once)"
end
old_s = edit["old_string"] || edit[:old_string]
new_s = edit["new_string"] || edit[:new_string]
replace_all = edit["replace_all"] || edit[:replace_all] || false
return "Error: edit ##{idx + 1} is missing old_string or new_string" if old_s.nil? || new_s.nil?
return "Error: edit ##{idx + 1}: old_string and new_string are identical" if old_s == new_s
unless working.include?(old_s)
return "Error: edit ##{idx + 1}: old_string not found (check whitespace; " \
"remember edits see the result of prior edits)"
end
count = working.scan(old_s).size
if count > 1 && !replace_all
return "Error: edit ##{idx + 1}: #{count} matches for old_string. " \
"Add surrounding context to disambiguate, or set replace_all: true."
end
working = if replace_all
working.gsub(old_s) { new_s }
else
working.sub(old_s) { new_s }
end
applied_count += replace_all ? count : 1
end
File.write(expanded, working)
"Applied #{edits.size} edit(s), #{applied_count} replacement(s) in #{file_path}"
rescue StandardError => e
"Error: #{e.message}"
end
|
#description ⇒ Object
17
18
19
20
21
|
# File 'lib/rubino/tools/multi_edit_tool.rb', line 17
def description
"Apply multiple exact string replacements to a single file atomically. " \
"Edits are applied sequentially in the given order; later edits see " \
"the result of earlier ones. If any edit fails, NO changes are written."
end
|
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
# File 'lib/rubino/tools/multi_edit_tool.rb', line 23
def input_schema
{
type: "object",
properties: {
file_path: {
type: "string",
description: "Path to the file to edit"
},
edits: {
type: "array",
description: "Ordered list of edits to apply",
items: {
type: "object",
properties: {
old_string: { type: "string", description: "Exact text to find" },
new_string: { type: "string", description: "Replacement text" },
replace_all: { type: "boolean", description: "Replace all occurrences (default false)" }
},
required: %w[old_string new_string]
}
}
},
required: %w[file_path edits]
}
end
|
#name ⇒ Object
13
14
15
|
# File 'lib/rubino/tools/multi_edit_tool.rb', line 13
def name
"multi_edit"
end
|
#risk_level ⇒ Object
49
50
51
|
# File 'lib/rubino/tools/multi_edit_tool.rb', line 49
def risk_level
:medium
end
|