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,389 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # Delegate tool for working with other agents in the swarm
6
- #
7
- # Creates agent-specific collaboration tools (e.g., WorkWithBackend)
8
- # that allow one agent to work with another agent.
9
- # Supports pre/post delegation hooks for customization.
10
- class Delegate < Base
11
- removable true # Delegate tools can be controlled by skills
12
- # Tool name prefix for delegation tools
13
- # Change this to customize the tool naming pattern (e.g., "DelegateTaskTo", "AskAgent", etc.)
14
- TOOL_NAME_PREFIX = "WorkWith"
15
-
16
- class << self
17
- # Generate tool name for a delegate agent
18
- #
19
- # This is the single source of truth for delegation tool naming.
20
- # Used both when creating Delegate instances and when predicting tool names
21
- # for agent context setup.
22
- #
23
- # Converts names to PascalCase: backend → Backend, slack_agent → SlackAgent
24
- #
25
- # @param delegate_name [String, Symbol] Name of the delegate agent
26
- # @return [String] Tool name (e.g., "WorkWithBackend", "WorkWithSlackAgent")
27
- #
28
- # @example Simple name
29
- # tool_name_for(:backend) # => "WorkWithBackend"
30
- #
31
- # @example Name with underscore
32
- # tool_name_for(:slack_agent) # => "WorkWithSlackAgent"
33
- def tool_name_for(delegate_name)
34
- # Convert to PascalCase: split on underscore, capitalize each part, join
35
- pascal_case = delegate_name.to_s.split("_").map(&:capitalize).join
36
- "#{TOOL_NAME_PREFIX}#{pascal_case}"
37
- end
38
- end
39
-
40
- attr_reader :delegate_name, :delegate_target, :tool_name, :preserve_context, :delegate_chat
41
-
42
- # Initialize a delegation tool
43
- #
44
- # @param delegate_name [String] Name of the delegate agent (e.g., "backend")
45
- # @param delegate_description [String] Description of the delegate agent
46
- # @param delegate_chat [AgentChat, nil] The chat instance for the delegate agent (nil if delegating to swarm)
47
- # @param agent_name [Symbol, String] Name of the agent using this tool
48
- # @param swarm [Swarm] The swarm instance (provides hook_registry, swarm_registry)
49
- # @param delegating_chat [Agent::Chat, nil] The chat instance of the agent doing the delegating (for accessing hooks)
50
- # @param custom_tool_name [String, nil] Optional custom tool name (overrides auto-generated name)
51
- # @param preserve_context [Boolean] Whether to preserve conversation context between delegations (default: true)
52
- def initialize(
53
- delegate_name:,
54
- delegate_description:,
55
- delegate_chat:,
56
- agent_name:,
57
- swarm:,
58
- delegating_chat: nil,
59
- custom_tool_name: nil,
60
- preserve_context: true
61
- )
62
- super()
63
-
64
- @delegate_name = delegate_name
65
- @delegate_description = delegate_description
66
- @delegate_chat = delegate_chat
67
- @agent_name = agent_name
68
- @swarm = swarm
69
- @delegating_chat = delegating_chat
70
- @preserve_context = preserve_context
71
-
72
- # Use custom tool name if provided, otherwise generate using canonical method
73
- @tool_name = custom_tool_name || self.class.tool_name_for(delegate_name)
74
- @delegate_target = delegate_name.to_s
75
-
76
- # Track concurrent delegations to this target.
77
- # When multiple parallel tool calls target the same delegate, only the first
78
- # preserves context; subsequent concurrent calls always clear context to
79
- # prevent cross-contamination between independent parallel work.
80
- #
81
- # No Mutex needed: Async Fibers run on a single thread and only switch at
82
- # explicit yield points (IO, sleep, semaphore.acquire). Integer increment
83
- # and decrement never yield, so they are inherently atomic.
84
- @active_count = 0
85
- end
86
-
87
- # Override description to return dynamic string based on delegate
88
- def description
89
- "Work with #{@delegate_name} to delegate work, ask questions, or collaborate. #{@delegate_description}"
90
- end
91
-
92
- param :message,
93
- type: "string",
94
- desc: "Message to send to the agent - can be a work request, question, or collaboration message",
95
- required: true
96
-
97
- param :reset_context,
98
- type: "boolean",
99
- desc: "Reset the agent's conversation history before sending the message. Use it to recover from 'prompt too long' errors or other 4XX errors.",
100
- required: false
101
-
102
- # Override name to return custom delegation tool name
103
- def name
104
- @tool_name
105
- end
106
-
107
- # Check if this delegate uses lazy loading
108
- #
109
- # @return [Boolean] True if delegate is lazy-loaded
110
- def lazy?
111
- @delegate_chat.is_a?(Swarm::LazyDelegateChat)
112
- end
113
-
114
- # Check if this delegate has been initialized
115
- #
116
- # @return [Boolean] True if delegate chat is ready (either eager or lazy-initialized)
117
- def initialized?
118
- return true unless lazy?
119
-
120
- @delegate_chat.initialized?
121
- end
122
-
123
- # Force initialization of lazy delegate
124
- #
125
- # If the delegate is lazy-loaded, this will trigger immediate initialization.
126
- # For eager delegates, this is a no-op.
127
- #
128
- # @return [Agent::Chat] The resolved chat instance
129
- def initialize_delegate!
130
- resolve_delegate_chat
131
- end
132
-
133
- # Execute delegation with pre/post hooks
134
- #
135
- # Uses Fiber-local path tracking for circular dependency detection.
136
- # Each concurrent delegation runs in its own Fiber (via Async), so the path
137
- # is isolated per execution path. This correctly distinguishes parallel fan-out
138
- # (A→B, A→B) from true circular dependencies (A→B→A).
139
- #
140
- # @param message [String] Message to send to the agent
141
- # @param reset_context [Boolean] Whether to reset the agent's conversation history before delegation
142
- # @return [String] Result from delegate agent or error message
143
- def execute(message:, reset_context: false)
144
- # Save the current delegation path so we can restore it after execution.
145
- # The extended path (with our target) is only needed during chat.ask() so
146
- # child Fibers (nested delegations) inherit it. After delegation returns,
147
- # this Fiber's path should be unchanged.
148
- saved_delegation_path = Fiber[:delegation_path]
149
-
150
- # Access swarm infrastructure
151
- hook_registry = @swarm.hook_registry
152
- swarm_registry = @swarm.swarm_registry
153
-
154
- # Check for circular dependency using Fiber-local path
155
- # Each Fiber inherits the parent's path, so nested delegations
156
- # accumulate the full chain while parallel siblings remain isolated
157
- delegation_path = saved_delegation_path || []
158
- if delegation_path.include?(@delegate_target)
159
- emit_circular_warning(delegation_path)
160
- return "Error: Circular delegation detected: #{delegation_path.join(" -> ")} -> #{@delegate_target}. " \
161
- "Please restructure your delegation to avoid infinite loops."
162
- end
163
-
164
- # Get agent-specific hooks from the delegating chat instance
165
- agent_hooks = if @delegating_chat&.respond_to?(:hook_agent_hooks)
166
- @delegating_chat.hook_agent_hooks || {}
167
- else
168
- {}
169
- end
170
-
171
- # Trigger pre_delegation callback
172
- context = Hooks::Context.new(
173
- event: :pre_delegation,
174
- agent_name: @agent_name,
175
- swarm: @swarm,
176
- delegation_target: @delegate_target,
177
- metadata: {
178
- tool_name: @tool_name,
179
- message: message,
180
- timestamp: Time.now.utc.iso8601,
181
- },
182
- )
183
-
184
- executor = Hooks::Executor.new(hook_registry, logger: RubyLLM.logger)
185
- pre_agent_hooks = agent_hooks[:pre_delegation] || []
186
- result = executor.execute_safe(event: :pre_delegation, context: context, callbacks: pre_agent_hooks)
187
-
188
- # Check if callback halted or replaced the delegation
189
- if result.halt?
190
- return result.value || "Delegation halted by callback"
191
- elsif result.replace?
192
- return result.value
193
- end
194
-
195
- # Determine delegation type and proceed
196
- delegation_result = if @delegate_chat
197
- # Delegate to agent
198
- delegate_to_agent(message, reset_context: reset_context)
199
- elsif swarm_registry&.registered?(@delegate_target)
200
- # Delegate to registered swarm
201
- delegate_to_swarm(message, swarm_registry, reset_context: reset_context)
202
- else
203
- raise ConfigurationError, "Unknown delegation target: #{@delegate_target}"
204
- end
205
-
206
- # Trigger post_delegation callback
207
- post_context = Hooks::Context.new(
208
- event: :post_delegation,
209
- agent_name: @agent_name,
210
- swarm: @swarm,
211
- delegation_target: @delegate_target,
212
- delegation_result: delegation_result,
213
- metadata: {
214
- tool_name: @tool_name,
215
- message: message,
216
- result: delegation_result,
217
- timestamp: Time.now.utc.iso8601,
218
- },
219
- )
220
-
221
- post_agent_hooks = agent_hooks[:post_delegation] || []
222
- post_result = executor.execute_safe(event: :post_delegation, context: post_context, callbacks: post_agent_hooks)
223
-
224
- # Return modified result if callback replaces it
225
- if post_result.replace?
226
- post_result.value
227
- else
228
- delegation_result
229
- end
230
- rescue Faraday::TimeoutError, Net::ReadTimeout => e
231
- # Log timeout error as JSON event
232
- LogStream.emit(
233
- type: "delegation_error",
234
- agent: @agent_name,
235
- swarm_id: @swarm.swarm_id,
236
- parent_swarm_id: @swarm.parent_swarm_id,
237
- delegate_to: @tool_name,
238
- error_class: e.class.name,
239
- error_message: "Request timed out",
240
- error_backtrace: e.backtrace&.first(5) || [],
241
- )
242
- "Error: Request to #{@tool_name} timed out. The agent may be overloaded or the LLM service is not responding. Please try again or simplify the task."
243
- rescue Faraday::Error => e
244
- # Log network error as JSON event
245
- LogStream.emit(
246
- type: "delegation_error",
247
- agent: @agent_name,
248
- swarm_id: @swarm.swarm_id,
249
- parent_swarm_id: @swarm.parent_swarm_id,
250
- delegate_to: @tool_name,
251
- error_class: e.class.name,
252
- error_message: e.message,
253
- error_backtrace: e.backtrace&.first(5) || [],
254
- )
255
- "Error: Network error communicating with #{@tool_name}: #{e.class.name}. Please check connectivity and try again."
256
- rescue StandardError => e
257
- # Log unexpected error as JSON event
258
- backtrace_array = e.backtrace&.first(5) || []
259
- LogStream.emit(
260
- type: "delegation_error",
261
- agent: @agent_name,
262
- swarm_id: @swarm.swarm_id,
263
- parent_swarm_id: @swarm.parent_swarm_id,
264
- delegate_to: @tool_name,
265
- error_class: e.class.name,
266
- error_message: e.message,
267
- error_backtrace: backtrace_array,
268
- )
269
- # Return error string for LLM
270
- backtrace_str = backtrace_array.join("\n ")
271
- "Error: #{@tool_name} encountered an error: #{e.class.name}: #{e.message}\nBacktrace:\n #{backtrace_str}"
272
- ensure
273
- # Restore the calling Fiber's delegation path.
274
- # The extended path was only needed during chat.ask() so child Fibers
275
- # (spawned for nested tool calls) could inherit it for circular detection.
276
- Fiber[:delegation_path] = saved_delegation_path
277
- end
278
-
279
- private
280
-
281
- # Delegate to an agent
282
- #
283
- # Handles both eager Agent::Chat instances and lazy-loaded delegates.
284
- # LazyDelegateChat instances are initialized on first access.
285
- # Sets Fiber-local delegation path so child Fibers (nested delegations)
286
- # inherit the full chain for circular dependency detection.
287
- #
288
- # Tracks concurrent delegations to this target. When multiple parallel
289
- # tool calls target the same delegate (fan-out), only the first call
290
- # preserves context; subsequent concurrent calls always clear context
291
- # to prevent cross-contamination between independent parallel work.
292
- # Context clearing happens inside Agent::Chat's ask_semaphore for safety.
293
- #
294
- # @param message [String] Message to send to the agent
295
- # @param reset_context [Boolean] Whether to reset the agent's conversation history before delegation
296
- # @return [String] Result from agent
297
- def delegate_to_agent(message, reset_context: false)
298
- @active_count += 1
299
- concurrent = @active_count > 1
300
-
301
- # Set Fiber-local delegation path for this execution path
302
- # Child Fibers (from nested delegations) inherit this path automatically
303
- # We create a new array to avoid mutating the parent Fiber's reference
304
- Fiber[:delegation_path] = (Fiber[:delegation_path] || []) + [@delegate_target]
305
-
306
- # Resolve the chat instance (handles lazy loading)
307
- chat = resolve_delegate_chat
308
-
309
- # Determine if context should be cleared:
310
- # - reset_context: explicit caller request
311
- # - !preserve_context: agent configuration
312
- # - concurrent: parallel fan-out to same delegate (always isolate)
313
- # Clearing is done inside chat.ask's semaphore to avoid race conditions
314
- should_clear = reset_context || !@preserve_context || concurrent
315
-
316
- response = chat.ask(message, source: "delegation", clear_context: should_clear)
317
- response.content
318
- ensure
319
- @active_count -= 1
320
- end
321
-
322
- # Resolve the delegate chat instance
323
- #
324
- # If the delegate is a LazyDelegateChat, initializes it on first access.
325
- # Otherwise, returns the chat directly.
326
- #
327
- # @return [Agent::Chat] The resolved chat instance
328
- def resolve_delegate_chat
329
- if @delegate_chat.is_a?(Swarm::LazyDelegateChat)
330
- @delegate_chat.chat
331
- else
332
- @delegate_chat
333
- end
334
- end
335
-
336
- # Delegate to a registered swarm
337
- #
338
- # Sets Fiber-local delegation path so child Fibers (nested delegations)
339
- # inherit the full chain for circular dependency detection.
340
- # Tracks concurrent delegations the same way as delegate_to_agent.
341
- #
342
- # @param message [String] Message to send to the swarm
343
- # @param swarm_registry [SwarmRegistry] Registry for sub-swarms
344
- # @param reset_context [Boolean] Whether to reset the swarm's conversation history before delegation
345
- # @return [String] Result from swarm's lead agent
346
- def delegate_to_swarm(message, swarm_registry, reset_context: false)
347
- @active_count += 1
348
- concurrent = @active_count > 1
349
-
350
- # Set Fiber-local delegation path for this execution path
351
- Fiber[:delegation_path] = (Fiber[:delegation_path] || []) + [@delegate_target]
352
-
353
- # Load sub-swarm (lazy load + cache)
354
- subswarm = swarm_registry.load_swarm(@delegate_target)
355
-
356
- # Reset swarm context if explicitly requested or concurrent fan-out
357
- swarm_registry.reset(@delegate_target) if reset_context || concurrent
358
-
359
- # Execute sub-swarm's lead agent (uses agent() to trigger lazy initialization)
360
- lead_agent = subswarm.agent(subswarm.lead_agent)
361
- response = lead_agent.ask(message, source: "delegation")
362
- result = response.content
363
-
364
- # Reset if keep_context: false (standard behavior)
365
- swarm_registry.reset_if_needed(@delegate_target)
366
-
367
- result
368
- ensure
369
- @active_count -= 1
370
- end
371
-
372
- # Emit circular dependency warning event
373
- #
374
- # @param delegation_path [Array<String>] Current Fiber-local delegation path
375
- # @return [void]
376
- def emit_circular_warning(delegation_path)
377
- LogStream.emit(
378
- type: "delegation_circular_dependency",
379
- agent: @agent_name,
380
- swarm_id: @swarm.swarm_id,
381
- parent_swarm_id: @swarm.parent_swarm_id,
382
- target: @delegate_target,
383
- delegation_path: delegation_path,
384
- timestamp: Time.now.utc.iso8601,
385
- )
386
- end
387
- end
388
- end
389
- end
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- module DocumentConverters
6
- # Base class for document converters
7
- # Provides common interface and utility methods for converting various document formats
8
- class BaseConverter
9
- class << self
10
- # The gem name required for this converter
11
- # @return [String]
12
- def gem_name
13
- raise NotImplementedError, "#{name} must implement .gem_name"
14
- end
15
-
16
- # Human-readable format name
17
- # @return [String]
18
- def format_name
19
- raise NotImplementedError, "#{name} must implement .format_name"
20
- end
21
-
22
- # File extensions this converter handles
23
- # @return [Array<String>]
24
- def extensions
25
- raise NotImplementedError, "#{name} must implement .extensions"
26
- end
27
-
28
- # Check if the required gem is available
29
- # @return [Boolean]
30
- def available?
31
- gem_available?(gem_name)
32
- end
33
-
34
- # Check if a gem is installed
35
- # @param gem_name [String] Name of the gem to check
36
- # @return [Boolean]
37
- def gem_available?(gem_name)
38
- Gem::Specification.find_by_name(gem_name)
39
- true
40
- rescue Gem::LoadError
41
- false
42
- end
43
- end
44
-
45
- # Convert a document file to text/content
46
- # @param file_path [String] Path to the file
47
- # @return [String, RubyLLM::Content] Converted content or error message
48
- def convert(file_path)
49
- raise NotImplementedError, "#{self.class.name} must implement #convert"
50
- end
51
-
52
- protected
53
-
54
- # Return a system reminder about missing gem
55
- # @param format [String] Format name (e.g., "PDF")
56
- # @param gem_name [String] Required gem name
57
- # @return [String]
58
- def unsupported_format_reminder(format, gem_name)
59
- <<~REMINDER
60
- <system-reminder>
61
- This file is a #{format} document, but the required gem is not installed.
62
-
63
- To enable #{format} file reading, please install the gem:
64
- gem install #{gem_name}
65
-
66
- Or add to your Gemfile:
67
- gem "#{gem_name}"
68
-
69
- Don't install the gem yourself. Ask the user if they would like you to install this gem.
70
- </system-reminder>
71
- REMINDER
72
- end
73
-
74
- # Return an error message
75
- # @param message [String] Error message
76
- # @return [String]
77
- def error(message)
78
- "Error: #{message}"
79
- end
80
- end
81
- end
82
- end
83
- end
@@ -1,99 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- module DocumentConverters
6
- # Converts DOCX documents to text with image extraction
7
- class DocxConverter < BaseConverter
8
- class << self
9
- def gem_name
10
- "docx"
11
- end
12
-
13
- def format_name
14
- "DOCX"
15
- end
16
-
17
- def extensions
18
- [".docx", ".doc"]
19
- end
20
- end
21
-
22
- # Convert a DOCX document to text/content
23
- # @param file_path [String] Path to the DOCX file
24
- # @return [String, RubyLLM::Content] Converted content or error message
25
- def convert(file_path)
26
- unless self.class.available?
27
- return unsupported_format_reminder(self.class.format_name, self.class.gem_name)
28
- end
29
-
30
- # Check for legacy DOC format
31
- if File.extname(file_path).downcase == ".doc"
32
- return error("DOC format is not supported. Please convert to DOCX first.")
33
- end
34
-
35
- begin
36
- require "docx"
37
- require "tmpdir"
38
-
39
- doc = Docx::Document.open(file_path)
40
-
41
- # Extract images from the DOCX
42
- image_paths = ImageExtractors::DocxImageExtractor.extract_images(doc, file_path)
43
-
44
- output = []
45
- output << "Document: #{File.basename(file_path)}"
46
- output << "=" * 60
47
- output << ""
48
-
49
- # Extract paragraphs
50
- paragraphs = doc.paragraphs.map(&:text).reject(&:empty?)
51
-
52
- # Check for empty document
53
- if paragraphs.empty? && doc.tables.empty?
54
- output << "(Document is empty - no paragraphs or tables)"
55
- else
56
- output += paragraphs
57
-
58
- # Extract tables with enhanced formatting
59
- if doc.tables.any?
60
- output << ""
61
- output << "Tables:"
62
- output << "-" * 60
63
-
64
- doc.tables.each_with_index do |table, idx|
65
- output << ""
66
- output << "Table #{idx + 1} (#{table.row_count} rows × #{table.column_count} columns):"
67
-
68
- table.rows.each do |row|
69
- output << row.cells.map(&:text).join(" | ")
70
- end
71
- end
72
- end
73
- end
74
-
75
- text_content = output.join("\n")
76
-
77
- # If there are images, return Content with attachments
78
- if image_paths.any?
79
- content = RubyLLM::Content.new(text_content)
80
- image_paths.each do |image_path|
81
- content.add_attachment(image_path)
82
- end
83
- content
84
- else
85
- # No images, return just text
86
- text_content
87
- end
88
- rescue Zip::Error => e
89
- error("Invalid or corrupted DOCX file: #{e.message}")
90
- rescue Errno::ENOENT => e
91
- error("File not found or missing document.xml: #{e.message}")
92
- rescue StandardError => e
93
- error("Failed to parse DOCX file: #{e.message}")
94
- end
95
- end
96
- end
97
- end
98
- end
99
- end
@@ -1,101 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- module DocumentConverters
6
- # Converter for HTML to Markdown
7
- # Uses reverse_markdown gem if available, otherwise falls back to simple regex-based conversion
8
- class HtmlConverter < BaseConverter
9
- class << self
10
- def gem_name
11
- "reverse_markdown"
12
- end
13
-
14
- def format_name
15
- "HTML"
16
- end
17
-
18
- def extensions
19
- [".html", ".htm"]
20
- end
21
- end
22
-
23
- # Convert HTML string to Markdown
24
- # @param html [String] HTML content to convert
25
- # @return [String] Markdown content
26
- def convert_string(html)
27
- if self.class.available?
28
- convert_with_gem(html)
29
- else
30
- convert_simple(html)
31
- end
32
- end
33
-
34
- # Convert HTML file to Markdown
35
- # @param file_path [String] Path to HTML file
36
- # @return [String] Markdown content
37
- def convert(file_path)
38
- html = File.read(file_path)
39
- convert_string(html)
40
- rescue StandardError => e
41
- error("Failed to read HTML file: #{e.message}")
42
- end
43
-
44
- private
45
-
46
- # Convert HTML to Markdown using reverse_markdown gem
47
- # @param html [String] HTML content
48
- # @return [String] Markdown content
49
- def convert_with_gem(html)
50
- require "reverse_markdown"
51
-
52
- ReverseMarkdown.convert(html, unknown_tags: :bypass, github_flavored: true)
53
- rescue StandardError
54
- # Fallback to simple conversion if gem conversion fails
55
- convert_simple(html)
56
- end
57
-
58
- # Simple regex-based HTML to Markdown conversion (fallback)
59
- # @param html [String] HTML content
60
- # @return [String] Markdown content
61
- def convert_simple(html)
62
- # Remove script and style tags
63
- content = html.gsub(%r{<script[^>]*>.*?</script>}im, "")
64
- content = content.gsub(%r{<style[^>]*>.*?</style>}im, "")
65
-
66
- # Convert common HTML elements
67
- content = content.gsub(%r{<h1[^>]*>(.*?)</h1>}im, "\n# \\1\n")
68
- content = content.gsub(%r{<h2[^>]*>(.*?)</h2>}im, "\n## \\1\n")
69
- content = content.gsub(%r{<h3[^>]*>(.*?)</h3>}im, "\n### \\1\n")
70
- content = content.gsub(%r{<h4[^>]*>(.*?)</h4>}im, "\n#### \\1\n")
71
- content = content.gsub(%r{<h5[^>]*>(.*?)</h5>}im, "\n##### \\1\n")
72
- content = content.gsub(%r{<h6[^>]*>(.*?)</h6>}im, "\n###### \\1\n")
73
- content = content.gsub(%r{<p[^>]*>(.*?)</p>}im, "\n\\1\n")
74
- content = content.gsub(%r{<br\s*/?>}i, "\n")
75
- content = content.gsub(%r{<strong[^>]*>(.*?)</strong>}im, "**\\1**")
76
- content = content.gsub(%r{<b[^>]*>(.*?)</b>}im, "**\\1**")
77
- content = content.gsub(%r{<em[^>]*>(.*?)</em>}im, "_\\1_")
78
- content = content.gsub(%r{<i[^>]*>(.*?)</i>}im, "_\\1_")
79
- content = content.gsub(%r{<code[^>]*>(.*?)</code>}im, "`\\1`")
80
- content = content.gsub(%r{<a[^>]*href=["']([^"']*)["'][^>]*>(.*?)</a>}im, "[\\2](\\1)")
81
- content = content.gsub(%r{<li[^>]*>(.*?)</li>}im, "- \\1\n")
82
-
83
- # Remove remaining HTML tags
84
- content = content.gsub(/<[^>]+>/, "")
85
-
86
- # Decode HTML entities
87
- content = content.gsub("&lt;", "<")
88
- content = content.gsub("&gt;", ">")
89
- content = content.gsub("&amp;", "&")
90
- content = content.gsub("&quot;", "\"")
91
- content = content.gsub("&#39;", "'")
92
- content = content.gsub("&nbsp;", " ")
93
-
94
- # Clean up whitespace
95
- content = content.gsub(/\n\n\n+/, "\n\n")
96
- content.strip
97
- end
98
- end
99
- end
100
- end
101
- end