swarm_sdk 2.7.13 → 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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +43 -22
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +6 -0
  4. data/lib/swarm_sdk/ruby_llm_patches/mcp_ssl_patch.rb +144 -0
  5. data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +3 -4
  6. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  7. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  8. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  9. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  10. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  11. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  12. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  13. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  14. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  15. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  16. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  17. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  18. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  19. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  20. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  24. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  25. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  26. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  27. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  28. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  29. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  30. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  31. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  32. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  33. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  34. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  35. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  36. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  37. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  38. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  39. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  40. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  41. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  42. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  43. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  45. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  46. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  47. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  48. data/lib/swarm_sdk/v3/tools/read.rb +181 -0
  49. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  50. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  51. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  52. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  53. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  54. data/lib/swarm_sdk/v3.rb +145 -0
  55. metadata +84 -148
  56. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  57. data/lib/swarm_sdk/agent/builder.rb +0 -680
  58. data/lib/swarm_sdk/agent/chat.rb +0 -1432
  59. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  60. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  61. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  62. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  63. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  64. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  65. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  66. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  67. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  68. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  69. data/lib/swarm_sdk/agent/context.rb +0 -115
  70. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  71. data/lib/swarm_sdk/agent/definition.rb +0 -581
  72. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  73. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  74. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  75. data/lib/swarm_sdk/agent_registry.rb +0 -146
  76. data/lib/swarm_sdk/builders/base_builder.rb +0 -553
  77. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  78. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  79. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  80. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  81. data/lib/swarm_sdk/config.rb +0 -367
  82. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  83. data/lib/swarm_sdk/configuration/translator.rb +0 -283
  84. data/lib/swarm_sdk/configuration.rb +0 -165
  85. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  86. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  87. data/lib/swarm_sdk/context_compactor.rb +0 -335
  88. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  89. data/lib/swarm_sdk/context_management/context.rb +0 -328
  90. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  91. data/lib/swarm_sdk/defaults.rb +0 -251
  92. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  93. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  94. data/lib/swarm_sdk/hooks/context.rb +0 -197
  95. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  96. data/lib/swarm_sdk/hooks/error.rb +0 -29
  97. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  98. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  99. data/lib/swarm_sdk/hooks/result.rb +0 -150
  100. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  101. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  102. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  103. data/lib/swarm_sdk/log_collector.rb +0 -227
  104. data/lib/swarm_sdk/log_stream.rb +0 -127
  105. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  106. data/lib/swarm_sdk/model_aliases.json +0 -8
  107. data/lib/swarm_sdk/models.json +0 -44002
  108. data/lib/swarm_sdk/models.rb +0 -161
  109. data/lib/swarm_sdk/node_context.rb +0 -245
  110. data/lib/swarm_sdk/observer/builder.rb +0 -81
  111. data/lib/swarm_sdk/observer/config.rb +0 -45
  112. data/lib/swarm_sdk/observer/manager.rb +0 -236
  113. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  114. data/lib/swarm_sdk/permissions/config.rb +0 -239
  115. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  116. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  117. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  118. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  119. data/lib/swarm_sdk/plugin.rb +0 -309
  120. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  121. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  122. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  123. data/lib/swarm_sdk/restore_result.rb +0 -65
  124. data/lib/swarm_sdk/result.rb +0 -212
  125. data/lib/swarm_sdk/snapshot.rb +0 -156
  126. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  127. data/lib/swarm_sdk/state_restorer.rb +0 -476
  128. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  129. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  130. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -195
  131. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  132. data/lib/swarm_sdk/swarm/executor.rb +0 -290
  133. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -151
  134. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  135. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -360
  136. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -270
  137. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  138. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  139. data/lib/swarm_sdk/swarm.rb +0 -843
  140. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  141. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  142. data/lib/swarm_sdk/tools/base.rb +0 -63
  143. data/lib/swarm_sdk/tools/bash.rb +0 -280
  144. data/lib/swarm_sdk/tools/clock.rb +0 -46
  145. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  146. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  147. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  148. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  149. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  150. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  151. data/lib/swarm_sdk/tools/edit.rb +0 -145
  152. data/lib/swarm_sdk/tools/glob.rb +0 -166
  153. data/lib/swarm_sdk/tools/grep.rb +0 -235
  154. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  155. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  156. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  157. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  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 -273
  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 -100
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  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 -95
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  182. data/lib/swarm_sdk/workflow.rb +0 -589
  183. data/lib/swarm_sdk.rb +0 -718
@@ -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