swarm_sdk 2.7.13 → 3.0.0.alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +43 -22
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +6 -0
  4. data/lib/swarm_sdk/ruby_llm_patches/mcp_ssl_patch.rb +144 -0
  5. data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +3 -4
  6. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  7. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  8. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  9. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  10. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  11. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  12. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  13. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  14. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  15. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  16. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  17. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  18. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  19. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  20. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  24. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  25. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  26. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  27. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  28. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  29. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  30. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  31. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  32. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  33. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  34. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  35. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  36. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  37. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  38. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  39. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  40. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  41. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  42. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  43. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  45. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  46. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  47. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  48. data/lib/swarm_sdk/v3/tools/read.rb +181 -0
  49. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  50. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  51. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  52. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  53. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  54. data/lib/swarm_sdk/v3.rb +145 -0
  55. metadata +84 -148
  56. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  57. data/lib/swarm_sdk/agent/builder.rb +0 -680
  58. data/lib/swarm_sdk/agent/chat.rb +0 -1432
  59. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  60. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  61. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  62. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  63. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  64. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  65. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  66. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  67. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  68. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  69. data/lib/swarm_sdk/agent/context.rb +0 -115
  70. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  71. data/lib/swarm_sdk/agent/definition.rb +0 -581
  72. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  73. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  74. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  75. data/lib/swarm_sdk/agent_registry.rb +0 -146
  76. data/lib/swarm_sdk/builders/base_builder.rb +0 -553
  77. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  78. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  79. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  80. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  81. data/lib/swarm_sdk/config.rb +0 -367
  82. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  83. data/lib/swarm_sdk/configuration/translator.rb +0 -283
  84. data/lib/swarm_sdk/configuration.rb +0 -165
  85. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  86. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  87. data/lib/swarm_sdk/context_compactor.rb +0 -335
  88. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  89. data/lib/swarm_sdk/context_management/context.rb +0 -328
  90. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  91. data/lib/swarm_sdk/defaults.rb +0 -251
  92. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  93. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  94. data/lib/swarm_sdk/hooks/context.rb +0 -197
  95. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  96. data/lib/swarm_sdk/hooks/error.rb +0 -29
  97. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  98. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  99. data/lib/swarm_sdk/hooks/result.rb +0 -150
  100. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  101. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  102. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  103. data/lib/swarm_sdk/log_collector.rb +0 -227
  104. data/lib/swarm_sdk/log_stream.rb +0 -127
  105. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  106. data/lib/swarm_sdk/model_aliases.json +0 -8
  107. data/lib/swarm_sdk/models.json +0 -44002
  108. data/lib/swarm_sdk/models.rb +0 -161
  109. data/lib/swarm_sdk/node_context.rb +0 -245
  110. data/lib/swarm_sdk/observer/builder.rb +0 -81
  111. data/lib/swarm_sdk/observer/config.rb +0 -45
  112. data/lib/swarm_sdk/observer/manager.rb +0 -236
  113. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  114. data/lib/swarm_sdk/permissions/config.rb +0 -239
  115. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  116. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  117. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  118. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  119. data/lib/swarm_sdk/plugin.rb +0 -309
  120. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  121. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  122. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  123. data/lib/swarm_sdk/restore_result.rb +0 -65
  124. data/lib/swarm_sdk/result.rb +0 -212
  125. data/lib/swarm_sdk/snapshot.rb +0 -156
  126. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  127. data/lib/swarm_sdk/state_restorer.rb +0 -476
  128. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  129. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  130. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -195
  131. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  132. data/lib/swarm_sdk/swarm/executor.rb +0 -290
  133. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -151
  134. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  135. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -360
  136. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -270
  137. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  138. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  139. data/lib/swarm_sdk/swarm.rb +0 -843
  140. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  141. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  142. data/lib/swarm_sdk/tools/base.rb +0 -63
  143. data/lib/swarm_sdk/tools/bash.rb +0 -280
  144. data/lib/swarm_sdk/tools/clock.rb +0 -46
  145. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  146. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  147. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  148. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  149. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  150. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  151. data/lib/swarm_sdk/tools/edit.rb +0 -145
  152. data/lib/swarm_sdk/tools/glob.rb +0 -166
  153. data/lib/swarm_sdk/tools/grep.rb +0 -235
  154. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  155. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  156. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  157. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  158. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  159. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  160. data/lib/swarm_sdk/tools/read.rb +0 -261
  161. data/lib/swarm_sdk/tools/registry.rb +0 -205
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  165. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  166. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  167. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  168. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  169. data/lib/swarm_sdk/tools/think.rb +0 -100
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  174. data/lib/swarm_sdk/utils.rb +0 -68
  175. data/lib/swarm_sdk/validation_result.rb +0 -33
  176. data/lib/swarm_sdk/version.rb +0 -5
  177. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  182. data/lib/swarm_sdk/workflow.rb +0 -589
  183. data/lib/swarm_sdk.rb +0 -718
@@ -1,226 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Agent
5
- # Faraday middleware for capturing LLM API requests and responses
6
- #
7
- # This middleware intercepts HTTP calls to LLM providers and emits
8
- # structured events via LogStream for logging and monitoring.
9
- #
10
- # Events emitted:
11
- # - llm_api_request: Before sending request to LLM API
12
- # - llm_api_response: After receiving response from LLM API
13
- #
14
- # The middleware is injected at runtime into the provider's Faraday
15
- # connection stack (see Agent::Chat#inject_llm_instrumentation).
16
- class LLMInstrumentationMiddleware < Faraday::Middleware
17
- # Initialize middleware
18
- #
19
- # @param app [Faraday::Connection] Faraday app
20
- # @param on_request [Proc] Callback for request events
21
- # @param on_response [Proc] Callback for response events
22
- # @param provider_name [String] Provider name for logging
23
- def initialize(app, on_request:, on_response:, provider_name:)
24
- super(app)
25
- @on_request = on_request
26
- @on_response = on_response
27
- @provider_name = provider_name
28
- end
29
-
30
- # Intercept HTTP call
31
- #
32
- # @param env [Faraday::Env] Request environment
33
- # @return [Faraday::Response] HTTP response
34
- def call(env)
35
- start_time = Time.now
36
- accumulated_raw_chunks = []
37
-
38
- # Emit request event
39
- emit_request_event(env, start_time)
40
-
41
- # Wrap existing on_data to capture raw SSE chunks for streaming
42
- if env.request&.on_data
43
- original_on_data = env.request.on_data
44
- env.request.on_data = proc do |chunk, bytes, response_env|
45
- # Capture raw chunk BEFORE RubyLLM processes it
46
- accumulated_raw_chunks << chunk
47
- # Call original handler (RubyLLM's stream processing)
48
- original_on_data.call(chunk, bytes, response_env)
49
- end
50
- end
51
-
52
- # Execute request
53
- @app.call(env).on_complete do |response_env|
54
- end_time = Time.now
55
-
56
- # Determine if this was a streaming request based on whether chunks were accumulated
57
- # This is more reliable than parsing response content
58
- is_streaming = accumulated_raw_chunks.any?
59
-
60
- # For streaming: use accumulated raw SSE chunks
61
- # For non-streaming: use response body
62
- raw_body = is_streaming ? accumulated_raw_chunks.join : response_env.body
63
-
64
- # Store SSE body in Fiber-local for citation extraction
65
- # This allows append_citations_to_content to access the full SSE body
66
- # even though response.body is empty for streaming responses
67
- Fiber[:last_sse_body] = raw_body if is_streaming
68
-
69
- # Emit response event
70
- timing = { start_time: start_time, end_time: end_time, duration: end_time - start_time }
71
- emit_response_event(response_env, timing, raw_body, is_streaming)
72
- end
73
- end
74
-
75
- private
76
-
77
- # Emit request event
78
- #
79
- # @param env [Faraday::Env] Request environment
80
- # @param timestamp [Time] Request timestamp
81
- # @return [void]
82
- def emit_request_event(env, timestamp)
83
- request_data = {
84
- provider: @provider_name,
85
- url: env.url.to_s,
86
- body: parse_body(env.body),
87
- timestamp: timestamp.utc.iso8601,
88
- }
89
-
90
- @on_request.call(request_data)
91
- rescue StandardError => e
92
- # Don't let logging errors break the request
93
- LogStream.emit_error(e, source: "llm_instrumentation_middleware", context: "emit_request_event", provider: @provider_name)
94
- RubyLLM.logger.debug("LLM instrumentation request error: #{e.message}")
95
- end
96
-
97
- # Emit response event
98
- #
99
- # @param env [Faraday::Env] Response environment
100
- # @param timing [Hash] Timing information with :start_time, :end_time, :duration keys
101
- # @param raw_body [String, nil] Raw response body (SSE stream for streaming, JSON for non-streaming)
102
- # @param streaming [Boolean] Whether this was a streaming response (determined by chunk accumulation)
103
- # @return [void]
104
- def emit_response_event(env, timing, raw_body, streaming)
105
- response_data = {
106
- provider: @provider_name,
107
- body: parse_body(raw_body),
108
- streaming: streaming,
109
- duration_seconds: timing[:duration].round(3),
110
- timestamp: timing[:end_time].utc.iso8601,
111
- status: env.status,
112
- }
113
-
114
- # Extract usage information from response body if available
115
- if raw_body.is_a?(String) && !raw_body.empty?
116
- begin
117
- if streaming
118
- # For streaming, parse the LAST SSE event which contains usage
119
- # Skip "[DONE]" marker and find the last actual data event
120
- last_data_line = raw_body.split("\n").reverse.find { |l| l.start_with?("data:") && !l.include?("[DONE]") }
121
- if last_data_line
122
- parsed = JSON.parse(last_data_line.sub(/^data:\s*/, ""))
123
- response_data[:usage] = extract_usage(parsed) if parsed.is_a?(Hash)
124
- response_data[:model] = parsed["model"] if parsed.is_a?(Hash)
125
- end
126
- else
127
- # For non-streaming, parse the full JSON response
128
- parsed = JSON.parse(raw_body)
129
- response_data[:usage] = extract_usage(parsed) if parsed.is_a?(Hash)
130
- response_data[:model] = parsed["model"] if parsed.is_a?(Hash)
131
- response_data[:finish_reason] = extract_finish_reason(parsed) if parsed.is_a?(Hash)
132
- end
133
- rescue JSON::ParserError
134
- # Not JSON, skip usage extraction
135
- end
136
- end
137
-
138
- @on_response.call(response_data)
139
- rescue StandardError => e
140
- # Don't let logging errors break the response
141
- LogStream.emit_error(e, source: "llm_instrumentation_middleware", context: "emit_response_event", provider: @provider_name)
142
- RubyLLM.logger.debug("LLM instrumentation response error: #{e.message}")
143
- end
144
-
145
- # Sanitize headers by removing sensitive data
146
- #
147
- # @param headers [Hash] HTTP headers
148
- # @return [Hash] Sanitized headers
149
- def sanitize_headers(headers)
150
- return {} unless headers
151
-
152
- headers.transform_keys(&:to_s).transform_values do |value|
153
- # Redact authorization headers
154
- if value.to_s.match?(/bearer|token|key/i)
155
- "[REDACTED]"
156
- else
157
- value.to_s
158
- end
159
- end
160
- rescue StandardError
161
- {}
162
- end
163
-
164
- # Parse request/response body
165
- #
166
- # For requests: returns parsed JSON hash
167
- # For responses: returns full body (JSON parsed or raw string for SSE)
168
- #
169
- # @param body [String, Hash, nil] HTTP body
170
- # @return [Hash, String, nil] Parsed body
171
- def parse_body(body)
172
- return if body.nil? || body == ""
173
-
174
- # Already parsed
175
- return body if body.is_a?(Hash)
176
-
177
- # Try to parse JSON
178
- JSON.parse(body)
179
- rescue JSON::ParserError
180
- # Return full body for SSE/non-JSON responses
181
- # Don't truncate - let consumers decide how to handle large bodies
182
- body.to_s
183
- rescue StandardError
184
- nil
185
- end
186
-
187
- # Extract usage statistics from response
188
- #
189
- # Handles different provider formats (OpenAI, Anthropic, etc.)
190
- #
191
- # @param parsed [Hash] Parsed response body
192
- # @return [Hash, nil] Usage statistics
193
- def extract_usage(parsed)
194
- usage = parsed["usage"] || parsed.dig("usage")
195
- return unless usage
196
-
197
- {
198
- input_tokens: usage["input_tokens"] || usage["prompt_tokens"],
199
- output_tokens: usage["output_tokens"] || usage["completion_tokens"],
200
- total_tokens: usage["total_tokens"],
201
- }.compact
202
- rescue StandardError
203
- nil
204
- end
205
-
206
- # Extract finish reason from response
207
- #
208
- # Handles different provider formats
209
- #
210
- # @param parsed [Hash] Parsed response body
211
- # @return [String, nil] Finish reason
212
- def extract_finish_reason(parsed)
213
- # Anthropic format
214
- return parsed["stop_reason"] if parsed["stop_reason"]
215
-
216
- # OpenAI format
217
- choices = parsed["choices"]
218
- return unless choices&.is_a?(Array) && !choices.empty?
219
-
220
- choices.first["finish_reason"]
221
- rescue StandardError
222
- nil
223
- end
224
- end
225
- end
226
- end
@@ -1,161 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Agent
5
- # Builds system prompts for agents
6
- #
7
- # This class encapsulates all system prompt construction logic, including:
8
- # - Base system prompt rendering (for coding agents)
9
- # - Non-coding base prompt rendering
10
- # - Plugin prompt contribution collection
11
- # - Combining base and custom prompts
12
- #
13
- # ## Safety Note for SwarmMemory Integration
14
- #
15
- # This is an INTERNAL helper that receives Definition attributes as input.
16
- # Definition remains the single source of truth with all instance variables.
17
- # SwarmMemory uses `agent_definition.instance_eval { binding }` for ERB templating,
18
- # which requires all properties to be on Definition object. This helper is safe
19
- # because it doesn't affect Definition's structure - it only extracts logic.
20
- class SystemPromptBuilder
21
- BASE_SYSTEM_PROMPT_PATH = File.expand_path("../prompts/base_system_prompt.md.erb", __dir__)
22
-
23
- class << self
24
- # Build the complete system prompt for an agent
25
- #
26
- # @param custom_prompt [String, nil] Custom system prompt from configuration
27
- # @param coding_agent [Boolean] Whether agent is configured for coding tasks
28
- # @param disable_default_tools [Boolean, Array, nil] Default tools disable configuration
29
- # @param directory [String] Agent's working directory
30
- # @param definition [Definition] Full definition for plugin contributions
31
- # @return [String] Complete system prompt
32
- def build(custom_prompt:, coding_agent:, disable_default_tools:, directory:, definition:)
33
- new(
34
- custom_prompt: custom_prompt,
35
- coding_agent: coding_agent,
36
- disable_default_tools: disable_default_tools,
37
- directory: directory,
38
- definition: definition,
39
- ).build
40
- end
41
- end
42
-
43
- def initialize(custom_prompt:, coding_agent:, disable_default_tools:, directory:, definition:)
44
- @custom_prompt = custom_prompt
45
- @coding_agent = coding_agent
46
- @disable_default_tools = disable_default_tools
47
- @directory = directory
48
- @definition = definition
49
- end
50
-
51
- def build
52
- prompt = base_prompt_section
53
- prompt = append_plugin_contributions(prompt)
54
- prompt
55
- end
56
-
57
- private
58
-
59
- def base_prompt_section
60
- if @coding_agent
61
- build_coding_agent_prompt
62
- elsif default_tools_enabled?
63
- build_non_coding_agent_prompt
64
- else
65
- (@custom_prompt || "").to_s
66
- end
67
- end
68
-
69
- def build_coding_agent_prompt
70
- rendered_base = render_base_system_prompt
71
-
72
- if @custom_prompt && !@custom_prompt.strip.empty?
73
- "#{rendered_base}\n\n#{@custom_prompt}"
74
- else
75
- rendered_base
76
- end
77
- end
78
-
79
- def build_non_coding_agent_prompt
80
- non_coding_base = render_non_coding_base_prompt
81
-
82
- if @custom_prompt && !@custom_prompt.strip.empty?
83
- "#{non_coding_base}\n\n#{@custom_prompt}"
84
- else
85
- non_coding_base
86
- end
87
- end
88
-
89
- def default_tools_enabled?
90
- @disable_default_tools != true
91
- end
92
-
93
- def render_base_system_prompt
94
- cwd = @directory || Dir.pwd
95
- platform = RUBY_PLATFORM
96
- os_version = begin
97
- %x(uname -sr 2>/dev/null).strip
98
- rescue
99
- RUBY_PLATFORM
100
- end
101
- date = Time.now.strftime("%Y-%m-%d")
102
-
103
- template_content = File.read(BASE_SYSTEM_PROMPT_PATH)
104
- ERB.new(template_content).result(binding)
105
- end
106
-
107
- def render_non_coding_base_prompt
108
- cwd = @directory || Dir.pwd
109
- platform = RUBY_PLATFORM
110
- os_version = begin
111
- %x(uname -sr 2>/dev/null).strip
112
- rescue
113
- RUBY_PLATFORM
114
- end
115
- date = Time.now.strftime("%Y-%m-%d")
116
-
117
- <<~PROMPT.strip
118
- # Today's date
119
-
120
- <today-date>
121
- #{date}
122
- #</today-date>
123
-
124
- # Current Environment
125
-
126
- <env>
127
- Working directory: #{cwd}
128
- Platform: #{platform}
129
- OS Version: #{os_version}
130
- </env>
131
- PROMPT
132
- end
133
-
134
- def append_plugin_contributions(prompt)
135
- contributions = collect_plugin_prompt_contributions
136
- return prompt if contributions.empty?
137
-
138
- combined_contributions = contributions.join("\n\n")
139
-
140
- if prompt && !prompt.strip.empty?
141
- "#{prompt}\n\n#{combined_contributions}"
142
- else
143
- combined_contributions
144
- end
145
- end
146
-
147
- def collect_plugin_prompt_contributions
148
- contributions = []
149
-
150
- PluginRegistry.all.each do |plugin|
151
- next unless plugin.memory_configured?(@definition)
152
-
153
- contribution = plugin.system_prompt_contribution(agent_definition: @definition, storage: nil)
154
- contributions << contribution if contribution && !contribution.strip.empty?
155
- end
156
-
157
- contributions
158
- end
159
- end
160
- end
161
- end
@@ -1,189 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Agent
5
- # Per-agent tool registry managing available and active tools
6
- #
7
- # ## Architecture
8
- #
9
- # - **Available tools**: All tool instances the agent CAN use (registry)
10
- # - **Active tools**: Subset sent to LLM based on skill state
11
- #
12
- # ## Thread Safety
13
- #
14
- # Registry access is protected by Async::Semaphore for fiber-safe operations.
15
- #
16
- # @example Registering tools
17
- # registry = ToolRegistry.new
18
- # registry.register(Read.new, source: :builtin)
19
- # registry.register(delegate_tool, source: :delegation, metadata: { delegate_name: :backend })
20
- #
21
- # @example Getting active tools (no skill)
22
- # active = registry.active_tools(skill_state: nil)
23
- # # Returns ALL available tools
24
- #
25
- # @example Getting active tools (with skill)
26
- # skill_state = SkillState.new( # From SwarmMemory plugin
27
- # file_path: "skill/audit.md",
28
- # tools: ["Read", "Grep"],
29
- # permissions: { "Bash" => { deny_commands: ["rm"] } }
30
- # )
31
- # active = registry.active_tools(skill_state: skill_state)
32
- # # Returns: skill's tools + non-removable tools
33
- class ToolRegistry
34
- # Tool metadata stored in registry
35
- #
36
- # @!attribute instance [r] Tool instance (possibly wrapped with permissions)
37
- # @!attribute base_instance [r] Unwrapped tool instance (for skill permission override)
38
- # @!attribute removable [r] Can be deactivated by skills
39
- # @!attribute source [r] Tool source (:builtin, :delegation, :mcp, :plugin, :custom)
40
- # @!attribute metadata [r] Source-specific metadata
41
- ToolEntry = Data.define(:instance, :base_instance, :removable, :source, :metadata)
42
-
43
- def initialize
44
- @available_tools = {} # String name => ToolEntry
45
- @mutex = Async::Semaphore.new(1) # Fiber-safe mutex
46
- end
47
-
48
- # Register a tool in the available tools registry
49
- #
50
- # @param tool [RubyLLM::Tool] Tool instance (possibly wrapped)
51
- # @param base_tool [RubyLLM::Tool, nil] Unwrapped instance (for permission override)
52
- # @param source [Symbol] Tool source (:builtin, :delegation, :mcp, :plugin, :custom)
53
- # @param metadata [Hash] Source-specific metadata (server_name, plugin_name, etc.)
54
- # @return [void]
55
- #
56
- # @example Register builtin tool
57
- # registry.register(Read.new, source: :builtin)
58
- #
59
- # @example Register delegation tool
60
- # registry.register(delegate_tool, source: :delegation, metadata: { delegate_name: :backend })
61
- #
62
- # @example Register MCP tool
63
- # registry.register(mcp_tool, source: :mcp, metadata: { server_name: "codebase" })
64
- #
65
- # @example Register with permission wrapper
66
- # wrapped_tool = PermissionWrapper.new(base_tool, permissions)
67
- # registry.register(wrapped_tool, base_tool: base_tool, source: :builtin)
68
- def register(tool, base_tool: nil, source:, metadata: {})
69
- @mutex.acquire do
70
- # Infer removability from tool class
71
- removable = tool.respond_to?(:removable?) ? tool.removable? : true
72
-
73
- @available_tools[tool.name] = ToolEntry.new(
74
- instance: tool,
75
- base_instance: base_tool || tool, # If no base, use same instance
76
- removable: removable,
77
- source: source,
78
- metadata: metadata,
79
- )
80
- end
81
- end
82
-
83
- # Unregister a tool (for testing/cleanup)
84
- #
85
- # @param name [String, Symbol] Tool name
86
- # @return [ToolEntry, nil] Removed entry
87
- def unregister(name)
88
- @mutex.acquire do
89
- @available_tools.delete(name.to_s)
90
- end
91
- end
92
-
93
- # Get active tools based on skill state
94
- #
95
- # Returns Hash of tool instances ready for RubyLLM::Chat.
96
- #
97
- # Logic:
98
- # - If skill_state is nil: Return ALL available tools
99
- # - If skill_state restricts tools: Return skill's tools + non-removable tools
100
- # - Skill permissions are applied during activation (wrapping base_instance)
101
- #
102
- # @param skill_state [Object, nil] Skill state object (from plugin), or nil for all
103
- # @param tool_configurator [ToolConfigurator, nil] For permission wrapping
104
- # @param agent_definition [Agent::Definition, nil] For permission wrapping
105
- # @return [Hash{String => RubyLLM::Tool}] name => instance mapping
106
- #
107
- # @example No skill loaded - all tools
108
- # registry.active_tools(skill_state: nil)
109
- # # => { "Read" => <Read>, "WorkWithBackend" => <Delegate>, ... }
110
- #
111
- # @example Skill loaded with focused toolset
112
- # registry.active_tools(skill_state: skill_state)
113
- # # => { "Read" => <Read>, "WorkWithBackend" => <Delegate>, "Think" => <Think>, "MemoryRead" => <MemoryRead> }
114
- # # Includes: requested tools + non-removable tools
115
- def active_tools(skill_state: nil, tool_configurator: nil, agent_definition: nil)
116
- @mutex.acquire do
117
- result = if skill_state&.restricts_tools?
118
- # Skill loaded with tool restriction - only skill's tools + non-removable
119
- filtered = {}
120
-
121
- # Always include non-removable tools (use wrapped instance)
122
- @available_tools.each do |name, entry|
123
- filtered[name] = entry.instance unless entry.removable
124
- end
125
-
126
- # Add requested tools from skill
127
- skill_state.tools.each do |name|
128
- entry = @available_tools[name.to_s]
129
- next unless entry
130
-
131
- # Check if skill has custom permissions for this tool
132
- skill_permissions = skill_state.permissions_for(name)
133
-
134
- if skill_permissions && tool_configurator && agent_definition
135
- # Skill overrides permissions - wrap the BASE instance
136
- wrapped = tool_configurator.wrap_tool_with_permissions(
137
- entry.base_instance,
138
- skill_permissions,
139
- agent_definition,
140
- )
141
- filtered[name.to_s] = wrapped
142
- else
143
- # No skill permission override - use registered instance
144
- filtered[name.to_s] = entry.instance
145
- end
146
- end
147
-
148
- filtered
149
- else
150
- # No skill OR skill doesn't restrict tools - all available tools
151
- @available_tools.transform_values(&:instance)
152
- end
153
-
154
- result
155
- end
156
- end
157
-
158
- # Check if tool exists in registry
159
- #
160
- # @param name [String, Symbol] Tool name
161
- # @return [Boolean]
162
- def has_tool?(name)
163
- @available_tools.key?(name.to_s)
164
- end
165
-
166
- # Get all available tool names
167
- #
168
- # @return [Array<String>]
169
- def tool_names
170
- @available_tools.keys
171
- end
172
-
173
- # Get tool entry with metadata
174
- #
175
- # @param name [String, Symbol] Tool name
176
- # @return [ToolEntry, nil]
177
- def get(name)
178
- @available_tools[name.to_s]
179
- end
180
-
181
- # Get all non-removable tool names
182
- #
183
- # @return [Array<String>]
184
- def non_removable_tool_names
185
- @available_tools.select { |_name, entry| !entry.removable }.keys
186
- end
187
- end
188
- end
189
- end