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,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