swarm_sdk 2.2.0 → 2.3.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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/agent/builder.rb +58 -0
  3. data/lib/swarm_sdk/agent/chat.rb +527 -1059
  4. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
  5. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  6. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
  7. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  8. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  9. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  10. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  11. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +12 -12
  12. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  13. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  14. data/lib/swarm_sdk/agent/context.rb +2 -2
  15. data/lib/swarm_sdk/agent/definition.rb +66 -154
  16. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  17. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  18. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  19. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  20. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  21. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  22. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  23. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  24. data/lib/swarm_sdk/configuration.rb +65 -543
  25. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  26. data/lib/swarm_sdk/context_compactor.rb +6 -11
  27. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  28. data/lib/swarm_sdk/context_management/context.rb +328 -0
  29. data/lib/swarm_sdk/defaults.rb +196 -0
  30. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  31. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  32. data/lib/swarm_sdk/log_collector.rb +179 -29
  33. data/lib/swarm_sdk/log_stream.rb +29 -0
  34. data/lib/swarm_sdk/node_context.rb +1 -1
  35. data/lib/swarm_sdk/observer/builder.rb +81 -0
  36. data/lib/swarm_sdk/observer/config.rb +45 -0
  37. data/lib/swarm_sdk/observer/manager.rb +236 -0
  38. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  39. data/lib/swarm_sdk/plugin.rb +93 -3
  40. data/lib/swarm_sdk/snapshot.rb +6 -6
  41. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  42. data/lib/swarm_sdk/state_restorer.rb +136 -151
  43. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  44. data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
  45. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  46. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  47. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  48. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  49. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  50. data/lib/swarm_sdk/swarm/tool_configurator.rb +42 -138
  51. data/lib/swarm_sdk/swarm.rb +137 -679
  52. data/lib/swarm_sdk/tools/bash.rb +11 -3
  53. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  54. data/lib/swarm_sdk/tools/edit.rb +8 -13
  55. data/lib/swarm_sdk/tools/glob.rb +9 -1
  56. data/lib/swarm_sdk/tools/grep.rb +7 -0
  57. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  58. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  59. data/lib/swarm_sdk/tools/read.rb +11 -13
  60. data/lib/swarm_sdk/tools/registry.rb +122 -10
  61. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -5
  62. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  63. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  64. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  65. data/lib/swarm_sdk/tools/write.rb +8 -13
  66. data/lib/swarm_sdk/version.rb +1 -1
  67. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  68. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  69. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  70. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +3 -3
  71. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  72. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  73. data/lib/swarm_sdk.rb +33 -3
  74. metadata +67 -15
  75. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
@@ -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,10 @@ module SwarmSDK
78
85
  desc: "Optional timeout in milliseconds (max 600000)",
79
86
  required: false
80
87
 
81
- DEFAULT_TIMEOUT_MS = 120_000 # 2 minutes
82
- MAX_TIMEOUT_MS = 600_000 # 10 minutes
83
- MAX_OUTPUT_LENGTH = 30_000 # characters
88
+ # Backward compatibility aliases - use Defaults module for new code
89
+ DEFAULT_TIMEOUT_MS = Defaults::Timeouts::BASH_COMMAND_MS
90
+ MAX_TIMEOUT_MS = Defaults::Timeouts::BASH_COMMAND_MAX_MS
91
+ MAX_OUTPUT_LENGTH = Defaults::Limits::OUTPUT_CHARACTERS
84
92
 
85
93
  # Commands that are ALWAYS blocked for safety reasons
86
94
  # These cannot be overridden by permissions configuration
@@ -2,12 +2,30 @@
2
2
 
3
3
  module SwarmSDK
4
4
  module Tools
5
- # Delegate tool for delegating tasks to other agents in the swarm
5
+ # Delegate tool for working with other agents in the swarm
6
6
  #
7
- # Creates agent-specific delegation tools (e.g., DelegateTaskToBackend)
8
- # that allow one agent to delegate work to another agent.
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 in the expected format: DelegateTaskTo[AgentName]
48
- @tool_name = "DelegateTaskTo#{delegate_name.to_s.capitalize}"
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
- "Delegate tasks to #{@delegate_name}. #{@delegate_description}"
63
+ "Work with #{@delegate_name} to delegate work, ask questions, or collaborate. #{@delegate_description}"
55
64
  end
56
65
 
57
- param :task,
66
+ param :message,
58
67
  type: "string",
59
- desc: "Task description for the agent",
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 task [String] Task to delegate
78
+ # @param message [String] Message to send to the agent
70
79
  # @return [String] Result from delegate agent or error message
71
- def execute(task:)
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 @call_stack.include?(@delegate_target)
74
- emit_circular_warning
75
- return "Error: Circular delegation detected: #{@call_stack.join(" -> ")} -> #{@delegate_target}. " \
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
- task: task,
108
+ message: message,
95
109
  timestamp: Time.now.utc.iso8601,
96
110
  },
97
111
  )
98
112
 
99
- executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
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(task)
114
- elsif @swarm_registry&.registered?(@delegate_target)
127
+ delegate_to_agent(message, call_stack)
128
+ elsif swarm_registry&.registered?(@delegate_target)
115
129
  # Delegate to registered swarm
116
- delegate_to_swarm(task)
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
- task: task,
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 task [String] Task to delegate
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(task)
210
+ def delegate_to_agent(message, call_stack)
196
211
  # Push delegate target onto call stack to track delegation chain
197
- @call_stack.push(@delegate_target)
212
+ call_stack.push(@delegate_target)
198
213
  begin
199
- response = @delegate_chat.ask(task)
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
- @call_stack.pop
218
+ call_stack.pop
204
219
  end
205
220
  end
206
221
 
207
222
  # Delegate to a registered swarm
208
223
  #
209
- # @param task [String] Task to delegate
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(task)
228
+ def delegate_to_swarm(message, call_stack, swarm_registry)
212
229
  # Load sub-swarm (lazy load + cache)
213
- subswarm = @swarm_registry.load_swarm(@delegate_target)
230
+ subswarm = swarm_registry.load_swarm(@delegate_target)
214
231
 
215
232
  # Push delegate target onto call stack to track delegation chain
216
- @call_stack.push(@delegate_target)
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(task)
237
+ response = lead_agent.ask(message, source: "delegation")
221
238
  result = response.content
222
239
 
223
240
  # Reset if keep_context: false
224
- @swarm_registry.reset_if_needed(@delegate_target)
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
- @call_stack.pop
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: @call_stack,
261
+ call_stack: call_stack,
244
262
  timestamp: Time.now.utc.iso8601,
245
263
  )
246
264
  end
@@ -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
- @agent_name = agent_name.to_sym
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
@@ -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,8 @@ 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
- MAX_RESULTS = 1000 # Limit results to prevent overwhelming output
53
+ # Backward compatibility alias - use Defaults module for new code
54
+ MAX_RESULTS = Defaults::Limits::GLOB_RESULTS
47
55
 
48
56
  def execute(pattern:, path: nil)
49
57
  # Validate inputs
@@ -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
- @agent_name = agent_name.to_sym
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
- # Helper methods
208
- def validation_error(message)
209
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
210
- end
211
-
212
- def error(message)
213
- "Error: #{message}"
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 all file 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
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
- # @directory = File.expand_path(directory)
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
@@ -10,8 +10,9 @@ module SwarmSDK
10
10
  class Read < RubyLLM::Tool
11
11
  include PathResolver
12
12
 
13
- MAX_LINE_LENGTH = 2000
14
- DEFAULT_LIMIT = 2000
13
+ # Backward compatibility aliases - use Defaults module for new code
14
+ MAX_LINE_LENGTH = Defaults::Limits::LINE_CHARACTERS
15
+ DEFAULT_LIMIT = Defaults::Limits::READ_LINES
15
16
 
16
17
  # List of available document converters
17
18
  CONVERTERS = [
@@ -28,6 +29,13 @@ module SwarmSDK
28
29
  ""
29
30
  end
30
31
 
32
+ # Factory pattern: declare what parameters this tool needs for instantiation
33
+ class << self
34
+ def creation_requirements
35
+ [:agent_name, :directory]
36
+ end
37
+ end
38
+
31
39
  description <<~DESC
32
40
  Reads a file from the local filesystem. You can access any file directly by using this tool.
33
41
  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 +75,7 @@ module SwarmSDK
67
75
  # @param directory [String] Agent's working directory
68
76
  def initialize(agent_name:, directory:)
69
77
  super()
70
- @agent_name = agent_name.to_sym
71
- @directory = File.expand_path(directory)
78
+ initialize_agent_context(agent_name: agent_name, directory: directory)
72
79
  end
73
80
 
74
81
  # Override name to return simple "Read" instead of full class path
@@ -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
 
@@ -5,24 +5,51 @@ module SwarmSDK
5
5
  # Registry for built-in SwarmSDK tools
6
6
  #
7
7
  # Maps tool names (symbols) to their RubyLLM::Tool classes.
8
- # Provides validation and lookup functionality for tool registration.
8
+ # Provides validation, lookup, and factory functionality for tool registration.
9
+ #
10
+ # ## Tool Creation Pattern
11
+ #
12
+ # Tools register themselves with their creation requirements via the `tool_factory` method.
13
+ # This eliminates the need for a giant case statement in ToolConfigurator.
14
+ #
15
+ # Tools fall into three categories:
16
+ # 1. **No params**: Simple tools with no initialization requirements (Think, Clock)
17
+ # 2. **Directory only**: Tools needing working directory (Bash, Grep, Glob)
18
+ # 3. **Agent context**: Tools needing agent tracking (Read, Write, Edit, MultiEdit)
19
+ # 4. **Scratchpad**: Tools needing scratchpad storage instance
20
+ #
21
+ # @example Adding a new tool with creation requirements
22
+ # # In the tool class:
23
+ # class MyTool < RubyLLM::Tool
24
+ # def self.creation_requirements
25
+ # [:agent_name, :directory]
26
+ # end
27
+ # end
28
+ #
29
+ # # In registry:
30
+ # BUILTIN_TOOLS = {
31
+ # MyTool: SwarmSDK::Tools::MyTool,
32
+ # }
9
33
  #
10
34
  # Note: Plugin-provided tools (e.g., memory tools) are NOT in this registry.
11
35
  # They are registered via SwarmSDK::PluginRegistry instead.
12
36
  class Registry
13
37
  # All available built-in tools
38
+ #
39
+ # Maps tool names to their classes. The class must respond to `creation_requirements`
40
+ # to specify what parameters are needed for instantiation.
14
41
  BUILTIN_TOOLS = {
15
- Read: :special, # Requires agent context for read tracking
16
- Write: :special, # Requires agent context for read-before-write enforcement
17
- Edit: :special, # Requires agent context for read-before-edit enforcement
42
+ Read: SwarmSDK::Tools::Read,
43
+ Write: SwarmSDK::Tools::Write,
44
+ Edit: SwarmSDK::Tools::Edit,
45
+ MultiEdit: SwarmSDK::Tools::MultiEdit,
18
46
  Bash: SwarmSDK::Tools::Bash,
19
47
  Grep: SwarmSDK::Tools::Grep,
20
48
  Glob: SwarmSDK::Tools::Glob,
21
- MultiEdit: :special, # Requires agent context for read-before-edit enforcement
22
- TodoWrite: :special, # Requires agent context for todo tracking
23
- ScratchpadWrite: :special, # Requires scratchpad storage instance
24
- ScratchpadRead: :special, # Requires scratchpad storage instance
25
- ScratchpadList: :special, # Requires scratchpad storage instance
49
+ TodoWrite: SwarmSDK::Tools::TodoWrite,
50
+ ScratchpadWrite: :scratchpad, # Requires scratchpad storage instance
51
+ ScratchpadRead: :scratchpad, # Requires scratchpad storage instance
52
+ ScratchpadList: :scratchpad, # Requires scratchpad storage instance
26
53
  Think: SwarmSDK::Tools::Think,
27
54
  WebFetch: SwarmSDK::Tools::WebFetch,
28
55
  Clock: SwarmSDK::Tools::Clock,
@@ -35,12 +62,49 @@ module SwarmSDK
35
62
  # They are managed by SwarmSDK::PluginRegistry instead.
36
63
  #
37
64
  # @param name [Symbol, String] Tool name
38
- # @return [Class, Symbol, nil] Tool class, :special, or nil if not found
65
+ # @return [Class, Symbol, nil] Tool class, :scratchpad marker, or nil if not found
39
66
  def get(name)
40
67
  name_sym = name.to_sym
41
68
  BUILTIN_TOOLS[name_sym]
42
69
  end
43
70
 
71
+ # Create a tool instance using the Factory Pattern
72
+ #
73
+ # Uses the tool's `creation_requirements` class method to determine
74
+ # what parameters to pass to the constructor.
75
+ #
76
+ # @param name [Symbol, String] Tool name
77
+ # @param context [Hash] Available context for tool creation
78
+ # @option context [Symbol] :agent_name Agent identifier
79
+ # @option context [String] :directory Agent's working directory
80
+ # @option context [Object] :scratchpad_storage Scratchpad storage instance
81
+ # @return [RubyLLM::Tool] Instantiated tool
82
+ # @raise [ConfigurationError] If tool is unknown or has unmet requirements
83
+ def create(name, context = {})
84
+ name_sym = name.to_sym
85
+ tool_entry = BUILTIN_TOOLS[name_sym]
86
+
87
+ raise ConfigurationError, "Unknown tool: #{name}" unless tool_entry
88
+
89
+ # Handle scratchpad tools specially (they use factory methods)
90
+ if tool_entry == :scratchpad
91
+ return create_scratchpad_tool(name_sym, context[:scratchpad_storage])
92
+ end
93
+
94
+ # Get the tool class and its requirements
95
+ tool_class = tool_entry
96
+
97
+ # Check if tool defines creation requirements
98
+ if tool_class.respond_to?(:creation_requirements)
99
+ requirements = tool_class.creation_requirements
100
+ params = extract_params(requirements, context, name)
101
+ tool_class.new(**params)
102
+ else
103
+ # No requirements - simple instantiation
104
+ tool_class.new
105
+ end
106
+ end
107
+
44
108
  # Get multiple tool classes by names
45
109
  #
46
110
  # @param names [Array<Symbol, String>] Tool names
@@ -87,6 +151,54 @@ module SwarmSDK
87
151
  def validate(names)
88
152
  names.reject { |name| exists?(name) }
89
153
  end
154
+
155
+ private
156
+
157
+ # Extract required parameters from context
158
+ #
159
+ # @param requirements [Array<Symbol>] Required parameter names
160
+ # @param context [Hash] Available context
161
+ # @param tool_name [Symbol] Tool name for error messages
162
+ # @return [Hash] Parameters to pass to tool constructor
163
+ # @raise [ConfigurationError] If required parameter is missing
164
+ def extract_params(requirements, context, tool_name)
165
+ params = {}
166
+
167
+ requirements.each do |req|
168
+ unless context.key?(req)
169
+ raise ConfigurationError,
170
+ "Tool #{tool_name} requires #{req} but it was not provided in context"
171
+ end
172
+
173
+ params[req] = context[req]
174
+ end
175
+
176
+ params
177
+ end
178
+
179
+ # Create a scratchpad tool using its factory method
180
+ #
181
+ # @param name [Symbol] Scratchpad tool name
182
+ # @param storage [Object] Scratchpad storage instance
183
+ # @return [RubyLLM::Tool] Instantiated scratchpad tool
184
+ # @raise [ConfigurationError] If storage is not provided
185
+ def create_scratchpad_tool(name, storage)
186
+ unless storage
187
+ raise ConfigurationError,
188
+ "Scratchpad tool #{name} requires scratchpad_storage in context"
189
+ end
190
+
191
+ case name
192
+ when :ScratchpadWrite
193
+ Tools::Scratchpad::ScratchpadWrite.create_for_scratchpad(storage)
194
+ when :ScratchpadRead
195
+ Tools::Scratchpad::ScratchpadRead.create_for_scratchpad(storage)
196
+ when :ScratchpadList
197
+ Tools::Scratchpad::ScratchpadList.create_for_scratchpad(storage)
198
+ else
199
+ raise ConfigurationError, "Unknown scratchpad tool: #{name}"
200
+ end
201
+ end
90
202
  end
91
203
  end
92
204
  end