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.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +43 -22
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +6 -0
- data/lib/swarm_sdk/ruby_llm_patches/mcp_ssl_patch.rb +144 -0
- data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +3 -4
- data/lib/swarm_sdk/v3/agent.rb +1165 -0
- data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
- data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
- data/lib/swarm_sdk/v3/configuration.rb +490 -0
- data/lib/swarm_sdk/v3/debug_log.rb +86 -0
- data/lib/swarm_sdk/v3/event_stream.rb +130 -0
- data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
- data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
- data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
- data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
- data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
- data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
- data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
- data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
- data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
- data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
- data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
- data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
- data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
- data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
- data/lib/swarm_sdk/v3/memory/card.rb +206 -0
- data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
- data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
- data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
- data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
- data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
- data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
- data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
- data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
- data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
- data/lib/swarm_sdk/v3/memory/store.rb +489 -0
- data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
- data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
- data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
- data/lib/swarm_sdk/v3/tools/base.rb +80 -0
- data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
- data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
- data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
- data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
- data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
- data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
- data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
- data/lib/swarm_sdk/v3/tools/read.rb +181 -0
- data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
- data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
- data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
- data/lib/swarm_sdk/v3/tools/think.rb +88 -0
- data/lib/swarm_sdk/v3/tools/write.rb +87 -0
- data/lib/swarm_sdk/v3.rb +145 -0
- metadata +84 -148
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
- data/lib/swarm_sdk/agent/builder.rb +0 -680
- data/lib/swarm_sdk/agent/chat.rb +0 -1432
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
- data/lib/swarm_sdk/agent/context.rb +0 -115
- data/lib/swarm_sdk/agent/context_manager.rb +0 -315
- data/lib/swarm_sdk/agent/definition.rb +0 -581
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
- data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
- data/lib/swarm_sdk/agent_registry.rb +0 -146
- data/lib/swarm_sdk/builders/base_builder.rb +0 -553
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
- data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
- data/lib/swarm_sdk/concerns/validatable.rb +0 -55
- data/lib/swarm_sdk/config.rb +0 -367
- data/lib/swarm_sdk/configuration/parser.rb +0 -397
- data/lib/swarm_sdk/configuration/translator.rb +0 -283
- data/lib/swarm_sdk/configuration.rb +0 -165
- data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
- data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
- data/lib/swarm_sdk/context_compactor.rb +0 -335
- data/lib/swarm_sdk/context_management/builder.rb +0 -128
- data/lib/swarm_sdk/context_management/context.rb +0 -328
- data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
- data/lib/swarm_sdk/defaults.rb +0 -251
- data/lib/swarm_sdk/events_to_messages.rb +0 -199
- data/lib/swarm_sdk/hooks/adapter.rb +0 -359
- data/lib/swarm_sdk/hooks/context.rb +0 -197
- data/lib/swarm_sdk/hooks/definition.rb +0 -80
- data/lib/swarm_sdk/hooks/error.rb +0 -29
- data/lib/swarm_sdk/hooks/executor.rb +0 -146
- data/lib/swarm_sdk/hooks/registry.rb +0 -147
- data/lib/swarm_sdk/hooks/result.rb +0 -150
- data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
- data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
- data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
- data/lib/swarm_sdk/log_collector.rb +0 -227
- data/lib/swarm_sdk/log_stream.rb +0 -127
- data/lib/swarm_sdk/markdown_parser.rb +0 -75
- data/lib/swarm_sdk/model_aliases.json +0 -8
- data/lib/swarm_sdk/models.json +0 -44002
- data/lib/swarm_sdk/models.rb +0 -161
- data/lib/swarm_sdk/node_context.rb +0 -245
- data/lib/swarm_sdk/observer/builder.rb +0 -81
- data/lib/swarm_sdk/observer/config.rb +0 -45
- data/lib/swarm_sdk/observer/manager.rb +0 -236
- data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
- data/lib/swarm_sdk/permissions/config.rb +0 -239
- data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
- data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
- data/lib/swarm_sdk/permissions/validator.rb +0 -173
- data/lib/swarm_sdk/permissions_builder.rb +0 -122
- data/lib/swarm_sdk/plugin.rb +0 -309
- data/lib/swarm_sdk/plugin_registry.rb +0 -101
- data/lib/swarm_sdk/proc_helpers.rb +0 -53
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -212
- data/lib/swarm_sdk/snapshot.rb +0 -156
- data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
- data/lib/swarm_sdk/state_restorer.rb +0 -476
- data/lib/swarm_sdk/state_snapshot.rb +0 -334
- data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -195
- data/lib/swarm_sdk/swarm/builder.rb +0 -256
- data/lib/swarm_sdk/swarm/executor.rb +0 -290
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -151
- data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -360
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -270
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
- data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
- data/lib/swarm_sdk/swarm.rb +0 -843
- data/lib/swarm_sdk/swarm_loader.rb +0 -145
- data/lib/swarm_sdk/swarm_registry.rb +0 -136
- data/lib/swarm_sdk/tools/base.rb +0 -63
- data/lib/swarm_sdk/tools/bash.rb +0 -280
- data/lib/swarm_sdk/tools/clock.rb +0 -46
- data/lib/swarm_sdk/tools/delegate.rb +0 -389
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
- data/lib/swarm_sdk/tools/edit.rb +0 -145
- data/lib/swarm_sdk/tools/glob.rb +0 -166
- data/lib/swarm_sdk/tools/grep.rb +0 -235
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
- data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
- data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
- data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
- data/lib/swarm_sdk/tools/read.rb +0 -261
- data/lib/swarm_sdk/tools/registry.rb +0 -205
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
- data/lib/swarm_sdk/tools/think.rb +0 -100
- data/lib/swarm_sdk/tools/todo_write.rb +0 -237
- data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
- data/lib/swarm_sdk/tools/write.rb +0 -112
- data/lib/swarm_sdk/transcript_builder.rb +0 -278
- data/lib/swarm_sdk/utils.rb +0 -68
- data/lib/swarm_sdk/validation_result.rb +0 -33
- data/lib/swarm_sdk/version.rb +0 -5
- data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
- data/lib/swarm_sdk/workflow/builder.rb +0 -227
- data/lib/swarm_sdk/workflow/executor.rb +0 -497
- data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
- data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
- data/lib/swarm_sdk/workflow.rb +0 -589
- data/lib/swarm_sdk.rb +0 -718
|
@@ -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
|