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.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +43 -22
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +6 -0
- data/lib/swarm_sdk/ruby_llm_patches/mcp_ssl_patch.rb +144 -0
- data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +3 -4
- data/lib/swarm_sdk/v3/agent.rb +1165 -0
- data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
- data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
- data/lib/swarm_sdk/v3/configuration.rb +490 -0
- data/lib/swarm_sdk/v3/debug_log.rb +86 -0
- data/lib/swarm_sdk/v3/event_stream.rb +130 -0
- data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
- data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
- data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
- data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
- data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
- data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
- data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
- data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
- data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
- data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
- data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
- data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
- data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
- data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
- data/lib/swarm_sdk/v3/memory/card.rb +206 -0
- data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
- data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
- data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
- data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
- data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
- data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
- data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
- data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
- data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
- data/lib/swarm_sdk/v3/memory/store.rb +489 -0
- data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
- data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
- data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
- data/lib/swarm_sdk/v3/tools/base.rb +80 -0
- data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
- data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
- data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
- data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
- data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
- data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
- data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
- data/lib/swarm_sdk/v3/tools/read.rb +181 -0
- data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
- data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
- data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
- data/lib/swarm_sdk/v3/tools/think.rb +88 -0
- data/lib/swarm_sdk/v3/tools/write.rb +87 -0
- data/lib/swarm_sdk/v3.rb +145 -0
- metadata +84 -148
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
- data/lib/swarm_sdk/agent/builder.rb +0 -680
- data/lib/swarm_sdk/agent/chat.rb +0 -1432
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
- data/lib/swarm_sdk/agent/context.rb +0 -115
- data/lib/swarm_sdk/agent/context_manager.rb +0 -315
- data/lib/swarm_sdk/agent/definition.rb +0 -581
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
- data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
- data/lib/swarm_sdk/agent_registry.rb +0 -146
- data/lib/swarm_sdk/builders/base_builder.rb +0 -553
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
- data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
- data/lib/swarm_sdk/concerns/validatable.rb +0 -55
- data/lib/swarm_sdk/config.rb +0 -367
- data/lib/swarm_sdk/configuration/parser.rb +0 -397
- data/lib/swarm_sdk/configuration/translator.rb +0 -283
- data/lib/swarm_sdk/configuration.rb +0 -165
- data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
- data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
- data/lib/swarm_sdk/context_compactor.rb +0 -335
- data/lib/swarm_sdk/context_management/builder.rb +0 -128
- data/lib/swarm_sdk/context_management/context.rb +0 -328
- data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
- data/lib/swarm_sdk/defaults.rb +0 -251
- data/lib/swarm_sdk/events_to_messages.rb +0 -199
- data/lib/swarm_sdk/hooks/adapter.rb +0 -359
- data/lib/swarm_sdk/hooks/context.rb +0 -197
- data/lib/swarm_sdk/hooks/definition.rb +0 -80
- data/lib/swarm_sdk/hooks/error.rb +0 -29
- data/lib/swarm_sdk/hooks/executor.rb +0 -146
- data/lib/swarm_sdk/hooks/registry.rb +0 -147
- data/lib/swarm_sdk/hooks/result.rb +0 -150
- data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
- data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
- data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
- data/lib/swarm_sdk/log_collector.rb +0 -227
- data/lib/swarm_sdk/log_stream.rb +0 -127
- data/lib/swarm_sdk/markdown_parser.rb +0 -75
- data/lib/swarm_sdk/model_aliases.json +0 -8
- data/lib/swarm_sdk/models.json +0 -44002
- data/lib/swarm_sdk/models.rb +0 -161
- data/lib/swarm_sdk/node_context.rb +0 -245
- data/lib/swarm_sdk/observer/builder.rb +0 -81
- data/lib/swarm_sdk/observer/config.rb +0 -45
- data/lib/swarm_sdk/observer/manager.rb +0 -236
- data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
- data/lib/swarm_sdk/permissions/config.rb +0 -239
- data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
- data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
- data/lib/swarm_sdk/permissions/validator.rb +0 -173
- data/lib/swarm_sdk/permissions_builder.rb +0 -122
- data/lib/swarm_sdk/plugin.rb +0 -309
- data/lib/swarm_sdk/plugin_registry.rb +0 -101
- data/lib/swarm_sdk/proc_helpers.rb +0 -53
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -212
- data/lib/swarm_sdk/snapshot.rb +0 -156
- data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
- data/lib/swarm_sdk/state_restorer.rb +0 -476
- data/lib/swarm_sdk/state_snapshot.rb +0 -334
- data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -195
- data/lib/swarm_sdk/swarm/builder.rb +0 -256
- data/lib/swarm_sdk/swarm/executor.rb +0 -290
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -151
- data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -360
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -270
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
- data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
- data/lib/swarm_sdk/swarm.rb +0 -843
- data/lib/swarm_sdk/swarm_loader.rb +0 -145
- data/lib/swarm_sdk/swarm_registry.rb +0 -136
- data/lib/swarm_sdk/tools/base.rb +0 -63
- data/lib/swarm_sdk/tools/bash.rb +0 -280
- data/lib/swarm_sdk/tools/clock.rb +0 -46
- data/lib/swarm_sdk/tools/delegate.rb +0 -389
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
- data/lib/swarm_sdk/tools/edit.rb +0 -145
- data/lib/swarm_sdk/tools/glob.rb +0 -166
- data/lib/swarm_sdk/tools/grep.rb +0 -235
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
- data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
- data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
- data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
- data/lib/swarm_sdk/tools/read.rb +0 -261
- data/lib/swarm_sdk/tools/registry.rb +0 -205
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
- data/lib/swarm_sdk/tools/think.rb +0 -100
- data/lib/swarm_sdk/tools/todo_write.rb +0 -237
- data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
- data/lib/swarm_sdk/tools/write.rb +0 -112
- data/lib/swarm_sdk/transcript_builder.rb +0 -278
- data/lib/swarm_sdk/utils.rb +0 -68
- data/lib/swarm_sdk/validation_result.rb +0 -33
- data/lib/swarm_sdk/version.rb +0 -5
- data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
- data/lib/swarm_sdk/workflow/builder.rb +0 -227
- data/lib/swarm_sdk/workflow/executor.rb +0 -497
- data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
- data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
- data/lib/swarm_sdk/workflow.rb +0 -589
- data/lib/swarm_sdk.rb +0 -718
|
@@ -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
|