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