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
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module MCP
6
+ # HTTP transport for MCP with configurable SSL verification.
7
+ #
8
+ # Uses Net::HTTP directly instead of Faraday to avoid the async-http
9
+ # adapter that Faraday selects when running inside Async. The async-http
10
+ # adapter ignores Faraday's SSL settings and uses IO::Endpoint::SSLEndpoint
11
+ # which enforces CRL checking on OpenSSL 3.6+, breaking most HTTPS MCP
12
+ # connections with "certificate verify failed (unable to get certificate CRL)".
13
+ #
14
+ # Conforms to the MCP transport interface: responds to +send_request(request:)+
15
+ # returning a parsed Hash.
16
+ #
17
+ # @example Default (SSL peer verification, no CRL checking)
18
+ # SslHttpTransport.new(url: "https://api.example.com/mcp")
19
+ #
20
+ # @example Disable SSL verification entirely
21
+ # SslHttpTransport.new(url: "https://localhost/mcp", ssl_verify: false)
22
+ class SslHttpTransport
23
+ # @return [String] HTTP endpoint URL
24
+ attr_reader :url
25
+
26
+ # @param url [String] HTTP endpoint URL
27
+ # @param headers [Hash] HTTP headers
28
+ # @param ssl_verify [Boolean] Whether to verify SSL certificates (default: true)
29
+ def initialize(url:, headers: {}, ssl_verify: true)
30
+ @url = url
31
+ @headers = headers
32
+ @ssl_verify = ssl_verify
33
+ @uri = URI.parse(url)
34
+ end
35
+
36
+ # Send a JSON-RPC request to the MCP server.
37
+ #
38
+ # @param request [Hash] JSON-RPC request body
39
+ # @return [Hash] Parsed JSON response
40
+ # @raise [MCP::Client::RequestHandlerError] On HTTP or connection errors
41
+ def send_request(request:)
42
+ http = build_http
43
+ post = build_post(request)
44
+
45
+ response = http.request(post)
46
+ JSON.parse(response.body)
47
+ rescue OpenSSL::SSL::SSLError => e
48
+ raise ::MCP::Client::RequestHandlerError.new(
49
+ "SSL error connecting to MCP server #{@uri.host}: #{e.message}",
50
+ extract_method_params(request),
51
+ error_type: :internal_error,
52
+ original_error: e,
53
+ )
54
+ rescue Net::HTTPError, Net::OpenTimeout, Net::ReadTimeout, SocketError, Errno::ECONNREFUSED => e
55
+ raise ::MCP::Client::RequestHandlerError.new(
56
+ "Connection error to MCP server #{@uri.host}: #{e.message}",
57
+ extract_method_params(request),
58
+ error_type: :internal_error,
59
+ original_error: e,
60
+ )
61
+ end
62
+
63
+ private
64
+
65
+ # Build a Net::HTTP client with SSL configuration.
66
+ #
67
+ # @return [Net::HTTP]
68
+ def build_http
69
+ http = Net::HTTP.new(@uri.host, @uri.port)
70
+
71
+ if @uri.scheme == "https"
72
+ http.use_ssl = true
73
+ http.verify_mode = @ssl_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
74
+ end
75
+
76
+ http
77
+ end
78
+
79
+ # Build the POST request with JSON body and headers.
80
+ #
81
+ # @param body [Hash] Request body
82
+ # @return [Net::HTTP::Post]
83
+ def build_post(body)
84
+ post = Net::HTTP::Post.new(@uri.request_uri)
85
+ post["Content-Type"] = "application/json"
86
+ post["Accept"] = "application/json"
87
+
88
+ @headers.each { |k, v| post[k] = v }
89
+ post.body = JSON.generate(body)
90
+ post
91
+ end
92
+
93
+ # Extract method and params from request for error reporting.
94
+ #
95
+ # @param request [Hash]
96
+ # @return [Hash]
97
+ def extract_method_params(request)
98
+ { method: request[:method] || request["method"], params: request[:params] || request["params"] }
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module MCP
6
+ # Subprocess-based JSON-RPC transport for MCP stdio servers
7
+ #
8
+ # Spawns a child process and communicates via JSON-RPC over stdin/stdout.
9
+ # Performs the MCP protocol initialization handshake automatically.
10
+ # Implements the same duck type as `MCP::Client::HTTP` (`send_request(request:)`).
11
+ #
12
+ # @example
13
+ # transport = StdioTransport.new(
14
+ # command: "npx",
15
+ # args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
16
+ # )
17
+ # response = transport.send_request(request: { jsonrpc: "2.0", id: "1", method: "tools/list" })
18
+ # transport.close
19
+ class StdioTransport
20
+ PROTOCOL_VERSION = "2024-11-05"
21
+
22
+ # Create a new stdio transport, spawn the subprocess, and initialize the MCP session
23
+ #
24
+ # @param command [String] Command to execute
25
+ # @param args [Array<String>] Command arguments
26
+ # @param env [Hash<String, String>] Environment variables
27
+ #
28
+ # @raise [McpError] If the subprocess fails to start or initialization fails
29
+ #
30
+ # @example
31
+ # StdioTransport.new(command: "node", args: ["server.js"])
32
+ def initialize(command:, args: [], env: {})
33
+ @stdin, @stdout, @wait_thread = Open3.popen2(
34
+ env.transform_keys(&:to_s), command, *args
35
+ )
36
+ initialize_session
37
+ end
38
+
39
+ # Send a JSON-RPC request and return the response
40
+ #
41
+ # Writes the request as a single JSON line to the subprocess stdin,
42
+ # then reads lines from stdout until a response with an "id" field
43
+ # is received (skipping server-initiated notifications).
44
+ #
45
+ # @param request [Hash] Complete JSON-RPC 2.0 request object
46
+ # @return [Hash] Parsed JSON-RPC response
47
+ # @raise [McpError] If the subprocess exits unexpectedly
48
+ #
49
+ # @example
50
+ # response = transport.send_request(
51
+ # request: { jsonrpc: "2.0", id: "1", method: "tools/list" }
52
+ # )
53
+ def send_request(request:)
54
+ @stdin.puts(JSON.generate(request))
55
+ @stdin.flush
56
+ read_response
57
+ end
58
+
59
+ # Gracefully shut down the subprocess
60
+ #
61
+ # Closes stdin to signal the subprocess to exit, then waits
62
+ # briefly before sending SIGTERM if still alive.
63
+ #
64
+ # @return [void]
65
+ def close
66
+ @stdin&.close unless @stdin&.closed?
67
+ return unless @wait_thread
68
+
69
+ # Give the process a moment to exit gracefully
70
+ return if @wait_thread.join(2)
71
+
72
+ # Force terminate if still running
73
+ Process.kill("TERM", @wait_thread.pid) if @wait_thread.alive?
74
+ @wait_thread.join(2)
75
+ rescue Errno::ESRCH, Errno::ECHILD
76
+ # Process already exited
77
+ end
78
+
79
+ # Whether the subprocess is still running
80
+ #
81
+ # @return [Boolean]
82
+ def alive?
83
+ @wait_thread&.alive? || false
84
+ end
85
+
86
+ private
87
+
88
+ # Perform the MCP protocol initialization handshake
89
+ #
90
+ # Sends `initialize` request followed by `notifications/initialized`
91
+ # notification, as required by the MCP specification for stdio servers.
92
+ #
93
+ # @return [void]
94
+ # @raise [McpError] If the server returns an error during initialization
95
+ def initialize_session
96
+ response = send_request(request: {
97
+ jsonrpc: "2.0",
98
+ id: SecureRandom.uuid,
99
+ method: "initialize",
100
+ params: {
101
+ protocolVersion: PROTOCOL_VERSION,
102
+ capabilities: {},
103
+ clientInfo: { name: "swarm_sdk", version: "1.0" },
104
+ },
105
+ })
106
+
107
+ if response.key?("error")
108
+ raise McpError, "MCP initialization failed: #{response["error"]["message"]}"
109
+ end
110
+
111
+ # Send initialized notification (no id — it's a notification, not a request)
112
+ @stdin.puts(JSON.generate({ jsonrpc: "2.0", method: "notifications/initialized" }))
113
+ @stdin.flush
114
+ end
115
+
116
+ # Read the next JSON-RPC response from the subprocess
117
+ #
118
+ # Skips server-initiated notifications (messages without an "id" field)
119
+ # and returns the first response that has one.
120
+ #
121
+ # @return [Hash] Parsed JSON-RPC response
122
+ # @raise [McpError] If the subprocess exits (stdout returns nil)
123
+ def read_response
124
+ loop do
125
+ line = @stdout.gets
126
+ raise McpError, "MCP server process exited unexpectedly" if line.nil?
127
+
128
+ parsed = JSON.parse(line)
129
+ return parsed if parsed.key?("id")
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module MCP
6
+ # Bridges MCP tools into RubyLLM::Tool instances
7
+ #
8
+ # Creates anonymous RubyLLM::Tool subclasses that delegate execution
9
+ # to an MCP connector. Each MCP tool gets its own Tool subclass with
10
+ # the correct name, description, and JSON Schema parameters.
11
+ #
12
+ # @example
13
+ # mcp_tool = connector.available_tools.first
14
+ # ruby_llm_tool = ToolProxy.create(mcp_tool, connector)
15
+ # ruby_llm_tool.name #=> "echo"
16
+ # ruby_llm_tool.call(message: "hi") #=> "hi"
17
+ module ToolProxy
18
+ extend self
19
+
20
+ # Create a RubyLLM::Tool instance from an MCP tool
21
+ #
22
+ # Builds an anonymous subclass of RubyLLM::Tool with the MCP tool's
23
+ # name, description, and input schema. The `execute` method delegates
24
+ # to the connector's `call_tool`.
25
+ #
26
+ # @param mcp_tool [MCP::Client::Tool] MCP tool descriptor
27
+ # @param connector [Connector] MCP connector for tool execution
28
+ # @return [RubyLLM::Tool] Instantiated tool ready for use with RubyLLM::Chat
29
+ #
30
+ # @example
31
+ # tool = ToolProxy.create(mcp_tool, connector)
32
+ # chat.with_tools(tool)
33
+ def create(mcp_tool, connector)
34
+ tool_name = mcp_tool.name
35
+ tool_desc = mcp_tool.description || "MCP tool: #{tool_name}"
36
+ schema = mcp_tool.input_schema
37
+
38
+ klass = Class.new(RubyLLM::Tool) do
39
+ description tool_desc
40
+ params schema if schema
41
+ end
42
+
43
+ klass.define_method(:name) { tool_name }
44
+ klass.define_method(:execute) do |**args|
45
+ connector.call_tool(tool_name, **args)
46
+ end
47
+
48
+ klass.new
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,297 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Memory
6
+ module Adapters
7
+ # Abstract adapter interface for memory storage
8
+ #
9
+ # Defines the contract that all storage backends must satisfy.
10
+ # SDK users can implement custom adapters (e.g., Postgres + pgvector)
11
+ # by subclassing this and implementing all methods.
12
+ #
13
+ # ## Why adapters own vector operations
14
+ #
15
+ # The SDK code (ContextBuilder, IngestionPipeline, Retriever, etc.)
16
+ # **never** computes similarity directly. All vector math goes through
17
+ # the adapter so that each backend can use its native capabilities:
18
+ #
19
+ # - **FilesystemAdapter**: FAISS for indexed search, Ruby cosine
20
+ # similarity (via {VectorUtils}) for pairwise comparison
21
+ # - **PgvectorAdapter**: `<=>` operator for both indexed and pairwise
22
+ # - **Qdrant/Pinecone**: Client API for all similarity operations
23
+ #
24
+ # There are two distinct vector operations:
25
+ #
26
+ # ### `vector_search(embedding, top_k:, threshold:)`
27
+ # Searches an **index** of card embeddings. Used by the Retriever
28
+ # for semantic search and by the Consolidator for dedup/conflict
29
+ # detection. The filesystem adapter uses FAISS for this. A pgvector
30
+ # adapter would use `ORDER BY embedding <=> query LIMIT k`.
31
+ #
32
+ # ### `similarity(embedding_a, embedding_b)`
33
+ # Computes pairwise similarity between **two arbitrary vectors**.
34
+ # This is needed when comparing vectors that aren't both in the
35
+ # card index — for example:
36
+ # - Card embedding vs. cluster centroid (clusters aren't indexed)
37
+ # - Two cards already in hand during dedup in the ContextBuilder
38
+ # - Query embedding vs. candidate cards in the exploration sprinkle
39
+ #
40
+ # FAISS cannot do this — it only supports `index.search([query], k)`,
41
+ # which searches vectors that have been `add`'d to the index.
42
+ #
43
+ # For in-memory adapters, include {VectorUtils} to get a default
44
+ # Ruby implementation. For database adapters, implement it using
45
+ # your backend's native operator.
46
+ #
47
+ # ## Implementing a custom adapter
48
+ #
49
+ # Subclass {Base} and implement every method. Methods that raise
50
+ # {NotImplementedError} are required. For a starting point, see
51
+ # {FilesystemAdapter} which implements the full interface.
52
+ #
53
+ # If your adapter doesn't have native vector math, include
54
+ # {VectorUtils} to get a default {#similarity} implementation:
55
+ #
56
+ # class MyAdapter < Base
57
+ # include VectorUtils # provides similarity()
58
+ # # ... implement the rest
59
+ # end
60
+ #
61
+ # @example Postgres + pgvector adapter
62
+ # class PgvectorAdapter < SwarmSDK::V3::Memory::Adapters::Base
63
+ # def initialize(connection_string)
64
+ # @db = PG.connect(connection_string)
65
+ # end
66
+ #
67
+ # def write_card(card)
68
+ # @db.exec_params(
69
+ # "INSERT INTO cards (id, text, type, embedding, ...) VALUES ($1, $2, $3, $4, ...)",
70
+ # [card.id, card.text, card.type.to_s, card.embedding.to_s]
71
+ # )
72
+ # end
73
+ #
74
+ # def similarity(embedding_a, embedding_b)
75
+ # result = @db.exec_params(
76
+ # "SELECT 1 - ($1::vector <=> $2::vector) AS sim",
77
+ # [embedding_a.to_s, embedding_b.to_s]
78
+ # )
79
+ # result[0]["sim"].to_f
80
+ # end
81
+ #
82
+ # def vector_search(embedding, top_k:, threshold: 0.0)
83
+ # rows = @db.exec_params(
84
+ # "SELECT id, 1 - (embedding <=> $1::vector) AS similarity " \
85
+ # "FROM cards WHERE 1 - (embedding <=> $1::vector) >= $3 " \
86
+ # "ORDER BY embedding <=> $1::vector LIMIT $2",
87
+ # [embedding.to_s, top_k, threshold]
88
+ # )
89
+ # rows.map { |r| { id: r["id"], similarity: r["similarity"].to_f } }
90
+ # end
91
+ #
92
+ # # ... implement remaining methods
93
+ # end
94
+ class Base
95
+ # --- Card CRUD ---
96
+
97
+ # Write a card to storage (insert or update)
98
+ #
99
+ # @param card [Card] Card to store
100
+ # @return [void]
101
+ def write_card(card)
102
+ raise NotImplementedError, "#{self.class}#write_card not implemented"
103
+ end
104
+
105
+ # Read a card by ID
106
+ #
107
+ # @param id [String] Card ID
108
+ # @return [Card, nil] Card or nil if not found
109
+ def read_card(id)
110
+ raise NotImplementedError, "#{self.class}#read_card not implemented"
111
+ end
112
+
113
+ # Delete a card by ID
114
+ #
115
+ # @param id [String] Card ID
116
+ # @return [void]
117
+ def delete_card(id)
118
+ raise NotImplementedError, "#{self.class}#delete_card not implemented"
119
+ end
120
+
121
+ # List cards, optionally filtered by ID prefix
122
+ #
123
+ # @param prefix [String, nil] ID prefix filter
124
+ # @return [Array<Card>]
125
+ def list_cards(prefix: nil)
126
+ raise NotImplementedError, "#{self.class}#list_cards not implemented"
127
+ end
128
+
129
+ # List cards eligible for compression
130
+ #
131
+ # Default implementation loads all cards and filters in Ruby.
132
+ # Database adapters should override with a server-side query.
133
+ #
134
+ # @param max_level [Integer] Maximum compression level to include (default: 3)
135
+ # @return [Array<Card>] Cards eligible for compression
136
+ #
137
+ # @example Override in a Postgres adapter
138
+ # def list_cards_for_compression(max_level: 3)
139
+ # @db.exec_params("SELECT * FROM cards WHERE compression_level <= $1", [max_level])
140
+ # .map { |row| card_from_row(row) }
141
+ # end
142
+ def list_cards_for_compression(max_level: 3)
143
+ list_cards.select { |c| c.compression_level <= max_level }
144
+ end
145
+
146
+ # --- Edge CRUD ---
147
+
148
+ # Write an edge to storage
149
+ #
150
+ # @param edge [Edge] Edge to store
151
+ # @return [void]
152
+ def write_edge(edge)
153
+ raise NotImplementedError, "#{self.class}#write_edge not implemented"
154
+ end
155
+
156
+ # Get edges for a card (as source or target)
157
+ #
158
+ # @param card_id [String] Card ID
159
+ # @param type [Symbol, nil] Filter by edge type
160
+ # @return [Array<Edge>]
161
+ def edges_for(card_id, type: nil)
162
+ raise NotImplementedError, "#{self.class}#edges_for not implemented"
163
+ end
164
+
165
+ # Delete all edges involving a card (as source or target)
166
+ #
167
+ # @param card_id [String] Card ID
168
+ # @return [void]
169
+ def delete_edges_for(card_id)
170
+ raise NotImplementedError, "#{self.class}#delete_edges_for not implemented"
171
+ end
172
+
173
+ # --- Cluster CRUD ---
174
+
175
+ # Write a cluster to storage (insert or update)
176
+ #
177
+ # @param cluster [Cluster] Cluster to store
178
+ # @return [void]
179
+ def write_cluster(cluster)
180
+ raise NotImplementedError, "#{self.class}#write_cluster not implemented"
181
+ end
182
+
183
+ # Read a cluster by ID
184
+ #
185
+ # @param id [String] Cluster ID
186
+ # @return [Cluster, nil]
187
+ def read_cluster(id)
188
+ raise NotImplementedError, "#{self.class}#read_cluster not implemented"
189
+ end
190
+
191
+ # List all clusters
192
+ #
193
+ # @return [Array<Cluster>]
194
+ def list_clusters
195
+ raise NotImplementedError, "#{self.class}#list_clusters not implemented"
196
+ end
197
+
198
+ # --- Vector Operations ---
199
+ #
200
+ # These two methods are the adapter's responsibility because
201
+ # different backends compute similarity differently:
202
+ #
203
+ # - FAISS: L2-normalized inner product (IndexFlatIP)
204
+ # - pgvector: `<=>` cosine distance operator
205
+ # - Qdrant: client.search() with scoring
206
+ #
207
+ # The SDK never computes cosine similarity directly. It always
208
+ # calls adapter.similarity() or adapter.vector_search().
209
+
210
+ # Compute similarity between two arbitrary embedding vectors
211
+ #
212
+ # Used by the SDK for pairwise comparisons where an indexed
213
+ # search doesn't apply:
214
+ # - Card vs. cluster centroid (cluster assignment in IngestionPipeline)
215
+ # - Card vs. card (deduplication in ContextBuilder)
216
+ # - Query vs. card (exploration sprinkle in ContextBuilder)
217
+ #
218
+ # For in-memory adapters, include {VectorUtils} to get a default
219
+ # cosine similarity implementation. For database adapters,
220
+ # implement using your backend's native operator.
221
+ #
222
+ # @param embedding_a [Array<Float>] First embedding vector
223
+ # @param embedding_b [Array<Float>] Second embedding vector
224
+ # @return [Float] Similarity score (-1.0 to 1.0 for cosine)
225
+ #
226
+ # @see VectorUtils Default in-memory implementation
227
+ def similarity(embedding_a, embedding_b)
228
+ raise NotImplementedError, "#{self.class}#similarity not implemented"
229
+ end
230
+
231
+ # Search the card embedding index for similar vectors
232
+ #
233
+ # Used by the SDK for top-k retrieval:
234
+ # - Retriever: semantic search for relevant cards
235
+ # - Consolidator: finding near-duplicates and conflicts
236
+ #
237
+ # This searches an **index** of card embeddings. The adapter
238
+ # decides how to implement the index (FAISS, pgvector, etc.).
239
+ #
240
+ # @param embedding [Array<Float>] Query embedding
241
+ # @param top_k [Integer] Maximum number of results
242
+ # @param threshold [Float] Minimum similarity to include
243
+ # @return [Array<Hash>] Array of `{ id: String, similarity: Float }`
244
+ def vector_search(embedding, top_k:, threshold: 0.0)
245
+ raise NotImplementedError, "#{self.class}#vector_search not implemented"
246
+ end
247
+
248
+ # Rebuild the vector index from all stored cards
249
+ #
250
+ # Called when the index needs to be reconstructed, e.g., after
251
+ # bulk imports or if the index file is missing/corrupted.
252
+ #
253
+ # @return [void]
254
+ def rebuild_index
255
+ raise NotImplementedError, "#{self.class}#rebuild_index not implemented"
256
+ end
257
+
258
+ # --- Transactions ---
259
+
260
+ # Execute a block within a transaction
261
+ #
262
+ # The default implementation is a no-op pass-through. Adapters
263
+ # with transactional backends (e.g., SQLite, Postgres) should
264
+ # override this to wrap the block in a real transaction.
265
+ #
266
+ # @yield Block to execute within the transaction
267
+ # @return [Object] Return value of the block
268
+ #
269
+ # @example
270
+ # adapter.transaction do
271
+ # adapter.write_card(card)
272
+ # adapter.write_edge(edge)
273
+ # end
274
+ def transaction
275
+ yield
276
+ end
277
+
278
+ # --- Persistence ---
279
+
280
+ # Save all in-memory state to durable storage
281
+ #
282
+ # @return [void]
283
+ def save
284
+ raise NotImplementedError, "#{self.class}#save not implemented"
285
+ end
286
+
287
+ # Load state from durable storage into memory
288
+ #
289
+ # @return [void]
290
+ def load
291
+ raise NotImplementedError, "#{self.class}#load not implemented"
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
297
+ end