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
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ # Glob tool for fast file pattern matching
7
+ #
8
+ # Finds files and directories matching glob patterns, sorted by modification time.
9
+ class Glob < Base
10
+ class << self
11
+ # @return [Array<Symbol>] Constructor requirements
12
+ def creation_requirements
13
+ [:directory]
14
+ end
15
+ end
16
+
17
+ description <<~DESC
18
+ Fast file pattern matching tool.
19
+
20
+ Supports glob patterns like "**/*.js" or "src/**/*.ts".
21
+ Returns matching file paths sorted by modification time (most recent first).
22
+ DESC
23
+
24
+ param :pattern,
25
+ type: "string",
26
+ desc: "The glob pattern to match files against",
27
+ required: true
28
+
29
+ param :path,
30
+ type: "string",
31
+ desc: "Directory to search in. Defaults to working directory.",
32
+ required: false
33
+
34
+ # @param directory [String] Working directory for pattern matching
35
+ def initialize(directory:)
36
+ super()
37
+ @directory = File.expand_path(directory)
38
+ end
39
+
40
+ # Execute glob search
41
+ #
42
+ # @param pattern [String] Glob pattern
43
+ # @param path [String, nil] Search directory
44
+ # @return [String] Matching paths or error
45
+ def execute(pattern:, path: nil)
46
+ return validation_error("pattern is required") if pattern.nil? || pattern.to_s.strip.empty?
47
+
48
+ search_path = resolve_search_path(path)
49
+ return search_path if search_path.start_with?("<tool_use_error>")
50
+
51
+ full_pattern = pattern.start_with?("/") ? pattern : File.join(search_path, pattern)
52
+ matches = Dir.glob(full_pattern, File::FNM_DOTMATCH)
53
+
54
+ matches.reject! do |f|
55
+ basename = File.basename(f.chomp("/"))
56
+ basename == "." || basename == ".."
57
+ end
58
+
59
+ return "No matches found for pattern: #{pattern}" if matches.empty?
60
+
61
+ matches.sort_by! { |f| -File.mtime(f).to_i }
62
+
63
+ max_results = Configuration.instance.glob_result_limit
64
+ truncated = matches.count > max_results
65
+ matches = matches.take(max_results) if truncated
66
+
67
+ output = matches.join("\n")
68
+ output += "\n\n<system-reminder>Results limited to first #{max_results} matches.</system-reminder>" if truncated
69
+ output
70
+ rescue Errno::EACCES => e
71
+ error("Permission denied: #{e.message}")
72
+ rescue StandardError => e
73
+ error("Failed to execute glob: #{e.class.name} - #{e.message}")
74
+ end
75
+
76
+ private
77
+
78
+ # Resolve search path with validation
79
+ #
80
+ # @param path [String, nil] User-provided path
81
+ # @return [String] Resolved absolute path or validation error
82
+ def resolve_search_path(path)
83
+ if path && !path.to_s.strip.empty?
84
+ return validation_error("Invalid path value.") if ["undefined", "null"].include?(path.to_s.strip.downcase)
85
+ return validation_error("Path does not exist: #{path}") unless File.exist?(path)
86
+ return validation_error("Path is not a directory: #{path}") unless File.directory?(path)
87
+
88
+ resolve_path(path)
89
+ else
90
+ @directory
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ # Grep tool for searching file contents using ripgrep
7
+ #
8
+ # Powerful search with regex support, context lines, and file filtering.
9
+ class Grep < Base
10
+ class << self
11
+ # @return [Array<Symbol>] Constructor requirements
12
+ def creation_requirements
13
+ [:directory]
14
+ end
15
+ end
16
+
17
+ description <<~DESC
18
+ Search file contents using ripgrep.
19
+
20
+ Supports regex patterns, file type filtering, and context lines.
21
+ Output modes: "content" (matching lines), "files_with_matches" (file paths, default), "count" (match counts).
22
+ DESC
23
+
24
+ param :pattern,
25
+ type: "string",
26
+ desc: "Regular expression pattern to search for",
27
+ required: true
28
+
29
+ param :path,
30
+ type: "string",
31
+ desc: "File or directory to search in. Defaults to working directory.",
32
+ required: false
33
+
34
+ param :glob,
35
+ type: "string",
36
+ desc: 'Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}")',
37
+ required: false
38
+
39
+ param :type,
40
+ type: "string",
41
+ desc: "File type to search (e.g. js, py, ruby, rust)",
42
+ required: false
43
+
44
+ param :output_mode,
45
+ type: "string",
46
+ desc: '"content", "files_with_matches" (default), or "count"',
47
+ required: false
48
+
49
+ param :case_insensitive,
50
+ type: "boolean",
51
+ desc: "Case insensitive search",
52
+ required: false
53
+
54
+ param :multiline,
55
+ type: "boolean",
56
+ desc: "Enable multiline matching",
57
+ required: false
58
+
59
+ param :context_before,
60
+ type: "integer",
61
+ desc: "Lines to show before each match (content mode only)",
62
+ required: false
63
+
64
+ param :context_after,
65
+ type: "integer",
66
+ desc: "Lines to show after each match (content mode only)",
67
+ required: false
68
+
69
+ param :context,
70
+ type: "integer",
71
+ desc: "Lines to show before and after each match (content mode only)",
72
+ required: false
73
+
74
+ param :show_line_numbers,
75
+ type: "boolean",
76
+ desc: "Show line numbers (content mode only)",
77
+ required: false
78
+
79
+ param :head_limit,
80
+ type: "integer",
81
+ desc: "Limit output to first N lines/entries",
82
+ required: false
83
+
84
+ # @param directory [String] Working directory for searches
85
+ def initialize(directory:)
86
+ super()
87
+ @directory = File.expand_path(directory)
88
+ end
89
+
90
+ # Execute content search
91
+ #
92
+ # @return [String] Search results or error
93
+ def execute(
94
+ pattern:,
95
+ path: nil,
96
+ glob: nil,
97
+ type: nil,
98
+ output_mode: "files_with_matches",
99
+ case_insensitive: false,
100
+ multiline: false,
101
+ context_before: nil,
102
+ context_after: nil,
103
+ context: nil,
104
+ show_line_numbers: false,
105
+ head_limit: nil
106
+ )
107
+ return validation_error("pattern is required") if pattern.nil? || pattern.empty?
108
+
109
+ search_path = resolve_search_path(path)
110
+
111
+ valid_modes = ["content", "files_with_matches", "count"]
112
+ return validation_error("output_mode must be one of: #{valid_modes.join(", ")}") unless valid_modes.include?(output_mode)
113
+
114
+ cmd = build_command(
115
+ pattern: pattern,
116
+ path: search_path,
117
+ glob: glob,
118
+ type: type,
119
+ output_mode: output_mode,
120
+ case_insensitive: case_insensitive,
121
+ multiline: multiline,
122
+ context_before: context_before,
123
+ context_after: context_after,
124
+ context: context,
125
+ show_line_numbers: show_line_numbers,
126
+ )
127
+
128
+ run_ripgrep(cmd, pattern, head_limit)
129
+ rescue StandardError => e
130
+ error("Unexpected error: #{e.class.name} - #{e.message}")
131
+ end
132
+
133
+ private
134
+
135
+ # Resolve the search path
136
+ #
137
+ # @param path [String, nil] User-provided path
138
+ # @return [String] Resolved absolute path
139
+ def resolve_search_path(path)
140
+ if path.nil? || path.to_s.strip.empty?
141
+ @directory
142
+ else
143
+ resolve_path(path)
144
+ end
145
+ end
146
+
147
+ # Build ripgrep command array
148
+ #
149
+ # @return [Array<String>] Command parts
150
+ def build_command(pattern:, path:, glob:, type:, output_mode:, case_insensitive:, multiline:, context_before:, context_after:, context:, show_line_numbers:)
151
+ cmd = ["rg"]
152
+
153
+ case output_mode
154
+ when "files_with_matches" then cmd << "-l"
155
+ when "count" then cmd << "-c"
156
+ when "content"
157
+ cmd << "-n" if show_line_numbers
158
+ cmd << "-B" << context_before.to_s if context_before
159
+ cmd << "-A" << context_after.to_s if context_after
160
+ cmd << "-C" << context.to_s if context
161
+ end
162
+
163
+ cmd << "-i" if case_insensitive
164
+ cmd.push("-U", "--multiline-dotall") if multiline
165
+ cmd.push("--type", type) if type && !type.to_s.strip.empty?
166
+ cmd.push("--glob", glob) if glob && !glob.to_s.strip.empty?
167
+ cmd.push("-e", pattern, path)
168
+
169
+ cmd
170
+ end
171
+
172
+ # Run ripgrep and format output
173
+ #
174
+ # @param cmd [Array<String>] Command parts
175
+ # @param pattern [String] Search pattern
176
+ # @param head_limit [Integer, nil] Output limit
177
+ # @return [String] Formatted results
178
+ def run_ripgrep(cmd, pattern, head_limit)
179
+ stdout, stderr, status = Open3.capture3(*cmd)
180
+
181
+ return "No matches found for pattern: #{pattern}" if status.exitstatus == 1 && stderr.empty?
182
+ return error("ripgrep error: #{stderr}") if status.exitstatus == 2 || !stderr.empty?
183
+
184
+ output = stdout
185
+ if head_limit && head_limit > 0
186
+ lines = output.lines
187
+ if lines.count > head_limit
188
+ output = lines.take(head_limit).join
189
+ output += "\n<system-reminder>Output limited to first #{head_limit} lines.</system-reminder>"
190
+ end
191
+ end
192
+
193
+ output.empty? ? "No matches found for pattern: #{pattern}" : output
194
+ rescue Errno::ENOENT
195
+ error("ripgrep (rg) is not installed or not in PATH.")
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ # Placeholder for inter-agent messaging (future hub integration)
7
+ #
8
+ # This tool will be implemented when the MessageHub primitive is built.
9
+ # For now it exists as a namespace placeholder.
10
+ class MessageTeammate < Base
11
+ # Not yet implemented — placeholder for hub integration
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ # Placeholder for user-facing messaging (future hub integration)
7
+ #
8
+ # This tool will be implemented when the MessageHub primitive is built.
9
+ # For now it exists as a namespace placeholder.
10
+ class MessageUser < Base
11
+ # Not yet implemented — placeholder for hub integration
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ # Read tool for reading file contents from the filesystem
7
+ #
8
+ # Supports reading entire files or specific line ranges with line numbers.
9
+ # Tracks reads per agent for enforcing read-before-write/edit rules.
10
+ class Read < Base
11
+ class << self
12
+ # @return [Array<Symbol>] Constructor requirements
13
+ def creation_requirements
14
+ [:agent_name, :directory, :read_tracker]
15
+ end
16
+ end
17
+
18
+ description <<~DESC
19
+ Reads a file from the local filesystem.
20
+
21
+ Supports text files with line numbers. Binary files (images) are returned as visual content.
22
+
23
+ Path handling:
24
+ - Relative paths resolve against your working directory
25
+ - Absolute paths (starting with /) are used as-is
26
+ DESC
27
+
28
+ param :file_path,
29
+ type: "string",
30
+ desc: "Path to the file to read",
31
+ required: true
32
+
33
+ param :offset,
34
+ type: "integer",
35
+ desc: "Line number to start reading from (1-indexed). Use for large files.",
36
+ required: false
37
+
38
+ param :limit,
39
+ type: "integer",
40
+ desc: "Number of lines to read. Use for large files.",
41
+ required: false
42
+
43
+ # @param agent_name [Symbol, String] Agent identifier for read tracking
44
+ # @param directory [String] Agent's working directory
45
+ # @param read_tracker [ReadTracker] Shared read tracker for cross-tool enforcement
46
+ def initialize(agent_name:, directory:, read_tracker:)
47
+ super()
48
+ @agent_name = agent_name.to_sym
49
+ @directory = File.expand_path(directory)
50
+ @read_tracker = read_tracker
51
+ end
52
+
53
+ # Check if a file has been read by this agent
54
+ #
55
+ # @param path [String] Resolved file path
56
+ # @return [Boolean]
57
+ def file_read?(path)
58
+ @read_tracker.file_read?(@agent_name, path)
59
+ end
60
+
61
+ # Execute file read
62
+ #
63
+ # @param file_path [String] Path to the file
64
+ # @param offset [Integer, nil] Starting line number (1-indexed)
65
+ # @param limit [Integer, nil] Number of lines to read
66
+ # @return [String, RubyLLM::Content] File contents or error message
67
+ def execute(file_path:, offset: nil, limit: nil)
68
+ return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
69
+
70
+ resolved_path = resolve_path(file_path)
71
+
72
+ return validation_error("File does not exist: #{file_path}") unless File.exist?(resolved_path)
73
+ return validation_error("Path is a directory. Use Bash with ls to list directories.") if File.directory?(resolved_path)
74
+
75
+ content = read_file_content(resolved_path)
76
+
77
+ # Binary file — return as-is
78
+ return content if content.is_a?(RubyLLM::Content)
79
+ return content if content.start_with?("Error:")
80
+
81
+ @read_tracker.register_read(@agent_name, resolved_path)
82
+
83
+ return format_empty_file if content.empty?
84
+
85
+ format_text_content(content, file_path, offset, limit)
86
+ rescue StandardError => e
87
+ error("Unexpected error reading file: #{e.class.name} - #{e.message}")
88
+ end
89
+
90
+ private
91
+
92
+ # @return [String] Config accessor
93
+ def config
94
+ Configuration.instance
95
+ end
96
+
97
+ # Read file content, handling encoding
98
+ #
99
+ # @param file_path [String] Resolved absolute path
100
+ # @return [String, RubyLLM::Content] Text content or binary content object
101
+ def read_file_content(file_path)
102
+ content = File.read(file_path, encoding: "UTF-8")
103
+
104
+ unless content.valid_encoding?
105
+ return binary_or_unsupported(file_path)
106
+ end
107
+
108
+ content
109
+ rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
110
+ binary_or_unsupported(file_path)
111
+ rescue Errno::EACCES
112
+ error("Permission denied: Cannot read file '#{file_path}'")
113
+ end
114
+
115
+ # Handle binary files
116
+ #
117
+ # @param file_path [String] Path to binary file
118
+ # @return [RubyLLM::Content, String] Content object for images, error for others
119
+ def binary_or_unsupported(file_path)
120
+ ext = File.extname(file_path).downcase
121
+ image_formats = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".tif", ".svg", ".ico"]
122
+
123
+ if image_formats.include?(ext)
124
+ RubyLLM::Content.new("File: #{File.basename(file_path)}", file_path)
125
+ else
126
+ "Error: File contains binary data and cannot be displayed as text."
127
+ end
128
+ end
129
+
130
+ # Format empty file output
131
+ #
132
+ # @return [String]
133
+ def format_empty_file
134
+ "<system-reminder>Warning: This file exists but has empty contents.</system-reminder>"
135
+ end
136
+
137
+ # Format text content with line numbers
138
+ #
139
+ # @param content [String] Raw file content
140
+ # @param file_path [String] Original file path for display
141
+ # @param offset [Integer, nil] Starting line (1-indexed)
142
+ # @param limit [Integer, nil] Lines to read
143
+ # @return [String] Formatted output with line numbers
144
+ def format_text_content(content, file_path, offset, limit)
145
+ lines = content.lines
146
+ total_lines = lines.count
147
+
148
+ start_line = offset ? [offset - 1, 0].max : 0
149
+ return validation_error("Offset #{offset} exceeds file length (#{total_lines} lines)") if start_line >= total_lines
150
+
151
+ lines = lines.drop(start_line)
152
+
153
+ default_limit = config.read_line_limit
154
+ effective_limit = limit || default_limit
155
+ lines = lines.take(effective_limit)
156
+ truncated = limit.nil? && total_lines > default_limit
157
+
158
+ max_line_length = config.line_character_limit
159
+ output_lines = lines.each_with_index.map do |line, idx|
160
+ line_number = start_line + idx + 1
161
+ display_line = line.chomp
162
+ display_line = "#{display_line[0...max_line_length]}... (line truncated)" if display_line.length > max_line_length
163
+ "#{line_number.to_s.rjust(6)}\t#{display_line}"
164
+ end
165
+
166
+ output = output_lines.join("\n")
167
+ output += truncation_notice(total_lines, default_limit) if truncated
168
+ output
169
+ end
170
+
171
+ # @param total_lines [Integer] Total lines in file
172
+ # @param limit [Integer] Applied limit
173
+ # @return [String] Truncation notice
174
+ def truncation_notice(total_lines, limit)
175
+ "\n\n<system-reminder>This file has #{total_lines} lines but only the first #{limit} are shown. " \
176
+ "Use offset and limit parameters to read more.</system-reminder>"
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ # Tracks which files have been read by each agent
7
+ #
8
+ # Shared across Read, Write, and Edit tool instances for the same agent.
9
+ # Enforces read-before-write/edit rules to prevent accidental overwrites.
10
+ #
11
+ # @example
12
+ # tracker = ReadTracker.new
13
+ # tracker.register_read(:backend, "/path/to/file.rb")
14
+ # tracker.file_read?(:backend, "/path/to/file.rb") #=> true
15
+ class ReadTracker
16
+ def initialize
17
+ @reads = Hash.new { |h, k| h[k] = Set.new }
18
+ end
19
+
20
+ # Register that an agent has read a file
21
+ #
22
+ # @param agent_name [Symbol] Agent identifier
23
+ # @param path [String] Absolute file path
24
+ # @return [void]
25
+ def register_read(agent_name, path)
26
+ @reads[agent_name].add(path)
27
+ end
28
+
29
+ # Check if an agent has read a file
30
+ #
31
+ # @param agent_name [Symbol] Agent identifier
32
+ # @param path [String] Absolute file path
33
+ # @return [Boolean]
34
+ def file_read?(agent_name, path)
35
+ @reads[agent_name].include?(path)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end