swarm_memory 2.1.5 → 2.1.6
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/lib/swarm_memory/version.rb +1 -1
- metadata +5 -184
- data/lib/claude_swarm/base_executor.rb +0 -133
- data/lib/claude_swarm/claude_code_executor.rb +0 -349
- data/lib/claude_swarm/claude_mcp_server.rb +0 -78
- data/lib/claude_swarm/cli.rb +0 -697
- data/lib/claude_swarm/commands/ps.rb +0 -215
- data/lib/claude_swarm/commands/show.rb +0 -139
- data/lib/claude_swarm/configuration.rb +0 -373
- data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
- data/lib/claude_swarm/json_handler.rb +0 -91
- data/lib/claude_swarm/mcp_generator.rb +0 -230
- data/lib/claude_swarm/openai/chat_completion.rb +0 -256
- data/lib/claude_swarm/openai/executor.rb +0 -256
- data/lib/claude_swarm/openai/responses.rb +0 -319
- data/lib/claude_swarm/orchestrator.rb +0 -878
- data/lib/claude_swarm/process_tracker.rb +0 -78
- data/lib/claude_swarm/session_cost_calculator.rb +0 -209
- data/lib/claude_swarm/session_path.rb +0 -42
- data/lib/claude_swarm/settings_generator.rb +0 -77
- data/lib/claude_swarm/system_utils.rb +0 -46
- data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
- data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
- data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
- data/lib/claude_swarm/tools/task_tool.rb +0 -63
- data/lib/claude_swarm/version.rb +0 -5
- data/lib/claude_swarm/worktree_manager.rb +0 -475
- data/lib/claude_swarm/yaml_loader.rb +0 -22
- data/lib/claude_swarm.rb +0 -67
- data/lib/swarm_cli/cli.rb +0 -201
- data/lib/swarm_cli/command_registry.rb +0 -61
- data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
- data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
- data/lib/swarm_cli/commands/migrate.rb +0 -55
- data/lib/swarm_cli/commands/run.rb +0 -173
- data/lib/swarm_cli/config_loader.rb +0 -98
- data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
- data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
- data/lib/swarm_cli/interactive_repl.rb +0 -924
- data/lib/swarm_cli/mcp_serve_options.rb +0 -44
- data/lib/swarm_cli/mcp_tools_options.rb +0 -59
- data/lib/swarm_cli/migrate_options.rb +0 -54
- data/lib/swarm_cli/migrator.rb +0 -132
- data/lib/swarm_cli/options.rb +0 -151
- data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
- data/lib/swarm_cli/ui/components/content_block.rb +0 -120
- data/lib/swarm_cli/ui/components/divider.rb +0 -57
- data/lib/swarm_cli/ui/components/panel.rb +0 -62
- data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
- data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
- data/lib/swarm_cli/ui/formatters/number.rb +0 -58
- data/lib/swarm_cli/ui/formatters/text.rb +0 -77
- data/lib/swarm_cli/ui/formatters/time.rb +0 -73
- data/lib/swarm_cli/ui/icons.rb +0 -36
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
- data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
- data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
- data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
- data/lib/swarm_cli/version.rb +0 -5
- data/lib/swarm_cli.rb +0 -46
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
- data/lib/swarm_sdk/agent/builder.rb +0 -552
- data/lib/swarm_sdk/agent/chat.rb +0 -774
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
- data/lib/swarm_sdk/agent/context.rb +0 -116
- data/lib/swarm_sdk/agent/context_manager.rb +0 -315
- data/lib/swarm_sdk/agent/definition.rb +0 -477
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
- data/lib/swarm_sdk/builders/base_builder.rb +0 -409
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
- data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
- data/lib/swarm_sdk/concerns/validatable.rb +0 -55
- data/lib/swarm_sdk/configuration/parser.rb +0 -353
- data/lib/swarm_sdk/configuration/translator.rb +0 -255
- data/lib/swarm_sdk/configuration.rb +0 -135
- data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
- data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
- data/lib/swarm_sdk/context_compactor.rb +0 -335
- data/lib/swarm_sdk/context_management/builder.rb +0 -128
- data/lib/swarm_sdk/context_management/context.rb +0 -328
- data/lib/swarm_sdk/defaults.rb +0 -196
- data/lib/swarm_sdk/events_to_messages.rb +0 -199
- data/lib/swarm_sdk/hooks/adapter.rb +0 -359
- data/lib/swarm_sdk/hooks/context.rb +0 -197
- data/lib/swarm_sdk/hooks/definition.rb +0 -80
- data/lib/swarm_sdk/hooks/error.rb +0 -29
- data/lib/swarm_sdk/hooks/executor.rb +0 -146
- data/lib/swarm_sdk/hooks/registry.rb +0 -147
- data/lib/swarm_sdk/hooks/result.rb +0 -150
- data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
- data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
- data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
- data/lib/swarm_sdk/log_collector.rb +0 -227
- data/lib/swarm_sdk/log_stream.rb +0 -127
- data/lib/swarm_sdk/markdown_parser.rb +0 -75
- data/lib/swarm_sdk/model_aliases.json +0 -8
- data/lib/swarm_sdk/models.json +0 -1
- data/lib/swarm_sdk/models.rb +0 -120
- data/lib/swarm_sdk/node_context.rb +0 -245
- data/lib/swarm_sdk/observer/builder.rb +0 -81
- data/lib/swarm_sdk/observer/config.rb +0 -45
- data/lib/swarm_sdk/observer/manager.rb +0 -236
- data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
- data/lib/swarm_sdk/permissions/config.rb +0 -239
- data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
- data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
- data/lib/swarm_sdk/permissions/validator.rb +0 -173
- data/lib/swarm_sdk/permissions_builder.rb +0 -122
- data/lib/swarm_sdk/plugin.rb +0 -309
- data/lib/swarm_sdk/plugin_registry.rb +0 -101
- data/lib/swarm_sdk/proc_helpers.rb +0 -53
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -123
- data/lib/swarm_sdk/snapshot.rb +0 -156
- data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
- data/lib/swarm_sdk/state_restorer.rb +0 -476
- data/lib/swarm_sdk/state_snapshot.rb +0 -334
- data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
- data/lib/swarm_sdk/swarm/builder.rb +0 -249
- data/lib/swarm_sdk/swarm/executor.rb +0 -213
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
- data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
- data/lib/swarm_sdk/swarm.rb +0 -717
- data/lib/swarm_sdk/swarm_loader.rb +0 -145
- data/lib/swarm_sdk/swarm_registry.rb +0 -136
- data/lib/swarm_sdk/tools/bash.rb +0 -282
- data/lib/swarm_sdk/tools/clock.rb +0 -44
- data/lib/swarm_sdk/tools/delegate.rb +0 -267
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
- data/lib/swarm_sdk/tools/edit.rb +0 -145
- data/lib/swarm_sdk/tools/glob.rb +0 -166
- data/lib/swarm_sdk/tools/grep.rb +0 -235
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
- data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
- data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
- data/lib/swarm_sdk/tools/read.rb +0 -261
- data/lib/swarm_sdk/tools/registry.rb +0 -205
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
- data/lib/swarm_sdk/tools/think.rb +0 -98
- data/lib/swarm_sdk/tools/todo_write.rb +0 -235
- data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
- data/lib/swarm_sdk/tools/write.rb +0 -112
- data/lib/swarm_sdk/utils.rb +0 -68
- data/lib/swarm_sdk/validation_result.rb +0 -33
- data/lib/swarm_sdk/version.rb +0 -5
- data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
- data/lib/swarm_sdk/workflow/builder.rb +0 -143
- data/lib/swarm_sdk/workflow/executor.rb +0 -497
- data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
- data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
- data/lib/swarm_sdk/workflow.rb +0 -554
- data/lib/swarm_sdk.rb +0 -524
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
# MultiEdit tool for performing multiple exact string replacements in a file
|
|
6
|
-
#
|
|
7
|
-
# Applies multiple edit operations sequentially to a single file.
|
|
8
|
-
# Each edit sees the result of all previous edits, allowing for
|
|
9
|
-
# coordinated multi-step transformations.
|
|
10
|
-
# Enforces read-before-edit rule.
|
|
11
|
-
class MultiEdit < RubyLLM::Tool
|
|
12
|
-
include PathResolver
|
|
13
|
-
|
|
14
|
-
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
15
|
-
class << self
|
|
16
|
-
def creation_requirements
|
|
17
|
-
[:agent_name, :directory]
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
description <<~DESC
|
|
22
|
-
Performs multiple exact string replacements in a single file.
|
|
23
|
-
Edits are applied sequentially, so later edits see the results of earlier ones.
|
|
24
|
-
You must use your Read tool at least once in the conversation before editing.
|
|
25
|
-
This tool will error if you attempt an edit without reading the file.
|
|
26
|
-
When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix.
|
|
27
|
-
The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match.
|
|
28
|
-
Never include any part of the line number prefix in the old_string or new_string.
|
|
29
|
-
ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
30
|
-
Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
|
|
31
|
-
Each edit will FAIL if old_string is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use replace_all to change every instance of old_string.
|
|
32
|
-
Use replace_all for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
|
|
33
|
-
|
|
34
|
-
IMPORTANT - Path Handling:
|
|
35
|
-
- Relative paths (e.g., "tmp/file.txt", "src/main.rb") are resolved relative to your agent's working directory
|
|
36
|
-
- Absolute paths (e.g., "/tmp/file.txt", "/etc/passwd") are treated as system absolute paths
|
|
37
|
-
- When the user says "tmp/file.txt" they mean the tmp directory in your working directory, NOT /tmp
|
|
38
|
-
- Only use absolute paths (starting with /) when explicitly referring to system-level paths
|
|
39
|
-
DESC
|
|
40
|
-
|
|
41
|
-
param :file_path,
|
|
42
|
-
type: "string",
|
|
43
|
-
desc: "Path to the file. Use relative paths (e.g., 'tmp/file.txt') for files in your working directory, or absolute paths (e.g., '/etc/passwd') for system files.",
|
|
44
|
-
required: true
|
|
45
|
-
|
|
46
|
-
param :edits_json,
|
|
47
|
-
type: "string",
|
|
48
|
-
desc: <<~DESC.chomp,
|
|
49
|
-
JSON array of edit operations. Each edit must have:
|
|
50
|
-
old_string (exact text to replace),
|
|
51
|
-
new_string (replacement text),
|
|
52
|
-
and optionally replace_all (boolean, default false).
|
|
53
|
-
Example: [{"old_string":"foo","new_string":"bar","replace_all":false}]
|
|
54
|
-
DESC
|
|
55
|
-
required: true
|
|
56
|
-
|
|
57
|
-
# Initialize the MultiEdit tool for a specific agent
|
|
58
|
-
#
|
|
59
|
-
# @param agent_name [Symbol, String] The agent identifier
|
|
60
|
-
# @param directory [String] Agent's working directory
|
|
61
|
-
def initialize(agent_name:, directory:)
|
|
62
|
-
super()
|
|
63
|
-
initialize_agent_context(agent_name: agent_name, directory: directory)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Override name to return simple "MultiEdit" instead of full class path
|
|
67
|
-
def name
|
|
68
|
-
"MultiEdit"
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def execute(file_path:, edits_json:)
|
|
72
|
-
# Validate inputs
|
|
73
|
-
return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
|
|
74
|
-
|
|
75
|
-
# CRITICAL: Resolve path against agent directory
|
|
76
|
-
resolved_path = resolve_path(file_path)
|
|
77
|
-
|
|
78
|
-
# Parse JSON
|
|
79
|
-
edits = begin
|
|
80
|
-
JSON.parse(edits_json)
|
|
81
|
-
rescue JSON::ParserError
|
|
82
|
-
nil
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
return validation_error("Invalid JSON format. Please provide a valid JSON array of edit operations.") if edits.nil?
|
|
86
|
-
|
|
87
|
-
return validation_error("edits must be an array") unless edits.is_a?(Array)
|
|
88
|
-
return validation_error("edits array cannot be empty") if edits.empty?
|
|
89
|
-
|
|
90
|
-
# File must exist (use resolved path)
|
|
91
|
-
unless File.exist?(resolved_path)
|
|
92
|
-
return validation_error("File does not exist: #{file_path}")
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Enforce read-before-edit (use resolved path)
|
|
96
|
-
unless Stores::ReadTracker.file_read?(@agent_name, resolved_path)
|
|
97
|
-
return validation_error(
|
|
98
|
-
"Cannot edit file without reading it first. " \
|
|
99
|
-
"You must use the Read tool on '#{file_path}' before editing it. " \
|
|
100
|
-
"This ensures you have the current file contents to match against.",
|
|
101
|
-
)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Read current content (use resolved path)
|
|
105
|
-
content = File.read(resolved_path, encoding: "UTF-8")
|
|
106
|
-
|
|
107
|
-
# Validate edit operations
|
|
108
|
-
validated_edits = []
|
|
109
|
-
edits.each_with_index do |edit, index|
|
|
110
|
-
unless edit.is_a?(Hash)
|
|
111
|
-
return validation_error("Edit at index #{index} must be a hash/object with old_string and new_string")
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Convert string keys to symbols for consistency
|
|
115
|
-
edit = edit.transform_keys(&:to_sym)
|
|
116
|
-
|
|
117
|
-
unless edit[:old_string]
|
|
118
|
-
return validation_error("Edit at index #{index} missing required field 'old_string'")
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
unless edit[:new_string]
|
|
122
|
-
return validation_error("Edit at index #{index} missing required field 'new_string'")
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# old_string and new_string must be different
|
|
126
|
-
if edit[:old_string] == edit[:new_string]
|
|
127
|
-
return validation_error("Edit at index #{index}: old_string and new_string must be different")
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
validated_edits << {
|
|
131
|
-
old_string: edit[:old_string].to_s,
|
|
132
|
-
new_string: edit[:new_string].to_s,
|
|
133
|
-
replace_all: edit[:replace_all] == true,
|
|
134
|
-
index: index,
|
|
135
|
-
}
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Apply edits sequentially
|
|
139
|
-
results = []
|
|
140
|
-
current_content = content
|
|
141
|
-
|
|
142
|
-
validated_edits.each do |edit|
|
|
143
|
-
# Check if old_string exists in current content
|
|
144
|
-
unless current_content.include?(edit[:old_string])
|
|
145
|
-
return error_with_results(
|
|
146
|
-
<<~ERROR.chomp,
|
|
147
|
-
Edit #{edit[:index]}: old_string not found in file.
|
|
148
|
-
Make sure it matches exactly, including all whitespace and indentation.
|
|
149
|
-
Do not include line number prefixes from Read tool output.
|
|
150
|
-
Note: This edit follows #{edit[:index]} previous edit(s) which may have changed the file content.
|
|
151
|
-
ERROR
|
|
152
|
-
results,
|
|
153
|
-
)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Count occurrences
|
|
157
|
-
occurrences = current_content.scan(edit[:old_string]).count
|
|
158
|
-
|
|
159
|
-
# If not replace_all and multiple occurrences, error
|
|
160
|
-
if !edit[:replace_all] && occurrences > 1
|
|
161
|
-
return error_with_results(
|
|
162
|
-
<<~ERROR.chomp,
|
|
163
|
-
Edit #{edit[:index]}: Found #{occurrences} occurrences of old_string.
|
|
164
|
-
Either provide more surrounding context to make the match unique, or set replace_all: true to replace all occurrences.
|
|
165
|
-
ERROR
|
|
166
|
-
results,
|
|
167
|
-
)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Perform replacement
|
|
171
|
-
new_content = if edit[:replace_all]
|
|
172
|
-
current_content.gsub(edit[:old_string], edit[:new_string])
|
|
173
|
-
else
|
|
174
|
-
current_content.sub(edit[:old_string], edit[:new_string])
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
# Record result
|
|
178
|
-
replaced_count = edit[:replace_all] ? occurrences : 1
|
|
179
|
-
results << {
|
|
180
|
-
index: edit[:index],
|
|
181
|
-
status: "success",
|
|
182
|
-
occurrences: replaced_count,
|
|
183
|
-
message: "Replaced #{replaced_count} occurrence(s)",
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
# Update content for next edit
|
|
187
|
-
current_content = new_content
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Write back to file (use resolved path)
|
|
191
|
-
File.write(resolved_path, current_content, encoding: "UTF-8")
|
|
192
|
-
|
|
193
|
-
# Build success message
|
|
194
|
-
total_replacements = results.sum { |r| r[:occurrences] }
|
|
195
|
-
message = "Successfully applied #{validated_edits.size} edit(s) to #{file_path}\n"
|
|
196
|
-
message += "Total replacements: #{total_replacements}\n\n"
|
|
197
|
-
message += "Details:\n"
|
|
198
|
-
results.each do |result|
|
|
199
|
-
message += " Edit #{result[:index]}: #{result[:message]}\n"
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
message
|
|
203
|
-
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
|
204
|
-
error("File contains invalid UTF-8. Cannot edit binary or improperly encoded files.")
|
|
205
|
-
rescue Errno::EACCES
|
|
206
|
-
error("Permission denied: Cannot read or write file '#{file_path}'")
|
|
207
|
-
rescue StandardError => e
|
|
208
|
-
error("Unexpected error editing file: #{e.class.name} - #{e.message}")
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
private
|
|
212
|
-
|
|
213
|
-
# Format an error that includes partial results
|
|
214
|
-
#
|
|
215
|
-
# Shows what edits succeeded before the error occurred.
|
|
216
|
-
#
|
|
217
|
-
# @param message [String] Error description
|
|
218
|
-
# @param results [Array<Hash>] Successful edit results before failure
|
|
219
|
-
# @return [String] Formatted error message with results summary
|
|
220
|
-
def error_with_results(message, results)
|
|
221
|
-
output = "<tool_use_error>InputValidationError: #{message}\n\n"
|
|
222
|
-
|
|
223
|
-
if results.any?
|
|
224
|
-
output += "Previous successful edits before error:\n"
|
|
225
|
-
results.each do |result|
|
|
226
|
-
output += " Edit #{result[:index]}: #{result[:message]}\n"
|
|
227
|
-
end
|
|
228
|
-
output += "\n"
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
output += "Note: The file has NOT been modified. All or nothing approach - if any edit fails, no changes are saved.</tool_use_error>"
|
|
232
|
-
output
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
end
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
# Shared path resolution and agent context logic for file tools
|
|
6
|
-
#
|
|
7
|
-
# This module provides:
|
|
8
|
-
# - Path resolution (relative to agent's working directory)
|
|
9
|
-
# - Agent context initialization (agent_name, directory expansion)
|
|
10
|
-
# - Standard error message formatting
|
|
11
|
-
#
|
|
12
|
-
# Tools resolve relative paths against the agent's directory.
|
|
13
|
-
# Absolute paths are used as-is.
|
|
14
|
-
#
|
|
15
|
-
# @example
|
|
16
|
-
# class Read < RubyLLM::Tool
|
|
17
|
-
# include PathResolver
|
|
18
|
-
#
|
|
19
|
-
# def initialize(agent_name:, directory:)
|
|
20
|
-
# super()
|
|
21
|
-
# initialize_agent_context(agent_name: agent_name, directory: directory)
|
|
22
|
-
# end
|
|
23
|
-
#
|
|
24
|
-
# def execute(file_path:)
|
|
25
|
-
# resolved_path = resolve_path(file_path)
|
|
26
|
-
# File.read(resolved_path)
|
|
27
|
-
# rescue StandardError => e
|
|
28
|
-
# error("Failed to read: #{e.message}")
|
|
29
|
-
# end
|
|
30
|
-
# end
|
|
31
|
-
module PathResolver
|
|
32
|
-
# Agent context attributes
|
|
33
|
-
# @return [Symbol] The agent identifier
|
|
34
|
-
attr_reader :agent_name
|
|
35
|
-
|
|
36
|
-
# @return [String] Absolute path to agent's working directory
|
|
37
|
-
attr_reader :directory
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
|
|
41
|
-
# Initialize agent context for file tools
|
|
42
|
-
#
|
|
43
|
-
# Sets up the common agent context needed by file tools:
|
|
44
|
-
# - Normalizes agent_name to symbol
|
|
45
|
-
# - Expands directory to absolute path
|
|
46
|
-
#
|
|
47
|
-
# @param agent_name [Symbol, String] The agent identifier
|
|
48
|
-
# @param directory [String] Agent's working directory (will be expanded)
|
|
49
|
-
# @return [void]
|
|
50
|
-
def initialize_agent_context(agent_name:, directory:)
|
|
51
|
-
@agent_name = agent_name.to_sym
|
|
52
|
-
@directory = File.expand_path(directory)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Resolve a path relative to the agent's directory
|
|
56
|
-
#
|
|
57
|
-
# - Absolute paths (starting with /) are returned as-is
|
|
58
|
-
# - Relative paths are resolved against @directory
|
|
59
|
-
#
|
|
60
|
-
# @param path [String] Path to resolve (relative or absolute)
|
|
61
|
-
# @return [String] Absolute path
|
|
62
|
-
# @raise [RuntimeError] If @directory not set (developer error)
|
|
63
|
-
def resolve_path(path)
|
|
64
|
-
raise "PathResolver requires @directory to be set" unless @directory
|
|
65
|
-
|
|
66
|
-
return path if path.to_s.start_with?("/")
|
|
67
|
-
|
|
68
|
-
File.expand_path(path, @directory)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Format a validation error response
|
|
72
|
-
#
|
|
73
|
-
# Used for input validation failures (missing required params, invalid formats, etc.)
|
|
74
|
-
#
|
|
75
|
-
# @param message [String] Error description
|
|
76
|
-
# @return [String] Formatted error message wrapped in tool_use_error tags
|
|
77
|
-
def validation_error(message)
|
|
78
|
-
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Format a general error response
|
|
82
|
-
#
|
|
83
|
-
# Used for runtime errors (permission denied, file not found, etc.)
|
|
84
|
-
#
|
|
85
|
-
# @param message [String] Error description
|
|
86
|
-
# @return [String] Formatted error message prefixed with "Error:"
|
|
87
|
-
def error(message)
|
|
88
|
-
"Error: #{message}"
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
data/lib/swarm_sdk/tools/read.rb
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
# Read tool for reading file contents from the filesystem
|
|
6
|
-
#
|
|
7
|
-
# Supports reading entire files or specific line ranges with line numbers.
|
|
8
|
-
# Provides system reminders to guide proper usage.
|
|
9
|
-
# Tracks reads per agent for enforcing read-before-write/edit rules.
|
|
10
|
-
class Read < RubyLLM::Tool
|
|
11
|
-
include PathResolver
|
|
12
|
-
|
|
13
|
-
# Backward compatibility aliases - use Defaults module for new code
|
|
14
|
-
MAX_LINE_LENGTH = Defaults::Limits::LINE_CHARACTERS
|
|
15
|
-
DEFAULT_LIMIT = Defaults::Limits::READ_LINES
|
|
16
|
-
|
|
17
|
-
# List of available document converters
|
|
18
|
-
CONVERTERS = [
|
|
19
|
-
DocumentConverters::PdfConverter,
|
|
20
|
-
DocumentConverters::DocxConverter,
|
|
21
|
-
DocumentConverters::XlsxConverter,
|
|
22
|
-
].freeze
|
|
23
|
-
|
|
24
|
-
# Build dynamic description based on available gems
|
|
25
|
-
available_formats = CONVERTERS.select(&:available?).map(&:format_name)
|
|
26
|
-
doc_support_text = if available_formats.any?
|
|
27
|
-
"- Document files: #{available_formats.join(", ")} are converted to text"
|
|
28
|
-
else
|
|
29
|
-
""
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
33
|
-
class << self
|
|
34
|
-
def creation_requirements
|
|
35
|
-
[:agent_name, :directory]
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
description <<~DESC
|
|
40
|
-
Reads a file from the local filesystem. You can access any file directly by using this tool.
|
|
41
|
-
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid.
|
|
42
|
-
It is okay to read a file that does not exist; an error will be returned.
|
|
43
|
-
|
|
44
|
-
Supports text, binary, and document files:
|
|
45
|
-
- Text files are returned with line numbers
|
|
46
|
-
- Binary files (images) are returned as visual content for analysis
|
|
47
|
-
- Supported image formats: PNG, JPG, GIF, WEBP, BMP, TIFF, SVG, ICO
|
|
48
|
-
#{doc_support_text}
|
|
49
|
-
|
|
50
|
-
IMPORTANT - Path Handling:
|
|
51
|
-
- Relative paths (e.g., "tmp/file.txt", "src/main.rb") are resolved relative to your agent's working directory
|
|
52
|
-
- Absolute paths (e.g., "/tmp/file.txt", "/etc/passwd") are treated as system absolute paths
|
|
53
|
-
- When the user says "tmp/file.txt" they mean the tmp directory in your working directory, NOT /tmp
|
|
54
|
-
- Only use absolute paths (starting with /) when explicitly referring to system-level paths
|
|
55
|
-
DESC
|
|
56
|
-
|
|
57
|
-
param :file_path,
|
|
58
|
-
type: "string",
|
|
59
|
-
desc: "Path to the file. Use relative paths (e.g., 'tmp/file.txt') for files in your working directory, or absolute paths (e.g., '/etc/passwd') for system files.",
|
|
60
|
-
required: true
|
|
61
|
-
|
|
62
|
-
param :offset,
|
|
63
|
-
type: "integer",
|
|
64
|
-
desc: "The line number to start reading from (1-indexed). Only provide if the file is too large to read at once.",
|
|
65
|
-
required: false
|
|
66
|
-
|
|
67
|
-
param :limit,
|
|
68
|
-
type: "integer",
|
|
69
|
-
desc: "The number of lines to read. Only provide if the file is too large to read at once.",
|
|
70
|
-
required: false
|
|
71
|
-
|
|
72
|
-
# Initialize the Read tool for a specific agent
|
|
73
|
-
#
|
|
74
|
-
# @param agent_name [Symbol, String] The agent identifier
|
|
75
|
-
# @param directory [String] Agent's working directory
|
|
76
|
-
def initialize(agent_name:, directory:)
|
|
77
|
-
super()
|
|
78
|
-
initialize_agent_context(agent_name: agent_name, directory: directory)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Override name to return simple "Read" instead of full class path
|
|
82
|
-
def name
|
|
83
|
-
"Read"
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def execute(file_path:, offset: nil, limit: nil)
|
|
87
|
-
# Validate file path
|
|
88
|
-
return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
|
|
89
|
-
|
|
90
|
-
# CRITICAL: Resolve path against agent directory
|
|
91
|
-
resolved_path = resolve_path(file_path)
|
|
92
|
-
|
|
93
|
-
unless File.exist?(resolved_path)
|
|
94
|
-
return validation_error("File does not exist: #{file_path}")
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Check if it's a directory
|
|
98
|
-
if File.directory?(resolved_path)
|
|
99
|
-
return validation_error("Path is a directory, not a file. Use Bash with ls to read directories.")
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Check if it's a document and try to convert it
|
|
103
|
-
converter = find_converter_for_file(resolved_path)
|
|
104
|
-
if converter
|
|
105
|
-
result = converter.new.convert(resolved_path)
|
|
106
|
-
# For document files, register the converted text content
|
|
107
|
-
# Extract text from result (may be wrapped in system-reminder tags)
|
|
108
|
-
if result.is_a?(String)
|
|
109
|
-
# Remove system-reminder wrapper if present to get clean text for digest
|
|
110
|
-
text_content = result.gsub(%r{<system-reminder>.*?</system-reminder>}m, "").strip
|
|
111
|
-
Stores::ReadTracker.register_read(@agent_name, resolved_path, text_content)
|
|
112
|
-
end
|
|
113
|
-
return result
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Try to read as text, handle binary files separately
|
|
117
|
-
content = read_file_content(resolved_path)
|
|
118
|
-
|
|
119
|
-
# If content is a Content object (binary file), track with binary digest and return
|
|
120
|
-
if content.is_a?(RubyLLM::Content)
|
|
121
|
-
# For binary files, read raw bytes for digest
|
|
122
|
-
binary_content = File.binread(resolved_path)
|
|
123
|
-
Stores::ReadTracker.register_read(@agent_name, resolved_path, binary_content)
|
|
124
|
-
return content
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Return early if we got an error message or system reminder
|
|
128
|
-
return content if content.is_a?(String) && (content.start_with?("Error:") || content.start_with?("<system-reminder>"))
|
|
129
|
-
|
|
130
|
-
# At this point, we have valid text content - register the read with digest
|
|
131
|
-
Stores::ReadTracker.register_read(@agent_name, resolved_path, content)
|
|
132
|
-
|
|
133
|
-
# Check if file is empty
|
|
134
|
-
if content.empty?
|
|
135
|
-
return format_with_reminder(
|
|
136
|
-
"",
|
|
137
|
-
"<system-reminder>Warning: This file exists but has empty contents. This may be intentional or indicate an issue.</system-reminder>",
|
|
138
|
-
)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Split into lines and apply offset/limit
|
|
142
|
-
lines = content.lines
|
|
143
|
-
total_lines = lines.count
|
|
144
|
-
|
|
145
|
-
# Apply offset if specified (1-indexed)
|
|
146
|
-
start_line = offset ? offset - 1 : 0
|
|
147
|
-
start_line = [start_line, 0].max # Ensure non-negative
|
|
148
|
-
|
|
149
|
-
if start_line >= total_lines
|
|
150
|
-
return validation_error("Offset #{offset} exceeds file length (#{total_lines} lines)")
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
lines = lines.drop(start_line)
|
|
154
|
-
|
|
155
|
-
# Apply limit if specified, otherwise use default
|
|
156
|
-
effective_limit = limit || DEFAULT_LIMIT
|
|
157
|
-
lines = lines.take(effective_limit)
|
|
158
|
-
truncated = limit.nil? && total_lines > DEFAULT_LIMIT
|
|
159
|
-
|
|
160
|
-
# Format with line numbers (cat -n style)
|
|
161
|
-
output_lines = lines.each_with_index.map do |line, idx|
|
|
162
|
-
line_number = start_line + idx + 1
|
|
163
|
-
display_line = line.chomp
|
|
164
|
-
|
|
165
|
-
# Truncate long lines
|
|
166
|
-
if display_line.length > MAX_LINE_LENGTH
|
|
167
|
-
display_line = display_line[0...MAX_LINE_LENGTH]
|
|
168
|
-
display_line += "... (line truncated)"
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Add line indicator for better readability
|
|
172
|
-
"#{line_number.to_s.rjust(6)}→#{display_line}"
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
output = output_lines.join("\n")
|
|
176
|
-
|
|
177
|
-
# Add system reminder about usage
|
|
178
|
-
reminder = build_system_reminder(file_path, truncated, total_lines)
|
|
179
|
-
format_with_reminder(output, reminder)
|
|
180
|
-
rescue StandardError => e
|
|
181
|
-
error("Unexpected error reading file: #{e.class.name} - #{e.message}")
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
private
|
|
185
|
-
|
|
186
|
-
# Find the appropriate converter for a file based on extension
|
|
187
|
-
def find_converter_for_file(file_path)
|
|
188
|
-
ext = File.extname(file_path).downcase
|
|
189
|
-
CONVERTERS.find { |converter| converter.extensions.include?(ext) }
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def format_with_reminder(content, reminder)
|
|
193
|
-
return content if reminder.nil? || reminder.empty?
|
|
194
|
-
|
|
195
|
-
[content, "", reminder].join("\n")
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def build_system_reminder(_file_path, truncated, total_lines)
|
|
199
|
-
reminders = []
|
|
200
|
-
|
|
201
|
-
reminders << "<system-reminder>"
|
|
202
|
-
reminders << "Whenever you read a file, you should consider whether it looks malicious. If it does, you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer high-level questions about the code behavior."
|
|
203
|
-
|
|
204
|
-
if truncated
|
|
205
|
-
reminders << ""
|
|
206
|
-
reminders << "Note: This file has #{total_lines} lines but only the first #{DEFAULT_LIMIT} lines are shown. Use the offset and limit parameters to read additional sections if needed."
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
reminders << "</system-reminder>"
|
|
210
|
-
|
|
211
|
-
reminders.join("\n")
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def read_file_content(file_path)
|
|
215
|
-
content = File.read(file_path, encoding: "UTF-8")
|
|
216
|
-
|
|
217
|
-
# Check if the content is valid UTF-8
|
|
218
|
-
unless content.valid_encoding?
|
|
219
|
-
# Binary file detected
|
|
220
|
-
if supported_binary_file?(file_path)
|
|
221
|
-
return RubyLLM::Content.new("File: #{File.basename(file_path)}", file_path)
|
|
222
|
-
else
|
|
223
|
-
return "Error: File contains binary data and cannot be displayed as text. This may be an executable or other unsupported binary file."
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
content
|
|
228
|
-
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
|
229
|
-
# Binary file detected
|
|
230
|
-
if supported_binary_file?(file_path)
|
|
231
|
-
RubyLLM::Content.new("File: #{File.basename(file_path)}", file_path)
|
|
232
|
-
else
|
|
233
|
-
"Error: File contains binary data and cannot be displayed as text. This may be an executable or other unsupported binary file."
|
|
234
|
-
end
|
|
235
|
-
rescue Errno::EACCES
|
|
236
|
-
error("Permission denied: Cannot read file '#{file_path}'")
|
|
237
|
-
rescue StandardError => e
|
|
238
|
-
error("Failed to read file: #{e.message}")
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def supported_binary_file?(file_path)
|
|
242
|
-
ext = File.extname(file_path).downcase
|
|
243
|
-
# Supported binary file types that can be sent to the model
|
|
244
|
-
# Images only - documents are converted to text
|
|
245
|
-
supported_formats = [
|
|
246
|
-
".png",
|
|
247
|
-
".jpg",
|
|
248
|
-
".jpeg",
|
|
249
|
-
".gif",
|
|
250
|
-
".webp",
|
|
251
|
-
".bmp",
|
|
252
|
-
".tiff",
|
|
253
|
-
".tif",
|
|
254
|
-
".svg",
|
|
255
|
-
".ico",
|
|
256
|
-
]
|
|
257
|
-
supported_formats.include?(ext)
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
end
|