swarm_sdk 2.7.14 → 3.0.0.alpha2

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 (185) 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/document_converters/base.rb +84 -0
  42. data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
  43. data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -0
  45. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  46. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  47. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  48. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  49. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  50. data/lib/swarm_sdk/v3/tools/read.rb +213 -0
  51. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  52. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  53. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  54. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  55. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  56. data/lib/swarm_sdk/v3.rb +145 -0
  57. metadata +88 -149
  58. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  59. data/lib/swarm_sdk/agent/builder.rb +0 -705
  60. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  61. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  62. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  63. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  64. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  65. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  66. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  67. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  68. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  69. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  70. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  71. data/lib/swarm_sdk/agent/context.rb +0 -115
  72. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  73. data/lib/swarm_sdk/agent/definition.rb +0 -588
  74. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  75. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  76. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  77. data/lib/swarm_sdk/agent_registry.rb +0 -146
  78. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  79. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  80. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  81. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  82. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  83. data/lib/swarm_sdk/config.rb +0 -368
  84. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  85. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  86. data/lib/swarm_sdk/configuration.rb +0 -165
  87. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  88. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  89. data/lib/swarm_sdk/context_compactor.rb +0 -335
  90. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  91. data/lib/swarm_sdk/context_management/context.rb +0 -328
  92. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  93. data/lib/swarm_sdk/defaults.rb +0 -251
  94. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  95. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  96. data/lib/swarm_sdk/hooks/context.rb +0 -197
  97. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  98. data/lib/swarm_sdk/hooks/error.rb +0 -29
  99. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  100. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  101. data/lib/swarm_sdk/hooks/result.rb +0 -150
  102. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  103. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  104. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  105. data/lib/swarm_sdk/log_collector.rb +0 -227
  106. data/lib/swarm_sdk/log_stream.rb +0 -127
  107. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  108. data/lib/swarm_sdk/model_aliases.json +0 -8
  109. data/lib/swarm_sdk/models.json +0 -44002
  110. data/lib/swarm_sdk/models.rb +0 -161
  111. data/lib/swarm_sdk/node_context.rb +0 -245
  112. data/lib/swarm_sdk/observer/builder.rb +0 -81
  113. data/lib/swarm_sdk/observer/config.rb +0 -45
  114. data/lib/swarm_sdk/observer/manager.rb +0 -248
  115. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  116. data/lib/swarm_sdk/permissions/config.rb +0 -239
  117. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  118. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  119. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  120. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  121. data/lib/swarm_sdk/plugin.rb +0 -309
  122. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  123. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  124. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  125. data/lib/swarm_sdk/restore_result.rb +0 -65
  126. data/lib/swarm_sdk/result.rb +0 -241
  127. data/lib/swarm_sdk/snapshot.rb +0 -156
  128. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  129. data/lib/swarm_sdk/state_restorer.rb +0 -476
  130. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  131. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  132. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  133. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  134. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  135. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  136. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  141. data/lib/swarm_sdk/swarm.rb +0 -973
  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/base.rb +0 -63
  145. data/lib/swarm_sdk/tools/bash.rb +0 -280
  146. data/lib/swarm_sdk/tools/clock.rb +0 -46
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  148. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  149. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  150. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  151. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  152. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  153. data/lib/swarm_sdk/tools/edit.rb +0 -145
  154. data/lib/swarm_sdk/tools/glob.rb +0 -166
  155. data/lib/swarm_sdk/tools/grep.rb +0 -235
  156. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  157. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  160. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  161. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  162. data/lib/swarm_sdk/tools/read.rb +0 -261
  163. data/lib/swarm_sdk/tools/registry.rb +0 -205
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  166. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  167. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  168. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  169. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  170. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  171. data/lib/swarm_sdk/tools/think.rb +0 -100
  172. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  173. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  174. data/lib/swarm_sdk/tools/write.rb +0 -112
  175. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  176. data/lib/swarm_sdk/utils.rb +0 -68
  177. data/lib/swarm_sdk/validation_result.rb +0 -33
  178. data/lib/swarm_sdk/version.rb +0 -5
  179. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  180. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  181. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  182. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  183. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  184. data/lib/swarm_sdk/workflow.rb +0 -589
  185. data/lib/swarm_sdk.rb +0 -721
@@ -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,213 @@
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
+ # Supports document formats (PDF, DOCX, XLSX) if gems installed.
11
+ class Read < Base
12
+ # Document converters (optional gems)
13
+ CONVERTERS = [
14
+ DocumentConverters::PdfConverter,
15
+ DocumentConverters::DocxConverter,
16
+ DocumentConverters::XlsxConverter,
17
+ ].freeze
18
+
19
+ class << self
20
+ # @return [Array<Symbol>] Constructor requirements
21
+ def creation_requirements
22
+ [:agent_name, :directory, :read_tracker]
23
+ end
24
+ end
25
+
26
+ description <<~DESC
27
+ Reads a file from the local filesystem.
28
+
29
+ Supports text files with line numbers. Binary files (images) are returned as visual content.
30
+ Supports document formats (PDF, DOCX, XLSX) if gems installed.
31
+
32
+ Path handling:
33
+ - Relative paths resolve against your working directory
34
+ - Absolute paths (starting with /) are used as-is
35
+ DESC
36
+
37
+ param :file_path,
38
+ type: "string",
39
+ desc: "Path to the file to read",
40
+ required: true
41
+
42
+ param :offset,
43
+ type: "integer",
44
+ desc: "Line number to start reading from (1-indexed). Use for large text files. Ignored for documents.",
45
+ required: false
46
+
47
+ param :limit,
48
+ type: "integer",
49
+ desc: "Number of lines to read. Use for large text files. Ignored for documents.",
50
+ required: false
51
+
52
+ # @param agent_name [Symbol, String] Agent identifier for read tracking
53
+ # @param directory [String] Agent's working directory
54
+ # @param read_tracker [ReadTracker] Shared read tracker for cross-tool enforcement
55
+ def initialize(agent_name:, directory:, read_tracker:)
56
+ super()
57
+ @agent_name = agent_name.to_sym
58
+ @directory = File.expand_path(directory)
59
+ @read_tracker = read_tracker
60
+ end
61
+
62
+ # Check if a file has been read by this agent
63
+ #
64
+ # @param path [String] Resolved file path
65
+ # @return [Boolean]
66
+ def file_read?(path)
67
+ @read_tracker.file_read?(@agent_name, path)
68
+ end
69
+
70
+ # Execute file read
71
+ #
72
+ # @param file_path [String] Path to the file
73
+ # @param offset [Integer, nil] Starting line number (1-indexed)
74
+ # @param limit [Integer, nil] Number of lines to read
75
+ # @return [String, RubyLLM::Content] File contents or error message
76
+ def execute(file_path:, offset: nil, limit: nil)
77
+ return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
78
+
79
+ resolved_path = resolve_path(file_path)
80
+
81
+ return validation_error("File does not exist: #{file_path}") unless File.exist?(resolved_path)
82
+ return validation_error("Path is a directory. Use Bash with ls to list directories.") if File.directory?(resolved_path)
83
+
84
+ # Try document converter first
85
+ converter_class = find_converter(resolved_path)
86
+ if converter_class
87
+ result = converter_class.new.convert(resolved_path)
88
+
89
+ # Register read for successful conversions
90
+ unless result.start_with?("<system-reminder>") || result.start_with?("Error:")
91
+ @read_tracker.register_read(@agent_name, resolved_path)
92
+ end
93
+
94
+ return result
95
+ end
96
+
97
+ # Standard text file handling
98
+ content = read_file_content(resolved_path)
99
+
100
+ # Binary file — return as-is
101
+ return content if content.is_a?(RubyLLM::Content)
102
+ return content if content.start_with?("Error:")
103
+
104
+ @read_tracker.register_read(@agent_name, resolved_path)
105
+
106
+ return format_empty_file if content.empty?
107
+
108
+ format_text_content(content, file_path, offset, limit)
109
+ rescue StandardError => e
110
+ error("Unexpected error reading file: #{e.class.name} - #{e.message}")
111
+ end
112
+
113
+ private
114
+
115
+ # @return [String] Config accessor
116
+ def config
117
+ Configuration.instance
118
+ end
119
+
120
+ # Read file content, handling encoding
121
+ #
122
+ # @param file_path [String] Resolved absolute path
123
+ # @return [String, RubyLLM::Content] Text content or binary content object
124
+ def read_file_content(file_path)
125
+ content = File.read(file_path, encoding: "UTF-8")
126
+
127
+ unless content.valid_encoding?
128
+ return binary_or_unsupported(file_path)
129
+ end
130
+
131
+ content
132
+ rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
133
+ binary_or_unsupported(file_path)
134
+ rescue Errno::EACCES
135
+ error("Permission denied: Cannot read file '#{file_path}'")
136
+ end
137
+
138
+ # Handle binary files
139
+ #
140
+ # @param file_path [String] Path to binary file
141
+ # @return [RubyLLM::Content, String] Content object for images, error for others
142
+ def binary_or_unsupported(file_path)
143
+ ext = File.extname(file_path).downcase
144
+ image_formats = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".tif", ".svg", ".ico"]
145
+
146
+ if image_formats.include?(ext)
147
+ RubyLLM::Content.new("File: #{File.basename(file_path)}", file_path)
148
+ else
149
+ "Error: File contains binary data and cannot be displayed as text."
150
+ end
151
+ end
152
+
153
+ # Format empty file output
154
+ #
155
+ # @return [String]
156
+ def format_empty_file
157
+ "<system-reminder>Warning: This file exists but has empty contents.</system-reminder>"
158
+ end
159
+
160
+ # Format text content with line numbers
161
+ #
162
+ # @param content [String] Raw file content
163
+ # @param file_path [String] Original file path for display
164
+ # @param offset [Integer, nil] Starting line (1-indexed)
165
+ # @param limit [Integer, nil] Lines to read
166
+ # @return [String] Formatted output with line numbers
167
+ def format_text_content(content, file_path, offset, limit)
168
+ lines = content.lines
169
+ total_lines = lines.count
170
+
171
+ start_line = offset ? [offset - 1, 0].max : 0
172
+ return validation_error("Offset #{offset} exceeds file length (#{total_lines} lines)") if start_line >= total_lines
173
+
174
+ lines = lines.drop(start_line)
175
+
176
+ default_limit = config.read_line_limit
177
+ effective_limit = limit || default_limit
178
+ lines = lines.take(effective_limit)
179
+ truncated = limit.nil? && total_lines > default_limit
180
+
181
+ max_line_length = config.line_character_limit
182
+ output_lines = lines.each_with_index.map do |line, idx|
183
+ line_number = start_line + idx + 1
184
+ display_line = line.chomp
185
+ display_line = "#{display_line[0...max_line_length]}... (line truncated)" if display_line.length > max_line_length
186
+ "#{line_number.to_s.rjust(6)}\t#{display_line}"
187
+ end
188
+
189
+ output = output_lines.join("\n")
190
+ output += truncation_notice(total_lines, default_limit) if truncated
191
+ output
192
+ end
193
+
194
+ # @param total_lines [Integer] Total lines in file
195
+ # @param limit [Integer] Applied limit
196
+ # @return [String] Truncation notice
197
+ def truncation_notice(total_lines, limit)
198
+ "\n\n<system-reminder>This file has #{total_lines} lines but only the first #{limit} are shown. " \
199
+ "Use offset and limit parameters to read more.</system-reminder>"
200
+ end
201
+
202
+ # Find appropriate document converter for file extension
203
+ #
204
+ # @param file_path [String] Resolved file path
205
+ # @return [Class, nil] Converter class or nil if no match
206
+ def find_converter(file_path)
207
+ ext = File.extname(file_path).downcase
208
+ CONVERTERS.find { |c| c.extensions.include?(ext) }
209
+ end
210
+ end
211
+ end
212
+ end
213
+ 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
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ # Registry for V3 tools (built-in and custom)
7
+ #
8
+ # Maps tool names (symbols) to their V3 tool classes.
9
+ # Provides lookup, validation, factory, and custom tool registration.
10
+ #
11
+ # Tools fall into categories based on creation requirements:
12
+ # 1. **No params**: Simple tools (Think, Clock)
13
+ # 2. **Directory only**: Tools needing working directory (Bash, Grep, Glob)
14
+ # 3. **Agent context**: Tools needing agent name + directory (Read, Write, Edit)
15
+ #
16
+ # @example Look up a tool
17
+ # klass = Registry.get(:Read)
18
+ #
19
+ # @example Create a tool instance
20
+ # tool = Registry.create(:Read, agent_name: :backend, directory: "/app")
21
+ #
22
+ # @example Register a custom tool
23
+ # Registry.register(:MyTool, MyCustomTool)
24
+ #
25
+ # @example List available tools
26
+ # Registry.available_names #=> [:Read, :Write, :Edit, ...]
27
+ class Registry
28
+ class << self
29
+ # Lazily-built tool mapping
30
+ #
31
+ # Uses lazy evaluation so tool classes are only resolved when first accessed,
32
+ # allowing Zeitwerk to load them on demand. Mutable to support custom registration.
33
+ #
34
+ # @return [Hash<Symbol, Class>] Tool name to class mapping
35
+ def builtin_tools
36
+ @builtin_tools ||= {
37
+ Read: SwarmSDK::V3::Tools::Read,
38
+ Write: SwarmSDK::V3::Tools::Write,
39
+ Edit: SwarmSDK::V3::Tools::Edit,
40
+ Bash: SwarmSDK::V3::Tools::Bash,
41
+ Grep: SwarmSDK::V3::Tools::Grep,
42
+ Glob: SwarmSDK::V3::Tools::Glob,
43
+ Think: SwarmSDK::V3::Tools::Think,
44
+ Clock: SwarmSDK::V3::Tools::Clock,
45
+ SubTask: SwarmSDK::V3::Tools::SubTask,
46
+ }
47
+ end
48
+
49
+ # Register a custom tool
50
+ #
51
+ # @param name [Symbol, String] Tool name
52
+ # @param klass [Class] Tool class (must inherit from Base or RubyLLM::Tool)
53
+ # @return [void]
54
+ # @raise [ConfigurationError] If name is already taken by a built-in tool
55
+ #
56
+ # @example
57
+ # Registry.register(:WebSearch, MyWebSearchTool)
58
+ def register(name, klass)
59
+ name_sym = name.to_sym
60
+ builtin_tools[name_sym] = klass
61
+ end
62
+
63
+ # Reset registry to built-in tools only
64
+ #
65
+ # Removes all custom tool registrations. Useful for test cleanup.
66
+ #
67
+ # @return [void]
68
+ def reset!
69
+ @builtin_tools = nil
70
+ end
71
+
72
+ # Get tool class by name
73
+ #
74
+ # Respects the `registered_tools` configuration filter. If `registered_tools`
75
+ # is set, only those tools are visible.
76
+ #
77
+ # @param name [Symbol, String] Tool name
78
+ # @return [Class, nil] Tool class or nil if not found/filtered
79
+ #
80
+ # @example
81
+ # Registry.get(:Read) #=> SwarmSDK::V3::Tools::Read
82
+ def get(name)
83
+ name_sym = name.to_sym
84
+ allowed = Configuration.instance.registered_tools
85
+ return if allowed && !allowed.map(&:to_sym).include?(name_sym)
86
+
87
+ builtin_tools[name_sym]
88
+ end
89
+
90
+ # Create a tool instance with context
91
+ #
92
+ # Uses the tool's `creation_requirements` to determine constructor params.
93
+ #
94
+ # @param name [Symbol, String] Tool name
95
+ # @param context [Hash] Available context for tool creation
96
+ # @option context [Symbol] :agent_name Agent identifier
97
+ # @option context [String] :directory Agent's working directory
98
+ # @return [RubyLLM::Tool] Instantiated tool
99
+ # @raise [ConfigurationError] If tool unknown or requirements unmet
100
+ #
101
+ # @example
102
+ # tool = Registry.create(:Read, agent_name: :backend, directory: "/app")
103
+ def create(name, **context)
104
+ name_sym = name.to_sym
105
+ tool_class = get(name_sym)
106
+
107
+ raise ConfigurationError, "Unknown tool: #{name}" unless tool_class
108
+
109
+ if tool_class.respond_to?(:creation_requirements) && tool_class.creation_requirements.any?
110
+ requirements = tool_class.creation_requirements
111
+ params = extract_params(requirements, context, name)
112
+ tool_class.new(**params)
113
+ else
114
+ tool_class.new
115
+ end
116
+ end
117
+
118
+ # Create all tools for an agent definition
119
+ #
120
+ # Merges the agent's tools with global `default_tools` from configuration.
121
+ # Filters against `registered_tools` if configured.
122
+ #
123
+ # @param definition [AgentDefinition] Agent definition with tool list
124
+ # @return [Array<RubyLLM::Tool>] Instantiated tools
125
+ # @raise [ConfigurationError] If any tool is unknown
126
+ #
127
+ # @example
128
+ # tools = Registry.create_all(definition)
129
+ def create_all(definition, memory_store: nil, subtask_depth: 0)
130
+ # Create shared read tracker for cross-tool enforcement
131
+ read_tracker = ReadTracker.new
132
+
133
+ context = {
134
+ agent_name: definition.name,
135
+ directory: definition.directory,
136
+ read_tracker: read_tracker,
137
+ memory_store: memory_store,
138
+ agent_definition: definition,
139
+ subtask_depth: subtask_depth,
140
+ }
141
+
142
+ config = Configuration.instance
143
+
144
+ # Start with the agent's declared tools + global default_tools
145
+ tool_names = definition.tools.dup
146
+ config.default_tools.each do |name|
147
+ tool_names << name.to_sym unless tool_names.include?(name.to_sym)
148
+ end
149
+
150
+ # Filter against registered_tools if configured
151
+ if config.registered_tools
152
+ allowed = config.registered_tools.map(&:to_sym)
153
+ tool_names.select! { |name| allowed.include?(name) }
154
+ end
155
+
156
+ tool_names.uniq.map { |name| create(name, **context) }
157
+ end
158
+
159
+ # Check if a tool exists (respects registered_tools filter)
160
+ #
161
+ # @param name [Symbol, String] Tool name
162
+ # @return [Boolean]
163
+ def exists?(name)
164
+ !get(name.to_sym).nil?
165
+ end
166
+
167
+ # Get all available tool names (respects registered_tools filter)
168
+ #
169
+ # @return [Array<Symbol>]
170
+ def available_names
171
+ allowed = Configuration.instance.registered_tools
172
+ names = builtin_tools.keys
173
+ names.select! { |n| allowed.map(&:to_sym).include?(n) } if allowed
174
+ names
175
+ end
176
+
177
+ # Validate tool names
178
+ #
179
+ # @param names [Array<Symbol, String>] Tool names to validate
180
+ # @return [Array<Symbol>] Invalid tool names
181
+ def validate(names)
182
+ names.map(&:to_sym).reject { |name| exists?(name) }
183
+ end
184
+
185
+ private
186
+
187
+ # Extract parameters from context for tool construction
188
+ #
189
+ # Includes each declared requirement that exists in the context.
190
+ # Missing keys are skipped — the tool constructor's own defaults
191
+ # and Ruby's `missing keyword` error handle validation naturally.
192
+ #
193
+ # @param requirements [Array<Symbol>] Parameter names the tool accepts
194
+ # @param context [Hash] Available context
195
+ # @param _tool_name [Symbol] Tool name (unused, kept for interface stability)
196
+ # @return [Hash] Parameters for constructor
197
+ def extract_params(requirements, context, _tool_name)
198
+ params = {}
199
+ requirements.each do |req|
200
+ params[req] = context[req] if context.key?(req)
201
+ end
202
+ params
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end