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.
- checksums.yaml +4 -4
- data/{CHANGELOG.md → CHANGELOG.claude-swarm.md} +3 -0
- data/CLAUDE.md +0 -1
- data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +12 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +139 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +249 -1
- data/docs/v2/README.md +15 -5
- data/docs/v2/guides/complete-tutorial.md +93 -7
- data/docs/v2/guides/getting-started.md +3 -1
- data/docs/v2/guides/memory-adapters.md +41 -0
- data/docs/v2/guides/{migrating-to-2.3.md → migrating-to-2.x.md} +213 -8
- data/docs/v2/guides/plugins.md +52 -5
- data/docs/v2/guides/rails-integration.md +6 -0
- data/docs/v2/guides/swarm-memory.md +2 -13
- data/docs/v2/reference/cli.md +0 -1
- data/docs/v2/reference/configuration_reference.md +300 -0
- data/docs/v2/reference/event_payload_structures.md +26 -4
- data/docs/v2/reference/ruby-dsl.md +457 -4
- data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
- data/docs/v2/reference/yaml.md +2 -2
- data/lib/claude_swarm/mcp_generator.rb +1 -1
- data/lib/claude_swarm/orchestrator.rb +8 -1
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/core/semantic_index.rb +10 -2
- data/lib/swarm_memory/core/storage.rb +7 -2
- data/lib/swarm_memory/dsl/memory_config.rb +37 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +120 -27
- data/lib/swarm_memory/optimization/defragmenter.rb +1 -1
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +0 -1
- data/lib/swarm_memory/tools/load_skill.rb +0 -1
- data/lib/swarm_memory/tools/memory_edit.rb +2 -1
- data/lib/swarm_memory/tools/memory_read.rb +1 -1
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +7 -5
- data/lib/swarm_sdk/agent/chat.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +4 -0
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +38 -4
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +2 -2
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +3 -5
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +48 -0
- data/lib/swarm_sdk/agent/context.rb +1 -2
- data/lib/swarm_sdk/agent/definition.rb +3 -3
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +1 -1
- data/lib/swarm_sdk/agent_registry.rb +146 -0
- data/lib/swarm_sdk/builders/base_builder.rb +91 -12
- data/lib/swarm_sdk/config.rb +302 -0
- data/lib/swarm_sdk/configuration/parser.rb +22 -2
- data/lib/swarm_sdk/configuration.rb +13 -4
- data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
- data/lib/swarm_sdk/custom_tool_registry.rb +226 -0
- data/lib/swarm_sdk/hooks/adapter.rb +3 -3
- data/lib/swarm_sdk/hooks/shell_executor.rb +4 -3
- data/lib/swarm_sdk/models.json +4333 -1
- data/lib/swarm_sdk/models.rb +43 -2
- data/lib/swarm_sdk/plugin.rb +2 -2
- data/lib/swarm_sdk/result.rb +52 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +1 -1
- data/lib/swarm_sdk/swarm/hook_triggers.rb +1 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +1 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +18 -4
- data/lib/swarm_sdk/swarm.rb +76 -13
- data/lib/swarm_sdk/tools/bash.rb +7 -9
- data/lib/swarm_sdk/tools/glob.rb +5 -5
- data/lib/swarm_sdk/tools/read.rb +8 -8
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +4 -3
- data/lib/swarm_sdk/tools/web_fetch.rb +20 -18
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/workflow/builder.rb +49 -0
- data/lib/swarm_sdk/workflow/node_builder.rb +4 -2
- data/lib/swarm_sdk/workflow/transformer_executor.rb +4 -3
- data/lib/swarm_sdk.rb +261 -105
- data/swarm_cli.gemspec +1 -1
- data/swarm_memory.gemspec +8 -3
- data/swarm_sdk.gemspec +4 -4
- data/team_full.yml +104 -300
- metadata +9 -5
- data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
- /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
data/lib/swarm_sdk/models.rb
CHANGED
|
@@ -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 [
|
|
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
|
data/lib/swarm_sdk/plugin.rb
CHANGED
|
@@ -139,11 +139,11 @@ module SwarmSDK
|
|
|
139
139
|
[]
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
-
#
|
|
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
|
|
146
|
+
def memory_configured?(agent_definition)
|
|
147
147
|
false
|
|
148
148
|
end
|
|
149
149
|
|
data/lib/swarm_sdk/result.rb
CHANGED
|
@@ -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.
|
|
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)
|
|
@@ -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
|
-
#
|
|
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.
|
|
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.
|
|
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.
|
|
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|
|
data/lib/swarm_sdk/swarm.rb
CHANGED
|
@@ -67,8 +67,7 @@ module SwarmSDK
|
|
|
67
67
|
include LoggingCallbacks
|
|
68
68
|
include HookTriggers
|
|
69
69
|
|
|
70
|
-
#
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
155
|
+
# 2. Global config
|
|
157
156
|
@allow_filesystem_tools = if allow_filesystem_tools.nil?
|
|
158
|
-
SwarmSDK.
|
|
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
|
data/lib/swarm_sdk/tools/bash.rb
CHANGED
|
@@ -85,10 +85,7 @@ module SwarmSDK
|
|
|
85
85
|
desc: "Optional timeout in milliseconds (max 600000)",
|
|
86
86
|
required: false
|
|
87
87
|
|
|
88
|
-
#
|
|
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 ||
|
|
111
|
-
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
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
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>"
|
|
155
153
|
output = truncated
|
|
156
154
|
end
|
|
157
155
|
|
data/lib/swarm_sdk/tools/glob.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
112
|
-
|
|
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 #{
|
|
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
|
data/lib/swarm_sdk/tools/read.rb
CHANGED
|
@@ -10,9 +10,7 @@ module SwarmSDK
|
|
|
10
10
|
class Read < RubyLLM::Tool
|
|
11
11
|
include PathResolver
|
|
12
12
|
|
|
13
|
-
#
|
|
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
|
-
|
|
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 >
|
|
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 >
|
|
167
|
-
display_line = display_line[0...
|
|
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 #{
|
|
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 ||
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
#
|
|
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
|
|
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 =
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
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 =
|
|
151
|
-
conn.options.open_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 #{
|
|
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
|
|
198
|
-
|
|
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:
|
|
203
|
-
provider:
|
|
204
|
+
model: sdk_config.webfetch_model,
|
|
205
|
+
provider: sdk_config.webfetch_provider.to_sym,
|
|
204
206
|
}
|
|
205
|
-
chat_params[: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:
|
|
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
|
|
data/lib/swarm_sdk/version.rb
CHANGED