swarm_memory 2.1.4 → 2.1.6

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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. data/lib/swarm_memory.rb +7 -2
  4. metadata +6 -185
  5. data/lib/claude_swarm/base_executor.rb +0 -133
  6. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  7. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  8. data/lib/claude_swarm/cli.rb +0 -697
  9. data/lib/claude_swarm/commands/ps.rb +0 -215
  10. data/lib/claude_swarm/commands/show.rb +0 -139
  11. data/lib/claude_swarm/configuration.rb +0 -373
  12. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  13. data/lib/claude_swarm/json_handler.rb +0 -91
  14. data/lib/claude_swarm/mcp_generator.rb +0 -243
  15. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  16. data/lib/claude_swarm/openai/executor.rb +0 -256
  17. data/lib/claude_swarm/openai/responses.rb +0 -319
  18. data/lib/claude_swarm/orchestrator.rb +0 -878
  19. data/lib/claude_swarm/process_tracker.rb +0 -78
  20. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  21. data/lib/claude_swarm/session_path.rb +0 -42
  22. data/lib/claude_swarm/settings_generator.rb +0 -77
  23. data/lib/claude_swarm/system_utils.rb +0 -46
  24. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  25. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  27. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  28. data/lib/claude_swarm/version.rb +0 -5
  29. data/lib/claude_swarm/worktree_manager.rb +0 -475
  30. data/lib/claude_swarm/yaml_loader.rb +0 -22
  31. data/lib/claude_swarm.rb +0 -67
  32. data/lib/swarm_cli/cli.rb +0 -201
  33. data/lib/swarm_cli/command_registry.rb +0 -61
  34. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  35. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  36. data/lib/swarm_cli/commands/migrate.rb +0 -55
  37. data/lib/swarm_cli/commands/run.rb +0 -173
  38. data/lib/swarm_cli/config_loader.rb +0 -98
  39. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  40. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  41. data/lib/swarm_cli/interactive_repl.rb +0 -924
  42. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  43. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  44. data/lib/swarm_cli/migrate_options.rb +0 -54
  45. data/lib/swarm_cli/migrator.rb +0 -132
  46. data/lib/swarm_cli/options.rb +0 -151
  47. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  48. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  49. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  50. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  51. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  52. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  53. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  54. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  55. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  56. data/lib/swarm_cli/ui/icons.rb +0 -36
  57. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  58. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  59. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  60. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  61. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  62. data/lib/swarm_cli/version.rb +0 -5
  63. data/lib/swarm_cli.rb +0 -46
  64. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  65. data/lib/swarm_sdk/agent/builder.rb +0 -552
  66. data/lib/swarm_sdk/agent/chat.rb +0 -774
  67. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  68. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  69. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  70. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  71. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  72. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  73. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  75. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  76. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  77. data/lib/swarm_sdk/agent/context.rb +0 -116
  78. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  79. data/lib/swarm_sdk/agent/definition.rb +0 -477
  80. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  81. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  82. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  83. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  84. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  85. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  86. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  87. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  88. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  89. data/lib/swarm_sdk/configuration.rb +0 -135
  90. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  91. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  92. data/lib/swarm_sdk/context_compactor.rb +0 -335
  93. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  94. data/lib/swarm_sdk/context_management/context.rb +0 -328
  95. data/lib/swarm_sdk/defaults.rb +0 -196
  96. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  97. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  98. data/lib/swarm_sdk/hooks/context.rb +0 -197
  99. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  100. data/lib/swarm_sdk/hooks/error.rb +0 -29
  101. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  102. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  103. data/lib/swarm_sdk/hooks/result.rb +0 -150
  104. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  105. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  106. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  107. data/lib/swarm_sdk/log_collector.rb +0 -227
  108. data/lib/swarm_sdk/log_stream.rb +0 -127
  109. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  110. data/lib/swarm_sdk/model_aliases.json +0 -8
  111. data/lib/swarm_sdk/models.json +0 -1
  112. data/lib/swarm_sdk/models.rb +0 -120
  113. data/lib/swarm_sdk/node_context.rb +0 -245
  114. data/lib/swarm_sdk/observer/builder.rb +0 -81
  115. data/lib/swarm_sdk/observer/config.rb +0 -45
  116. data/lib/swarm_sdk/observer/manager.rb +0 -236
  117. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  118. data/lib/swarm_sdk/permissions/config.rb +0 -239
  119. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  120. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  121. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  122. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  123. data/lib/swarm_sdk/plugin.rb +0 -309
  124. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  125. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  126. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  127. data/lib/swarm_sdk/restore_result.rb +0 -65
  128. data/lib/swarm_sdk/result.rb +0 -123
  129. data/lib/swarm_sdk/snapshot.rb +0 -156
  130. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  131. data/lib/swarm_sdk/state_restorer.rb +0 -476
  132. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  133. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  134. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  135. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  136. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  137. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  138. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  139. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  140. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  141. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  142. data/lib/swarm_sdk/swarm.rb +0 -717
  143. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  144. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  145. data/lib/swarm_sdk/tools/bash.rb +0 -282
  146. data/lib/swarm_sdk/tools/clock.rb +0 -44
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  148. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  149. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  150. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  151. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  152. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  153. data/lib/swarm_sdk/tools/edit.rb +0 -145
  154. data/lib/swarm_sdk/tools/glob.rb +0 -166
  155. data/lib/swarm_sdk/tools/grep.rb +0 -235
  156. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  157. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  160. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  161. data/lib/swarm_sdk/tools/read.rb +0 -261
  162. data/lib/swarm_sdk/tools/registry.rb +0 -205
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  166. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  167. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
  168. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  169. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  170. data/lib/swarm_sdk/tools/think.rb +0 -98
  171. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  172. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  173. data/lib/swarm_sdk/tools/write.rb +0 -112
  174. data/lib/swarm_sdk/utils.rb +0 -68
  175. data/lib/swarm_sdk/validation_result.rb +0 -33
  176. data/lib/swarm_sdk/version.rb +0 -5
  177. data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  182. data/lib/swarm_sdk/workflow.rb +0 -554
  183. data/lib/swarm_sdk.rb +0 -524
  184. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -1,123 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- class Result
5
- attr_reader :content, :agent, :duration, :logs, :error, :metadata
6
-
7
- def initialize(content: nil, agent:, cost: nil, tokens: nil, duration: 0.0, logs: [], error: nil, metadata: {})
8
- @content = content
9
- @agent = agent
10
- @duration = duration
11
- @logs = logs
12
- @error = error
13
- @metadata = metadata
14
- # Legacy parameters kept for backward compatibility but not stored
15
- # Use total_cost and tokens methods instead which calculate from logs
16
- end
17
-
18
- def success?
19
- @error.nil?
20
- end
21
-
22
- def failure?
23
- !success?
24
- end
25
-
26
- # Calculate total cost from logs
27
- #
28
- # Delegates to total_cost for consistency. This attribute is calculated
29
- # dynamically rather than stored.
30
- #
31
- # @return [Float] Total cost in dollars
32
- def cost
33
- total_cost
34
- end
35
-
36
- # Get token breakdown from logs
37
- #
38
- # Returns input and output tokens from the last log entry with usage data.
39
- # This attribute is calculated dynamically rather than stored.
40
- #
41
- # @return [Hash] Token breakdown with :input and :output keys, or empty hash if no usage data
42
- def tokens
43
- last_entry = @logs.reverse.find { |entry| entry.dig(:usage, :cumulative_input_tokens) }
44
- return {} unless last_entry
45
-
46
- {
47
- input: last_entry.dig(:usage, :cumulative_input_tokens) || 0,
48
- output: last_entry.dig(:usage, :cumulative_output_tokens) || 0,
49
- }
50
- end
51
-
52
- def to_h
53
- {
54
- content: @content,
55
- agent: @agent,
56
- cost: cost,
57
- tokens: tokens,
58
- duration: @duration,
59
- success: success?,
60
- error: @error&.message,
61
- metadata: @metadata,
62
- }.compact
63
- end
64
-
65
- def to_json(*args)
66
- to_h.to_json(*args)
67
- end
68
-
69
- # Calculate total cost across all LLM responses
70
- #
71
- # Cost accumulation works as follows:
72
- # - Input cost: The LAST response's input_cost already includes the cost for the
73
- # full conversation history (all previous messages + current context)
74
- # - Output cost: Each response generates NEW tokens, so we SUM all output_costs
75
- # - Total = Last input_cost + Sum of all output_costs
76
- #
77
- # IMPORTANT: Do NOT sum total_cost across all entries - that would count
78
- # input costs multiple times since each call includes the full history!
79
- def total_cost
80
- entries_with_usage = @logs.select { |entry| entry.dig(:usage, :total_cost) }
81
- return 0.0 if entries_with_usage.empty?
82
-
83
- # Last entry's input cost (includes full conversation history)
84
- last_input_cost = entries_with_usage.last.dig(:usage, :input_cost) || 0.0
85
-
86
- # Sum all output costs (each response generates new tokens)
87
- total_output_cost = entries_with_usage.sum { |entry| entry.dig(:usage, :output_cost) || 0.0 }
88
-
89
- last_input_cost + total_output_cost
90
- end
91
-
92
- # Get total tokens from the last LLM response with cumulative tracking
93
- #
94
- # Token accumulation works as follows:
95
- # - Input tokens: Each API call sends the full conversation history, so the latest
96
- # response's cumulative_input_tokens already represents the full context
97
- # - Output tokens: Each response generates new tokens, cumulative_output_tokens sums them
98
- # - The cumulative_total_tokens in the last response already does this correctly
99
- #
100
- # IMPORTANT: Do NOT sum total_tokens across all log entries - that would count
101
- # input tokens multiple times since each call includes the full history!
102
- def total_tokens
103
- last_entry = @logs.reverse.find { |entry| entry.dig(:usage, :cumulative_total_tokens) }
104
- last_entry&.dig(:usage, :cumulative_total_tokens) || 0
105
- end
106
-
107
- # Get list of all agents involved in execution
108
- def agents_involved
109
- @logs.map { |entry| entry[:agent] }.compact.uniq.map(&:to_sym)
110
- end
111
-
112
- # Count total LLM requests made
113
- # Each LLM API call produces either agent_step (tool calls) or agent_stop (final answer)
114
- def llm_requests
115
- @logs.count { |entry| entry[:type] == "agent_step" || entry[:type] == "agent_stop" }
116
- end
117
-
118
- # Count total tool calls made
119
- def tool_calls_count
120
- @logs.count { |entry| entry[:type] == "tool_call" }
121
- end
122
- end
123
- end
@@ -1,156 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- # Snapshot of swarm conversation state
5
- #
6
- # Encapsulates snapshot data with methods for serialization and deserialization.
7
- # Provides a clean API for saving/loading snapshots in various formats.
8
- #
9
- # @example Create and save snapshot
10
- # snapshot = swarm.snapshot
11
- # snapshot.write_to_file("session.json")
12
- #
13
- # @example Load and restore snapshot
14
- # snapshot = SwarmSDK::Snapshot.from_file("session.json")
15
- # result = swarm.restore(snapshot)
16
- #
17
- # @example Convert to/from hash
18
- # hash = snapshot.to_hash
19
- # snapshot = SwarmSDK::Snapshot.from_hash(hash)
20
- class Snapshot
21
- attr_reader :data
22
-
23
- # Initialize snapshot with data
24
- #
25
- # @param data [Hash] Snapshot data hash
26
- def initialize(data)
27
- @data = data
28
- end
29
-
30
- # Convert snapshot to hash
31
- #
32
- # @return [Hash] Snapshot data as hash
33
- def to_hash
34
- @data
35
- end
36
-
37
- # Convert snapshot to JSON string
38
- #
39
- # @param pretty [Boolean] Whether to pretty-print JSON (default: true)
40
- # @return [String] JSON string
41
- def to_json(pretty: true)
42
- if pretty
43
- JSON.pretty_generate(@data)
44
- else
45
- JSON.generate(@data)
46
- end
47
- end
48
-
49
- # Write snapshot to file as JSON
50
- #
51
- # Uses atomic write pattern (write to temp file, then rename) to prevent
52
- # corruption if process crashes during write.
53
- #
54
- # @param path [String] File path to write to
55
- # @param pretty [Boolean] Whether to pretty-print JSON (default: true)
56
- # @return [void]
57
- def write_to_file(path, pretty: true)
58
- # Atomic write: write to temp file, then rename
59
- # This ensures the snapshot file is never corrupted even if process crashes
60
- temp_path = "#{path}.tmp.#{Process.pid}.#{Time.now.to_i}.#{SecureRandom.hex(4)}"
61
-
62
- File.write(temp_path, to_json(pretty: pretty))
63
- File.rename(temp_path, path)
64
- rescue
65
- # Clean up temp file if it exists
66
- File.delete(temp_path) if File.exist?(temp_path)
67
- raise
68
- end
69
-
70
- class << self
71
- # Create snapshot from hash
72
- #
73
- # @param hash [Hash] Snapshot data hash
74
- # @return [Snapshot] New snapshot instance
75
- def from_hash(hash)
76
- new(hash)
77
- end
78
-
79
- # Create snapshot from JSON string
80
- #
81
- # @param json_string [String] JSON string
82
- # @return [Snapshot] New snapshot instance
83
- def from_json(json_string)
84
- hash = JSON.parse(json_string, symbolize_names: true)
85
- new(hash)
86
- end
87
-
88
- # Create snapshot from JSON file
89
- #
90
- # @param path [String] File path to read from
91
- # @return [Snapshot] New snapshot instance
92
- def from_file(path)
93
- json_string = File.read(path)
94
- from_json(json_string)
95
- end
96
- end
97
-
98
- # Get snapshot version
99
- #
100
- # @return [String] Snapshot version
101
- def version
102
- @data[:version] || @data["version"]
103
- end
104
-
105
- # Get snapshot type (swarm or workflow)
106
- #
107
- # @return [String] Snapshot type
108
- def type
109
- @data[:type] || @data["type"]
110
- end
111
-
112
- # Get timestamp when snapshot was created
113
- #
114
- # @return [String] ISO8601 timestamp
115
- def snapshot_at
116
- @data[:snapshot_at] || @data["snapshot_at"]
117
- end
118
-
119
- # Get SwarmSDK version that created this snapshot
120
- #
121
- # @return [String] SwarmSDK version
122
- def swarm_sdk_version
123
- @data[:swarm_sdk_version] || @data["swarm_sdk_version"]
124
- end
125
-
126
- # Get agent names from snapshot
127
- #
128
- # @return [Array<String>] Agent names
129
- def agent_names
130
- agents = @data[:agents] || @data["agents"]
131
- agents ? agents.keys.map(&:to_s) : []
132
- end
133
-
134
- # Get delegation instance names from snapshot
135
- #
136
- # @return [Array<String>] Delegation instance names
137
- def delegation_instance_names
138
- delegations = @data[:delegation_instances] || @data["delegation_instances"]
139
- delegations ? delegations.keys.map(&:to_s) : []
140
- end
141
-
142
- # Check if snapshot is for a swarm (vs workflow)
143
- #
144
- # @return [Boolean] true if swarm snapshot
145
- def swarm?
146
- type == "swarm"
147
- end
148
-
149
- # Check if snapshot is for a workflow
150
- #
151
- # @return [Boolean] true if workflow snapshot
152
- def workflow?
153
- type == "workflow"
154
- end
155
- end
156
- end
@@ -1,397 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- # Reconstructs a complete StateSnapshot from event logs
5
- #
6
- # This class enables full swarm state reconstruction from event streams,
7
- # supporting session persistence, time travel debugging, and event sourcing.
8
- #
9
- # ## Usage
10
- #
11
- # # Collect events during execution
12
- # events = []
13
- # swarm.execute("Build feature") { |event| events << event }
14
- #
15
- # # Save events to storage (DB, file, Redis, etc.)
16
- # File.write("session.json", JSON.generate(events))
17
- #
18
- # # Later: Reconstruct snapshot from events
19
- # events = JSON.parse(File.read("session.json"), symbolize_names: true)
20
- # snapshot_data = SwarmSDK::SnapshotFromEvents.reconstruct(events)
21
- #
22
- # # Restore swarm from reconstructed snapshot
23
- # swarm = SwarmSDK::Swarm.from_config("swarm.yml")
24
- # swarm.restore(snapshot_data)
25
- #
26
- # ## What Gets Reconstructed
27
- #
28
- # - Swarm metadata (swarm_id, parent_swarm_id, first_message_sent)
29
- # - Agent conversations (all RubyLLM::Message objects)
30
- # - Agent context state (warnings, compression, todowrite, skills)
31
- # - Delegation instance conversations
32
- # - Scratchpad contents
33
- # - Read tracking with digests
34
- # - Memory read tracking with digests
35
- #
36
- # ## Requirements
37
- #
38
- # Events must have:
39
- # - :timestamp field (ISO 8601 format) for chronological ordering
40
- # - :agent field to identify which agent
41
- # - :type field to identify event type
42
- #
43
- # @see EventsToMessages for message reconstruction details
44
- class SnapshotFromEvents
45
- class << self
46
- # Reconstruct a complete StateSnapshot from event stream
47
- #
48
- # @param events [Array<Hash>] Event stream with timestamps
49
- # @return [Hash] Complete StateSnapshot hash compatible with StateRestorer
50
- #
51
- # @example
52
- # snapshot_data = SnapshotFromEvents.reconstruct(events)
53
- # swarm.restore(snapshot_data)
54
- def reconstruct(events)
55
- new(events).reconstruct
56
- end
57
- end
58
-
59
- # Initialize reconstructor
60
- #
61
- # @param events [Array<Hash>] Event stream
62
- def initialize(events)
63
- # Sort events by timestamp for chronological processing
64
- @events = events.sort_by { |e| parse_timestamp(e[:timestamp]) }
65
- end
66
-
67
- # Reconstruct complete snapshot
68
- #
69
- # @return [Hash] StateSnapshot hash
70
- def reconstruct
71
- {
72
- version: "2.1.0",
73
- type: "swarm",
74
- snapshot_at: @events.last&.fetch(:timestamp, Time.now.utc.iso8601),
75
- swarm_sdk_version: SwarmSDK::VERSION,
76
- metadata: reconstruct_swarm_metadata,
77
- agents: reconstruct_all_agents,
78
- delegation_instances: reconstruct_all_delegations,
79
- scratchpad: reconstruct_scratchpad,
80
- read_tracking: reconstruct_read_tracking,
81
- memory_read_tracking: reconstruct_memory_read_tracking,
82
- plugin_states: reconstruct_plugin_states,
83
- }
84
- end
85
-
86
- private
87
-
88
- # Reconstruct swarm metadata
89
- #
90
- # @return [Hash] Swarm metadata
91
- def reconstruct_swarm_metadata
92
- first_event = @events.first || {}
93
-
94
- {
95
- id: first_event[:swarm_id],
96
- parent_id: first_event[:parent_swarm_id],
97
- first_message_sent: !@events.empty?,
98
- }
99
- end
100
-
101
- # Reconstruct all primary agents
102
- #
103
- # @return [Hash] { agent_name => { conversation:, context_state:, system_prompt: } }
104
- def reconstruct_all_agents
105
- # Get unique primary agent names (exclude delegations with @)
106
- agent_names = @events
107
- .map { |e| e[:agent] }
108
- .compact
109
- .uniq
110
- .reject { |name| name.to_s.include?("@") }
111
-
112
- agent_names.each_with_object({}) do |agent, hash|
113
- hash[agent.to_s] = {
114
- conversation: reconstruct_conversation(agent),
115
- context_state: reconstruct_context_state(agent),
116
- system_prompt: reconstruct_system_prompt(agent),
117
- }
118
- end
119
- end
120
-
121
- # Reconstruct all delegation instances
122
- #
123
- # @return [Hash] { "delegate@delegator" => { conversation:, context_state:, system_prompt: } }
124
- def reconstruct_all_delegations
125
- # Get unique delegation instance names (contain @)
126
- delegation_names = @events
127
- .map { |e| e[:agent] }
128
- .compact
129
- .uniq
130
- .select { |name| name.to_s.include?("@") }
131
-
132
- delegation_names.each_with_object({}) do |delegation, hash|
133
- hash[delegation.to_s] = {
134
- conversation: reconstruct_conversation(delegation),
135
- context_state: reconstruct_context_state(delegation),
136
- system_prompt: reconstruct_system_prompt(delegation),
137
- }
138
- end
139
- end
140
-
141
- # Reconstruct conversation for an agent
142
- #
143
- # @param agent [Symbol, String] Agent name
144
- # @return [Array<Hash>] Serialized RubyLLM::Message objects
145
- def reconstruct_conversation(agent)
146
- messages = SwarmSDK::EventsToMessages.reconstruct(@events, agent: agent)
147
- messages.map { |msg| serialize_message(msg) }
148
- end
149
-
150
- # Serialize a RubyLLM::Message to hash format
151
- #
152
- # Matches the format used by StateSnapshot.
153
- #
154
- # @param msg [RubyLLM::Message] Message to serialize
155
- # @return [Hash] Serialized message
156
- def serialize_message(msg)
157
- hash = { role: msg.role, content: msg.content }
158
-
159
- # Serialize tool calls if present
160
- # Must manually extract fields - RubyLLM::ToolCall#to_h doesn't reliably serialize id/name
161
- # msg.tool_calls is a Hash<String, ToolCall>, so we need .values
162
- if msg.tool_calls && !msg.tool_calls.empty?
163
- hash[:tool_calls] = msg.tool_calls.values.map do |tc|
164
- {
165
- id: tc.id,
166
- name: tc.name,
167
- arguments: tc.arguments,
168
- }
169
- end
170
- end
171
-
172
- # Add optional fields
173
- hash[:tool_call_id] = msg.tool_call_id if msg.tool_call_id
174
- hash[:input_tokens] = msg.input_tokens if msg.input_tokens
175
- hash[:output_tokens] = msg.output_tokens if msg.output_tokens
176
- hash[:model_id] = msg.model_id if msg.model_id
177
-
178
- hash
179
- end
180
-
181
- # Reconstruct context state for an agent
182
- #
183
- # @param agent [Symbol, String] Agent name
184
- # @return [Hash] Context state
185
- def reconstruct_context_state(agent)
186
- {
187
- warning_thresholds_hit: reconstruct_warning_thresholds(agent),
188
- compression_applied: reconstruct_compression_applied(agent),
189
- last_todowrite_message_index: reconstruct_todowrite_index(agent),
190
- active_skill_path: reconstruct_active_skill_path(agent),
191
- }
192
- end
193
-
194
- # Reconstruct which warning thresholds were hit
195
- #
196
- # @param agent [Symbol, String] Agent name
197
- # @return [Array<Integer>] Sorted array of thresholds that were hit
198
- def reconstruct_warning_thresholds(agent)
199
- @events
200
- .select { |e| e[:type] == "context_threshold_hit" && normalize_agent(e[:agent]) == normalize_agent(agent) }
201
- .map { |e| e[:threshold] }
202
- .uniq
203
- .sort
204
- end
205
-
206
- # Reconstruct compression state
207
- #
208
- # @param agent [Symbol, String] Agent name
209
- # @return [Boolean, nil] true if compression was applied, nil otherwise
210
- def reconstruct_compression_applied(agent)
211
- compression_event = @events
212
- .select { |e| e[:type] == "compression_completed" && normalize_agent(e[:agent]) == normalize_agent(agent) }
213
- .last
214
-
215
- # NOTE: StateSnapshot stores nil (not false) when no compression applied
216
- compression_event ? true : nil
217
- end
218
-
219
- # Reconstruct last TodoWrite message index
220
- #
221
- # Finds the last TodoWrite tool call and determines which message index it corresponds to.
222
- #
223
- # @param agent [Symbol, String] Agent name
224
- # @return [Integer, nil] Message index or nil if no TodoWrite calls
225
- def reconstruct_todowrite_index(agent)
226
- # Find last TodoWrite tool call
227
- last_todowrite_call = @events
228
- .select { |e| e[:type] == "tool_call" && e[:tool] == "TodoWrite" && normalize_agent(e[:agent]) == normalize_agent(agent) }
229
- .sort_by { |e| parse_timestamp(e[:timestamp]) }
230
- .last
231
-
232
- return unless last_todowrite_call
233
-
234
- # Reconstruct messages to find index
235
- messages = SwarmSDK::EventsToMessages.reconstruct(@events, agent: agent)
236
-
237
- # Find the message containing this tool_call_id
238
- messages.each_with_index do |msg, idx|
239
- if msg.role == :assistant && msg.tool_calls
240
- return idx if msg.tool_calls.key?(last_todowrite_call[:tool_call_id])
241
- end
242
- end
243
-
244
- nil
245
- end
246
-
247
- # Reconstruct active skill path
248
- #
249
- # @param agent [Symbol, String] Agent name
250
- # @return [String, nil] Skill path or nil if no skill loaded
251
- def reconstruct_active_skill_path(agent)
252
- last_load_skill = @events
253
- .select { |e| e[:type] == "tool_call" && e[:tool] == "LoadSkill" && normalize_agent(e[:agent]) == normalize_agent(agent) }
254
- .sort_by { |e| parse_timestamp(e[:timestamp]) }
255
- .last
256
-
257
- return unless last_load_skill
258
-
259
- # Extract skill path from arguments (handle both symbol and string keys)
260
- args = last_load_skill[:arguments]
261
- args[:file_path] || args["file_path"]
262
- end
263
-
264
- # Reconstruct system prompt from the last agent_start event
265
- #
266
- # Uses the LAST agent_start event for this agent, which represents the most
267
- # recent system prompt that was active. This handles cases where the swarm
268
- # was restarted with updated configuration.
269
- #
270
- # @param agent [Symbol, String] Agent name
271
- # @return [String, nil] System prompt or nil if no agent_start event found
272
- def reconstruct_system_prompt(agent)
273
- last_agent_start = @events
274
- .select { |e| e[:type] == "agent_start" && normalize_agent(e[:agent]) == normalize_agent(agent) }
275
- .sort_by { |e| parse_timestamp(e[:timestamp]) }
276
- .last
277
-
278
- return unless last_agent_start
279
-
280
- # Handle both symbol and string keys from JSON
281
- last_agent_start[:system_prompt] || last_agent_start["system_prompt"]
282
- end
283
-
284
- # Reconstruct scratchpad contents
285
- #
286
- # Replays all ScratchpadWrite tool calls in chronological order.
287
- # Later writes to the same path overwrite earlier ones.
288
- #
289
- # @return [Hash] { path => { content:, title:, updated_at:, size: } }
290
- def reconstruct_scratchpad
291
- scratchpad = {}
292
-
293
- @events
294
- .select { |e| e[:type] == "tool_call" && e[:tool] == "ScratchpadWrite" }
295
- .sort_by { |e| parse_timestamp(e[:timestamp]) }
296
- .each do |event|
297
- args = event[:arguments]
298
-
299
- # Handle both symbol and string keys
300
- path = args[:file_path] || args["file_path"]
301
- content = args[:content] || args["content"]
302
- title = args[:title] || args["title"]
303
-
304
- next unless path && content
305
-
306
- scratchpad[path] = {
307
- content: content,
308
- title: title,
309
- updated_at: event[:timestamp],
310
- size: content.bytesize,
311
- }
312
- end
313
-
314
- scratchpad
315
- end
316
-
317
- # Reconstruct read tracking (file digests)
318
- #
319
- # Extracts digests from Read tool_result metadata.
320
- # Later reads to the same file update the digest.
321
- #
322
- # @return [Hash] { agent_name => { file_path => digest } }
323
- def reconstruct_read_tracking
324
- tracking = {}
325
-
326
- @events
327
- .select { |e| e[:type] == "tool_result" && e[:tool] == "Read" }
328
- .each do |event|
329
- agent = event[:agent].to_s # Use string key to match StateSnapshot format
330
- digest = event.dig(:metadata, :read_digest)
331
- path = event.dig(:metadata, :read_path)
332
-
333
- next unless digest && path
334
-
335
- tracking[agent] ||= {}
336
- tracking[agent][path] = digest
337
- end
338
-
339
- tracking
340
- end
341
-
342
- # Reconstruct memory read tracking (entry digests)
343
- #
344
- # Extracts digests from MemoryRead tool_result metadata.
345
- # Later reads to the same entry update the digest.
346
- #
347
- # @return [Hash] { agent_name => { entry_path => digest } }
348
- def reconstruct_memory_read_tracking
349
- tracking = {}
350
-
351
- @events
352
- .select { |e| e[:type] == "tool_result" && e[:tool] == "MemoryRead" }
353
- .each do |event|
354
- agent = event[:agent].to_s # Use string key to match StateSnapshot format
355
- digest = event.dig(:metadata, :read_digest)
356
- path = event.dig(:metadata, :read_path)
357
-
358
- next unless digest && path
359
-
360
- tracking[agent] ||= {}
361
- tracking[agent][path] = digest
362
- end
363
-
364
- tracking
365
- end
366
-
367
- # Reconstruct plugin states
368
- #
369
- # Plugin states cannot be fully reconstructed from events alone as they
370
- # contain internal plugin data. Returns empty hash for compatibility.
371
- #
372
- # @return [Hash] Empty plugin states hash
373
- def reconstruct_plugin_states
374
- {}
375
- end
376
-
377
- # Parse timestamp string to Time object
378
- #
379
- # @param timestamp [String, nil] ISO 8601 timestamp
380
- # @return [Time] Parsed time or epoch if nil/invalid
381
- def parse_timestamp(timestamp)
382
- return Time.at(0) unless timestamp
383
-
384
- Time.parse(timestamp)
385
- rescue ArgumentError
386
- Time.at(0)
387
- end
388
-
389
- # Normalize agent name for comparison
390
- #
391
- # @param agent [Symbol, String, nil] Agent name
392
- # @return [Symbol] Normalized agent name
393
- def normalize_agent(agent)
394
- agent.to_s.to_sym
395
- end
396
- end
397
- end