swarm_memory 2.1.5 → 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.
- checksums.yaml +4 -4
- data/lib/swarm_memory/version.rb +1 -1
- metadata +5 -184
- data/lib/claude_swarm/base_executor.rb +0 -133
- data/lib/claude_swarm/claude_code_executor.rb +0 -349
- data/lib/claude_swarm/claude_mcp_server.rb +0 -78
- data/lib/claude_swarm/cli.rb +0 -697
- data/lib/claude_swarm/commands/ps.rb +0 -215
- data/lib/claude_swarm/commands/show.rb +0 -139
- data/lib/claude_swarm/configuration.rb +0 -373
- data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
- data/lib/claude_swarm/json_handler.rb +0 -91
- data/lib/claude_swarm/mcp_generator.rb +0 -230
- data/lib/claude_swarm/openai/chat_completion.rb +0 -256
- data/lib/claude_swarm/openai/executor.rb +0 -256
- data/lib/claude_swarm/openai/responses.rb +0 -319
- data/lib/claude_swarm/orchestrator.rb +0 -878
- data/lib/claude_swarm/process_tracker.rb +0 -78
- data/lib/claude_swarm/session_cost_calculator.rb +0 -209
- data/lib/claude_swarm/session_path.rb +0 -42
- data/lib/claude_swarm/settings_generator.rb +0 -77
- data/lib/claude_swarm/system_utils.rb +0 -46
- data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
- data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
- data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
- data/lib/claude_swarm/tools/task_tool.rb +0 -63
- data/lib/claude_swarm/version.rb +0 -5
- data/lib/claude_swarm/worktree_manager.rb +0 -475
- data/lib/claude_swarm/yaml_loader.rb +0 -22
- data/lib/claude_swarm.rb +0 -67
- data/lib/swarm_cli/cli.rb +0 -201
- data/lib/swarm_cli/command_registry.rb +0 -61
- data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
- data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
- data/lib/swarm_cli/commands/migrate.rb +0 -55
- data/lib/swarm_cli/commands/run.rb +0 -173
- data/lib/swarm_cli/config_loader.rb +0 -98
- data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
- data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
- data/lib/swarm_cli/interactive_repl.rb +0 -924
- data/lib/swarm_cli/mcp_serve_options.rb +0 -44
- data/lib/swarm_cli/mcp_tools_options.rb +0 -59
- data/lib/swarm_cli/migrate_options.rb +0 -54
- data/lib/swarm_cli/migrator.rb +0 -132
- data/lib/swarm_cli/options.rb +0 -151
- data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
- data/lib/swarm_cli/ui/components/content_block.rb +0 -120
- data/lib/swarm_cli/ui/components/divider.rb +0 -57
- data/lib/swarm_cli/ui/components/panel.rb +0 -62
- data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
- data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
- data/lib/swarm_cli/ui/formatters/number.rb +0 -58
- data/lib/swarm_cli/ui/formatters/text.rb +0 -77
- data/lib/swarm_cli/ui/formatters/time.rb +0 -73
- data/lib/swarm_cli/ui/icons.rb +0 -36
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
- data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
- data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
- data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
- data/lib/swarm_cli/version.rb +0 -5
- data/lib/swarm_cli.rb +0 -46
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
- data/lib/swarm_sdk/agent/builder.rb +0 -552
- data/lib/swarm_sdk/agent/chat.rb +0 -774
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
- data/lib/swarm_sdk/agent/context.rb +0 -116
- data/lib/swarm_sdk/agent/context_manager.rb +0 -315
- data/lib/swarm_sdk/agent/definition.rb +0 -477
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
- data/lib/swarm_sdk/builders/base_builder.rb +0 -409
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
- data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
- data/lib/swarm_sdk/concerns/validatable.rb +0 -55
- data/lib/swarm_sdk/configuration/parser.rb +0 -353
- data/lib/swarm_sdk/configuration/translator.rb +0 -255
- data/lib/swarm_sdk/configuration.rb +0 -135
- data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
- data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
- data/lib/swarm_sdk/context_compactor.rb +0 -335
- data/lib/swarm_sdk/context_management/builder.rb +0 -128
- data/lib/swarm_sdk/context_management/context.rb +0 -328
- data/lib/swarm_sdk/defaults.rb +0 -196
- data/lib/swarm_sdk/events_to_messages.rb +0 -199
- data/lib/swarm_sdk/hooks/adapter.rb +0 -359
- data/lib/swarm_sdk/hooks/context.rb +0 -197
- data/lib/swarm_sdk/hooks/definition.rb +0 -80
- data/lib/swarm_sdk/hooks/error.rb +0 -29
- data/lib/swarm_sdk/hooks/executor.rb +0 -146
- data/lib/swarm_sdk/hooks/registry.rb +0 -147
- data/lib/swarm_sdk/hooks/result.rb +0 -150
- data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
- data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
- data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
- data/lib/swarm_sdk/log_collector.rb +0 -227
- data/lib/swarm_sdk/log_stream.rb +0 -127
- data/lib/swarm_sdk/markdown_parser.rb +0 -75
- data/lib/swarm_sdk/model_aliases.json +0 -8
- data/lib/swarm_sdk/models.json +0 -1
- data/lib/swarm_sdk/models.rb +0 -120
- data/lib/swarm_sdk/node_context.rb +0 -245
- data/lib/swarm_sdk/observer/builder.rb +0 -81
- data/lib/swarm_sdk/observer/config.rb +0 -45
- data/lib/swarm_sdk/observer/manager.rb +0 -236
- data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
- data/lib/swarm_sdk/permissions/config.rb +0 -239
- data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
- data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
- data/lib/swarm_sdk/permissions/validator.rb +0 -173
- data/lib/swarm_sdk/permissions_builder.rb +0 -122
- data/lib/swarm_sdk/plugin.rb +0 -309
- data/lib/swarm_sdk/plugin_registry.rb +0 -101
- data/lib/swarm_sdk/proc_helpers.rb +0 -53
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -123
- data/lib/swarm_sdk/snapshot.rb +0 -156
- data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
- data/lib/swarm_sdk/state_restorer.rb +0 -476
- data/lib/swarm_sdk/state_snapshot.rb +0 -334
- data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
- data/lib/swarm_sdk/swarm/builder.rb +0 -249
- data/lib/swarm_sdk/swarm/executor.rb +0 -213
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
- data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
- data/lib/swarm_sdk/swarm.rb +0 -717
- data/lib/swarm_sdk/swarm_loader.rb +0 -145
- data/lib/swarm_sdk/swarm_registry.rb +0 -136
- data/lib/swarm_sdk/tools/bash.rb +0 -282
- data/lib/swarm_sdk/tools/clock.rb +0 -44
- data/lib/swarm_sdk/tools/delegate.rb +0 -267
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
- data/lib/swarm_sdk/tools/edit.rb +0 -145
- data/lib/swarm_sdk/tools/glob.rb +0 -166
- data/lib/swarm_sdk/tools/grep.rb +0 -235
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
- data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
- data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
- data/lib/swarm_sdk/tools/read.rb +0 -261
- data/lib/swarm_sdk/tools/registry.rb +0 -205
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
- data/lib/swarm_sdk/tools/think.rb +0 -98
- data/lib/swarm_sdk/tools/todo_write.rb +0 -235
- data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
- data/lib/swarm_sdk/tools/write.rb +0 -112
- data/lib/swarm_sdk/utils.rb +0 -68
- data/lib/swarm_sdk/validation_result.rb +0 -33
- data/lib/swarm_sdk/version.rb +0 -5
- data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
- data/lib/swarm_sdk/workflow/builder.rb +0 -143
- data/lib/swarm_sdk/workflow/executor.rb +0 -497
- data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
- data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
- data/lib/swarm_sdk/workflow.rb +0 -554
- data/lib/swarm_sdk.rb +0 -524
data/lib/swarm_sdk/result.rb
DELETED
|
@@ -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
|
data/lib/swarm_sdk/snapshot.rb
DELETED
|
@@ -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
|