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,533 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ # DSL builder for creating a single V3 Agent
6
+ #
7
+ # Provides a clean, block-based interface for defining an agent without
8
+ # needing to construct AgentDefinition kwargs directly. Unlike the V2
9
+ # builder which manages multi-agent swarms, this creates a single agent.
10
+ #
11
+ # @example Minimal agent
12
+ # agent = SwarmSDK::V3.agent do
13
+ # name :assistant
14
+ # description "A helpful assistant"
15
+ # end
16
+ #
17
+ # @example Full agent with memory
18
+ # agent = SwarmSDK::V3.agent do
19
+ # name :researcher
20
+ # description "Research specialist"
21
+ # model "claude-sonnet-4"
22
+ # provider "anthropic"
23
+ # system_prompt "You are an expert researcher."
24
+ # tools :Read, :Write, :Edit, :Bash, :Grep, :Glob
25
+ # directory "/path/to/project"
26
+ #
27
+ # memory do
28
+ # directory ".swarm/memory"
29
+ # stm_turns 8
30
+ # retrieval_top_k 15
31
+ # end
32
+ # end
33
+ #
34
+ # agent.ask("Hello!")
35
+ class AgentBuilder
36
+ class << self
37
+ # Build an Agent from a DSL block
38
+ #
39
+ # @yield DSL block evaluated via instance_eval
40
+ # @return [Agent] Initialized agent ready for use
41
+ #
42
+ # @example
43
+ # agent = AgentBuilder.build do
44
+ # name :coder
45
+ # description "Code writer"
46
+ # tools :Read, :Edit
47
+ # end
48
+ def build(&block)
49
+ builder = new
50
+ builder.instance_eval(&block)
51
+ builder.to_agent
52
+ end
53
+ end
54
+
55
+ def initialize
56
+ @config = {}
57
+ @tools = []
58
+ @skills = []
59
+ @mcp_servers = []
60
+ @hooks = []
61
+ @memory_config = nil
62
+ end
63
+
64
+ # @!group DSL Methods
65
+
66
+ # Set agent name
67
+ #
68
+ # @param value [Symbol, String] Agent identifier
69
+ # @return [void]
70
+ #
71
+ # @example
72
+ # name :researcher
73
+ def name(value)
74
+ @config[:name] = value
75
+ end
76
+
77
+ # Set agent description
78
+ #
79
+ # @param value [String] Agent role description
80
+ # @return [void]
81
+ #
82
+ # @example
83
+ # description "Research specialist"
84
+ def description(value)
85
+ @config[:description] = value
86
+ end
87
+
88
+ # Set LLM model
89
+ #
90
+ # @param value [String] Model identifier
91
+ # @return [void]
92
+ #
93
+ # @example
94
+ # model "claude-sonnet-4"
95
+ def model(value)
96
+ @config[:model] = value
97
+ end
98
+
99
+ # Set LLM provider
100
+ #
101
+ # @param value [String] Provider name
102
+ # @return [void]
103
+ #
104
+ # @example
105
+ # provider "anthropic"
106
+ def provider(value)
107
+ @config[:provider] = value
108
+ end
109
+
110
+ # Set system prompt instructions
111
+ #
112
+ # @param value [String] System prompt text
113
+ # @return [void]
114
+ #
115
+ # @example
116
+ # system_prompt "You are an expert researcher."
117
+ def system_prompt(value)
118
+ @config[:system_prompt] = value
119
+ end
120
+
121
+ # Add tools to the agent
122
+ #
123
+ # Can be called multiple times; tools accumulate across calls.
124
+ #
125
+ # @param names [Array<Symbol, String>] Tool names
126
+ # @return [void]
127
+ #
128
+ # @example Single call
129
+ # tools :Read, :Write, :Edit
130
+ #
131
+ # @example Multiple calls accumulate
132
+ # tools :Read, :Write
133
+ # tools :Bash, :Grep
134
+ # # => [:Read, :Write, :Bash, :Grep]
135
+ def tools(*names)
136
+ @tools.concat(names.flatten)
137
+ end
138
+
139
+ # Add skill directories to the agent
140
+ #
141
+ # Can be called multiple times; paths accumulate across calls.
142
+ # Each path should be a directory containing a SKILL.md file,
143
+ # or a parent directory whose children contain SKILL.md files.
144
+ #
145
+ # @param paths [Array<String>] Skill directory paths
146
+ # @return [void]
147
+ #
148
+ # @example Single call
149
+ # skills "/path/to/skills"
150
+ #
151
+ # @example Multiple calls accumulate
152
+ # skills "/skills/pdf"
153
+ # skills "/skills/csv"
154
+ def skills(*paths)
155
+ @skills.concat(paths.flatten)
156
+ end
157
+
158
+ # Set working directory
159
+ #
160
+ # @param value [String] Directory path
161
+ # @return [void]
162
+ #
163
+ # @example
164
+ # directory "/path/to/project"
165
+ def directory(value)
166
+ @config[:directory] = value
167
+ end
168
+
169
+ # Set custom API endpoint
170
+ #
171
+ # @param value [String] Base URL
172
+ # @return [void]
173
+ #
174
+ # @example
175
+ # base_url "https://api.example.com"
176
+ def base_url(value)
177
+ @config[:base_url] = value
178
+ end
179
+
180
+ # Set maximum concurrent tool executions
181
+ #
182
+ # @param value [Integer] Max concurrent tools
183
+ # @return [void]
184
+ #
185
+ # @example
186
+ # max_concurrent_tools 5
187
+ def max_concurrent_tools(value)
188
+ @config[:max_concurrent_tools] = value
189
+ end
190
+
191
+ # Set raw API body parameters
192
+ #
193
+ # @param value [Hash] Parameters hash
194
+ # @return [void]
195
+ #
196
+ # @example
197
+ # parameters(temperature: 0.7, thinking: { type: "enabled", budget_tokens: 10_000 })
198
+ def parameters(value)
199
+ @config[:parameters] = value
200
+ end
201
+
202
+ # Set raw HTTP headers
203
+ #
204
+ # @param value [Hash] Headers hash
205
+ # @return [void]
206
+ #
207
+ # @example
208
+ # headers("anthropic-beta" => "some-feature")
209
+ def headers(value)
210
+ @config[:headers] = value
211
+ end
212
+
213
+ # Set output schema for structured output
214
+ #
215
+ # The schema is passed through to RubyLLM's Chat#with_schema.
216
+ # Can be a Hash (raw JSON Schema) or any object responding to #to_json_schema.
217
+ #
218
+ # @param value [Hash, Object] Schema definition
219
+ # @return [void]
220
+ #
221
+ # @example Raw JSON Schema
222
+ # output_schema({ type: "object", properties: { name: { type: "string" } } })
223
+ def output_schema(value)
224
+ @config[:output_schema] = value
225
+ end
226
+
227
+ # Set API version for OpenAI provider
228
+ #
229
+ # Controls which OpenAI API endpoint to use. Only applicable when
230
+ # provider is "openai". Defaults to "v1/responses" for OpenAI agents.
231
+ #
232
+ # @param value [String] API version ("v1/responses" or "v1/chat/completions")
233
+ # @return [void]
234
+ #
235
+ # @example Use Chat Completions API
236
+ # api_version "v1/chat/completions"
237
+ #
238
+ # @example Use Responses API (default for OpenAI)
239
+ # api_version "v1/responses"
240
+ def api_version(value)
241
+ @config[:api_version] = value
242
+ end
243
+
244
+ # Configure memory via a sub-block
245
+ #
246
+ # @yield DSL block evaluated on a MemoryBuilder
247
+ # @return [void]
248
+ #
249
+ # @example
250
+ # memory do
251
+ # directory ".swarm/memory"
252
+ # stm_turns 8
253
+ # retrieval_top_k 15
254
+ # end
255
+ def memory(&block)
256
+ @memory_config = MemoryBuilder.build(&block)
257
+ end
258
+
259
+ # Register a before_ask hook
260
+ #
261
+ # Fires before {Agent#execute_turn}. Can halt (returns nil from ask)
262
+ # or replace the prompt.
263
+ #
264
+ # @yield [ctx] Hook block receiving a {Hooks::Context}
265
+ # @return [void]
266
+ #
267
+ # @example Log every prompt
268
+ # before_ask { |ctx| puts "Processing: #{ctx.prompt}" }
269
+ #
270
+ # @example Block certain prompts
271
+ # before_ask { |ctx| ctx.halt("Blocked") if ctx.prompt.include?("secret") }
272
+ def before_ask(&block)
273
+ @hooks << { event: :before_ask, block: block }
274
+ end
275
+
276
+ # Register an after_ask hook
277
+ #
278
+ # Fires after {Agent#execute_turn} completes. Observation only —
279
+ # halt and replace have no effect.
280
+ #
281
+ # @yield [ctx] Hook block receiving a {Hooks::Context}
282
+ # @return [void]
283
+ #
284
+ # @example Log responses
285
+ # after_ask { |ctx| log_response(ctx.response) }
286
+ def after_ask(&block)
287
+ @hooks << { event: :after_ask, block: block }
288
+ end
289
+
290
+ # Register a before_tool hook
291
+ #
292
+ # Fires before tool execution. Can halt (returns error string to LLM
293
+ # without executing the tool). Supports match filtering.
294
+ #
295
+ # @param match [Symbol, String, Regexp, Array, nil] Tool name matcher
296
+ # @yield [ctx] Hook block receiving a {Hooks::Context}
297
+ # @return [void]
298
+ #
299
+ # @example Block Bash tool
300
+ # before_tool(match: :Bash) { |ctx| ctx.halt("Bash disabled") }
301
+ #
302
+ # @example Validate Write/Edit arguments
303
+ # before_tool(match: [:Write, :Edit]) { |ctx| validate(ctx.tool_arguments) }
304
+ def before_tool(match: nil, &block)
305
+ @hooks << { event: :before_tool, match: compile_matcher(match), block: block }
306
+ end
307
+
308
+ # Register an after_tool hook
309
+ #
310
+ # Fires after tool execution. Can replace the tool result.
311
+ # Supports match filtering.
312
+ #
313
+ # @param match [Symbol, String, Regexp, Array, nil] Tool name matcher
314
+ # @yield [ctx] Hook block receiving a {Hooks::Context}
315
+ # @return [void]
316
+ #
317
+ # @example Sanitize Bash output
318
+ # after_tool(match: /Bash/) { |ctx| ctx.replace(sanitize(ctx.tool_result)) }
319
+ def after_tool(match: nil, &block)
320
+ @hooks << { event: :after_tool, match: compile_matcher(match), block: block }
321
+ end
322
+
323
+ # Register an on_stop hook
324
+ #
325
+ # Fires after ask() completes successfully. Observation only —
326
+ # the response has already been produced. Useful for logging,
327
+ # metrics, or cleanup.
328
+ #
329
+ # @yield [ctx] Hook block receiving a {Hooks::Context}
330
+ # @return [void]
331
+ #
332
+ # @example Track completions
333
+ # on_stop { |ctx| metrics.increment("agent.#{ctx.agent_name}.completed") }
334
+ def on_stop(&block)
335
+ @hooks << { event: :on_stop, block: block }
336
+ end
337
+
338
+ # Add an MCP server connection
339
+ #
340
+ # Can be called multiple times; servers accumulate across calls.
341
+ # Each server provides tools that the agent can invoke via MCP protocol.
342
+ #
343
+ # @param name [Symbol, String] Server identifier
344
+ # @param options [Hash] Server configuration options
345
+ # @option options [Symbol] :type Transport type (:stdio or :http)
346
+ # @option options [String] :command Subprocess command (stdio)
347
+ # @option options [Array<String>] :args Subprocess arguments (stdio)
348
+ # @option options [Hash] :env Subprocess environment (stdio)
349
+ # @option options [String] :url HTTP endpoint URL (http)
350
+ # @option options [Hash] :headers HTTP headers (http)
351
+ # @option options [Array<Symbol>] :tools Tool names to expose (nil = all)
352
+ # @return [void]
353
+ #
354
+ # @example HTTP server
355
+ # mcp_server :api,
356
+ # type: :http,
357
+ # url: "https://example.com/mcp",
358
+ # headers: { "Authorization" => "Bearer token" }
359
+ #
360
+ # @example Stdio server with tool filtering
361
+ # mcp_server :filesystem,
362
+ # type: :stdio,
363
+ # command: "npx",
364
+ # args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
365
+ # tools: [:read_file, :list_directory]
366
+ def mcp_server(name, **options)
367
+ @mcp_servers << options.merge(name: name)
368
+ end
369
+
370
+ # @!endgroup
371
+
372
+ # Build the Agent from collected configuration
373
+ #
374
+ # @return [Agent] Initialized agent
375
+ # @raise [ConfigurationError] If required fields are missing
376
+ def to_agent
377
+ definition = AgentDefinition.new(**definition_kwargs)
378
+ Agent.new(definition)
379
+ end
380
+
381
+ private
382
+
383
+ # Assemble keyword arguments for AgentDefinition
384
+ #
385
+ # @return [Hash] Keyword arguments
386
+ def definition_kwargs
387
+ kwargs = @config.dup
388
+ kwargs[:tools] = @tools unless @tools.empty?
389
+ kwargs[:skills] = @skills unless @skills.empty?
390
+ kwargs[:mcp_servers] = @mcp_servers unless @mcp_servers.empty?
391
+ kwargs[:hooks] = @hooks unless @hooks.empty?
392
+ kwargs.merge!(@memory_config) if @memory_config
393
+ kwargs
394
+ end
395
+
396
+ # Compile a match specification into a normalized matcher
397
+ #
398
+ # Handles Symbol, String, Regexp, Array, and nil inputs.
399
+ # Arrays are recursively compiled so nested values are normalized.
400
+ #
401
+ # @param match [Symbol, String, Regexp, Array, nil] Raw match input
402
+ # @return [Symbol, String, Regexp, Array, nil] Compiled matcher
403
+ def compile_matcher(match)
404
+ case match
405
+ when nil, Symbol, String, Regexp then match
406
+ when Array then match.map { |m| compile_matcher(m) }
407
+ else
408
+ raise ArgumentError, "Invalid match type: #{match.class}. Use Symbol, String, Regexp, Array, or nil."
409
+ end
410
+ end
411
+ end
412
+
413
+ # Sub-builder for memory configuration within AgentBuilder
414
+ #
415
+ # Collects memory-related settings and returns them as a hash
416
+ # with the `memory_` prefix expected by AgentDefinition.
417
+ #
418
+ # @example
419
+ # config = MemoryBuilder.build do
420
+ # directory ".swarm/memory"
421
+ # stm_turns 8
422
+ # end
423
+ # # => { memory_directory: ".swarm/memory", memory_stm_turns: 8 }
424
+ class MemoryBuilder
425
+ class << self
426
+ # Build memory config from a DSL block
427
+ #
428
+ # @yield DSL block evaluated via instance_eval
429
+ # @return [Hash] Memory configuration with `memory_` prefixed keys
430
+ def build(&block)
431
+ builder = new
432
+ builder.instance_eval(&block)
433
+ builder.to_h
434
+ end
435
+ end
436
+
437
+ def initialize
438
+ @config = {}
439
+ end
440
+
441
+ # Set memory storage directory
442
+ #
443
+ # @param value [String] Directory path
444
+ # @return [void]
445
+ def directory(value)
446
+ @config[:memory_directory] = value
447
+ end
448
+
449
+ # Set number of recent turns to keep in short-term memory
450
+ #
451
+ # @param value [Integer] STM turn count
452
+ # @return [void]
453
+ def stm_turns(value)
454
+ @config[:memory_stm_turns] = value
455
+ end
456
+
457
+ # Set number of memory cards to retrieve per turn
458
+ #
459
+ # @param value [Integer] Top-k retrieval count
460
+ # @return [void]
461
+ def retrieval_top_k(value)
462
+ @config[:memory_retrieval_top_k] = value
463
+ end
464
+
465
+ # Set semantic search weight for hybrid retrieval
466
+ #
467
+ # @param value [Float] Weight between 0.0 and 1.0
468
+ # @return [void]
469
+ def semantic_weight(value)
470
+ @config[:memory_semantic_weight] = value
471
+ end
472
+
473
+ # Set keyword search weight for hybrid retrieval
474
+ #
475
+ # @param value [Float] Weight between 0.0 and 1.0
476
+ # @return [void]
477
+ def keyword_weight(value)
478
+ @config[:memory_keyword_weight] = value
479
+ end
480
+
481
+ # Valid adapter type symbols
482
+ VALID_ADAPTER_TYPES = [:sqlite, :filesystem].freeze
483
+
484
+ # Set the memory adapter
485
+ #
486
+ # @param value [Symbol, Memory::Adapters::Base] Adapter type or instance
487
+ # @return [void]
488
+ # @raise [ArgumentError] If value is not a valid adapter type or instance
489
+ #
490
+ # @example Using a symbol
491
+ # adapter :sqlite
492
+ # adapter :filesystem
493
+ #
494
+ # @example Using an adapter instance
495
+ # adapter MyCustomAdapter.new("/path/to/storage")
496
+ def adapter(value)
497
+ case value
498
+ when Symbol
499
+ unless VALID_ADAPTER_TYPES.include?(value)
500
+ raise ArgumentError, "Unknown memory adapter type: #{value.inspect}. " \
501
+ "Valid types are: #{VALID_ADAPTER_TYPES.map(&:inspect).join(", ")}"
502
+ end
503
+ when Memory::Adapters::Base
504
+ # Valid adapter instance
505
+ else
506
+ raise ArgumentError, "Memory adapter must be a Symbol (#{VALID_ADAPTER_TYPES.map(&:inspect).join(", ")}) " \
507
+ "or an instance of Memory::Adapters::Base, got: #{value.class}"
508
+ end
509
+ @config[:memory_adapter] = value
510
+ end
511
+
512
+ # Enable associative memory
513
+ #
514
+ # When enabled, the agent naturally surfaces tangential memories
515
+ # in conversation, like a person who brings up related topics.
516
+ # Exploration cards get a distinct "YOU ALSO REMEMBER" section
517
+ # and guidance is injected into the system prompt.
518
+ #
519
+ # @param value [Boolean] Whether to enable associative memory
520
+ # @return [void]
521
+ def associative(value)
522
+ @config[:memory_associative] = value
523
+ end
524
+
525
+ # Return collected memory configuration
526
+ #
527
+ # @return [Hash] Memory config with `memory_` prefixed keys
528
+ def to_h
529
+ @config.dup
530
+ end
531
+ end
532
+ end
533
+ end