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.
Files changed (182) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. metadata +5 -184
  4. data/lib/claude_swarm/base_executor.rb +0 -133
  5. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  6. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  7. data/lib/claude_swarm/cli.rb +0 -697
  8. data/lib/claude_swarm/commands/ps.rb +0 -215
  9. data/lib/claude_swarm/commands/show.rb +0 -139
  10. data/lib/claude_swarm/configuration.rb +0 -373
  11. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  12. data/lib/claude_swarm/json_handler.rb +0 -91
  13. data/lib/claude_swarm/mcp_generator.rb +0 -230
  14. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  15. data/lib/claude_swarm/openai/executor.rb +0 -256
  16. data/lib/claude_swarm/openai/responses.rb +0 -319
  17. data/lib/claude_swarm/orchestrator.rb +0 -878
  18. data/lib/claude_swarm/process_tracker.rb +0 -78
  19. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  20. data/lib/claude_swarm/session_path.rb +0 -42
  21. data/lib/claude_swarm/settings_generator.rb +0 -77
  22. data/lib/claude_swarm/system_utils.rb +0 -46
  23. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  24. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  25. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  27. data/lib/claude_swarm/version.rb +0 -5
  28. data/lib/claude_swarm/worktree_manager.rb +0 -475
  29. data/lib/claude_swarm/yaml_loader.rb +0 -22
  30. data/lib/claude_swarm.rb +0 -67
  31. data/lib/swarm_cli/cli.rb +0 -201
  32. data/lib/swarm_cli/command_registry.rb +0 -61
  33. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  34. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  35. data/lib/swarm_cli/commands/migrate.rb +0 -55
  36. data/lib/swarm_cli/commands/run.rb +0 -173
  37. data/lib/swarm_cli/config_loader.rb +0 -98
  38. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  39. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  40. data/lib/swarm_cli/interactive_repl.rb +0 -924
  41. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  42. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  43. data/lib/swarm_cli/migrate_options.rb +0 -54
  44. data/lib/swarm_cli/migrator.rb +0 -132
  45. data/lib/swarm_cli/options.rb +0 -151
  46. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  47. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  48. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  49. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  50. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  51. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  52. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  53. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  54. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  55. data/lib/swarm_cli/ui/icons.rb +0 -36
  56. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  57. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  58. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  59. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  60. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  61. data/lib/swarm_cli/version.rb +0 -5
  62. data/lib/swarm_cli.rb +0 -46
  63. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  64. data/lib/swarm_sdk/agent/builder.rb +0 -552
  65. data/lib/swarm_sdk/agent/chat.rb +0 -774
  66. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  67. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  68. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  69. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  70. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  71. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  72. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  73. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  75. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  76. data/lib/swarm_sdk/agent/context.rb +0 -116
  77. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  78. data/lib/swarm_sdk/agent/definition.rb +0 -477
  79. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  80. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  81. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  82. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  83. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  84. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  85. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  86. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  87. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  88. data/lib/swarm_sdk/configuration.rb +0 -135
  89. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  90. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  91. data/lib/swarm_sdk/context_compactor.rb +0 -335
  92. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  93. data/lib/swarm_sdk/context_management/context.rb +0 -328
  94. data/lib/swarm_sdk/defaults.rb +0 -196
  95. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  96. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  97. data/lib/swarm_sdk/hooks/context.rb +0 -197
  98. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  99. data/lib/swarm_sdk/hooks/error.rb +0 -29
  100. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  101. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  102. data/lib/swarm_sdk/hooks/result.rb +0 -150
  103. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  104. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  105. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  106. data/lib/swarm_sdk/log_collector.rb +0 -227
  107. data/lib/swarm_sdk/log_stream.rb +0 -127
  108. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  109. data/lib/swarm_sdk/model_aliases.json +0 -8
  110. data/lib/swarm_sdk/models.json +0 -1
  111. data/lib/swarm_sdk/models.rb +0 -120
  112. data/lib/swarm_sdk/node_context.rb +0 -245
  113. data/lib/swarm_sdk/observer/builder.rb +0 -81
  114. data/lib/swarm_sdk/observer/config.rb +0 -45
  115. data/lib/swarm_sdk/observer/manager.rb +0 -236
  116. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  117. data/lib/swarm_sdk/permissions/config.rb +0 -239
  118. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  119. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  120. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  121. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  122. data/lib/swarm_sdk/plugin.rb +0 -309
  123. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  124. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  125. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  126. data/lib/swarm_sdk/restore_result.rb +0 -65
  127. data/lib/swarm_sdk/result.rb +0 -123
  128. data/lib/swarm_sdk/snapshot.rb +0 -156
  129. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  130. data/lib/swarm_sdk/state_restorer.rb +0 -476
  131. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  132. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  133. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  134. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  135. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  136. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  141. data/lib/swarm_sdk/swarm.rb +0 -717
  142. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  143. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  144. data/lib/swarm_sdk/tools/bash.rb +0 -282
  145. data/lib/swarm_sdk/tools/clock.rb +0 -44
  146. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  147. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  148. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  149. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  150. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  151. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  152. data/lib/swarm_sdk/tools/edit.rb +0 -145
  153. data/lib/swarm_sdk/tools/glob.rb +0 -166
  154. data/lib/swarm_sdk/tools/grep.rb +0 -235
  155. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  156. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
  157. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  158. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  159. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  160. data/lib/swarm_sdk/tools/read.rb +0 -261
  161. data/lib/swarm_sdk/tools/registry.rb +0 -205
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  165. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  166. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
  167. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  168. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  169. data/lib/swarm_sdk/tools/think.rb +0 -98
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/utils.rb +0 -68
  174. data/lib/swarm_sdk/validation_result.rb +0 -33
  175. data/lib/swarm_sdk/version.rb +0 -5
  176. data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
  177. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  178. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  179. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  180. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  181. data/lib/swarm_sdk/workflow.rb +0 -554
  182. data/lib/swarm_sdk.rb +0 -524
@@ -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