swarm_sdk 2.1.3 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/agent/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 +14 -2
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
- data/lib/swarm_sdk/configuration.rb +387 -94
- 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/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 +337 -42
- 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/stores/read_tracker.rb +47 -12
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +45 -0
- 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 +40 -8
- metadata +17 -6
- data/lib/swarm_sdk/mcp.rb +0 -16
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Restores swarm conversation state from snapshots
|
|
5
|
+
#
|
|
6
|
+
# Unified implementation that works for both Swarm and NodeOrchestrator.
|
|
7
|
+
# Validates compatibility between snapshot and current configuration,
|
|
8
|
+
# restores conversation history, context state, scratchpad contents, and
|
|
9
|
+
# read tracking information.
|
|
10
|
+
#
|
|
11
|
+
# Handles configuration mismatches gracefully by skipping agents that
|
|
12
|
+
# don't exist in the current swarm and returning warnings in RestoreResult.
|
|
13
|
+
#
|
|
14
|
+
# ## System Prompt Handling
|
|
15
|
+
#
|
|
16
|
+
# By default, system prompts are taken from the **current YAML configuration**,
|
|
17
|
+
# not from the snapshot. This makes configuration the source of truth and allows
|
|
18
|
+
# you to update system prompts without creating new sessions.
|
|
19
|
+
#
|
|
20
|
+
# Set `preserve_system_prompts: true` to use historical prompts from the snapshot
|
|
21
|
+
# (useful for debugging, auditing, or exact reproducibility).
|
|
22
|
+
#
|
|
23
|
+
# @example Restore with current system prompts (default)
|
|
24
|
+
# swarm = SwarmSDK.build { ... }
|
|
25
|
+
# snapshot_data = JSON.parse(File.read("session.json"), symbolize_names: true)
|
|
26
|
+
# result = swarm.restore(snapshot_data)
|
|
27
|
+
# # Uses system prompts from current YAML config
|
|
28
|
+
#
|
|
29
|
+
# @example Restore with historical system prompts
|
|
30
|
+
# result = swarm.restore(snapshot_data, preserve_system_prompts: true)
|
|
31
|
+
# # Uses system prompts that were active when snapshot was created
|
|
32
|
+
class StateRestorer
|
|
33
|
+
# Initialize state restorer
|
|
34
|
+
#
|
|
35
|
+
# @param orchestration [Swarm, NodeOrchestrator] Swarm or orchestrator to restore into
|
|
36
|
+
# @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
|
|
37
|
+
# @param preserve_system_prompts [Boolean] If true, use system prompts from snapshot instead of current config (default: false)
|
|
38
|
+
def initialize(orchestration, snapshot, preserve_system_prompts: false)
|
|
39
|
+
@orchestration = orchestration
|
|
40
|
+
@type = orchestration.is_a?(SwarmSDK::NodeOrchestrator) ? :node_orchestrator : :swarm
|
|
41
|
+
@preserve_system_prompts = preserve_system_prompts
|
|
42
|
+
|
|
43
|
+
# Handle different input types
|
|
44
|
+
@snapshot_data = case snapshot
|
|
45
|
+
when Snapshot
|
|
46
|
+
snapshot.to_hash
|
|
47
|
+
when String
|
|
48
|
+
JSON.parse(snapshot, symbolize_names: true)
|
|
49
|
+
when Hash
|
|
50
|
+
snapshot
|
|
51
|
+
else
|
|
52
|
+
raise ArgumentError, "snapshot must be a Snapshot object, Hash, or JSON string"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
validate_version!
|
|
56
|
+
validate_type_match!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Restore state from snapshot
|
|
60
|
+
#
|
|
61
|
+
# Three-phase process:
|
|
62
|
+
# 1. Validate compatibility (which agents can be restored)
|
|
63
|
+
# 2. Restore state (only for matched agents)
|
|
64
|
+
# 3. Return result with warnings about skipped agents
|
|
65
|
+
#
|
|
66
|
+
# @return [RestoreResult] Result with warnings about partial restores
|
|
67
|
+
def restore
|
|
68
|
+
# Phase 1: Validate compatibility
|
|
69
|
+
validation = validate_compatibility
|
|
70
|
+
|
|
71
|
+
# Phase 2: Restore state (only for matched agents)
|
|
72
|
+
restore_metadata
|
|
73
|
+
restore_agent_conversations(validation.restorable_agents)
|
|
74
|
+
restore_delegation_conversations(validation.restorable_delegations)
|
|
75
|
+
restore_scratchpad
|
|
76
|
+
restore_read_tracking
|
|
77
|
+
restore_memory_read_tracking
|
|
78
|
+
|
|
79
|
+
# Phase 3: Return result with warnings
|
|
80
|
+
SwarmSDK::RestoreResult.new(
|
|
81
|
+
warnings: validation.warnings,
|
|
82
|
+
skipped_agents: validation.skipped_agents,
|
|
83
|
+
skipped_delegations: validation.skipped_delegations,
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
# Validate snapshot version
|
|
90
|
+
#
|
|
91
|
+
# @raise [StateError] if version is unsupported
|
|
92
|
+
def validate_version!
|
|
93
|
+
version = @snapshot_data[:version] || @snapshot_data["version"]
|
|
94
|
+
unless version == "1.0.0"
|
|
95
|
+
raise StateError, "Unsupported snapshot version: #{version}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Validate snapshot type matches orchestration type
|
|
100
|
+
#
|
|
101
|
+
# @raise [StateError] if types don't match
|
|
102
|
+
def validate_type_match!
|
|
103
|
+
snapshot_type = (@snapshot_data[:type] || @snapshot_data["type"]).to_sym
|
|
104
|
+
unless snapshot_type == @type
|
|
105
|
+
raise StateError, "Snapshot type '#{snapshot_type}' doesn't match orchestration type '#{@type}'"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Validate compatibility between snapshot and current configuration
|
|
110
|
+
#
|
|
111
|
+
# Checks which agents from the snapshot exist in current configuration
|
|
112
|
+
# and generates warnings for any that don't match.
|
|
113
|
+
#
|
|
114
|
+
# @return [ValidationResult] Validation results
|
|
115
|
+
def validate_compatibility
|
|
116
|
+
warnings = []
|
|
117
|
+
skipped_agents = []
|
|
118
|
+
restorable_agents = []
|
|
119
|
+
skipped_delegations = []
|
|
120
|
+
restorable_delegations = []
|
|
121
|
+
|
|
122
|
+
# Get current agent names from configuration
|
|
123
|
+
current_agents = Set.new(@orchestration.agent_definitions.keys)
|
|
124
|
+
|
|
125
|
+
# Check each snapshot agent
|
|
126
|
+
snapshot_agents = @snapshot_data[:agents] || @snapshot_data["agents"]
|
|
127
|
+
snapshot_agents.keys.each do |agent_name|
|
|
128
|
+
agent_name_sym = agent_name.to_sym
|
|
129
|
+
|
|
130
|
+
if current_agents.include?(agent_name_sym)
|
|
131
|
+
restorable_agents << agent_name_sym
|
|
132
|
+
else
|
|
133
|
+
skipped_agents << agent_name_sym
|
|
134
|
+
warnings << {
|
|
135
|
+
type: :agent_not_found,
|
|
136
|
+
agent: agent_name,
|
|
137
|
+
message: "Agent '#{agent_name}' in snapshot not found in current configuration. " \
|
|
138
|
+
"Conversation will not be restored.",
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Check delegation instances
|
|
144
|
+
delegation_instances = @snapshot_data[:delegation_instances] || @snapshot_data["delegation_instances"]
|
|
145
|
+
delegation_instances&.each do |instance_name, _data|
|
|
146
|
+
base_name, delegator_name = instance_name.split("@")
|
|
147
|
+
|
|
148
|
+
# Delegation can be restored if:
|
|
149
|
+
# 1. The base agent exists in current configuration (may not be in snapshot as primary agent)
|
|
150
|
+
# 2. The delegator was a restorable primary agent from the snapshot
|
|
151
|
+
if current_agents.include?(base_name.to_sym) &&
|
|
152
|
+
restorable_agents.include?(delegator_name.to_sym)
|
|
153
|
+
restorable_delegations << instance_name
|
|
154
|
+
else
|
|
155
|
+
skipped_delegations << instance_name
|
|
156
|
+
warnings << {
|
|
157
|
+
type: :delegation_instance_not_restorable,
|
|
158
|
+
instance: instance_name,
|
|
159
|
+
message: "Delegation instance '#{instance_name}' cannot be restored " \
|
|
160
|
+
"(base agent or delegator not in current swarm).",
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
SwarmSDK::ValidationResult.new(
|
|
166
|
+
warnings: warnings,
|
|
167
|
+
skipped_agents: skipped_agents,
|
|
168
|
+
restorable_agents: restorable_agents,
|
|
169
|
+
skipped_delegations: skipped_delegations,
|
|
170
|
+
restorable_delegations: restorable_delegations,
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Restore orchestration metadata
|
|
175
|
+
#
|
|
176
|
+
# For Swarm: restores first_message_sent flag
|
|
177
|
+
# For NodeOrchestrator: no additional metadata to restore
|
|
178
|
+
#
|
|
179
|
+
# @return [void]
|
|
180
|
+
def restore_metadata
|
|
181
|
+
# Restore type-specific metadata
|
|
182
|
+
if @type == :swarm
|
|
183
|
+
# Restore first_message_sent flag for Swarm only
|
|
184
|
+
swarm_data = @snapshot_data[:swarm] || @snapshot_data["swarm"]
|
|
185
|
+
first_sent = swarm_data[:first_message_sent] || swarm_data["first_message_sent"]
|
|
186
|
+
@orchestration.first_message_sent = first_sent
|
|
187
|
+
end
|
|
188
|
+
# NodeOrchestrator has no additional metadata to restore
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Restore agent conversations
|
|
192
|
+
#
|
|
193
|
+
# @param restorable_agents [Array<Symbol>] Agents that can be restored
|
|
194
|
+
# @return [void]
|
|
195
|
+
def restore_agent_conversations(restorable_agents)
|
|
196
|
+
restorable_agents.each do |agent_name|
|
|
197
|
+
# Get agent chat from appropriate source
|
|
198
|
+
agent_chat = if @type == :swarm
|
|
199
|
+
# Swarm: agents are lazily initialized, access triggers init
|
|
200
|
+
@orchestration.agent(agent_name)
|
|
201
|
+
else
|
|
202
|
+
# NodeOrchestrator: agents are cached lazily during node execution
|
|
203
|
+
# If restoring before first execution, cache will be empty
|
|
204
|
+
# We need to create agents now so they can be injected later
|
|
205
|
+
cache = @orchestration.agent_instance_cache[:primary]
|
|
206
|
+
unless cache[agent_name]
|
|
207
|
+
# For NodeOrchestrator, we can't easily create agents here
|
|
208
|
+
# because we'd need the full swarm setup (initializer, etc.)
|
|
209
|
+
# Skip this agent if it's not in cache yet
|
|
210
|
+
next
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
cache[agent_name]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Get agent snapshot data - handle both symbol and string keys
|
|
217
|
+
agents_data = @snapshot_data[:agents] || @snapshot_data["agents"]
|
|
218
|
+
snapshot_data = agents_data[agent_name] || agents_data[agent_name.to_s]
|
|
219
|
+
next unless snapshot_data # Skip if agent not in snapshot (shouldn't happen due to validation)
|
|
220
|
+
|
|
221
|
+
# Clear existing messages FIRST (before adding system prompt)
|
|
222
|
+
messages = agent_chat.messages
|
|
223
|
+
messages.clear
|
|
224
|
+
|
|
225
|
+
# Determine which system prompt to use
|
|
226
|
+
# By default, use current prompt from YAML config (allows prompt iteration)
|
|
227
|
+
# With preserve_system_prompts: true, use historical prompt from snapshot
|
|
228
|
+
system_prompt = if @preserve_system_prompts
|
|
229
|
+
# Historical: Use prompt that was active when snapshot was created
|
|
230
|
+
snapshot_data[:system_prompt] || snapshot_data["system_prompt"]
|
|
231
|
+
else
|
|
232
|
+
# Current: Use prompt from current agent definition (default)
|
|
233
|
+
agent_definition = @orchestration.agent_definitions[agent_name]
|
|
234
|
+
agent_definition&.system_prompt
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Apply system prompt as system message
|
|
238
|
+
# NOTE: with_instructions adds a system message, so call AFTER clearing
|
|
239
|
+
agent_chat.with_instructions(system_prompt) if system_prompt
|
|
240
|
+
|
|
241
|
+
# Restore conversation messages (after system prompt)
|
|
242
|
+
conversation = snapshot_data[:conversation] || snapshot_data["conversation"]
|
|
243
|
+
conversation.each do |msg_data|
|
|
244
|
+
message = deserialize_message(msg_data)
|
|
245
|
+
messages << message
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Restore context state
|
|
249
|
+
context_state = snapshot_data[:context_state] || snapshot_data["context_state"]
|
|
250
|
+
restore_context_state(agent_chat, context_state)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Deserialize a message from snapshot data
|
|
255
|
+
#
|
|
256
|
+
# Handles Content objects and tool calls properly.
|
|
257
|
+
#
|
|
258
|
+
# @param msg_data [Hash] Message data from snapshot
|
|
259
|
+
# @return [RubyLLM::Message] Deserialized message
|
|
260
|
+
def deserialize_message(msg_data)
|
|
261
|
+
# Handle Content objects
|
|
262
|
+
content = if msg_data[:content].is_a?(Hash) && (msg_data[:content].key?(:text) || msg_data[:content].key?("text"))
|
|
263
|
+
content_data = msg_data[:content]
|
|
264
|
+
# Handle both symbol and string keys from JSON
|
|
265
|
+
text = content_data[:text] || content_data["text"]
|
|
266
|
+
attachments = content_data[:attachments] || content_data["attachments"] || []
|
|
267
|
+
|
|
268
|
+
# Recreate Content object
|
|
269
|
+
# NOTE: Attachments are hashes from JSON - RubyLLM::Content constructor handles this
|
|
270
|
+
RubyLLM::Content.new(text, attachments)
|
|
271
|
+
else
|
|
272
|
+
# Plain string content
|
|
273
|
+
msg_data[:content]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Handle tool calls - deserialize from hash array
|
|
277
|
+
# IMPORTANT: RubyLLM expects tool_calls to be Hash<String, ToolCall>, not Array!
|
|
278
|
+
tool_calls_hash = if msg_data[:tool_calls] && !msg_data[:tool_calls].empty?
|
|
279
|
+
msg_data[:tool_calls].each_with_object({}) do |tc_data, hash|
|
|
280
|
+
# Handle both symbol and string keys from JSON
|
|
281
|
+
id = tc_data[:id] || tc_data["id"]
|
|
282
|
+
name = tc_data[:name] || tc_data["name"]
|
|
283
|
+
arguments = tc_data[:arguments] || tc_data["arguments"] || {}
|
|
284
|
+
|
|
285
|
+
# Use ID as hash key (convert to string for consistency)
|
|
286
|
+
hash[id.to_s] = RubyLLM::ToolCall.new(
|
|
287
|
+
id: id,
|
|
288
|
+
name: name,
|
|
289
|
+
arguments: arguments,
|
|
290
|
+
)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
RubyLLM::Message.new(
|
|
295
|
+
role: (msg_data[:role] || msg_data["role"]).to_sym,
|
|
296
|
+
content: content,
|
|
297
|
+
tool_calls: tool_calls_hash,
|
|
298
|
+
tool_call_id: msg_data[:tool_call_id] || msg_data["tool_call_id"],
|
|
299
|
+
input_tokens: msg_data[:input_tokens] || msg_data["input_tokens"],
|
|
300
|
+
output_tokens: msg_data[:output_tokens] || msg_data["output_tokens"],
|
|
301
|
+
model_id: msg_data[:model_id] || msg_data["model_id"],
|
|
302
|
+
)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Restore context state for an agent
|
|
306
|
+
#
|
|
307
|
+
# @param agent_chat [Agent::Chat] Agent chat instance
|
|
308
|
+
# @param context_state [Hash] Context state data
|
|
309
|
+
# @return [void]
|
|
310
|
+
def restore_context_state(agent_chat, context_state)
|
|
311
|
+
# Access via public accessors
|
|
312
|
+
context_manager = agent_chat.context_manager
|
|
313
|
+
agent_context = agent_chat.agent_context
|
|
314
|
+
|
|
315
|
+
# Restore warning thresholds (Set - add one by one)
|
|
316
|
+
if context_state[:warning_thresholds_hit] || context_state["warning_thresholds_hit"]
|
|
317
|
+
thresholds_array = context_state[:warning_thresholds_hit] || context_state["warning_thresholds_hit"]
|
|
318
|
+
thresholds_set = agent_context.warning_thresholds_hit
|
|
319
|
+
thresholds_array.each { |t| thresholds_set.add(t) }
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Restore compression flag using public setter
|
|
323
|
+
compression = context_state[:compression_applied] || context_state["compression_applied"]
|
|
324
|
+
context_manager.compression_applied = compression
|
|
325
|
+
|
|
326
|
+
# Restore TodoWrite tracking using public setter
|
|
327
|
+
todowrite_index = context_state[:last_todowrite_message_index] || context_state["last_todowrite_message_index"]
|
|
328
|
+
agent_chat.last_todowrite_message_index = todowrite_index
|
|
329
|
+
|
|
330
|
+
# Restore active skill path using public setter
|
|
331
|
+
skill_path = context_state[:active_skill_path] || context_state["active_skill_path"]
|
|
332
|
+
agent_chat.active_skill_path = skill_path
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Restore delegation instance conversations
|
|
336
|
+
#
|
|
337
|
+
# @param restorable_delegations [Array<String>] Delegation instances that can be restored
|
|
338
|
+
# @return [void]
|
|
339
|
+
def restore_delegation_conversations(restorable_delegations)
|
|
340
|
+
restorable_delegations.each do |instance_name|
|
|
341
|
+
# Get delegation chat from appropriate source
|
|
342
|
+
delegation_chat = if @type == :swarm
|
|
343
|
+
@orchestration.delegation_instances[instance_name]
|
|
344
|
+
else
|
|
345
|
+
cache = @orchestration.agent_instance_cache[:delegations]
|
|
346
|
+
unless cache[instance_name]
|
|
347
|
+
# Skip if delegation not in cache yet (NodeOrchestrator)
|
|
348
|
+
next
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
cache[instance_name]
|
|
352
|
+
end
|
|
353
|
+
next unless delegation_chat
|
|
354
|
+
|
|
355
|
+
# Get delegation snapshot data - handle both symbol and string keys
|
|
356
|
+
delegations_data = @snapshot_data[:delegation_instances] || @snapshot_data["delegation_instances"]
|
|
357
|
+
snapshot_data = delegations_data[instance_name.to_sym] || delegations_data[instance_name.to_s] || delegations_data[instance_name]
|
|
358
|
+
next unless snapshot_data # Skip if delegation not in snapshot (shouldn't happen due to validation)
|
|
359
|
+
|
|
360
|
+
# Clear existing messages FIRST (before adding system prompt)
|
|
361
|
+
messages = delegation_chat.messages
|
|
362
|
+
messages.clear
|
|
363
|
+
|
|
364
|
+
# Determine which system prompt to use
|
|
365
|
+
# Extract base agent name from delegation instance (e.g., "bob@jarvis" -> "bob")
|
|
366
|
+
base_name = instance_name.to_s.split("@").first.to_sym
|
|
367
|
+
|
|
368
|
+
# By default, use current prompt from YAML config (allows prompt iteration)
|
|
369
|
+
# With preserve_system_prompts: true, use historical prompt from snapshot
|
|
370
|
+
system_prompt = if @preserve_system_prompts
|
|
371
|
+
# Historical: Use prompt that was active when snapshot was created
|
|
372
|
+
snapshot_data[:system_prompt] || snapshot_data["system_prompt"]
|
|
373
|
+
else
|
|
374
|
+
# Current: Use prompt from current base agent definition (default)
|
|
375
|
+
agent_definition = @orchestration.agent_definitions[base_name]
|
|
376
|
+
agent_definition&.system_prompt
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Apply system prompt as system message
|
|
380
|
+
# NOTE: with_instructions adds a system message, so call AFTER clearing
|
|
381
|
+
delegation_chat.with_instructions(system_prompt) if system_prompt
|
|
382
|
+
|
|
383
|
+
# Restore conversation messages (after system prompt)
|
|
384
|
+
conversation = snapshot_data[:conversation] || snapshot_data["conversation"]
|
|
385
|
+
conversation.each do |msg_data|
|
|
386
|
+
message = deserialize_message(msg_data)
|
|
387
|
+
messages << message
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Restore context state
|
|
391
|
+
context_state = snapshot_data[:context_state] || snapshot_data["context_state"]
|
|
392
|
+
restore_context_state(delegation_chat, context_state)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Restore scratchpad contents
|
|
397
|
+
#
|
|
398
|
+
# For Swarm: uses scratchpad_storage (flat format)
|
|
399
|
+
# For NodeOrchestrator: { shared: bool, data: ... }
|
|
400
|
+
# - shared: true → :enabled mode (shared across nodes)
|
|
401
|
+
# - shared: false → :per_node mode (isolated per node)
|
|
402
|
+
#
|
|
403
|
+
# @return [void]
|
|
404
|
+
def restore_scratchpad
|
|
405
|
+
scratchpad_data = @snapshot_data[:scratchpad] || @snapshot_data["scratchpad"]
|
|
406
|
+
return unless scratchpad_data&.any?
|
|
407
|
+
|
|
408
|
+
if @type == :node_orchestrator
|
|
409
|
+
restore_node_orchestrator_scratchpad(scratchpad_data)
|
|
410
|
+
else
|
|
411
|
+
restore_swarm_scratchpad(scratchpad_data)
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Restore scratchpad for NodeOrchestrator
|
|
416
|
+
#
|
|
417
|
+
# @param scratchpad_data [Hash] { shared: bool, data: ... }
|
|
418
|
+
# @return [void]
|
|
419
|
+
def restore_node_orchestrator_scratchpad(scratchpad_data)
|
|
420
|
+
snapshot_shared_mode = scratchpad_data[:shared] || scratchpad_data["shared"]
|
|
421
|
+
data = scratchpad_data[:data] || scratchpad_data["data"]
|
|
422
|
+
|
|
423
|
+
return unless data&.any?
|
|
424
|
+
|
|
425
|
+
# Warn if snapshot mode doesn't match current configuration
|
|
426
|
+
if snapshot_shared_mode != @orchestration.shared_scratchpad?
|
|
427
|
+
RubyLLM.logger.warn(
|
|
428
|
+
"SwarmSDK: Scratchpad mode mismatch: snapshot=#{snapshot_shared_mode ? "enabled" : "per_node"}, " \
|
|
429
|
+
"current=#{@orchestration.shared_scratchpad? ? "enabled" : "per_node"}",
|
|
430
|
+
)
|
|
431
|
+
RubyLLM.logger.warn("SwarmSDK: Restoring anyway - data may not behave as expected")
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
if snapshot_shared_mode
|
|
435
|
+
# Restore shared scratchpad
|
|
436
|
+
shared_scratchpad = @orchestration.scratchpad_for(@orchestration.start_node)
|
|
437
|
+
shared_scratchpad&.restore_entries(data)
|
|
438
|
+
else
|
|
439
|
+
# Restore per-node scratchpads
|
|
440
|
+
data.each do |node_name, entries|
|
|
441
|
+
next unless entries&.any?
|
|
442
|
+
|
|
443
|
+
scratchpad = @orchestration.scratchpad_for(node_name.to_sym)
|
|
444
|
+
scratchpad&.restore_entries(entries)
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# Restore scratchpad for Swarm
|
|
450
|
+
#
|
|
451
|
+
# @param scratchpad_data [Hash] Flat scratchpad entries
|
|
452
|
+
# @return [void]
|
|
453
|
+
def restore_swarm_scratchpad(scratchpad_data)
|
|
454
|
+
scratchpad = @orchestration.scratchpad_storage
|
|
455
|
+
return unless scratchpad
|
|
456
|
+
|
|
457
|
+
scratchpad.restore_entries(scratchpad_data)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Restore read tracking state
|
|
461
|
+
#
|
|
462
|
+
# @return [void]
|
|
463
|
+
def restore_read_tracking
|
|
464
|
+
read_tracking_data = @snapshot_data[:read_tracking] || @snapshot_data["read_tracking"]
|
|
465
|
+
return unless read_tracking_data
|
|
466
|
+
|
|
467
|
+
# Restore tracking for each agent using new API
|
|
468
|
+
# read_tracking_data format: { agent_name => { file_path => digest } }
|
|
469
|
+
read_tracking_data.each do |agent_name, files_with_digests|
|
|
470
|
+
agent_sym = agent_name.to_sym
|
|
471
|
+
Tools::Stores::ReadTracker.restore_read_files(agent_sym, files_with_digests)
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Restore memory read tracking state
|
|
476
|
+
#
|
|
477
|
+
# @return [void]
|
|
478
|
+
def restore_memory_read_tracking
|
|
479
|
+
memory_tracking_data = @snapshot_data[:memory_read_tracking] || @snapshot_data["memory_read_tracking"]
|
|
480
|
+
return unless memory_tracking_data
|
|
481
|
+
return unless defined?(SwarmMemory::Core::StorageReadTracker)
|
|
482
|
+
|
|
483
|
+
# Restore tracking for each agent using new API
|
|
484
|
+
# memory_tracking_data format: { agent_name => { entry_path => digest } }
|
|
485
|
+
memory_tracking_data.each do |agent_name, entries_with_digests|
|
|
486
|
+
agent_sym = agent_name.to_sym
|
|
487
|
+
SwarmMemory::Core::StorageReadTracker.restore_read_entries(agent_sym, entries_with_digests)
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|