swarm_sdk 2.0.2 → 2.0.4
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/agent/builder.rb +17 -6
- data/lib/swarm_sdk/agent/definition.rb +16 -6
- data/lib/swarm_sdk/configuration.rb +8 -0
- data/lib/swarm_sdk/swarm/builder.rb +1 -1
- data/lib/swarm_sdk/swarm/tool_configurator.rb +40 -7
- data/lib/swarm_sdk/swarm.rb +7 -2
- data/lib/swarm_sdk/tools/registry.rb +3 -1
- data/lib/swarm_sdk/tools/scratchpad_edit.rb +143 -0
- data/lib/swarm_sdk/tools/{scratchpad_list.rb → scratchpad_glob.rb} +25 -21
- data/lib/swarm_sdk/tools/scratchpad_grep.rb +145 -0
- data/lib/swarm_sdk/tools/scratchpad_multi_edit.rb +226 -0
- data/lib/swarm_sdk/tools/scratchpad_read.rb +27 -6
- data/lib/swarm_sdk/tools/stores/scratchpad.rb +196 -33
- data/lib/swarm_sdk/tools/stores/scratchpad_read_tracker.rb +61 -0
- data/lib/swarm_sdk/tools/think.rb +95 -0
- data/lib/swarm_sdk/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2d1058470f6d37e95003807ae0ea2201aa587c8026417a5d8a793cbd59b80b46
|
4
|
+
data.tar.gz: 266b8559bf752f72a6db3a8b47fae0890d6aa34f3edf7d66a61e6a4cb9c28d6c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb6a9ccd6fa58cf55c062201f23391ebe10476894d5fc7f4838fbf90ade073d9e438257c0a007e20a636bba35764de8c434e381fdbc9114354e76c9d87252239
|
7
|
+
data.tar.gz: f281689946db6a04ab879f23fc922d7d06d3719a68f515e316b3497cc0ce0e075428623518e6f3ba9cee235f5b0389e2360406c49479a1b9e7c7a22ec533fcec
|
@@ -44,7 +44,7 @@ module SwarmSDK
|
|
44
44
|
@headers = {}
|
45
45
|
@timeout = nil
|
46
46
|
@mcp_servers = []
|
47
|
-
@
|
47
|
+
@disable_default_tools = nil # nil = include all default tools
|
48
48
|
@bypass_permissions = false
|
49
49
|
@coding_agent = nil # nil = not set (will default to false in Definition)
|
50
50
|
@assume_model_exists = nil
|
@@ -124,9 +124,19 @@ module SwarmSDK
|
|
124
124
|
@mcp_servers << server_config
|
125
125
|
end
|
126
126
|
|
127
|
-
#
|
128
|
-
|
129
|
-
|
127
|
+
# Disable default tools
|
128
|
+
#
|
129
|
+
# @param value [Boolean, Array<Symbol>]
|
130
|
+
# - true: Disable ALL default tools
|
131
|
+
# - Array of symbols: Disable specific tools (e.g., [:Think, :TodoWrite])
|
132
|
+
#
|
133
|
+
# @example Disable all default tools
|
134
|
+
# disable_default_tools true
|
135
|
+
#
|
136
|
+
# @example Disable specific tools
|
137
|
+
# disable_default_tools [:Think, :TodoWrite]
|
138
|
+
def disable_default_tools(value)
|
139
|
+
@disable_default_tools = value
|
130
140
|
end
|
131
141
|
|
132
142
|
# Set bypass_permissions flag
|
@@ -188,7 +198,8 @@ module SwarmSDK
|
|
188
198
|
def tools(*tool_names, include_default: true, replace: false)
|
189
199
|
@tools = Set.new if replace
|
190
200
|
@tools.merge(tool_names.map(&:to_sym))
|
191
|
-
|
201
|
+
# When include_default is false, disable all default tools
|
202
|
+
@disable_default_tools = true unless include_default
|
192
203
|
end
|
193
204
|
|
194
205
|
# Add tools from all_agents configuration
|
@@ -342,7 +353,7 @@ module SwarmSDK
|
|
342
353
|
agent_config[:headers] = @headers if @headers.any?
|
343
354
|
agent_config[:timeout] = @timeout if @timeout
|
344
355
|
agent_config[:mcp_servers] = @mcp_servers if @mcp_servers.any?
|
345
|
-
agent_config[:
|
356
|
+
agent_config[:disable_default_tools] = @disable_default_tools unless @disable_default_tools.nil?
|
346
357
|
agent_config[:bypass_permissions] = @bypass_permissions
|
347
358
|
agent_config[:coding_agent] = @coding_agent
|
348
359
|
agent_config[:assume_model_exists] = @assume_model_exists unless @assume_model_exists.nil?
|
@@ -38,7 +38,7 @@ module SwarmSDK
|
|
38
38
|
:parameters,
|
39
39
|
:headers,
|
40
40
|
:timeout,
|
41
|
-
:
|
41
|
+
:disable_default_tools,
|
42
42
|
:coding_agent,
|
43
43
|
:default_permissions,
|
44
44
|
:agent_permissions,
|
@@ -77,8 +77,11 @@ module SwarmSDK
|
|
77
77
|
# This prevents RubyLLM from trying to validate models in its registry
|
78
78
|
@assume_model_exists = true
|
79
79
|
|
80
|
-
#
|
81
|
-
|
80
|
+
# disable_default_tools can be:
|
81
|
+
# - nil/not set: include all default tools (default behavior)
|
82
|
+
# - true: disable ALL default tools
|
83
|
+
# - Array of symbols: disable specific tools (e.g., [:Think, :TodoWrite])
|
84
|
+
@disable_default_tools = config[:disable_default_tools]
|
82
85
|
|
83
86
|
# coding_agent defaults to false if not specified
|
84
87
|
# When true, includes the base system prompt for coding tasks
|
@@ -117,7 +120,7 @@ module SwarmSDK
|
|
117
120
|
{
|
118
121
|
name: @name,
|
119
122
|
description: @description,
|
120
|
-
model: @model,
|
123
|
+
model: SwarmSDK::Models.resolve_alias(@model), # Resolve model aliases
|
121
124
|
directory: @directory,
|
122
125
|
tools: @tools,
|
123
126
|
delegates_to: @delegates_to,
|
@@ -130,7 +133,7 @@ module SwarmSDK
|
|
130
133
|
headers: @headers,
|
131
134
|
timeout: @timeout,
|
132
135
|
bypass_permissions: @bypass_permissions,
|
133
|
-
|
136
|
+
disable_default_tools: @disable_default_tools,
|
134
137
|
coding_agent: @coding_agent,
|
135
138
|
assume_model_exists: @assume_model_exists,
|
136
139
|
max_concurrent_tools: @max_concurrent_tools,
|
@@ -224,7 +227,7 @@ module SwarmSDK
|
|
224
227
|
else
|
225
228
|
rendered_base
|
226
229
|
end
|
227
|
-
elsif
|
230
|
+
elsif default_tools_enabled?
|
228
231
|
# Non-coding agent: optionally include TODO/Scratchpad sections if default tools available
|
229
232
|
non_coding_base = render_non_coding_base_prompt
|
230
233
|
|
@@ -242,6 +245,13 @@ module SwarmSDK
|
|
242
245
|
end
|
243
246
|
end
|
244
247
|
|
248
|
+
# Check if default tools are enabled (i.e., not disabled)
|
249
|
+
#
|
250
|
+
# @return [Boolean] True if default tools should be included
|
251
|
+
def default_tools_enabled?
|
252
|
+
@disable_default_tools != true
|
253
|
+
end
|
254
|
+
|
245
255
|
def render_base_system_prompt
|
246
256
|
cwd = @directory || Dir.pwd
|
247
257
|
platform = RUBY_PLATFORM
|
@@ -116,6 +116,11 @@ module SwarmSDK
|
|
116
116
|
return unless @config[:swarm]
|
117
117
|
|
118
118
|
@all_agents_config = @config[:swarm][:all_agents] || {}
|
119
|
+
|
120
|
+
# Convert disable_default_tools array elements to symbols
|
121
|
+
if @all_agents_config[:disable_default_tools].is_a?(Array)
|
122
|
+
@all_agents_config[:disable_default_tools] = @all_agents_config[:disable_default_tools].map(&:to_sym)
|
123
|
+
end
|
119
124
|
end
|
120
125
|
|
121
126
|
def load_hooks_config
|
@@ -212,6 +217,9 @@ module SwarmSDK
|
|
212
217
|
# Merge headers: all_agents.headers + agent.headers
|
213
218
|
# Agent values override all_agents values for same keys
|
214
219
|
merged[:headers] = (merged[:headers] || {}).merge(value || {})
|
220
|
+
when :disable_default_tools
|
221
|
+
# Convert array elements to symbols if it's an array
|
222
|
+
merged[key] = value.is_a?(Array) ? value.map(&:to_sym) : value
|
215
223
|
else
|
216
224
|
# For everything else (model, provider, base_url, timeout, coding_agent, etc.),
|
217
225
|
# agent value overrides all_agents value
|
@@ -278,7 +278,7 @@ module SwarmSDK
|
|
278
278
|
# Don't apply assume_model_exists from markdown - let DSL overrides or auto-enable handle it
|
279
279
|
# builder.assume_model_exists(config[:assume_model_exists]) unless config[:assume_model_exists].nil?
|
280
280
|
builder.bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
|
281
|
-
builder.
|
281
|
+
builder.disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
|
282
282
|
|
283
283
|
# Add tools from markdown
|
284
284
|
if config[:tools]&.any?
|
@@ -12,7 +12,7 @@ module SwarmSDK
|
|
12
12
|
#
|
13
13
|
# This encapsulates all tool-related logic that was previously in Swarm.
|
14
14
|
class ToolConfigurator
|
15
|
-
# Default tools available to all agents (unless
|
15
|
+
# Default tools available to all agents (unless disable_default_tools is set)
|
16
16
|
DEFAULT_TOOLS = [
|
17
17
|
:Read,
|
18
18
|
:Grep,
|
@@ -20,7 +20,9 @@ module SwarmSDK
|
|
20
20
|
:TodoWrite,
|
21
21
|
:ScratchpadWrite,
|
22
22
|
:ScratchpadRead,
|
23
|
-
:
|
23
|
+
:ScratchpadGlob,
|
24
|
+
:ScratchpadGrep,
|
25
|
+
:Think,
|
24
26
|
].freeze
|
25
27
|
|
26
28
|
def initialize(swarm, scratchpad)
|
@@ -72,9 +74,17 @@ module SwarmSDK
|
|
72
74
|
when :ScratchpadWrite
|
73
75
|
Tools::ScratchpadWrite.create_for_scratchpad(@scratchpad)
|
74
76
|
when :ScratchpadRead
|
75
|
-
Tools::ScratchpadRead.create_for_scratchpad(@scratchpad)
|
76
|
-
when :
|
77
|
-
Tools::
|
77
|
+
Tools::ScratchpadRead.create_for_scratchpad(@scratchpad, agent_name)
|
78
|
+
when :ScratchpadEdit
|
79
|
+
Tools::ScratchpadEdit.create_for_scratchpad(@scratchpad, agent_name)
|
80
|
+
when :ScratchpadMultiEdit
|
81
|
+
Tools::ScratchpadMultiEdit.create_for_scratchpad(@scratchpad, agent_name)
|
82
|
+
when :ScratchpadGlob
|
83
|
+
Tools::ScratchpadGlob.create_for_scratchpad(@scratchpad)
|
84
|
+
when :ScratchpadGrep
|
85
|
+
Tools::ScratchpadGrep.create_for_scratchpad(@scratchpad)
|
86
|
+
when :Think
|
87
|
+
Tools::Think.new
|
78
88
|
else
|
79
89
|
# Regular tools - get class from registry and instantiate
|
80
90
|
tool_class = Tools::Registry.get(tool_name_sym)
|
@@ -133,13 +143,14 @@ module SwarmSDK
|
|
133
143
|
end
|
134
144
|
end
|
135
145
|
|
136
|
-
# Register default tools for agents
|
146
|
+
# Register default tools for agents (unless disabled)
|
137
147
|
#
|
138
148
|
# @param chat [AgentChat] The chat instance
|
139
149
|
# @param agent_name [Symbol] Agent name
|
140
150
|
# @param agent_definition [AgentDefinition] Agent definition
|
141
151
|
def register_default_tools(chat, agent_name:, agent_definition:)
|
142
|
-
|
152
|
+
# If disable_default_tools is true, skip all default tools
|
153
|
+
return if agent_definition.disable_default_tools == true
|
143
154
|
|
144
155
|
# Get explicit tool names to avoid duplicates
|
145
156
|
explicit_tool_names = agent_definition.tools.map { |t| t[:name] }.to_set
|
@@ -148,6 +159,9 @@ module SwarmSDK
|
|
148
159
|
# Skip if already registered explicitly
|
149
160
|
next if explicit_tool_names.include?(tool_name)
|
150
161
|
|
162
|
+
# Skip if tool is in the disable list
|
163
|
+
next if tool_disabled?(tool_name, agent_definition.disable_default_tools)
|
164
|
+
|
151
165
|
tool_instance = create_tool_instance(tool_name, agent_name, agent_definition.directory)
|
152
166
|
|
153
167
|
# Resolve permissions for default tool (same logic as AgentDefinition)
|
@@ -166,6 +180,25 @@ module SwarmSDK
|
|
166
180
|
end
|
167
181
|
end
|
168
182
|
|
183
|
+
# Check if a tool should be disabled based on disable_default_tools config
|
184
|
+
#
|
185
|
+
# @param tool_name [Symbol] Tool name to check
|
186
|
+
# @param disable_config [nil, Boolean, Array<Symbol>] Disable configuration
|
187
|
+
# @return [Boolean] True if tool should be disabled
|
188
|
+
def tool_disabled?(tool_name, disable_config)
|
189
|
+
return false if disable_config.nil?
|
190
|
+
|
191
|
+
if disable_config == true
|
192
|
+
# Disable all default tools
|
193
|
+
true
|
194
|
+
elsif disable_config.is_a?(Array)
|
195
|
+
# Disable only tools in the array
|
196
|
+
disable_config.include?(tool_name)
|
197
|
+
else
|
198
|
+
false
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
169
202
|
# Register agent delegation tools
|
170
203
|
#
|
171
204
|
# Creates delegation tools that allow one agent to call another.
|
data/lib/swarm_sdk/swarm.rb
CHANGED
@@ -129,7 +129,8 @@ module SwarmSDK
|
|
129
129
|
# @param name [String] Human-readable swarm name
|
130
130
|
# @param global_concurrency [Integer] Max concurrent LLM calls across entire swarm
|
131
131
|
# @param default_local_concurrency [Integer] Default max concurrent tool calls per agent
|
132
|
-
|
132
|
+
# @param scratchpad [Tools::Stores::Scratchpad, nil] Optional scratchpad instance (for testing)
|
133
|
+
def initialize(name:, global_concurrency: DEFAULT_GLOBAL_CONCURRENCY, default_local_concurrency: DEFAULT_LOCAL_CONCURRENCY, scratchpad: nil)
|
133
134
|
@name = name
|
134
135
|
@global_concurrency = global_concurrency
|
135
136
|
@default_local_concurrency = default_local_concurrency
|
@@ -138,7 +139,11 @@ module SwarmSDK
|
|
138
139
|
@global_semaphore = Async::Semaphore.new(@global_concurrency)
|
139
140
|
|
140
141
|
# Shared scratchpad for all agents
|
141
|
-
|
142
|
+
# Use provided scratchpad (for testing) or create persistent one
|
143
|
+
@scratchpad = scratchpad || begin
|
144
|
+
scratchpad_path = File.join(Dir.pwd, ".swarm", "scratchpad.json")
|
145
|
+
Tools::Stores::Scratchpad.new(persist_to: scratchpad_path)
|
146
|
+
end
|
142
147
|
|
143
148
|
# Hook registry for named hooks and swarm defaults
|
144
149
|
@hook_registry = Hooks::Registry.new
|
@@ -19,7 +19,9 @@ module SwarmSDK
|
|
19
19
|
TodoWrite: :special, # Requires agent context for todo tracking
|
20
20
|
ScratchpadWrite: :special, # Requires scratchpad instance
|
21
21
|
ScratchpadRead: :special, # Requires scratchpad instance
|
22
|
-
|
22
|
+
ScratchpadGlob: :special, # Requires scratchpad instance
|
23
|
+
ScratchpadGrep: :special, # Requires scratchpad instance
|
24
|
+
Think: SwarmSDK::Tools::Think,
|
23
25
|
}.freeze
|
24
26
|
|
25
27
|
class << self
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwarmSDK
|
4
|
+
module Tools
|
5
|
+
# Tool for editing scratchpad entries with exact string replacement
|
6
|
+
#
|
7
|
+
# Performs exact string replacements in scratchpad content.
|
8
|
+
# All agents in the swarm share the same scratchpad.
|
9
|
+
class ScratchpadEdit < RubyLLM::Tool
|
10
|
+
define_method(:name) { "ScratchpadEdit" }
|
11
|
+
|
12
|
+
description <<~DESC
|
13
|
+
Performs exact string replacements in scratchpad entries.
|
14
|
+
Works like the Edit tool but operates on scratchpad content instead of files.
|
15
|
+
You must use ScratchpadRead on the entry before editing it.
|
16
|
+
When editing text from ScratchpadRead output, ensure you preserve the exact indentation as it appears AFTER the line number prefix.
|
17
|
+
The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual content to match.
|
18
|
+
Never include any part of the line number prefix in the old_string or new_string.
|
19
|
+
The edit will FAIL if old_string is not unique in the entry. Either provide a larger string with more surrounding context to make it unique or use replace_all to change every instance of old_string.
|
20
|
+
Use replace_all for replacing and renaming strings across the entry.
|
21
|
+
DESC
|
22
|
+
|
23
|
+
param :file_path,
|
24
|
+
desc: "Path to the scratchpad entry (e.g., 'analysis/report', 'parallel/batch1/task_0')",
|
25
|
+
required: true
|
26
|
+
|
27
|
+
param :old_string,
|
28
|
+
desc: "The exact text to replace (must match exactly including whitespace)",
|
29
|
+
required: true
|
30
|
+
|
31
|
+
param :new_string,
|
32
|
+
desc: "The text to replace it with (must be different from old_string)",
|
33
|
+
required: true
|
34
|
+
|
35
|
+
param :replace_all,
|
36
|
+
desc: "Replace all occurrences of old_string (default false)",
|
37
|
+
required: false
|
38
|
+
|
39
|
+
class << self
|
40
|
+
# Create a ScratchpadEdit tool for a specific scratchpad instance
|
41
|
+
#
|
42
|
+
# @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
|
43
|
+
# @param agent_name [Symbol, String] Agent identifier for tracking reads
|
44
|
+
# @return [ScratchpadEdit] Tool instance
|
45
|
+
def create_for_scratchpad(scratchpad, agent_name)
|
46
|
+
new(scratchpad, agent_name)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Initialize with scratchpad instance and agent name
|
51
|
+
#
|
52
|
+
# @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
|
53
|
+
# @param agent_name [Symbol, String] Agent identifier
|
54
|
+
def initialize(scratchpad, agent_name)
|
55
|
+
super() # Call RubyLLM::Tool's initialize
|
56
|
+
@scratchpad = scratchpad
|
57
|
+
@agent_name = agent_name.to_sym
|
58
|
+
end
|
59
|
+
|
60
|
+
# Execute the tool
|
61
|
+
#
|
62
|
+
# @param file_path [String] Path to scratchpad entry
|
63
|
+
# @param old_string [String] Text to replace
|
64
|
+
# @param new_string [String] Replacement text
|
65
|
+
# @param replace_all [Boolean] Replace all occurrences
|
66
|
+
# @return [String] Success message or error
|
67
|
+
def execute(file_path:, old_string:, new_string:, replace_all: false)
|
68
|
+
# Validate inputs
|
69
|
+
return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
|
70
|
+
return validation_error("old_string is required") if old_string.nil? || old_string.empty?
|
71
|
+
return validation_error("new_string is required") if new_string.nil?
|
72
|
+
|
73
|
+
# old_string and new_string must be different
|
74
|
+
if old_string == new_string
|
75
|
+
return validation_error("old_string and new_string must be different. They are currently identical.")
|
76
|
+
end
|
77
|
+
|
78
|
+
# Read current content (this will raise ArgumentError if entry doesn't exist)
|
79
|
+
content = scratchpad.read(file_path: file_path)
|
80
|
+
|
81
|
+
# Enforce read-before-edit
|
82
|
+
unless Stores::ScratchpadReadTracker.entry_read?(@agent_name, file_path)
|
83
|
+
return validation_error(
|
84
|
+
"Cannot edit scratchpad entry without reading it first. " \
|
85
|
+
"You must use ScratchpadRead on 'scratchpad://#{file_path}' before editing it. " \
|
86
|
+
"This ensures you have the current content to match against.",
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Check if old_string exists in content
|
91
|
+
unless content.include?(old_string)
|
92
|
+
return validation_error(<<~ERROR.chomp)
|
93
|
+
old_string not found in scratchpad entry. Make sure it matches exactly, including all whitespace and indentation.
|
94
|
+
Do not include line number prefixes from ScratchpadRead tool output.
|
95
|
+
ERROR
|
96
|
+
end
|
97
|
+
|
98
|
+
# Count occurrences
|
99
|
+
occurrences = content.scan(old_string).count
|
100
|
+
|
101
|
+
# If not replace_all and multiple occurrences, error
|
102
|
+
if !replace_all && occurrences > 1
|
103
|
+
return validation_error(<<~ERROR.chomp)
|
104
|
+
Found #{occurrences} occurrences of old_string.
|
105
|
+
Either provide more surrounding context to make the match unique, or use replace_all: true to replace all occurrences.
|
106
|
+
ERROR
|
107
|
+
end
|
108
|
+
|
109
|
+
# Perform replacement
|
110
|
+
new_content = if replace_all
|
111
|
+
content.gsub(old_string, new_string)
|
112
|
+
else
|
113
|
+
content.sub(old_string, new_string)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get existing entry metadata
|
117
|
+
entries = scratchpad.list
|
118
|
+
existing_entry = entries.find { |e| e[:path] == file_path }
|
119
|
+
|
120
|
+
# Write updated content back (preserving the title)
|
121
|
+
scratchpad.write(
|
122
|
+
file_path: file_path,
|
123
|
+
content: new_content,
|
124
|
+
title: existing_entry[:title],
|
125
|
+
)
|
126
|
+
|
127
|
+
# Build success message
|
128
|
+
replaced_count = replace_all ? occurrences : 1
|
129
|
+
"Successfully replaced #{replaced_count} occurrence(s) in scratchpad://#{file_path}"
|
130
|
+
rescue ArgumentError => e
|
131
|
+
validation_error(e.message)
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
attr_reader :scratchpad
|
137
|
+
|
138
|
+
def validation_error(message)
|
139
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -2,28 +2,34 @@
|
|
2
2
|
|
3
3
|
module SwarmSDK
|
4
4
|
module Tools
|
5
|
-
# Tool for
|
5
|
+
# Tool for searching scratchpad entries by glob pattern
|
6
6
|
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
class
|
10
|
-
define_method(:name) { "
|
7
|
+
# Finds scratchpad entries matching a glob pattern (like filesystem glob).
|
8
|
+
# All agents in the swarm share the same scratchpad.
|
9
|
+
class ScratchpadGlob < RubyLLM::Tool
|
10
|
+
define_method(:name) { "ScratchpadGlob" }
|
11
11
|
|
12
12
|
description <<~DESC
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
Search scratchpad entries by glob pattern.
|
14
|
+
Works like filesystem glob - use * for wildcards, ** for recursive matching.
|
15
|
+
Use this to discover entries matching specific patterns.
|
16
|
+
|
17
|
+
Examples:
|
18
|
+
- "parallel/*" - all entries directly under parallel/
|
19
|
+
- "parallel/**" - all entries under parallel/ (recursive)
|
20
|
+
- "*/report" - all entries named "report" in any top-level directory
|
21
|
+
- "analysis/*/result_*" - entries like "analysis/foo/result_1"
|
16
22
|
DESC
|
17
23
|
|
18
|
-
param :
|
19
|
-
desc: "
|
20
|
-
required:
|
24
|
+
param :pattern,
|
25
|
+
desc: "Glob pattern to match (e.g., '**/*.txt', 'parallel/*/task_*')",
|
26
|
+
required: true
|
21
27
|
|
22
28
|
class << self
|
23
|
-
# Create a
|
29
|
+
# Create a ScratchpadGlob tool for a specific scratchpad instance
|
24
30
|
#
|
25
31
|
# @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
|
26
|
-
# @return [
|
32
|
+
# @return [ScratchpadGlob] Tool instance
|
27
33
|
def create_for_scratchpad(scratchpad)
|
28
34
|
new(scratchpad)
|
29
35
|
end
|
@@ -39,19 +45,17 @@ module SwarmSDK
|
|
39
45
|
|
40
46
|
# Execute the tool
|
41
47
|
#
|
42
|
-
# @param
|
43
|
-
# @return [String] Formatted list of entries
|
44
|
-
def execute(
|
45
|
-
entries = scratchpad.
|
48
|
+
# @param pattern [String] Glob pattern to match
|
49
|
+
# @return [String] Formatted list of matching entries
|
50
|
+
def execute(pattern:)
|
51
|
+
entries = scratchpad.glob(pattern: pattern)
|
46
52
|
|
47
53
|
if entries.empty?
|
48
|
-
return "
|
49
|
-
|
50
|
-
return "No entries found with prefix '#{prefix}'"
|
54
|
+
return "No entries found matching pattern '#{pattern}'"
|
51
55
|
end
|
52
56
|
|
53
57
|
result = []
|
54
|
-
result << "Scratchpad
|
58
|
+
result << "Scratchpad entries matching '#{pattern}' (#{entries.size} #{entries.size == 1 ? "entry" : "entries"}):"
|
55
59
|
|
56
60
|
entries.each do |entry|
|
57
61
|
result << " scratchpad://#{entry[:path]} - \"#{entry[:title]}\" (#{format_bytes(entry[:size])})"
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwarmSDK
|
4
|
+
module Tools
|
5
|
+
# Tool for searching scratchpad content by pattern
|
6
|
+
#
|
7
|
+
# Searches content stored in scratchpad entries using regex patterns.
|
8
|
+
# All agents in the swarm share the same scratchpad.
|
9
|
+
class ScratchpadGrep < RubyLLM::Tool
|
10
|
+
define_method(:name) { "ScratchpadGrep" }
|
11
|
+
|
12
|
+
description <<~DESC
|
13
|
+
Search scratchpad content by pattern (like grep).
|
14
|
+
Use regex patterns to search content within scratchpad entries.
|
15
|
+
Returns matching entries and optionally line numbers and content.
|
16
|
+
|
17
|
+
Output modes:
|
18
|
+
- files_with_matches: Only list paths containing matches (default)
|
19
|
+
- content: Show matching lines with line numbers
|
20
|
+
- count: Show number of matches per file
|
21
|
+
DESC
|
22
|
+
|
23
|
+
param :pattern,
|
24
|
+
desc: "Regular expression pattern to search for",
|
25
|
+
required: true
|
26
|
+
|
27
|
+
param :case_insensitive,
|
28
|
+
desc: "Perform case-insensitive search (default: false)",
|
29
|
+
required: false
|
30
|
+
|
31
|
+
param :output_mode,
|
32
|
+
desc: "Output mode: 'files_with_matches' (default), 'content', or 'count'",
|
33
|
+
required: false
|
34
|
+
|
35
|
+
class << self
|
36
|
+
# Create a ScratchpadGrep tool for a specific scratchpad instance
|
37
|
+
#
|
38
|
+
# @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
|
39
|
+
# @return [ScratchpadGrep] Tool instance
|
40
|
+
def create_for_scratchpad(scratchpad)
|
41
|
+
new(scratchpad)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Initialize with scratchpad instance
|
46
|
+
#
|
47
|
+
# @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
|
48
|
+
def initialize(scratchpad)
|
49
|
+
super() # Call RubyLLM::Tool's initialize
|
50
|
+
@scratchpad = scratchpad
|
51
|
+
end
|
52
|
+
|
53
|
+
# Execute the tool
|
54
|
+
#
|
55
|
+
# @param pattern [String] Regex pattern to search for
|
56
|
+
# @param case_insensitive [Boolean] Whether to perform case-insensitive search
|
57
|
+
# @param output_mode [String] Output mode
|
58
|
+
# @return [String] Formatted search results
|
59
|
+
def execute(pattern:, case_insensitive: false, output_mode: "files_with_matches")
|
60
|
+
results = scratchpad.grep(
|
61
|
+
pattern: pattern,
|
62
|
+
case_insensitive: case_insensitive,
|
63
|
+
output_mode: output_mode,
|
64
|
+
)
|
65
|
+
|
66
|
+
format_results(results, pattern, output_mode)
|
67
|
+
rescue ArgumentError => e
|
68
|
+
validation_error(e.message)
|
69
|
+
rescue RegexpError => e
|
70
|
+
validation_error("Invalid regex pattern: #{e.message}")
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
attr_reader :scratchpad
|
76
|
+
|
77
|
+
def validation_error(message)
|
78
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
79
|
+
end
|
80
|
+
|
81
|
+
def format_results(results, pattern, output_mode)
|
82
|
+
case output_mode
|
83
|
+
when "files_with_matches"
|
84
|
+
format_files_with_matches(results, pattern)
|
85
|
+
when "content"
|
86
|
+
format_content(results, pattern)
|
87
|
+
when "count"
|
88
|
+
format_count(results, pattern)
|
89
|
+
else
|
90
|
+
validation_error("Invalid output_mode: #{output_mode}")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def format_files_with_matches(paths, pattern)
|
95
|
+
if paths.empty?
|
96
|
+
return "No matches found for pattern '#{pattern}'"
|
97
|
+
end
|
98
|
+
|
99
|
+
result = []
|
100
|
+
result << "Scratchpad entries matching '#{pattern}' (#{paths.size} #{paths.size == 1 ? "entry" : "entries"}):"
|
101
|
+
paths.each do |path|
|
102
|
+
result << " scratchpad://#{path}"
|
103
|
+
end
|
104
|
+
result.join("\n")
|
105
|
+
end
|
106
|
+
|
107
|
+
def format_content(results, pattern)
|
108
|
+
if results.empty?
|
109
|
+
return "No matches found for pattern '#{pattern}'"
|
110
|
+
end
|
111
|
+
|
112
|
+
total_matches = results.sum { |r| r[:matches].size }
|
113
|
+
output = []
|
114
|
+
output << "Scratchpad entries matching '#{pattern}' (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} #{total_matches == 1 ? "match" : "matches"}):"
|
115
|
+
output << ""
|
116
|
+
|
117
|
+
results.each do |result|
|
118
|
+
output << "scratchpad://#{result[:path]}:"
|
119
|
+
result[:matches].each do |match|
|
120
|
+
output << " #{match[:line_number]}: #{match[:content]}"
|
121
|
+
end
|
122
|
+
output << ""
|
123
|
+
end
|
124
|
+
|
125
|
+
output.join("\n").rstrip
|
126
|
+
end
|
127
|
+
|
128
|
+
def format_count(results, pattern)
|
129
|
+
if results.empty?
|
130
|
+
return "No matches found for pattern '#{pattern}'"
|
131
|
+
end
|
132
|
+
|
133
|
+
total_matches = results.sum { |r| r[:count] }
|
134
|
+
output = []
|
135
|
+
output << "Scratchpad entries matching '#{pattern}' (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} total #{total_matches == 1 ? "match" : "matches"}):"
|
136
|
+
|
137
|
+
results.each do |result|
|
138
|
+
output << " scratchpad://#{result[:path]}: #{result[:count]} #{result[:count] == 1 ? "match" : "matches"}"
|
139
|
+
end
|
140
|
+
|
141
|
+
output.join("\n")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|