swarm_sdk 2.1.2 → 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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/agent/builder.rb +33 -0
  3. data/lib/swarm_sdk/agent/chat/context_tracker.rb +33 -0
  4. data/lib/swarm_sdk/agent/chat/hook_integration.rb +41 -0
  5. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +11 -27
  6. data/lib/swarm_sdk/agent/chat.rb +198 -51
  7. data/lib/swarm_sdk/agent/context.rb +6 -2
  8. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  9. data/lib/swarm_sdk/agent/definition.rb +15 -22
  10. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
  11. data/lib/swarm_sdk/configuration.rb +420 -103
  12. data/lib/swarm_sdk/events_to_messages.rb +181 -0
  13. data/lib/swarm_sdk/log_collector.rb +31 -5
  14. data/lib/swarm_sdk/log_stream.rb +37 -8
  15. data/lib/swarm_sdk/model_aliases.json +4 -1
  16. data/lib/swarm_sdk/node/agent_config.rb +33 -8
  17. data/lib/swarm_sdk/node/builder.rb +39 -18
  18. data/lib/swarm_sdk/node_orchestrator.rb +293 -26
  19. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  20. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  21. data/lib/swarm_sdk/providers/openai_with_responses.rb +22 -15
  22. data/lib/swarm_sdk/restore_result.rb +65 -0
  23. data/lib/swarm_sdk/snapshot.rb +156 -0
  24. data/lib/swarm_sdk/snapshot_from_events.rb +386 -0
  25. data/lib/swarm_sdk/state_restorer.rb +491 -0
  26. data/lib/swarm_sdk/state_snapshot.rb +369 -0
  27. data/lib/swarm_sdk/swarm/agent_initializer.rb +360 -55
  28. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  29. data/lib/swarm_sdk/swarm/builder.rb +208 -12
  30. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  31. data/lib/swarm_sdk/swarm/tool_configurator.rb +46 -11
  32. data/lib/swarm_sdk/swarm.rb +367 -90
  33. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  34. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  35. data/lib/swarm_sdk/tools/delegate.rb +92 -7
  36. data/lib/swarm_sdk/tools/read.rb +17 -5
  37. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  38. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  39. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  40. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  41. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +45 -0
  42. data/lib/swarm_sdk/tools/stores/storage.rb +4 -4
  43. data/lib/swarm_sdk/tools/think.rb +4 -1
  44. data/lib/swarm_sdk/tools/todo_write.rb +20 -8
  45. data/lib/swarm_sdk/utils.rb +18 -0
  46. data/lib/swarm_sdk/validation_result.rb +33 -0
  47. data/lib/swarm_sdk/version.rb +1 -1
  48. data/lib/swarm_sdk.rb +362 -21
  49. metadata +17 -5
@@ -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