claude_swarm 1.0.10 → 1.0.11

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/{CHANGELOG.md → CHANGELOG.claude-swarm.md} +3 -0
  3. data/CLAUDE.md +0 -1
  4. data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
  5. data/docs/v2/CHANGELOG.swarm_cli.md +12 -0
  6. data/docs/v2/CHANGELOG.swarm_memory.md +139 -0
  7. data/docs/v2/CHANGELOG.swarm_sdk.md +249 -1
  8. data/docs/v2/README.md +15 -5
  9. data/docs/v2/guides/complete-tutorial.md +93 -7
  10. data/docs/v2/guides/getting-started.md +3 -1
  11. data/docs/v2/guides/memory-adapters.md +41 -0
  12. data/docs/v2/guides/{migrating-to-2.3.md → migrating-to-2.x.md} +213 -8
  13. data/docs/v2/guides/plugins.md +52 -5
  14. data/docs/v2/guides/rails-integration.md +6 -0
  15. data/docs/v2/guides/swarm-memory.md +2 -13
  16. data/docs/v2/reference/cli.md +0 -1
  17. data/docs/v2/reference/configuration_reference.md +300 -0
  18. data/docs/v2/reference/event_payload_structures.md +26 -4
  19. data/docs/v2/reference/ruby-dsl.md +457 -4
  20. data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
  21. data/docs/v2/reference/yaml.md +2 -2
  22. data/lib/claude_swarm/mcp_generator.rb +1 -1
  23. data/lib/claude_swarm/orchestrator.rb +8 -1
  24. data/lib/claude_swarm/version.rb +1 -1
  25. data/lib/swarm_cli/version.rb +1 -1
  26. data/lib/swarm_memory/core/semantic_index.rb +10 -2
  27. data/lib/swarm_memory/core/storage.rb +7 -2
  28. data/lib/swarm_memory/dsl/memory_config.rb +37 -0
  29. data/lib/swarm_memory/integration/sdk_plugin.rb +120 -27
  30. data/lib/swarm_memory/optimization/defragmenter.rb +1 -1
  31. data/lib/swarm_memory/prompts/memory_researcher.md.erb +0 -1
  32. data/lib/swarm_memory/tools/load_skill.rb +0 -1
  33. data/lib/swarm_memory/tools/memory_edit.rb +2 -1
  34. data/lib/swarm_memory/tools/memory_read.rb +1 -1
  35. data/lib/swarm_memory/version.rb +1 -1
  36. data/lib/swarm_memory.rb +7 -5
  37. data/lib/swarm_sdk/agent/chat.rb +1 -1
  38. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +4 -0
  39. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +1 -1
  40. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +38 -4
  41. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +2 -2
  42. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +3 -5
  43. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +48 -0
  44. data/lib/swarm_sdk/agent/context.rb +1 -2
  45. data/lib/swarm_sdk/agent/definition.rb +3 -3
  46. data/lib/swarm_sdk/agent/system_prompt_builder.rb +1 -1
  47. data/lib/swarm_sdk/agent_registry.rb +146 -0
  48. data/lib/swarm_sdk/builders/base_builder.rb +91 -12
  49. data/lib/swarm_sdk/config.rb +302 -0
  50. data/lib/swarm_sdk/configuration/parser.rb +22 -2
  51. data/lib/swarm_sdk/configuration.rb +13 -4
  52. data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
  53. data/lib/swarm_sdk/custom_tool_registry.rb +226 -0
  54. data/lib/swarm_sdk/hooks/adapter.rb +3 -3
  55. data/lib/swarm_sdk/hooks/shell_executor.rb +4 -3
  56. data/lib/swarm_sdk/models.json +4333 -1
  57. data/lib/swarm_sdk/models.rb +43 -2
  58. data/lib/swarm_sdk/plugin.rb +2 -2
  59. data/lib/swarm_sdk/result.rb +52 -0
  60. data/lib/swarm_sdk/swarm/agent_initializer.rb +1 -1
  61. data/lib/swarm_sdk/swarm/hook_triggers.rb +1 -0
  62. data/lib/swarm_sdk/swarm/logging_callbacks.rb +1 -0
  63. data/lib/swarm_sdk/swarm/tool_configurator.rb +18 -4
  64. data/lib/swarm_sdk/swarm.rb +76 -13
  65. data/lib/swarm_sdk/tools/bash.rb +7 -9
  66. data/lib/swarm_sdk/tools/glob.rb +5 -5
  67. data/lib/swarm_sdk/tools/read.rb +8 -8
  68. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +4 -3
  69. data/lib/swarm_sdk/tools/web_fetch.rb +20 -18
  70. data/lib/swarm_sdk/version.rb +1 -1
  71. data/lib/swarm_sdk/workflow/builder.rb +49 -0
  72. data/lib/swarm_sdk/workflow/node_builder.rb +4 -2
  73. data/lib/swarm_sdk/workflow/transformer_executor.rb +4 -3
  74. data/lib/swarm_sdk.rb +261 -105
  75. data/swarm_cli.gemspec +1 -1
  76. data/swarm_memory.gemspec +8 -3
  77. data/swarm_sdk.gemspec +4 -4
  78. data/team_full.yml +104 -300
  79. metadata +9 -5
  80. data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
  81. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -18,16 +18,57 @@ module SwarmSDK
18
18
  MODELS_JSON_PATH = File.expand_path("models.json", __dir__)
19
19
  ALIASES_JSON_PATH = File.expand_path("model_aliases.json", __dir__)
20
20
 
21
+ # Model information wrapper providing method access to model data
22
+ #
23
+ # Wraps the raw Hash from models.json to provide RubyLLM::Model::Info-like
24
+ # interface for compatibility with code expecting method access.
25
+ #
26
+ # @example
27
+ # model = SwarmSDK::Models.find("claude-sonnet-4-5-20250929")
28
+ # model.context_window #=> 200000
29
+ # model.id #=> "claude-sonnet-4-5-20250929"
30
+ class ModelInfo
31
+ attr_reader :id,
32
+ :name,
33
+ :provider,
34
+ :family,
35
+ :context_window,
36
+ :max_output_tokens,
37
+ :knowledge_cutoff,
38
+ :modalities,
39
+ :capabilities,
40
+ :pricing,
41
+ :metadata
42
+
43
+ # Create a ModelInfo from a Hash
44
+ #
45
+ # @param data [Hash] Model data from models.json
46
+ def initialize(data)
47
+ @id = data["id"] || data[:id]
48
+ @name = data["name"] || data[:name]
49
+ @provider = data["provider"] || data[:provider]
50
+ @family = data["family"] || data[:family]
51
+ @context_window = data["context_window"] || data[:context_window]
52
+ @max_output_tokens = data["max_output_tokens"] || data[:max_output_tokens]
53
+ @knowledge_cutoff = data["knowledge_cutoff"] || data[:knowledge_cutoff]
54
+ @modalities = data["modalities"] || data[:modalities]
55
+ @capabilities = data["capabilities"] || data[:capabilities]
56
+ @pricing = data["pricing"] || data[:pricing]
57
+ @metadata = data["metadata"] || data[:metadata]
58
+ end
59
+ end
60
+
21
61
  class << self
22
62
  # Find a model by ID or alias
23
63
  #
24
64
  # @param model_id [String] Model ID or alias to find
25
- # @return [Hash, nil] Model data or nil if not found
65
+ # @return [ModelInfo, nil] Model info or nil if not found
26
66
  def find(model_id)
27
67
  # Check if it's an alias first
28
68
  resolved_id = resolve_alias(model_id)
29
69
 
30
- all.find { |m| m["id"] == resolved_id || m[:id] == resolved_id }
70
+ model_hash = all.find { |m| m["id"] == resolved_id || m[:id] == resolved_id }
71
+ model_hash ? ModelInfo.new(model_hash) : nil
31
72
  end
32
73
 
33
74
  # Resolve a model alias to full model ID
@@ -139,11 +139,11 @@ module SwarmSDK
139
139
  []
140
140
  end
141
141
 
142
- # Agent storage enabled for this agent? (optional)
142
+ # Check if memory is configured for this agent (optional)
143
143
  #
144
144
  # @param agent_definition [Agent::Definition] Agent definition
145
145
  # @return [Boolean] True if storage should be created
146
- def storage_enabled?(agent_definition)
146
+ def memory_configured?(agent_definition)
147
147
  false
148
148
  end
149
149
 
@@ -109,6 +109,58 @@ module SwarmSDK
109
109
  @logs.map { |entry| entry[:agent] }.compact.uniq.map(&:to_sym)
110
110
  end
111
111
 
112
+ # Get per-agent usage breakdown from logs
113
+ #
114
+ # Aggregates context usage, tokens, and cost for each agent from their
115
+ # final agent_stop or agent_step events. Each agent's entry includes:
116
+ # - input_tokens, output_tokens, total_tokens
117
+ # - context_limit, usage_percentage, tokens_remaining
118
+ # - input_cost, output_cost, total_cost
119
+ #
120
+ # @return [Hash{Symbol => Hash}] Per-agent usage breakdown
121
+ #
122
+ # @example
123
+ # result.per_agent_usage[:backend]
124
+ # # => {
125
+ # # input_tokens: 15000,
126
+ # # output_tokens: 5000,
127
+ # # total_tokens: 20000,
128
+ # # context_limit: 200000,
129
+ # # usage_percentage: "10.0%",
130
+ # # tokens_remaining: 180000,
131
+ # # input_cost: 0.045,
132
+ # # output_cost: 0.075,
133
+ # # total_cost: 0.12
134
+ # # }
135
+ def per_agent_usage
136
+ # Find the last usage entry for each agent
137
+ agent_entries = {}
138
+
139
+ @logs.each do |entry|
140
+ next unless entry[:usage] && entry[:agent]
141
+ next unless entry[:type] == "agent_step" || entry[:type] == "agent_stop"
142
+
143
+ agent_name = entry[:agent].to_sym
144
+ agent_entries[agent_name] = entry[:usage]
145
+ end
146
+
147
+ # Build breakdown from final usage entries
148
+ agent_entries.transform_values do |usage|
149
+ {
150
+ input_tokens: usage[:cumulative_input_tokens] || 0,
151
+ output_tokens: usage[:cumulative_output_tokens] || 0,
152
+ total_tokens: usage[:cumulative_total_tokens] || 0,
153
+ cached_tokens: usage[:cumulative_cached_tokens] || 0,
154
+ context_limit: usage[:context_limit],
155
+ usage_percentage: usage[:tokens_used_percentage],
156
+ tokens_remaining: usage[:tokens_remaining],
157
+ input_cost: usage[:input_cost] || 0.0,
158
+ output_cost: usage[:output_cost] || 0.0,
159
+ total_cost: usage[:total_cost] || 0.0,
160
+ }
161
+ end
162
+ end
163
+
112
164
  # Count total LLM requests made
113
165
  # Each LLM API call produces either agent_step (tool calls) or agent_stop (final answer)
114
166
  def llm_requests
@@ -569,7 +569,7 @@ module SwarmSDK
569
569
  PluginRegistry.all.each do |plugin|
570
570
  @swarm.agent_definitions.each do |agent_name, agent_definition|
571
571
  # Check if this plugin needs storage for this agent
572
- next unless plugin.storage_enabled?(agent_definition)
572
+ next unless plugin.memory_configured?(agent_definition)
573
573
 
574
574
  # Get plugin config for this agent
575
575
  config = get_plugin_config(agent_definition, plugin.name)
@@ -77,6 +77,7 @@ module SwarmSDK
77
77
  total_cost: result.total_cost,
78
78
  total_tokens: result.total_tokens,
79
79
  agents_involved: result.agents_involved,
80
+ per_agent_usage: result.per_agent_usage,
80
81
  result: result,
81
82
  timestamp: Time.now.utc.iso8601,
82
83
  },
@@ -208,6 +208,7 @@ module SwarmSDK
208
208
  total_cost: context.metadata[:total_cost],
209
209
  total_tokens: context.metadata[:total_tokens],
210
210
  agents_involved: context.metadata[:agents_involved],
211
+ per_agent_usage: context.metadata[:per_agent_usage],
211
212
  timestamp: context.metadata[:timestamp],
212
213
  )
213
214
  end
@@ -60,6 +60,11 @@ module SwarmSDK
60
60
  # Uses the Registry factory pattern to instantiate tools based on their
61
61
  # declared requirements. This eliminates the need for a giant case statement.
62
62
  #
63
+ # Tool lookup order:
64
+ # 1. Plugin tools (registered via SwarmSDK::PluginRegistry)
65
+ # 2. Custom tools (registered via SwarmSDK.register_tool)
66
+ # 3. Built-in tools (SwarmSDK::Tools::Registry)
67
+ #
63
68
  # File tools and TodoWrite require agent context for tracking state.
64
69
  # Scratchpad tools require shared scratchpad instance.
65
70
  # Plugin tools are delegated to their respective plugins.
@@ -80,7 +85,16 @@ module SwarmSDK
80
85
  return create_plugin_tool(tool_name_sym, agent_name, directory, chat, agent_definition)
81
86
  end
82
87
 
83
- # Use Registry factory pattern - tools declare their own requirements
88
+ # Check if tool is a custom registered tool
89
+ if CustomToolRegistry.registered?(tool_name_sym)
90
+ context = {
91
+ agent_name: agent_name,
92
+ directory: directory,
93
+ }
94
+ return CustomToolRegistry.create(tool_name_sym, context)
95
+ end
96
+
97
+ # Use Registry factory pattern for built-in tools
84
98
  context = {
85
99
  agent_name: agent_name,
86
100
  directory: directory,
@@ -127,10 +141,10 @@ module SwarmSDK
127
141
  forbidden = tool_configs.map { |tc| tc[:name].to_sym }.select { |name| FILESYSTEM_TOOLS.include?(name) }
128
142
  unless forbidden.empty?
129
143
  raise ConfigurationError,
130
- "Filesystem tools are globally disabled (SwarmSDK.settings.allow_filesystem_tools = false) " \
144
+ "Filesystem tools are globally disabled (SwarmSDK.config.allow_filesystem_tools = false) " \
131
145
  "but agent '#{agent_name}' attempts to use: #{forbidden.join(", ")}.\n\n" \
132
146
  "This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
133
- "To use filesystem tools, set SwarmSDK.settings.allow_filesystem_tools = true before loading the swarm."
147
+ "To use filesystem tools, set SwarmSDK.config.allow_filesystem_tools = true before loading the swarm."
134
148
  end
135
149
  end
136
150
 
@@ -267,7 +281,7 @@ module SwarmSDK
267
281
  def register_plugin_tools(chat, agent_name, agent_definition, explicit_tool_names)
268
282
  PluginRegistry.all.each do |plugin|
269
283
  # Check if plugin has storage enabled for this agent
270
- next unless plugin.storage_enabled?(agent_definition)
284
+ next unless plugin.memory_configured?(agent_definition)
271
285
 
272
286
  # Register each tool provided by the plugin
273
287
  plugin.tools.each do |tool_name|
@@ -67,8 +67,7 @@ module SwarmSDK
67
67
  include LoggingCallbacks
68
68
  include HookTriggers
69
69
 
70
- # Backward compatibility aliases - use Defaults module for new code
71
- DEFAULT_MCP_LOG_LEVEL = Defaults::Logging::MCP_LOG_LEVEL
70
+ # NOTE: MCP log level now accessed via SwarmSDK.config.mcp_log_level
72
71
 
73
72
  # Default tools available to all agents
74
73
  DEFAULT_TOOLS = ToolConfigurator::DEFAULT_TOOLS
@@ -98,7 +97,7 @@ module SwarmSDK
98
97
  attr_writer :first_message_sent
99
98
 
100
99
  # Class-level MCP log level configuration
101
- @mcp_log_level = DEFAULT_MCP_LOG_LEVEL
100
+ @mcp_log_level = nil
102
101
  @mcp_logging_configured = false
103
102
 
104
103
  class << self
@@ -111,8 +110,8 @@ module SwarmSDK
111
110
  #
112
111
  # @param level [Integer] Log level (Logger::DEBUG, Logger::INFO, Logger::WARN, Logger::ERROR, Logger::FATAL)
113
112
  # @return [void]
114
- def configure_mcp_logging(level = DEFAULT_MCP_LOG_LEVEL)
115
- @mcp_log_level = level
113
+ def configure_mcp_logging(level = nil)
114
+ @mcp_log_level = level || SwarmSDK.config.mcp_log_level
116
115
  apply_mcp_logging_configuration
117
116
  end
118
117
 
@@ -123,7 +122,7 @@ module SwarmSDK
123
122
  return if @mcp_logging_configured
124
123
 
125
124
  RubyLLM::MCP.configure do |config|
126
- config.log_level = @mcp_log_level
125
+ config.log_level = @mcp_log_level || SwarmSDK.config.mcp_log_level
127
126
  end
128
127
 
129
128
  @mcp_logging_configured = true
@@ -135,17 +134,17 @@ module SwarmSDK
135
134
  # @param name [String] Human-readable swarm name
136
135
  # @param swarm_id [String, nil] Optional swarm ID (auto-generated if not provided)
137
136
  # @param parent_swarm_id [String, nil] Optional parent swarm ID (nil for root swarms)
138
- # @param global_concurrency [Integer] Max concurrent LLM calls across entire swarm
139
- # @param default_local_concurrency [Integer] Default max concurrent tool calls per agent
137
+ # @param global_concurrency [Integer, nil] Max concurrent LLM calls across entire swarm (nil uses config default)
138
+ # @param default_local_concurrency [Integer, nil] Default max concurrent tool calls per agent (nil uses config default)
140
139
  # @param scratchpad [Tools::Stores::Scratchpad, nil] Optional scratchpad instance (for testing/internal use)
141
140
  # @param scratchpad_mode [Symbol, String] Scratchpad mode (:enabled or :disabled). :per_node not allowed for non-node swarms.
142
141
  # @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
143
- def initialize(name:, swarm_id: nil, parent_swarm_id: nil, global_concurrency: Defaults::Concurrency::GLOBAL_LIMIT, default_local_concurrency: Defaults::Concurrency::LOCAL_LIMIT, scratchpad: nil, scratchpad_mode: :enabled, allow_filesystem_tools: nil)
142
+ def initialize(name:, swarm_id: nil, parent_swarm_id: nil, global_concurrency: nil, default_local_concurrency: nil, scratchpad: nil, scratchpad_mode: :enabled, allow_filesystem_tools: nil)
144
143
  @name = name
145
144
  @swarm_id = swarm_id || generate_swarm_id(name)
146
145
  @parent_swarm_id = parent_swarm_id
147
- @global_concurrency = global_concurrency
148
- @default_local_concurrency = default_local_concurrency
146
+ @global_concurrency = global_concurrency || SwarmSDK.config.global_concurrency_limit
147
+ @default_local_concurrency = default_local_concurrency || SwarmSDK.config.local_concurrency_limit
149
148
 
150
149
  # Handle scratchpad_mode parameter
151
150
  # For Swarm: :enabled or :disabled (not :per_node - that's for nodes)
@@ -153,9 +152,9 @@ module SwarmSDK
153
152
 
154
153
  # Resolve allow_filesystem_tools with priority:
155
154
  # 1. Explicit parameter (if not nil)
156
- # 2. Global settings
155
+ # 2. Global config
157
156
  @allow_filesystem_tools = if allow_filesystem_tools.nil?
158
- SwarmSDK.settings.allow_filesystem_tools
157
+ SwarmSDK.config.allow_filesystem_tools
159
158
  else
160
159
  allow_filesystem_tools
161
160
  end
@@ -367,6 +366,47 @@ module SwarmSDK
367
366
  @agent_definitions.keys
368
367
  end
369
368
 
369
+ # Get context usage breakdown for all agents
370
+ #
371
+ # Returns per-agent context statistics including tokens used, context limit,
372
+ # usage percentage, and cost. Useful for monitoring context window consumption
373
+ # across the swarm.
374
+ #
375
+ # @return [Hash{Symbol => Hash}] Per-agent context breakdown
376
+ #
377
+ # @example
378
+ # breakdown = swarm.context_breakdown
379
+ # breakdown[:backend]
380
+ # # => {
381
+ # # input_tokens: 15000,
382
+ # # output_tokens: 5000,
383
+ # # total_tokens: 20000,
384
+ # # cached_tokens: 2000,
385
+ # # context_limit: 200000,
386
+ # # usage_percentage: 10.0,
387
+ # # tokens_remaining: 180000,
388
+ # # input_cost: 0.045,
389
+ # # output_cost: 0.075,
390
+ # # total_cost: 0.12
391
+ # # }
392
+ def context_breakdown
393
+ initialize_agents unless @agents_initialized
394
+
395
+ breakdown = {}
396
+
397
+ # Include primary agents
398
+ @agents.each do |name, chat|
399
+ breakdown[name] = build_agent_context_info(chat)
400
+ end
401
+
402
+ # Include delegation instances
403
+ @delegation_instances.each do |instance_name, chat|
404
+ breakdown[instance_name.to_sym] = build_agent_context_info(chat)
405
+ end
406
+
407
+ breakdown
408
+ end
409
+
370
410
  # Implement Snapshotable interface
371
411
  def primary_agents
372
412
  @agents
@@ -547,6 +587,29 @@ module SwarmSDK
547
587
  end
548
588
  end
549
589
 
590
+ # Build context info hash for an agent chat instance
591
+ #
592
+ # @param chat [Agent::Chat] Agent chat instance with TokenTracking
593
+ # @return [Hash] Context usage information
594
+ def build_agent_context_info(chat)
595
+ return {} unless chat.respond_to?(:cumulative_input_tokens)
596
+
597
+ {
598
+ input_tokens: chat.cumulative_input_tokens,
599
+ output_tokens: chat.cumulative_output_tokens,
600
+ total_tokens: chat.cumulative_total_tokens,
601
+ cached_tokens: chat.cumulative_cached_tokens,
602
+ cache_creation_tokens: chat.cumulative_cache_creation_tokens,
603
+ effective_input_tokens: chat.effective_input_tokens,
604
+ context_limit: chat.context_limit,
605
+ usage_percentage: chat.context_usage_percentage,
606
+ tokens_remaining: chat.tokens_remaining,
607
+ input_cost: chat.cumulative_input_cost,
608
+ output_cost: chat.cumulative_output_cost,
609
+ total_cost: chat.cumulative_total_cost,
610
+ }
611
+ end
612
+
550
613
  # Validate that observer agent exists
551
614
  #
552
615
  # @param agent_name [Symbol] Name of the observer agent
@@ -85,10 +85,7 @@ module SwarmSDK
85
85
  desc: "Optional timeout in milliseconds (max 600000)",
86
86
  required: false
87
87
 
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
88
+ # NOTE: Timeout and output limits now accessed via SwarmSDK.config
92
89
 
93
90
  # Commands that are ALWAYS blocked for safety reasons
94
91
  # These cannot be overridden by permissions configuration
@@ -107,8 +104,8 @@ module SwarmSDK
107
104
  end
108
105
 
109
106
  # Validate and set timeout
110
- timeout_ms = timeout || DEFAULT_TIMEOUT_MS
111
- 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
112
109
  timeout_seconds = timeout_ms / 1000.0
113
110
 
114
111
  # Execute command with timeout
@@ -149,9 +146,10 @@ module SwarmSDK
149
146
  output = format_command_output(command, description, stdout, stderr, exit_status)
150
147
 
151
148
  # Truncate if too long
152
- if output.length > MAX_OUTPUT_LENGTH
153
- truncated = output[0...MAX_OUTPUT_LENGTH]
154
- 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>"
155
153
  output = truncated
156
154
  end
157
155
 
@@ -50,8 +50,7 @@ module SwarmSDK
50
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.",
51
51
  required: false
52
52
 
53
- # Backward compatibility alias - use Defaults module for new code
54
- MAX_RESULTS = Defaults::Limits::GLOB_RESULTS
53
+ # NOTE: Result limit now accessed via SwarmSDK.config.glob_result_limit
55
54
 
56
55
  def execute(pattern:, path: nil)
57
56
  # Validate inputs
@@ -108,8 +107,9 @@ module SwarmSDK
108
107
  matches.sort_by! { |f| -File.mtime(f).to_i }
109
108
 
110
109
  # Limit results
111
- if matches.count > MAX_RESULTS
112
- 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)
113
113
  truncated = true
114
114
  else
115
115
  truncated = false
@@ -123,7 +123,7 @@ module SwarmSDK
123
123
  output += <<~REMINDER
124
124
 
125
125
  <system-reminder>
126
- 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).
127
127
  Consider using a more specific pattern to narrow your search.
128
128
  </system-reminder>
129
129
  REMINDER
@@ -10,9 +10,7 @@ module SwarmSDK
10
10
  class Read < RubyLLM::Tool
11
11
  include PathResolver
12
12
 
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
13
+ # NOTE: Line length and limit now accessed via SwarmSDK.config
16
14
 
17
15
  # List of available document converters
18
16
  CONVERTERS = [
@@ -153,18 +151,20 @@ module SwarmSDK
153
151
  lines = lines.drop(start_line)
154
152
 
155
153
  # Apply limit if specified, otherwise use default
156
- effective_limit = limit || DEFAULT_LIMIT
154
+ default_limit = SwarmSDK.config.read_line_limit
155
+ effective_limit = limit || default_limit
157
156
  lines = lines.take(effective_limit)
158
- truncated = limit.nil? && total_lines > DEFAULT_LIMIT
157
+ truncated = limit.nil? && total_lines > default_limit
159
158
 
160
159
  # Format with line numbers (cat -n style)
160
+ max_line_length = SwarmSDK.config.line_character_limit
161
161
  output_lines = lines.each_with_index.map do |line, idx|
162
162
  line_number = start_line + idx + 1
163
163
  display_line = line.chomp
164
164
 
165
165
  # Truncate long lines
166
- if display_line.length > MAX_LINE_LENGTH
167
- 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]
168
168
  display_line += "... (line truncated)"
169
169
  end
170
170
 
@@ -203,7 +203,7 @@ module SwarmSDK
203
203
 
204
204
  if truncated
205
205
  reminders << ""
206
- 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."
207
207
  end
208
208
 
209
209
  reminders << "</system-reminder>"
@@ -21,7 +21,7 @@ module SwarmSDK
21
21
  super() # Initialize parent Storage class
22
22
  @entries = {}
23
23
  @total_size = 0
24
- @total_size_limit = total_size_limit || Defaults::Storage::TOTAL_SIZE_BYTES
24
+ @total_size_limit = total_size_limit || SwarmSDK.config.scratchpad_total_size_limit
25
25
  @mutex = Mutex.new
26
26
  end
27
27
 
@@ -41,8 +41,9 @@ module SwarmSDK
41
41
  content_size = content.bytesize
42
42
 
43
43
  # Check entry size limit
44
- if content_size > Defaults::Storage::ENTRY_SIZE_BYTES
45
- raise ArgumentError, "Content exceeds maximum size (#{format_bytes(Defaults::Storage::ENTRY_SIZE_BYTES)}). " \
44
+ entry_size_limit = SwarmSDK.config.scratchpad_entry_size_limit
45
+ if content_size > entry_size_limit
46
+ raise ArgumentError, "Content exceeds maximum size (#{format_bytes(entry_size_limit)}). " \
46
47
  "Current: #{format_bytes(content_size)}"
47
48
  end
48
49
 
@@ -11,7 +11,6 @@ module SwarmSDK
11
11
  super()
12
12
  @cache = {}
13
13
  @cache_ttl = 900 # 15 minutes in seconds
14
- @llm_enabled = SwarmSDK.settings.webfetch_llm_enabled?
15
14
  end
16
15
 
17
16
  def name
@@ -51,17 +50,18 @@ module SwarmSDK
51
50
  desc: "The prompt to run on the fetched content. Required when SwarmSDK is configured with webfetch_provider and webfetch_model. Optional otherwise (ignored if LLM processing not configured).",
52
51
  required: false
53
52
 
54
- # Backward compatibility aliases - use Defaults module for new code
55
- MAX_CONTENT_LENGTH = Defaults::Limits::WEB_FETCH_CHARACTERS
53
+ # NOTE: Content length and timeout now accessed via SwarmSDK.config
56
54
  USER_AGENT = "SwarmSDK WebFetch Tool (https://github.com/parruda/claude-swarm)"
57
- TIMEOUT = Defaults::Timeouts::WEB_FETCH_SECONDS
58
55
 
59
56
  def execute(url:, prompt: nil)
60
57
  # Validate inputs
61
58
  return validation_error("url is required") if url.nil? || url.empty?
62
59
 
60
+ # Check if LLM processing is enabled (lazy check)
61
+ llm_enabled = SwarmSDK.config.webfetch_llm_enabled?
62
+
63
63
  # Validate prompt when LLM processing is enabled
64
- if @llm_enabled && (prompt.nil? || prompt.empty?)
64
+ if llm_enabled && (prompt.nil? || prompt.empty?)
65
65
  return validation_error("prompt is required when LLM processing is configured")
66
66
  end
67
67
 
@@ -70,7 +70,7 @@ module SwarmSDK
70
70
  return validation_error("Invalid URL format: #{url}") unless normalized_url
71
71
 
72
72
  # Check cache first (cache key includes prompt if LLM is enabled)
73
- cache_key = @llm_enabled ? "#{normalized_url}:#{prompt}" : normalized_url
73
+ cache_key = llm_enabled ? "#{normalized_url}:#{prompt}" : normalized_url
74
74
  cached = get_from_cache(cache_key)
75
75
  return cached if cached
76
76
 
@@ -87,13 +87,14 @@ module SwarmSDK
87
87
  markdown_content = html_to_markdown(fetch_result[:body])
88
88
 
89
89
  # Truncate if too long
90
- if markdown_content.length > MAX_CONTENT_LENGTH
91
- markdown_content = markdown_content[0...MAX_CONTENT_LENGTH]
90
+ max_content = SwarmSDK.config.web_fetch_character_limit
91
+ if markdown_content.length > max_content
92
+ markdown_content = markdown_content[0...max_content]
92
93
  markdown_content += "\n\n[Content truncated due to length]"
93
94
  end
94
95
 
95
96
  # Process with AI model if LLM is enabled, otherwise return markdown
96
- result = if @llm_enabled
97
+ result = if llm_enabled
97
98
  process_with_llm(markdown_content, prompt, normalized_url)
98
99
  else
99
100
  markdown_content
@@ -143,12 +144,13 @@ module SwarmSDK
143
144
  require "faraday"
144
145
  require "faraday/follow_redirects"
145
146
 
147
+ timeout = SwarmSDK.config.web_fetch_timeout
146
148
  response = Faraday.new(url: url) do |conn|
147
149
  conn.request(:url_encoded)
148
150
  conn.response(:follow_redirects, limit: 5)
149
151
  conn.adapter(Faraday.default_adapter)
150
- conn.options.timeout = TIMEOUT
151
- conn.options.open_timeout = TIMEOUT
152
+ conn.options.timeout = timeout
153
+ conn.options.open_timeout = timeout
152
154
  end.get do |req|
153
155
  req.headers["User-Agent"] = USER_AGENT
154
156
  req.headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
@@ -167,7 +169,7 @@ module SwarmSDK
167
169
  redirect_url: redirect_url,
168
170
  }
169
171
  rescue Faraday::TimeoutError
170
- error("Request timed out after #{TIMEOUT} seconds")
172
+ error("Request timed out after #{SwarmSDK.config.web_fetch_timeout} seconds")
171
173
  rescue Faraday::ConnectionFailed => e
172
174
  error("Connection failed: #{e.message}")
173
175
  rescue StandardError => e
@@ -194,17 +196,17 @@ module SwarmSDK
194
196
  Please respond to the user's request based on the content above.
195
197
  PROMPT
196
198
 
197
- # Get settings
198
- config = SwarmSDK.settings
199
+ # Get config
200
+ sdk_config = SwarmSDK.config
199
201
 
200
202
  # Build chat with configured provider and model
201
203
  chat_params = {
202
- model: config.webfetch_model,
203
- provider: config.webfetch_provider.to_sym,
204
+ model: sdk_config.webfetch_model,
205
+ provider: sdk_config.webfetch_provider.to_sym,
204
206
  }
205
- chat_params[:base_url] = config.webfetch_base_url if config.webfetch_base_url
207
+ chat_params[:base_url] = sdk_config.webfetch_base_url if sdk_config.webfetch_base_url
206
208
 
207
- chat = RubyLLM.chat(**chat_params).with_params(max_tokens: config.webfetch_max_tokens)
209
+ chat = RubyLLM.chat(**chat_params).with_params(max_tokens: sdk_config.webfetch_max_tokens)
208
210
 
209
211
  response = chat.ask(full_prompt)
210
212
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- VERSION = "2.3.0"
4
+ VERSION = "2.5.1"
5
5
  end