swarm_sdk 2.7.14 → 3.0.0.alpha1

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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
  4. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  5. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  6. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  7. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  8. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  9. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  10. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  11. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  12. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  13. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  14. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  15. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  16. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  17. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  18. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  19. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  20. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  24. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  25. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  26. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  27. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  28. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  29. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  30. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  31. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  32. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  33. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  34. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  35. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  36. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  37. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  38. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  39. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  40. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  41. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  42. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  43. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  44. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  45. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  46. data/lib/swarm_sdk/v3/tools/read.rb +181 -0
  47. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  48. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  49. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  50. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  51. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  52. data/lib/swarm_sdk/v3.rb +145 -0
  53. metadata +83 -148
  54. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  55. data/lib/swarm_sdk/agent/builder.rb +0 -705
  56. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  57. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  58. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  59. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  60. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  61. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  62. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  63. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  64. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  65. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  66. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  67. data/lib/swarm_sdk/agent/context.rb +0 -115
  68. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  69. data/lib/swarm_sdk/agent/definition.rb +0 -588
  70. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  71. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  72. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  73. data/lib/swarm_sdk/agent_registry.rb +0 -146
  74. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  75. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  76. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  77. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  78. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  79. data/lib/swarm_sdk/config.rb +0 -368
  80. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  81. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  82. data/lib/swarm_sdk/configuration.rb +0 -165
  83. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  84. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  85. data/lib/swarm_sdk/context_compactor.rb +0 -335
  86. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  87. data/lib/swarm_sdk/context_management/context.rb +0 -328
  88. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  89. data/lib/swarm_sdk/defaults.rb +0 -251
  90. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  91. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  92. data/lib/swarm_sdk/hooks/context.rb +0 -197
  93. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  94. data/lib/swarm_sdk/hooks/error.rb +0 -29
  95. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  96. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  97. data/lib/swarm_sdk/hooks/result.rb +0 -150
  98. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  99. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  100. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  101. data/lib/swarm_sdk/log_collector.rb +0 -227
  102. data/lib/swarm_sdk/log_stream.rb +0 -127
  103. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  104. data/lib/swarm_sdk/model_aliases.json +0 -8
  105. data/lib/swarm_sdk/models.json +0 -44002
  106. data/lib/swarm_sdk/models.rb +0 -161
  107. data/lib/swarm_sdk/node_context.rb +0 -245
  108. data/lib/swarm_sdk/observer/builder.rb +0 -81
  109. data/lib/swarm_sdk/observer/config.rb +0 -45
  110. data/lib/swarm_sdk/observer/manager.rb +0 -248
  111. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  112. data/lib/swarm_sdk/permissions/config.rb +0 -239
  113. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  114. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  115. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  116. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  117. data/lib/swarm_sdk/plugin.rb +0 -309
  118. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  119. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  120. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  121. data/lib/swarm_sdk/restore_result.rb +0 -65
  122. data/lib/swarm_sdk/result.rb +0 -241
  123. data/lib/swarm_sdk/snapshot.rb +0 -156
  124. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  125. data/lib/swarm_sdk/state_restorer.rb +0 -476
  126. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  127. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  128. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  129. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  130. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  131. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  132. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  133. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  134. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  135. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  136. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  137. data/lib/swarm_sdk/swarm.rb +0 -973
  138. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  139. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  140. data/lib/swarm_sdk/tools/base.rb +0 -63
  141. data/lib/swarm_sdk/tools/bash.rb +0 -280
  142. data/lib/swarm_sdk/tools/clock.rb +0 -46
  143. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  144. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  145. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  146. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  147. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  148. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  149. data/lib/swarm_sdk/tools/edit.rb +0 -145
  150. data/lib/swarm_sdk/tools/glob.rb +0 -166
  151. data/lib/swarm_sdk/tools/grep.rb +0 -235
  152. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  153. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  154. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  155. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  156. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  157. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  158. data/lib/swarm_sdk/tools/read.rb +0 -261
  159. data/lib/swarm_sdk/tools/registry.rb +0 -205
  160. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  161. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  163. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  164. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  165. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  166. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  167. data/lib/swarm_sdk/tools/think.rb +0 -100
  168. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  169. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  170. data/lib/swarm_sdk/tools/write.rb +0 -112
  171. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  172. data/lib/swarm_sdk/utils.rb +0 -68
  173. data/lib/swarm_sdk/validation_result.rb +0 -33
  174. data/lib/swarm_sdk/version.rb +0 -5
  175. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  176. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  177. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  178. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  179. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  180. data/lib/swarm_sdk/workflow.rb +0 -589
  181. data/lib/swarm_sdk.rb +0 -721
@@ -1,375 +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
- # This method is idempotent - calling it multiple times has no effect.
59
- #
60
- # @return [void]
61
- def setup_logging
62
- return if @logging_setup
63
-
64
- register_logging_callbacks
65
- @logging_setup = true
66
- end
67
-
68
- # Extract agent name from delegation tool name
69
- #
70
- # Converts "#{Tools::Delegate::TOOL_NAME_PREFIX}[AgentName]" to "agent_name"
71
- # Example: "WorkWithWorker" -> "worker"
72
- #
73
- # @param tool_name [String] Delegation tool name
74
- # @return [String] Agent name
75
- def extract_delegate_agent_name(tool_name)
76
- # Remove tool name prefix and lowercase first letter
77
- agent_name = tool_name.to_s.sub(/^#{Tools::Delegate::TOOL_NAME_PREFIX}/, "")
78
- # Convert from PascalCase to lowercase (e.g., "Worker" -> "worker", "BackendDev" -> "backendDev")
79
- agent_name[0] = agent_name[0].downcase unless agent_name.empty?
80
- agent_name
81
- end
82
-
83
- private
84
-
85
- # Format citations for appending to response content
86
- #
87
- # Creates a markdown-formatted citations section with numbered links.
88
- #
89
- # @param citations [Array<String>] Array of citation URLs
90
- # @return [String] Formatted citations section
91
- def format_citations(citations)
92
- return "" if citations.nil? || citations.empty?
93
-
94
- formatted = "\n\n# Citations\n"
95
- citations.each_with_index do |citation, index|
96
- formatted += "- [#{index + 1}] #{citation}\n"
97
- end
98
- formatted
99
- end
100
-
101
- # Emit citations as a content_chunk event
102
- #
103
- # @param formatted_citations [String] Formatted citations text
104
- # @param model_id [String] Model identifier
105
- # @return [void]
106
- def emit_citations_chunk(formatted_citations, model_id)
107
- LogStream.emit(
108
- type: "content_chunk",
109
- agent: @agent_context.name,
110
- chunk_type: "citations",
111
- content: formatted_citations,
112
- tool_calls: nil,
113
- model: model_id,
114
- )
115
- rescue StandardError => e
116
- RubyLLM.logger.debug("SwarmSDK: Failed to emit citations chunk: #{e.message}")
117
- end
118
-
119
- # Extract citations and search results from an assistant message
120
- #
121
- # These fields are provided by some LLM providers (e.g., Perplexity's sonar models)
122
- # when the model performs web search or cites sources.
123
- #
124
- # @param message [RubyLLM::Message] Assistant message with potential citations
125
- # @return [Hash] Citations and search results (empty if not present)
126
- def extract_citations_and_search(message)
127
- return {} unless message.raw&.body
128
-
129
- body = message.raw.body
130
-
131
- # For streaming responses, body might be empty - check Fiber-local
132
- # (set by LLMInstrumentationMiddleware with accumulated SSE chunks)
133
- if body.is_a?(String) && body.empty?
134
- fiber_body = Fiber[:last_sse_body]
135
- body = fiber_body if fiber_body
136
- end
137
-
138
- return {} unless body
139
-
140
- # Handle SSE streaming responses (body is a string starting with "data:")
141
- if body.is_a?(String) && body.start_with?("data:")
142
- # Parse the LAST SSE event which contains citations
143
- last_data_line = body.split("\n").reverse.find { |l| l.start_with?("data:") && !l.include?("[DONE]") && !l.include?("message_stop") }
144
- if last_data_line
145
- body = JSON.parse(last_data_line.sub(/^data:\s*/, ""))
146
- end
147
- elsif body.is_a?(String)
148
- # Regular JSON string response
149
- body = JSON.parse(body)
150
- end
151
-
152
- # Handle Faraday::Response objects (has .body method)
153
- body = body.body if body.respond_to?(:body) && !body.is_a?(Hash)
154
-
155
- return {} unless body.is_a?(Hash)
156
-
157
- result = {}
158
- result[:citations] = body["citations"] if body["citations"]
159
- result[:search_results] = body["search_results"] if body["search_results"]
160
- result
161
- rescue StandardError => e
162
- # Includes JSON::ParserError and other parsing errors
163
- RubyLLM.logger.debug("SwarmSDK: Failed to extract citations: #{e.message}")
164
- {}
165
- end
166
-
167
- # Extract usage information from an assistant message
168
- #
169
- # @param message [RubyLLM::Message] Assistant message with usage data
170
- # @return [Hash] Usage information
171
- def extract_usage_info(message)
172
- cost_info = calculate_cost(message)
173
- context_usage = if @chat.respond_to?(:cumulative_input_tokens)
174
- {
175
- cumulative_input_tokens: @chat.cumulative_input_tokens,
176
- cumulative_output_tokens: @chat.cumulative_output_tokens,
177
- cumulative_total_tokens: @chat.cumulative_total_tokens,
178
- cumulative_cached_tokens: @chat.cumulative_cached_tokens,
179
- cumulative_cache_creation_tokens: @chat.cumulative_cache_creation_tokens,
180
- effective_input_tokens: @chat.effective_input_tokens,
181
- context_limit: @chat.context_limit,
182
- tokens_used_percentage: "#{@chat.context_usage_percentage}%",
183
- tokens_remaining: @chat.tokens_remaining,
184
- }
185
- else
186
- {}
187
- end
188
-
189
- {
190
- input_tokens: message.input_tokens,
191
- output_tokens: message.output_tokens,
192
- cached_tokens: message.cached_tokens,
193
- cache_creation_tokens: message.cache_creation_tokens,
194
- total_tokens: (message.input_tokens || 0) + (message.output_tokens || 0),
195
- input_cost: cost_info[:input_cost],
196
- output_cost: cost_info[:output_cost],
197
- total_cost: cost_info[:total_cost],
198
- }.merge(context_usage)
199
- end
200
-
201
- # Register RubyLLM chat callbacks to collect data and trigger logging
202
- #
203
- # This sets up low-level RubyLLM callbacks for technical plumbing (tracking state,
204
- # collecting tool results), then emits log events via LogStream.
205
- #
206
- # @return [void]
207
- def register_logging_callbacks
208
- # Collect tool execution results (technical plumbing)
209
- @chat.on_tool_result do |result|
210
- @tool_executions << {
211
- result: serialize_result(result),
212
- completed_at: Time.now.utc.iso8601,
213
- }
214
- end
215
-
216
- # Track delegations and emit agent_step/agent_stop events
217
- @chat.on_end_message do |message|
218
- next unless message
219
-
220
- case message.role
221
- when :assistant
222
- if message.tool_call?
223
- # Assistant made tool calls - emit agent_step event
224
- trigger_agent_step(message, tool_executions: @tool_executions) if @chat.hook_executor
225
- @tool_executions.clear
226
- elsif @chat.hook_executor
227
- # Final response (finish_reason: "stop") - fire agent_stop
228
- trigger_agent_stop(message, tool_executions: @tool_executions)
229
- end
230
-
231
- # Check context warnings after each assistant message
232
- # Uses unified implementation in HookIntegration
233
- @chat.check_context_warnings if @chat.respond_to?(:check_context_warnings)
234
- when :tool
235
- # Handle delegation tracking and logging (technical plumbing)
236
- if @agent_context.delegation?(call_id: message.tool_call_id)
237
- delegate_from = @agent_context.delegation_target(call_id: message.tool_call_id)
238
-
239
- # Emit delegation result log event
240
- LogStream.emit(
241
- type: "delegation_result",
242
- agent: @agent_context.name,
243
- delegate_from: delegate_from,
244
- tool_call_id: message.tool_call_id,
245
- result: serialize_result(message.content),
246
- metadata: @agent_context.metadata,
247
- )
248
-
249
- @agent_context.clear_delegation(call_id: message.tool_call_id)
250
- end
251
- end
252
- end
253
-
254
- # Track delegations when tool calls are made
255
- @chat.on_tool_call do |tool_call|
256
- if @agent_context.delegation_tool?(tool_call.name)
257
- # Extract agent name from tool name (DelegateTaskTo[AgentName] -> agent_name)
258
- agent_name = extract_delegate_agent_name(tool_call.name)
259
-
260
- @agent_context.track_delegation(call_id: tool_call.id, target: agent_name)
261
-
262
- # Emit delegation log event
263
- LogStream.emit(
264
- type: "agent_delegation",
265
- agent: @agent_context.name,
266
- tool_call_id: tool_call.id,
267
- delegate_to: agent_name,
268
- arguments: tool_call.arguments,
269
- metadata: @agent_context.metadata,
270
- )
271
- end
272
- end
273
- end
274
-
275
- # Trigger agent_step callback
276
- #
277
- # This fires when the agent makes an intermediate response with tool calls.
278
- # The agent hasn't finished yet - it's requesting tools to continue processing.
279
- #
280
- # @param message [RubyLLM::Message] Assistant message with tool calls
281
- # @param tool_executions [Array<Hash>] Tool execution results (should be empty for steps)
282
- # @return [void]
283
- def trigger_agent_step(message, tool_executions: [])
284
- return unless @chat.hook_executor
285
-
286
- usage_info = extract_usage_info(message)
287
- citations_data = extract_citations_and_search(message)
288
-
289
- context = Hooks::Context.new(
290
- event: :agent_step,
291
- agent_name: @agent_context.name,
292
- swarm: @chat.hook_swarm,
293
- metadata: {
294
- model: message.model_id,
295
- content: message.content,
296
- tool_calls: format_tool_calls(message.tool_calls),
297
- finish_reason: "tool_calls",
298
- usage: usage_info,
299
- citations: citations_data[:citations],
300
- search_results: citations_data[:search_results],
301
- tool_executions: tool_executions.empty? ? nil : tool_executions,
302
- timestamp: Time.now.utc.iso8601,
303
- }.compact,
304
- )
305
-
306
- agent_hooks = @chat.hook_agent_hooks[:agent_step] || []
307
-
308
- @chat.hook_executor.execute_safe(
309
- event: :agent_step,
310
- context: context,
311
- callbacks: agent_hooks,
312
- )
313
- end
314
-
315
- # Trigger agent_stop callback
316
- #
317
- # This fires when the agent completes with a final response (no more tool calls).
318
- #
319
- # @param message [RubyLLM::Message] Assistant message with final content
320
- # @param tool_executions [Array<Hash>] Tool execution results (if any)
321
- # @return [void]
322
- def trigger_agent_stop(message, tool_executions: [])
323
- return unless @chat.hook_executor
324
-
325
- usage_info = extract_usage_info(message)
326
- citations_data = extract_citations_and_search(message)
327
-
328
- # Format content with citations appended
329
- content_with_citations = message.content
330
- if citations_data[:citations] && !citations_data[:citations].empty?
331
- formatted_citations = format_citations(citations_data[:citations])
332
- content_with_citations = message.content + formatted_citations
333
-
334
- # Also modify the original message for Result.content
335
- message.content = content_with_citations
336
-
337
- # Emit citations chunk if streaming is enabled
338
- if @chat.streaming_enabled?
339
- emit_citations_chunk(formatted_citations, message.model_id)
340
- end
341
- end
342
-
343
- # Use override if set (e.g., "finish_agent"), otherwise default to "stop"
344
- finish_reason = @finish_reason_override || "stop"
345
- @finish_reason_override = nil # Clear after use
346
-
347
- context = Hooks::Context.new(
348
- event: :agent_stop,
349
- agent_name: @agent_context.name,
350
- swarm: @chat.hook_swarm,
351
- metadata: {
352
- model: message.model_id,
353
- content: content_with_citations, # Content with citations appended
354
- tool_calls: nil, # Final response has no tool calls
355
- finish_reason: finish_reason,
356
- usage: usage_info,
357
- citations: citations_data[:citations],
358
- search_results: citations_data[:search_results],
359
- tool_executions: tool_executions.empty? ? nil : tool_executions,
360
- timestamp: Time.now.utc.iso8601,
361
- }.compact,
362
- )
363
-
364
- agent_hooks = @chat.hook_agent_hooks[:agent_stop] || []
365
-
366
- @chat.hook_executor.execute_safe(
367
- event: :agent_stop,
368
- context: context,
369
- callbacks: agent_hooks,
370
- )
371
- end
372
- end
373
- end
374
- end
375
- 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