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
data/lib/swarm_sdk/swarm.rb
CHANGED
|
@@ -4,25 +4,10 @@ module SwarmSDK
|
|
|
4
4
|
# Swarm orchestrates multiple AI agents with shared rate limiting and coordination.
|
|
5
5
|
#
|
|
6
6
|
# This is the main user-facing API for SwarmSDK. Users create swarms using:
|
|
7
|
-
# -
|
|
8
|
-
# -
|
|
9
|
-
# - YAML:
|
|
10
|
-
#
|
|
11
|
-
# ## Direct API
|
|
12
|
-
#
|
|
13
|
-
# swarm = Swarm.new(name: "Development Team")
|
|
14
|
-
#
|
|
15
|
-
# backend_agent = Agent::Definition.new(:backend, {
|
|
16
|
-
# description: "Backend developer",
|
|
17
|
-
# model: "gpt-5",
|
|
18
|
-
# system_prompt: "You build APIs and databases...",
|
|
19
|
-
# tools: [:Read, :Edit, :Bash],
|
|
20
|
-
# delegates_to: [:database]
|
|
21
|
-
# })
|
|
22
|
-
# swarm.add_agent(backend_agent)
|
|
23
|
-
#
|
|
24
|
-
# swarm.lead = :backend
|
|
25
|
-
# result = swarm.execute("Build authentication")
|
|
7
|
+
# - Ruby DSL: SwarmSDK.build { ... } (Recommended)
|
|
8
|
+
# - YAML String: SwarmSDK.load(yaml, base_dir:)
|
|
9
|
+
# - YAML File: SwarmSDK.load_file(path)
|
|
10
|
+
# - Direct API: Swarm.new + add_agent (Advanced)
|
|
26
11
|
#
|
|
27
12
|
# ## Ruby DSL (Recommended)
|
|
28
13
|
#
|
|
@@ -39,14 +24,36 @@ module SwarmSDK
|
|
|
39
24
|
# end
|
|
40
25
|
# result = swarm.execute("Build authentication")
|
|
41
26
|
#
|
|
42
|
-
# ## YAML API
|
|
27
|
+
# ## YAML String API
|
|
28
|
+
#
|
|
29
|
+
# yaml = File.read("swarm.yml")
|
|
30
|
+
# swarm = SwarmSDK.load(yaml, base_dir: "/path/to/project")
|
|
31
|
+
# result = swarm.execute("Build authentication")
|
|
32
|
+
#
|
|
33
|
+
# ## YAML File API (Convenience)
|
|
34
|
+
#
|
|
35
|
+
# swarm = SwarmSDK.load_file("swarm.yml")
|
|
36
|
+
# result = swarm.execute("Build authentication")
|
|
37
|
+
#
|
|
38
|
+
# ## Direct API (Advanced)
|
|
39
|
+
#
|
|
40
|
+
# swarm = Swarm.new(name: "Development Team")
|
|
41
|
+
#
|
|
42
|
+
# backend_agent = Agent::Definition.new(:backend, {
|
|
43
|
+
# description: "Backend developer",
|
|
44
|
+
# model: "gpt-5",
|
|
45
|
+
# system_prompt: "You build APIs and databases...",
|
|
46
|
+
# tools: [:Read, :Edit, :Bash],
|
|
47
|
+
# delegates_to: [:database]
|
|
48
|
+
# })
|
|
49
|
+
# swarm.add_agent(backend_agent)
|
|
43
50
|
#
|
|
44
|
-
# swarm =
|
|
51
|
+
# swarm.lead = :backend
|
|
45
52
|
# result = swarm.execute("Build authentication")
|
|
46
53
|
#
|
|
47
54
|
# ## Architecture
|
|
48
55
|
#
|
|
49
|
-
# All
|
|
56
|
+
# All APIs converge on Agent::Definition for validation.
|
|
50
57
|
# Swarm delegates to specialized concerns:
|
|
51
58
|
# - Agent::Definition: Validates configuration, builds system prompts
|
|
52
59
|
# - AgentInitializer: Complex 5-pass agent setup
|
|
@@ -54,23 +61,42 @@ module SwarmSDK
|
|
|
54
61
|
# - McpConfigurator: MCP client management (via AgentInitializer)
|
|
55
62
|
#
|
|
56
63
|
class Swarm
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
include Concerns::Snapshotable
|
|
65
|
+
include Concerns::Validatable
|
|
66
|
+
include Concerns::Cleanupable
|
|
67
|
+
include LoggingCallbacks
|
|
68
|
+
include HookTriggers
|
|
69
|
+
|
|
70
|
+
# Backward compatibility aliases - use Defaults module for new code
|
|
71
|
+
DEFAULT_MCP_LOG_LEVEL = Defaults::Logging::MCP_LOG_LEVEL
|
|
60
72
|
|
|
61
73
|
# Default tools available to all agents
|
|
62
74
|
DEFAULT_TOOLS = ToolConfigurator::DEFAULT_TOOLS
|
|
63
75
|
|
|
64
|
-
attr_reader :name, :agents, :lead_agent, :mcp_clients
|
|
76
|
+
attr_reader :name, :agents, :lead_agent, :mcp_clients, :delegation_instances, :agent_definitions, :swarm_id, :parent_swarm_id, :swarm_registry, :scratchpad_storage, :allow_filesystem_tools, :hook_registry, :global_semaphore, :plugin_storages, :config_for_hooks, :observer_configs
|
|
77
|
+
attr_accessor :delegation_call_stack
|
|
65
78
|
|
|
66
79
|
# Check if scratchpad tools are enabled
|
|
67
80
|
#
|
|
68
81
|
# @return [Boolean]
|
|
69
82
|
def scratchpad_enabled?
|
|
70
|
-
@
|
|
83
|
+
@scratchpad_mode == :enabled
|
|
71
84
|
end
|
|
72
85
|
attr_writer :config_for_hooks
|
|
73
86
|
|
|
87
|
+
# Check if first message has been sent (for system reminder injection)
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean]
|
|
90
|
+
def first_message_sent?
|
|
91
|
+
@first_message_sent
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Set first message sent flag (used by snapshot/restore)
|
|
95
|
+
#
|
|
96
|
+
# @param value [Boolean] New value
|
|
97
|
+
# @return [void]
|
|
98
|
+
attr_writer :first_message_sent
|
|
99
|
+
|
|
74
100
|
# Class-level MCP log level configuration
|
|
75
101
|
@mcp_log_level = DEFAULT_MCP_LOG_LEVEL
|
|
76
102
|
@mcp_logging_configured = false
|
|
@@ -102,54 +128,54 @@ module SwarmSDK
|
|
|
102
128
|
|
|
103
129
|
@mcp_logging_configured = true
|
|
104
130
|
end
|
|
105
|
-
|
|
106
|
-
# Load swarm from YAML configuration file
|
|
107
|
-
#
|
|
108
|
-
# @param config_path [String] Path to YAML configuration file
|
|
109
|
-
# @return [Swarm] Configured swarm instance
|
|
110
|
-
def load(config_path)
|
|
111
|
-
config = Configuration.load(config_path)
|
|
112
|
-
swarm = config.to_swarm
|
|
113
|
-
|
|
114
|
-
# Apply hooks if any are configured (YAML-only feature)
|
|
115
|
-
if hooks_configured?(config)
|
|
116
|
-
Hooks::Adapter.apply_hooks(swarm, config)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Store config reference for agent hooks (applied during initialize_agents)
|
|
120
|
-
swarm.config_for_hooks = config
|
|
121
|
-
|
|
122
|
-
swarm
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
private
|
|
126
|
-
|
|
127
|
-
def hooks_configured?(config)
|
|
128
|
-
config.swarm_hooks.any? ||
|
|
129
|
-
config.all_agents_hooks.any? ||
|
|
130
|
-
config.agents.any? { |_, agent_def| agent_def.hooks&.any? }
|
|
131
|
-
end
|
|
132
131
|
end
|
|
133
132
|
|
|
134
133
|
# Initialize a new Swarm
|
|
135
134
|
#
|
|
136
135
|
# @param name [String] Human-readable swarm name
|
|
136
|
+
# @param swarm_id [String, nil] Optional swarm ID (auto-generated if not provided)
|
|
137
|
+
# @param parent_swarm_id [String, nil] Optional parent swarm ID (nil for root swarms)
|
|
137
138
|
# @param global_concurrency [Integer] Max concurrent LLM calls across entire swarm
|
|
138
139
|
# @param default_local_concurrency [Integer] Default max concurrent tool calls per agent
|
|
139
|
-
# @param scratchpad [Tools::Stores::Scratchpad, nil] Optional scratchpad instance (for testing)
|
|
140
|
-
# @param
|
|
141
|
-
|
|
140
|
+
# @param scratchpad [Tools::Stores::Scratchpad, nil] Optional scratchpad instance (for testing/internal use)
|
|
141
|
+
# @param scratchpad_mode [Symbol, String] Scratchpad mode (:enabled or :disabled). :per_node not allowed for non-node swarms.
|
|
142
|
+
# @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
|
|
143
|
+
def initialize(name:, swarm_id: nil, parent_swarm_id: nil, global_concurrency: Defaults::Concurrency::GLOBAL_LIMIT, default_local_concurrency: Defaults::Concurrency::LOCAL_LIMIT, scratchpad: nil, scratchpad_mode: :enabled, allow_filesystem_tools: nil)
|
|
142
144
|
@name = name
|
|
145
|
+
@swarm_id = swarm_id || generate_swarm_id(name)
|
|
146
|
+
@parent_swarm_id = parent_swarm_id
|
|
143
147
|
@global_concurrency = global_concurrency
|
|
144
148
|
@default_local_concurrency = default_local_concurrency
|
|
145
|
-
|
|
149
|
+
|
|
150
|
+
# Handle scratchpad_mode parameter
|
|
151
|
+
# For Swarm: :enabled or :disabled (not :per_node - that's for nodes)
|
|
152
|
+
@scratchpad_mode = validate_swarm_scratchpad_mode(scratchpad_mode)
|
|
153
|
+
|
|
154
|
+
# Resolve allow_filesystem_tools with priority:
|
|
155
|
+
# 1. Explicit parameter (if not nil)
|
|
156
|
+
# 2. Global settings
|
|
157
|
+
@allow_filesystem_tools = if allow_filesystem_tools.nil?
|
|
158
|
+
SwarmSDK.settings.allow_filesystem_tools
|
|
159
|
+
else
|
|
160
|
+
allow_filesystem_tools
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Swarm registry for managing sub-swarms (initialized later if needed)
|
|
164
|
+
@swarm_registry = nil
|
|
165
|
+
|
|
166
|
+
# Delegation call stack for circular dependency detection
|
|
167
|
+
@delegation_call_stack = []
|
|
146
168
|
|
|
147
169
|
# Shared semaphore for all agents
|
|
148
170
|
@global_semaphore = Async::Semaphore.new(@global_concurrency)
|
|
149
171
|
|
|
150
172
|
# Shared scratchpad storage for all agents (volatile)
|
|
151
|
-
# Use provided scratchpad storage (for testing) or create volatile one
|
|
152
|
-
@scratchpad_storage = scratchpad
|
|
173
|
+
# Use provided scratchpad storage (for testing) or create volatile one based on mode
|
|
174
|
+
@scratchpad_storage = if scratchpad
|
|
175
|
+
scratchpad # Testing/internal use - explicit instance provided
|
|
176
|
+
elsif @scratchpad_mode == :enabled
|
|
177
|
+
Tools::Stores::ScratchpadStorage.new
|
|
178
|
+
end
|
|
153
179
|
|
|
154
180
|
# Per-agent plugin storages (persistent)
|
|
155
181
|
# Format: { plugin_name => { agent_name => storage } }
|
|
@@ -165,6 +191,7 @@ module SwarmSDK
|
|
|
165
191
|
# Agent definitions and instances
|
|
166
192
|
@agent_definitions = {}
|
|
167
193
|
@agents = {}
|
|
194
|
+
@delegation_instances = {} # { "delegate@delegator" => Agent::Chat }
|
|
168
195
|
@agents_initialized = false
|
|
169
196
|
@agent_contexts = {}
|
|
170
197
|
|
|
@@ -175,6 +202,14 @@ module SwarmSDK
|
|
|
175
202
|
|
|
176
203
|
# Track if first message has been sent
|
|
177
204
|
@first_message_sent = false
|
|
205
|
+
|
|
206
|
+
# Track if agent_start events have been emitted
|
|
207
|
+
# This prevents duplicate emissions and ensures events are emitted when logging is ready
|
|
208
|
+
@agent_start_events_emitted = false
|
|
209
|
+
|
|
210
|
+
# Observer agent configurations
|
|
211
|
+
@observer_configs = []
|
|
212
|
+
@observer_manager = nil
|
|
178
213
|
end
|
|
179
214
|
|
|
180
215
|
# Add an agent to the swarm
|
|
@@ -230,36 +265,53 @@ module SwarmSDK
|
|
|
230
265
|
# and the entire swarm coordinates with shared rate limiting.
|
|
231
266
|
# Supports reprompting via swarm_stop hooks.
|
|
232
267
|
#
|
|
268
|
+
# By default, this method blocks until execution completes. Set wait: false
|
|
269
|
+
# to return an Async::Task immediately, enabling cancellation via task.stop.
|
|
270
|
+
#
|
|
233
271
|
# @param prompt [String] Task to execute
|
|
272
|
+
# @param wait [Boolean] If true (default), blocks until execution completes.
|
|
273
|
+
# If false, returns Async::Task immediately for non-blocking execution.
|
|
234
274
|
# @yield [Hash] Log entry if block given (for streaming)
|
|
235
|
-
# @return [Result]
|
|
236
|
-
|
|
275
|
+
# @return [Result, Async::Task] Result if wait: true, Async::Task if wait: false
|
|
276
|
+
#
|
|
277
|
+
# @example Blocking execution (default)
|
|
278
|
+
# result = swarm.execute("Build auth")
|
|
279
|
+
# puts result.content
|
|
280
|
+
#
|
|
281
|
+
# @example Non-blocking execution with cancellation
|
|
282
|
+
# task = swarm.execute("Build auth", wait: false) { |event| puts event }
|
|
283
|
+
# # ... do other work ...
|
|
284
|
+
# task.stop # Cancel anytime
|
|
285
|
+
# result = task.wait # Returns nil for cancelled tasks
|
|
286
|
+
def execute(prompt, wait: true, &block)
|
|
237
287
|
raise ConfigurationError, "No lead agent set. Set lead= first." unless @lead_agent
|
|
238
288
|
|
|
239
|
-
start_time = Time.now
|
|
240
289
|
logs = []
|
|
241
290
|
current_prompt = prompt
|
|
291
|
+
has_logging = block_given?
|
|
292
|
+
|
|
293
|
+
# Save original Fiber storage for restoration (preserves parent context for nested swarms)
|
|
294
|
+
original_fiber_storage = {
|
|
295
|
+
execution_id: Fiber[:execution_id],
|
|
296
|
+
swarm_id: Fiber[:swarm_id],
|
|
297
|
+
parent_swarm_id: Fiber[:parent_swarm_id],
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# Set fiber-local execution context
|
|
301
|
+
# Use ||= to inherit parent's execution_id if one exists (for mini-swarms)
|
|
302
|
+
Fiber[:execution_id] ||= generate_execution_id
|
|
303
|
+
Fiber[:swarm_id] = @swarm_id
|
|
304
|
+
Fiber[:parent_swarm_id] = @parent_swarm_id
|
|
242
305
|
|
|
243
306
|
# Setup logging FIRST if block given (so swarm_start event can be emitted)
|
|
244
|
-
if
|
|
245
|
-
# Register callback to collect logs and forward to user's block
|
|
246
|
-
LogCollector.on_log do |entry|
|
|
247
|
-
logs << entry
|
|
248
|
-
block.call(entry)
|
|
249
|
-
end
|
|
307
|
+
setup_logging(logs, &block) if has_logging
|
|
250
308
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
309
|
+
# Setup observer execution if any observers configured
|
|
310
|
+
# MUST happen AFTER setup_logging (which clears Fiber[:log_subscriptions])
|
|
311
|
+
setup_observer_execution if @observer_configs.any?
|
|
254
312
|
|
|
255
313
|
# Trigger swarm_start hooks (before any execution)
|
|
256
|
-
|
|
257
|
-
# Default callback emits swarm_start event to LogStream
|
|
258
|
-
swarm_start_result = trigger_swarm_start(current_prompt)
|
|
259
|
-
if swarm_start_result&.replace?
|
|
260
|
-
# Hook provided stdout to append to prompt
|
|
261
|
-
current_prompt = "#{current_prompt}\n\n<hook-context>\n#{swarm_start_result.value}\n</hook-context>"
|
|
262
|
-
end
|
|
314
|
+
current_prompt = apply_swarm_start_hooks(current_prompt)
|
|
263
315
|
|
|
264
316
|
# Trigger first_message hooks on first execution
|
|
265
317
|
unless @first_message_sent
|
|
@@ -270,128 +322,18 @@ module SwarmSDK
|
|
|
270
322
|
# Lazy initialization of agents (with optional logging)
|
|
271
323
|
initialize_agents unless @agents_initialized
|
|
272
324
|
|
|
273
|
-
#
|
|
274
|
-
|
|
275
|
-
swarm_stop_triggered = false
|
|
276
|
-
|
|
277
|
-
loop do
|
|
278
|
-
# Execute within Async reactor to enable fiber scheduler for parallel execution
|
|
279
|
-
# This sets Fiber.scheduler, making Faraday fiber-aware so HTTP requests yield during I/O
|
|
280
|
-
# Use finished: false to suppress warnings for expected task failures
|
|
281
|
-
lead = @agents[@lead_agent]
|
|
282
|
-
response = Async(finished: false) do
|
|
283
|
-
lead.ask(current_prompt)
|
|
284
|
-
end.wait
|
|
285
|
-
|
|
286
|
-
# Check if swarm was finished by a hook (finish_swarm)
|
|
287
|
-
if response.is_a?(Hash) && response[:__finish_swarm__]
|
|
288
|
-
result = Result.new(
|
|
289
|
-
content: response[:message],
|
|
290
|
-
agent: @lead_agent.to_s,
|
|
291
|
-
logs: logs,
|
|
292
|
-
duration: Time.now - start_time,
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
# Trigger swarm_stop hooks for event emission
|
|
296
|
-
trigger_swarm_stop(result)
|
|
297
|
-
swarm_stop_triggered = true
|
|
298
|
-
|
|
299
|
-
# Break immediately - don't allow reprompting when swarm is finished by hook
|
|
300
|
-
break
|
|
301
|
-
end
|
|
325
|
+
# Emit agent_start events if agents were initialized before logging was set up
|
|
326
|
+
emit_retroactive_agent_start_events if has_logging
|
|
302
327
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
# Trigger swarm_stop hooks (for reprompt check and event emission)
|
|
311
|
-
hook_result = trigger_swarm_stop(result)
|
|
312
|
-
swarm_stop_triggered = true
|
|
313
|
-
|
|
314
|
-
# Check if hook requests reprompting
|
|
315
|
-
if hook_result&.reprompt?
|
|
316
|
-
current_prompt = hook_result.value
|
|
317
|
-
swarm_stop_triggered = false # Will trigger again in next iteration
|
|
318
|
-
# Continue loop with new prompt
|
|
319
|
-
else
|
|
320
|
-
# Exit loop - execution complete
|
|
321
|
-
break
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
result
|
|
326
|
-
rescue ConfigurationError, AgentNotFoundError
|
|
327
|
-
# Re-raise configuration errors - these should be fixed, not caught
|
|
328
|
-
raise
|
|
329
|
-
rescue TypeError => e
|
|
330
|
-
# Catch the specific "String does not have #dig method" error
|
|
331
|
-
if e.message.include?("does not have #dig method")
|
|
332
|
-
agent_definition = @agent_definitions[@lead_agent]
|
|
333
|
-
error_msg = if agent_definition.base_url
|
|
334
|
-
"LLM API request failed: The proxy/server at '#{agent_definition.base_url}' returned an invalid response. " \
|
|
335
|
-
"This usually means the proxy is unreachable, requires authentication, or returned an error in non-JSON format. " \
|
|
336
|
-
"Original error: #{e.message}"
|
|
337
|
-
else
|
|
338
|
-
"LLM API request failed with unexpected response format. Original error: #{e.message}"
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
result = Result.new(
|
|
342
|
-
content: nil,
|
|
343
|
-
agent: @lead_agent.to_s,
|
|
344
|
-
error: LLMError.new(error_msg),
|
|
345
|
-
logs: logs,
|
|
346
|
-
duration: Time.now - start_time,
|
|
347
|
-
)
|
|
348
|
-
else
|
|
349
|
-
result = Result.new(
|
|
350
|
-
content: nil,
|
|
351
|
-
agent: @lead_agent.to_s,
|
|
352
|
-
error: e,
|
|
353
|
-
logs: logs,
|
|
354
|
-
duration: Time.now - start_time,
|
|
355
|
-
)
|
|
356
|
-
end
|
|
357
|
-
result
|
|
358
|
-
rescue StandardError => e
|
|
359
|
-
result = Result.new(
|
|
360
|
-
content: nil,
|
|
361
|
-
agent: @lead_agent&.to_s || "unknown",
|
|
362
|
-
error: e,
|
|
328
|
+
# Delegate to Executor for actual execution
|
|
329
|
+
executor = Executor.new(self)
|
|
330
|
+
@current_task = executor.run(
|
|
331
|
+
current_prompt,
|
|
332
|
+
wait: wait,
|
|
363
333
|
logs: logs,
|
|
364
|
-
|
|
334
|
+
has_logging: has_logging,
|
|
335
|
+
original_fiber_storage: original_fiber_storage,
|
|
365
336
|
)
|
|
366
|
-
result
|
|
367
|
-
ensure
|
|
368
|
-
# Trigger swarm_stop if not already triggered (handles error cases)
|
|
369
|
-
unless swarm_stop_triggered
|
|
370
|
-
trigger_swarm_stop_final(result, start_time, logs)
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
# Cleanup MCP clients after execution
|
|
374
|
-
cleanup
|
|
375
|
-
|
|
376
|
-
# Reset logging state for next execution if we set it up
|
|
377
|
-
#
|
|
378
|
-
# IMPORTANT: Only reset if we set up logging (block_given? == true).
|
|
379
|
-
# When this swarm is a mini-swarm within a NodeOrchestrator workflow,
|
|
380
|
-
# the orchestrator manages LogCollector and we don't set up logging.
|
|
381
|
-
#
|
|
382
|
-
# Flow in NodeOrchestrator:
|
|
383
|
-
# 1. NodeOrchestrator sets up LogCollector + LogStream (no block given to mini-swarms)
|
|
384
|
-
# 2. Each mini-swarm executes without logging block (block_given? == false)
|
|
385
|
-
# 3. Each mini-swarm skips reset (didn't set up logging)
|
|
386
|
-
# 4. NodeOrchestrator resets once at the very end
|
|
387
|
-
#
|
|
388
|
-
# Flow in standalone swarm / interactive REPL:
|
|
389
|
-
# 1. Swarm.execute sets up LogCollector + LogStream (block given)
|
|
390
|
-
# 2. Swarm.execute resets in ensure block (cleanup for next call)
|
|
391
|
-
if block_given?
|
|
392
|
-
LogCollector.reset!
|
|
393
|
-
LogStream.reset!
|
|
394
|
-
end
|
|
395
337
|
end
|
|
396
338
|
|
|
397
339
|
# Get an agent chat instance by name
|
|
@@ -425,77 +367,17 @@ module SwarmSDK
|
|
|
425
367
|
@agent_definitions.keys
|
|
426
368
|
end
|
|
427
369
|
|
|
428
|
-
#
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
# Useful for displaying configuration warnings before execution.
|
|
432
|
-
#
|
|
433
|
-
# @return [Array<Hash>] Array of warning hashes from all agent definitions
|
|
434
|
-
#
|
|
435
|
-
# @example
|
|
436
|
-
# swarm = Swarm.load("config.yml")
|
|
437
|
-
# warnings = swarm.validate
|
|
438
|
-
# warnings.each do |warning|
|
|
439
|
-
# puts "⚠️ #{warning[:agent]}: #{warning[:model]} not found"
|
|
440
|
-
# end
|
|
441
|
-
def validate
|
|
442
|
-
@agent_definitions.flat_map { |_name, definition| definition.validate }
|
|
370
|
+
# Implement Snapshotable interface
|
|
371
|
+
def primary_agents
|
|
372
|
+
@agents
|
|
443
373
|
end
|
|
444
374
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
# This validates all agent definitions and emits any warnings as
|
|
448
|
-
# model_lookup_warning events through LogStream. Useful for emitting
|
|
449
|
-
# warnings before execution starts (e.g., in REPL after welcome screen).
|
|
450
|
-
#
|
|
451
|
-
# Requires LogStream.emitter to be set.
|
|
452
|
-
#
|
|
453
|
-
# @return [Array<Hash>] The validation warnings that were emitted
|
|
454
|
-
#
|
|
455
|
-
# @example
|
|
456
|
-
# LogCollector.on_log { |event| puts event }
|
|
457
|
-
# LogStream.emitter = LogCollector
|
|
458
|
-
# swarm.emit_validation_warnings
|
|
459
|
-
def emit_validation_warnings
|
|
460
|
-
warnings = validate
|
|
461
|
-
|
|
462
|
-
warnings.each do |warning|
|
|
463
|
-
case warning[:type]
|
|
464
|
-
when :model_not_found
|
|
465
|
-
LogStream.emit(
|
|
466
|
-
type: "model_lookup_warning",
|
|
467
|
-
agent: warning[:agent],
|
|
468
|
-
model: warning[:model],
|
|
469
|
-
error_message: warning[:error_message],
|
|
470
|
-
suggestions: warning[:suggestions],
|
|
471
|
-
timestamp: Time.now.utc.iso8601,
|
|
472
|
-
)
|
|
473
|
-
end
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
warnings
|
|
375
|
+
def delegation_instances_hash
|
|
376
|
+
@delegation_instances
|
|
477
377
|
end
|
|
478
378
|
|
|
479
|
-
#
|
|
480
|
-
#
|
|
481
|
-
# Stops all MCP client connections gracefully.
|
|
482
|
-
# Should be called when the swarm is no longer needed.
|
|
483
|
-
#
|
|
484
|
-
# @return [void]
|
|
485
|
-
def cleanup
|
|
486
|
-
return if @mcp_clients.empty?
|
|
487
|
-
|
|
488
|
-
@mcp_clients.each do |agent_name, clients|
|
|
489
|
-
clients.each do |client|
|
|
490
|
-
client.stop if client.alive?
|
|
491
|
-
RubyLLM.logger.debug("SwarmSDK: Stopped MCP client '#{client.name}' for agent #{agent_name}")
|
|
492
|
-
rescue StandardError => e
|
|
493
|
-
RubyLLM.logger.error("SwarmSDK: Error stopping MCP client '#{client.name}' for agent #{agent_name}: #{e.message}")
|
|
494
|
-
end
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
@mcp_clients.clear
|
|
498
|
-
end
|
|
379
|
+
# NOTE: validate() and emit_validation_warnings() are provided by Concerns::Validatable
|
|
380
|
+
# Note: cleanup() is provided by Concerns::Cleanupable
|
|
499
381
|
|
|
500
382
|
# Register a named hook that can be referenced in agent configurations
|
|
501
383
|
#
|
|
@@ -515,28 +397,229 @@ module SwarmSDK
|
|
|
515
397
|
self
|
|
516
398
|
end
|
|
517
399
|
|
|
518
|
-
#
|
|
400
|
+
# Reset context for all agents
|
|
519
401
|
#
|
|
520
|
-
#
|
|
521
|
-
#
|
|
402
|
+
# Clears conversation history for all agents. This is used by composable swarms
|
|
403
|
+
# to reset sub-swarm context when keep_context: false is specified.
|
|
522
404
|
#
|
|
523
|
-
# @
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
405
|
+
# @return [void]
|
|
406
|
+
def reset_context!
|
|
407
|
+
@agents.each_value do |agent_chat|
|
|
408
|
+
agent_chat.clear_conversation if agent_chat.respond_to?(:clear_conversation)
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Add observer configuration
|
|
413
|
+
#
|
|
414
|
+
# Called by Swarm::Builder to register observer agent configurations.
|
|
415
|
+
# Validates that the referenced agent exists.
|
|
416
|
+
#
|
|
417
|
+
# @param config [Observer::Config] Observer configuration
|
|
418
|
+
# @return [void]
|
|
419
|
+
def add_observer_config(config)
|
|
420
|
+
validate_observer_agent(config.agent_name)
|
|
421
|
+
@observer_configs << config
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Wait for all observer tasks to complete
|
|
425
|
+
#
|
|
426
|
+
# Called by Executor to wait for observer agents before cleanup.
|
|
427
|
+
# Safe to call even if no observers are configured.
|
|
528
428
|
#
|
|
529
|
-
# @
|
|
530
|
-
|
|
531
|
-
|
|
429
|
+
# @return [void]
|
|
430
|
+
def wait_for_observers
|
|
431
|
+
@observer_manager&.wait_for_completion
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Cleanup observer subscriptions
|
|
435
|
+
#
|
|
436
|
+
# Called by Executor.cleanup_after_execution to unsubscribe observers.
|
|
437
|
+
# Matches the MCP cleanup pattern.
|
|
438
|
+
#
|
|
439
|
+
# @return [void]
|
|
440
|
+
def cleanup_observers
|
|
441
|
+
@observer_manager&.cleanup
|
|
442
|
+
@observer_manager = nil
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Create snapshot of current conversation state
|
|
446
|
+
#
|
|
447
|
+
# Returns a Snapshot object containing:
|
|
448
|
+
# - All agent conversations (@messages arrays)
|
|
449
|
+
# - Agent context state (warnings, compression, TodoWrite tracking, skills)
|
|
450
|
+
# - Delegation instance conversations
|
|
451
|
+
# - Scratchpad contents (volatile shared storage)
|
|
452
|
+
# - Read tracking state (which files each agent has read with digests)
|
|
453
|
+
# - Memory read tracking state (which memory entries each agent has read with digests)
|
|
454
|
+
#
|
|
455
|
+
# Configuration (agent definitions, tools, prompts) stays in your YAML/DSL
|
|
456
|
+
# and is NOT included in snapshots.
|
|
457
|
+
#
|
|
458
|
+
# @return [Snapshot] Snapshot object with convenient serialization methods
|
|
459
|
+
#
|
|
460
|
+
# @example Save snapshot to JSON file
|
|
461
|
+
# snapshot = swarm.snapshot
|
|
462
|
+
# snapshot.write_to_file("session.json")
|
|
463
|
+
#
|
|
464
|
+
# @example Convert to hash or JSON string
|
|
465
|
+
# snapshot = swarm.snapshot
|
|
466
|
+
# hash = snapshot.to_hash
|
|
467
|
+
# json_string = snapshot.to_json
|
|
468
|
+
def snapshot
|
|
469
|
+
StateSnapshot.new(self).snapshot
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Restore conversation state from snapshot
|
|
473
|
+
#
|
|
474
|
+
# Accepts a Snapshot object, hash, or JSON string. Validates compatibility
|
|
475
|
+
# between snapshot and current swarm configuration, restores agent conversations,
|
|
476
|
+
# context state, scratchpad, and read tracking. Returns RestoreResult with
|
|
477
|
+
# warnings about any agents that couldn't be restored due to configuration
|
|
478
|
+
# mismatches.
|
|
479
|
+
#
|
|
480
|
+
# The swarm must be created with the SAME configuration (agent definitions,
|
|
481
|
+
# tools, prompts) as when the snapshot was created. Only conversation state
|
|
482
|
+
# is restored from the snapshot.
|
|
483
|
+
#
|
|
484
|
+
# @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
|
|
485
|
+
# @return [RestoreResult] Result with warnings about skipped agents
|
|
486
|
+
#
|
|
487
|
+
# @example Restore from Snapshot object
|
|
488
|
+
# swarm = SwarmSDK.build { ... } # Same config as snapshot
|
|
489
|
+
# snapshot = Snapshot.from_file("session.json")
|
|
490
|
+
# result = swarm.restore(snapshot)
|
|
491
|
+
# if result.success?
|
|
492
|
+
# puts "All agents restored"
|
|
493
|
+
# else
|
|
494
|
+
# puts result.summary
|
|
495
|
+
# result.warnings.each { |w| puts " - #{w[:message]}" }
|
|
532
496
|
# end
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
497
|
+
#
|
|
498
|
+
# Restore swarm state from snapshot
|
|
499
|
+
#
|
|
500
|
+
# By default, uses current system prompts from agent definitions (YAML + SDK defaults + plugin injections).
|
|
501
|
+
# Set preserve_system_prompts: true to use historical prompts from snapshot.
|
|
502
|
+
#
|
|
503
|
+
# @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
|
|
504
|
+
# @param preserve_system_prompts [Boolean] Use historical system prompts instead of current config (default: false)
|
|
505
|
+
# @return [RestoreResult] Result with warnings about partial restores
|
|
506
|
+
def restore(snapshot, preserve_system_prompts: false)
|
|
507
|
+
StateRestorer.new(self, snapshot, preserve_system_prompts: preserve_system_prompts).restore
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Override swarm IDs for composable swarms
|
|
511
|
+
#
|
|
512
|
+
# Used by SwarmLoader to set hierarchical IDs when loading sub-swarms.
|
|
513
|
+
# This is called after the swarm is built to ensure proper parent/child relationships.
|
|
514
|
+
#
|
|
515
|
+
# @param swarm_id [String] New swarm ID
|
|
516
|
+
# @param parent_swarm_id [String] New parent swarm ID
|
|
517
|
+
# @return [void]
|
|
518
|
+
def override_swarm_ids(swarm_id:, parent_swarm_id:)
|
|
519
|
+
@swarm_id = swarm_id
|
|
520
|
+
@parent_swarm_id = parent_swarm_id
|
|
536
521
|
end
|
|
537
522
|
|
|
523
|
+
# Set swarm registry for composable swarms
|
|
524
|
+
#
|
|
525
|
+
# Used by Builder to set the registry after swarm creation.
|
|
526
|
+
# This must be called before agent initialization to enable swarm delegation.
|
|
527
|
+
#
|
|
528
|
+
# @param registry [SwarmRegistry] Configured swarm registry
|
|
529
|
+
# @return [void]
|
|
530
|
+
attr_writer :swarm_registry
|
|
531
|
+
|
|
532
|
+
# --- Internal API (for Executor use only) ---
|
|
533
|
+
# Hook triggers for swarm lifecycle events are provided by HookTriggers module
|
|
534
|
+
|
|
538
535
|
private
|
|
539
536
|
|
|
537
|
+
# Apply swarm_start hooks to prompt
|
|
538
|
+
#
|
|
539
|
+
# @param prompt [String] Original prompt
|
|
540
|
+
# @return [String] Modified prompt (possibly with hook context appended)
|
|
541
|
+
def apply_swarm_start_hooks(prompt)
|
|
542
|
+
swarm_start_result = trigger_swarm_start(prompt)
|
|
543
|
+
if swarm_start_result&.replace?
|
|
544
|
+
"#{prompt}\n\n<hook-context>\n#{swarm_start_result.value}\n</hook-context>"
|
|
545
|
+
else
|
|
546
|
+
prompt
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Validate that observer agent exists
|
|
551
|
+
#
|
|
552
|
+
# @param agent_name [Symbol] Name of the observer agent
|
|
553
|
+
# @raise [ConfigurationError] If agent not found
|
|
554
|
+
# @return [void]
|
|
555
|
+
def validate_observer_agent(agent_name)
|
|
556
|
+
return if @agent_definitions.key?(agent_name)
|
|
557
|
+
|
|
558
|
+
raise ConfigurationError,
|
|
559
|
+
"Observer agent '#{agent_name}' not found. " \
|
|
560
|
+
"Define the agent first with `agent :#{agent_name} do ... end`"
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Setup observer manager and subscriptions
|
|
564
|
+
#
|
|
565
|
+
# Creates Observer::Manager and registers event subscriptions.
|
|
566
|
+
# Must be called AFTER setup_logging (which clears Fiber[:log_subscriptions]).
|
|
567
|
+
#
|
|
568
|
+
# @return [void]
|
|
569
|
+
def setup_observer_execution
|
|
570
|
+
@observer_manager = Observer::Manager.new(self)
|
|
571
|
+
@observer_configs.each { |c| @observer_manager.add_config(c) }
|
|
572
|
+
@observer_manager.setup
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Validate and normalize scratchpad mode for Swarm
|
|
576
|
+
#
|
|
577
|
+
# Regular Swarms support :enabled or :disabled.
|
|
578
|
+
# Rejects :per_node since it only makes sense for Workflow with multiple nodes.
|
|
579
|
+
#
|
|
580
|
+
# @param value [Symbol, String] Scratchpad mode (strings from YAML converted to symbols)
|
|
581
|
+
# @return [Symbol] :enabled or :disabled
|
|
582
|
+
# @raise [ArgumentError] If :per_node used, or invalid value
|
|
583
|
+
def validate_swarm_scratchpad_mode(value)
|
|
584
|
+
# Convert strings from YAML to symbols
|
|
585
|
+
value = value.to_sym if value.is_a?(String)
|
|
586
|
+
|
|
587
|
+
case value
|
|
588
|
+
when :enabled, :disabled
|
|
589
|
+
value
|
|
590
|
+
when :per_node
|
|
591
|
+
raise ArgumentError,
|
|
592
|
+
"scratchpad: :per_node is only valid for Workflow with nodes. " \
|
|
593
|
+
"For regular Swarms, use :enabled or :disabled."
|
|
594
|
+
else
|
|
595
|
+
raise ArgumentError,
|
|
596
|
+
"Invalid scratchpad mode for Swarm: #{value.inspect}. " \
|
|
597
|
+
"Use :enabled or :disabled."
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# Generate a unique swarm ID from name
|
|
602
|
+
#
|
|
603
|
+
# Creates a swarm ID by sanitizing the name and appending a random suffix.
|
|
604
|
+
# Used when swarm_id is not explicitly provided.
|
|
605
|
+
#
|
|
606
|
+
# @param name [String] Swarm name
|
|
607
|
+
# @return [String] Generated swarm ID (e.g., "dev_team_a3f2b1c8")
|
|
608
|
+
def generate_swarm_id(name)
|
|
609
|
+
sanitized = name.to_s.gsub(/[^a-z0-9_-]/i, "_").downcase
|
|
610
|
+
"#{sanitized}_#{SecureRandom.hex(4)}"
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# Generate a unique execution ID
|
|
614
|
+
#
|
|
615
|
+
# Creates an execution ID that uniquely identifies a single swarm.execute() call.
|
|
616
|
+
# Format: "exec_{swarm_id}_{random_hex}"
|
|
617
|
+
#
|
|
618
|
+
# @return [String] Generated execution ID (e.g., "exec_main_a3f2b1c8")
|
|
619
|
+
def generate_execution_id
|
|
620
|
+
"exec_#{@swarm_id}_#{SecureRandom.hex(8)}"
|
|
621
|
+
end
|
|
622
|
+
|
|
540
623
|
# Initialize all agents using AgentInitializer
|
|
541
624
|
#
|
|
542
625
|
# This is called automatically (lazy initialization) by execute() and agent().
|
|
@@ -546,58 +629,14 @@ module SwarmSDK
|
|
|
546
629
|
def initialize_agents
|
|
547
630
|
return if @agents_initialized
|
|
548
631
|
|
|
549
|
-
initializer = AgentInitializer.new(
|
|
550
|
-
self,
|
|
551
|
-
@agent_definitions,
|
|
552
|
-
@global_semaphore,
|
|
553
|
-
@hook_registry,
|
|
554
|
-
@scratchpad_storage,
|
|
555
|
-
@plugin_storages,
|
|
556
|
-
config_for_hooks: @config_for_hooks,
|
|
557
|
-
)
|
|
632
|
+
initializer = AgentInitializer.new(self)
|
|
558
633
|
|
|
559
634
|
@agents = initializer.initialize_all
|
|
560
635
|
@agent_contexts = initializer.agent_contexts
|
|
561
636
|
@agents_initialized = true
|
|
562
637
|
|
|
563
|
-
#
|
|
564
|
-
|
|
565
|
-
end
|
|
566
|
-
|
|
567
|
-
# Emit agent_start events for all initialized agents
|
|
568
|
-
def emit_agent_start_events
|
|
569
|
-
# Only emit if LogStream is enabled
|
|
570
|
-
return unless LogStream.emitter
|
|
571
|
-
|
|
572
|
-
@agents.each do |agent_name, chat|
|
|
573
|
-
agent_def = @agent_definitions[agent_name]
|
|
574
|
-
|
|
575
|
-
# Build plugin storage info for logging
|
|
576
|
-
plugin_storage_info = {}
|
|
577
|
-
@plugin_storages.each do |plugin_name, agent_storages|
|
|
578
|
-
next unless agent_storages.key?(agent_name)
|
|
579
|
-
|
|
580
|
-
plugin_storage_info[plugin_name] = {
|
|
581
|
-
enabled: true,
|
|
582
|
-
# Get additional info from agent definition if available
|
|
583
|
-
config: agent_def.respond_to?(plugin_name) ? extract_plugin_config_info(agent_def.public_send(plugin_name)) : nil,
|
|
584
|
-
}
|
|
585
|
-
end
|
|
586
|
-
|
|
587
|
-
LogStream.emit(
|
|
588
|
-
type: "agent_start",
|
|
589
|
-
agent: agent_name,
|
|
590
|
-
swarm_name: @name,
|
|
591
|
-
model: agent_def.model,
|
|
592
|
-
provider: agent_def.provider || "openai",
|
|
593
|
-
directory: agent_def.directory,
|
|
594
|
-
system_prompt: agent_def.system_prompt,
|
|
595
|
-
tools: chat.tools.keys,
|
|
596
|
-
delegates_to: agent_def.delegates_to,
|
|
597
|
-
plugin_storages: plugin_storage_info,
|
|
598
|
-
timestamp: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
599
|
-
)
|
|
600
|
-
end
|
|
638
|
+
# NOTE: agent_start events are emitted in execute() when logging is set up
|
|
639
|
+
# This ensures events are never lost, even if agents are initialized early (e.g., by restore())
|
|
601
640
|
end
|
|
602
641
|
|
|
603
642
|
# Normalize tools to internal format (kept for add_agent)
|
|
@@ -647,7 +686,7 @@ module SwarmSDK
|
|
|
647
686
|
|
|
648
687
|
# Create delegation tool (delegates to AgentInitializer)
|
|
649
688
|
def create_delegation_tool(name:, description:, delegate_chat:, agent_name:)
|
|
650
|
-
AgentInitializer.new(self
|
|
689
|
+
AgentInitializer.new(self)
|
|
651
690
|
.create_delegation_tool(name: name, description: description, delegate_chat: delegate_chat, agent_name: agent_name)
|
|
652
691
|
end
|
|
653
692
|
|
|
@@ -674,309 +713,5 @@ module SwarmSDK
|
|
|
674
713
|
# Unknown config type
|
|
675
714
|
nil
|
|
676
715
|
end
|
|
677
|
-
|
|
678
|
-
# Register default logging hooks that emit LogStream events
|
|
679
|
-
#
|
|
680
|
-
# These hooks implement the standard SwarmSDK logging behavior.
|
|
681
|
-
# Users can override or extend them by registering their own hooks.
|
|
682
|
-
#
|
|
683
|
-
# @return [void]
|
|
684
|
-
def register_default_logging_callbacks
|
|
685
|
-
# Log swarm start
|
|
686
|
-
add_default_callback(:swarm_start, priority: -100) do |context|
|
|
687
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
688
|
-
next unless LogStream.emitter
|
|
689
|
-
|
|
690
|
-
LogStream.emit(
|
|
691
|
-
type: "swarm_start",
|
|
692
|
-
agent: context.metadata[:lead_agent], # Include agent for consistency
|
|
693
|
-
swarm_name: context.metadata[:swarm_name],
|
|
694
|
-
lead_agent: context.metadata[:lead_agent],
|
|
695
|
-
prompt: context.metadata[:prompt],
|
|
696
|
-
timestamp: context.metadata[:timestamp],
|
|
697
|
-
)
|
|
698
|
-
end
|
|
699
|
-
|
|
700
|
-
# Log swarm stop
|
|
701
|
-
add_default_callback(:swarm_stop, priority: -100) do |context|
|
|
702
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
703
|
-
next unless LogStream.emitter
|
|
704
|
-
|
|
705
|
-
LogStream.emit(
|
|
706
|
-
type: "swarm_stop",
|
|
707
|
-
swarm_name: context.metadata[:swarm_name],
|
|
708
|
-
lead_agent: context.metadata[:lead_agent],
|
|
709
|
-
last_agent: context.metadata[:last_agent], # Agent that produced final response
|
|
710
|
-
content: context.metadata[:content], # Final response content
|
|
711
|
-
success: context.metadata[:success],
|
|
712
|
-
duration: context.metadata[:duration],
|
|
713
|
-
total_cost: context.metadata[:total_cost],
|
|
714
|
-
total_tokens: context.metadata[:total_tokens],
|
|
715
|
-
agents_involved: context.metadata[:agents_involved],
|
|
716
|
-
timestamp: context.metadata[:timestamp],
|
|
717
|
-
)
|
|
718
|
-
end
|
|
719
|
-
|
|
720
|
-
# Log user requests
|
|
721
|
-
add_default_callback(:user_prompt, priority: -100) do |context|
|
|
722
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
723
|
-
next unless LogStream.emitter
|
|
724
|
-
|
|
725
|
-
LogStream.emit(
|
|
726
|
-
type: "user_prompt",
|
|
727
|
-
agent: context.agent_name,
|
|
728
|
-
model: context.metadata[:model] || "unknown",
|
|
729
|
-
provider: context.metadata[:provider] || "unknown",
|
|
730
|
-
message_count: context.metadata[:message_count] || 0,
|
|
731
|
-
tools: context.metadata[:tools] || [],
|
|
732
|
-
delegates_to: context.metadata[:delegates_to] || [],
|
|
733
|
-
metadata: context.metadata,
|
|
734
|
-
)
|
|
735
|
-
end
|
|
736
|
-
|
|
737
|
-
# Log intermediate agent responses with tool calls
|
|
738
|
-
add_default_callback(:agent_step, priority: -100) do |context|
|
|
739
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
740
|
-
next unless LogStream.emitter
|
|
741
|
-
|
|
742
|
-
# Extract top-level fields and remove from metadata to avoid duplication
|
|
743
|
-
metadata_without_duplicates = context.metadata.except(:model, :content, :tool_calls, :finish_reason, :usage, :tool_executions)
|
|
744
|
-
|
|
745
|
-
LogStream.emit(
|
|
746
|
-
type: "agent_step",
|
|
747
|
-
agent: context.agent_name,
|
|
748
|
-
model: context.metadata[:model],
|
|
749
|
-
content: context.metadata[:content],
|
|
750
|
-
tool_calls: context.metadata[:tool_calls],
|
|
751
|
-
finish_reason: context.metadata[:finish_reason],
|
|
752
|
-
usage: context.metadata[:usage],
|
|
753
|
-
tool_executions: context.metadata[:tool_executions],
|
|
754
|
-
metadata: metadata_without_duplicates,
|
|
755
|
-
)
|
|
756
|
-
end
|
|
757
|
-
|
|
758
|
-
# Log final agent responses
|
|
759
|
-
add_default_callback(:agent_stop, priority: -100) do |context|
|
|
760
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
761
|
-
next unless LogStream.emitter
|
|
762
|
-
|
|
763
|
-
# Extract top-level fields and remove from metadata to avoid duplication
|
|
764
|
-
metadata_without_duplicates = context.metadata.except(:model, :content, :tool_calls, :finish_reason, :usage, :tool_executions)
|
|
765
|
-
|
|
766
|
-
LogStream.emit(
|
|
767
|
-
type: "agent_stop",
|
|
768
|
-
agent: context.agent_name,
|
|
769
|
-
model: context.metadata[:model],
|
|
770
|
-
content: context.metadata[:content],
|
|
771
|
-
tool_calls: context.metadata[:tool_calls],
|
|
772
|
-
finish_reason: context.metadata[:finish_reason],
|
|
773
|
-
usage: context.metadata[:usage],
|
|
774
|
-
tool_executions: context.metadata[:tool_executions],
|
|
775
|
-
metadata: metadata_without_duplicates,
|
|
776
|
-
)
|
|
777
|
-
end
|
|
778
|
-
|
|
779
|
-
# Log tool calls (pre_tool_use)
|
|
780
|
-
add_default_callback(:pre_tool_use, priority: -100) do |context|
|
|
781
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
782
|
-
next unless LogStream.emitter
|
|
783
|
-
|
|
784
|
-
# Delegation tracking is handled separately in AgentChat
|
|
785
|
-
# Just log the tool call - delegation info will be in metadata if needed
|
|
786
|
-
LogStream.emit(
|
|
787
|
-
type: "tool_call",
|
|
788
|
-
agent: context.agent_name,
|
|
789
|
-
tool_call_id: context.tool_call.id,
|
|
790
|
-
tool: context.tool_call.name,
|
|
791
|
-
arguments: context.tool_call.parameters,
|
|
792
|
-
metadata: context.metadata,
|
|
793
|
-
)
|
|
794
|
-
end
|
|
795
|
-
|
|
796
|
-
# Log tool results (post_tool_use)
|
|
797
|
-
add_default_callback(:post_tool_use, priority: -100) do |context|
|
|
798
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
799
|
-
next unless LogStream.emitter
|
|
800
|
-
|
|
801
|
-
# Delegation tracking is handled separately in AgentChat
|
|
802
|
-
# Usage tracking is handled in agent_step/agent_stop events
|
|
803
|
-
LogStream.emit(
|
|
804
|
-
type: "tool_result",
|
|
805
|
-
agent: context.agent_name,
|
|
806
|
-
tool_call_id: context.tool_result.tool_call_id,
|
|
807
|
-
tool: context.tool_result.tool_name,
|
|
808
|
-
result: context.tool_result.content,
|
|
809
|
-
metadata: context.metadata,
|
|
810
|
-
)
|
|
811
|
-
end
|
|
812
|
-
|
|
813
|
-
# Log context warnings
|
|
814
|
-
add_default_callback(:context_warning, priority: -100) do |context|
|
|
815
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
816
|
-
next unless LogStream.emitter
|
|
817
|
-
|
|
818
|
-
LogStream.emit(
|
|
819
|
-
type: "context_limit_warning",
|
|
820
|
-
agent: context.agent_name,
|
|
821
|
-
model: context.metadata[:model] || "unknown",
|
|
822
|
-
threshold: "#{context.metadata[:threshold]}%",
|
|
823
|
-
current_usage: "#{context.metadata[:percentage]}%",
|
|
824
|
-
tokens_used: context.metadata[:tokens_used],
|
|
825
|
-
tokens_remaining: context.metadata[:tokens_remaining],
|
|
826
|
-
context_limit: context.metadata[:context_limit],
|
|
827
|
-
metadata: context.metadata,
|
|
828
|
-
)
|
|
829
|
-
end
|
|
830
|
-
end
|
|
831
|
-
|
|
832
|
-
# Trigger swarm_start hooks when swarm execution begins
|
|
833
|
-
#
|
|
834
|
-
# This is a swarm-level event that fires when Swarm.execute is called
|
|
835
|
-
# (before first user message is sent). Hooks can halt execution or append stdout to prompt.
|
|
836
|
-
# Default callback emits to LogStream for logging.
|
|
837
|
-
#
|
|
838
|
-
# @param prompt [String] The user's task prompt
|
|
839
|
-
# @return [Hooks::Result, nil] Result with stdout to append (if exit 0) or nil
|
|
840
|
-
# @raise [Hooks::Error] If hook halts execution
|
|
841
|
-
def trigger_swarm_start(prompt)
|
|
842
|
-
context = Hooks::Context.new(
|
|
843
|
-
event: :swarm_start,
|
|
844
|
-
agent_name: @lead_agent.to_s,
|
|
845
|
-
swarm: self,
|
|
846
|
-
metadata: {
|
|
847
|
-
swarm_name: @name,
|
|
848
|
-
lead_agent: @lead_agent,
|
|
849
|
-
prompt: prompt,
|
|
850
|
-
timestamp: Time.now.utc.iso8601,
|
|
851
|
-
},
|
|
852
|
-
)
|
|
853
|
-
|
|
854
|
-
executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
|
|
855
|
-
result = executor.execute_safe(event: :swarm_start, context: context, callbacks: [])
|
|
856
|
-
|
|
857
|
-
# Halt execution if hook requests it
|
|
858
|
-
raise Hooks::Error, "Swarm start halted by hook: #{result.value}" if result.halt?
|
|
859
|
-
|
|
860
|
-
# Return result so caller can check for replace (stdout injection)
|
|
861
|
-
result
|
|
862
|
-
rescue StandardError => e
|
|
863
|
-
RubyLLM.logger.error("SwarmSDK: Error in swarm_start hook: #{e.message}")
|
|
864
|
-
raise
|
|
865
|
-
end
|
|
866
|
-
|
|
867
|
-
# Trigger swarm_stop for final event emission (called in ensure block)
|
|
868
|
-
#
|
|
869
|
-
# This ALWAYS emits the swarm_stop event, even if there was an error.
|
|
870
|
-
# It does NOT check for reprompt (that's done in trigger_swarm_stop_for_reprompt_check).
|
|
871
|
-
#
|
|
872
|
-
# @param result [Result, nil] Execution result (may be nil if exception before result created)
|
|
873
|
-
# @param start_time [Time] Execution start time
|
|
874
|
-
# @param logs [Array] Collected logs
|
|
875
|
-
# @return [void]
|
|
876
|
-
def trigger_swarm_stop_final(result, start_time, logs)
|
|
877
|
-
# Create a minimal result if one doesn't exist (exception before result created)
|
|
878
|
-
result ||= Result.new(
|
|
879
|
-
content: nil,
|
|
880
|
-
agent: @lead_agent&.to_s || "unknown",
|
|
881
|
-
logs: logs,
|
|
882
|
-
duration: Time.now - start_time,
|
|
883
|
-
error: StandardError.new("Unknown error"),
|
|
884
|
-
)
|
|
885
|
-
|
|
886
|
-
context = Hooks::Context.new(
|
|
887
|
-
event: :swarm_stop,
|
|
888
|
-
agent_name: @lead_agent.to_s,
|
|
889
|
-
swarm: self,
|
|
890
|
-
metadata: {
|
|
891
|
-
swarm_name: @name,
|
|
892
|
-
lead_agent: @lead_agent,
|
|
893
|
-
last_agent: result.agent, # Agent that produced the final response
|
|
894
|
-
content: result.content, # Final response content
|
|
895
|
-
success: result.success?,
|
|
896
|
-
duration: result.duration,
|
|
897
|
-
total_cost: result.total_cost,
|
|
898
|
-
total_tokens: result.total_tokens,
|
|
899
|
-
agents_involved: result.agents_involved,
|
|
900
|
-
result: result,
|
|
901
|
-
timestamp: Time.now.utc.iso8601,
|
|
902
|
-
},
|
|
903
|
-
)
|
|
904
|
-
|
|
905
|
-
executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
|
|
906
|
-
executor.execute_safe(event: :swarm_stop, context: context, callbacks: [])
|
|
907
|
-
rescue StandardError => e
|
|
908
|
-
# Don't let swarm_stop errors break the ensure block
|
|
909
|
-
RubyLLM.logger.error("SwarmSDK: Error in swarm_stop final emission: #{e.message}")
|
|
910
|
-
end
|
|
911
|
-
|
|
912
|
-
# Trigger swarm_stop hooks for reprompt check and event emission
|
|
913
|
-
#
|
|
914
|
-
# This is called in the normal execution flow to check if hooks request reprompting.
|
|
915
|
-
# The default callback also emits the swarm_stop event to LogStream.
|
|
916
|
-
#
|
|
917
|
-
# @param result [Result] The execution result
|
|
918
|
-
# @return [Hooks::Result, nil] Hook result (reprompt action if applicable)
|
|
919
|
-
def trigger_swarm_stop(result)
|
|
920
|
-
context = Hooks::Context.new(
|
|
921
|
-
event: :swarm_stop,
|
|
922
|
-
agent_name: @lead_agent.to_s,
|
|
923
|
-
swarm: self,
|
|
924
|
-
metadata: {
|
|
925
|
-
swarm_name: @name,
|
|
926
|
-
lead_agent: @lead_agent,
|
|
927
|
-
last_agent: result.agent, # Agent that produced the final response
|
|
928
|
-
content: result.content, # Final response content
|
|
929
|
-
success: result.success?,
|
|
930
|
-
duration: result.duration,
|
|
931
|
-
total_cost: result.total_cost,
|
|
932
|
-
total_tokens: result.total_tokens,
|
|
933
|
-
agents_involved: result.agents_involved,
|
|
934
|
-
result: result, # Include full result for hook access
|
|
935
|
-
timestamp: Time.now.utc.iso8601,
|
|
936
|
-
},
|
|
937
|
-
)
|
|
938
|
-
|
|
939
|
-
executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
|
|
940
|
-
hook_result = executor.execute_safe(event: :swarm_stop, context: context, callbacks: [])
|
|
941
|
-
|
|
942
|
-
# Return hook result so caller can handle reprompt
|
|
943
|
-
hook_result
|
|
944
|
-
rescue StandardError => e
|
|
945
|
-
RubyLLM.logger.error("SwarmSDK: Error in swarm_stop hook: #{e.message}")
|
|
946
|
-
nil
|
|
947
|
-
end
|
|
948
|
-
|
|
949
|
-
# Trigger first_message hooks when first user message is sent
|
|
950
|
-
#
|
|
951
|
-
# This is a swarm-level event that fires once on the first call to execute().
|
|
952
|
-
# Hooks can halt execution before the first message is sent.
|
|
953
|
-
#
|
|
954
|
-
# @param prompt [String] The first user message
|
|
955
|
-
# @return [void]
|
|
956
|
-
# @raise [Hooks::Error] If hook halts execution
|
|
957
|
-
def trigger_first_message(prompt)
|
|
958
|
-
return if @hook_registry.get_defaults(:first_message).empty?
|
|
959
|
-
|
|
960
|
-
context = Hooks::Context.new(
|
|
961
|
-
event: :first_message,
|
|
962
|
-
agent_name: @lead_agent.to_s,
|
|
963
|
-
swarm: self,
|
|
964
|
-
metadata: {
|
|
965
|
-
swarm_name: @name,
|
|
966
|
-
lead_agent: @lead_agent,
|
|
967
|
-
prompt: prompt,
|
|
968
|
-
timestamp: Time.now.utc.iso8601,
|
|
969
|
-
},
|
|
970
|
-
)
|
|
971
|
-
|
|
972
|
-
executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
|
|
973
|
-
result = executor.execute_safe(event: :first_message, context: context, callbacks: [])
|
|
974
|
-
|
|
975
|
-
# Halt execution if hook requests it
|
|
976
|
-
raise Hooks::Error, "First message halted by hook: #{result.value}" if result.halt?
|
|
977
|
-
rescue StandardError => e
|
|
978
|
-
RubyLLM.logger.error("SwarmSDK: Error in first_message hook: #{e.message}")
|
|
979
|
-
raise
|
|
980
|
-
end
|
|
981
716
|
end
|
|
982
717
|
end
|