swarm_memory 2.1.3 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/lib/claude_swarm/claude_mcp_server.rb +1 -0
  3. data/lib/claude_swarm/cli.rb +5 -18
  4. data/lib/claude_swarm/configuration.rb +2 -15
  5. data/lib/claude_swarm/mcp_generator.rb +1 -0
  6. data/lib/claude_swarm/openai/chat_completion.rb +4 -12
  7. data/lib/claude_swarm/openai/executor.rb +3 -1
  8. data/lib/claude_swarm/openai/responses.rb +13 -32
  9. data/lib/claude_swarm/version.rb +1 -1
  10. data/lib/swarm_cli/commands/run.rb +2 -2
  11. data/lib/swarm_cli/config_loader.rb +11 -11
  12. data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
  13. data/lib/swarm_cli/interactive_repl.rb +11 -5
  14. data/lib/swarm_cli/ui/icons.rb +0 -23
  15. data/lib/swarm_cli/version.rb +1 -1
  16. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  17. data/lib/swarm_memory/integration/sdk_plugin.rb +87 -7
  18. data/lib/swarm_memory/version.rb +1 -1
  19. data/lib/swarm_memory.rb +1 -1
  20. data/lib/swarm_sdk/agent/builder.rb +58 -0
  21. data/lib/swarm_sdk/agent/chat.rb +527 -1059
  22. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
  23. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  24. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
  25. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  26. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  27. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  28. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  29. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +12 -12
  30. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  31. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  32. data/lib/swarm_sdk/agent/context.rb +2 -2
  33. data/lib/swarm_sdk/agent/definition.rb +66 -154
  34. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  35. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  36. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  37. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  38. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  39. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  40. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  41. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  42. data/lib/swarm_sdk/configuration.rb +65 -543
  43. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  44. data/lib/swarm_sdk/context_compactor.rb +6 -11
  45. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  46. data/lib/swarm_sdk/context_management/context.rb +328 -0
  47. data/lib/swarm_sdk/defaults.rb +196 -0
  48. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  49. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  50. data/lib/swarm_sdk/log_collector.rb +179 -29
  51. data/lib/swarm_sdk/log_stream.rb +29 -0
  52. data/lib/swarm_sdk/node_context.rb +1 -1
  53. data/lib/swarm_sdk/observer/builder.rb +81 -0
  54. data/lib/swarm_sdk/observer/config.rb +45 -0
  55. data/lib/swarm_sdk/observer/manager.rb +236 -0
  56. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  57. data/lib/swarm_sdk/plugin.rb +93 -3
  58. data/lib/swarm_sdk/snapshot.rb +6 -6
  59. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  60. data/lib/swarm_sdk/state_restorer.rb +136 -151
  61. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  62. data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
  63. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  64. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  65. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  66. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  67. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  68. data/lib/swarm_sdk/swarm/tool_configurator.rb +42 -138
  69. data/lib/swarm_sdk/swarm.rb +137 -679
  70. data/lib/swarm_sdk/tools/bash.rb +11 -3
  71. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  72. data/lib/swarm_sdk/tools/edit.rb +8 -13
  73. data/lib/swarm_sdk/tools/glob.rb +9 -1
  74. data/lib/swarm_sdk/tools/grep.rb +7 -0
  75. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  76. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  77. data/lib/swarm_sdk/tools/read.rb +11 -13
  78. data/lib/swarm_sdk/tools/registry.rb +122 -10
  79. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -5
  80. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  81. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  82. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  83. data/lib/swarm_sdk/tools/write.rb +8 -13
  84. data/lib/swarm_sdk/version.rb +1 -1
  85. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  86. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  87. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  88. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +3 -3
  89. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  90. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  91. data/lib/swarm_sdk.rb +33 -3
  92. metadata +37 -14
  93. data/lib/swarm_memory/chat_extension.rb +0 -34
  94. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- # Restores swarm conversation state from snapshots
4
+ # Restores swarm/workflow conversation state from snapshots
5
5
  #
6
- # Unified implementation that works for both Swarm and NodeOrchestrator.
6
+ # Unified implementation that works for both Swarm and Workflow.
7
7
  # Validates compatibility between snapshot and current configuration,
8
8
  # restores conversation history, context state, scratchpad contents, and
9
9
  # read tracking information.
10
10
  #
11
11
  # Handles configuration mismatches gracefully by skipping agents that
12
- # don't exist in the current swarm and returning warnings in RestoreResult.
12
+ # don't exist in the current swarm/workflow and returning warnings in RestoreResult.
13
13
  #
14
14
  # ## System Prompt Handling
15
15
  #
@@ -32,12 +32,11 @@ module SwarmSDK
32
32
  class StateRestorer
33
33
  # Initialize state restorer
34
34
  #
35
- # @param orchestration [Swarm, NodeOrchestrator] Swarm or orchestrator to restore into
35
+ # @param orchestration [Swarm, Workflow] Swarm or workflow to restore into
36
36
  # @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
37
37
  # @param preserve_system_prompts [Boolean] If true, use system prompts from snapshot instead of current config (default: false)
38
38
  def initialize(orchestration, snapshot, preserve_system_prompts: false)
39
39
  @orchestration = orchestration
40
- @type = orchestration.is_a?(SwarmSDK::NodeOrchestrator) ? :node_orchestrator : :swarm
41
40
  @preserve_system_prompts = preserve_system_prompts
42
41
 
43
42
  # Handle different input types
@@ -74,7 +73,7 @@ module SwarmSDK
74
73
  restore_delegation_conversations(validation.restorable_delegations)
75
74
  restore_scratchpad
76
75
  restore_read_tracking
77
- restore_memory_read_tracking
76
+ restore_plugin_states
78
77
 
79
78
  # Phase 3: Return result with warnings
80
79
  SwarmSDK::RestoreResult.new(
@@ -91,8 +90,8 @@ module SwarmSDK
91
90
  # @raise [StateError] if version is unsupported
92
91
  def validate_version!
93
92
  version = @snapshot_data[:version] || @snapshot_data["version"]
94
- unless version == "1.0.0"
95
- raise StateError, "Unsupported snapshot version: #{version}"
93
+ unless version == "2.1.0"
94
+ raise StateError, "Unsupported snapshot version: #{version}. Expected: 2.1.0"
96
95
  end
97
96
  end
98
97
 
@@ -100,17 +99,16 @@ module SwarmSDK
100
99
  #
101
100
  # @raise [StateError] if types don't match
102
101
  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}'"
102
+ snapshot_type = (@snapshot_data[:type] || @snapshot_data["type"]).to_s.downcase
103
+ actual_type = @orchestration.class.name.split("::").last.downcase
104
+
105
+ unless snapshot_type == actual_type
106
+ raise StateError, "Snapshot type '#{snapshot_type}' doesn't match orchestration type '#{actual_type}'"
106
107
  end
107
108
  end
108
109
 
109
110
  # Validate compatibility between snapshot and current configuration
110
111
  #
111
- # Checks which agents from the snapshot exist in current configuration
112
- # and generates warnings for any that don't match.
113
- #
114
112
  # @return [ValidationResult] Validation results
115
113
  def validate_compatibility
116
114
  warnings = []
@@ -145,9 +143,7 @@ module SwarmSDK
145
143
  delegation_instances&.each do |instance_name, _data|
146
144
  base_name, delegator_name = instance_name.split("@")
147
145
 
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
146
+ # Delegation can be restored if both agents exist in current configuration
151
147
  if current_agents.include?(base_name.to_sym) &&
152
148
  restorable_agents.include?(delegator_name.to_sym)
153
149
  restorable_delegations << instance_name
@@ -157,7 +153,7 @@ module SwarmSDK
157
153
  type: :delegation_instance_not_restorable,
158
154
  instance: instance_name,
159
155
  message: "Delegation instance '#{instance_name}' cannot be restored " \
160
- "(base agent or delegator not in current swarm).",
156
+ "(base agent or delegator not in current swarm/workflow).",
161
157
  }
162
158
  end
163
159
  end
@@ -173,116 +169,107 @@ module SwarmSDK
173
169
 
174
170
  # Restore orchestration metadata
175
171
  #
176
- # For Swarm: restores first_message_sent flag
177
- # For NodeOrchestrator: no additional metadata to restore
178
- #
179
172
  # @return [void]
180
173
  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"]
174
+ # Restore metadata
175
+ metadata = @snapshot_data[:metadata] || @snapshot_data["metadata"]
176
+ return unless metadata
177
+
178
+ # Restore first_message_sent flag (Swarm only, no-op for Workflow)
179
+ if @orchestration.respond_to?(:first_message_sent=)
180
+ first_sent = metadata[:first_message_sent] || metadata["first_message_sent"]
186
181
  @orchestration.first_message_sent = first_sent
187
182
  end
188
- # NodeOrchestrator has no additional metadata to restore
189
183
  end
190
184
 
191
185
  # Restore agent conversations
192
186
  #
187
+ # Uses interface methods - no type checking!
188
+ #
193
189
  # @param restorable_agents [Array<Symbol>] Agents that can be restored
194
190
  # @return [void]
195
191
  def restore_agent_conversations(restorable_agents)
196
192
  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
193
+ # For Swarm: lazy initialization triggers when we call agent()
194
+ # For Workflow: agents are in cache if already used, otherwise skip
195
+ agent_chat = if @orchestration.is_a?(Swarm)
200
196
  @orchestration.agent(agent_name)
201
197
  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]
198
+ # Workflow: only restore if agent is already in cache
199
+ @orchestration.primary_agents[agent_name]
214
200
  end
215
201
 
216
- # Get agent snapshot data - handle both symbol and string keys
202
+ next unless agent_chat
203
+
204
+ # Get agent snapshot data
217
205
  agents_data = @snapshot_data[:agents] || @snapshot_data["agents"]
218
206
  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
207
+ next unless snapshot_data
208
+
209
+ # Restore conversation
210
+ restore_agent_conversation(agent_chat, agent_name, snapshot_data)
211
+ end
212
+ end
236
213
 
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
214
+ # Restore a single agent's conversation
215
+ #
216
+ # @param agent_chat [Agent::Chat] Chat instance
217
+ # @param agent_name [Symbol] Agent name
218
+ # @param snapshot_data [Hash] Snapshot data for this agent
219
+ # @return [void]
220
+ def restore_agent_conversation(agent_chat, agent_name, snapshot_data)
221
+ # Determine which system prompt to use
222
+ system_prompt = if @preserve_system_prompts
223
+ snapshot_data[:system_prompt] || snapshot_data["system_prompt"]
224
+ else
225
+ agent_definition = @orchestration.agent_definitions[agent_name]
226
+ agent_definition&.system_prompt
227
+ end
240
228
 
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
229
+ # Build complete message list including system message
230
+ all_messages = []
247
231
 
248
- # Restore context state
249
- context_state = snapshot_data[:context_state] || snapshot_data["context_state"]
250
- restore_context_state(agent_chat, context_state)
232
+ # Add system message first if we have a system prompt
233
+ if system_prompt
234
+ all_messages << RubyLLM::Message.new(role: :system, content: system_prompt)
251
235
  end
236
+
237
+ # Add conversation messages
238
+ conversation = snapshot_data[:conversation] || snapshot_data["conversation"]
239
+ restored_messages = conversation.map { |msg_data| deserialize_message(msg_data) }
240
+ all_messages.concat(restored_messages)
241
+
242
+ # Replace all messages using proper abstraction
243
+ agent_chat.replace_messages(all_messages)
244
+
245
+ # Restore context state
246
+ context_state = snapshot_data[:context_state] || snapshot_data["context_state"]
247
+ restore_context_state(agent_chat, context_state)
252
248
  end
253
249
 
254
250
  # Deserialize a message from snapshot data
255
251
  #
256
- # Handles Content objects and tool calls properly.
257
- #
258
252
  # @param msg_data [Hash] Message data from snapshot
259
253
  # @return [RubyLLM::Message] Deserialized message
260
254
  def deserialize_message(msg_data)
261
255
  # Handle Content objects
262
256
  content = if msg_data[:content].is_a?(Hash) && (msg_data[:content].key?(:text) || msg_data[:content].key?("text"))
263
257
  content_data = msg_data[:content]
264
- # Handle both symbol and string keys from JSON
265
258
  text = content_data[:text] || content_data["text"]
266
259
  attachments = content_data[:attachments] || content_data["attachments"] || []
267
260
 
268
- # Recreate Content object
269
- # NOTE: Attachments are hashes from JSON - RubyLLM::Content constructor handles this
270
261
  RubyLLM::Content.new(text, attachments)
271
262
  else
272
- # Plain string content
273
263
  msg_data[:content]
274
264
  end
275
265
 
276
- # Handle tool calls - deserialize from hash array
277
- # IMPORTANT: RubyLLM expects tool_calls to be Hash<String, ToolCall>, not Array!
266
+ # Handle tool calls
278
267
  tool_calls_hash = if msg_data[:tool_calls] && !msg_data[:tool_calls].empty?
279
268
  msg_data[:tool_calls].each_with_object({}) do |tc_data, hash|
280
- # Handle both symbol and string keys from JSON
281
269
  id = tc_data[:id] || tc_data["id"]
282
270
  name = tc_data[:name] || tc_data["name"]
283
271
  arguments = tc_data[:arguments] || tc_data["arguments"] || {}
284
272
 
285
- # Use ID as hash key (convert to string for consistency)
286
273
  hash[id.to_s] = RubyLLM::ToolCall.new(
287
274
  id: id,
288
275
  name: name,
@@ -308,115 +295,109 @@ module SwarmSDK
308
295
  # @param context_state [Hash] Context state data
309
296
  # @return [void]
310
297
  def restore_context_state(agent_chat, context_state)
311
- # Access via public accessors
312
298
  context_manager = agent_chat.context_manager
313
299
  agent_context = agent_chat.agent_context
314
300
 
315
- # Restore warning thresholds (Set - add one by one)
301
+ # Restore warning thresholds
316
302
  if context_state[:warning_thresholds_hit] || context_state["warning_thresholds_hit"]
317
303
  thresholds_array = context_state[:warning_thresholds_hit] || context_state["warning_thresholds_hit"]
318
304
  thresholds_set = agent_context.warning_thresholds_hit
319
305
  thresholds_array.each { |t| thresholds_set.add(t) }
320
306
  end
321
307
 
322
- # Restore compression flag using public setter
308
+ # Restore compression flag
323
309
  compression = context_state[:compression_applied] || context_state["compression_applied"]
324
310
  context_manager.compression_applied = compression
325
311
 
326
- # Restore TodoWrite tracking using public setter
312
+ # Restore TodoWrite tracking
327
313
  todowrite_index = context_state[:last_todowrite_message_index] || context_state["last_todowrite_message_index"]
328
314
  agent_chat.last_todowrite_message_index = todowrite_index
329
315
 
330
- # Restore active skill path using public setter
316
+ # Restore active skill path
331
317
  skill_path = context_state[:active_skill_path] || context_state["active_skill_path"]
332
318
  agent_chat.active_skill_path = skill_path
333
319
  end
334
320
 
335
321
  # Restore delegation instance conversations
336
322
  #
323
+ # Uses interface methods - no type checking!
324
+ #
337
325
  # @param restorable_delegations [Array<String>] Delegation instances that can be restored
338
326
  # @return [void]
339
327
  def restore_delegation_conversations(restorable_delegations)
340
328
  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
329
+ # Use interface method - works for both!
330
+ delegation_chat = @orchestration.delegation_instances_hash[instance_name]
353
331
  next unless delegation_chat
354
332
 
355
- # Get delegation snapshot data - handle both symbol and string keys
333
+ # Get delegation snapshot data
356
334
  delegations_data = @snapshot_data[:delegation_instances] || @snapshot_data["delegation_instances"]
357
335
  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
336
+ next unless snapshot_data
363
337
 
364
- # Determine which system prompt to use
365
- # Extract base agent name from delegation instance (e.g., "bob@jarvis" -> "bob")
338
+ # Extract base agent name
366
339
  base_name = instance_name.to_s.split("@").first.to_sym
367
340
 
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
341
+ # Restore conversation
342
+ restore_delegation_conversation(delegation_chat, base_name, snapshot_data)
343
+ end
344
+ end
378
345
 
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
346
+ # Restore a single delegation's conversation
347
+ #
348
+ # @param delegation_chat [Agent::Chat] Chat instance
349
+ # @param base_name [Symbol] Base agent name
350
+ # @param snapshot_data [Hash] Snapshot data
351
+ # @return [void]
352
+ def restore_delegation_conversation(delegation_chat, base_name, snapshot_data)
353
+ # Determine which system prompt to use
354
+ system_prompt = if @preserve_system_prompts
355
+ snapshot_data[:system_prompt] || snapshot_data["system_prompt"]
356
+ else
357
+ agent_definition = @orchestration.agent_definitions[base_name]
358
+ agent_definition&.system_prompt
359
+ end
382
360
 
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
361
+ # Build complete message list including system message
362
+ all_messages = []
389
363
 
390
- # Restore context state
391
- context_state = snapshot_data[:context_state] || snapshot_data["context_state"]
392
- restore_context_state(delegation_chat, context_state)
364
+ # Add system message first if we have a system prompt
365
+ if system_prompt
366
+ all_messages << RubyLLM::Message.new(role: :system, content: system_prompt)
393
367
  end
368
+
369
+ # Restore conversation messages
370
+ conversation = snapshot_data[:conversation] || snapshot_data["conversation"]
371
+ restored_messages = conversation.map { |msg_data| deserialize_message(msg_data) }
372
+ all_messages.concat(restored_messages)
373
+
374
+ # Replace all messages using proper abstraction
375
+ delegation_chat.replace_messages(all_messages)
376
+
377
+ # Restore context state
378
+ context_state = snapshot_data[:context_state] || snapshot_data["context_state"]
379
+ restore_context_state(delegation_chat, context_state)
394
380
  end
395
381
 
396
382
  # Restore scratchpad contents
397
383
  #
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
384
  # @return [void]
404
385
  def restore_scratchpad
405
386
  scratchpad_data = @snapshot_data[:scratchpad] || @snapshot_data["scratchpad"]
406
387
  return unless scratchpad_data&.any?
407
388
 
408
- if @type == :node_orchestrator
409
- restore_node_orchestrator_scratchpad(scratchpad_data)
389
+ if @orchestration.is_a?(Workflow)
390
+ restore_workflow_scratchpad(scratchpad_data)
410
391
  else
411
392
  restore_swarm_scratchpad(scratchpad_data)
412
393
  end
413
394
  end
414
395
 
415
- # Restore scratchpad for NodeOrchestrator
396
+ # Restore scratchpad for Workflow
416
397
  #
417
398
  # @param scratchpad_data [Hash] { shared: bool, data: ... }
418
399
  # @return [void]
419
- def restore_node_orchestrator_scratchpad(scratchpad_data)
400
+ def restore_workflow_scratchpad(scratchpad_data)
420
401
  snapshot_shared_mode = scratchpad_data[:shared] || scratchpad_data["shared"]
421
402
  data = scratchpad_data[:data] || scratchpad_data["data"]
422
403
 
@@ -464,27 +445,31 @@ module SwarmSDK
464
445
  read_tracking_data = @snapshot_data[:read_tracking] || @snapshot_data["read_tracking"]
465
446
  return unless read_tracking_data
466
447
 
467
- # Restore tracking for each agent using new API
468
- # read_tracking_data format: { agent_name => { file_path => digest } }
469
448
  read_tracking_data.each do |agent_name, files_with_digests|
470
449
  agent_sym = agent_name.to_sym
471
450
  Tools::Stores::ReadTracker.restore_read_files(agent_sym, files_with_digests)
472
451
  end
473
452
  end
474
453
 
475
- # Restore memory read tracking state
454
+ # Restore plugin-specific state for all plugins
476
455
  #
477
456
  # @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)
457
+ def restore_plugin_states
458
+ plugin_states_data = @snapshot_data[:plugin_states] || @snapshot_data["plugin_states"]
459
+ return unless plugin_states_data
460
+
461
+ plugin_states_data.each do |plugin_name, agents_state|
462
+ # Find plugin by name
463
+ plugin = PluginRegistry.all.find { |p| p.name.to_s == plugin_name.to_s }
464
+ next unless plugin
465
+
466
+ # Restore state for each agent
467
+ agents_state.each do |agent_name, state|
468
+ agent_sym = agent_name.to_sym
469
+ # Symbolize keys for consistent access
470
+ symbolized_state = state.is_a?(Hash) ? state.transform_keys(&:to_sym) : state
471
+ plugin.restore_agent_state(agent_sym, symbolized_state)
472
+ end
488
473
  end
489
474
  end
490
475
  end