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,268 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Agent
5
- module ChatHelpers
6
- # Manages context tracking, delegation tracking, and logging callbacks
7
- #
8
- # Responsibilities:
9
- # - Register RubyLLM callbacks for logging
10
- # - Track tool executions
11
- # - Track delegations (which tool calls are delegations)
12
- # - Emit log events via LogStream
13
- # - Check context warnings
14
- #
15
- # This is a stateful helper that's instantiated per Agent::Chat instance.
16
- #
17
- # ## Thread Safety and Fiber-Local Storage
18
- #
19
- # IMPORTANT: LogStream.emit calls in this class DO NOT explicitly pass
20
- # swarm_id, parent_swarm_id, or execution_id. These values are automatically
21
- # injected from Fiber-local storage (Fiber[:swarm_id], etc.) by LogStream.emit.
22
- #
23
- # Why: In threaded environments (Puma, Sidekiq), swarm/agent instances may be
24
- # reused across multiple requests/jobs. If we explicitly pass @agent_context.swarm_id,
25
- # callbacks would use STALE values from the first request, causing events to be
26
- # lost or misattributed.
27
- #
28
- # By relying on Fiber-local storage, each request/job gets the correct context
29
- # even when reusing the same swarm instance. Fiber storage is set at the start
30
- # of Swarm#execute and inherited by child fibers (tool calls, delegations).
31
- #
32
- # This design works correctly in both:
33
- # - Single-threaded environments (rails runner, console)
34
- # - Multi-threaded environments (Puma, Sidekiq)
35
- class ContextTracker
36
- include LoggingHelpers
37
-
38
- attr_reader :agent_context
39
-
40
- def initialize(chat, agent_context)
41
- @chat = chat
42
- @agent_context = agent_context
43
- @tool_executions = []
44
- @finish_reason_override = nil
45
- end
46
-
47
- # Set a custom finish reason for the next agent_stop event
48
- #
49
- # This is used when finish_agent or finish_swarm terminates execution early.
50
- #
51
- # @param reason [String] Custom finish reason (e.g., "finish_agent", "finish_swarm")
52
- attr_writer :finish_reason_override
53
-
54
- # Setup logging callbacks
55
- #
56
- # Registers RubyLLM callbacks to collect data and emit log events.
57
- # Should only be called when LogStream.emitter is set.
58
- #
59
- # @return [void]
60
- def setup_logging
61
- register_logging_callbacks
62
- end
63
-
64
- # Extract agent name from delegation tool name
65
- #
66
- # Converts "#{Tools::Delegate::TOOL_NAME_PREFIX}[AgentName]" to "agent_name"
67
- # Example: "WorkWithWorker" -> "worker"
68
- #
69
- # @param tool_name [String] Delegation tool name
70
- # @return [String] Agent name
71
- def extract_delegate_agent_name(tool_name)
72
- # Remove tool name prefix and lowercase first letter
73
- agent_name = tool_name.to_s.sub(/^#{Tools::Delegate::TOOL_NAME_PREFIX}/, "")
74
- # Convert from PascalCase to lowercase (e.g., "Worker" -> "worker", "BackendDev" -> "backendDev")
75
- agent_name[0] = agent_name[0].downcase unless agent_name.empty?
76
- agent_name
77
- end
78
-
79
- private
80
-
81
- # Extract usage information from an assistant message
82
- #
83
- # @param message [RubyLLM::Message] Assistant message with usage data
84
- # @return [Hash] Usage information
85
- def extract_usage_info(message)
86
- cost_info = calculate_cost(message)
87
- context_usage = if @chat.respond_to?(:cumulative_input_tokens)
88
- {
89
- cumulative_input_tokens: @chat.cumulative_input_tokens,
90
- cumulative_output_tokens: @chat.cumulative_output_tokens,
91
- cumulative_total_tokens: @chat.cumulative_total_tokens,
92
- cumulative_cached_tokens: @chat.cumulative_cached_tokens,
93
- cumulative_cache_creation_tokens: @chat.cumulative_cache_creation_tokens,
94
- effective_input_tokens: @chat.effective_input_tokens,
95
- context_limit: @chat.context_limit,
96
- tokens_used_percentage: "#{@chat.context_usage_percentage}%",
97
- tokens_remaining: @chat.tokens_remaining,
98
- }
99
- else
100
- {}
101
- end
102
-
103
- {
104
- input_tokens: message.input_tokens,
105
- output_tokens: message.output_tokens,
106
- cached_tokens: message.cached_tokens,
107
- cache_creation_tokens: message.cache_creation_tokens,
108
- total_tokens: (message.input_tokens || 0) + (message.output_tokens || 0),
109
- input_cost: cost_info[:input_cost],
110
- output_cost: cost_info[:output_cost],
111
- total_cost: cost_info[:total_cost],
112
- }.merge(context_usage)
113
- end
114
-
115
- # Register RubyLLM chat callbacks to collect data and trigger logging
116
- #
117
- # This sets up low-level RubyLLM callbacks for technical plumbing (tracking state,
118
- # collecting tool results), then emits log events via LogStream.
119
- #
120
- # @return [void]
121
- def register_logging_callbacks
122
- # Collect tool execution results (technical plumbing)
123
- @chat.on_tool_result do |result|
124
- @tool_executions << {
125
- result: serialize_result(result),
126
- completed_at: Time.now.utc.iso8601,
127
- }
128
- end
129
-
130
- # Track delegations and emit agent_step/agent_stop events
131
- @chat.on_end_message do |message|
132
- next unless message
133
-
134
- case message.role
135
- when :assistant
136
- if message.tool_call?
137
- # Assistant made tool calls - emit agent_step event
138
- trigger_agent_step(message, tool_executions: @tool_executions) if @chat.hook_executor
139
- @tool_executions.clear
140
- elsif @chat.hook_executor
141
- # Final response (finish_reason: "stop") - fire agent_stop
142
- trigger_agent_stop(message, tool_executions: @tool_executions)
143
- end
144
-
145
- # Check context warnings after each assistant message
146
- # Uses unified implementation in HookIntegration
147
- @chat.check_context_warnings if @chat.respond_to?(:check_context_warnings)
148
- when :tool
149
- # Handle delegation tracking and logging (technical plumbing)
150
- if @agent_context.delegation?(call_id: message.tool_call_id)
151
- delegate_from = @agent_context.delegation_target(call_id: message.tool_call_id)
152
-
153
- # Emit delegation result log event
154
- LogStream.emit(
155
- type: "delegation_result",
156
- agent: @agent_context.name,
157
- delegate_from: delegate_from,
158
- tool_call_id: message.tool_call_id,
159
- result: serialize_result(message.content),
160
- metadata: @agent_context.metadata,
161
- )
162
-
163
- @agent_context.clear_delegation(call_id: message.tool_call_id)
164
- end
165
- end
166
- end
167
-
168
- # Track delegations when tool calls are made
169
- @chat.on_tool_call do |tool_call|
170
- if @agent_context.delegation_tool?(tool_call.name)
171
- # Extract agent name from tool name (DelegateTaskTo[AgentName] -> agent_name)
172
- agent_name = extract_delegate_agent_name(tool_call.name)
173
-
174
- @agent_context.track_delegation(call_id: tool_call.id, target: agent_name)
175
-
176
- # Emit delegation log event
177
- LogStream.emit(
178
- type: "agent_delegation",
179
- agent: @agent_context.name,
180
- tool_call_id: tool_call.id,
181
- delegate_to: agent_name,
182
- arguments: tool_call.arguments,
183
- metadata: @agent_context.metadata,
184
- )
185
- end
186
- end
187
- end
188
-
189
- # Trigger agent_step callback
190
- #
191
- # This fires when the agent makes an intermediate response with tool calls.
192
- # The agent hasn't finished yet - it's requesting tools to continue processing.
193
- #
194
- # @param message [RubyLLM::Message] Assistant message with tool calls
195
- # @param tool_executions [Array<Hash>] Tool execution results (should be empty for steps)
196
- # @return [void]
197
- def trigger_agent_step(message, tool_executions: [])
198
- return unless @chat.hook_executor
199
-
200
- usage_info = extract_usage_info(message)
201
-
202
- context = Hooks::Context.new(
203
- event: :agent_step,
204
- agent_name: @agent_context.name,
205
- swarm: @chat.hook_swarm,
206
- metadata: {
207
- model: message.model_id,
208
- content: message.content,
209
- tool_calls: format_tool_calls(message.tool_calls),
210
- finish_reason: "tool_calls",
211
- usage: usage_info,
212
- tool_executions: tool_executions.empty? ? nil : tool_executions,
213
- timestamp: Time.now.utc.iso8601,
214
- },
215
- )
216
-
217
- agent_hooks = @chat.hook_agent_hooks[:agent_step] || []
218
-
219
- @chat.hook_executor.execute_safe(
220
- event: :agent_step,
221
- context: context,
222
- callbacks: agent_hooks,
223
- )
224
- end
225
-
226
- # Trigger agent_stop callback
227
- #
228
- # This fires when the agent completes with a final response (no more tool calls).
229
- #
230
- # @param message [RubyLLM::Message] Assistant message with final content
231
- # @param tool_executions [Array<Hash>] Tool execution results (if any)
232
- # @return [void]
233
- def trigger_agent_stop(message, tool_executions: [])
234
- return unless @chat.hook_executor
235
-
236
- usage_info = extract_usage_info(message)
237
-
238
- # Use override if set (e.g., "finish_agent"), otherwise default to "stop"
239
- finish_reason = @finish_reason_override || "stop"
240
- @finish_reason_override = nil # Clear after use
241
-
242
- context = Hooks::Context.new(
243
- event: :agent_stop,
244
- agent_name: @agent_context.name,
245
- swarm: @chat.hook_swarm,
246
- metadata: {
247
- model: message.model_id,
248
- content: message.content,
249
- tool_calls: nil, # Final response has no tool calls
250
- finish_reason: finish_reason,
251
- usage: usage_info,
252
- tool_executions: tool_executions.empty? ? nil : tool_executions,
253
- timestamp: Time.now.utc.iso8601,
254
- },
255
- )
256
-
257
- agent_hooks = @chat.hook_agent_hooks[:agent_stop] || []
258
-
259
- @chat.hook_executor.execute_safe(
260
- event: :agent_stop,
261
- context: context,
262
- callbacks: agent_hooks,
263
- )
264
- end
265
- end
266
- end
267
- end
268
- end
@@ -1,204 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "monitor"
4
-
5
- module SwarmSDK
6
- module Agent
7
- module ChatHelpers
8
- # Minimal event emitter that mirrors RubyLLM::Chat's callback pattern
9
- #
10
- # Provides multi-subscriber support for events like tool_call, tool_result,
11
- # new_message, end_message. This is thread-safe and supports unsubscription.
12
- module EventEmitter
13
- # Represents an active subscription to a callback event.
14
- # Returned by {#subscribe} and can be used to unsubscribe later.
15
- class Subscription
16
- attr_reader :tag
17
-
18
- def initialize(callback_list, callback, monitor:, tag: nil)
19
- @callback_list = callback_list
20
- @callback = callback
21
- @monitor = monitor
22
- @tag = tag
23
- @active = true
24
- end
25
-
26
- # Removes this subscription from the callback list.
27
- # @return [Boolean] true if successfully unsubscribed, false if already inactive
28
- def unsubscribe # rubocop:disable Naming/PredicateMethod
29
- @monitor.synchronize do
30
- return false unless @active
31
-
32
- @callback_list.delete(@callback)
33
- @active = false
34
- end
35
- true
36
- end
37
-
38
- # Checks if this subscription is still active.
39
- # @return [Boolean] true if still subscribed
40
- def active?
41
- @monitor.synchronize do
42
- @active && @callback_list.include?(@callback)
43
- end
44
- end
45
-
46
- def inspect
47
- "#<#{self.class.name} tag=#{@tag.inspect} active=#{active?}>"
48
- end
49
- end
50
-
51
- # Initialize the event emitter system
52
- #
53
- # Sets up @callbacks hash and @callback_monitor for thread safety.
54
- # Must be called in Chat#initialize.
55
- #
56
- # @return [void]
57
- def initialize_event_emitter
58
- @callbacks = {
59
- new_message: [],
60
- end_message: [],
61
- tool_call: [],
62
- tool_result: [],
63
- }
64
- @callback_monitor = Monitor.new
65
- end
66
-
67
- # Subscribes to an event with the given block.
68
- # Returns a {Subscription} that can be used to unsubscribe.
69
- #
70
- # @param event [Symbol] The event to subscribe to
71
- # @param tag [String, nil] Optional tag for debugging/identification
72
- # @yield The block to call when the event fires
73
- # @return [Subscription] An object that can be used to unsubscribe
74
- # @raise [ArgumentError] if event is not recognized
75
- def subscribe(event, tag: nil, &block)
76
- @callback_monitor.synchronize do
77
- unless @callbacks.key?(event)
78
- raise ArgumentError, "Unknown event: #{event}. Valid events: #{@callbacks.keys.join(", ")}"
79
- end
80
-
81
- @callbacks[event] << block
82
- Subscription.new(@callbacks[event], block, monitor: @callback_monitor, tag: tag)
83
- end
84
- end
85
-
86
- # Subscribes to an event that automatically unsubscribes after firing once.
87
- #
88
- # @param event [Symbol] The event to subscribe to
89
- # @param tag [String, nil] Optional tag for debugging/identification
90
- # @yield The block to call when the event fires (once)
91
- # @return [Subscription] An object that can be used to unsubscribe before it fires
92
- def once(event, tag: nil, &block)
93
- subscription = nil
94
- wrapper = lambda do |*args|
95
- subscription&.unsubscribe
96
- block.call(*args)
97
- end
98
- subscription = subscribe(event, tag: tag, &wrapper)
99
- end
100
-
101
- # Registers a callback for when a new message starts being generated.
102
- # Multiple callbacks can be registered and all will fire in registration order.
103
- #
104
- # @yield Block called when a new message starts
105
- # @return [self] for chaining
106
- def on_new_message(&block)
107
- subscribe(:new_message, &block)
108
- self
109
- end
110
-
111
- # Registers a callback for when a message is complete.
112
- # Multiple callbacks can be registered and all will fire in registration order.
113
- #
114
- # @yield [Message] Block called with the completed message
115
- # @return [self] for chaining
116
- def on_end_message(&block)
117
- subscribe(:end_message, &block)
118
- self
119
- end
120
-
121
- # Registers a callback for when a tool is called.
122
- # Multiple callbacks can be registered and all will fire in registration order.
123
- #
124
- # @yield [ToolCall] Block called with the tool call object
125
- # @return [self] for chaining
126
- def on_tool_call(&block)
127
- subscribe(:tool_call, &block)
128
- self
129
- end
130
-
131
- # Registers a callback for when a tool returns a result.
132
- # Multiple callbacks can be registered and all will fire in registration order.
133
- #
134
- # @yield [Object] Block called with the tool result
135
- # @return [self] for chaining
136
- def on_tool_result(&block)
137
- subscribe(:tool_result, &block)
138
- self
139
- end
140
-
141
- # Clears all callbacks for the specified event, or all events if none specified.
142
- #
143
- # @param event [Symbol, nil] The event to clear callbacks for, or nil for all events
144
- # @return [self] for chaining
145
- def clear_callbacks(event = nil)
146
- @callback_monitor.synchronize do
147
- if event
148
- @callbacks[event]&.clear
149
- else
150
- @callbacks.each_value(&:clear)
151
- end
152
- end
153
- self
154
- end
155
-
156
- # Returns the number of callbacks registered for the specified event.
157
- #
158
- # @param event [Symbol, nil] The event to count callbacks for, or nil for all events
159
- # @return [Integer, Hash] Count for specific event, or hash of counts for all events
160
- def callback_count(event = nil)
161
- @callback_monitor.synchronize do
162
- if event
163
- @callbacks[event]&.size || 0
164
- else
165
- @callbacks.transform_values(&:size)
166
- end
167
- end
168
- end
169
-
170
- private
171
-
172
- # Emits an event to all registered subscribers.
173
- # Callbacks are executed in registration order (FIFO).
174
- # Errors in callbacks are isolated - one failing callback doesn't prevent others from running.
175
- #
176
- # @param event [Symbol] The event to emit
177
- # @param args [Array] Arguments to pass to each callback
178
- # @return [void]
179
- def emit(event, *args)
180
- # Snapshot callbacks under lock (fast operation)
181
- callbacks = @callback_monitor.synchronize { @callbacks[event]&.dup || [] }
182
-
183
- # Execute callbacks outside lock (safe, non-blocking)
184
- callbacks.each do |callback|
185
- callback.call(*args)
186
- rescue StandardError => e
187
- handle_callback_error(event, callback, e)
188
- end
189
- end
190
-
191
- # Hook for custom error handling when a callback raises an exception.
192
- # Override this method in Chat to customize error behavior.
193
- #
194
- # @param event [Symbol] The event that was being emitted
195
- # @param callback [Proc] The callback that raised the error
196
- # @param error [StandardError] The error that was raised
197
- # @return [void]
198
- def handle_callback_error(event, _callback, error)
199
- warn("[SwarmSDK] Callback error in #{event}: #{error.class} - #{error.message}")
200
- end
201
- end
202
- end
203
- end
204
- end