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
@@ -1,198 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # Lazy-loading wrapper for MCP tools
6
- #
7
- # Creates minimal tool stub without calling tools/list.
8
- # Schema is fetched on-demand when LLM needs it.
9
- #
10
- # ## Boot Optimization
11
- #
12
- # When MCP server tools are pre-specified in configuration:
13
- # - Boot time: Create stubs instantly (no RPC)
14
- # - First LLM request: Fetch schema lazily (~100ms one-time cost)
15
- # - Subsequent requests: Use cached schema (instant)
16
- #
17
- # ## Thread Safety
18
- #
19
- # Schema loading is protected by Async::Semaphore with double-check pattern
20
- # to ensure only one fiber fetches the schema even under concurrent access.
21
- #
22
- # @example Creating a stub
23
- # coordinator = RubyLLM::MCP::Coordinator.new(client)
24
- # stub = McpToolStub.new(
25
- # coordinator: coordinator,
26
- # name: "search_code",
27
- # description: "Search code in repository"
28
- # )
29
- #
30
- # @example Schema is fetched lazily
31
- # stub.params_schema # First access triggers tools/list RPC
32
- # stub.params_schema # Cached, instant
33
- class McpToolStub < Base
34
- removable true # MCP tools can be controlled by skills
35
-
36
- attr_reader :name, :client, :server_name
37
-
38
- # Create a new MCP tool stub
39
- #
40
- # @param client [RubyLLM::MCP::Client] MCP client instance
41
- # @param name [String] Tool name
42
- # @param server_name [String, nil] MCP server name for error context
43
- # @param description [String, nil] Tool description (optional, fetched if nil)
44
- # @param schema [Hash, nil] Tool input schema (optional, fetched if nil)
45
- #
46
- # @example Minimal stub (lazy description + schema)
47
- # McpToolStub.new(client: client, name: "search", server_name: "codebase")
48
- #
49
- # @example With description (lazy schema only)
50
- # McpToolStub.new(
51
- # client: client,
52
- # name: "search",
53
- # server_name: "codebase",
54
- # description: "Search the codebase"
55
- # )
56
- #
57
- # @example Fully specified (no lazy loading)
58
- # McpToolStub.new(
59
- # client: client,
60
- # name: "search",
61
- # server_name: "codebase",
62
- # description: "Search the codebase",
63
- # schema: { type: "object", properties: {...} }
64
- # )
65
- def initialize(client:, name:, server_name: nil, description: nil, schema: nil)
66
- super()
67
- @client = client
68
- @name = name
69
- @mcp_name = name
70
- @server_name = server_name || "unknown"
71
- @description = description || "MCP tool: #{name}"
72
- @input_schema = schema
73
- @schema_loaded = !schema.nil?
74
- @schema_mutex = Async::Semaphore.new(1) # Thread-safe schema loading
75
- end
76
-
77
- # Get tool description
78
- #
79
- # @return [String]
80
- attr_reader :description
81
-
82
- # Get parameter schema (lazy-loaded on first access)
83
- #
84
- # This method is called by RubyLLM when building tool schemas for LLM requests.
85
- # On first access, it triggers a tools/list RPC to fetch the schema.
86
- #
87
- # @return [Hash, nil] JSON Schema for tool parameters
88
- def params_schema
89
- ensure_schema_loaded!
90
- @input_schema
91
- end
92
-
93
- # Execute the MCP tool
94
- #
95
- # Calls the MCP server's tools/call endpoint with the provided parameters.
96
- # Schema is NOT required for execution - the server validates parameters.
97
- #
98
- # @param params [Hash] Tool parameters
99
- # @return [String, Hash] Tool result content or error hash
100
- # @raise [MCPTimeoutError] When the MCP server times out
101
- # @raise [MCPTransportError] When there's a transport-level error
102
- # @raise [MCPError] When any other MCP error occurs
103
- def execute(**params)
104
- # Use client.call_tool (client has internal coordinator)
105
- result = @client.call_tool(
106
- name: @mcp_name,
107
- arguments: params,
108
- )
109
-
110
- # client.call_tool returns the result content directly
111
- result
112
- rescue RubyLLM::MCP::Errors::TimeoutError => e
113
- raise MCPTimeoutError, format_mcp_error(
114
- "MCP request timed out",
115
- original_message: e.message,
116
- request_id: e.request_id,
117
- )
118
- rescue RubyLLM::MCP::Errors::TransportError => e
119
- raise MCPTransportError, format_mcp_error(
120
- "MCP transport error",
121
- original_message: e.message,
122
- code: e.code,
123
- )
124
- rescue RubyLLM::MCP::Errors::BaseError => e
125
- raise MCPError, format_mcp_error(
126
- "MCP error",
127
- original_message: e.message,
128
- )
129
- end
130
-
131
- private
132
-
133
- # Lazy-load schema on first access (when LLM needs it)
134
- #
135
- # Thread-safe via semaphore with double-check pattern.
136
- # Multiple concurrent fibers will only trigger one fetch.
137
- #
138
- # @return [void]
139
- # @raise [MCPTimeoutError] When the MCP server times out during schema fetch
140
- # @raise [MCPTransportError] When there's a transport-level error
141
- # @raise [MCPError] When any other MCP error occurs
142
- def ensure_schema_loaded!
143
- return if @schema_loaded
144
-
145
- @schema_mutex.acquire do
146
- return if @schema_loaded # Double-check after acquiring lock
147
-
148
- # Fetch tool info from client (calls tools/list if not cached)
149
- tool_info = @client.tool_info(@mcp_name)
150
-
151
- if tool_info
152
- @description = tool_info["description"] || @description
153
- @input_schema = tool_info["inputSchema"]
154
- else
155
- # Tool doesn't exist on server - schema remains nil
156
- RubyLLM.logger.warn("SwarmSDK: MCP tool '#{@mcp_name}' not found on server during schema fetch")
157
- end
158
-
159
- @schema_loaded = true
160
- end
161
- rescue RubyLLM::MCP::Errors::TimeoutError => e
162
- raise MCPTimeoutError, format_mcp_error(
163
- "MCP schema fetch timed out",
164
- original_message: e.message,
165
- request_id: e.request_id,
166
- )
167
- rescue RubyLLM::MCP::Errors::TransportError => e
168
- raise MCPTransportError, format_mcp_error(
169
- "MCP transport error during schema fetch",
170
- original_message: e.message,
171
- code: e.code,
172
- )
173
- rescue RubyLLM::MCP::Errors::BaseError => e
174
- raise MCPError, format_mcp_error(
175
- "MCP error during schema fetch",
176
- original_message: e.message,
177
- )
178
- end
179
-
180
- # Format MCP error message with contextual information
181
- #
182
- # @param prefix [String] Error message prefix
183
- # @param original_message [String] Original error message from RubyLLM::MCP
184
- # @param request_id [String, nil] MCP request ID (for timeout errors)
185
- # @param code [Integer, nil] HTTP status code (for transport errors)
186
- # @return [String] Formatted error message with full context
187
- def format_mcp_error(prefix, original_message:, request_id: nil, code: nil)
188
- parts = [prefix]
189
- parts << "[server: #{@server_name}]"
190
- parts << "[tool: #{@mcp_name}]"
191
- parts << "[request_id: #{request_id}]" if request_id
192
- parts << "[code: #{code}]" if code
193
- parts << "- #{original_message}"
194
- parts.join(" ")
195
- end
196
- end
197
- end
198
- end
@@ -1,236 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # MultiEdit tool for performing multiple exact string replacements in a file
6
- #
7
- # Applies multiple edit operations sequentially to a single file.
8
- # Each edit sees the result of all previous edits, allowing for
9
- # coordinated multi-step transformations.
10
- # Enforces read-before-edit rule.
11
- class MultiEdit < Base
12
- include PathResolver
13
-
14
- # Factory pattern: declare what parameters this tool needs for instantiation
15
- class << self
16
- def creation_requirements
17
- [:agent_name, :directory]
18
- end
19
- end
20
-
21
- description <<~DESC
22
- Performs multiple exact string replacements in a single file.
23
- Edits are applied sequentially, so later edits see the results of earlier ones.
24
- You must use your Read tool at least once in the conversation before editing.
25
- This tool will error if you attempt an edit without reading the file.
26
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix.
27
- The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match.
28
- Never include any part of the line number prefix in the old_string or new_string.
29
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
30
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
31
- Each edit will FAIL if old_string is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use replace_all to change every instance of old_string.
32
- Use replace_all for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
33
-
34
- IMPORTANT - Path Handling:
35
- - Relative paths (e.g., "tmp/file.txt", "src/main.rb") are resolved relative to your agent's working directory
36
- - Absolute paths (e.g., "/tmp/file.txt", "/etc/passwd") are treated as system absolute paths
37
- - When the user says "tmp/file.txt" they mean the tmp directory in your working directory, NOT /tmp
38
- - Only use absolute paths (starting with /) when explicitly referring to system-level paths
39
- DESC
40
-
41
- param :file_path,
42
- type: "string",
43
- desc: "Path to the file. Use relative paths (e.g., 'tmp/file.txt') for files in your working directory, or absolute paths (e.g., '/etc/passwd') for system files.",
44
- required: true
45
-
46
- param :edits_json,
47
- type: "string",
48
- desc: <<~DESC.chomp,
49
- JSON array of edit operations. Each edit must have:
50
- old_string (exact text to replace),
51
- new_string (replacement text),
52
- and optionally replace_all (boolean, default false).
53
- Example: [{"old_string":"foo","new_string":"bar","replace_all":false}]
54
- DESC
55
- required: true
56
-
57
- # Initialize the MultiEdit tool for a specific agent
58
- #
59
- # @param agent_name [Symbol, String] The agent identifier
60
- # @param directory [String] Agent's working directory
61
- def initialize(agent_name:, directory:)
62
- super()
63
- initialize_agent_context(agent_name: agent_name, directory: directory)
64
- end
65
-
66
- # Override name to return simple "MultiEdit" instead of full class path
67
- def name
68
- "MultiEdit"
69
- end
70
-
71
- def execute(file_path:, edits_json:)
72
- # Validate inputs
73
- return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
74
-
75
- # CRITICAL: Resolve path against agent directory
76
- resolved_path = resolve_path(file_path)
77
-
78
- # Parse JSON
79
- edits = begin
80
- JSON.parse(edits_json)
81
- rescue JSON::ParserError
82
- nil
83
- end
84
-
85
- return validation_error("Invalid JSON format. Please provide a valid JSON array of edit operations.") if edits.nil?
86
-
87
- return validation_error("edits must be an array") unless edits.is_a?(Array)
88
- return validation_error("edits array cannot be empty") if edits.empty?
89
-
90
- # File must exist (use resolved path)
91
- unless File.exist?(resolved_path)
92
- return validation_error("File does not exist: #{file_path}")
93
- end
94
-
95
- # Enforce read-before-edit (use resolved path)
96
- unless Stores::ReadTracker.file_read?(@agent_name, resolved_path)
97
- return validation_error(
98
- "Cannot edit file without reading it first. " \
99
- "You must use the Read tool on '#{file_path}' before editing it. " \
100
- "This ensures you have the current file contents to match against.",
101
- )
102
- end
103
-
104
- # Read current content (use resolved path)
105
- content = File.read(resolved_path, encoding: "UTF-8")
106
-
107
- # Validate edit operations
108
- validated_edits = []
109
- edits.each_with_index do |edit, index|
110
- unless edit.is_a?(Hash)
111
- return validation_error("Edit at index #{index} must be a hash/object with old_string and new_string")
112
- end
113
-
114
- # Convert string keys to symbols for consistency
115
- edit = edit.transform_keys(&:to_sym)
116
-
117
- unless edit[:old_string]
118
- return validation_error("Edit at index #{index} missing required field 'old_string'")
119
- end
120
-
121
- unless edit[:new_string]
122
- return validation_error("Edit at index #{index} missing required field 'new_string'")
123
- end
124
-
125
- # old_string and new_string must be different
126
- if edit[:old_string] == edit[:new_string]
127
- return validation_error("Edit at index #{index}: old_string and new_string must be different")
128
- end
129
-
130
- validated_edits << {
131
- old_string: edit[:old_string].to_s,
132
- new_string: edit[:new_string].to_s,
133
- replace_all: edit[:replace_all] == true,
134
- index: index,
135
- }
136
- end
137
-
138
- # Apply edits sequentially
139
- results = []
140
- current_content = content
141
-
142
- validated_edits.each do |edit|
143
- # Check if old_string exists in current content
144
- unless current_content.include?(edit[:old_string])
145
- return error_with_results(
146
- <<~ERROR.chomp,
147
- Edit #{edit[:index]}: old_string not found in file.
148
- Make sure it matches exactly, including all whitespace and indentation.
149
- Do not include line number prefixes from Read tool output.
150
- Note: This edit follows #{edit[:index]} previous edit(s) which may have changed the file content.
151
- ERROR
152
- results,
153
- )
154
- end
155
-
156
- # Count occurrences
157
- occurrences = current_content.scan(edit[:old_string]).count
158
-
159
- # If not replace_all and multiple occurrences, error
160
- if !edit[:replace_all] && occurrences > 1
161
- return error_with_results(
162
- <<~ERROR.chomp,
163
- Edit #{edit[:index]}: Found #{occurrences} occurrences of old_string.
164
- Either provide more surrounding context to make the match unique, or set replace_all: true to replace all occurrences.
165
- ERROR
166
- results,
167
- )
168
- end
169
-
170
- # Perform replacement
171
- new_content = if edit[:replace_all]
172
- current_content.gsub(edit[:old_string], edit[:new_string])
173
- else
174
- current_content.sub(edit[:old_string], edit[:new_string])
175
- end
176
-
177
- # Record result
178
- replaced_count = edit[:replace_all] ? occurrences : 1
179
- results << {
180
- index: edit[:index],
181
- status: "success",
182
- occurrences: replaced_count,
183
- message: "Replaced #{replaced_count} occurrence(s)",
184
- }
185
-
186
- # Update content for next edit
187
- current_content = new_content
188
- end
189
-
190
- # Write back to file (use resolved path)
191
- File.write(resolved_path, current_content, encoding: "UTF-8")
192
-
193
- # Build success message
194
- total_replacements = results.sum { |r| r[:occurrences] }
195
- message = "Successfully applied #{validated_edits.size} edit(s) to #{file_path}\n"
196
- message += "Total replacements: #{total_replacements}\n\n"
197
- message += "Details:\n"
198
- results.each do |result|
199
- message += " Edit #{result[:index]}: #{result[:message]}\n"
200
- end
201
-
202
- message
203
- rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
204
- error("File contains invalid UTF-8. Cannot edit binary or improperly encoded files.")
205
- rescue Errno::EACCES
206
- error("Permission denied: Cannot read or write file '#{file_path}'")
207
- rescue StandardError => e
208
- error("Unexpected error editing file: #{e.class.name} - #{e.message}")
209
- end
210
-
211
- private
212
-
213
- # Format an error that includes partial results
214
- #
215
- # Shows what edits succeeded before the error occurred.
216
- #
217
- # @param message [String] Error description
218
- # @param results [Array<Hash>] Successful edit results before failure
219
- # @return [String] Formatted error message with results summary
220
- def error_with_results(message, results)
221
- output = "<tool_use_error>InputValidationError: #{message}\n\n"
222
-
223
- if results.any?
224
- output += "Previous successful edits before error:\n"
225
- results.each do |result|
226
- output += " Edit #{result[:index]}: #{result[:message]}\n"
227
- end
228
- output += "\n"
229
- end
230
-
231
- output += "Note: The file has NOT been modified. All or nothing approach - if any edit fails, no changes are saved.</tool_use_error>"
232
- output
233
- end
234
- end
235
- end
236
- end
@@ -1,92 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # Shared path resolution and agent context logic for file tools
6
- #
7
- # This module provides:
8
- # - Path resolution (relative to agent's working directory)
9
- # - Agent context initialization (agent_name, directory expansion)
10
- # - Standard error message formatting
11
- #
12
- # Tools resolve relative paths against the agent's directory.
13
- # Absolute paths are used as-is.
14
- #
15
- # @example
16
- # class Read < RubyLLM::Tool
17
- # include PathResolver
18
- #
19
- # def initialize(agent_name:, directory:)
20
- # super()
21
- # initialize_agent_context(agent_name: agent_name, directory: directory)
22
- # end
23
- #
24
- # def execute(file_path:)
25
- # resolved_path = resolve_path(file_path)
26
- # File.read(resolved_path)
27
- # rescue StandardError => e
28
- # error("Failed to read: #{e.message}")
29
- # end
30
- # end
31
- module PathResolver
32
- # Agent context attributes
33
- # @return [Symbol] The agent identifier
34
- attr_reader :agent_name
35
-
36
- # @return [String] Absolute path to agent's working directory
37
- attr_reader :directory
38
-
39
- private
40
-
41
- # Initialize agent context for file tools
42
- #
43
- # Sets up the common agent context needed by file tools:
44
- # - Normalizes agent_name to symbol
45
- # - Expands directory to absolute path
46
- #
47
- # @param agent_name [Symbol, String] The agent identifier
48
- # @param directory [String] Agent's working directory (will be expanded)
49
- # @return [void]
50
- def initialize_agent_context(agent_name:, directory:)
51
- @agent_name = agent_name.to_sym
52
- @directory = File.expand_path(directory)
53
- end
54
-
55
- # Resolve a path relative to the agent's directory
56
- #
57
- # - Absolute paths (starting with /) are returned as-is
58
- # - Relative paths are resolved against @directory
59
- #
60
- # @param path [String] Path to resolve (relative or absolute)
61
- # @return [String] Absolute path
62
- # @raise [RuntimeError] If @directory not set (developer error)
63
- def resolve_path(path)
64
- raise "PathResolver requires @directory to be set" unless @directory
65
-
66
- return path if path.to_s.start_with?("/")
67
-
68
- File.expand_path(path, @directory)
69
- end
70
-
71
- # Format a validation error response
72
- #
73
- # Used for input validation failures (missing required params, invalid formats, etc.)
74
- #
75
- # @param message [String] Error description
76
- # @return [String] Formatted error message wrapped in tool_use_error tags
77
- def validation_error(message)
78
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
79
- end
80
-
81
- # Format a general error response
82
- #
83
- # Used for runtime errors (permission denied, file not found, etc.)
84
- #
85
- # @param message [String] Error description
86
- # @return [String] Formatted error message prefixed with "Error:"
87
- def error(message)
88
- "Error: #{message}"
89
- end
90
- end
91
- end
92
- end