swarm_sdk 2.7.14 → 3.0.0.alpha2

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 (185) 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/document_converters/base.rb +84 -0
  42. data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
  43. data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -0
  45. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  46. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  47. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  48. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  49. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  50. data/lib/swarm_sdk/v3/tools/read.rb +213 -0
  51. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  52. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  53. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  54. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  55. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  56. data/lib/swarm_sdk/v3.rb +145 -0
  57. metadata +88 -149
  58. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  59. data/lib/swarm_sdk/agent/builder.rb +0 -705
  60. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  61. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  62. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  63. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  64. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  65. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  66. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  67. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  68. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  69. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  70. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  71. data/lib/swarm_sdk/agent/context.rb +0 -115
  72. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  73. data/lib/swarm_sdk/agent/definition.rb +0 -588
  74. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  75. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  76. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  77. data/lib/swarm_sdk/agent_registry.rb +0 -146
  78. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  79. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  80. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  81. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  82. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  83. data/lib/swarm_sdk/config.rb +0 -368
  84. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  85. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  86. data/lib/swarm_sdk/configuration.rb +0 -165
  87. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  88. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  89. data/lib/swarm_sdk/context_compactor.rb +0 -335
  90. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  91. data/lib/swarm_sdk/context_management/context.rb +0 -328
  92. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  93. data/lib/swarm_sdk/defaults.rb +0 -251
  94. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  95. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  96. data/lib/swarm_sdk/hooks/context.rb +0 -197
  97. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  98. data/lib/swarm_sdk/hooks/error.rb +0 -29
  99. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  100. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  101. data/lib/swarm_sdk/hooks/result.rb +0 -150
  102. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  103. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  104. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  105. data/lib/swarm_sdk/log_collector.rb +0 -227
  106. data/lib/swarm_sdk/log_stream.rb +0 -127
  107. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  108. data/lib/swarm_sdk/model_aliases.json +0 -8
  109. data/lib/swarm_sdk/models.json +0 -44002
  110. data/lib/swarm_sdk/models.rb +0 -161
  111. data/lib/swarm_sdk/node_context.rb +0 -245
  112. data/lib/swarm_sdk/observer/builder.rb +0 -81
  113. data/lib/swarm_sdk/observer/config.rb +0 -45
  114. data/lib/swarm_sdk/observer/manager.rb +0 -248
  115. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  116. data/lib/swarm_sdk/permissions/config.rb +0 -239
  117. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  118. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  119. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  120. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  121. data/lib/swarm_sdk/plugin.rb +0 -309
  122. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  123. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  124. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  125. data/lib/swarm_sdk/restore_result.rb +0 -65
  126. data/lib/swarm_sdk/result.rb +0 -241
  127. data/lib/swarm_sdk/snapshot.rb +0 -156
  128. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  129. data/lib/swarm_sdk/state_restorer.rb +0 -476
  130. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  131. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  132. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  133. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  134. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  135. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  136. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  141. data/lib/swarm_sdk/swarm.rb +0 -973
  142. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  143. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  144. data/lib/swarm_sdk/tools/base.rb +0 -63
  145. data/lib/swarm_sdk/tools/bash.rb +0 -280
  146. data/lib/swarm_sdk/tools/clock.rb +0 -46
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  148. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  149. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  150. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  151. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  152. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  153. data/lib/swarm_sdk/tools/edit.rb +0 -145
  154. data/lib/swarm_sdk/tools/glob.rb +0 -166
  155. data/lib/swarm_sdk/tools/grep.rb +0 -235
  156. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  157. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  160. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  161. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  162. data/lib/swarm_sdk/tools/read.rb +0 -261
  163. data/lib/swarm_sdk/tools/registry.rb +0 -205
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  166. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  167. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  168. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  169. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  170. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  171. data/lib/swarm_sdk/tools/think.rb +0 -100
  172. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  173. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  174. data/lib/swarm_sdk/tools/write.rb +0 -112
  175. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  176. data/lib/swarm_sdk/utils.rb +0 -68
  177. data/lib/swarm_sdk/validation_result.rb +0 -33
  178. data/lib/swarm_sdk/version.rb +0 -5
  179. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  180. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  181. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  182. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  183. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  184. data/lib/swarm_sdk/workflow.rb +0 -589
  185. data/lib/swarm_sdk.rb +0 -721
@@ -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