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,290 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Agent
5
- module ChatHelpers
6
- # LLM configuration and provider setup
7
- #
8
- # Extracted from Chat to reduce class size and centralize RubyLLM setup logic.
9
- module LlmConfiguration
10
- private
11
-
12
- # Create the internal RubyLLM::Chat instance
13
- #
14
- # @return [RubyLLM::Chat] Chat instance
15
- def create_llm_chat(model_id:, provider_name:, base_url:, api_version:, timeout:, assume_model_exists:, max_concurrent_tools:)
16
- chat_options = build_chat_options(max_concurrent_tools)
17
-
18
- chat = instantiate_chat(
19
- model_id: model_id,
20
- provider_name: provider_name,
21
- base_url: base_url,
22
- timeout: timeout,
23
- assume_model_exists: assume_model_exists,
24
- chat_options: chat_options,
25
- )
26
-
27
- # Enable RubyLLM's native Responses API if configured
28
- enable_responses_api(chat, api_version, base_url) if api_version == "v1/responses"
29
-
30
- chat
31
- end
32
-
33
- # Build chat options hash
34
- #
35
- # @param max_concurrent_tools [Integer, nil] Max concurrent tool executions
36
- # @return [Hash] Chat options
37
- def build_chat_options(max_concurrent_tools)
38
- return {} unless max_concurrent_tools
39
-
40
- {
41
- tool_concurrency: :async,
42
- max_concurrency: max_concurrent_tools,
43
- }
44
- end
45
-
46
- # Instantiate RubyLLM::Chat with appropriate configuration
47
- #
48
- # @return [RubyLLM::Chat] Chat instance
49
- def instantiate_chat(model_id:, provider_name:, base_url:, timeout:, assume_model_exists:, chat_options:)
50
- if base_url || timeout != SwarmSDK.config.agent_request_timeout
51
- instantiate_with_custom_context(
52
- model_id: model_id,
53
- provider_name: provider_name,
54
- base_url: base_url,
55
- timeout: timeout,
56
- assume_model_exists: assume_model_exists,
57
- chat_options: chat_options,
58
- )
59
- elsif provider_name
60
- instantiate_with_provider(
61
- model_id: model_id,
62
- provider_name: provider_name,
63
- assume_model_exists: assume_model_exists,
64
- chat_options: chat_options,
65
- )
66
- else
67
- instantiate_default(
68
- model_id: model_id,
69
- assume_model_exists: assume_model_exists,
70
- chat_options: chat_options,
71
- )
72
- end
73
- end
74
-
75
- # Instantiate chat with custom context (base_url/timeout overrides)
76
- def instantiate_with_custom_context(model_id:, provider_name:, base_url:, timeout:, assume_model_exists:, chat_options:)
77
- raise ArgumentError, "Provider must be specified when base_url is set" if base_url && !provider_name
78
-
79
- context = build_custom_context(provider: provider_name, base_url: base_url, timeout: timeout)
80
- assume_model_exists = base_url ? true : false if assume_model_exists.nil?
81
-
82
- RubyLLM.chat(
83
- model: model_id,
84
- provider: provider_name,
85
- assume_model_exists: assume_model_exists,
86
- context: context,
87
- **chat_options,
88
- )
89
- end
90
-
91
- # Instantiate chat with explicit provider
92
- def instantiate_with_provider(model_id:, provider_name:, assume_model_exists:, chat_options:)
93
- assume_model_exists = false if assume_model_exists.nil?
94
-
95
- RubyLLM.chat(
96
- model: model_id,
97
- provider: provider_name,
98
- assume_model_exists: assume_model_exists,
99
- **chat_options,
100
- )
101
- end
102
-
103
- # Instantiate chat with default configuration
104
- def instantiate_default(model_id:, assume_model_exists:, chat_options:)
105
- assume_model_exists = false if assume_model_exists.nil?
106
-
107
- RubyLLM.chat(
108
- model: model_id,
109
- assume_model_exists: assume_model_exists,
110
- **chat_options,
111
- )
112
- end
113
-
114
- # Build custom RubyLLM context for base_url/timeout overrides
115
- #
116
- # @return [RubyLLM::Context] Configured context
117
- def build_custom_context(provider:, base_url:, timeout:)
118
- RubyLLM.context do |config|
119
- config.request_timeout = timeout
120
- # Set read_timeout to match request_timeout for streaming support
121
- # This ensures long gaps between chunks (model thinking) don't cause timeouts
122
- config.read_timeout = SwarmSDK.config.llm_read_timeout || timeout
123
-
124
- configure_provider_base_url(config, provider, base_url) if base_url
125
- end
126
- end
127
-
128
- # Configure provider-specific base URL
129
- #
130
- # @param config [RubyLLM::Config] RubyLLM configuration context
131
- # @param provider [String] Provider name
132
- # @param base_url [String] Custom base URL
133
- # @raise [ConfigurationError] If API key is required but not configured
134
- # @raise [ArgumentError] If provider doesn't support custom base_url
135
- def configure_provider_base_url(config, provider, base_url)
136
- case provider.to_s
137
- when "openai", "deepseek", "perplexity", "mistral", "openrouter"
138
- config.openai_api_base = base_url
139
- api_key = SwarmSDK.config.openai_api_key
140
-
141
- # For local endpoints, API key is optional
142
- # For cloud endpoints, require API key
143
- unless api_key || local_endpoint?(base_url)
144
- raise ConfigurationError,
145
- "OpenAI API key required for '#{provider}' with base_url '#{base_url}'. " \
146
- "Configure with: SwarmSDK.configure { |c| c.openai_api_key = '...' }"
147
- end
148
-
149
- config.openai_api_key = api_key if api_key
150
- config.openai_use_system_role = true
151
- when "anthropic"
152
- config.anthropic_api_base = base_url
153
- api_key = SwarmSDK.config.anthropic_api_key
154
- config.anthropic_api_key = api_key if api_key
155
- when "ollama"
156
- config.ollama_api_base = base_url
157
- # Ollama doesn't need an API key
158
- when "gpustack"
159
- config.gpustack_api_base = base_url
160
- api_key = SwarmSDK.config.gpustack_api_key
161
- config.gpustack_api_key = api_key if api_key
162
- else
163
- raise ArgumentError, "Provider '#{provider}' doesn't support custom base_url."
164
- end
165
- end
166
-
167
- # Check if a URL points to a local endpoint
168
- #
169
- # @param url [String] URL to check
170
- # @return [Boolean] true if URL is a local endpoint
171
- def local_endpoint?(url)
172
- uri = URI.parse(url)
173
- ["localhost", "127.0.0.1", "0.0.0.0"].include?(uri.host)
174
- rescue URI::InvalidURIError
175
- false
176
- end
177
-
178
- # Fetch real model info for accurate context tracking
179
- #
180
- # Uses SwarmSDK::Models for model lookup (reads from models.json).
181
- # Falls back to RubyLLM.models if not found in SwarmSDK.
182
- #
183
- # @param model_id [String] Model ID to lookup
184
- def fetch_real_model_info(model_id)
185
- @model_lookup_error = nil
186
- @real_model_info = begin
187
- # Try SwarmSDK::Models first (reads from local models.json)
188
- # Returns ModelInfo object with method access (context_window, etc.)
189
- SwarmSDK::Models.find(model_id) || RubyLLM.models.find(model_id)
190
- rescue StandardError => e
191
- suggestions = suggest_similar_models(model_id)
192
- @model_lookup_error = {
193
- model: model_id,
194
- error_message: e.message,
195
- suggestions: suggestions,
196
- }
197
- nil
198
- end
199
- end
200
-
201
- # Enable RubyLLM's native Responses API on the chat instance
202
- #
203
- # Uses RubyLLM's built-in support for OpenAI's Responses API (v1/responses endpoint)
204
- # which provides automatic stateful conversation tracking with 5-minute TTL.
205
- #
206
- # @param chat [RubyLLM::Chat] Chat instance to configure
207
- # @param api_version [String] API version (should be "v1/responses")
208
- # @param base_url [String, nil] Custom endpoint URL if any
209
- def enable_responses_api(chat, api_version, base_url)
210
- return unless api_version == "v1/responses"
211
-
212
- # Warn if using custom endpoint (typically doesn't support Responses API)
213
- if base_url && !base_url.include?("api.openai.com")
214
- RubyLLM.logger.warn(
215
- "SwarmSDK: Responses API requested but using custom endpoint #{base_url}. " \
216
- "Custom endpoints typically don't support /v1/responses.",
217
- )
218
- end
219
-
220
- # Enable native RubyLLM Responses API support
221
- # - stateful: true enables automatic previous_response_id tracking
222
- # - store: true enables server-side conversation storage
223
- chat.with_responses_api(stateful: true, store: true)
224
- RubyLLM.logger.debug("SwarmSDK: Enabled native Responses API support")
225
- end
226
-
227
- # Configure extended thinking on the RubyLLM chat instance
228
- #
229
- # @param thinking_config [Hash, nil] Thinking configuration with :effort and/or :budget
230
- # @return [self]
231
- #
232
- # @example
233
- # configure_thinking(budget: 10_000)
234
- # configure_thinking(effort: :high)
235
- # configure_thinking(effort: :high, budget: 10_000)
236
- def configure_thinking(thinking_config)
237
- return self unless thinking_config
238
-
239
- @llm_chat.with_thinking(**thinking_config)
240
- self
241
- end
242
-
243
- # Configure LLM parameters with proper temperature normalization
244
- #
245
- # @param params [Hash] Parameter hash
246
- # @return [self]
247
- def configure_parameters(params)
248
- return self if params.nil? || params.empty?
249
-
250
- if params[:temperature]
251
- @llm_chat.with_temperature(params[:temperature])
252
- params = params.except(:temperature)
253
- end
254
-
255
- @llm_chat.with_params(**params) if params.any?
256
-
257
- self
258
- end
259
-
260
- # Configure custom HTTP headers for LLM requests
261
- #
262
- # @param headers [Hash, nil] Custom HTTP headers
263
- # @return [self]
264
- def configure_headers(custom_headers)
265
- return self if custom_headers.nil? || custom_headers.empty?
266
-
267
- @llm_chat.with_headers(**custom_headers)
268
-
269
- self
270
- end
271
-
272
- # Suggest similar models when a model is not found
273
- #
274
- # @param query [String] Model name to search for
275
- # @return [Array<RubyLLM::Model::Info>] Up to 3 similar models
276
- def suggest_similar_models(query)
277
- normalized_query = query.to_s.downcase.gsub(/[.\-_]/, "")
278
-
279
- RubyLLM.models.all.select do |model_info|
280
- normalized_id = model_info.id.downcase.gsub(/[.\-_]/, "")
281
- normalized_id.include?(normalized_query) ||
282
- model_info.name&.downcase&.gsub(/[.\-_]/, "")&.include?(normalized_query)
283
- end.first(3)
284
- rescue StandardError
285
- []
286
- end
287
- end
288
- end
289
- end
290
- end
@@ -1,116 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Agent
5
- module ChatHelpers
6
- # Helper methods for logging and serialization of tool calls and results
7
- #
8
- # Responsibilities:
9
- # - Format tool calls for logging
10
- # - Serialize tool results (handling different types)
11
- # - Calculate LLM costs based on token usage
12
- #
13
- # These are stateless utility methods that operate on data structures.
14
- module LoggingHelpers
15
- # Format tool calls for logging
16
- #
17
- # @param tool_calls_hash [Hash] Tool calls from message
18
- # @return [Array<Hash>, nil] Formatted tool calls
19
- def format_tool_calls(tool_calls_hash)
20
- return unless tool_calls_hash
21
-
22
- tool_calls_hash.map do |_id, tc|
23
- {
24
- id: tc.id,
25
- name: tc.name,
26
- arguments: tc.arguments,
27
- }
28
- end
29
- end
30
-
31
- # Serialize a tool result for logging
32
- #
33
- # Handles multiple result types:
34
- # - String: pass through
35
- # - Hash/Array: pass through
36
- # - RubyLLM::Content: extract text and attachment info
37
- # - Other: convert to string
38
- #
39
- # @param result [String, Hash, Array, RubyLLM::Content, Object] Tool result
40
- # @return [String, Hash, Array] Serialized result
41
- def serialize_result(result)
42
- case result
43
- when String then result
44
- when Hash, Array then result
45
- when RubyLLM::Content
46
- # Format Content objects to show text and attachment info
47
- parts = []
48
- parts << result.text if result.text && !result.text.empty?
49
-
50
- if result.attachments.any?
51
- attachment_info = result.attachments.map do |att|
52
- "#{att.source} (#{att.mime_type})"
53
- end.join(", ")
54
- parts << "[Attachments: #{attachment_info}]"
55
- end
56
-
57
- parts.join(" ")
58
- else
59
- result.to_s
60
- end
61
- end
62
-
63
- # Calculate LLM cost for a message
64
- #
65
- # Uses RubyLLM's model registry to get pricing information.
66
- # Returns zero cost if pricing is unavailable.
67
- #
68
- # @param message [RubyLLM::Message] Message with token counts
69
- # @return [Hash] Cost breakdown { input_cost:, output_cost:, total_cost: }
70
- def calculate_cost(message)
71
- return zero_cost unless message.input_tokens && message.output_tokens
72
-
73
- # Use SwarmSDK's model registry (not RubyLLM's) for up-to-date pricing
74
- model_info = SwarmSDK::Models.find(message.model_id)
75
- return zero_cost unless model_info
76
-
77
- # Extract pricing from SwarmSDK's ModelInfo (method access for top-level, Hash for nested)
78
- pricing = model_info.pricing
79
- return zero_cost unless pricing
80
-
81
- text_pricing = pricing["text_tokens"] || pricing[:text_tokens]
82
- return zero_cost unless text_pricing
83
-
84
- standard_pricing = text_pricing["standard"] || text_pricing[:standard]
85
- return zero_cost unless standard_pricing
86
-
87
- input_price = standard_pricing["input_per_million"] || standard_pricing[:input_per_million]
88
- output_price = standard_pricing["output_per_million"] || standard_pricing[:output_per_million]
89
-
90
- return zero_cost unless input_price && output_price
91
-
92
- # Calculate costs (prices are per million tokens in USD)
93
- input_cost = (message.input_tokens / 1_000_000.0) * input_price
94
- output_cost = (message.output_tokens / 1_000_000.0) * output_price
95
-
96
- {
97
- input_cost: input_cost,
98
- output_cost: output_cost,
99
- total_cost: input_cost + output_cost,
100
- }
101
- rescue StandardError => e
102
- # Model not found in registry or pricing not available
103
- RubyLLM.logger.debug("Cost calculation failed for #{message.model_id}: #{e.message}")
104
- zero_cost
105
- end
106
-
107
- # Zero cost fallback
108
- #
109
- # @return [Hash] Zero cost breakdown
110
- def zero_cost
111
- { input_cost: 0.0, output_cost: 0.0, total_cost: 0.0 }
112
- end
113
- end
114
- end
115
- end
116
- end
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Agent
5
- module ChatHelpers
6
- # Message serialization and deserialization for snapshots
7
- #
8
- # Extracted from Chat to reduce class size and centralize persistence logic.
9
- module Serialization
10
- # Create snapshot of current conversation state
11
- #
12
- # @return [Hash] Serialized conversation data
13
- def conversation_snapshot
14
- {
15
- messages: @llm_chat.messages.map { |msg| serialize_message(msg) },
16
- model_id: model_id,
17
- provider: model_provider,
18
- timestamp: Time.now.utc.iso8601,
19
- }
20
- end
21
-
22
- # Restore conversation from snapshot
23
- #
24
- # @param snapshot [Hash] Serialized conversation data
25
- # @return [self]
26
- def restore_conversation(snapshot)
27
- raise ArgumentError, "Invalid snapshot: missing messages" unless snapshot[:messages]
28
-
29
- @llm_chat.messages.clear
30
- snapshot[:messages].each do |msg_data|
31
- @llm_chat.messages << deserialize_message(msg_data)
32
- end
33
-
34
- self
35
- end
36
-
37
- private
38
-
39
- # Serialize a RubyLLM::Message to a plain hash
40
- #
41
- # @param message [RubyLLM::Message] Message to serialize
42
- # @return [Hash] Serialized message data
43
- def serialize_message(message)
44
- data = message.to_h
45
-
46
- # Convert tool_calls to plain hashes (they're ToolCall objects)
47
- if data[:tool_calls]
48
- data[:tool_calls] = data[:tool_calls].transform_values(&:to_h)
49
- end
50
-
51
- # Handle Content objects
52
- if data[:content].respond_to?(:to_h)
53
- data[:content] = data[:content].to_h
54
- end
55
-
56
- data
57
- end
58
-
59
- # Deserialize a hash back to a RubyLLM::Message
60
- #
61
- # @param data [Hash] Serialized message data
62
- # @return [RubyLLM::Message] Reconstructed message
63
- def deserialize_message(data)
64
- data = data.transform_keys(&:to_sym)
65
-
66
- # Convert tool_calls back to ToolCall objects
67
- if data[:tool_calls]
68
- data[:tool_calls] = data[:tool_calls].transform_values do |tc_data|
69
- tc_data = tc_data.transform_keys(&:to_sym)
70
- RubyLLM::ToolCall.new(
71
- id: tc_data[:id],
72
- name: tc_data[:name],
73
- arguments: tc_data[:arguments] || {},
74
- )
75
- end
76
- end
77
-
78
- RubyLLM::Message.new(**data)
79
- end
80
- end
81
- end
82
- end
83
- end
@@ -1,134 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Agent
5
- module ChatHelpers
6
- # Handles injection of system reminders at strategic points in the conversation
7
- #
8
- # Responsibilities:
9
- # - Inject reminders before/after first user message
10
- # - Inject periodic TodoWrite reminders
11
- # - Track when reminders were last injected
12
- #
13
- # This class is stateless - it operates on the chat's message history.
14
- class SystemReminderInjector
15
- # System reminder to inject AFTER the first user message
16
- AFTER_FIRST_MESSAGE_REMINDER = <<~REMINDER.strip
17
- <system-reminder>Your todo list is currently empty. DO NOT mention this to the user. If this task requires multiple steps: (1) FIRST analyze the scope by searching/reading files, (2) SECOND create a COMPLETE todo list with ALL tasks before starting work, (3) THIRD execute tasks one by one. Only skip the todo list for simple single-step tasks. Do not mention this message to the user.</system-reminder>
18
- REMINDER
19
-
20
- # Periodic reminder about TodoWrite tool usage
21
- TODOWRITE_PERIODIC_REMINDER = <<~REMINDER.strip
22
- <system-reminder>The TodoWrite tool hasn't been used recently. If you're working on tasks that would benefit from tracking progress, consider using the TodoWrite tool to track progress. Also consider cleaning up the todo list if has become stale and no longer matches what you are working on. Only use it if it's relevant to the current work. This is just a gentle reminder - ignore if not applicable.</system-reminder>
23
- REMINDER
24
-
25
- class << self
26
- # Check if this is the first user message in the conversation
27
- #
28
- # @param chat [Agent::Chat] The chat instance
29
- # @return [Boolean] true if no user messages exist yet
30
- def first_message?(chat)
31
- !chat.has_user_message?
32
- end
33
-
34
- # Inject first message reminders
35
- #
36
- # This manually constructs the first message sequence with system reminders.
37
- #
38
- # Sequence:
39
- # 1. User's actual prompt
40
- # 2. Toolset reminder (list of available tools)
41
- # 3. AFTER_FIRST_MESSAGE_REMINDER (todo list reminder - only if TodoWrite available)
42
- #
43
- # @param chat [Agent::Chat] The chat instance
44
- # @param prompt [String] The user's actual prompt
45
- # @return [void]
46
- def inject_first_message_reminders(chat, prompt)
47
- # Build user message with embedded reminders
48
- # Reminders are embedded in the content, not separate messages
49
- parts = [
50
- prompt,
51
- build_toolset_reminder(chat),
52
- ]
53
-
54
- # Only include todo list reminder if agent has TodoWrite tool
55
- parts << AFTER_FIRST_MESSAGE_REMINDER if chat.has_tool?("TodoWrite")
56
-
57
- full_content = parts.join("\n\n")
58
-
59
- # Extract reminders and add clean prompt to persistent history
60
- reminders = chat.context_manager.extract_system_reminders(full_content)
61
- clean_prompt = chat.context_manager.strip_system_reminders(full_content)
62
-
63
- # Store clean prompt (without reminders) in conversation history
64
- chat.add_message(role: :user, content: clean_prompt)
65
-
66
- # Track reminders to embed in this message when sending to LLM
67
- reminders.each do |reminder|
68
- chat.add_ephemeral_reminder(reminder)
69
- end
70
- end
71
-
72
- # Build toolset reminder listing all available tools
73
- #
74
- # @param chat [Agent::Chat] The chat instance
75
- # @return [String] System reminder with tool list
76
- def build_toolset_reminder(chat)
77
- tools_list = chat.tool_names
78
-
79
- reminder = "<system-reminder>\n"
80
- reminder += "Tools available: #{tools_list.join(", ")}\n\n"
81
- reminder += "Only use tools from this list. Do not attempt to use tools that are not listed here.\n"
82
- reminder += "</system-reminder>"
83
-
84
- reminder
85
- end
86
-
87
- # Check if we should inject a periodic TodoWrite reminder
88
- #
89
- # Injects a reminder if:
90
- # 1. Enough messages have passed (>= 5)
91
- # 2. TodoWrite hasn't been used in the last TODOWRITE_REMINDER_INTERVAL messages
92
- #
93
- # @param chat [Agent::Chat] The chat instance
94
- # @param last_todowrite_index [Integer, nil] Index of last TodoWrite usage
95
- # @return [Boolean] true if reminder should be injected
96
- def should_inject_todowrite_reminder?(chat, last_todowrite_index)
97
- # Need at least a few messages before reminding
98
- return false if chat.message_count < 5
99
-
100
- # Find the last message that contains TodoWrite tool usage
101
- last_todo_index = chat.find_last_message_index do |msg|
102
- msg.role == :tool && msg.content.to_s.include?("TodoWrite")
103
- end
104
-
105
- # Check if enough messages have passed since last TodoWrite
106
- reminder_interval = SwarmSDK.config.todowrite_reminder_interval
107
- if last_todo_index.nil? && last_todowrite_index.nil?
108
- # Never used TodoWrite - check if we've exceeded interval
109
- chat.message_count >= reminder_interval
110
- elsif last_todo_index
111
- # Recently used - don't remind
112
- false
113
- elsif last_todowrite_index
114
- # Used before - check if interval has passed
115
- chat.message_count - last_todowrite_index >= reminder_interval
116
- else
117
- false
118
- end
119
- end
120
-
121
- # Update the last TodoWrite index by finding it in messages
122
- #
123
- # @param chat [Agent::Chat] The chat instance
124
- # @return [Integer, nil] Index of last TodoWrite usage, or nil
125
- def find_last_todowrite_index(chat)
126
- chat.find_last_message_index do |msg|
127
- msg.role == :tool && msg.content.to_s.include?("TodoWrite")
128
- end
129
- end
130
- end
131
- end
132
- end
133
- end
134
- end