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.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/lib/claude_swarm/claude_mcp_server.rb +1 -0
  3. data/lib/claude_swarm/cli.rb +5 -18
  4. data/lib/claude_swarm/configuration.rb +30 -19
  5. data/lib/claude_swarm/mcp_generator.rb +5 -10
  6. data/lib/claude_swarm/openai/chat_completion.rb +4 -12
  7. data/lib/claude_swarm/openai/executor.rb +3 -1
  8. data/lib/claude_swarm/openai/responses.rb +13 -32
  9. data/lib/claude_swarm/version.rb +1 -1
  10. data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
  11. data/lib/swarm_cli/commands/run.rb +2 -2
  12. data/lib/swarm_cli/config_loader.rb +14 -14
  13. data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
  14. data/lib/swarm_cli/interactive_repl.rb +11 -5
  15. data/lib/swarm_cli/ui/icons.rb +0 -23
  16. data/lib/swarm_cli/version.rb +1 -1
  17. data/lib/swarm_memory/adapters/base.rb +4 -4
  18. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  19. data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
  20. data/lib/swarm_memory/integration/cli_registration.rb +3 -2
  21. data/lib/swarm_memory/integration/sdk_plugin.rb +98 -12
  22. data/lib/swarm_memory/tools/memory_edit.rb +2 -2
  23. data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
  24. data/lib/swarm_memory/tools/memory_read.rb +3 -3
  25. data/lib/swarm_memory/version.rb +1 -1
  26. data/lib/swarm_memory.rb +6 -1
  27. data/lib/swarm_sdk/agent/builder.rb +91 -0
  28. data/lib/swarm_sdk/agent/chat.rb +540 -925
  29. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +33 -79
  30. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  31. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +147 -39
  32. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  33. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  34. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  35. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  36. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +22 -38
  37. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  38. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  39. data/lib/swarm_sdk/agent/context.rb +8 -4
  40. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  41. data/lib/swarm_sdk/agent/definition.rb +79 -174
  42. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +182 -0
  43. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  44. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  45. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  46. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  47. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  48. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  49. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  50. data/lib/swarm_sdk/configuration.rb +100 -261
  51. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  52. data/lib/swarm_sdk/context_compactor.rb +6 -11
  53. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  54. data/lib/swarm_sdk/context_management/context.rb +328 -0
  55. data/lib/swarm_sdk/defaults.rb +196 -0
  56. data/lib/swarm_sdk/events_to_messages.rb +199 -0
  57. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  58. data/lib/swarm_sdk/log_collector.rb +192 -16
  59. data/lib/swarm_sdk/log_stream.rb +66 -8
  60. data/lib/swarm_sdk/model_aliases.json +4 -1
  61. data/lib/swarm_sdk/node_context.rb +1 -1
  62. data/lib/swarm_sdk/observer/builder.rb +81 -0
  63. data/lib/swarm_sdk/observer/config.rb +45 -0
  64. data/lib/swarm_sdk/observer/manager.rb +236 -0
  65. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  66. data/lib/swarm_sdk/plugin.rb +93 -3
  67. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  68. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  69. data/lib/swarm_sdk/restore_result.rb +65 -0
  70. data/lib/swarm_sdk/snapshot.rb +156 -0
  71. data/lib/swarm_sdk/snapshot_from_events.rb +397 -0
  72. data/lib/swarm_sdk/state_restorer.rb +476 -0
  73. data/lib/swarm_sdk/state_snapshot.rb +334 -0
  74. data/lib/swarm_sdk/swarm/agent_initializer.rb +428 -79
  75. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  76. data/lib/swarm_sdk/swarm/builder.rb +69 -407
  77. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  78. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  79. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  80. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  81. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  82. data/lib/swarm_sdk/swarm/tool_configurator.rb +88 -149
  83. data/lib/swarm_sdk/swarm.rb +366 -631
  84. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  85. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  86. data/lib/swarm_sdk/tools/bash.rb +11 -3
  87. data/lib/swarm_sdk/tools/delegate.rb +127 -24
  88. data/lib/swarm_sdk/tools/edit.rb +8 -13
  89. data/lib/swarm_sdk/tools/glob.rb +9 -1
  90. data/lib/swarm_sdk/tools/grep.rb +7 -0
  91. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  92. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  93. data/lib/swarm_sdk/tools/read.rb +28 -18
  94. data/lib/swarm_sdk/tools/registry.rb +122 -10
  95. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  96. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  97. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  98. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  99. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +53 -5
  100. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  101. data/lib/swarm_sdk/tools/think.rb +4 -1
  102. data/lib/swarm_sdk/tools/todo_write.rb +27 -8
  103. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  104. data/lib/swarm_sdk/tools/write.rb +8 -13
  105. data/lib/swarm_sdk/utils.rb +18 -0
  106. data/lib/swarm_sdk/validation_result.rb +33 -0
  107. data/lib/swarm_sdk/version.rb +1 -1
  108. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +34 -9
  109. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  110. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  111. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +42 -21
  112. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  113. data/lib/swarm_sdk/workflow.rb +554 -0
  114. data/lib/swarm_sdk.rb +393 -22
  115. metadata +51 -16
  116. data/lib/swarm_memory/chat_extension.rb +0 -34
  117. data/lib/swarm_sdk/node_orchestrator.rb +0 -591
  118. 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
- ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
6
-
7
- attr_reader :config_path, :swarm_name, :lead_agent, :agents, :all_agents_config, :swarm_hooks, :all_agents_hooks, :scratchpad_enabled
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
- def load(path)
11
- new(path).tap(&:load_and_validate)
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
- def initialize(config_path)
16
- @config_path = Pathname.new(config_path).expand_path
17
- @config_dir = @config_path.dirname
18
- @agents = {}
19
- @all_agents_config = {} # Settings applied to all agents
20
- @swarm_hooks = {} # Swarm-level hooks (swarm_start, swarm_stop)
21
- @all_agents_hooks = {} # Hooks applied to all agents
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
- @config = YAML.load_file(@config_path, aliases: true)
80
+ @parser = Parser.new(@yaml_content, base_dir: @base_dir)
81
+ @parser.parse
26
82
 
27
- unless @config.is_a?(Hash)
28
- raise ConfigurationError, "Invalid YAML syntax: configuration must be a Hash"
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]&.delegates_to || []
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 instance using Ruby API
101
+ # Convert configuration to Swarm or Workflow using appropriate builder
55
102
  #
56
- # This method bridges YAML configuration to the Ruby API, making YAML
57
- # a thin convenience layer over the programmatic interface.
103
+ # Delegates to Translator for all DSL translation logic.
58
104
  #
59
- # @return [Swarm] Configured swarm instance
60
- def to_swarm
61
- swarm = Swarm.new(
62
- name: @swarm_name,
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
- # Add all agents - pass definitions directly
69
- @agents.each do |_name, agent_def|
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
- def interpolate_env_vars!(obj)
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
- # @param agent_config [Hash] Agent-specific configuration
199
- # @return [Hash] Merged configuration
200
- def merge_all_agents_config(agent_config)
201
- merged = @all_agents_config.dup
202
-
203
- # For arrays, concatenate
204
- # For hashes, merge (agent values override)
205
- # For scalars, agent value overrides
206
- agent_config.each do |key, value|
207
- case key
208
- when :tools
209
- # Concatenate tools: all_agents.tools + agent.tools
210
- merged[:tools] = Array(merged[:tools]) + Array(value)
211
- when :delegates_to
212
- # Concatenate delegates_to
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
- # Average characters per token for different content types
21
- CHARS_PER_TOKEN_PROSE = 4.0
22
- CHARS_PER_TOKEN_CODE = 3.5
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.dup
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
- RubyLLM.logger.warn("ContextCompactor: Summarization failed: #{e.message}")
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
- # RubyLLM::Chat doesn't have a public API for replacing all messages,
326
- # so we need to work with the internal messages array.
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
- # Clear existing messages
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