swarm_sdk 2.7.13 → 3.0.0.alpha1

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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +43 -22
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +6 -0
  4. data/lib/swarm_sdk/ruby_llm_patches/mcp_ssl_patch.rb +144 -0
  5. data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +3 -4
  6. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  7. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  8. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  9. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  10. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  11. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  12. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  13. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  14. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  15. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  16. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  17. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  18. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  19. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  20. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  24. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  25. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  26. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  27. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  28. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  29. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  30. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  31. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  32. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  33. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  34. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  35. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  36. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  37. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  38. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  39. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  40. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  41. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  42. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  43. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  45. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  46. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  47. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  48. data/lib/swarm_sdk/v3/tools/read.rb +181 -0
  49. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  50. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  51. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  52. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  53. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  54. data/lib/swarm_sdk/v3.rb +145 -0
  55. metadata +84 -148
  56. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  57. data/lib/swarm_sdk/agent/builder.rb +0 -680
  58. data/lib/swarm_sdk/agent/chat.rb +0 -1432
  59. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  60. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  61. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  62. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  63. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  64. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  65. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  66. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  67. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  68. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  69. data/lib/swarm_sdk/agent/context.rb +0 -115
  70. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  71. data/lib/swarm_sdk/agent/definition.rb +0 -581
  72. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  73. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  74. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  75. data/lib/swarm_sdk/agent_registry.rb +0 -146
  76. data/lib/swarm_sdk/builders/base_builder.rb +0 -553
  77. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  78. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  79. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  80. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  81. data/lib/swarm_sdk/config.rb +0 -367
  82. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  83. data/lib/swarm_sdk/configuration/translator.rb +0 -283
  84. data/lib/swarm_sdk/configuration.rb +0 -165
  85. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  86. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  87. data/lib/swarm_sdk/context_compactor.rb +0 -335
  88. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  89. data/lib/swarm_sdk/context_management/context.rb +0 -328
  90. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  91. data/lib/swarm_sdk/defaults.rb +0 -251
  92. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  93. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  94. data/lib/swarm_sdk/hooks/context.rb +0 -197
  95. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  96. data/lib/swarm_sdk/hooks/error.rb +0 -29
  97. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  98. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  99. data/lib/swarm_sdk/hooks/result.rb +0 -150
  100. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  101. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  102. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  103. data/lib/swarm_sdk/log_collector.rb +0 -227
  104. data/lib/swarm_sdk/log_stream.rb +0 -127
  105. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  106. data/lib/swarm_sdk/model_aliases.json +0 -8
  107. data/lib/swarm_sdk/models.json +0 -44002
  108. data/lib/swarm_sdk/models.rb +0 -161
  109. data/lib/swarm_sdk/node_context.rb +0 -245
  110. data/lib/swarm_sdk/observer/builder.rb +0 -81
  111. data/lib/swarm_sdk/observer/config.rb +0 -45
  112. data/lib/swarm_sdk/observer/manager.rb +0 -236
  113. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  114. data/lib/swarm_sdk/permissions/config.rb +0 -239
  115. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  116. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  117. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  118. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  119. data/lib/swarm_sdk/plugin.rb +0 -309
  120. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  121. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  122. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  123. data/lib/swarm_sdk/restore_result.rb +0 -65
  124. data/lib/swarm_sdk/result.rb +0 -212
  125. data/lib/swarm_sdk/snapshot.rb +0 -156
  126. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  127. data/lib/swarm_sdk/state_restorer.rb +0 -476
  128. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  129. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  130. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -195
  131. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  132. data/lib/swarm_sdk/swarm/executor.rb +0 -290
  133. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -151
  134. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  135. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -360
  136. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -270
  137. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  138. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  139. data/lib/swarm_sdk/swarm.rb +0 -843
  140. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  141. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  142. data/lib/swarm_sdk/tools/base.rb +0 -63
  143. data/lib/swarm_sdk/tools/bash.rb +0 -280
  144. data/lib/swarm_sdk/tools/clock.rb +0 -46
  145. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  146. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  147. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  148. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  149. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  150. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  151. data/lib/swarm_sdk/tools/edit.rb +0 -145
  152. data/lib/swarm_sdk/tools/glob.rb +0 -166
  153. data/lib/swarm_sdk/tools/grep.rb +0 -235
  154. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  155. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  156. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  157. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  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 -273
  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 -100
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  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 -95
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  182. data/lib/swarm_sdk/workflow.rb +0 -589
  183. data/lib/swarm_sdk.rb +0 -718
@@ -1,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- module DocumentConverters
6
- # Converts PDF documents to text with image extraction
7
- class PdfConverter < BaseConverter
8
- class << self
9
- def gem_name
10
- "pdf-reader"
11
- end
12
-
13
- def format_name
14
- "PDF"
15
- end
16
-
17
- def extensions
18
- [".pdf"]
19
- end
20
- end
21
-
22
- # Convert a PDF document to text/content
23
- # @param file_path [String] Path to the PDF file
24
- # @return [String, RubyLLM::Content] Converted content or error message
25
- def convert(file_path)
26
- unless self.class.available?
27
- return unsupported_format_reminder(self.class.format_name, self.class.gem_name)
28
- end
29
-
30
- begin
31
- require "pdf-reader"
32
- require "tmpdir"
33
- require "fileutils"
34
-
35
- reader = PDF::Reader.new(file_path)
36
- output = []
37
- output << "PDF Document: #{File.basename(file_path)}"
38
- output << "=" * 60
39
- output << "Pages: #{reader.page_count}"
40
- output << ""
41
-
42
- # Extract images from the PDF
43
- image_paths = ImageExtractors::PdfImageExtractor.extract_images(reader, file_path)
44
-
45
- # Extract text from each page
46
- reader.pages.each_with_index do |page, index|
47
- output << "Page #{index + 1}:"
48
- output << "-" * 60
49
- text = page.text.strip
50
- output << (text.empty? ? "(No text content on this page)" : text)
51
- output << ""
52
- end
53
-
54
- text_content = output.join("\n")
55
-
56
- # If there are images, return Content with attachments
57
- if image_paths.any?
58
- content = RubyLLM::Content.new(text_content)
59
- image_paths.each do |image_path|
60
- content.add_attachment(image_path)
61
- end
62
- content
63
- else
64
- # No images, return just text
65
- text_content
66
- end
67
- rescue PDF::Reader::MalformedPDFError => e
68
- error("PDF file is malformed: #{e.message}")
69
- rescue PDF::Reader::UnsupportedFeatureError => e
70
- error("PDF contains unsupported features: #{e.message}")
71
- rescue StandardError => e
72
- error("Failed to parse PDF file: #{e.message}")
73
- end
74
- end
75
- end
76
- end
77
- end
78
- end
@@ -1,194 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- module DocumentConverters
6
- # Converts XLSX/XLS spreadsheets to text with image extraction
7
- class XlsxConverter < BaseConverter
8
- class << self
9
- def gem_name
10
- "roo"
11
- end
12
-
13
- def format_name
14
- "XLSX/XLS"
15
- end
16
-
17
- def extensions
18
- [".xlsx", ".xls"]
19
- end
20
-
21
- # XLS files require an additional gem
22
- def xls_gem_available?
23
- gem_available?("roo-xls")
24
- end
25
- end
26
-
27
- # Convert a spreadsheet to text/content
28
- # @param file_path [String] Path to the spreadsheet file
29
- # @return [String, RubyLLM::Content] Converted content or error message
30
- def convert(file_path)
31
- unless self.class.available?
32
- return unsupported_format_reminder(self.class.format_name, self.class.gem_name)
33
- end
34
-
35
- # Check for legacy XLS files
36
- extension = File.extname(file_path).downcase
37
- if extension == ".xls" && !self.class.xls_gem_available?
38
- return error("Legacy .xls files require the 'roo-xls' gem. Install with: gem install roo-xls")
39
- end
40
-
41
- spreadsheet = nil
42
-
43
- begin
44
- require "roo"
45
- require "csv"
46
- require "tmpdir"
47
-
48
- spreadsheet = Roo::Spreadsheet.open(file_path)
49
-
50
- # Extract images from all sheets
51
- image_paths = extract_images(spreadsheet, file_path)
52
-
53
- output = []
54
- output << "Spreadsheet: #{File.basename(file_path)}"
55
- output << "Sheets: #{spreadsheet.sheets.size}"
56
- output << "=" * 60
57
- output << ""
58
-
59
- spreadsheet.sheets.each_with_index do |sheet_name, sheet_idx|
60
- sheet = spreadsheet.sheet(sheet_name)
61
-
62
- output << "Sheet #{sheet_idx + 1}: #{sheet_name}"
63
- output << "-" * 60
64
-
65
- # Add sheet dimensions
66
- first_row = sheet.first_row
67
- last_row = sheet.last_row
68
- first_col = sheet.first_column
69
- last_col = sheet.last_column
70
-
71
- if first_row && last_row && first_col && last_col
72
- row_count = last_row - first_row + 1
73
- col_count = last_col - first_col + 1
74
- output << "Dimensions: #{row_count} rows × #{col_count} columns"
75
- output << ""
76
- else
77
- output << "(Empty sheet)"
78
- output << ""
79
- next
80
- end
81
-
82
- # Extract data rows
83
- sheet.each_row_streaming(pad_cells: true) do |row|
84
- row_values = row.map do |cell|
85
- format_cell_value(cell)
86
- end
87
-
88
- # Skip completely empty rows
89
- next if row_values.all? { |v| v.nil? || v.empty? }
90
-
91
- # Format as CSV with proper escaping
92
- output << CSV.generate_line(row_values).chomp
93
- end
94
-
95
- output << ""
96
- end
97
-
98
- text_content = output.join("\n")
99
-
100
- # If there are images, return Content with attachments
101
- if image_paths.any?
102
- content = RubyLLM::Content.new(text_content)
103
- image_paths.each do |image_path|
104
- content.add_attachment(image_path)
105
- end
106
- content
107
- else
108
- # No images, return just text
109
- text_content
110
- end
111
- rescue ArgumentError => e
112
- error("Failed to open spreadsheet: #{e.message}")
113
- rescue RangeError => e
114
- error("Sheet access error: #{e.message}")
115
- rescue Zip::Error => e
116
- error("Corrupted or invalid XLSX file: #{e.message}")
117
- rescue IOError => e
118
- error("File reading error: #{e.message}")
119
- rescue StandardError => e
120
- error("Failed to parse spreadsheet: #{e.message}")
121
- ensure
122
- # Always clean up resources
123
- spreadsheet.close if spreadsheet&.respond_to?(:close)
124
- end
125
- end
126
-
127
- private
128
-
129
- # Format cell value based on type
130
- # @param cell [Roo::Cell] The cell to format
131
- # @return [String] Formatted cell value
132
- def format_cell_value(cell)
133
- return "" if cell.nil? || cell.empty?
134
-
135
- case cell.type
136
- when :string
137
- cell.value.to_s
138
- when :float, :number
139
- cell.value.to_s
140
- when :date
141
- cell.value.strftime("%Y-%m-%d")
142
- when :datetime
143
- cell.value.strftime("%Y-%m-%d %H:%M:%S")
144
- when :time
145
- hours = (cell.value / 3600).to_i
146
- minutes = ((cell.value % 3600) / 60).to_i
147
- seconds = (cell.value % 60).to_i
148
- format("%02d:%02d:%02d", hours, minutes, seconds)
149
- when :boolean
150
- cell.value ? "TRUE" : "FALSE"
151
- when :formula
152
- # Returns calculated value, not the formula itself
153
- cell.value.to_s
154
- when :link
155
- cell.value.to_s
156
- when :percentage
157
- (cell.value * 100).to_s + "%"
158
- else
159
- cell.value.to_s
160
- end
161
- rescue StandardError
162
- "[ERROR]"
163
- end
164
-
165
- # Extract images from spreadsheet
166
- # @param spreadsheet [Roo::Spreadsheet] The spreadsheet
167
- # @param _xlsx_path [String] Path to the XLSX file (unused but kept for consistency)
168
- # @return [Array<String>] Array of temporary file paths containing extracted images
169
- def extract_images(spreadsheet, _xlsx_path)
170
- image_paths = []
171
-
172
- spreadsheet.sheets.each do |sheet_name|
173
- # Check if the spreadsheet supports image extraction
174
- next unless spreadsheet.respond_to?(:images)
175
-
176
- images = spreadsheet.images(sheet_name)
177
- next unless images && !images.empty?
178
-
179
- images.each do |img_path|
180
- if img_path && File.exist?(img_path)
181
- image_paths << img_path
182
- end
183
- end
184
- end
185
-
186
- image_paths
187
- rescue StandardError
188
- # If image extraction fails, don't fail the entire spreadsheet read
189
- []
190
- end
191
- end
192
- end
193
- end
194
- end
@@ -1,145 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # Edit tool for performing exact string replacements in files
6
- #
7
- # Uses exact string matching to find and replace content.
8
- # Requires unique matches and proper Read tool usage beforehand.
9
- # Enforces read-before-edit rule.
10
- class Edit < Base
11
- include PathResolver
12
-
13
- # Factory pattern: declare what parameters this tool needs for instantiation
14
- class << self
15
- def creation_requirements
16
- [:agent_name, :directory]
17
- end
18
- end
19
-
20
- description <<~DESC
21
- Performs exact string replacements in files.
22
- You must use your Read tool at least once in the conversation before editing.
23
- This tool will error if you attempt an edit without reading the file.
24
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix.
25
- The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match.
26
- Never include any part of the line number prefix in the old_string or new_string.
27
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
28
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
29
- The 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.
30
- Use replace_all for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
31
-
32
- IMPORTANT - Path Handling:
33
- - Relative paths (e.g., "tmp/file.txt", "src/main.rb") are resolved relative to your agent's working directory
34
- - Absolute paths (e.g., "/tmp/file.txt", "/etc/passwd") are treated as system absolute paths
35
- - When the user says "tmp/file.txt" they mean the tmp directory in your working directory, NOT /tmp
36
- - Only use absolute paths (starting with /) when explicitly referring to system-level paths
37
- DESC
38
-
39
- param :file_path,
40
- type: "string",
41
- 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.",
42
- required: true
43
-
44
- param :old_string,
45
- type: "string",
46
- desc: "The exact text to replace (must match exactly including whitespace)",
47
- required: true
48
-
49
- param :new_string,
50
- type: "string",
51
- desc: "The text to replace it with (must be different from old_string)",
52
- required: true
53
-
54
- param :replace_all,
55
- type: "boolean",
56
- desc: "Replace all occurrences of old_string (default false)",
57
- required: false
58
-
59
- # Initialize the Edit tool for a specific agent
60
- #
61
- # @param agent_name [Symbol, String] The agent identifier
62
- # @param directory [String] Agent's working directory
63
- def initialize(agent_name:, directory:)
64
- super()
65
- initialize_agent_context(agent_name: agent_name, directory: directory)
66
- end
67
-
68
- # Override name to return simple "Edit" instead of full class path
69
- def name
70
- "Edit"
71
- end
72
-
73
- def execute(file_path:, old_string:, new_string:, replace_all: false)
74
- # Validate inputs
75
- return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
76
- return validation_error("old_string is required") if old_string.nil? || old_string.empty?
77
- return validation_error("new_string is required") if new_string.nil?
78
-
79
- # old_string and new_string must be different
80
- if old_string == new_string
81
- return validation_error("old_string and new_string must be different. They are currently identical.")
82
- end
83
-
84
- # CRITICAL: Resolve path against agent directory
85
- resolved_path = resolve_path(file_path)
86
-
87
- # File must exist (use resolved path)
88
- unless File.exist?(resolved_path)
89
- return validation_error("File does not exist: #{file_path}")
90
- end
91
-
92
- # Enforce read-before-edit (use resolved path)
93
- unless Stores::ReadTracker.file_read?(@agent_name, resolved_path)
94
- return validation_error(
95
- "Cannot edit file without reading it first. " \
96
- "You must use the Read tool on '#{file_path}' before editing it. " \
97
- "This ensures you have the current file contents to match against.",
98
- )
99
- end
100
-
101
- # Read current content (use resolved path)
102
- content = File.read(resolved_path, encoding: "UTF-8")
103
-
104
- # Check if old_string exists in file
105
- unless content.include?(old_string)
106
- return validation_error(<<~ERROR.chomp)
107
- old_string not found in file. Make sure it matches exactly, including all whitespace and indentation.
108
- Do not include line number prefixes from Read tool output.
109
- ERROR
110
- end
111
-
112
- # Count occurrences
113
- occurrences = content.scan(old_string).count
114
-
115
- # If not replace_all and multiple occurrences, error
116
- if !replace_all && occurrences > 1
117
- return validation_error(<<~ERROR.chomp)
118
- Found #{occurrences} occurrences of old_string.
119
- Either provide more surrounding context to make the match unique, or use replace_all: true to replace all occurrences.
120
- ERROR
121
- end
122
-
123
- # Perform replacement
124
- new_content = if replace_all
125
- content.gsub(old_string, new_string)
126
- else
127
- content.sub(old_string, new_string)
128
- end
129
-
130
- # Write back to file (use resolved path)
131
- File.write(resolved_path, new_content, encoding: "UTF-8")
132
-
133
- # Build success message
134
- replaced_count = replace_all ? occurrences : 1
135
- "Successfully replaced #{replaced_count} occurrence(s) in #{file_path}"
136
- rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
137
- error("File contains invalid UTF-8. Cannot edit binary or improperly encoded files.")
138
- rescue Errno::EACCES
139
- error("Permission denied: Cannot read or write file '#{file_path}'")
140
- rescue StandardError => e
141
- error("Unexpected error editing file: #{e.class.name} - #{e.message}")
142
- end
143
- end
144
- end
145
- end
@@ -1,166 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # Glob tool for fast file and directory pattern matching
6
- #
7
- # Finds files and directories matching glob patterns, sorted by modification time.
8
- # Works efficiently with any codebase size.
9
- class Glob < Base
10
- include PathResolver
11
-
12
- # Factory pattern: declare what parameters this tool needs for instantiation
13
- class << self
14
- def creation_requirements
15
- [:directory]
16
- end
17
- end
18
-
19
- def initialize(directory:)
20
- super()
21
- @directory = File.expand_path(directory)
22
- end
23
-
24
- define_method(:name) { "Glob" }
25
-
26
- description <<~DESC
27
- - Fast file pattern matching tool that works with any directory size
28
-
29
- - Supports glob patterns like "**/*.js" or "src/**/*.ts"
30
-
31
- - Returns matching file paths sorted by modification time
32
-
33
- - Use this tool when you need to find files by name patterns
34
-
35
- - When you are doing an open ended search that may require multiple rounds of
36
- globbing and grepping, use the Agent tool instead
37
-
38
- - You have the capability to call multiple tools in a single response. It is
39
- always better to speculatively perform multiple searches as a batch that are
40
- potentially useful.
41
- DESC
42
-
43
- param :pattern,
44
- type: "string",
45
- desc: "The glob pattern to match files against",
46
- required: true
47
-
48
- param :path,
49
- type: "string",
50
- desc: "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.",
51
- required: false
52
-
53
- # NOTE: Result limit now accessed via SwarmSDK.config.glob_result_limit
54
-
55
- def execute(pattern:, path: nil)
56
- # Validate inputs
57
- return validation_error("pattern is required") if pattern.nil? || pattern.to_s.strip.empty?
58
-
59
- # Validate path if provided
60
- if path && !path.to_s.strip.empty?
61
- # Check for literal "undefined" or "null" strings
62
- if path.to_s.strip.downcase == "undefined" || path.to_s.strip.downcase == "null"
63
- return validation_error("Invalid path value. Omit the path parameter entirely to use the current working directory.")
64
- end
65
-
66
- unless File.exist?(path)
67
- return validation_error("Path does not exist: #{path}")
68
- end
69
-
70
- unless File.directory?(path)
71
- return validation_error("Path is not a directory: #{path}")
72
- end
73
-
74
- # CRITICAL: Resolve relative paths against agent directory
75
- search_path = resolve_path(path)
76
- else
77
- # CRITICAL: Use agent's directory as default (NOT Dir.pwd)
78
- search_path = @directory
79
- end
80
-
81
- # Execute glob from specified path
82
- begin
83
- # Build full pattern by joining search path with pattern
84
- # If pattern is already absolute, File.join will use it as-is
85
- full_pattern = if pattern.start_with?("/")
86
- # Pattern is absolute, use it directly
87
- pattern
88
- else
89
- # Pattern is relative, join with search path
90
- File.join(search_path, pattern)
91
- end
92
-
93
- matches = Dir.glob(full_pattern, File::FNM_DOTMATCH)
94
-
95
- # Remove . and .. entries (handle both with and without trailing slashes)
96
- matches.reject! do |f|
97
- basename = File.basename(f.chomp("/"))
98
- basename == "." || basename == ".."
99
- end
100
-
101
- # Handle no matches
102
- if matches.empty?
103
- return "No matches found for pattern: #{pattern}"
104
- end
105
-
106
- # Sort by modification time (most recent first)
107
- matches.sort_by! { |f| -File.mtime(f).to_i }
108
-
109
- # Limit results
110
- max_results = SwarmSDK.config.glob_result_limit
111
- if matches.count > max_results
112
- matches = matches.take(max_results)
113
- truncated = true
114
- else
115
- truncated = false
116
- end
117
-
118
- # Format output
119
- output = matches.join("\n")
120
-
121
- # Add system reminder if truncated
122
- if truncated
123
- output += <<~REMINDER
124
-
125
- <system-reminder>
126
- Results limited to first #{max_results} matches (sorted by most recently modified).
127
- Consider using a more specific pattern to narrow your search.
128
- </system-reminder>
129
- REMINDER
130
- end
131
-
132
- # Add usage reminder
133
- output += "\n\n" + build_usage_reminder(matches.count, pattern)
134
-
135
- output
136
- rescue Errno::EACCES => e
137
- error("Permission denied: #{e.message}")
138
- rescue StandardError => e
139
- error("Failed to execute glob: #{e.class.name} - #{e.message}")
140
- end
141
- rescue StandardError => e
142
- error("Unexpected error during glob: #{e.class.name} - #{e.message}")
143
- end
144
-
145
- private
146
-
147
- def validation_error(message)
148
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
149
- end
150
-
151
- def error(message)
152
- "Error: #{message}"
153
- end
154
-
155
- def build_usage_reminder(count, pattern)
156
- <<~REMINDER
157
- <system-reminder>
158
- Found #{count} match#{"es" if count != 1} for '#{pattern}' (files and directories).
159
- These paths are sorted by modification time (most recent first).
160
- You can now use the Read tool to examine specific files, or use Grep to search within these files.
161
- </system-reminder>
162
- REMINDER
163
- end
164
- end
165
- end
166
- end