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.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
- 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/document_converters/base.rb +84 -0
- data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
- data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
- data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -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 +213 -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 +88 -149
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
- data/lib/swarm_sdk/agent/builder.rb +0 -705
- data/lib/swarm_sdk/agent/chat.rb +0 -1438
- 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 -588
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
- 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 -558
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
- 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 -368
- data/lib/swarm_sdk/configuration/parser.rb +0 -397
- data/lib/swarm_sdk/configuration/translator.rb +0 -285
- 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 -248
- 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 -119
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -241
- 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 -204
- data/lib/swarm_sdk/swarm/builder.rb +0 -256
- data/lib/swarm_sdk/swarm/executor.rb +0 -446
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
- data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
- 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 -973
- 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 -721
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Hooks
|
|
6
|
+
# Read-only context passed to hook blocks
|
|
7
|
+
#
|
|
8
|
+
# Each hook event type populates a different subset of fields:
|
|
9
|
+
#
|
|
10
|
+
# | Field | before_ask | after_ask | before_tool | after_tool | on_stop |
|
|
11
|
+
# |-----------------|------------|-----------|-------------|------------|---------|
|
|
12
|
+
# | event | yes | yes | yes | yes | yes |
|
|
13
|
+
# | agent_name | yes | yes | yes | yes | yes |
|
|
14
|
+
# | prompt | yes | yes | - | - | - |
|
|
15
|
+
# | response | - | yes | - | - | yes |
|
|
16
|
+
# | tool_name | - | - | yes | yes | - |
|
|
17
|
+
# | tool_arguments | - | - | yes | yes | - |
|
|
18
|
+
# | tool_result | - | - | - | yes | - |
|
|
19
|
+
#
|
|
20
|
+
# Hook blocks use convenience methods to return {Result} objects:
|
|
21
|
+
#
|
|
22
|
+
# @example Halt processing
|
|
23
|
+
# before_ask { |ctx| ctx.halt("Not allowed") }
|
|
24
|
+
#
|
|
25
|
+
# @example Replace a value
|
|
26
|
+
# after_tool { |ctx| ctx.replace(sanitize(ctx.tool_result)) }
|
|
27
|
+
#
|
|
28
|
+
# @example Continue normally (explicit)
|
|
29
|
+
# before_tool { |ctx| ctx.continue }
|
|
30
|
+
class Context
|
|
31
|
+
# @return [Symbol] Event type (:before_ask, :after_ask, :before_tool, :after_tool, :on_stop)
|
|
32
|
+
attr_reader :event
|
|
33
|
+
|
|
34
|
+
# @return [Symbol] Agent name from definition
|
|
35
|
+
attr_reader :agent_name
|
|
36
|
+
|
|
37
|
+
# @return [String, nil] User prompt (ask events only)
|
|
38
|
+
attr_reader :prompt
|
|
39
|
+
|
|
40
|
+
# @return [RubyLLM::Message, nil] LLM response (after_ask and on_stop only)
|
|
41
|
+
attr_reader :response
|
|
42
|
+
|
|
43
|
+
# @return [String, nil] Tool name (tool events only)
|
|
44
|
+
attr_reader :tool_name
|
|
45
|
+
|
|
46
|
+
# @return [Hash, nil] Tool call arguments (tool events only)
|
|
47
|
+
attr_reader :tool_arguments
|
|
48
|
+
|
|
49
|
+
# @return [Object, nil] Tool execution result (after_tool only)
|
|
50
|
+
attr_reader :tool_result
|
|
51
|
+
|
|
52
|
+
# Create a new hook context
|
|
53
|
+
#
|
|
54
|
+
# @param event [Symbol] Event type
|
|
55
|
+
# @param agent_name [Symbol] Agent identifier
|
|
56
|
+
# @param prompt [String, nil] User prompt
|
|
57
|
+
# @param response [RubyLLM::Message, nil] LLM response
|
|
58
|
+
# @param tool_name [String, nil] Tool name
|
|
59
|
+
# @param tool_arguments [Hash, nil] Tool arguments
|
|
60
|
+
# @param tool_result [Object, nil] Tool result
|
|
61
|
+
def initialize(event:, agent_name:, prompt: nil, response: nil, tool_name: nil, tool_arguments: nil, tool_result: nil)
|
|
62
|
+
@event = event
|
|
63
|
+
@agent_name = agent_name
|
|
64
|
+
@prompt = prompt
|
|
65
|
+
@response = response
|
|
66
|
+
@tool_name = tool_name
|
|
67
|
+
@tool_arguments = tool_arguments
|
|
68
|
+
@tool_result = tool_result
|
|
69
|
+
freeze
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Signal to continue normal processing
|
|
73
|
+
#
|
|
74
|
+
# @return [Result] A continue result
|
|
75
|
+
#
|
|
76
|
+
# @example
|
|
77
|
+
# before_ask { |ctx| ctx.continue }
|
|
78
|
+
def continue
|
|
79
|
+
Result.continue
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Signal to halt processing
|
|
83
|
+
#
|
|
84
|
+
# @param message [String, nil] Optional halt message
|
|
85
|
+
# @return [Result] A halt result
|
|
86
|
+
#
|
|
87
|
+
# @example Block tool execution
|
|
88
|
+
# before_tool { |ctx| ctx.halt("Tool disabled") }
|
|
89
|
+
#
|
|
90
|
+
# @example Abort ask
|
|
91
|
+
# before_ask { |ctx| ctx.halt }
|
|
92
|
+
def halt(message = nil)
|
|
93
|
+
Result.halt(message)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Signal to replace a value
|
|
97
|
+
#
|
|
98
|
+
# @param value [Object] Replacement value
|
|
99
|
+
# @return [Result] A replace result
|
|
100
|
+
#
|
|
101
|
+
# @example Replace prompt
|
|
102
|
+
# before_ask { |ctx| ctx.replace("Modified: #{ctx.prompt}") }
|
|
103
|
+
#
|
|
104
|
+
# @example Replace tool result
|
|
105
|
+
# after_tool { |ctx| ctx.replace(sanitize(ctx.tool_result)) }
|
|
106
|
+
def replace(value)
|
|
107
|
+
Result.replace(value)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Hooks
|
|
6
|
+
# Immutable value object representing the outcome of a hook execution
|
|
7
|
+
#
|
|
8
|
+
# A Result controls flow after a hook runs. Three actions are possible:
|
|
9
|
+
#
|
|
10
|
+
# - **continue** — proceed normally (default when hook returns nil or non-Result)
|
|
11
|
+
# - **halt** — stop processing; meaning varies by event:
|
|
12
|
+
# - `before_ask`: returns nil from ask (same as interrupt)
|
|
13
|
+
# - `before_tool`: returns error string to LLM without executing tool
|
|
14
|
+
# - **replace** — substitute a value:
|
|
15
|
+
# - `before_ask`: replaces the prompt
|
|
16
|
+
# - `after_tool`: replaces the tool result
|
|
17
|
+
#
|
|
18
|
+
# Results are created via factory methods, never directly instantiated
|
|
19
|
+
# by hook authors. Hook blocks return a Result to signal flow control.
|
|
20
|
+
#
|
|
21
|
+
# @example In a before_ask hook
|
|
22
|
+
# before_ask { |ctx| ctx.halt("Not allowed") }
|
|
23
|
+
#
|
|
24
|
+
# @example In an after_tool hook
|
|
25
|
+
# after_tool { |ctx| ctx.replace(sanitize(ctx.tool_result)) }
|
|
26
|
+
#
|
|
27
|
+
# @example Continue (explicit)
|
|
28
|
+
# before_ask { |ctx| ctx.continue }
|
|
29
|
+
class Result
|
|
30
|
+
# Valid result actions
|
|
31
|
+
ACTIONS = [:continue, :halt, :replace].freeze
|
|
32
|
+
|
|
33
|
+
# @return [Symbol] The action (:continue, :halt, or :replace)
|
|
34
|
+
attr_reader :action
|
|
35
|
+
|
|
36
|
+
# @return [Object, nil] Associated value (halt message or replacement value)
|
|
37
|
+
attr_reader :value
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
# Create a continue result (proceed normally)
|
|
41
|
+
#
|
|
42
|
+
# @return [Result] A result with action :continue
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# Result.continue
|
|
46
|
+
def continue
|
|
47
|
+
new(action: :continue)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Create a halt result (stop processing)
|
|
51
|
+
#
|
|
52
|
+
# @param message [String, nil] Optional halt message
|
|
53
|
+
# @return [Result] A result with action :halt
|
|
54
|
+
#
|
|
55
|
+
# @example With message
|
|
56
|
+
# Result.halt("Bash disabled for this agent")
|
|
57
|
+
#
|
|
58
|
+
# @example Without message
|
|
59
|
+
# Result.halt
|
|
60
|
+
def halt(message = nil)
|
|
61
|
+
new(action: :halt, value: message)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Create a replace result (substitute a value)
|
|
65
|
+
#
|
|
66
|
+
# @param value [Object] Replacement value
|
|
67
|
+
# @return [Result] A result with action :replace
|
|
68
|
+
#
|
|
69
|
+
# @example Replace prompt
|
|
70
|
+
# Result.replace("Modified prompt text")
|
|
71
|
+
#
|
|
72
|
+
# @example Replace tool result
|
|
73
|
+
# Result.replace(sanitized_output)
|
|
74
|
+
def replace(value)
|
|
75
|
+
new(action: :replace, value: value)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Whether this result signals to continue normally
|
|
80
|
+
#
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
def continue?
|
|
83
|
+
@action == :continue
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Whether this result signals to halt processing
|
|
87
|
+
#
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
def halt?
|
|
90
|
+
@action == :halt
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Whether this result signals to replace a value
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
def replace?
|
|
97
|
+
@action == :replace
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# @param action [Symbol] One of ACTIONS
|
|
103
|
+
# @param value [Object, nil] Associated value
|
|
104
|
+
# @raise [ArgumentError] If action is not a valid action
|
|
105
|
+
def initialize(action:, value: nil)
|
|
106
|
+
raise ArgumentError, "Invalid action: #{action.inspect}. Must be one of: #{ACTIONS.join(", ")}" unless ACTIONS.include?(action)
|
|
107
|
+
|
|
108
|
+
@action = action
|
|
109
|
+
@value = value
|
|
110
|
+
freeze
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Hooks
|
|
6
|
+
# Executes hooks for a given event in registration order
|
|
7
|
+
#
|
|
8
|
+
# Hooks are grouped by event type and executed sequentially. For tool events,
|
|
9
|
+
# hooks are filtered by their matcher before execution. The first hook that
|
|
10
|
+
# returns a non-continue {Result} short-circuits the chain.
|
|
11
|
+
#
|
|
12
|
+
# ## Match Parameter (tool hooks only)
|
|
13
|
+
#
|
|
14
|
+
# | Input | Behavior |
|
|
15
|
+
# |-------------|-----------------------------------|
|
|
16
|
+
# | `nil` | Matches all tools |
|
|
17
|
+
# | `:Bash` | Exact match on tool name |
|
|
18
|
+
# | `"Bash"` | Exact match (same as Symbol) |
|
|
19
|
+
# | `/pattern/` | Regex match (user controls anchor)|
|
|
20
|
+
# | `[:W, :E]` | Exact match on any in the array |
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# hooks = [
|
|
24
|
+
# { event: :before_ask, block: ->(ctx) { puts ctx.prompt } },
|
|
25
|
+
# { event: :before_tool, match: :Bash, block: ->(ctx) { ctx.halt("no bash") } },
|
|
26
|
+
# ]
|
|
27
|
+
# runner = Runner.new(hooks)
|
|
28
|
+
# result = runner.run(:before_ask, context)
|
|
29
|
+
class Runner
|
|
30
|
+
# Supported hook events
|
|
31
|
+
EVENTS = [:before_ask, :after_ask, :before_tool, :after_tool, :on_stop].freeze
|
|
32
|
+
|
|
33
|
+
# Tool-specific event types (these support match filtering)
|
|
34
|
+
TOOL_EVENTS = [:before_tool, :after_tool].freeze
|
|
35
|
+
|
|
36
|
+
# Create a new runner from a list of hook configurations
|
|
37
|
+
#
|
|
38
|
+
# @param hooks [Array<Hash>] Hook config hashes with :event, :block, and optional :match
|
|
39
|
+
# @raise [KeyError] If a hook references an unknown event
|
|
40
|
+
#
|
|
41
|
+
# @example
|
|
42
|
+
# Runner.new([
|
|
43
|
+
# { event: :before_ask, block: my_proc },
|
|
44
|
+
# { event: :before_tool, match: :Bash, block: guard_proc },
|
|
45
|
+
# ])
|
|
46
|
+
def initialize(hooks = [])
|
|
47
|
+
@hooks = EVENTS.each_with_object({}) { |e, h| h[e] = [] }
|
|
48
|
+
hooks.each { |hook| @hooks.fetch(hook[:event]) << hook }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Execute hooks for an event and return the controlling result
|
|
52
|
+
#
|
|
53
|
+
# Hooks run in registration order. The first hook that returns a
|
|
54
|
+
# non-continue {Result} wins and short-circuits the chain.
|
|
55
|
+
# If no hook returns a controlling result, {Result.continue} is returned.
|
|
56
|
+
#
|
|
57
|
+
# For tool events (:before_tool, :after_tool), hooks are filtered
|
|
58
|
+
# by their matcher against the context's tool_name.
|
|
59
|
+
#
|
|
60
|
+
# @param event [Symbol] Event type (one of EVENTS)
|
|
61
|
+
# @param context [Context] Hook context with event data
|
|
62
|
+
# @return [Result] The controlling result
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# result = runner.run(:before_ask, context)
|
|
66
|
+
# if result.halt?
|
|
67
|
+
# # stop processing
|
|
68
|
+
# end
|
|
69
|
+
def run(event, context)
|
|
70
|
+
hooks_for_event = @hooks.fetch(event)
|
|
71
|
+
hooks_for_event = filter_by_matcher(hooks_for_event, context.tool_name) if tool_event?(event)
|
|
72
|
+
|
|
73
|
+
hooks_for_event.each do |hook|
|
|
74
|
+
result = hook[:block].call(context)
|
|
75
|
+
return result if result.is_a?(Result) && !result.continue?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
Result.continue
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Whether any tool hooks are registered
|
|
82
|
+
#
|
|
83
|
+
# Used by Agent to skip around_tool_execution registration when
|
|
84
|
+
# no tool hooks exist, avoiding unnecessary overhead.
|
|
85
|
+
#
|
|
86
|
+
# @return [Boolean]
|
|
87
|
+
def any_tool_hooks?
|
|
88
|
+
@hooks[:before_tool].any? || @hooks[:after_tool].any?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Whether the event is a tool-specific event
|
|
94
|
+
#
|
|
95
|
+
# @param event [Symbol] Event type
|
|
96
|
+
# @return [Boolean]
|
|
97
|
+
def tool_event?(event)
|
|
98
|
+
TOOL_EVENTS.include?(event)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Filter hooks by their matcher against a tool name
|
|
102
|
+
#
|
|
103
|
+
# @param hooks [Array<Hash>] Hook configs to filter
|
|
104
|
+
# @param tool_name [String, nil] Tool name to match against
|
|
105
|
+
# @return [Array<Hash>] Matching hooks
|
|
106
|
+
def filter_by_matcher(hooks, tool_name)
|
|
107
|
+
hooks.select { |hook| matches?(hook[:match], tool_name) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Test whether a matcher matches a tool name
|
|
111
|
+
#
|
|
112
|
+
# @param matcher [nil, Symbol, String, Regexp, Array] Match specification
|
|
113
|
+
# @param tool_name [String, nil] Tool name to test
|
|
114
|
+
# @return [Boolean]
|
|
115
|
+
def matches?(matcher, tool_name)
|
|
116
|
+
case matcher
|
|
117
|
+
when nil then true
|
|
118
|
+
when Symbol then tool_name.to_s == matcher.to_s
|
|
119
|
+
when String then tool_name.to_s == matcher
|
|
120
|
+
when Regexp then matcher.match?(tool_name.to_s)
|
|
121
|
+
when Array then matcher.any? { |m| matches?(m, tool_name) }
|
|
122
|
+
else false
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module MCP
|
|
6
|
+
# Manages the full MCP client lifecycle
|
|
7
|
+
#
|
|
8
|
+
# Connects to an MCP server, discovers available tools, and provides
|
|
9
|
+
# methods to call tools and convert them to RubyLLM::Tool instances.
|
|
10
|
+
# Accepts an optional `transport:` for dependency injection in tests.
|
|
11
|
+
#
|
|
12
|
+
# @example Production usage
|
|
13
|
+
# server = ServerDefinition.new(name: :api, type: :http, url: "http://localhost:3000/mcp")
|
|
14
|
+
# connector = Connector.new(server)
|
|
15
|
+
# connector.connect!
|
|
16
|
+
# connector.available_tools #=> [#<MCP::Client::Tool name="echo" ...>]
|
|
17
|
+
# connector.call_tool("echo", message: "hello")
|
|
18
|
+
# connector.disconnect!
|
|
19
|
+
#
|
|
20
|
+
# @example Test usage with injected transport
|
|
21
|
+
# connector = Connector.new(server_def, transport: mock_transport)
|
|
22
|
+
# connector.connect!
|
|
23
|
+
class Connector
|
|
24
|
+
# @return [ServerDefinition] Server configuration
|
|
25
|
+
attr_reader :server_definition
|
|
26
|
+
|
|
27
|
+
# @return [Array<MCP::Client::Tool>] Discovered MCP tools
|
|
28
|
+
attr_reader :available_tools
|
|
29
|
+
|
|
30
|
+
# Create a new connector
|
|
31
|
+
#
|
|
32
|
+
# @param server_definition [ServerDefinition] Server configuration
|
|
33
|
+
# @param transport [Object, nil] Optional transport for dependency injection.
|
|
34
|
+
# Must respond to `send_request(request:)`. When nil, the transport
|
|
35
|
+
# is built automatically from the server definition.
|
|
36
|
+
#
|
|
37
|
+
# @example
|
|
38
|
+
# Connector.new(server_def)
|
|
39
|
+
# Connector.new(server_def, transport: mock_transport)
|
|
40
|
+
def initialize(server_definition, transport: nil)
|
|
41
|
+
@server_definition = server_definition
|
|
42
|
+
@injected_transport = transport
|
|
43
|
+
@client = nil
|
|
44
|
+
@transport = nil
|
|
45
|
+
@available_tools = []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Connect to the MCP server and discover tools
|
|
49
|
+
#
|
|
50
|
+
# Builds or uses the injected transport, creates an MCP::Client,
|
|
51
|
+
# and fetches the list of available tools from the server.
|
|
52
|
+
#
|
|
53
|
+
# @return [self] Returns self for chaining
|
|
54
|
+
# @raise [McpError] If the connection or tool discovery fails
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# connector.connect!
|
|
58
|
+
# connector.available_tools.map(&:name) #=> ["echo", "read_file"]
|
|
59
|
+
def connect!
|
|
60
|
+
@transport = @injected_transport || build_transport
|
|
61
|
+
@client = ::MCP::Client.new(transport: @transport)
|
|
62
|
+
@available_tools = @client.tools
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Whether the connector is currently connected
|
|
67
|
+
#
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def connected?
|
|
70
|
+
!@client.nil?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Call an MCP tool by name
|
|
74
|
+
#
|
|
75
|
+
# @param name [String, Symbol] Tool name
|
|
76
|
+
# @param arguments [Hash] Tool arguments
|
|
77
|
+
# @return [String] Tool result content
|
|
78
|
+
# @raise [McpError] If the tool is not found
|
|
79
|
+
#
|
|
80
|
+
# @example
|
|
81
|
+
# connector.call_tool("echo", message: "hello")
|
|
82
|
+
# #=> "hello"
|
|
83
|
+
def call_tool(name, **arguments)
|
|
84
|
+
tool = @available_tools.find { |t| t.name == name.to_s }
|
|
85
|
+
raise McpError, "Unknown MCP tool: #{name}" unless tool
|
|
86
|
+
|
|
87
|
+
result = @client.call_tool(tool: tool, arguments: arguments)
|
|
88
|
+
extract_content(result)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Convert MCP tools to RubyLLM::Tool instances
|
|
92
|
+
#
|
|
93
|
+
# Filters tools based on the server definition's tool list,
|
|
94
|
+
# then creates RubyLLM::Tool proxies for each.
|
|
95
|
+
#
|
|
96
|
+
# @return [Array<RubyLLM::Tool>] RubyLLM-compatible tool instances
|
|
97
|
+
#
|
|
98
|
+
# @example
|
|
99
|
+
# tools = connector.to_ruby_llm_tools
|
|
100
|
+
# chat.with_tools(*tools)
|
|
101
|
+
def to_ruby_llm_tools
|
|
102
|
+
tools_to_expose.map { |mcp_tool| ToolProxy.create(mcp_tool, self) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Disconnect from the MCP server
|
|
106
|
+
#
|
|
107
|
+
# Closes the transport and clears all state. Safe to call
|
|
108
|
+
# multiple times or when not connected.
|
|
109
|
+
#
|
|
110
|
+
# @return [void]
|
|
111
|
+
def disconnect!
|
|
112
|
+
@transport.close if @transport.respond_to?(:close)
|
|
113
|
+
@client = nil
|
|
114
|
+
@transport = nil
|
|
115
|
+
@available_tools = []
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# Select tools to expose based on the server definition filter
|
|
121
|
+
#
|
|
122
|
+
# @return [Array<MCP::Client::Tool>] Filtered or all tools
|
|
123
|
+
def tools_to_expose
|
|
124
|
+
return @available_tools unless @server_definition.filter_tools?
|
|
125
|
+
|
|
126
|
+
allowed = @server_definition.tools.map(&:to_s)
|
|
127
|
+
@available_tools.select { |t| allowed.include?(t.name) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Build a transport from the server definition
|
|
131
|
+
#
|
|
132
|
+
# @return [StdioTransport, SslHttpTransport] Transport instance
|
|
133
|
+
def build_transport
|
|
134
|
+
case @server_definition.type
|
|
135
|
+
when :stdio
|
|
136
|
+
StdioTransport.new(
|
|
137
|
+
command: @server_definition.command,
|
|
138
|
+
args: @server_definition.args,
|
|
139
|
+
env: @server_definition.env,
|
|
140
|
+
)
|
|
141
|
+
when :http
|
|
142
|
+
SslHttpTransport.new(
|
|
143
|
+
url: @server_definition.url,
|
|
144
|
+
headers: @server_definition.headers.to_h,
|
|
145
|
+
ssl_verify: resolve_ssl_verify,
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Resolve effective SSL verification setting.
|
|
151
|
+
#
|
|
152
|
+
# Per-server ssl_verify takes precedence over the global config.
|
|
153
|
+
#
|
|
154
|
+
# @return [Boolean]
|
|
155
|
+
def resolve_ssl_verify
|
|
156
|
+
server_setting = @server_definition.ssl_verify
|
|
157
|
+
return server_setting unless server_setting.nil?
|
|
158
|
+
|
|
159
|
+
Configuration.instance.mcp_ssl_verify
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Extract text content from an MCP tool call result
|
|
163
|
+
#
|
|
164
|
+
# MCP tool results have the structure:
|
|
165
|
+
# { "result" => { "content" => [{ "type" => "text", "text" => "..." }] } }
|
|
166
|
+
#
|
|
167
|
+
# @param result [Hash] Raw MCP tool call response
|
|
168
|
+
# @return [String] Extracted text content
|
|
169
|
+
def extract_content(result)
|
|
170
|
+
return result.to_s unless result.is_a?(Hash)
|
|
171
|
+
|
|
172
|
+
inner = result["result"]
|
|
173
|
+
return result.to_s unless inner.is_a?(Hash)
|
|
174
|
+
|
|
175
|
+
content = inner["content"]
|
|
176
|
+
return result.to_s unless content.is_a?(Array)
|
|
177
|
+
|
|
178
|
+
content.map { |c| c["text"] || c.to_s }.join("\n")
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module MCP
|
|
6
|
+
# Error raised for MCP-related failures
|
|
7
|
+
#
|
|
8
|
+
# Covers connection errors, tool call failures, and transport issues.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# raise McpError, "MCP server process exited unexpectedly"
|
|
12
|
+
class McpError < V3::Error; end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module MCP
|
|
6
|
+
# Immutable value object for MCP server configuration
|
|
7
|
+
#
|
|
8
|
+
# Supports two transport types:
|
|
9
|
+
# - `:stdio` — spawns a subprocess and communicates via JSON-RPC over stdin/stdout
|
|
10
|
+
# - `:http` — connects to an HTTP MCP endpoint
|
|
11
|
+
#
|
|
12
|
+
# @example Stdio server
|
|
13
|
+
# ServerDefinition.new(
|
|
14
|
+
# name: :filesystem,
|
|
15
|
+
# type: :stdio,
|
|
16
|
+
# command: "npx",
|
|
17
|
+
# args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# @example HTTP server with tool filtering
|
|
21
|
+
# ServerDefinition.new(
|
|
22
|
+
# name: :api,
|
|
23
|
+
# type: :http,
|
|
24
|
+
# url: "https://example.com/mcp",
|
|
25
|
+
# headers: { "Authorization" => "Bearer token" },
|
|
26
|
+
# tools: [:read_file, :list_directory],
|
|
27
|
+
# )
|
|
28
|
+
class ServerDefinition
|
|
29
|
+
# @return [Symbol] Server identifier
|
|
30
|
+
attr_reader :name
|
|
31
|
+
|
|
32
|
+
# @return [Symbol] Transport type (:stdio or :http)
|
|
33
|
+
attr_reader :type
|
|
34
|
+
|
|
35
|
+
# @return [Array<Symbol>, nil] Tool names to expose (nil = all)
|
|
36
|
+
attr_reader :tools
|
|
37
|
+
|
|
38
|
+
# @return [String, nil] Subprocess command (stdio only)
|
|
39
|
+
attr_reader :command
|
|
40
|
+
|
|
41
|
+
# @return [Array<String>] Subprocess arguments (stdio only)
|
|
42
|
+
attr_reader :args
|
|
43
|
+
|
|
44
|
+
# @return [Hash<String, String>] Subprocess environment variables (stdio only)
|
|
45
|
+
attr_reader :env
|
|
46
|
+
|
|
47
|
+
# @return [String, nil] HTTP endpoint URL (http only)
|
|
48
|
+
attr_reader :url
|
|
49
|
+
|
|
50
|
+
# @return [Hash<String, String>] HTTP headers (http only)
|
|
51
|
+
attr_reader :headers
|
|
52
|
+
|
|
53
|
+
# @return [Boolean, nil] Per-server SSL verification override (nil = use global config)
|
|
54
|
+
attr_reader :ssl_verify
|
|
55
|
+
|
|
56
|
+
# Create a new server definition
|
|
57
|
+
#
|
|
58
|
+
# @param name [Symbol, String] Server identifier
|
|
59
|
+
# @param type [Symbol, String] Transport type (:stdio or :http)
|
|
60
|
+
# @param tools [Array<Symbol, String>, nil] Tool names to expose (nil = all)
|
|
61
|
+
# @param command [String, nil] Subprocess command (stdio)
|
|
62
|
+
# @param args [Array<String>] Subprocess arguments (stdio)
|
|
63
|
+
# @param env [Hash] Subprocess environment variables (stdio)
|
|
64
|
+
# @param url [String, nil] HTTP endpoint URL (http)
|
|
65
|
+
# @param headers [Hash] HTTP headers (http)
|
|
66
|
+
# @param ssl_verify [Boolean, nil] Override global SSL verification (nil = use global)
|
|
67
|
+
#
|
|
68
|
+
# @raise [ConfigurationError] If required fields are missing or type is invalid
|
|
69
|
+
#
|
|
70
|
+
# @example
|
|
71
|
+
# ServerDefinition.new(name: :api, type: :http, url: "https://example.com/mcp")
|
|
72
|
+
#
|
|
73
|
+
# @example Disable SSL verification
|
|
74
|
+
# ServerDefinition.new(name: :api, type: :http, url: "https://localhost/mcp", ssl_verify: false)
|
|
75
|
+
def initialize(name:, type:, tools: nil, **options)
|
|
76
|
+
@name = name.to_sym
|
|
77
|
+
@type = type.to_sym
|
|
78
|
+
@command = options[:command]
|
|
79
|
+
@args = Array(options[:args]).freeze
|
|
80
|
+
@env = (options[:env] || {}).transform_keys(&:to_s).freeze
|
|
81
|
+
@url = options[:url]
|
|
82
|
+
@headers = (options[:headers] || {}).freeze
|
|
83
|
+
@ssl_verify = options[:ssl_verify]
|
|
84
|
+
@tools = tools ? Array(tools).map(&:to_sym).freeze : nil
|
|
85
|
+
|
|
86
|
+
validate!
|
|
87
|
+
freeze
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Whether this definition filters exposed tools
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean] true if a specific tool list was provided
|
|
93
|
+
#
|
|
94
|
+
# @example
|
|
95
|
+
# defn = ServerDefinition.new(name: :api, type: :http, url: "...", tools: [:echo])
|
|
96
|
+
# defn.filter_tools? #=> true
|
|
97
|
+
#
|
|
98
|
+
# defn = ServerDefinition.new(name: :api, type: :http, url: "...")
|
|
99
|
+
# defn.filter_tools? #=> false
|
|
100
|
+
def filter_tools?
|
|
101
|
+
!@tools.nil?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# Validate configuration
|
|
107
|
+
#
|
|
108
|
+
# @raise [ConfigurationError] If validation fails
|
|
109
|
+
def validate!
|
|
110
|
+
unless [:stdio, :http].include?(@type)
|
|
111
|
+
raise ConfigurationError, "MCP server type must be :stdio or :http, got #{@type.inspect}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if @type == :stdio && (@command.nil? || @command.to_s.strip.empty?)
|
|
115
|
+
raise ConfigurationError, "MCP stdio server requires a command"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if @type == :http && (@url.nil? || @url.to_s.strip.empty?)
|
|
119
|
+
raise ConfigurationError, "MCP http server requires a url"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|