swarm_memory 2.1.2 → 2.1.3
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/configuration.rb +28 -4
- data/lib/claude_swarm/mcp_generator.rb +4 -10
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
- data/lib/swarm_cli/config_loader.rb +3 -3
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/adapters/base.rb +4 -4
- 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 +11 -5
- 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 +5 -0
- data/lib/swarm_sdk/agent/builder.rb +33 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +33 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +41 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +11 -27
- data/lib/swarm_sdk/agent/chat.rb +198 -51
- data/lib/swarm_sdk/agent/context.rb +6 -2
- data/lib/swarm_sdk/agent/context_manager.rb +6 -0
- data/lib/swarm_sdk/agent/definition.rb +15 -22
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
- data/lib/swarm_sdk/configuration.rb +420 -103
- data/lib/swarm_sdk/events_to_messages.rb +181 -0
- data/lib/swarm_sdk/log_collector.rb +31 -5
- data/lib/swarm_sdk/log_stream.rb +37 -8
- data/lib/swarm_sdk/model_aliases.json +4 -1
- data/lib/swarm_sdk/node/agent_config.rb +33 -8
- data/lib/swarm_sdk/node/builder.rb +39 -18
- data/lib/swarm_sdk/node_orchestrator.rb +293 -26
- 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/providers/openai_with_responses.rb +22 -15
- 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 +386 -0
- data/lib/swarm_sdk/state_restorer.rb +491 -0
- data/lib/swarm_sdk/state_snapshot.rb +369 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +360 -55
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
- data/lib/swarm_sdk/swarm/builder.rb +208 -12
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +46 -11
- data/lib/swarm_sdk/swarm.rb +367 -90
- data/lib/swarm_sdk/swarm_loader.rb +145 -0
- data/lib/swarm_sdk/swarm_registry.rb +136 -0
- data/lib/swarm_sdk/tools/delegate.rb +92 -7
- data/lib/swarm_sdk/tools/read.rb +17 -5
- 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 +45 -0
- data/lib/swarm_sdk/tools/stores/storage.rb +4 -4
- data/lib/swarm_sdk/tools/think.rb +4 -1
- data/lib/swarm_sdk/tools/todo_write.rb +20 -8
- 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.rb +362 -21
- metadata +17 -5
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)
|
|
43
34
|
#
|
|
44
|
-
# swarm =
|
|
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)
|
|
50
|
+
#
|
|
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
|
|
@@ -61,16 +68,30 @@ module SwarmSDK
|
|
|
61
68
|
# Default tools available to all agents
|
|
62
69
|
DEFAULT_TOOLS = ToolConfigurator::DEFAULT_TOOLS
|
|
63
70
|
|
|
64
|
-
attr_reader :name, :agents, :lead_agent, :mcp_clients
|
|
71
|
+
attr_reader :name, :agents, :lead_agent, :mcp_clients, :delegation_instances, :agent_definitions, :swarm_id, :parent_swarm_id, :swarm_registry, :scratchpad_storage, :allow_filesystem_tools
|
|
72
|
+
attr_accessor :delegation_call_stack
|
|
65
73
|
|
|
66
74
|
# Check if scratchpad tools are enabled
|
|
67
75
|
#
|
|
68
76
|
# @return [Boolean]
|
|
69
77
|
def scratchpad_enabled?
|
|
70
|
-
@
|
|
78
|
+
@scratchpad_mode == :enabled
|
|
71
79
|
end
|
|
72
80
|
attr_writer :config_for_hooks
|
|
73
81
|
|
|
82
|
+
# Check if first message has been sent (for system reminder injection)
|
|
83
|
+
#
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
def first_message_sent?
|
|
86
|
+
@first_message_sent
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Set first message sent flag (used by snapshot/restore)
|
|
90
|
+
#
|
|
91
|
+
# @param value [Boolean] New value
|
|
92
|
+
# @return [void]
|
|
93
|
+
attr_writer :first_message_sent
|
|
94
|
+
|
|
74
95
|
# Class-level MCP log level configuration
|
|
75
96
|
@mcp_log_level = DEFAULT_MCP_LOG_LEVEL
|
|
76
97
|
@mcp_logging_configured = false
|
|
@@ -102,54 +123,54 @@ module SwarmSDK
|
|
|
102
123
|
|
|
103
124
|
@mcp_logging_configured = true
|
|
104
125
|
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
126
|
end
|
|
133
127
|
|
|
134
128
|
# Initialize a new Swarm
|
|
135
129
|
#
|
|
136
130
|
# @param name [String] Human-readable swarm name
|
|
131
|
+
# @param swarm_id [String, nil] Optional swarm ID (auto-generated if not provided)
|
|
132
|
+
# @param parent_swarm_id [String, nil] Optional parent swarm ID (nil for root swarms)
|
|
137
133
|
# @param global_concurrency [Integer] Max concurrent LLM calls across entire swarm
|
|
138
134
|
# @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
|
-
|
|
135
|
+
# @param scratchpad [Tools::Stores::Scratchpad, nil] Optional scratchpad instance (for testing/internal use)
|
|
136
|
+
# @param scratchpad_mode [Symbol, String] Scratchpad mode (:enabled or :disabled). :per_node not allowed for non-node swarms.
|
|
137
|
+
# @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
|
|
138
|
+
def initialize(name:, swarm_id: nil, parent_swarm_id: nil, global_concurrency: DEFAULT_GLOBAL_CONCURRENCY, default_local_concurrency: DEFAULT_LOCAL_CONCURRENCY, scratchpad: nil, scratchpad_mode: :enabled, allow_filesystem_tools: nil)
|
|
142
139
|
@name = name
|
|
140
|
+
@swarm_id = swarm_id || generate_swarm_id(name)
|
|
141
|
+
@parent_swarm_id = parent_swarm_id
|
|
143
142
|
@global_concurrency = global_concurrency
|
|
144
143
|
@default_local_concurrency = default_local_concurrency
|
|
145
|
-
|
|
144
|
+
|
|
145
|
+
# Handle scratchpad_mode parameter
|
|
146
|
+
# For Swarm: :enabled or :disabled (not :per_node - that's for nodes)
|
|
147
|
+
@scratchpad_mode = validate_swarm_scratchpad_mode(scratchpad_mode)
|
|
148
|
+
|
|
149
|
+
# Resolve allow_filesystem_tools with priority:
|
|
150
|
+
# 1. Explicit parameter (if not nil)
|
|
151
|
+
# 2. Global settings
|
|
152
|
+
@allow_filesystem_tools = if allow_filesystem_tools.nil?
|
|
153
|
+
SwarmSDK.settings.allow_filesystem_tools
|
|
154
|
+
else
|
|
155
|
+
allow_filesystem_tools
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Swarm registry for managing sub-swarms (initialized later if needed)
|
|
159
|
+
@swarm_registry = nil
|
|
160
|
+
|
|
161
|
+
# Delegation call stack for circular dependency detection
|
|
162
|
+
@delegation_call_stack = []
|
|
146
163
|
|
|
147
164
|
# Shared semaphore for all agents
|
|
148
165
|
@global_semaphore = Async::Semaphore.new(@global_concurrency)
|
|
149
166
|
|
|
150
167
|
# Shared scratchpad storage for all agents (volatile)
|
|
151
|
-
# Use provided scratchpad storage (for testing) or create volatile one
|
|
152
|
-
@scratchpad_storage = scratchpad
|
|
168
|
+
# Use provided scratchpad storage (for testing) or create volatile one based on mode
|
|
169
|
+
@scratchpad_storage = if scratchpad
|
|
170
|
+
scratchpad # Testing/internal use - explicit instance provided
|
|
171
|
+
elsif @scratchpad_mode == :enabled
|
|
172
|
+
Tools::Stores::ScratchpadStorage.new
|
|
173
|
+
end
|
|
153
174
|
|
|
154
175
|
# Per-agent plugin storages (persistent)
|
|
155
176
|
# Format: { plugin_name => { agent_name => storage } }
|
|
@@ -165,6 +186,7 @@ module SwarmSDK
|
|
|
165
186
|
# Agent definitions and instances
|
|
166
187
|
@agent_definitions = {}
|
|
167
188
|
@agents = {}
|
|
189
|
+
@delegation_instances = {} # { "delegate@delegator" => Agent::Chat }
|
|
168
190
|
@agents_initialized = false
|
|
169
191
|
@agent_contexts = {}
|
|
170
192
|
|
|
@@ -175,6 +197,10 @@ module SwarmSDK
|
|
|
175
197
|
|
|
176
198
|
# Track if first message has been sent
|
|
177
199
|
@first_message_sent = false
|
|
200
|
+
|
|
201
|
+
# Track if agent_start events have been emitted
|
|
202
|
+
# This prevents duplicate emissions and ensures events are emitted when logging is ready
|
|
203
|
+
@agent_start_events_emitted = false
|
|
178
204
|
end
|
|
179
205
|
|
|
180
206
|
# Add an agent to the swarm
|
|
@@ -240,8 +266,25 @@ module SwarmSDK
|
|
|
240
266
|
logs = []
|
|
241
267
|
current_prompt = prompt
|
|
242
268
|
|
|
269
|
+
# Force cleanup of any lingering scheduler from previous requests
|
|
270
|
+
# This ensures we always take the clean Path C in Async()
|
|
271
|
+
# See: Async expert analysis - prevents scheduler leak in Puma
|
|
272
|
+
if Fiber.scheduler && !Async::Task.current?
|
|
273
|
+
Fiber.set_scheduler(nil)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Set fiber-local execution context
|
|
277
|
+
# Use ||= to inherit parent's execution_id if one exists (for mini-swarms)
|
|
278
|
+
# Child fibers (tools, delegations) inherit automatically
|
|
279
|
+
Fiber[:execution_id] ||= generate_execution_id
|
|
280
|
+
Fiber[:swarm_id] = @swarm_id
|
|
281
|
+
Fiber[:parent_swarm_id] = @parent_swarm_id
|
|
282
|
+
|
|
243
283
|
# Setup logging FIRST if block given (so swarm_start event can be emitted)
|
|
244
284
|
if block_given?
|
|
285
|
+
# Force fresh callback array for this execution
|
|
286
|
+
Fiber[:log_callbacks] = []
|
|
287
|
+
|
|
245
288
|
# Register callback to collect logs and forward to user's block
|
|
246
289
|
LogCollector.on_log do |entry|
|
|
247
290
|
logs << entry
|
|
@@ -270,6 +313,17 @@ module SwarmSDK
|
|
|
270
313
|
# Lazy initialization of agents (with optional logging)
|
|
271
314
|
initialize_agents unless @agents_initialized
|
|
272
315
|
|
|
316
|
+
# If agents were initialized BEFORE logging was set up (e.g., via restore()),
|
|
317
|
+
# we need to retroactively set up logging callbacks and emit agent_start events
|
|
318
|
+
if block_given? && @agents_initialized && !@agent_start_events_emitted
|
|
319
|
+
# Setup logging callbacks for all agents (they were skipped during initialization)
|
|
320
|
+
setup_logging_for_all_agents
|
|
321
|
+
|
|
322
|
+
# Emit agent_start events now that logging is ready
|
|
323
|
+
emit_agent_start_events
|
|
324
|
+
@agent_start_events_emitted = true
|
|
325
|
+
end
|
|
326
|
+
|
|
273
327
|
# Execution loop (supports reprompting)
|
|
274
328
|
result = nil
|
|
275
329
|
swarm_stop_triggered = false
|
|
@@ -373,6 +427,14 @@ module SwarmSDK
|
|
|
373
427
|
# Cleanup MCP clients after execution
|
|
374
428
|
cleanup
|
|
375
429
|
|
|
430
|
+
# Only clear Fiber storage if we set up logging (same pattern as LogCollector)
|
|
431
|
+
# Mini-swarms are called without block, so they don't clear
|
|
432
|
+
if block_given?
|
|
433
|
+
Fiber[:execution_id] = nil
|
|
434
|
+
Fiber[:swarm_id] = nil
|
|
435
|
+
Fiber[:parent_swarm_id] = nil
|
|
436
|
+
end
|
|
437
|
+
|
|
376
438
|
# Reset logging state for next execution if we set it up
|
|
377
439
|
#
|
|
378
440
|
# IMPORTANT: Only reset if we set up logging (block_given? == true).
|
|
@@ -433,7 +495,7 @@ module SwarmSDK
|
|
|
433
495
|
# @return [Array<Hash>] Array of warning hashes from all agent definitions
|
|
434
496
|
#
|
|
435
497
|
# @example
|
|
436
|
-
# swarm =
|
|
498
|
+
# swarm = SwarmSDK.load_file("config.yml")
|
|
437
499
|
# warnings = swarm.validate
|
|
438
500
|
# warnings.each do |warning|
|
|
439
501
|
# puts "⚠️ #{warning[:agent]}: #{warning[:model]} not found"
|
|
@@ -465,6 +527,8 @@ module SwarmSDK
|
|
|
465
527
|
LogStream.emit(
|
|
466
528
|
type: "model_lookup_warning",
|
|
467
529
|
agent: warning[:agent],
|
|
530
|
+
swarm_id: @swarm_id,
|
|
531
|
+
parent_swarm_id: @parent_swarm_id,
|
|
468
532
|
model: warning[:model],
|
|
469
533
|
error_message: warning[:error_message],
|
|
470
534
|
suggestions: warning[:suggestions],
|
|
@@ -483,18 +547,25 @@ module SwarmSDK
|
|
|
483
547
|
#
|
|
484
548
|
# @return [void]
|
|
485
549
|
def cleanup
|
|
486
|
-
|
|
550
|
+
# Check if there's anything to clean up
|
|
551
|
+
return if @mcp_clients.empty? && (!@delegation_instances || @delegation_instances.empty?)
|
|
487
552
|
|
|
553
|
+
# Stop MCP clients for all agents (primaries + delegations tracked by instance name)
|
|
488
554
|
@mcp_clients.each do |agent_name, clients|
|
|
489
555
|
clients.each do |client|
|
|
490
|
-
|
|
556
|
+
# Always call stop - this sets @running = false and stops background threads
|
|
557
|
+
client.stop
|
|
491
558
|
RubyLLM.logger.debug("SwarmSDK: Stopped MCP client '#{client.name}' for agent #{agent_name}")
|
|
492
559
|
rescue StandardError => e
|
|
493
|
-
|
|
560
|
+
# Don't fail cleanup if stopping one client fails
|
|
561
|
+
RubyLLM.logger.debug("SwarmSDK: Error stopping MCP client '#{client.name}': #{e.message}")
|
|
494
562
|
end
|
|
495
563
|
end
|
|
496
564
|
|
|
497
565
|
@mcp_clients.clear
|
|
566
|
+
|
|
567
|
+
# Clear delegation instances (V7.0: Added for completeness)
|
|
568
|
+
@delegation_instances&.clear
|
|
498
569
|
end
|
|
499
570
|
|
|
500
571
|
# Register a named hook that can be referenced in agent configurations
|
|
@@ -535,8 +606,155 @@ module SwarmSDK
|
|
|
535
606
|
self
|
|
536
607
|
end
|
|
537
608
|
|
|
609
|
+
# Reset context for all agents
|
|
610
|
+
#
|
|
611
|
+
# Clears conversation history for all agents. This is used by composable swarms
|
|
612
|
+
# to reset sub-swarm context when keep_context: false is specified.
|
|
613
|
+
#
|
|
614
|
+
# @return [void]
|
|
615
|
+
def reset_context!
|
|
616
|
+
@agents.each_value do |agent_chat|
|
|
617
|
+
agent_chat.clear_conversation if agent_chat.respond_to?(:clear_conversation)
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# Create snapshot of current conversation state
|
|
622
|
+
#
|
|
623
|
+
# Returns a Snapshot object containing:
|
|
624
|
+
# - All agent conversations (@messages arrays)
|
|
625
|
+
# - Agent context state (warnings, compression, TodoWrite tracking, skills)
|
|
626
|
+
# - Delegation instance conversations
|
|
627
|
+
# - Scratchpad contents (volatile shared storage)
|
|
628
|
+
# - Read tracking state (which files each agent has read with digests)
|
|
629
|
+
# - Memory read tracking state (which memory entries each agent has read with digests)
|
|
630
|
+
#
|
|
631
|
+
# Configuration (agent definitions, tools, prompts) stays in your YAML/DSL
|
|
632
|
+
# and is NOT included in snapshots.
|
|
633
|
+
#
|
|
634
|
+
# @return [Snapshot] Snapshot object with convenient serialization methods
|
|
635
|
+
#
|
|
636
|
+
# @example Save snapshot to JSON file
|
|
637
|
+
# snapshot = swarm.snapshot
|
|
638
|
+
# snapshot.write_to_file("session.json")
|
|
639
|
+
#
|
|
640
|
+
# @example Convert to hash or JSON string
|
|
641
|
+
# snapshot = swarm.snapshot
|
|
642
|
+
# hash = snapshot.to_hash
|
|
643
|
+
# json_string = snapshot.to_json
|
|
644
|
+
def snapshot
|
|
645
|
+
StateSnapshot.new(self).snapshot
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# Restore conversation state from snapshot
|
|
649
|
+
#
|
|
650
|
+
# Accepts a Snapshot object, hash, or JSON string. Validates compatibility
|
|
651
|
+
# between snapshot and current swarm configuration, restores agent conversations,
|
|
652
|
+
# context state, scratchpad, and read tracking. Returns RestoreResult with
|
|
653
|
+
# warnings about any agents that couldn't be restored due to configuration
|
|
654
|
+
# mismatches.
|
|
655
|
+
#
|
|
656
|
+
# The swarm must be created with the SAME configuration (agent definitions,
|
|
657
|
+
# tools, prompts) as when the snapshot was created. Only conversation state
|
|
658
|
+
# is restored from the snapshot.
|
|
659
|
+
#
|
|
660
|
+
# @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
|
|
661
|
+
# @return [RestoreResult] Result with warnings about skipped agents
|
|
662
|
+
#
|
|
663
|
+
# @example Restore from Snapshot object
|
|
664
|
+
# swarm = SwarmSDK.build { ... } # Same config as snapshot
|
|
665
|
+
# snapshot = Snapshot.from_file("session.json")
|
|
666
|
+
# result = swarm.restore(snapshot)
|
|
667
|
+
# if result.success?
|
|
668
|
+
# puts "All agents restored"
|
|
669
|
+
# else
|
|
670
|
+
# puts result.summary
|
|
671
|
+
# result.warnings.each { |w| puts " - #{w[:message]}" }
|
|
672
|
+
# end
|
|
673
|
+
#
|
|
674
|
+
# Restore swarm state from snapshot
|
|
675
|
+
#
|
|
676
|
+
# By default, uses current system prompts from agent definitions (YAML + SDK defaults + plugin injections).
|
|
677
|
+
# Set preserve_system_prompts: true to use historical prompts from snapshot.
|
|
678
|
+
#
|
|
679
|
+
# @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
|
|
680
|
+
# @param preserve_system_prompts [Boolean] Use historical system prompts instead of current config (default: false)
|
|
681
|
+
# @return [RestoreResult] Result with warnings about partial restores
|
|
682
|
+
def restore(snapshot, preserve_system_prompts: false)
|
|
683
|
+
StateRestorer.new(self, snapshot, preserve_system_prompts: preserve_system_prompts).restore
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# Override swarm IDs for composable swarms
|
|
687
|
+
#
|
|
688
|
+
# Used by SwarmLoader to set hierarchical IDs when loading sub-swarms.
|
|
689
|
+
# This is called after the swarm is built to ensure proper parent/child relationships.
|
|
690
|
+
#
|
|
691
|
+
# @param swarm_id [String] New swarm ID
|
|
692
|
+
# @param parent_swarm_id [String] New parent swarm ID
|
|
693
|
+
# @return [void]
|
|
694
|
+
def override_swarm_ids(swarm_id:, parent_swarm_id:)
|
|
695
|
+
@swarm_id = swarm_id
|
|
696
|
+
@parent_swarm_id = parent_swarm_id
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
# Set swarm registry for composable swarms
|
|
700
|
+
#
|
|
701
|
+
# Used by Builder to set the registry after swarm creation.
|
|
702
|
+
# This must be called before agent initialization to enable swarm delegation.
|
|
703
|
+
#
|
|
704
|
+
# @param registry [SwarmRegistry] Configured swarm registry
|
|
705
|
+
# @return [void]
|
|
706
|
+
attr_writer :swarm_registry
|
|
707
|
+
|
|
538
708
|
private
|
|
539
709
|
|
|
710
|
+
# Validate and normalize scratchpad mode for Swarm
|
|
711
|
+
#
|
|
712
|
+
# Regular Swarms support :enabled or :disabled.
|
|
713
|
+
# Rejects :per_node since it only makes sense for NodeOrchestrator with multiple nodes.
|
|
714
|
+
#
|
|
715
|
+
# @param value [Symbol, String] Scratchpad mode (strings from YAML converted to symbols)
|
|
716
|
+
# @return [Symbol] :enabled or :disabled
|
|
717
|
+
# @raise [ArgumentError] If :per_node used, or invalid value
|
|
718
|
+
def validate_swarm_scratchpad_mode(value)
|
|
719
|
+
# Convert strings from YAML to symbols
|
|
720
|
+
value = value.to_sym if value.is_a?(String)
|
|
721
|
+
|
|
722
|
+
case value
|
|
723
|
+
when :enabled, :disabled
|
|
724
|
+
value
|
|
725
|
+
when :per_node
|
|
726
|
+
raise ArgumentError,
|
|
727
|
+
"scratchpad: :per_node is only valid for NodeOrchestrator with nodes. " \
|
|
728
|
+
"For regular Swarms, use :enabled or :disabled."
|
|
729
|
+
else
|
|
730
|
+
raise ArgumentError,
|
|
731
|
+
"Invalid scratchpad mode for Swarm: #{value.inspect}. " \
|
|
732
|
+
"Use :enabled or :disabled."
|
|
733
|
+
end
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
# Generate a unique swarm ID from name
|
|
737
|
+
#
|
|
738
|
+
# Creates a swarm ID by sanitizing the name and appending a random suffix.
|
|
739
|
+
# Used when swarm_id is not explicitly provided.
|
|
740
|
+
#
|
|
741
|
+
# @param name [String] Swarm name
|
|
742
|
+
# @return [String] Generated swarm ID (e.g., "dev_team_a3f2b1c8")
|
|
743
|
+
def generate_swarm_id(name)
|
|
744
|
+
sanitized = name.to_s.gsub(/[^a-z0-9_-]/i, "_").downcase
|
|
745
|
+
"#{sanitized}_#{SecureRandom.hex(4)}"
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
# Generate a unique execution ID
|
|
749
|
+
#
|
|
750
|
+
# Creates an execution ID that uniquely identifies a single swarm.execute() call.
|
|
751
|
+
# Format: "exec_{swarm_id}_{random_hex}"
|
|
752
|
+
#
|
|
753
|
+
# @return [String] Generated execution ID (e.g., "exec_main_a3f2b1c8")
|
|
754
|
+
def generate_execution_id
|
|
755
|
+
"exec_#{@swarm_id}_#{SecureRandom.hex(8)}"
|
|
756
|
+
end
|
|
757
|
+
|
|
540
758
|
# Initialize all agents using AgentInitializer
|
|
541
759
|
#
|
|
542
760
|
# This is called automatically (lazy initialization) by execute() and agent().
|
|
@@ -560,44 +778,87 @@ module SwarmSDK
|
|
|
560
778
|
@agent_contexts = initializer.agent_contexts
|
|
561
779
|
@agents_initialized = true
|
|
562
780
|
|
|
563
|
-
#
|
|
564
|
-
|
|
781
|
+
# NOTE: agent_start events are emitted in execute() when logging is set up
|
|
782
|
+
# This ensures events are never lost, even if agents are initialized early (e.g., by restore())
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# Setup logging callbacks for all agents
|
|
786
|
+
#
|
|
787
|
+
# Called when agents were initialized before logging was set up (e.g., via restore()).
|
|
788
|
+
# Retroactively registers RubyLLM callbacks (on_tool_call, on_end_message, etc.)
|
|
789
|
+
# so events are properly emitted during execution.
|
|
790
|
+
# Safe to call multiple times - RubyLLM callbacks are replaced, not appended.
|
|
791
|
+
#
|
|
792
|
+
# @return [void]
|
|
793
|
+
def setup_logging_for_all_agents
|
|
794
|
+
# Setup for PRIMARY agents
|
|
795
|
+
@agents.each_value do |chat|
|
|
796
|
+
chat.setup_logging if chat.respond_to?(:setup_logging)
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
# Setup for DELEGATION instances
|
|
800
|
+
@delegation_instances.each_value do |chat|
|
|
801
|
+
chat.setup_logging if chat.respond_to?(:setup_logging)
|
|
802
|
+
end
|
|
565
803
|
end
|
|
566
804
|
|
|
567
805
|
# Emit agent_start events for all initialized agents
|
|
568
806
|
def emit_agent_start_events
|
|
569
|
-
# Only emit if LogStream is enabled
|
|
570
807
|
return unless LogStream.emitter
|
|
571
808
|
|
|
809
|
+
# Emit for PRIMARY agents
|
|
572
810
|
@agents.each do |agent_name, chat|
|
|
573
|
-
|
|
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
|
|
811
|
+
emit_agent_start_for(agent_name, chat, is_delegation: false)
|
|
812
|
+
end
|
|
586
813
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
)
|
|
814
|
+
# Emit for DELEGATION instances
|
|
815
|
+
@delegation_instances.each do |instance_name, chat|
|
|
816
|
+
base_name = extract_base_name(instance_name)
|
|
817
|
+
emit_agent_start_for(instance_name.to_sym, chat, is_delegation: true, base_name: base_name)
|
|
600
818
|
end
|
|
819
|
+
|
|
820
|
+
# Mark as emitted to prevent duplicate emissions
|
|
821
|
+
@agent_start_events_emitted = true
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# Helper for emitting agent_start event
|
|
825
|
+
def emit_agent_start_for(agent_name, chat, is_delegation:, base_name: nil)
|
|
826
|
+
base_name ||= agent_name
|
|
827
|
+
agent_def = @agent_definitions[base_name]
|
|
828
|
+
|
|
829
|
+
# Build plugin storage info using base name
|
|
830
|
+
plugin_storage_info = {}
|
|
831
|
+
@plugin_storages.each do |plugin_name, agent_storages|
|
|
832
|
+
next unless agent_storages.key?(base_name)
|
|
833
|
+
|
|
834
|
+
plugin_storage_info[plugin_name] = {
|
|
835
|
+
enabled: true,
|
|
836
|
+
config: agent_def.respond_to?(plugin_name) ? extract_plugin_config_info(agent_def.public_send(plugin_name)) : nil,
|
|
837
|
+
}
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
LogStream.emit(
|
|
841
|
+
type: "agent_start",
|
|
842
|
+
agent: agent_name,
|
|
843
|
+
swarm_id: @swarm_id,
|
|
844
|
+
parent_swarm_id: @parent_swarm_id,
|
|
845
|
+
swarm_name: @name,
|
|
846
|
+
model: agent_def.model,
|
|
847
|
+
provider: agent_def.provider || "openai",
|
|
848
|
+
directory: agent_def.directory,
|
|
849
|
+
system_prompt: agent_def.system_prompt,
|
|
850
|
+
tools: chat.tools.keys,
|
|
851
|
+
delegates_to: agent_def.delegates_to,
|
|
852
|
+
plugin_storages: plugin_storage_info,
|
|
853
|
+
is_delegation_instance: is_delegation,
|
|
854
|
+
base_agent: (base_name if is_delegation),
|
|
855
|
+
timestamp: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
856
|
+
)
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
# Extract base name from instance name
|
|
860
|
+
def extract_base_name(instance_name)
|
|
861
|
+
instance_name.to_s.split("@").first.to_sym
|
|
601
862
|
end
|
|
602
863
|
|
|
603
864
|
# Normalize tools to internal format (kept for add_agent)
|
|
@@ -690,6 +951,8 @@ module SwarmSDK
|
|
|
690
951
|
LogStream.emit(
|
|
691
952
|
type: "swarm_start",
|
|
692
953
|
agent: context.metadata[:lead_agent], # Include agent for consistency
|
|
954
|
+
swarm_id: @swarm_id,
|
|
955
|
+
parent_swarm_id: @parent_swarm_id,
|
|
693
956
|
swarm_name: context.metadata[:swarm_name],
|
|
694
957
|
lead_agent: context.metadata[:lead_agent],
|
|
695
958
|
prompt: context.metadata[:prompt],
|
|
@@ -704,6 +967,8 @@ module SwarmSDK
|
|
|
704
967
|
|
|
705
968
|
LogStream.emit(
|
|
706
969
|
type: "swarm_stop",
|
|
970
|
+
swarm_id: @swarm_id,
|
|
971
|
+
parent_swarm_id: @parent_swarm_id,
|
|
707
972
|
swarm_name: context.metadata[:swarm_name],
|
|
708
973
|
lead_agent: context.metadata[:lead_agent],
|
|
709
974
|
last_agent: context.metadata[:last_agent], # Agent that produced final response
|
|
@@ -725,6 +990,8 @@ module SwarmSDK
|
|
|
725
990
|
LogStream.emit(
|
|
726
991
|
type: "user_prompt",
|
|
727
992
|
agent: context.agent_name,
|
|
993
|
+
swarm_id: @swarm_id,
|
|
994
|
+
parent_swarm_id: @parent_swarm_id,
|
|
728
995
|
model: context.metadata[:model] || "unknown",
|
|
729
996
|
provider: context.metadata[:provider] || "unknown",
|
|
730
997
|
message_count: context.metadata[:message_count] || 0,
|
|
@@ -745,6 +1012,8 @@ module SwarmSDK
|
|
|
745
1012
|
LogStream.emit(
|
|
746
1013
|
type: "agent_step",
|
|
747
1014
|
agent: context.agent_name,
|
|
1015
|
+
swarm_id: @swarm_id,
|
|
1016
|
+
parent_swarm_id: @parent_swarm_id,
|
|
748
1017
|
model: context.metadata[:model],
|
|
749
1018
|
content: context.metadata[:content],
|
|
750
1019
|
tool_calls: context.metadata[:tool_calls],
|
|
@@ -766,6 +1035,8 @@ module SwarmSDK
|
|
|
766
1035
|
LogStream.emit(
|
|
767
1036
|
type: "agent_stop",
|
|
768
1037
|
agent: context.agent_name,
|
|
1038
|
+
swarm_id: @swarm_id,
|
|
1039
|
+
parent_swarm_id: @parent_swarm_id,
|
|
769
1040
|
model: context.metadata[:model],
|
|
770
1041
|
content: context.metadata[:content],
|
|
771
1042
|
tool_calls: context.metadata[:tool_calls],
|
|
@@ -786,6 +1057,8 @@ module SwarmSDK
|
|
|
786
1057
|
LogStream.emit(
|
|
787
1058
|
type: "tool_call",
|
|
788
1059
|
agent: context.agent_name,
|
|
1060
|
+
swarm_id: @swarm_id,
|
|
1061
|
+
parent_swarm_id: @parent_swarm_id,
|
|
789
1062
|
tool_call_id: context.tool_call.id,
|
|
790
1063
|
tool: context.tool_call.name,
|
|
791
1064
|
arguments: context.tool_call.parameters,
|
|
@@ -803,6 +1076,8 @@ module SwarmSDK
|
|
|
803
1076
|
LogStream.emit(
|
|
804
1077
|
type: "tool_result",
|
|
805
1078
|
agent: context.agent_name,
|
|
1079
|
+
swarm_id: @swarm_id,
|
|
1080
|
+
parent_swarm_id: @parent_swarm_id,
|
|
806
1081
|
tool_call_id: context.tool_result.tool_call_id,
|
|
807
1082
|
tool: context.tool_result.tool_name,
|
|
808
1083
|
result: context.tool_result.content,
|
|
@@ -818,6 +1093,8 @@ module SwarmSDK
|
|
|
818
1093
|
LogStream.emit(
|
|
819
1094
|
type: "context_limit_warning",
|
|
820
1095
|
agent: context.agent_name,
|
|
1096
|
+
swarm_id: @swarm_id,
|
|
1097
|
+
parent_swarm_id: @parent_swarm_id,
|
|
821
1098
|
model: context.metadata[:model] || "unknown",
|
|
822
1099
|
threshold: "#{context.metadata[:threshold]}%",
|
|
823
1100
|
current_usage: "#{context.metadata[:percentage]}%",
|