swarm_sdk 2.2.0 → 2.4.0
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 +58 -0
- data/lib/swarm_sdk/agent/chat.rb +527 -1059
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +262 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +11 -13
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
- data/lib/swarm_sdk/agent/context.rb +1 -2
- data/lib/swarm_sdk/agent/definition.rb +66 -154
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
- data/lib/swarm_sdk/builders/base_builder.rb +409 -0
- data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
- data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
- data/lib/swarm_sdk/concerns/validatable.rb +55 -0
- data/lib/swarm_sdk/config.rb +301 -0
- data/lib/swarm_sdk/configuration/parser.rb +353 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +65 -543
- data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
- data/lib/swarm_sdk/context_compactor.rb +6 -11
- data/lib/swarm_sdk/context_management/builder.rb +128 -0
- data/lib/swarm_sdk/context_management/context.rb +328 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +18 -0
- data/lib/swarm_sdk/hooks/adapter.rb +3 -3
- data/lib/swarm_sdk/hooks/shell_executor.rb +4 -2
- data/lib/swarm_sdk/log_collector.rb +179 -29
- data/lib/swarm_sdk/log_stream.rb +29 -0
- data/lib/swarm_sdk/models.json +4333 -1
- data/lib/swarm_sdk/node_context.rb +1 -1
- data/lib/swarm_sdk/observer/builder.rb +81 -0
- data/lib/swarm_sdk/observer/config.rb +45 -0
- data/lib/swarm_sdk/observer/manager.rb +236 -0
- data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
- data/lib/swarm_sdk/plugin.rb +93 -3
- data/lib/swarm_sdk/snapshot.rb +6 -6
- data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
- data/lib/swarm_sdk/state_restorer.rb +136 -151
- data/lib/swarm_sdk/state_snapshot.rb +65 -100
- data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
- data/lib/swarm_sdk/swarm/builder.rb +44 -578
- data/lib/swarm_sdk/swarm/executor.rb +213 -0
- data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/tool_configurator.rb +44 -140
- data/lib/swarm_sdk/swarm.rb +146 -689
- data/lib/swarm_sdk/tools/bash.rb +14 -8
- data/lib/swarm_sdk/tools/delegate.rb +61 -43
- data/lib/swarm_sdk/tools/edit.rb +8 -13
- data/lib/swarm_sdk/tools/glob.rb +12 -4
- data/lib/swarm_sdk/tools/grep.rb +7 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
- data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
- data/lib/swarm_sdk/tools/read.rb +16 -18
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +9 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/todo_write.rb +7 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +20 -17
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
- data/lib/swarm_sdk/workflow/builder.rb +143 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +7 -5
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +5 -3
- data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
- data/lib/swarm_sdk.rb +64 -104
- metadata +68 -15
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
data/lib/swarm_sdk/tools/bash.rb
CHANGED
|
@@ -7,6 +7,13 @@ module SwarmSDK
|
|
|
7
7
|
# Executes commands in a persistent shell session with timeout support.
|
|
8
8
|
# Provides comprehensive guidance on proper usage patterns.
|
|
9
9
|
class Bash < RubyLLM::Tool
|
|
10
|
+
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
11
|
+
class << self
|
|
12
|
+
def creation_requirements
|
|
13
|
+
[:directory]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
10
17
|
def initialize(directory:)
|
|
11
18
|
super()
|
|
12
19
|
@directory = File.expand_path(directory)
|
|
@@ -78,9 +85,7 @@ module SwarmSDK
|
|
|
78
85
|
desc: "Optional timeout in milliseconds (max 600000)",
|
|
79
86
|
required: false
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
MAX_TIMEOUT_MS = 600_000 # 10 minutes
|
|
83
|
-
MAX_OUTPUT_LENGTH = 30_000 # characters
|
|
88
|
+
# NOTE: Timeout and output limits now accessed via SwarmSDK.config
|
|
84
89
|
|
|
85
90
|
# Commands that are ALWAYS blocked for safety reasons
|
|
86
91
|
# These cannot be overridden by permissions configuration
|
|
@@ -99,8 +104,8 @@ module SwarmSDK
|
|
|
99
104
|
end
|
|
100
105
|
|
|
101
106
|
# Validate and set timeout
|
|
102
|
-
timeout_ms = timeout ||
|
|
103
|
-
timeout_ms = [timeout_ms,
|
|
107
|
+
timeout_ms = timeout || SwarmSDK.config.bash_command_timeout
|
|
108
|
+
timeout_ms = [timeout_ms, SwarmSDK.config.bash_command_max_timeout].min
|
|
104
109
|
timeout_seconds = timeout_ms / 1000.0
|
|
105
110
|
|
|
106
111
|
# Execute command with timeout
|
|
@@ -141,9 +146,10 @@ module SwarmSDK
|
|
|
141
146
|
output = format_command_output(command, description, stdout, stderr, exit_status)
|
|
142
147
|
|
|
143
148
|
# Truncate if too long
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
truncated
|
|
149
|
+
max_output = SwarmSDK.config.output_character_limit
|
|
150
|
+
if output.length > max_output
|
|
151
|
+
truncated = output[0...max_output]
|
|
152
|
+
truncated += "\n\n<system-reminder>Output truncated at #{max_output} characters. The full output was #{output.length} characters.</system-reminder>"
|
|
147
153
|
output = truncated
|
|
148
154
|
end
|
|
149
155
|
|
|
@@ -2,12 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
module SwarmSDK
|
|
4
4
|
module Tools
|
|
5
|
-
# Delegate tool for
|
|
5
|
+
# Delegate tool for working with other agents in the swarm
|
|
6
6
|
#
|
|
7
|
-
# Creates agent-specific
|
|
8
|
-
# that allow one agent to
|
|
7
|
+
# Creates agent-specific collaboration tools (e.g., WorkWithBackend)
|
|
8
|
+
# that allow one agent to work with another agent.
|
|
9
9
|
# Supports pre/post delegation hooks for customization.
|
|
10
10
|
class Delegate < RubyLLM::Tool
|
|
11
|
+
# Tool name prefix for delegation tools
|
|
12
|
+
# Change this to customize the tool naming pattern (e.g., "DelegateTaskTo", "AskAgent", etc.)
|
|
13
|
+
TOOL_NAME_PREFIX = "WorkWith"
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Generate tool name for a delegate agent
|
|
17
|
+
#
|
|
18
|
+
# This is the single source of truth for delegation tool naming.
|
|
19
|
+
# Used both when creating Delegate instances and when predicting tool names
|
|
20
|
+
# for agent context setup.
|
|
21
|
+
#
|
|
22
|
+
# @param delegate_name [String, Symbol] Name of the delegate agent
|
|
23
|
+
# @return [String] Tool name (e.g., "WorkWithBackend")
|
|
24
|
+
def tool_name_for(delegate_name)
|
|
25
|
+
"#{TOOL_NAME_PREFIX}#{delegate_name.to_s.capitalize}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
11
29
|
attr_reader :delegate_name, :delegate_target, :tool_name
|
|
12
30
|
|
|
13
31
|
# Initialize a delegation tool
|
|
@@ -16,10 +34,7 @@ module SwarmSDK
|
|
|
16
34
|
# @param delegate_description [String] Description of the delegate agent
|
|
17
35
|
# @param delegate_chat [AgentChat, nil] The chat instance for the delegate agent (nil if delegating to swarm)
|
|
18
36
|
# @param agent_name [Symbol, String] Name of the agent using this tool
|
|
19
|
-
# @param swarm [Swarm] The swarm instance
|
|
20
|
-
# @param hook_registry [Hooks::Registry] Registry for callbacks
|
|
21
|
-
# @param call_stack [Array] Delegation call stack for circular dependency detection
|
|
22
|
-
# @param swarm_registry [SwarmRegistry, nil] Registry for sub-swarms (nil if not using composable swarms)
|
|
37
|
+
# @param swarm [Swarm] The swarm instance (provides hook_registry, delegation_call_stack, swarm_registry)
|
|
23
38
|
# @param delegating_chat [Agent::Chat, nil] The chat instance of the agent doing the delegating (for accessing hooks)
|
|
24
39
|
def initialize(
|
|
25
40
|
delegate_name:,
|
|
@@ -27,9 +42,6 @@ module SwarmSDK
|
|
|
27
42
|
delegate_chat:,
|
|
28
43
|
agent_name:,
|
|
29
44
|
swarm:,
|
|
30
|
-
hook_registry:,
|
|
31
|
-
call_stack:,
|
|
32
|
-
swarm_registry: nil,
|
|
33
45
|
delegating_chat: nil
|
|
34
46
|
)
|
|
35
47
|
super()
|
|
@@ -39,24 +51,21 @@ module SwarmSDK
|
|
|
39
51
|
@delegate_chat = delegate_chat
|
|
40
52
|
@agent_name = agent_name
|
|
41
53
|
@swarm = swarm
|
|
42
|
-
@hook_registry = hook_registry
|
|
43
|
-
@call_stack = call_stack
|
|
44
|
-
@swarm_registry = swarm_registry
|
|
45
54
|
@delegating_chat = delegating_chat
|
|
46
55
|
|
|
47
|
-
# Generate tool name
|
|
48
|
-
@tool_name =
|
|
56
|
+
# Generate tool name using canonical method
|
|
57
|
+
@tool_name = self.class.tool_name_for(delegate_name)
|
|
49
58
|
@delegate_target = delegate_name.to_s
|
|
50
59
|
end
|
|
51
60
|
|
|
52
61
|
# Override description to return dynamic string based on delegate
|
|
53
62
|
def description
|
|
54
|
-
"
|
|
63
|
+
"Work with #{@delegate_name} to delegate work, ask questions, or collaborate. #{@delegate_description}"
|
|
55
64
|
end
|
|
56
65
|
|
|
57
|
-
param :
|
|
66
|
+
param :message,
|
|
58
67
|
type: "string",
|
|
59
|
-
desc: "
|
|
68
|
+
desc: "Message to send to the agent - can be a work request, question, or collaboration message",
|
|
60
69
|
required: true
|
|
61
70
|
|
|
62
71
|
# Override name to return custom delegation tool name
|
|
@@ -66,13 +75,18 @@ module SwarmSDK
|
|
|
66
75
|
|
|
67
76
|
# Execute delegation with pre/post hooks
|
|
68
77
|
#
|
|
69
|
-
# @param
|
|
78
|
+
# @param message [String] Message to send to the agent
|
|
70
79
|
# @return [String] Result from delegate agent or error message
|
|
71
|
-
def execute(
|
|
80
|
+
def execute(message:)
|
|
81
|
+
# Access swarm infrastructure
|
|
82
|
+
call_stack = @swarm.delegation_call_stack
|
|
83
|
+
hook_registry = @swarm.hook_registry
|
|
84
|
+
swarm_registry = @swarm.swarm_registry
|
|
85
|
+
|
|
72
86
|
# Check for circular dependency
|
|
73
|
-
if
|
|
74
|
-
emit_circular_warning
|
|
75
|
-
return "Error: Circular delegation detected: #{
|
|
87
|
+
if call_stack.include?(@delegate_target)
|
|
88
|
+
emit_circular_warning(call_stack)
|
|
89
|
+
return "Error: Circular delegation detected: #{call_stack.join(" -> ")} -> #{@delegate_target}. " \
|
|
76
90
|
"Please restructure your delegation to avoid infinite loops."
|
|
77
91
|
end
|
|
78
92
|
|
|
@@ -91,12 +105,12 @@ module SwarmSDK
|
|
|
91
105
|
delegation_target: @delegate_target,
|
|
92
106
|
metadata: {
|
|
93
107
|
tool_name: @tool_name,
|
|
94
|
-
|
|
108
|
+
message: message,
|
|
95
109
|
timestamp: Time.now.utc.iso8601,
|
|
96
110
|
},
|
|
97
111
|
)
|
|
98
112
|
|
|
99
|
-
executor = Hooks::Executor.new(
|
|
113
|
+
executor = Hooks::Executor.new(hook_registry, logger: RubyLLM.logger)
|
|
100
114
|
pre_agent_hooks = agent_hooks[:pre_delegation] || []
|
|
101
115
|
result = executor.execute_safe(event: :pre_delegation, context: context, callbacks: pre_agent_hooks)
|
|
102
116
|
|
|
@@ -110,10 +124,10 @@ module SwarmSDK
|
|
|
110
124
|
# Determine delegation type and proceed
|
|
111
125
|
delegation_result = if @delegate_chat
|
|
112
126
|
# Delegate to agent
|
|
113
|
-
delegate_to_agent(
|
|
114
|
-
elsif
|
|
127
|
+
delegate_to_agent(message, call_stack)
|
|
128
|
+
elsif swarm_registry&.registered?(@delegate_target)
|
|
115
129
|
# Delegate to registered swarm
|
|
116
|
-
delegate_to_swarm(
|
|
130
|
+
delegate_to_swarm(message, call_stack, swarm_registry)
|
|
117
131
|
else
|
|
118
132
|
raise ConfigurationError, "Unknown delegation target: #{@delegate_target}"
|
|
119
133
|
end
|
|
@@ -127,7 +141,7 @@ module SwarmSDK
|
|
|
127
141
|
delegation_result: delegation_result,
|
|
128
142
|
metadata: {
|
|
129
143
|
tool_name: @tool_name,
|
|
130
|
-
|
|
144
|
+
message: message,
|
|
131
145
|
result: delegation_result,
|
|
132
146
|
timestamp: Time.now.utc.iso8601,
|
|
133
147
|
},
|
|
@@ -190,57 +204,61 @@ module SwarmSDK
|
|
|
190
204
|
|
|
191
205
|
# Delegate to an agent
|
|
192
206
|
#
|
|
193
|
-
# @param
|
|
207
|
+
# @param message [String] Message to send to the agent
|
|
208
|
+
# @param call_stack [Array] Delegation call stack for circular dependency detection
|
|
194
209
|
# @return [String] Result from agent
|
|
195
|
-
def delegate_to_agent(
|
|
210
|
+
def delegate_to_agent(message, call_stack)
|
|
196
211
|
# Push delegate target onto call stack to track delegation chain
|
|
197
|
-
|
|
212
|
+
call_stack.push(@delegate_target)
|
|
198
213
|
begin
|
|
199
|
-
response = @delegate_chat.ask(
|
|
214
|
+
response = @delegate_chat.ask(message, source: "delegation")
|
|
200
215
|
response.content
|
|
201
216
|
ensure
|
|
202
217
|
# Always pop from stack, even if delegation fails
|
|
203
|
-
|
|
218
|
+
call_stack.pop
|
|
204
219
|
end
|
|
205
220
|
end
|
|
206
221
|
|
|
207
222
|
# Delegate to a registered swarm
|
|
208
223
|
#
|
|
209
|
-
# @param
|
|
224
|
+
# @param message [String] Message to send to the swarm
|
|
225
|
+
# @param call_stack [Array] Delegation call stack for circular dependency detection
|
|
226
|
+
# @param swarm_registry [SwarmRegistry] Registry for sub-swarms
|
|
210
227
|
# @return [String] Result from swarm's lead agent
|
|
211
|
-
def delegate_to_swarm(
|
|
228
|
+
def delegate_to_swarm(message, call_stack, swarm_registry)
|
|
212
229
|
# Load sub-swarm (lazy load + cache)
|
|
213
|
-
subswarm =
|
|
230
|
+
subswarm = swarm_registry.load_swarm(@delegate_target)
|
|
214
231
|
|
|
215
232
|
# Push delegate target onto call stack to track delegation chain
|
|
216
|
-
|
|
233
|
+
call_stack.push(@delegate_target)
|
|
217
234
|
begin
|
|
218
235
|
# Execute sub-swarm's lead agent
|
|
219
236
|
lead_agent = subswarm.agents[subswarm.lead_agent]
|
|
220
|
-
response = lead_agent.ask(
|
|
237
|
+
response = lead_agent.ask(message, source: "delegation")
|
|
221
238
|
result = response.content
|
|
222
239
|
|
|
223
240
|
# Reset if keep_context: false
|
|
224
|
-
|
|
241
|
+
swarm_registry.reset_if_needed(@delegate_target)
|
|
225
242
|
|
|
226
243
|
result
|
|
227
244
|
ensure
|
|
228
245
|
# Always pop from stack, even if delegation fails
|
|
229
|
-
|
|
246
|
+
call_stack.pop
|
|
230
247
|
end
|
|
231
248
|
end
|
|
232
249
|
|
|
233
250
|
# Emit circular dependency warning event
|
|
234
251
|
#
|
|
252
|
+
# @param call_stack [Array] Current delegation call stack
|
|
235
253
|
# @return [void]
|
|
236
|
-
def emit_circular_warning
|
|
254
|
+
def emit_circular_warning(call_stack)
|
|
237
255
|
LogStream.emit(
|
|
238
256
|
type: "delegation_circular_dependency",
|
|
239
257
|
agent: @agent_name,
|
|
240
258
|
swarm_id: @swarm.swarm_id,
|
|
241
259
|
parent_swarm_id: @swarm.parent_swarm_id,
|
|
242
260
|
target: @delegate_target,
|
|
243
|
-
call_stack:
|
|
261
|
+
call_stack: call_stack,
|
|
244
262
|
timestamp: Time.now.utc.iso8601,
|
|
245
263
|
)
|
|
246
264
|
end
|
data/lib/swarm_sdk/tools/edit.rb
CHANGED
|
@@ -10,6 +10,13 @@ module SwarmSDK
|
|
|
10
10
|
class Edit < RubyLLM::Tool
|
|
11
11
|
include PathResolver
|
|
12
12
|
|
|
13
|
+
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
14
|
+
class << self
|
|
15
|
+
def creation_requirements
|
|
16
|
+
[:agent_name, :directory]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
13
20
|
description <<~DESC
|
|
14
21
|
Performs exact string replacements in files.
|
|
15
22
|
You must use your Read tool at least once in the conversation before editing.
|
|
@@ -55,8 +62,7 @@ module SwarmSDK
|
|
|
55
62
|
# @param directory [String] Agent's working directory
|
|
56
63
|
def initialize(agent_name:, directory:)
|
|
57
64
|
super()
|
|
58
|
-
|
|
59
|
-
@directory = File.expand_path(directory)
|
|
65
|
+
initialize_agent_context(agent_name: agent_name, directory: directory)
|
|
60
66
|
end
|
|
61
67
|
|
|
62
68
|
# Override name to return simple "Edit" instead of full class path
|
|
@@ -134,17 +140,6 @@ module SwarmSDK
|
|
|
134
140
|
rescue StandardError => e
|
|
135
141
|
error("Unexpected error editing file: #{e.class.name} - #{e.message}")
|
|
136
142
|
end
|
|
137
|
-
|
|
138
|
-
private
|
|
139
|
-
|
|
140
|
-
# Helper methods
|
|
141
|
-
def validation_error(message)
|
|
142
|
-
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def error(message)
|
|
146
|
-
"Error: #{message}"
|
|
147
|
-
end
|
|
148
143
|
end
|
|
149
144
|
end
|
|
150
145
|
end
|
data/lib/swarm_sdk/tools/glob.rb
CHANGED
|
@@ -9,6 +9,13 @@ module SwarmSDK
|
|
|
9
9
|
class Glob < RubyLLM::Tool
|
|
10
10
|
include PathResolver
|
|
11
11
|
|
|
12
|
+
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
13
|
+
class << self
|
|
14
|
+
def creation_requirements
|
|
15
|
+
[:directory]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
12
19
|
def initialize(directory:)
|
|
13
20
|
super()
|
|
14
21
|
@directory = File.expand_path(directory)
|
|
@@ -43,7 +50,7 @@ module SwarmSDK
|
|
|
43
50
|
desc: "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.",
|
|
44
51
|
required: false
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
# NOTE: Result limit now accessed via SwarmSDK.config.glob_result_limit
|
|
47
54
|
|
|
48
55
|
def execute(pattern:, path: nil)
|
|
49
56
|
# Validate inputs
|
|
@@ -100,8 +107,9 @@ module SwarmSDK
|
|
|
100
107
|
matches.sort_by! { |f| -File.mtime(f).to_i }
|
|
101
108
|
|
|
102
109
|
# Limit results
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
max_results = SwarmSDK.config.glob_result_limit
|
|
111
|
+
if matches.count > max_results
|
|
112
|
+
matches = matches.take(max_results)
|
|
105
113
|
truncated = true
|
|
106
114
|
else
|
|
107
115
|
truncated = false
|
|
@@ -115,7 +123,7 @@ module SwarmSDK
|
|
|
115
123
|
output += <<~REMINDER
|
|
116
124
|
|
|
117
125
|
<system-reminder>
|
|
118
|
-
Results limited to first #{
|
|
126
|
+
Results limited to first #{max_results} matches (sorted by most recently modified).
|
|
119
127
|
Consider using a more specific pattern to narrow your search.
|
|
120
128
|
</system-reminder>
|
|
121
129
|
REMINDER
|
data/lib/swarm_sdk/tools/grep.rb
CHANGED
|
@@ -9,6 +9,13 @@ module SwarmSDK
|
|
|
9
9
|
class Grep < RubyLLM::Tool
|
|
10
10
|
include PathResolver
|
|
11
11
|
|
|
12
|
+
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
13
|
+
class << self
|
|
14
|
+
def creation_requirements
|
|
15
|
+
[:directory]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
12
19
|
def initialize(directory:)
|
|
13
20
|
super()
|
|
14
21
|
@directory = File.expand_path(directory)
|
|
@@ -11,6 +11,13 @@ module SwarmSDK
|
|
|
11
11
|
class MultiEdit < RubyLLM::Tool
|
|
12
12
|
include PathResolver
|
|
13
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
|
+
|
|
14
21
|
description <<~DESC
|
|
15
22
|
Performs multiple exact string replacements in a single file.
|
|
16
23
|
Edits are applied sequentially, so later edits see the results of earlier ones.
|
|
@@ -53,8 +60,7 @@ module SwarmSDK
|
|
|
53
60
|
# @param directory [String] Agent's working directory
|
|
54
61
|
def initialize(agent_name:, directory:)
|
|
55
62
|
super()
|
|
56
|
-
|
|
57
|
-
@directory = File.expand_path(directory)
|
|
63
|
+
initialize_agent_context(agent_name: agent_name, directory: directory)
|
|
58
64
|
end
|
|
59
65
|
|
|
60
66
|
# Override name to return simple "MultiEdit" instead of full class path
|
|
@@ -204,15 +210,13 @@ module SwarmSDK
|
|
|
204
210
|
|
|
205
211
|
private
|
|
206
212
|
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
end
|
|
215
|
-
|
|
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
|
|
216
220
|
def error_with_results(message, results)
|
|
217
221
|
output = "<tool_use_error>InputValidationError: #{message}\n\n"
|
|
218
222
|
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module SwarmSDK
|
|
4
4
|
module Tools
|
|
5
|
-
# Shared path resolution logic for
|
|
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
|
|
6
11
|
#
|
|
7
12
|
# Tools resolve relative paths against the agent's directory.
|
|
8
13
|
# Absolute paths are used as-is.
|
|
@@ -12,17 +17,41 @@ module SwarmSDK
|
|
|
12
17
|
# include PathResolver
|
|
13
18
|
#
|
|
14
19
|
# def initialize(agent_name:, directory:)
|
|
15
|
-
#
|
|
20
|
+
# super()
|
|
21
|
+
# initialize_agent_context(agent_name: agent_name, directory: directory)
|
|
16
22
|
# end
|
|
17
23
|
#
|
|
18
24
|
# def execute(file_path:)
|
|
19
25
|
# resolved_path = resolve_path(file_path)
|
|
20
26
|
# File.read(resolved_path)
|
|
27
|
+
# rescue StandardError => e
|
|
28
|
+
# error("Failed to read: #{e.message}")
|
|
21
29
|
# end
|
|
22
30
|
# end
|
|
23
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
|
+
|
|
24
39
|
private
|
|
25
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
|
+
|
|
26
55
|
# Resolve a path relative to the agent's directory
|
|
27
56
|
#
|
|
28
57
|
# - Absolute paths (starting with /) are returned as-is
|
|
@@ -38,6 +67,26 @@ module SwarmSDK
|
|
|
38
67
|
|
|
39
68
|
File.expand_path(path, @directory)
|
|
40
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
|
|
41
90
|
end
|
|
42
91
|
end
|
|
43
92
|
end
|
data/lib/swarm_sdk/tools/read.rb
CHANGED
|
@@ -10,8 +10,7 @@ module SwarmSDK
|
|
|
10
10
|
class Read < RubyLLM::Tool
|
|
11
11
|
include PathResolver
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
DEFAULT_LIMIT = 2000
|
|
13
|
+
# NOTE: Line length and limit now accessed via SwarmSDK.config
|
|
15
14
|
|
|
16
15
|
# List of available document converters
|
|
17
16
|
CONVERTERS = [
|
|
@@ -28,6 +27,13 @@ module SwarmSDK
|
|
|
28
27
|
""
|
|
29
28
|
end
|
|
30
29
|
|
|
30
|
+
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
31
|
+
class << self
|
|
32
|
+
def creation_requirements
|
|
33
|
+
[:agent_name, :directory]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
31
37
|
description <<~DESC
|
|
32
38
|
Reads a file from the local filesystem. You can access any file directly by using this tool.
|
|
33
39
|
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid.
|
|
@@ -67,8 +73,7 @@ module SwarmSDK
|
|
|
67
73
|
# @param directory [String] Agent's working directory
|
|
68
74
|
def initialize(agent_name:, directory:)
|
|
69
75
|
super()
|
|
70
|
-
|
|
71
|
-
@directory = File.expand_path(directory)
|
|
76
|
+
initialize_agent_context(agent_name: agent_name, directory: directory)
|
|
72
77
|
end
|
|
73
78
|
|
|
74
79
|
# Override name to return simple "Read" instead of full class path
|
|
@@ -146,18 +151,20 @@ module SwarmSDK
|
|
|
146
151
|
lines = lines.drop(start_line)
|
|
147
152
|
|
|
148
153
|
# Apply limit if specified, otherwise use default
|
|
149
|
-
|
|
154
|
+
default_limit = SwarmSDK.config.read_line_limit
|
|
155
|
+
effective_limit = limit || default_limit
|
|
150
156
|
lines = lines.take(effective_limit)
|
|
151
|
-
truncated = limit.nil? && total_lines >
|
|
157
|
+
truncated = limit.nil? && total_lines > default_limit
|
|
152
158
|
|
|
153
159
|
# Format with line numbers (cat -n style)
|
|
160
|
+
max_line_length = SwarmSDK.config.line_character_limit
|
|
154
161
|
output_lines = lines.each_with_index.map do |line, idx|
|
|
155
162
|
line_number = start_line + idx + 1
|
|
156
163
|
display_line = line.chomp
|
|
157
164
|
|
|
158
165
|
# Truncate long lines
|
|
159
|
-
if display_line.length >
|
|
160
|
-
display_line = display_line[0...
|
|
166
|
+
if display_line.length > max_line_length
|
|
167
|
+
display_line = display_line[0...max_line_length]
|
|
161
168
|
display_line += "... (line truncated)"
|
|
162
169
|
end
|
|
163
170
|
|
|
@@ -182,15 +189,6 @@ module SwarmSDK
|
|
|
182
189
|
CONVERTERS.find { |converter| converter.extensions.include?(ext) }
|
|
183
190
|
end
|
|
184
191
|
|
|
185
|
-
# Helper methods
|
|
186
|
-
def validation_error(message)
|
|
187
|
-
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def error(message)
|
|
191
|
-
"Error: #{message}"
|
|
192
|
-
end
|
|
193
|
-
|
|
194
192
|
def format_with_reminder(content, reminder)
|
|
195
193
|
return content if reminder.nil? || reminder.empty?
|
|
196
194
|
|
|
@@ -205,7 +203,7 @@ module SwarmSDK
|
|
|
205
203
|
|
|
206
204
|
if truncated
|
|
207
205
|
reminders << ""
|
|
208
|
-
reminders << "Note: This file has #{total_lines} lines but only the first #{
|
|
206
|
+
reminders << "Note: This file has #{total_lines} lines but only the first #{SwarmSDK.config.read_line_limit} lines are shown. Use the offset and limit parameters to read additional sections if needed."
|
|
209
207
|
end
|
|
210
208
|
|
|
211
209
|
reminders << "</system-reminder>"
|