swarm_sdk 2.7.14 → 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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
  4. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  5. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  6. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  7. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  8. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  9. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  10. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  11. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  12. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  13. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  14. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  15. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  16. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  17. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  18. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  19. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  20. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  24. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  25. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  26. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  27. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  28. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  29. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  30. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  31. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  32. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  33. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  34. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  35. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  36. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  37. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  38. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  39. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  40. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  41. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  42. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  43. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  44. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  45. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  46. data/lib/swarm_sdk/v3/tools/read.rb +181 -0
  47. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  48. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  49. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  50. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  51. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  52. data/lib/swarm_sdk/v3.rb +145 -0
  53. metadata +83 -148
  54. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  55. data/lib/swarm_sdk/agent/builder.rb +0 -705
  56. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  57. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  58. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  59. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  60. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  61. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  62. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  63. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  64. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  65. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  66. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  67. data/lib/swarm_sdk/agent/context.rb +0 -115
  68. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  69. data/lib/swarm_sdk/agent/definition.rb +0 -588
  70. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  71. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  72. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  73. data/lib/swarm_sdk/agent_registry.rb +0 -146
  74. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  75. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  76. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  77. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  78. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  79. data/lib/swarm_sdk/config.rb +0 -368
  80. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  81. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  82. data/lib/swarm_sdk/configuration.rb +0 -165
  83. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  84. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  85. data/lib/swarm_sdk/context_compactor.rb +0 -335
  86. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  87. data/lib/swarm_sdk/context_management/context.rb +0 -328
  88. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  89. data/lib/swarm_sdk/defaults.rb +0 -251
  90. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  91. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  92. data/lib/swarm_sdk/hooks/context.rb +0 -197
  93. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  94. data/lib/swarm_sdk/hooks/error.rb +0 -29
  95. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  96. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  97. data/lib/swarm_sdk/hooks/result.rb +0 -150
  98. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  99. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  100. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  101. data/lib/swarm_sdk/log_collector.rb +0 -227
  102. data/lib/swarm_sdk/log_stream.rb +0 -127
  103. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  104. data/lib/swarm_sdk/model_aliases.json +0 -8
  105. data/lib/swarm_sdk/models.json +0 -44002
  106. data/lib/swarm_sdk/models.rb +0 -161
  107. data/lib/swarm_sdk/node_context.rb +0 -245
  108. data/lib/swarm_sdk/observer/builder.rb +0 -81
  109. data/lib/swarm_sdk/observer/config.rb +0 -45
  110. data/lib/swarm_sdk/observer/manager.rb +0 -248
  111. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  112. data/lib/swarm_sdk/permissions/config.rb +0 -239
  113. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  114. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  115. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  116. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  117. data/lib/swarm_sdk/plugin.rb +0 -309
  118. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  119. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  120. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  121. data/lib/swarm_sdk/restore_result.rb +0 -65
  122. data/lib/swarm_sdk/result.rb +0 -241
  123. data/lib/swarm_sdk/snapshot.rb +0 -156
  124. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  125. data/lib/swarm_sdk/state_restorer.rb +0 -476
  126. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  127. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  128. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  129. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  130. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  131. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  132. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  133. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  134. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  135. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  136. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  137. data/lib/swarm_sdk/swarm.rb +0 -973
  138. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  139. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  140. data/lib/swarm_sdk/tools/base.rb +0 -63
  141. data/lib/swarm_sdk/tools/bash.rb +0 -280
  142. data/lib/swarm_sdk/tools/clock.rb +0 -46
  143. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  144. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  145. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  146. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  147. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  148. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  149. data/lib/swarm_sdk/tools/edit.rb +0 -145
  150. data/lib/swarm_sdk/tools/glob.rb +0 -166
  151. data/lib/swarm_sdk/tools/grep.rb +0 -235
  152. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  153. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  154. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  155. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  156. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  157. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  158. data/lib/swarm_sdk/tools/read.rb +0 -261
  159. data/lib/swarm_sdk/tools/registry.rb +0 -205
  160. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  161. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  163. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  164. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  165. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  166. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  167. data/lib/swarm_sdk/tools/think.rb +0 -100
  168. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  169. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  170. data/lib/swarm_sdk/tools/write.rb +0 -112
  171. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  172. data/lib/swarm_sdk/utils.rb +0 -68
  173. data/lib/swarm_sdk/validation_result.rb +0 -33
  174. data/lib/swarm_sdk/version.rb +0 -5
  175. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  176. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  177. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  178. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  179. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  180. data/lib/swarm_sdk/workflow.rb +0 -589
  181. data/lib/swarm_sdk.rb +0 -721
@@ -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