swarm_memory 2.1.2 → 2.1.4
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/claude_swarm/claude_mcp_server.rb +1 -0
- data/lib/claude_swarm/cli.rb +5 -18
- data/lib/claude_swarm/configuration.rb +30 -19
- data/lib/claude_swarm/mcp_generator.rb +5 -10
- data/lib/claude_swarm/openai/chat_completion.rb +4 -12
- data/lib/claude_swarm/openai/executor.rb +3 -1
- data/lib/claude_swarm/openai/responses.rb +13 -32
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
- data/lib/swarm_cli/commands/run.rb +2 -2
- data/lib/swarm_cli/config_loader.rb +14 -14
- data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
- data/lib/swarm_cli/interactive_repl.rb +11 -5
- data/lib/swarm_cli/ui/icons.rb +0 -23
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/adapters/base.rb +4 -4
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
- data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
- data/lib/swarm_memory/integration/cli_registration.rb +3 -2
- data/lib/swarm_memory/integration/sdk_plugin.rb +98 -12
- data/lib/swarm_memory/tools/memory_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_read.rb +3 -3
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +6 -1
- data/lib/swarm_sdk/agent/builder.rb +91 -0
- data/lib/swarm_sdk/agent/chat.rb +540 -925
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +33 -79
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +147 -39
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +22 -38
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
- data/lib/swarm_sdk/agent/context.rb +8 -4
- data/lib/swarm_sdk/agent/context_manager.rb +6 -0
- data/lib/swarm_sdk/agent/definition.rb +79 -174
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +182 -0
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
- data/lib/swarm_sdk/builders/base_builder.rb +409 -0
- data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
- data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
- data/lib/swarm_sdk/concerns/validatable.rb +55 -0
- data/lib/swarm_sdk/configuration/parser.rb +353 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +100 -261
- data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
- data/lib/swarm_sdk/context_compactor.rb +6 -11
- data/lib/swarm_sdk/context_management/builder.rb +128 -0
- data/lib/swarm_sdk/context_management/context.rb +328 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +199 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
- data/lib/swarm_sdk/log_collector.rb +192 -16
- data/lib/swarm_sdk/log_stream.rb +66 -8
- data/lib/swarm_sdk/model_aliases.json +4 -1
- data/lib/swarm_sdk/node_context.rb +1 -1
- data/lib/swarm_sdk/observer/builder.rb +81 -0
- data/lib/swarm_sdk/observer/config.rb +45 -0
- data/lib/swarm_sdk/observer/manager.rb +236 -0
- data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
- data/lib/swarm_sdk/plugin.rb +93 -3
- data/lib/swarm_sdk/proc_helpers.rb +53 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
- data/lib/swarm_sdk/restore_result.rb +65 -0
- data/lib/swarm_sdk/snapshot.rb +156 -0
- data/lib/swarm_sdk/snapshot_from_events.rb +397 -0
- data/lib/swarm_sdk/state_restorer.rb +476 -0
- data/lib/swarm_sdk/state_snapshot.rb +334 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +428 -79
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
- data/lib/swarm_sdk/swarm/builder.rb +69 -407
- data/lib/swarm_sdk/swarm/executor.rb +213 -0
- data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +88 -149
- data/lib/swarm_sdk/swarm.rb +366 -631
- data/lib/swarm_sdk/swarm_loader.rb +145 -0
- data/lib/swarm_sdk/swarm_registry.rb +136 -0
- data/lib/swarm_sdk/tools/bash.rb +11 -3
- data/lib/swarm_sdk/tools/delegate.rb +127 -24
- data/lib/swarm_sdk/tools/edit.rb +8 -13
- data/lib/swarm_sdk/tools/glob.rb +9 -1
- data/lib/swarm_sdk/tools/grep.rb +7 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
- data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
- data/lib/swarm_sdk/tools/read.rb +28 -18
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +53 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/think.rb +4 -1
- data/lib/swarm_sdk/tools/todo_write.rb +27 -8
- data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/utils.rb +18 -0
- data/lib/swarm_sdk/validation_result.rb +33 -0
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +34 -9
- data/lib/swarm_sdk/workflow/builder.rb +143 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +42 -21
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
- data/lib/swarm_sdk/workflow.rb +554 -0
- data/lib/swarm_sdk.rb +393 -22
- metadata +51 -16
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_sdk/node_orchestrator.rb +0 -591
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -582
|
@@ -1,46 +1,89 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SwarmSDK
|
|
4
|
+
# Configuration facade that delegates to Parser and Translator
|
|
5
|
+
#
|
|
6
|
+
# This class maintains the public API while internally delegating to:
|
|
7
|
+
# - Configuration::Parser - YAML parsing, validation, and normalization
|
|
8
|
+
# - Configuration::Translator - Translation to Swarm/Workflow DSL builders
|
|
9
|
+
#
|
|
10
|
+
# ## Public API (unchanged)
|
|
11
|
+
# - Configuration.load_file(path) - Load from file
|
|
12
|
+
# - Configuration.new(yaml_content, base_dir:) - Load from string
|
|
13
|
+
# - config.load_and_validate - Parse and validate
|
|
14
|
+
# - config.to_swarm(allow_filesystem_tools:) - Convert to Swarm/Workflow
|
|
15
|
+
# - config.agent_names - Get list of agent names
|
|
16
|
+
# - config.connections_for(agent_name) - Get delegation targets
|
|
17
|
+
#
|
|
18
|
+
# ## Architecture
|
|
19
|
+
# The facade pattern keeps backward compatibility while separating concerns:
|
|
20
|
+
# - Parser handles all YAML parsing and validation logic
|
|
21
|
+
# - Translator handles all DSL builder translation logic
|
|
22
|
+
# - Configuration delegates to both, exposing parsed data via attr_readers
|
|
4
23
|
class Configuration
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
24
|
+
attr_reader :config_type,
|
|
25
|
+
:swarm_name,
|
|
26
|
+
:swarm_id,
|
|
27
|
+
:lead_agent,
|
|
28
|
+
:start_node,
|
|
29
|
+
:agents,
|
|
30
|
+
:all_agents_config,
|
|
31
|
+
:swarm_hooks,
|
|
32
|
+
:all_agents_hooks,
|
|
33
|
+
:scratchpad_enabled,
|
|
34
|
+
:nodes,
|
|
35
|
+
:external_swarms
|
|
8
36
|
|
|
9
37
|
class << self
|
|
10
|
-
|
|
11
|
-
|
|
38
|
+
# Load configuration from YAML file
|
|
39
|
+
#
|
|
40
|
+
# @param path [String, Pathname] Path to YAML configuration file
|
|
41
|
+
# @return [Configuration] Validated configuration instance
|
|
42
|
+
# @raise [ConfigurationError] If file not found or invalid
|
|
43
|
+
def load_file(path)
|
|
44
|
+
path = Pathname.new(path).expand_path
|
|
45
|
+
|
|
46
|
+
unless path.exist?
|
|
47
|
+
raise ConfigurationError, "Configuration file not found: #{path}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
yaml_content = File.read(path)
|
|
51
|
+
base_dir = path.dirname
|
|
52
|
+
|
|
53
|
+
new(yaml_content, base_dir: base_dir).tap(&:load_and_validate)
|
|
54
|
+
rescue Errno::ENOENT
|
|
55
|
+
raise ConfigurationError, "Configuration file not found: #{path}"
|
|
12
56
|
end
|
|
13
57
|
end
|
|
14
58
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
59
|
+
# Initialize configuration from YAML string
|
|
60
|
+
#
|
|
61
|
+
# @param yaml_content [String] YAML configuration content
|
|
62
|
+
# @param base_dir [String, Pathname] Base directory for resolving agent file paths (default: Dir.pwd)
|
|
63
|
+
def initialize(yaml_content, base_dir: Dir.pwd)
|
|
64
|
+
raise ArgumentError, "yaml_content cannot be nil" if yaml_content.nil?
|
|
65
|
+
raise ArgumentError, "base_dir cannot be nil" if base_dir.nil?
|
|
66
|
+
|
|
67
|
+
@yaml_content = yaml_content
|
|
68
|
+
@base_dir = Pathname.new(base_dir).expand_path
|
|
69
|
+
@parser = nil
|
|
70
|
+
@translator = nil
|
|
22
71
|
end
|
|
23
72
|
|
|
73
|
+
# Parse and validate YAML configuration
|
|
74
|
+
#
|
|
75
|
+
# Delegates to Parser for all parsing logic, then syncs parsed data
|
|
76
|
+
# to instance variables for backward compatibility.
|
|
77
|
+
#
|
|
78
|
+
# @return [self]
|
|
24
79
|
def load_and_validate
|
|
25
|
-
@
|
|
80
|
+
@parser = Parser.new(@yaml_content, base_dir: @base_dir)
|
|
81
|
+
@parser.parse
|
|
26
82
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
end
|
|
83
|
+
# Sync parsed data to instance variables for backward compatibility
|
|
84
|
+
sync_from_parser
|
|
30
85
|
|
|
31
|
-
@config = Utils.symbolize_keys(@config)
|
|
32
|
-
interpolate_env_vars!(@config)
|
|
33
|
-
validate_version
|
|
34
|
-
load_all_agents_config
|
|
35
|
-
load_hooks_config
|
|
36
|
-
validate_swarm
|
|
37
|
-
load_agents
|
|
38
|
-
detect_circular_dependencies
|
|
39
86
|
self
|
|
40
|
-
rescue Errno::ENOENT
|
|
41
|
-
raise ConfigurationError, "Configuration file not found: #{@config_path}"
|
|
42
|
-
rescue Psych::SyntaxError => e
|
|
43
|
-
raise ConfigurationError, "Invalid YAML syntax: #{e.message}"
|
|
44
87
|
end
|
|
45
88
|
|
|
46
89
|
def agent_names
|
|
@@ -48,249 +91,45 @@ module SwarmSDK
|
|
|
48
91
|
end
|
|
49
92
|
|
|
50
93
|
def connections_for(agent_name)
|
|
51
|
-
@agents[agent_name]
|
|
94
|
+
agent_config = @agents[agent_name]
|
|
95
|
+
return [] unless agent_config
|
|
96
|
+
|
|
97
|
+
delegates = agent_config[:delegates_to] || []
|
|
98
|
+
Array(delegates).map(&:to_sym)
|
|
52
99
|
end
|
|
53
100
|
|
|
54
|
-
# Convert configuration to Swarm
|
|
101
|
+
# Convert configuration to Swarm or Workflow using appropriate builder
|
|
55
102
|
#
|
|
56
|
-
#
|
|
57
|
-
# a thin convenience layer over the programmatic interface.
|
|
103
|
+
# Delegates to Translator for all DSL translation logic.
|
|
58
104
|
#
|
|
59
|
-
# @
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
global_concurrency: Swarm::DEFAULT_GLOBAL_CONCURRENCY,
|
|
64
|
-
default_local_concurrency: Swarm::DEFAULT_LOCAL_CONCURRENCY,
|
|
65
|
-
scratchpad_enabled: @scratchpad_enabled,
|
|
66
|
-
)
|
|
105
|
+
# @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
|
|
106
|
+
# @return [Swarm, Workflow] Configured swarm or workflow
|
|
107
|
+
def to_swarm(allow_filesystem_tools: nil)
|
|
108
|
+
raise ConfigurationError, "Configuration not loaded. Call load_and_validate first." unless @parser
|
|
67
109
|
|
|
68
|
-
|
|
69
|
-
@
|
|
70
|
-
swarm.add_agent(agent_def)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Set lead agent
|
|
74
|
-
swarm.lead = @lead_agent
|
|
75
|
-
|
|
76
|
-
swarm
|
|
110
|
+
@translator = Translator.new(@parser)
|
|
111
|
+
@translator.to_swarm(allow_filesystem_tools: allow_filesystem_tools)
|
|
77
112
|
end
|
|
78
113
|
|
|
79
114
|
private
|
|
80
115
|
|
|
81
|
-
|
|
82
|
-
case obj
|
|
83
|
-
when String
|
|
84
|
-
interpolate_env_string(obj)
|
|
85
|
-
when Hash
|
|
86
|
-
obj.transform_values! { |v| interpolate_env_vars!(v) }
|
|
87
|
-
when Array
|
|
88
|
-
obj.map! { |v| interpolate_env_vars!(v) }
|
|
89
|
-
else
|
|
90
|
-
obj
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def interpolate_env_string(str)
|
|
95
|
-
str.gsub(ENV_VAR_WITH_DEFAULT_PATTERN) do |_match|
|
|
96
|
-
env_var = Regexp.last_match(1)
|
|
97
|
-
has_default = Regexp.last_match(2)
|
|
98
|
-
default_value = Regexp.last_match(3)
|
|
99
|
-
|
|
100
|
-
if ENV.key?(env_var)
|
|
101
|
-
ENV[env_var]
|
|
102
|
-
elsif has_default
|
|
103
|
-
default_value || ""
|
|
104
|
-
else
|
|
105
|
-
raise ConfigurationError, "Environment variable '#{env_var}' is not set"
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def validate_version
|
|
111
|
-
version = @config[:version]
|
|
112
|
-
raise ConfigurationError, "Missing 'version' field in configuration" unless version
|
|
113
|
-
raise ConfigurationError, "SwarmSDK requires version: 2 configuration. Got version: #{version}" unless version == 2
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def load_all_agents_config
|
|
117
|
-
return unless @config[:swarm]
|
|
118
|
-
|
|
119
|
-
@all_agents_config = @config[:swarm][:all_agents] || {}
|
|
120
|
-
|
|
121
|
-
# Convert disable_default_tools array elements to symbols
|
|
122
|
-
if @all_agents_config[:disable_default_tools].is_a?(Array)
|
|
123
|
-
@all_agents_config[:disable_default_tools] = @all_agents_config[:disable_default_tools].map(&:to_sym)
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def load_hooks_config
|
|
128
|
-
return unless @config[:swarm]
|
|
129
|
-
|
|
130
|
-
# Load swarm-level hooks (only swarm_start, swarm_stop allowed)
|
|
131
|
-
@swarm_hooks = Utils.symbolize_keys(@config[:swarm][:hooks] || {})
|
|
132
|
-
|
|
133
|
-
# Load all_agents hooks (applied as swarm defaults)
|
|
134
|
-
if @config[:swarm][:all_agents]
|
|
135
|
-
@all_agents_hooks = Utils.symbolize_keys(@config[:swarm][:all_agents][:hooks] || {})
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def validate_swarm
|
|
140
|
-
raise ConfigurationError, "Missing 'swarm' field in configuration" unless @config[:swarm]
|
|
141
|
-
|
|
142
|
-
swarm = @config[:swarm]
|
|
143
|
-
raise ConfigurationError, "Missing 'name' field in swarm configuration" unless swarm[:name]
|
|
144
|
-
raise ConfigurationError, "Missing 'agents' field in swarm configuration" unless swarm[:agents]
|
|
145
|
-
raise ConfigurationError, "Missing 'lead' field in swarm configuration" unless swarm[:lead]
|
|
146
|
-
raise ConfigurationError, "No agents defined" if swarm[:agents].empty?
|
|
147
|
-
|
|
148
|
-
@swarm_name = swarm[:name]
|
|
149
|
-
@lead_agent = swarm[:lead].to_sym # Convert to symbol for consistency
|
|
150
|
-
@scratchpad_enabled = swarm[:use_scratchpad].nil? ? true : swarm[:use_scratchpad] # Default: enabled
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def load_agents
|
|
154
|
-
swarm_agents = @config[:swarm][:agents]
|
|
155
|
-
|
|
156
|
-
swarm_agents.each do |name, agent_config|
|
|
157
|
-
# Support three formats:
|
|
158
|
-
# 1. String: assistant: "agents/assistant.md" (file path)
|
|
159
|
-
# 2. Hash with agent_file: assistant: { agent_file: "..." }
|
|
160
|
-
# 3. Hash with inline definition: assistant: { description: "...", model: "..." }
|
|
161
|
-
|
|
162
|
-
if agent_config.is_a?(String)
|
|
163
|
-
# Format 1: Direct file path as string
|
|
164
|
-
file_path = agent_config
|
|
165
|
-
merged_config = merge_all_agents_config({})
|
|
166
|
-
@agents[name] = load_agent_from_file(name, file_path, merged_config)
|
|
167
|
-
else
|
|
168
|
-
# Format 2 or 3: Hash configuration
|
|
169
|
-
agent_config ||= {}
|
|
170
|
-
|
|
171
|
-
# Merge all_agents_config into agent config
|
|
172
|
-
# Agent-specific config overrides all_agents config
|
|
173
|
-
merged_config = merge_all_agents_config(agent_config)
|
|
174
|
-
|
|
175
|
-
@agents[name] = if agent_config[:agent_file]
|
|
176
|
-
# Format 2: Hash with agent_file key
|
|
177
|
-
load_agent_from_file(name, agent_config[:agent_file], merged_config)
|
|
178
|
-
else
|
|
179
|
-
# Format 3: Inline definition
|
|
180
|
-
Agent::Definition.new(name, merged_config)
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
unless @agents.key?(@lead_agent)
|
|
186
|
-
raise ConfigurationError, "Lead agent '#{@lead_agent}' not found in agents"
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Merge all_agents config with agent-specific config
|
|
191
|
-
# Agent config takes precedence over all_agents config
|
|
192
|
-
#
|
|
193
|
-
# Merge strategy:
|
|
194
|
-
# - Arrays (tools, delegates_to): Concatenate
|
|
195
|
-
# - Hashes (parameters, headers): Merge (agent values override)
|
|
196
|
-
# - Scalars (model, provider, base_url, timeout, coding_agent): Agent overrides
|
|
116
|
+
# Sync parsed data from Parser to instance variables
|
|
197
117
|
#
|
|
198
|
-
#
|
|
199
|
-
# @
|
|
200
|
-
def
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
merged[:delegates_to] = Array(merged[:delegates_to]) + Array(value)
|
|
214
|
-
when :parameters
|
|
215
|
-
# Merge parameters: all_agents.parameters + agent.parameters
|
|
216
|
-
# Agent values override all_agents values for same keys
|
|
217
|
-
merged[:parameters] = (merged[:parameters] || {}).merge(value || {})
|
|
218
|
-
when :headers
|
|
219
|
-
# Merge headers: all_agents.headers + agent.headers
|
|
220
|
-
# Agent values override all_agents values for same keys
|
|
221
|
-
merged[:headers] = (merged[:headers] || {}).merge(value || {})
|
|
222
|
-
when :disable_default_tools
|
|
223
|
-
# Convert array elements to symbols if it's an array
|
|
224
|
-
merged[key] = value.is_a?(Array) ? value.map(&:to_sym) : value
|
|
225
|
-
else
|
|
226
|
-
# For everything else (model, provider, base_url, timeout, coding_agent, etc.),
|
|
227
|
-
# agent value overrides all_agents value
|
|
228
|
-
merged[key] = value
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# Pass all_agents permissions as default_permissions for backward compat with AgentDefinition
|
|
233
|
-
if @all_agents_config[:permissions]
|
|
234
|
-
merged[:default_permissions] = @all_agents_config[:permissions]
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
merged
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def load_agent_from_file(name, file_path, merged_config)
|
|
241
|
-
agent_file_path = resolve_agent_file_path(file_path)
|
|
242
|
-
|
|
243
|
-
unless File.exist?(agent_file_path)
|
|
244
|
-
raise ConfigurationError, "Agent file not found: #{agent_file_path}"
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
content = File.read(agent_file_path)
|
|
248
|
-
# Parse markdown and merge with YAML config
|
|
249
|
-
agent_def_from_file = MarkdownParser.parse(content, name)
|
|
250
|
-
|
|
251
|
-
# Merge: YAML config overrides markdown file (YAML takes precedence)
|
|
252
|
-
# This allows YAML to override any settings from the markdown file
|
|
253
|
-
final_config = agent_def_from_file.to_h.compact.merge(merged_config.compact)
|
|
254
|
-
|
|
255
|
-
Agent::Definition.new(name, final_config)
|
|
256
|
-
rescue StandardError => e
|
|
257
|
-
raise ConfigurationError, "Error loading agent '#{name}' from file '#{file_path}': #{e.message}"
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def resolve_agent_file_path(file_path)
|
|
261
|
-
return file_path if Pathname.new(file_path).absolute?
|
|
262
|
-
|
|
263
|
-
@config_dir.join(file_path).to_s
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
def detect_circular_dependencies
|
|
267
|
-
@agents.each_key do |agent_name|
|
|
268
|
-
visited = Set.new
|
|
269
|
-
path = []
|
|
270
|
-
detect_cycle_from(agent_name, visited, path)
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
def detect_cycle_from(agent_name, visited, path)
|
|
275
|
-
return if visited.include?(agent_name)
|
|
276
|
-
|
|
277
|
-
if path.include?(agent_name)
|
|
278
|
-
cycle_start = path.index(agent_name)
|
|
279
|
-
cycle = path[cycle_start..] + [agent_name]
|
|
280
|
-
raise CircularDependencyError, "Circular dependency detected: #{cycle.join(" -> ")}"
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
path.push(agent_name)
|
|
284
|
-
connections_for(agent_name).each do |connection|
|
|
285
|
-
connection_sym = connection.to_sym # Convert to symbol for lookup
|
|
286
|
-
unless @agents.key?(connection_sym)
|
|
287
|
-
raise ConfigurationError, "Agent '#{agent_name}' has connection to unknown agent '#{connection}'"
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
detect_cycle_from(connection_sym, visited, path)
|
|
291
|
-
end
|
|
292
|
-
path.pop
|
|
293
|
-
visited.add(agent_name)
|
|
118
|
+
# This maintains backward compatibility with code that accesses
|
|
119
|
+
# @config_type, @agents, etc. directly via attr_readers.
|
|
120
|
+
def sync_from_parser
|
|
121
|
+
@config_type = @parser.config_type
|
|
122
|
+
@swarm_name = @parser.swarm_name
|
|
123
|
+
@swarm_id = @parser.swarm_id
|
|
124
|
+
@lead_agent = @parser.lead_agent
|
|
125
|
+
@start_node = @parser.start_node
|
|
126
|
+
@agents = @parser.agents
|
|
127
|
+
@all_agents_config = @parser.all_agents_config
|
|
128
|
+
@swarm_hooks = @parser.swarm_hooks
|
|
129
|
+
@all_agents_hooks = @parser.all_agents_hooks
|
|
130
|
+
@external_swarms = @parser.external_swarms
|
|
131
|
+
@nodes = @parser.nodes
|
|
132
|
+
@scratchpad_enabled = @parser.scratchpad_mode # NOTE: attr_reader says scratchpad_enabled
|
|
294
133
|
end
|
|
295
134
|
end
|
|
296
135
|
end
|
|
@@ -17,9 +17,9 @@ module SwarmSDK
|
|
|
17
17
|
# total_tokens = TokenCounter.estimate_messages(messages)
|
|
18
18
|
#
|
|
19
19
|
class TokenCounter
|
|
20
|
-
#
|
|
21
|
-
CHARS_PER_TOKEN_PROSE =
|
|
22
|
-
CHARS_PER_TOKEN_CODE =
|
|
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
23
|
|
|
24
24
|
class << self
|
|
25
25
|
# Estimate tokens for a single message
|
|
@@ -58,7 +58,7 @@ module SwarmSDK
|
|
|
58
58
|
# @return [ContextCompactor::Metrics] Compression metrics
|
|
59
59
|
def compact
|
|
60
60
|
start_time = Time.now
|
|
61
|
-
original_messages = @chat.messages
|
|
61
|
+
original_messages = @chat.messages
|
|
62
62
|
|
|
63
63
|
# Emit compression_started event
|
|
64
64
|
LogStream.emit(
|
|
@@ -308,7 +308,8 @@ module SwarmSDK
|
|
|
308
308
|
response.content
|
|
309
309
|
rescue StandardError => e
|
|
310
310
|
# If summarization fails, create a simple fallback summary
|
|
311
|
-
|
|
311
|
+
LogStream.emit_error(e, source: "context_compactor", context: "generate_summary", agent: @agent_name)
|
|
312
|
+
RubyLLM.logger.debug("ContextCompactor: Summarization failed: #{e.message}")
|
|
312
313
|
|
|
313
314
|
<<~FALLBACK
|
|
314
315
|
## Summary
|
|
@@ -322,19 +323,13 @@ module SwarmSDK
|
|
|
322
323
|
|
|
323
324
|
# Replace messages in the chat
|
|
324
325
|
#
|
|
325
|
-
#
|
|
326
|
-
#
|
|
326
|
+
# Delegates to the Chat's replace_messages method which provides
|
|
327
|
+
# a safe abstraction over the internal message array.
|
|
327
328
|
#
|
|
328
329
|
# @param new_messages [Array<RubyLLM::Message>] New message array
|
|
329
330
|
# @return [void]
|
|
330
331
|
def replace_messages(new_messages)
|
|
331
|
-
|
|
332
|
-
@chat.messages.clear
|
|
333
|
-
|
|
334
|
-
# Add new messages
|
|
335
|
-
new_messages.each do |msg|
|
|
336
|
-
@chat.messages << msg
|
|
337
|
-
end
|
|
332
|
+
@chat.replace_messages(new_messages)
|
|
338
333
|
end
|
|
339
334
|
end
|
|
340
335
|
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module ContextManagement
|
|
5
|
+
# DSL for defining context management handlers
|
|
6
|
+
#
|
|
7
|
+
# This builder provides a clean, idiomatic way to register handlers for
|
|
8
|
+
# context warning thresholds. Handlers receive a rich context object
|
|
9
|
+
# with message manipulation methods.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# context_management do
|
|
13
|
+
# on :warning_60 do |ctx|
|
|
14
|
+
# ctx.compress_tool_results(keep_recent: 10)
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# on :warning_80 do |ctx|
|
|
18
|
+
# ctx.prune_old_messages(keep_recent: 20)
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @example Progressive compression
|
|
23
|
+
# context_management do
|
|
24
|
+
# on :warning_60 do |ctx|
|
|
25
|
+
# ctx.compress_tool_results(keep_recent: 15, truncate_to: 500)
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# on :warning_80 do |ctx|
|
|
29
|
+
# ctx.prune_old_messages(keep_recent: 30)
|
|
30
|
+
# ctx.compress_tool_results(keep_recent: 5, truncate_to: 200)
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# on :warning_90 do |ctx|
|
|
34
|
+
# ctx.log_action("emergency_pruning", tokens_remaining: ctx.tokens_remaining)
|
|
35
|
+
# ctx.prune_old_messages(keep_recent: 15)
|
|
36
|
+
# end
|
|
37
|
+
# end
|
|
38
|
+
class Builder
|
|
39
|
+
# Map semantic event names to threshold percentages
|
|
40
|
+
EVENT_MAP = {
|
|
41
|
+
warning_60: 60,
|
|
42
|
+
warning_80: 80,
|
|
43
|
+
warning_90: 90,
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
def initialize
|
|
47
|
+
@handlers = {} # { threshold => block }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Register a handler for a context warning threshold
|
|
51
|
+
#
|
|
52
|
+
# Handlers take full responsibility for managing context at their threshold.
|
|
53
|
+
# When a handler is registered for a threshold, automatic compression is disabled
|
|
54
|
+
# for that threshold.
|
|
55
|
+
#
|
|
56
|
+
# @param event [Symbol] Event name (:warning_60, :warning_80, :warning_90)
|
|
57
|
+
# @yield [ContextManagement::Context] Context with message manipulation methods
|
|
58
|
+
# @return [void]
|
|
59
|
+
#
|
|
60
|
+
# @raise [ArgumentError] If event is unknown or block is missing
|
|
61
|
+
#
|
|
62
|
+
# @example Compress tool results at 60%
|
|
63
|
+
# on :warning_60 do |ctx|
|
|
64
|
+
# ctx.compress_tool_results(keep_recent: 10)
|
|
65
|
+
# end
|
|
66
|
+
#
|
|
67
|
+
# @example Custom logic at 80%
|
|
68
|
+
# on :warning_80 do |ctx|
|
|
69
|
+
# if ctx.usage_percentage > 85
|
|
70
|
+
# ctx.prune_old_messages(keep_recent: 10)
|
|
71
|
+
# else
|
|
72
|
+
# ctx.summarize_old_exchanges(older_than: 20)
|
|
73
|
+
# end
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# @example Log and prune at 90%
|
|
77
|
+
# on :warning_90 do |ctx|
|
|
78
|
+
# ctx.log_action("critical_threshold", remaining: ctx.tokens_remaining)
|
|
79
|
+
# ctx.prune_old_messages(keep_recent: 10)
|
|
80
|
+
# end
|
|
81
|
+
def on(event, &block)
|
|
82
|
+
threshold = EVENT_MAP[event]
|
|
83
|
+
raise ArgumentError, "Unknown event: #{event}. Valid events: #{EVENT_MAP.keys.join(", ")}" unless threshold
|
|
84
|
+
raise ArgumentError, "Block required for #{event}" unless block
|
|
85
|
+
|
|
86
|
+
@handlers[threshold] = block
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Build hook definitions from handlers
|
|
90
|
+
#
|
|
91
|
+
# Creates Hooks::Definition objects that wrap user blocks to provide
|
|
92
|
+
# rich context objects instead of raw Hooks::Context. Each handler
|
|
93
|
+
# becomes a hook for the :context_warning event.
|
|
94
|
+
#
|
|
95
|
+
# @return [Array<Hooks::Definition>] Hook definitions for :context_warning event
|
|
96
|
+
def build
|
|
97
|
+
@handlers.map do |threshold, user_block|
|
|
98
|
+
# Create a hook that filters by threshold and wraps context
|
|
99
|
+
Hooks::Definition.new(
|
|
100
|
+
event: :context_warning,
|
|
101
|
+
matcher: nil, # No tool matching needed
|
|
102
|
+
priority: 0,
|
|
103
|
+
proc: create_threshold_matcher(threshold, user_block),
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# Create a proc that matches threshold and wraps context
|
|
111
|
+
#
|
|
112
|
+
# @param target_threshold [Integer] Threshold to match (60, 80, 90)
|
|
113
|
+
# @param user_block [Proc] User's handler block
|
|
114
|
+
# @return [Proc] Hook proc
|
|
115
|
+
def create_threshold_matcher(target_threshold, user_block)
|
|
116
|
+
proc do |hooks_context|
|
|
117
|
+
# Only execute for matching threshold
|
|
118
|
+
current_threshold = hooks_context.metadata[:threshold]
|
|
119
|
+
next unless current_threshold == target_threshold
|
|
120
|
+
|
|
121
|
+
# Wrap in rich context object
|
|
122
|
+
rich_context = Context.new(hooks_context)
|
|
123
|
+
user_block.call(rich_context)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|