swarm_sdk 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/agent/chat.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +32 -3
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +3 -5
- data/lib/swarm_sdk/agent/context.rb +1 -2
- data/lib/swarm_sdk/agent/definition.rb +3 -3
- data/lib/swarm_sdk/builders/base_builder.rb +6 -6
- data/lib/swarm_sdk/config.rb +301 -0
- data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
- 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/swarm/tool_configurator.rb +2 -2
- data/lib/swarm_sdk/swarm.rb +12 -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/node_builder.rb +4 -2
- data/lib/swarm_sdk/workflow/transformer_executor.rb +4 -3
- data/lib/swarm_sdk.rb +31 -101
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9921bd8cfa2073d8a470b7c9f39ec925f07a4d4c3a38ea03fd1e5bc5e83fd2a7
|
|
4
|
+
data.tar.gz: e7a77f34453d3d3521eea2c3461563050b5057b340f54e7544ad3d9efecbda5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a37d856b2989058fa3a62fb9847de0fe810d03140fb4fee10f88af2f5995c3a79cd2f6443e233403499619c8a9b99c825f64a68386d411a35eb92accc11a225
|
|
7
|
+
data.tar.gz: 42e1849a8b7d9660627b0b1477cf558829e0b7afd46f4087c1a5204a1dd0dd7ae76a09be2c963884fc61f926c3b8c93fd17331cc5d8d1fa5b025c5c44fc71740
|
data/lib/swarm_sdk/agent/chat.rb
CHANGED
|
@@ -122,7 +122,7 @@ module SwarmSDK
|
|
|
122
122
|
max_concurrent_tools = definition[:max_concurrent_tools]
|
|
123
123
|
base_url = definition[:base_url]
|
|
124
124
|
api_version = definition[:api_version]
|
|
125
|
-
timeout = definition[:timeout] ||
|
|
125
|
+
timeout = definition[:timeout] || SwarmSDK.config.agent_request_timeout
|
|
126
126
|
assume_model_exists = definition[:assume_model_exists]
|
|
127
127
|
system_prompt = definition[:system_prompt]
|
|
128
128
|
parameters = definition[:parameters]
|
|
@@ -117,7 +117,7 @@ module SwarmSDK
|
|
|
117
117
|
|
|
118
118
|
# Trigger automatic compression at 60% ONLY if no custom handler
|
|
119
119
|
compression_triggered = false
|
|
120
|
-
if threshold ==
|
|
120
|
+
if threshold == SwarmSDK.config.context_compression_threshold && !has_custom_handler
|
|
121
121
|
compressed_count = apply_automatic_compression
|
|
122
122
|
compression_triggered = compressed_count > 0
|
|
123
123
|
end
|
|
@@ -47,7 +47,7 @@ module SwarmSDK
|
|
|
47
47
|
#
|
|
48
48
|
# @return [RubyLLM::Chat] Chat instance
|
|
49
49
|
def instantiate_chat(model_id:, provider_name:, base_url:, timeout:, assume_model_exists:, chat_options:)
|
|
50
|
-
if base_url || timeout !=
|
|
50
|
+
if base_url || timeout != SwarmSDK.config.agent_request_timeout
|
|
51
51
|
instantiate_with_custom_context(
|
|
52
52
|
model_id: model_id,
|
|
53
53
|
provider_name: provider_name,
|
|
@@ -123,22 +123,51 @@ module SwarmSDK
|
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
# Configure provider-specific base URL
|
|
126
|
+
#
|
|
127
|
+
# @param config [RubyLLM::Config] RubyLLM configuration context
|
|
128
|
+
# @param provider [String] Provider name
|
|
129
|
+
# @param base_url [String] Custom base URL
|
|
130
|
+
# @raise [ConfigurationError] If API key is required but not configured
|
|
131
|
+
# @raise [ArgumentError] If provider doesn't support custom base_url
|
|
126
132
|
def configure_provider_base_url(config, provider, base_url)
|
|
127
133
|
case provider.to_s
|
|
128
134
|
when "openai", "deepseek", "perplexity", "mistral", "openrouter"
|
|
129
135
|
config.openai_api_base = base_url
|
|
130
|
-
config.openai_api_key
|
|
136
|
+
api_key = SwarmSDK.config.openai_api_key
|
|
137
|
+
|
|
138
|
+
# For local endpoints, API key is optional
|
|
139
|
+
# For cloud endpoints, require API key
|
|
140
|
+
unless api_key || local_endpoint?(base_url)
|
|
141
|
+
raise ConfigurationError,
|
|
142
|
+
"OpenAI API key required for '#{provider}' with base_url '#{base_url}'. " \
|
|
143
|
+
"Configure with: SwarmSDK.configure { |c| c.openai_api_key = '...' }"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
config.openai_api_key = api_key if api_key
|
|
131
147
|
config.openai_use_system_role = true
|
|
132
148
|
when "ollama"
|
|
133
149
|
config.ollama_api_base = base_url
|
|
150
|
+
# Ollama doesn't need an API key
|
|
134
151
|
when "gpustack"
|
|
135
152
|
config.gpustack_api_base = base_url
|
|
136
|
-
config.gpustack_api_key
|
|
153
|
+
api_key = SwarmSDK.config.gpustack_api_key
|
|
154
|
+
config.gpustack_api_key = api_key if api_key
|
|
137
155
|
else
|
|
138
156
|
raise ArgumentError, "Provider '#{provider}' doesn't support custom base_url."
|
|
139
157
|
end
|
|
140
158
|
end
|
|
141
159
|
|
|
160
|
+
# Check if a URL points to a local endpoint
|
|
161
|
+
#
|
|
162
|
+
# @param url [String] URL to check
|
|
163
|
+
# @return [Boolean] true if URL is a local endpoint
|
|
164
|
+
def local_endpoint?(url)
|
|
165
|
+
uri = URI.parse(url)
|
|
166
|
+
["localhost", "127.0.0.1", "0.0.0.0"].include?(uri.host)
|
|
167
|
+
rescue URI::InvalidURIError
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
142
171
|
# Fetch real model info for accurate context tracking
|
|
143
172
|
#
|
|
144
173
|
# @param model_id [String] Model ID to lookup
|
|
@@ -22,9 +22,6 @@ module SwarmSDK
|
|
|
22
22
|
<system-reminder>The TodoWrite tool hasn't been used recently. If you're working on tasks that would benefit from tracking progress, consider using the TodoWrite tool to track progress. Also consider cleaning up the todo list if has become stale and no longer matches what you are working on. Only use it if it's relevant to the current work. This is just a gentle reminder - ignore if not applicable.</system-reminder>
|
|
23
23
|
REMINDER
|
|
24
24
|
|
|
25
|
-
# Backward compatibility alias - use Defaults module for new code
|
|
26
|
-
TODOWRITE_REMINDER_INTERVAL = Defaults::Context::TODOWRITE_REMINDER_INTERVAL
|
|
27
|
-
|
|
28
25
|
class << self
|
|
29
26
|
# Check if this is the first user message in the conversation
|
|
30
27
|
#
|
|
@@ -106,15 +103,16 @@ module SwarmSDK
|
|
|
106
103
|
end
|
|
107
104
|
|
|
108
105
|
# Check if enough messages have passed since last TodoWrite
|
|
106
|
+
reminder_interval = SwarmSDK.config.todowrite_reminder_interval
|
|
109
107
|
if last_todo_index.nil? && last_todowrite_index.nil?
|
|
110
108
|
# Never used TodoWrite - check if we've exceeded interval
|
|
111
|
-
chat.message_count >=
|
|
109
|
+
chat.message_count >= reminder_interval
|
|
112
110
|
elsif last_todo_index
|
|
113
111
|
# Recently used - don't remind
|
|
114
112
|
false
|
|
115
113
|
elsif last_todowrite_index
|
|
116
114
|
# Used before - check if interval has passed
|
|
117
|
-
chat.message_count - last_todowrite_index >=
|
|
115
|
+
chat.message_count - last_todowrite_index >= reminder_interval
|
|
118
116
|
else
|
|
119
117
|
false
|
|
120
118
|
end
|
|
@@ -30,8 +30,7 @@ module SwarmSDK
|
|
|
30
30
|
# 60% triggers automatic compression, 80%/90% are informational warnings
|
|
31
31
|
CONTEXT_WARNING_THRESHOLDS = [60, 80, 90].freeze
|
|
32
32
|
|
|
33
|
-
#
|
|
34
|
-
COMPRESSION_THRESHOLD = Defaults::Context::COMPRESSION_THRESHOLD_PERCENT
|
|
33
|
+
# NOTE: Compression threshold now accessed via SwarmSDK.config.context_compression_threshold
|
|
35
34
|
|
|
36
35
|
attr_reader :name, :delegation_tools, :metadata, :warning_thresholds_hit, :swarm_id, :parent_swarm_id
|
|
37
36
|
|
|
@@ -67,14 +67,14 @@ module SwarmSDK
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
@description = config[:description]
|
|
70
|
-
@model = config[:model] ||
|
|
71
|
-
@provider = config[:provider] ||
|
|
70
|
+
@model = config[:model] || SwarmSDK.config.default_model
|
|
71
|
+
@provider = config[:provider] || SwarmSDK.config.default_provider
|
|
72
72
|
@base_url = config[:base_url]
|
|
73
73
|
@api_version = config[:api_version]
|
|
74
74
|
@context_window = config[:context_window] # Explicit context window override
|
|
75
75
|
@parameters = config[:parameters] || {}
|
|
76
76
|
@headers = Utils.stringify_keys(config[:headers] || {})
|
|
77
|
-
@timeout = config[:timeout] ||
|
|
77
|
+
@timeout = config[:timeout] || SwarmSDK.config.agent_request_timeout
|
|
78
78
|
@bypass_permissions = config[:bypass_permissions] || false
|
|
79
79
|
@max_concurrent_tools = config[:max_concurrent_tools]
|
|
80
80
|
# Always assume model exists - SwarmSDK validates models separately using models.json
|
|
@@ -318,7 +318,7 @@ module SwarmSDK
|
|
|
318
318
|
# @return [void]
|
|
319
319
|
def validate_all_agents_filesystem_tools
|
|
320
320
|
resolved_setting = if @allow_filesystem_tools.nil?
|
|
321
|
-
SwarmSDK.
|
|
321
|
+
SwarmSDK.config.allow_filesystem_tools
|
|
322
322
|
else
|
|
323
323
|
@allow_filesystem_tools
|
|
324
324
|
end
|
|
@@ -333,10 +333,10 @@ module SwarmSDK
|
|
|
333
333
|
return if forbidden.empty?
|
|
334
334
|
|
|
335
335
|
raise ConfigurationError,
|
|
336
|
-
"Filesystem tools are globally disabled (SwarmSDK.
|
|
336
|
+
"Filesystem tools are globally disabled (SwarmSDK.config.allow_filesystem_tools = false) " \
|
|
337
337
|
"but all_agents configuration includes: #{forbidden.join(", ")}.\n\n" \
|
|
338
338
|
"This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
|
|
339
|
-
"To use filesystem tools, set SwarmSDK.
|
|
339
|
+
"To use filesystem tools, set SwarmSDK.config.allow_filesystem_tools = true before loading the swarm."
|
|
340
340
|
end
|
|
341
341
|
|
|
342
342
|
# Validate individual agent filesystem tools
|
|
@@ -345,7 +345,7 @@ module SwarmSDK
|
|
|
345
345
|
# @return [void]
|
|
346
346
|
def validate_agent_filesystem_tools
|
|
347
347
|
resolved_setting = if @allow_filesystem_tools.nil?
|
|
348
|
-
SwarmSDK.
|
|
348
|
+
SwarmSDK.config.allow_filesystem_tools
|
|
349
349
|
else
|
|
350
350
|
@allow_filesystem_tools
|
|
351
351
|
end
|
|
@@ -373,10 +373,10 @@ module SwarmSDK
|
|
|
373
373
|
next if forbidden.empty?
|
|
374
374
|
|
|
375
375
|
raise ConfigurationError,
|
|
376
|
-
"Filesystem tools are globally disabled (SwarmSDK.
|
|
376
|
+
"Filesystem tools are globally disabled (SwarmSDK.config.allow_filesystem_tools = false) " \
|
|
377
377
|
"but agent '#{agent_name}' attempts to use: #{forbidden.join(", ")}.\n\n" \
|
|
378
378
|
"This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
|
|
379
|
-
"To use filesystem tools, set SwarmSDK.
|
|
379
|
+
"To use filesystem tools, set SwarmSDK.config.allow_filesystem_tools = true before loading the swarm."
|
|
380
380
|
end
|
|
381
381
|
end
|
|
382
382
|
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Centralized configuration for SwarmSDK
|
|
5
|
+
#
|
|
6
|
+
# Config provides a single entry point for all SwarmSDK configuration,
|
|
7
|
+
# including API keys (proxied to RubyLLM), defaults override, and
|
|
8
|
+
# WebFetch settings.
|
|
9
|
+
#
|
|
10
|
+
# ## Priority Order
|
|
11
|
+
#
|
|
12
|
+
# Configuration values are resolved in this order:
|
|
13
|
+
# 1. Explicit value (set via SwarmSDK.configure)
|
|
14
|
+
# 2. Environment variable
|
|
15
|
+
# 3. Module default (from SwarmSDK::Defaults)
|
|
16
|
+
#
|
|
17
|
+
# ## API Key Proxying
|
|
18
|
+
#
|
|
19
|
+
# API keys are automatically proxied to RubyLLM.config when set,
|
|
20
|
+
# ensuring RubyLLM always has the correct credentials.
|
|
21
|
+
#
|
|
22
|
+
# @example Basic configuration
|
|
23
|
+
# SwarmSDK.configure do |config|
|
|
24
|
+
# config.openai_api_key = "sk-..."
|
|
25
|
+
# config.default_model = "claude-sonnet-4"
|
|
26
|
+
# config.agent_request_timeout = 600
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @example Testing setup
|
|
30
|
+
# def setup
|
|
31
|
+
# SwarmSDK.reset_config!
|
|
32
|
+
# end
|
|
33
|
+
class Config
|
|
34
|
+
# API keys that proxy to RubyLLM.config
|
|
35
|
+
# Maps SwarmSDK config key => [RubyLLM config key, ENV variable]
|
|
36
|
+
API_KEY_MAPPINGS = {
|
|
37
|
+
openai_api_key: [:openai_api_key, "OPENAI_API_KEY"],
|
|
38
|
+
openai_api_base: [:openai_api_base, "OPENAI_API_BASE"],
|
|
39
|
+
openai_organization_id: [:openai_organization_id, "OPENAI_ORG_ID"],
|
|
40
|
+
openai_project_id: [:openai_project_id, "OPENAI_PROJECT_ID"],
|
|
41
|
+
anthropic_api_key: [:anthropic_api_key, "ANTHROPIC_API_KEY"],
|
|
42
|
+
gemini_api_key: [:gemini_api_key, "GEMINI_API_KEY"],
|
|
43
|
+
gemini_api_base: [:gemini_api_base, "GEMINI_API_BASE"],
|
|
44
|
+
vertexai_project_id: [:vertexai_project_id, "GOOGLE_CLOUD_PROJECT"],
|
|
45
|
+
vertexai_location: [:vertexai_location, "GOOGLE_CLOUD_LOCATION"],
|
|
46
|
+
deepseek_api_key: [:deepseek_api_key, "DEEPSEEK_API_KEY"],
|
|
47
|
+
mistral_api_key: [:mistral_api_key, "MISTRAL_API_KEY"],
|
|
48
|
+
perplexity_api_key: [:perplexity_api_key, "PERPLEXITY_API_KEY"],
|
|
49
|
+
openrouter_api_key: [:openrouter_api_key, "OPENROUTER_API_KEY"],
|
|
50
|
+
bedrock_api_key: [:bedrock_api_key, "AWS_ACCESS_KEY_ID"],
|
|
51
|
+
bedrock_secret_key: [:bedrock_secret_key, "AWS_SECRET_ACCESS_KEY"],
|
|
52
|
+
bedrock_region: [:bedrock_region, "AWS_REGION"],
|
|
53
|
+
bedrock_session_token: [:bedrock_session_token, "AWS_SESSION_TOKEN"],
|
|
54
|
+
ollama_api_base: [:ollama_api_base, "OLLAMA_API_BASE"],
|
|
55
|
+
gpustack_api_base: [:gpustack_api_base, "GPUSTACK_API_BASE"],
|
|
56
|
+
gpustack_api_key: [:gpustack_api_key, "GPUSTACK_API_KEY"],
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
# SwarmSDK defaults that can be overridden
|
|
60
|
+
# Maps config key => [ENV variable, default proc]
|
|
61
|
+
DEFAULTS_MAPPINGS = {
|
|
62
|
+
default_model: ["SWARM_SDK_DEFAULT_MODEL", -> { Defaults::Agent::MODEL }],
|
|
63
|
+
default_provider: ["SWARM_SDK_DEFAULT_PROVIDER", -> { Defaults::Agent::PROVIDER }],
|
|
64
|
+
agent_request_timeout: ["SWARM_SDK_AGENT_REQUEST_TIMEOUT", -> { Defaults::Timeouts::AGENT_REQUEST_SECONDS }],
|
|
65
|
+
bash_command_timeout: ["SWARM_SDK_BASH_COMMAND_TIMEOUT", -> { Defaults::Timeouts::BASH_COMMAND_MS }],
|
|
66
|
+
bash_command_max_timeout: ["SWARM_SDK_BASH_COMMAND_MAX_TIMEOUT", -> { Defaults::Timeouts::BASH_COMMAND_MAX_MS }],
|
|
67
|
+
web_fetch_timeout: ["SWARM_SDK_WEB_FETCH_TIMEOUT", -> { Defaults::Timeouts::WEB_FETCH_SECONDS }],
|
|
68
|
+
hook_shell_timeout: ["SWARM_SDK_HOOK_SHELL_TIMEOUT", -> { Defaults::Timeouts::HOOK_SHELL_SECONDS }],
|
|
69
|
+
transformer_command_timeout: ["SWARM_SDK_TRANSFORMER_COMMAND_TIMEOUT", -> { Defaults::Timeouts::TRANSFORMER_COMMAND_SECONDS }],
|
|
70
|
+
global_concurrency_limit: ["SWARM_SDK_GLOBAL_CONCURRENCY_LIMIT", -> { Defaults::Concurrency::GLOBAL_LIMIT }],
|
|
71
|
+
local_concurrency_limit: ["SWARM_SDK_LOCAL_CONCURRENCY_LIMIT", -> { Defaults::Concurrency::LOCAL_LIMIT }],
|
|
72
|
+
output_character_limit: ["SWARM_SDK_OUTPUT_CHARACTER_LIMIT", -> { Defaults::Limits::OUTPUT_CHARACTERS }],
|
|
73
|
+
read_line_limit: ["SWARM_SDK_READ_LINE_LIMIT", -> { Defaults::Limits::READ_LINES }],
|
|
74
|
+
line_character_limit: ["SWARM_SDK_LINE_CHARACTER_LIMIT", -> { Defaults::Limits::LINE_CHARACTERS }],
|
|
75
|
+
web_fetch_character_limit: ["SWARM_SDK_WEB_FETCH_CHARACTER_LIMIT", -> { Defaults::Limits::WEB_FETCH_CHARACTERS }],
|
|
76
|
+
glob_result_limit: ["SWARM_SDK_GLOB_RESULT_LIMIT", -> { Defaults::Limits::GLOB_RESULTS }],
|
|
77
|
+
scratchpad_entry_size_limit: ["SWARM_SDK_SCRATCHPAD_ENTRY_SIZE_LIMIT", -> { Defaults::Storage::ENTRY_SIZE_BYTES }],
|
|
78
|
+
scratchpad_total_size_limit: ["SWARM_SDK_SCRATCHPAD_TOTAL_SIZE_LIMIT", -> { Defaults::Storage::TOTAL_SIZE_BYTES }],
|
|
79
|
+
context_compression_threshold: ["SWARM_SDK_CONTEXT_COMPRESSION_THRESHOLD", -> { Defaults::Context::COMPRESSION_THRESHOLD_PERCENT }],
|
|
80
|
+
todowrite_reminder_interval: ["SWARM_SDK_TODOWRITE_REMINDER_INTERVAL", -> { Defaults::Context::TODOWRITE_REMINDER_INTERVAL }],
|
|
81
|
+
chars_per_token_prose: ["SWARM_SDK_CHARS_PER_TOKEN_PROSE", -> { Defaults::TokenEstimation::CHARS_PER_TOKEN_PROSE }],
|
|
82
|
+
chars_per_token_code: ["SWARM_SDK_CHARS_PER_TOKEN_CODE", -> { Defaults::TokenEstimation::CHARS_PER_TOKEN_CODE }],
|
|
83
|
+
mcp_log_level: ["SWARM_SDK_MCP_LOG_LEVEL", -> { Defaults::Logging::MCP_LOG_LEVEL }],
|
|
84
|
+
}.freeze
|
|
85
|
+
|
|
86
|
+
# WebFetch and control settings
|
|
87
|
+
# Maps config key => [ENV variable, default value]
|
|
88
|
+
SETTINGS_MAPPINGS = {
|
|
89
|
+
webfetch_provider: ["SWARM_SDK_WEBFETCH_PROVIDER", nil],
|
|
90
|
+
webfetch_model: ["SWARM_SDK_WEBFETCH_MODEL", nil],
|
|
91
|
+
webfetch_base_url: ["SWARM_SDK_WEBFETCH_BASE_URL", nil],
|
|
92
|
+
webfetch_max_tokens: ["SWARM_SDK_WEBFETCH_MAX_TOKENS", 4096],
|
|
93
|
+
allow_filesystem_tools: ["SWARM_SDK_ALLOW_FILESYSTEM_TOOLS", true],
|
|
94
|
+
}.freeze
|
|
95
|
+
|
|
96
|
+
class << self
|
|
97
|
+
# Get the singleton Config instance
|
|
98
|
+
#
|
|
99
|
+
# @return [Config] The singleton instance
|
|
100
|
+
def instance
|
|
101
|
+
@instance ||= new
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Reset the Config instance
|
|
105
|
+
#
|
|
106
|
+
# Clears all configuration including explicit values and cached ENV values.
|
|
107
|
+
# Use in tests to ensure clean state.
|
|
108
|
+
#
|
|
109
|
+
# @return [void]
|
|
110
|
+
def reset!
|
|
111
|
+
@instance = nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Initialize a new Config instance
|
|
116
|
+
#
|
|
117
|
+
# @note Use Config.instance instead of new for the singleton pattern
|
|
118
|
+
def initialize
|
|
119
|
+
@explicit_values = {}
|
|
120
|
+
@env_values = {}
|
|
121
|
+
@env_loaded = false
|
|
122
|
+
@env_mutex = Mutex.new
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# ========== API Key Accessors (with RubyLLM proxying) ==========
|
|
126
|
+
|
|
127
|
+
# @!method openai_api_key
|
|
128
|
+
# Get the OpenAI API key
|
|
129
|
+
# @return [String, nil] The API key
|
|
130
|
+
#
|
|
131
|
+
# @!method openai_api_key=(value)
|
|
132
|
+
# Set the OpenAI API key (proxied to RubyLLM)
|
|
133
|
+
# @param value [String] The API key
|
|
134
|
+
|
|
135
|
+
API_KEY_MAPPINGS.each_key do |config_key|
|
|
136
|
+
ruby_llm_key, _ = API_KEY_MAPPINGS[config_key]
|
|
137
|
+
|
|
138
|
+
# Getter
|
|
139
|
+
define_method(config_key) do
|
|
140
|
+
ensure_env_loaded!
|
|
141
|
+
@explicit_values[config_key] || @env_values[config_key]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Setter with RubyLLM proxying
|
|
145
|
+
define_method("#{config_key}=") do |value|
|
|
146
|
+
@explicit_values[config_key] = value
|
|
147
|
+
RubyLLM.config.public_send("#{ruby_llm_key}=", value) if value
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# ========== Defaults Accessors (with module constant fallback) ==========
|
|
152
|
+
|
|
153
|
+
# @!method default_model
|
|
154
|
+
# Get the default model
|
|
155
|
+
# @return [String] The default model (falls back to Defaults::Agent::MODEL)
|
|
156
|
+
#
|
|
157
|
+
# @!method default_model=(value)
|
|
158
|
+
# Set the default model
|
|
159
|
+
# @param value [String] The default model
|
|
160
|
+
|
|
161
|
+
DEFAULTS_MAPPINGS.each_key do |config_key|
|
|
162
|
+
_env_key, default_proc = DEFAULTS_MAPPINGS[config_key]
|
|
163
|
+
|
|
164
|
+
# Getter with default fallback
|
|
165
|
+
define_method(config_key) do
|
|
166
|
+
ensure_env_loaded!
|
|
167
|
+
@explicit_values[config_key] || @env_values[config_key] || default_proc.call
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Setter
|
|
171
|
+
define_method("#{config_key}=") do |value|
|
|
172
|
+
@explicit_values[config_key] = value
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# ========== Settings Accessors (WebFetch and control) ==========
|
|
177
|
+
|
|
178
|
+
# @!method webfetch_provider
|
|
179
|
+
# Get the WebFetch LLM provider
|
|
180
|
+
# @return [String, nil] The provider
|
|
181
|
+
#
|
|
182
|
+
# @!method allow_filesystem_tools
|
|
183
|
+
# Get whether filesystem tools are allowed
|
|
184
|
+
# @return [Boolean] true if allowed
|
|
185
|
+
|
|
186
|
+
SETTINGS_MAPPINGS.each_key do |config_key|
|
|
187
|
+
_env_key, default_value = SETTINGS_MAPPINGS[config_key]
|
|
188
|
+
|
|
189
|
+
# Getter with default fallback
|
|
190
|
+
define_method(config_key) do
|
|
191
|
+
ensure_env_loaded!
|
|
192
|
+
if @explicit_values.key?(config_key)
|
|
193
|
+
@explicit_values[config_key]
|
|
194
|
+
elsif @env_values.key?(config_key)
|
|
195
|
+
@env_values[config_key]
|
|
196
|
+
else
|
|
197
|
+
default_value
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Setter
|
|
202
|
+
define_method("#{config_key}=") do |value|
|
|
203
|
+
@explicit_values[config_key] = value
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# ========== Convenience Methods ==========
|
|
208
|
+
|
|
209
|
+
# Check if WebFetch LLM processing is enabled
|
|
210
|
+
#
|
|
211
|
+
# WebFetch uses LLM processing when both provider and model are configured.
|
|
212
|
+
#
|
|
213
|
+
# @return [Boolean] true if WebFetch LLM is configured
|
|
214
|
+
def webfetch_llm_enabled?
|
|
215
|
+
!webfetch_provider.nil? && !webfetch_model.nil?
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
# Ensure ENV values are loaded (lazy loading with double-check locking)
|
|
221
|
+
#
|
|
222
|
+
# Thread-safe lazy loading of ENV values. Only loads once per Config instance.
|
|
223
|
+
#
|
|
224
|
+
# @return [void]
|
|
225
|
+
def ensure_env_loaded!
|
|
226
|
+
return if @env_loaded
|
|
227
|
+
|
|
228
|
+
@env_mutex.synchronize do
|
|
229
|
+
return if @env_loaded
|
|
230
|
+
|
|
231
|
+
load_env_values!
|
|
232
|
+
@env_loaded = true
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Load environment variable values
|
|
237
|
+
#
|
|
238
|
+
# Loads API keys (with RubyLLM proxying), defaults, and settings from ENV.
|
|
239
|
+
# Only loads values that haven't been explicitly set.
|
|
240
|
+
#
|
|
241
|
+
# @return [void]
|
|
242
|
+
def load_env_values!
|
|
243
|
+
# Load API keys and proxy to RubyLLM
|
|
244
|
+
API_KEY_MAPPINGS.each do |config_key, (ruby_llm_key, env_key)|
|
|
245
|
+
next if @explicit_values.key?(config_key)
|
|
246
|
+
next unless ENV.key?(env_key)
|
|
247
|
+
|
|
248
|
+
value = ENV[env_key]
|
|
249
|
+
@env_values[config_key] = value
|
|
250
|
+
|
|
251
|
+
# Proxy to RubyLLM
|
|
252
|
+
RubyLLM.config.public_send("#{ruby_llm_key}=", value)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Load defaults (no RubyLLM proxy)
|
|
256
|
+
DEFAULTS_MAPPINGS.each do |config_key, (env_key, _default_proc)|
|
|
257
|
+
next if @explicit_values.key?(config_key)
|
|
258
|
+
next unless ENV.key?(env_key)
|
|
259
|
+
|
|
260
|
+
@env_values[config_key] = parse_env_value(ENV[env_key], config_key)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Load settings (no RubyLLM proxy)
|
|
264
|
+
SETTINGS_MAPPINGS.each do |config_key, (env_key, _default_value)|
|
|
265
|
+
next if @explicit_values.key?(config_key)
|
|
266
|
+
next unless ENV.key?(env_key)
|
|
267
|
+
|
|
268
|
+
@env_values[config_key] = parse_env_value(ENV[env_key], config_key)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Parse environment variable value to appropriate type
|
|
273
|
+
#
|
|
274
|
+
# Converts string ENV values to integers, floats, or booleans based on
|
|
275
|
+
# the configuration key pattern.
|
|
276
|
+
#
|
|
277
|
+
# @param value [String] The ENV value string
|
|
278
|
+
# @param key [Symbol] The configuration key
|
|
279
|
+
# @return [Integer, Float, Boolean, String] The parsed value
|
|
280
|
+
def parse_env_value(value, key)
|
|
281
|
+
case key
|
|
282
|
+
when :allow_filesystem_tools
|
|
283
|
+
# Convert string to boolean
|
|
284
|
+
case value.to_s.downcase
|
|
285
|
+
when "true", "yes", "1", "on", "enabled"
|
|
286
|
+
true
|
|
287
|
+
when "false", "no", "0", "off", "disabled"
|
|
288
|
+
false
|
|
289
|
+
else
|
|
290
|
+
true # Default to true if unrecognized
|
|
291
|
+
end
|
|
292
|
+
when /_timeout$/, /_limit$/, /_interval$/, /_threshold$/, :mcp_log_level, :webfetch_max_tokens
|
|
293
|
+
value.to_i
|
|
294
|
+
when /^chars_per_token/
|
|
295
|
+
value.to_f
|
|
296
|
+
else
|
|
297
|
+
value
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
@@ -17,10 +17,6 @@ module SwarmSDK
|
|
|
17
17
|
# total_tokens = TokenCounter.estimate_messages(messages)
|
|
18
18
|
#
|
|
19
19
|
class TokenCounter
|
|
20
|
-
# Backward compatibility aliases - use Defaults module for new code
|
|
21
|
-
CHARS_PER_TOKEN_PROSE = Defaults::TokenEstimation::CHARS_PER_TOKEN_PROSE
|
|
22
|
-
CHARS_PER_TOKEN_CODE = Defaults::TokenEstimation::CHARS_PER_TOKEN_CODE
|
|
23
|
-
|
|
24
20
|
class << self
|
|
25
21
|
# Estimate tokens for a single message
|
|
26
22
|
#
|
|
@@ -78,9 +74,9 @@ module SwarmSDK
|
|
|
78
74
|
|
|
79
75
|
# Choose characters per token based on content type
|
|
80
76
|
chars_per_token = if code_ratio > 0.1
|
|
81
|
-
|
|
77
|
+
SwarmSDK.config.chars_per_token_code # Code
|
|
82
78
|
else
|
|
83
|
-
|
|
79
|
+
SwarmSDK.config.chars_per_token_prose # Prose
|
|
84
80
|
end
|
|
85
81
|
|
|
86
82
|
(text.length / chars_per_token).ceil
|
|
@@ -167,7 +167,7 @@ module SwarmSDK
|
|
|
167
167
|
def create_hook_callback(hook_def, event_symbol, agent_name, swarm_name)
|
|
168
168
|
# Support both string and symbol keys (YAML may be symbolized)
|
|
169
169
|
command = hook_def[:command] || hook_def["command"]
|
|
170
|
-
timeout = hook_def[:timeout] || hook_def["timeout"] ||
|
|
170
|
+
timeout = hook_def[:timeout] || hook_def["timeout"] || SwarmSDK.config.hook_shell_timeout
|
|
171
171
|
|
|
172
172
|
lambda do |context|
|
|
173
173
|
input_json = build_input_json(context, event_symbol, agent_name)
|
|
@@ -191,7 +191,7 @@ module SwarmSDK
|
|
|
191
191
|
def create_all_agents_hook_callback(hook_def, event_symbol, swarm_name)
|
|
192
192
|
# Support both string and symbol keys (YAML may be symbolized)
|
|
193
193
|
command = hook_def[:command] || hook_def["command"]
|
|
194
|
-
timeout = hook_def[:timeout] || hook_def["timeout"] ||
|
|
194
|
+
timeout = hook_def[:timeout] || hook_def["timeout"] || SwarmSDK.config.hook_shell_timeout
|
|
195
195
|
|
|
196
196
|
lambda do |context|
|
|
197
197
|
# Agent name comes from context
|
|
@@ -217,7 +217,7 @@ module SwarmSDK
|
|
|
217
217
|
def create_swarm_hook_callback(hook_def, event_symbol, swarm_name)
|
|
218
218
|
# Support both string and symbol keys (YAML may be symbolized)
|
|
219
219
|
command = hook_def[:command] || hook_def["command"]
|
|
220
|
-
timeout = hook_def[:timeout] || hook_def["timeout"] ||
|
|
220
|
+
timeout = hook_def[:timeout] || hook_def["timeout"] || SwarmSDK.config.hook_shell_timeout
|
|
221
221
|
|
|
222
222
|
lambda do |context|
|
|
223
223
|
input_json = build_swarm_input_json(context, event_symbol, swarm_name)
|
|
@@ -47,8 +47,7 @@ module SwarmSDK
|
|
|
47
47
|
# )
|
|
48
48
|
# # => Result (continue or halt based on exit code)
|
|
49
49
|
class ShellExecutor
|
|
50
|
-
#
|
|
51
|
-
DEFAULT_TIMEOUT = Defaults::Timeouts::HOOK_SHELL_SECONDS
|
|
50
|
+
# NOTE: Timeout now accessed via SwarmSDK.config.hook_shell_timeout
|
|
52
51
|
|
|
53
52
|
class << self
|
|
54
53
|
# Execute a shell command hook
|
|
@@ -60,7 +59,9 @@ module SwarmSDK
|
|
|
60
59
|
# @param swarm_name [String, nil] Swarm name for environment variables
|
|
61
60
|
# @param event [Symbol] Event type for context-aware behavior
|
|
62
61
|
# @return [Result] Result based on exit code (continue or halt)
|
|
63
|
-
def execute(command:, input_json:, timeout:
|
|
62
|
+
def execute(command:, input_json:, timeout: nil, agent_name: nil, swarm_name: nil, event: nil)
|
|
63
|
+
timeout ||= SwarmSDK.config.hook_shell_timeout
|
|
64
|
+
|
|
64
65
|
# Build environment variables
|
|
65
66
|
env = build_environment(agent_name: agent_name, swarm_name: swarm_name)
|
|
66
67
|
|