swarm_memory 2.1.4 → 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.
Files changed (184) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. data/lib/swarm_memory.rb +7 -2
  4. metadata +6 -185
  5. data/lib/claude_swarm/base_executor.rb +0 -133
  6. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  7. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  8. data/lib/claude_swarm/cli.rb +0 -697
  9. data/lib/claude_swarm/commands/ps.rb +0 -215
  10. data/lib/claude_swarm/commands/show.rb +0 -139
  11. data/lib/claude_swarm/configuration.rb +0 -373
  12. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  13. data/lib/claude_swarm/json_handler.rb +0 -91
  14. data/lib/claude_swarm/mcp_generator.rb +0 -243
  15. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  16. data/lib/claude_swarm/openai/executor.rb +0 -256
  17. data/lib/claude_swarm/openai/responses.rb +0 -319
  18. data/lib/claude_swarm/orchestrator.rb +0 -878
  19. data/lib/claude_swarm/process_tracker.rb +0 -78
  20. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  21. data/lib/claude_swarm/session_path.rb +0 -42
  22. data/lib/claude_swarm/settings_generator.rb +0 -77
  23. data/lib/claude_swarm/system_utils.rb +0 -46
  24. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  25. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  27. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  28. data/lib/claude_swarm/version.rb +0 -5
  29. data/lib/claude_swarm/worktree_manager.rb +0 -475
  30. data/lib/claude_swarm/yaml_loader.rb +0 -22
  31. data/lib/claude_swarm.rb +0 -67
  32. data/lib/swarm_cli/cli.rb +0 -201
  33. data/lib/swarm_cli/command_registry.rb +0 -61
  34. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  35. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  36. data/lib/swarm_cli/commands/migrate.rb +0 -55
  37. data/lib/swarm_cli/commands/run.rb +0 -173
  38. data/lib/swarm_cli/config_loader.rb +0 -98
  39. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  40. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  41. data/lib/swarm_cli/interactive_repl.rb +0 -924
  42. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  43. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  44. data/lib/swarm_cli/migrate_options.rb +0 -54
  45. data/lib/swarm_cli/migrator.rb +0 -132
  46. data/lib/swarm_cli/options.rb +0 -151
  47. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  48. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  49. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  50. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  51. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  52. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  53. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  54. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  55. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  56. data/lib/swarm_cli/ui/icons.rb +0 -36
  57. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  58. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  59. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  60. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  61. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  62. data/lib/swarm_cli/version.rb +0 -5
  63. data/lib/swarm_cli.rb +0 -46
  64. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  65. data/lib/swarm_sdk/agent/builder.rb +0 -552
  66. data/lib/swarm_sdk/agent/chat.rb +0 -774
  67. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  68. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  69. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  70. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  71. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  72. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  73. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  75. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  76. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  77. data/lib/swarm_sdk/agent/context.rb +0 -116
  78. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  79. data/lib/swarm_sdk/agent/definition.rb +0 -477
  80. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  81. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  82. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  83. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  84. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  85. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  86. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  87. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  88. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  89. data/lib/swarm_sdk/configuration.rb +0 -135
  90. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  91. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  92. data/lib/swarm_sdk/context_compactor.rb +0 -335
  93. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  94. data/lib/swarm_sdk/context_management/context.rb +0 -328
  95. data/lib/swarm_sdk/defaults.rb +0 -196
  96. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  97. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  98. data/lib/swarm_sdk/hooks/context.rb +0 -197
  99. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  100. data/lib/swarm_sdk/hooks/error.rb +0 -29
  101. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  102. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  103. data/lib/swarm_sdk/hooks/result.rb +0 -150
  104. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  105. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  106. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  107. data/lib/swarm_sdk/log_collector.rb +0 -227
  108. data/lib/swarm_sdk/log_stream.rb +0 -127
  109. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  110. data/lib/swarm_sdk/model_aliases.json +0 -8
  111. data/lib/swarm_sdk/models.json +0 -1
  112. data/lib/swarm_sdk/models.rb +0 -120
  113. data/lib/swarm_sdk/node_context.rb +0 -245
  114. data/lib/swarm_sdk/observer/builder.rb +0 -81
  115. data/lib/swarm_sdk/observer/config.rb +0 -45
  116. data/lib/swarm_sdk/observer/manager.rb +0 -236
  117. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  118. data/lib/swarm_sdk/permissions/config.rb +0 -239
  119. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  120. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  121. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  122. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  123. data/lib/swarm_sdk/plugin.rb +0 -309
  124. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  125. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  126. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  127. data/lib/swarm_sdk/restore_result.rb +0 -65
  128. data/lib/swarm_sdk/result.rb +0 -123
  129. data/lib/swarm_sdk/snapshot.rb +0 -156
  130. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  131. data/lib/swarm_sdk/state_restorer.rb +0 -476
  132. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  133. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  134. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  135. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  136. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  137. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  138. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  139. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  140. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  141. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  142. data/lib/swarm_sdk/swarm.rb +0 -717
  143. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  144. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  145. data/lib/swarm_sdk/tools/bash.rb +0 -282
  146. data/lib/swarm_sdk/tools/clock.rb +0 -44
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  148. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  149. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  150. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  151. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  152. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  153. data/lib/swarm_sdk/tools/edit.rb +0 -145
  154. data/lib/swarm_sdk/tools/glob.rb +0 -166
  155. data/lib/swarm_sdk/tools/grep.rb +0 -235
  156. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  157. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  160. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  161. data/lib/swarm_sdk/tools/read.rb +0 -261
  162. data/lib/swarm_sdk/tools/registry.rb +0 -205
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  166. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  167. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
  168. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  169. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  170. data/lib/swarm_sdk/tools/think.rb +0 -98
  171. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  172. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  173. data/lib/swarm_sdk/tools/write.rb +0 -112
  174. data/lib/swarm_sdk/utils.rb +0 -68
  175. data/lib/swarm_sdk/validation_result.rb +0 -33
  176. data/lib/swarm_sdk/version.rb +0 -5
  177. data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  182. data/lib/swarm_sdk/workflow.rb +0 -554
  183. data/lib/swarm_sdk.rb +0 -524
  184. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -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
@@ -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