swarm_memory 2.1.1 → 2.1.3

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/lib/claude_swarm/cli.rb +9 -11
  3. data/lib/claude_swarm/commands/ps.rb +1 -2
  4. data/lib/claude_swarm/configuration.rb +30 -7
  5. data/lib/claude_swarm/mcp_generator.rb +4 -10
  6. data/lib/claude_swarm/orchestrator.rb +43 -44
  7. data/lib/claude_swarm/system_utils.rb +4 -4
  8. data/lib/claude_swarm/version.rb +1 -1
  9. data/lib/claude_swarm.rb +5 -9
  10. data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
  11. data/lib/swarm_cli/commands/mcp_tools.rb +3 -3
  12. data/lib/swarm_cli/config_loader.rb +14 -13
  13. data/lib/swarm_cli/version.rb +1 -1
  14. data/lib/swarm_cli.rb +2 -0
  15. data/lib/swarm_memory/adapters/base.rb +4 -4
  16. data/lib/swarm_memory/adapters/filesystem_adapter.rb +0 -12
  17. data/lib/swarm_memory/core/storage.rb +66 -6
  18. data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
  19. data/lib/swarm_memory/integration/cli_registration.rb +3 -2
  20. data/lib/swarm_memory/integration/sdk_plugin.rb +24 -4
  21. data/lib/swarm_memory/optimization/defragmenter.rb +4 -0
  22. data/lib/swarm_memory/tools/memory_edit.rb +3 -2
  23. data/lib/swarm_memory/tools/memory_glob.rb +24 -1
  24. data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
  25. data/lib/swarm_memory/tools/memory_read.rb +3 -3
  26. data/lib/swarm_memory/tools/memory_write.rb +2 -2
  27. data/lib/swarm_memory/version.rb +1 -1
  28. data/lib/swarm_memory.rb +7 -0
  29. data/lib/swarm_sdk/agent/builder.rb +33 -0
  30. data/lib/swarm_sdk/agent/chat/context_tracker.rb +33 -0
  31. data/lib/swarm_sdk/agent/chat/hook_integration.rb +41 -0
  32. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +11 -27
  33. data/lib/swarm_sdk/agent/chat.rb +199 -52
  34. data/lib/swarm_sdk/agent/context.rb +6 -2
  35. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  36. data/lib/swarm_sdk/agent/definition.rb +32 -23
  37. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
  38. data/lib/swarm_sdk/configuration.rb +420 -103
  39. data/lib/swarm_sdk/events_to_messages.rb +181 -0
  40. data/lib/swarm_sdk/log_collector.rb +31 -5
  41. data/lib/swarm_sdk/log_stream.rb +37 -8
  42. data/lib/swarm_sdk/model_aliases.json +4 -1
  43. data/lib/swarm_sdk/node/agent_config.rb +39 -9
  44. data/lib/swarm_sdk/node/builder.rb +158 -42
  45. data/lib/swarm_sdk/node_context.rb +75 -0
  46. data/lib/swarm_sdk/node_orchestrator.rb +492 -18
  47. data/lib/swarm_sdk/plugin.rb +73 -1
  48. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  49. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  50. data/lib/swarm_sdk/providers/openai_with_responses.rb +22 -15
  51. data/lib/swarm_sdk/restore_result.rb +65 -0
  52. data/lib/swarm_sdk/result.rb +32 -6
  53. data/lib/swarm_sdk/snapshot.rb +156 -0
  54. data/lib/swarm_sdk/snapshot_from_events.rb +386 -0
  55. data/lib/swarm_sdk/state_restorer.rb +491 -0
  56. data/lib/swarm_sdk/state_snapshot.rb +369 -0
  57. data/lib/swarm_sdk/swarm/agent_initializer.rb +360 -55
  58. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  59. data/lib/swarm_sdk/swarm/builder.rb +208 -11
  60. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  61. data/lib/swarm_sdk/swarm/tool_configurator.rb +46 -11
  62. data/lib/swarm_sdk/swarm.rb +367 -90
  63. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  64. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  65. data/lib/swarm_sdk/tools/delegate.rb +94 -9
  66. data/lib/swarm_sdk/tools/read.rb +17 -5
  67. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  68. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  69. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  70. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  71. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +45 -0
  72. data/lib/swarm_sdk/tools/stores/storage.rb +4 -4
  73. data/lib/swarm_sdk/tools/think.rb +4 -1
  74. data/lib/swarm_sdk/tools/todo_write.rb +20 -8
  75. data/lib/swarm_sdk/utils.rb +18 -0
  76. data/lib/swarm_sdk/validation_result.rb +33 -0
  77. data/lib/swarm_sdk/version.rb +1 -1
  78. data/lib/swarm_sdk.rb +365 -28
  79. metadata +17 -5
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ # Reconstructs RubyLLM::Message objects from SwarmSDK event streams
5
+ #
6
+ # This class enables conversation replay and analysis from event logs.
7
+ # It uses timestamps to maintain chronological ordering of messages.
8
+ #
9
+ # ## Limitations
10
+ #
11
+ # This reconstructs ONLY conversation messages. It does NOT restore:
12
+ # - Context state (warning thresholds, compression, todowrite index)
13
+ # - Scratchpad contents
14
+ # - Read tracking information
15
+ # - Full swarm state
16
+ #
17
+ # For full state restoration, use StateSnapshot/StateRestorer or SnapshotFromEvents.
18
+ #
19
+ # ## Usage
20
+ #
21
+ # # Collect events during execution
22
+ # events = []
23
+ # swarm.execute("Build feature") do |event|
24
+ # events << event
25
+ # end
26
+ #
27
+ # # Reconstruct conversation for an agent
28
+ # messages = SwarmSDK::EventsToMessages.reconstruct(events, agent: :backend)
29
+ #
30
+ # # View conversation
31
+ # messages.each do |msg|
32
+ # puts "[#{msg.role}] #{msg.content}"
33
+ # end
34
+ #
35
+ # ## Event Requirements
36
+ #
37
+ # Events must have:
38
+ # - `:timestamp` field (ISO 8601 format) for ordering
39
+ # - `:agent` field to filter by agent
40
+ # - `:type` field to identify event type
41
+ #
42
+ # Supported event types:
43
+ # - `user_prompt`: Reconstructs user message (prompt in metadata or top-level)
44
+ # - `agent_step`: Reconstructs assistant message with tool calls
45
+ # - `agent_stop`: Reconstructs final assistant message
46
+ # - `tool_result`: Reconstructs tool result message
47
+ class EventsToMessages
48
+ class << self
49
+ # Reconstruct messages for an agent from event stream
50
+ #
51
+ # @param events [Array<Hash>] Event stream with timestamps
52
+ # @param agent [Symbol, String] Agent name to reconstruct messages for
53
+ # @return [Array<RubyLLM::Message>] Reconstructed messages in chronological order
54
+ #
55
+ # @example
56
+ # messages = EventsToMessages.reconstruct(events, agent: :backend)
57
+ # messages.each { |msg| puts msg.content }
58
+ def reconstruct(events, agent:)
59
+ new(events, agent).reconstruct
60
+ end
61
+ end
62
+
63
+ # Initialize reconstructor
64
+ #
65
+ # @param events [Array<Hash>] Event stream
66
+ # @param agent [Symbol, String] Agent name
67
+ def initialize(events, agent)
68
+ @events = events
69
+ @agent = agent.to_sym
70
+ end
71
+
72
+ # Reconstruct messages from events
73
+ #
74
+ # Filters events by agent, sorts by timestamp, and converts to RubyLLM::Message objects.
75
+ #
76
+ # @return [Array<RubyLLM::Message>] Reconstructed messages
77
+ def reconstruct
78
+ messages = []
79
+
80
+ # Filter events for this agent and sort by timestamp
81
+ agent_events = @events
82
+ .select { |e| normalize_agent(e[:agent]) == @agent }
83
+ .sort_by { |e| parse_timestamp(e[:timestamp]) }
84
+
85
+ agent_events.each do |event|
86
+ message = case event[:type]&.to_s
87
+ when "user_prompt"
88
+ reconstruct_user_message(event)
89
+ when "agent_step", "agent_stop"
90
+ reconstruct_assistant_message(event)
91
+ when "tool_result"
92
+ reconstruct_tool_result_message(event)
93
+ end
94
+
95
+ messages << message if message
96
+ end
97
+
98
+ messages
99
+ end
100
+
101
+ private
102
+
103
+ # Reconstruct user message from user_prompt event
104
+ #
105
+ # Extracts prompt from metadata or top-level field.
106
+ #
107
+ # @param event [Hash] user_prompt event
108
+ # @return [RubyLLM::Message, nil] User message or nil if prompt not found
109
+ def reconstruct_user_message(event)
110
+ # Try to extract prompt from metadata (current location) or top-level (potential future location)
111
+ prompt = event.dig(:metadata, :prompt) || event[:prompt]
112
+ return unless prompt && !prompt.to_s.empty?
113
+
114
+ RubyLLM::Message.new(
115
+ role: :user,
116
+ content: prompt,
117
+ )
118
+ end
119
+
120
+ # Reconstruct assistant message from agent_step or agent_stop event
121
+ #
122
+ # Converts tool_calls array to hash format expected by RubyLLM.
123
+ #
124
+ # @param event [Hash] agent_step or agent_stop event
125
+ # @return [RubyLLM::Message] Assistant message
126
+ def reconstruct_assistant_message(event)
127
+ # Convert tool_calls array to hash (RubyLLM format)
128
+ # Events emit tool_calls as Array, but RubyLLM expects Hash<String, ToolCall>
129
+ tool_calls_hash = if event[:tool_calls] && !event[:tool_calls].empty?
130
+ event[:tool_calls].each_with_object({}) do |tc, hash|
131
+ hash[tc[:id].to_s] = RubyLLM::ToolCall.new(
132
+ id: tc[:id],
133
+ name: tc[:name],
134
+ arguments: tc[:arguments] || {},
135
+ )
136
+ end
137
+ end
138
+
139
+ RubyLLM::Message.new(
140
+ role: :assistant,
141
+ content: event[:content] || "",
142
+ tool_calls: tool_calls_hash,
143
+ input_tokens: event.dig(:usage, :input_tokens),
144
+ output_tokens: event.dig(:usage, :output_tokens),
145
+ model_id: event[:model],
146
+ )
147
+ end
148
+
149
+ # Reconstruct tool result message from tool_result event
150
+ #
151
+ # @param event [Hash] tool_result event
152
+ # @return [RubyLLM::Message] Tool result message
153
+ def reconstruct_tool_result_message(event)
154
+ RubyLLM::Message.new(
155
+ role: :tool,
156
+ content: event[:result].to_s,
157
+ tool_call_id: event[:tool_call_id],
158
+ )
159
+ end
160
+
161
+ # Parse timestamp string to Time object
162
+ #
163
+ # @param timestamp [String, nil] ISO 8601 timestamp
164
+ # @return [Time] Parsed time or epoch if nil/invalid
165
+ def parse_timestamp(timestamp)
166
+ return Time.at(0) unless timestamp
167
+
168
+ Time.parse(timestamp)
169
+ rescue ArgumentError
170
+ Time.at(0)
171
+ end
172
+
173
+ # Normalize agent name to symbol
174
+ #
175
+ # @param agent [Symbol, String, nil] Agent name
176
+ # @return [Symbol] Normalized agent name
177
+ def normalize_agent(agent)
178
+ agent.to_s.to_sym
179
+ end
180
+ end
181
+ end
@@ -7,6 +7,20 @@ module SwarmSDK
7
7
  # to user-registered callbacks. It's designed to be set as the LogStream
8
8
  # emitter during swarm execution.
9
9
  #
10
+ # ## Thread Safety for Multi-Threaded Environments (Puma, Sidekiq)
11
+ #
12
+ # Callbacks are stored in Fiber-local storage (Fiber[:log_callbacks]) instead
13
+ # of class instance variables. This ensures callbacks registered in the parent
14
+ # thread/fiber are accessible to child fibers created by Async reactor.
15
+ #
16
+ # Why: In Puma/Sidekiq, class instance variables (@callbacks) are thread-isolated
17
+ # and don't properly propagate to child fibers. Using Fiber-local storage ensures
18
+ # events emitted from within Async blocks can reach registered callbacks.
19
+ #
20
+ # Child fibers inherit parent fiber-local storage automatically, so events
21
+ # emitted from agent callbacks (on_tool_call, on_end_message, etc.) executing
22
+ # in child fibers can still reach the parent's registered callbacks.
23
+ #
10
24
  # ## Usage
11
25
  #
12
26
  # # Register a callback (before execution starts)
@@ -24,19 +38,31 @@ module SwarmSDK
24
38
  class << self
25
39
  # Register a callback to receive log events
26
40
  #
41
+ # Stores callback in Fiber-local storage to ensure accessibility
42
+ # from child fibers in multi-threaded environments.
43
+ #
27
44
  # @yield [Hash] Log event entry
28
45
  def on_log(&block)
29
- @callbacks ||= []
30
- @callbacks << block
46
+ Fiber[:log_callbacks] ||= []
47
+ Fiber[:log_callbacks] << block
31
48
  end
32
49
 
33
50
  # Emit an event to all registered callbacks
34
51
  #
52
+ # Automatically adds a timestamp if one doesn't exist.
53
+ # Reads callbacks from Fiber-local storage to support multi-threaded execution.
54
+ #
35
55
  # @param entry [Hash] Log event entry
36
56
  # @return [void]
37
57
  def emit(entry)
38
- Array(@callbacks).each do |callback|
39
- callback.call(entry)
58
+ # Ensure timestamp exists (LogStream adds it, but direct calls might not)
59
+ # Use microsecond precision (6 digits) for proper event ordering
60
+ entry_with_timestamp = entry.key?(:timestamp) ? entry : entry.merge(timestamp: Time.now.utc.iso8601(6))
61
+
62
+ # Read callbacks from Fiber-local storage (set by on_log in parent fiber)
63
+ callbacks = Fiber[:log_callbacks] || []
64
+ callbacks.each do |callback|
65
+ callback.call(entry_with_timestamp)
40
66
  end
41
67
  end
42
68
 
@@ -44,7 +70,7 @@ module SwarmSDK
44
70
  #
45
71
  # @return [void]
46
72
  def reset!
47
- @callbacks = []
73
+ Fiber[:log_callbacks] = []
48
74
  end
49
75
  end
50
76
  end
@@ -16,9 +16,15 @@ module SwarmSDK
16
16
  # message_count: 5
17
17
  # )
18
18
  #
19
- # ## Fiber Safety
19
+ # ## Thread Safety
20
20
  #
21
- # LogStream is fiber-safe when following this pattern:
21
+ # LogStream is thread-safe and fiber-safe:
22
+ # - Uses Fiber storage for per-request isolation in multi-threaded servers (Puma, Sidekiq)
23
+ # - Each thread/request has its own emitter instance
24
+ # - Child fibers inherit the emitter from their parent fiber
25
+ # - No cross-thread contamination of log events
26
+ #
27
+ # Usage pattern:
22
28
  # 1. Set emitter BEFORE starting Async execution
23
29
  # 2. During Async execution, only emit() (reads emitter)
24
30
  # 3. Each event includes agent context for identification
@@ -35,34 +41,57 @@ module SwarmSDK
35
41
  # Emit a log event
36
42
  #
37
43
  # Adds timestamp and forwards to the registered emitter.
44
+ # Auto-injects execution_id, swarm_id, and parent_swarm_id from Fiber storage.
45
+ # Explicit values in data override auto-injected ones.
38
46
  #
39
47
  # @param data [Hash] Event data (type, agent, and event-specific fields)
40
48
  # @return [void]
41
49
  def emit(**data)
42
- return unless @emitter
50
+ emitter = Fiber[:log_stream_emitter]
51
+ return unless emitter
52
+
53
+ # Auto-inject execution context from Fiber storage
54
+ # Explicit values in data override auto-injected ones
55
+ auto_injected = {
56
+ execution_id: Fiber[:execution_id],
57
+ swarm_id: Fiber[:swarm_id],
58
+ parent_swarm_id: Fiber[:parent_swarm_id],
59
+ }.compact
43
60
 
44
- entry = data.merge(timestamp: Time.now.utc.iso8601).compact
61
+ entry = auto_injected.merge(data).merge(timestamp: Time.now.utc.iso8601(6)).compact
45
62
 
46
- @emitter.emit(entry)
63
+ emitter.emit(entry)
47
64
  end
48
65
 
49
66
  # Set the emitter (for dependency injection in tests)
50
67
  #
68
+ # Stores emitter in Fiber storage for thread-safe, per-request isolation.
69
+ #
51
70
  # @param emitter [#emit] Object responding to emit(Hash)
52
- attr_accessor :emitter
71
+ # @return [void]
72
+ def emitter=(emitter)
73
+ Fiber[:log_stream_emitter] = emitter
74
+ end
75
+
76
+ # Get the current emitter
77
+ #
78
+ # @return [#emit, nil] Current emitter or nil if not set
79
+ def emitter
80
+ Fiber[:log_stream_emitter]
81
+ end
53
82
 
54
83
  # Reset the emitter (for test cleanup)
55
84
  #
56
85
  # @return [void]
57
86
  def reset!
58
- @emitter = nil
87
+ Fiber[:log_stream_emitter] = nil
59
88
  end
60
89
 
61
90
  # Check if logging is enabled
62
91
  #
63
92
  # @return [Boolean] true if an emitter is configured
64
93
  def enabled?
65
- !@emitter.nil?
94
+ !Fiber[:log_stream_emitter].nil?
66
95
  end
67
96
  end
68
97
  end
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "sonnet": "claude-sonnet-4-5-20250929",
3
3
  "opus": "claude-opus-4-1-20250805",
4
- "haiku": "claude-haiku-4-5-20251001"
4
+ "haiku": "claude-haiku-4-5-20251001",
5
+ "claude-sonnet-4-5": "claude-sonnet-4-5-20250929",
6
+ "claude-opus-4-1": "claude-opus-4-1-20250805",
7
+ "claude-haiku-4-5": "claude-haiku-4-5-20251001"
5
8
  }
@@ -6,19 +6,32 @@ module SwarmSDK
6
6
  #
7
7
  # This class enables the chainable syntax:
8
8
  # agent(:backend).delegates_to(:tester, :database)
9
+ # agent(:backend, reset_context: false) # Preserve context across nodes
10
+ # agent(:backend).tools(:Read, :Edit) # Override tools for this node
9
11
  #
10
12
  # @example Basic delegation
11
13
  # agent(:backend).delegates_to(:tester)
12
14
  #
13
15
  # @example No delegation (solo agent)
14
16
  # agent(:planner)
17
+ #
18
+ # @example Preserve agent context
19
+ # agent(:architect, reset_context: false)
20
+ #
21
+ # @example Override tools for this node
22
+ # agent(:backend).tools(:Read, :Think)
23
+ #
24
+ # @example Combine delegation and tool override
25
+ # agent(:backend).delegates_to(:tester).tools(:Read, :Edit, :Write)
15
26
  class AgentConfig
16
27
  attr_reader :agent_name
17
28
 
18
- def initialize(agent_name, node_builder)
29
+ def initialize(agent_name, node_builder, reset_context: true)
19
30
  @agent_name = agent_name
20
31
  @node_builder = node_builder
21
32
  @delegates_to = []
33
+ @reset_context = reset_context
34
+ @tools = nil # nil means use global agent definition tools
22
35
  @finalized = false
23
36
  end
24
37
 
@@ -28,21 +41,38 @@ module SwarmSDK
28
41
  # @return [self] For method chaining
29
42
  def delegates_to(*agent_names)
30
43
  @delegates_to = agent_names.map(&:to_sym)
31
- finalize
44
+ update_registration
45
+ self
46
+ end
47
+
48
+ # Override tools for this agent in this node
49
+ #
50
+ # @param tool_names [Array<Symbol>] Tool names to use (overrides global agent definition)
51
+ # @return [self] For method chaining
52
+ #
53
+ # @example
54
+ # agent(:backend).tools(:Read, :Edit)
55
+ def tools(*tool_names)
56
+ @tools = tool_names.map(&:to_sym)
57
+ update_registration
32
58
  self
33
59
  end
34
60
 
35
- # Finalize agent configuration (called automatically)
61
+ # Update agent registration (called after each fluent method)
36
62
  #
37
- # Registers this agent configuration with the parent node builder.
38
- # If delegates_to was never called, registers with empty delegation.
63
+ # Always updates the registration with current state.
64
+ # This allows chaining: .delegates_to(...).tools(...)
39
65
  #
40
66
  # @return [void]
41
- def finalize
42
- return if @finalized
67
+ def update_registration
68
+ @node_builder.register_agent(@agent_name, @delegates_to, @reset_context, @tools)
69
+ end
43
70
 
44
- @node_builder.register_agent(@agent_name, @delegates_to)
45
- @finalized = true
71
+ # Finalize agent configuration (backward compatibility)
72
+ #
73
+ # @return [void]
74
+ def finalize
75
+ update_registration
46
76
  end
47
77
  end
48
78
  end