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.
Files changed (78) 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 +262 -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 +11 -13
  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 +1 -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/config.rb +301 -0
  23. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  24. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  25. data/lib/swarm_sdk/configuration.rb +65 -543
  26. data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
  27. data/lib/swarm_sdk/context_compactor.rb +6 -11
  28. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  29. data/lib/swarm_sdk/context_management/context.rb +328 -0
  30. data/lib/swarm_sdk/defaults.rb +196 -0
  31. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  32. data/lib/swarm_sdk/hooks/adapter.rb +3 -3
  33. data/lib/swarm_sdk/hooks/shell_executor.rb +4 -2
  34. data/lib/swarm_sdk/log_collector.rb +179 -29
  35. data/lib/swarm_sdk/log_stream.rb +29 -0
  36. data/lib/swarm_sdk/models.json +4333 -1
  37. data/lib/swarm_sdk/node_context.rb +1 -1
  38. data/lib/swarm_sdk/observer/builder.rb +81 -0
  39. data/lib/swarm_sdk/observer/config.rb +45 -0
  40. data/lib/swarm_sdk/observer/manager.rb +236 -0
  41. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  42. data/lib/swarm_sdk/plugin.rb +93 -3
  43. data/lib/swarm_sdk/snapshot.rb +6 -6
  44. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  45. data/lib/swarm_sdk/state_restorer.rb +136 -151
  46. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  47. data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
  48. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  49. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  50. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  51. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  52. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  53. data/lib/swarm_sdk/swarm/tool_configurator.rb +44 -140
  54. data/lib/swarm_sdk/swarm.rb +146 -689
  55. data/lib/swarm_sdk/tools/bash.rb +14 -8
  56. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  57. data/lib/swarm_sdk/tools/edit.rb +8 -13
  58. data/lib/swarm_sdk/tools/glob.rb +12 -4
  59. data/lib/swarm_sdk/tools/grep.rb +7 -0
  60. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  61. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  62. data/lib/swarm_sdk/tools/read.rb +16 -18
  63. data/lib/swarm_sdk/tools/registry.rb +122 -10
  64. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +9 -5
  65. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  66. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  67. data/lib/swarm_sdk/tools/web_fetch.rb +20 -17
  68. data/lib/swarm_sdk/tools/write.rb +8 -13
  69. data/lib/swarm_sdk/version.rb +1 -1
  70. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  71. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  72. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  73. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +7 -5
  74. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +5 -3
  75. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  76. data/lib/swarm_sdk.rb +64 -104
  77. metadata +68 -15
  78. 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,7 @@ 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
+ # 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 || DEFAULT_TIMEOUT_MS
103
- timeout_ms = [timeout_ms, MAX_TIMEOUT_MS].min
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
- if output.length > MAX_OUTPUT_LENGTH
145
- truncated = output[0...MAX_OUTPUT_LENGTH]
146
- truncated += "\n\n<system-reminder>Output truncated at #{MAX_OUTPUT_LENGTH} characters. The full output was #{output.length} characters.</system-reminder>"
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 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,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
- MAX_RESULTS = 1000 # Limit results to prevent overwhelming output
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
- if matches.count > MAX_RESULTS
104
- matches = matches.take(MAX_RESULTS)
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 #{MAX_RESULTS} matches (sorted by most recently modified).
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
@@ -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,7 @@ module SwarmSDK
10
10
  class Read < RubyLLM::Tool
11
11
  include PathResolver
12
12
 
13
- MAX_LINE_LENGTH = 2000
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
- @agent_name = agent_name.to_sym
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
- effective_limit = limit || DEFAULT_LIMIT
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 > DEFAULT_LIMIT
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 > MAX_LINE_LENGTH
160
- display_line = display_line[0...MAX_LINE_LENGTH]
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 #{DEFAULT_LIMIT} lines are shown. Use the offset and limit parameters to read additional sections if needed."
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>"