swarm_sdk 2.7.14 → 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 (181) 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/edit.rb +111 -0
  42. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  43. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  44. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  45. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  46. data/lib/swarm_sdk/v3/tools/read.rb +181 -0
  47. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  48. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  49. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  50. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  51. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  52. data/lib/swarm_sdk/v3.rb +145 -0
  53. metadata +83 -148
  54. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  55. data/lib/swarm_sdk/agent/builder.rb +0 -705
  56. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  57. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  58. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  59. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  60. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  61. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  62. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  63. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  64. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  65. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  66. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  67. data/lib/swarm_sdk/agent/context.rb +0 -115
  68. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  69. data/lib/swarm_sdk/agent/definition.rb +0 -588
  70. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  71. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  72. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  73. data/lib/swarm_sdk/agent_registry.rb +0 -146
  74. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  75. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  76. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  77. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  78. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  79. data/lib/swarm_sdk/config.rb +0 -368
  80. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  81. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  82. data/lib/swarm_sdk/configuration.rb +0 -165
  83. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  84. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  85. data/lib/swarm_sdk/context_compactor.rb +0 -335
  86. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  87. data/lib/swarm_sdk/context_management/context.rb +0 -328
  88. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  89. data/lib/swarm_sdk/defaults.rb +0 -251
  90. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  91. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  92. data/lib/swarm_sdk/hooks/context.rb +0 -197
  93. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  94. data/lib/swarm_sdk/hooks/error.rb +0 -29
  95. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  96. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  97. data/lib/swarm_sdk/hooks/result.rb +0 -150
  98. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  99. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  100. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  101. data/lib/swarm_sdk/log_collector.rb +0 -227
  102. data/lib/swarm_sdk/log_stream.rb +0 -127
  103. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  104. data/lib/swarm_sdk/model_aliases.json +0 -8
  105. data/lib/swarm_sdk/models.json +0 -44002
  106. data/lib/swarm_sdk/models.rb +0 -161
  107. data/lib/swarm_sdk/node_context.rb +0 -245
  108. data/lib/swarm_sdk/observer/builder.rb +0 -81
  109. data/lib/swarm_sdk/observer/config.rb +0 -45
  110. data/lib/swarm_sdk/observer/manager.rb +0 -248
  111. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  112. data/lib/swarm_sdk/permissions/config.rb +0 -239
  113. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  114. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  115. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  116. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  117. data/lib/swarm_sdk/plugin.rb +0 -309
  118. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  119. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  120. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  121. data/lib/swarm_sdk/restore_result.rb +0 -65
  122. data/lib/swarm_sdk/result.rb +0 -241
  123. data/lib/swarm_sdk/snapshot.rb +0 -156
  124. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  125. data/lib/swarm_sdk/state_restorer.rb +0 -476
  126. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  127. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  128. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  129. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  130. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  131. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  132. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  133. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  134. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  135. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  136. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  137. data/lib/swarm_sdk/swarm.rb +0 -973
  138. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  139. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  140. data/lib/swarm_sdk/tools/base.rb +0 -63
  141. data/lib/swarm_sdk/tools/bash.rb +0 -280
  142. data/lib/swarm_sdk/tools/clock.rb +0 -46
  143. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  144. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  145. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  146. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  147. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  148. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  149. data/lib/swarm_sdk/tools/edit.rb +0 -145
  150. data/lib/swarm_sdk/tools/glob.rb +0 -166
  151. data/lib/swarm_sdk/tools/grep.rb +0 -235
  152. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  153. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  154. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  155. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  156. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  157. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  158. data/lib/swarm_sdk/tools/read.rb +0 -261
  159. data/lib/swarm_sdk/tools/registry.rb +0 -205
  160. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  161. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  163. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  164. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  165. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  166. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  167. data/lib/swarm_sdk/tools/think.rb +0 -100
  168. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  169. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  170. data/lib/swarm_sdk/tools/write.rb +0 -112
  171. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  172. data/lib/swarm_sdk/utils.rb +0 -68
  173. data/lib/swarm_sdk/validation_result.rb +0 -33
  174. data/lib/swarm_sdk/version.rb +0 -5
  175. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  176. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  177. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  178. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  179. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  180. data/lib/swarm_sdk/workflow.rb +0 -589
  181. data/lib/swarm_sdk.rb +0 -721
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ # Registry for V3 tools (built-in and custom)
7
+ #
8
+ # Maps tool names (symbols) to their V3 tool classes.
9
+ # Provides lookup, validation, factory, and custom tool registration.
10
+ #
11
+ # Tools fall into categories based on creation requirements:
12
+ # 1. **No params**: Simple tools (Think, Clock)
13
+ # 2. **Directory only**: Tools needing working directory (Bash, Grep, Glob)
14
+ # 3. **Agent context**: Tools needing agent name + directory (Read, Write, Edit)
15
+ #
16
+ # @example Look up a tool
17
+ # klass = Registry.get(:Read)
18
+ #
19
+ # @example Create a tool instance
20
+ # tool = Registry.create(:Read, agent_name: :backend, directory: "/app")
21
+ #
22
+ # @example Register a custom tool
23
+ # Registry.register(:MyTool, MyCustomTool)
24
+ #
25
+ # @example List available tools
26
+ # Registry.available_names #=> [:Read, :Write, :Edit, ...]
27
+ class Registry
28
+ class << self
29
+ # Lazily-built tool mapping
30
+ #
31
+ # Uses lazy evaluation so tool classes are only resolved when first accessed,
32
+ # allowing Zeitwerk to load them on demand. Mutable to support custom registration.
33
+ #
34
+ # @return [Hash<Symbol, Class>] Tool name to class mapping
35
+ def builtin_tools
36
+ @builtin_tools ||= {
37
+ Read: SwarmSDK::V3::Tools::Read,
38
+ Write: SwarmSDK::V3::Tools::Write,
39
+ Edit: SwarmSDK::V3::Tools::Edit,
40
+ Bash: SwarmSDK::V3::Tools::Bash,
41
+ Grep: SwarmSDK::V3::Tools::Grep,
42
+ Glob: SwarmSDK::V3::Tools::Glob,
43
+ Think: SwarmSDK::V3::Tools::Think,
44
+ Clock: SwarmSDK::V3::Tools::Clock,
45
+ SubTask: SwarmSDK::V3::Tools::SubTask,
46
+ }
47
+ end
48
+
49
+ # Register a custom tool
50
+ #
51
+ # @param name [Symbol, String] Tool name
52
+ # @param klass [Class] Tool class (must inherit from Base or RubyLLM::Tool)
53
+ # @return [void]
54
+ # @raise [ConfigurationError] If name is already taken by a built-in tool
55
+ #
56
+ # @example
57
+ # Registry.register(:WebSearch, MyWebSearchTool)
58
+ def register(name, klass)
59
+ name_sym = name.to_sym
60
+ builtin_tools[name_sym] = klass
61
+ end
62
+
63
+ # Reset registry to built-in tools only
64
+ #
65
+ # Removes all custom tool registrations. Useful for test cleanup.
66
+ #
67
+ # @return [void]
68
+ def reset!
69
+ @builtin_tools = nil
70
+ end
71
+
72
+ # Get tool class by name
73
+ #
74
+ # Respects the `registered_tools` configuration filter. If `registered_tools`
75
+ # is set, only those tools are visible.
76
+ #
77
+ # @param name [Symbol, String] Tool name
78
+ # @return [Class, nil] Tool class or nil if not found/filtered
79
+ #
80
+ # @example
81
+ # Registry.get(:Read) #=> SwarmSDK::V3::Tools::Read
82
+ def get(name)
83
+ name_sym = name.to_sym
84
+ allowed = Configuration.instance.registered_tools
85
+ return if allowed && !allowed.map(&:to_sym).include?(name_sym)
86
+
87
+ builtin_tools[name_sym]
88
+ end
89
+
90
+ # Create a tool instance with context
91
+ #
92
+ # Uses the tool's `creation_requirements` to determine constructor params.
93
+ #
94
+ # @param name [Symbol, String] Tool name
95
+ # @param context [Hash] Available context for tool creation
96
+ # @option context [Symbol] :agent_name Agent identifier
97
+ # @option context [String] :directory Agent's working directory
98
+ # @return [RubyLLM::Tool] Instantiated tool
99
+ # @raise [ConfigurationError] If tool unknown or requirements unmet
100
+ #
101
+ # @example
102
+ # tool = Registry.create(:Read, agent_name: :backend, directory: "/app")
103
+ def create(name, **context)
104
+ name_sym = name.to_sym
105
+ tool_class = get(name_sym)
106
+
107
+ raise ConfigurationError, "Unknown tool: #{name}" unless tool_class
108
+
109
+ if tool_class.respond_to?(:creation_requirements) && tool_class.creation_requirements.any?
110
+ requirements = tool_class.creation_requirements
111
+ params = extract_params(requirements, context, name)
112
+ tool_class.new(**params)
113
+ else
114
+ tool_class.new
115
+ end
116
+ end
117
+
118
+ # Create all tools for an agent definition
119
+ #
120
+ # Merges the agent's tools with global `default_tools` from configuration.
121
+ # Filters against `registered_tools` if configured.
122
+ #
123
+ # @param definition [AgentDefinition] Agent definition with tool list
124
+ # @return [Array<RubyLLM::Tool>] Instantiated tools
125
+ # @raise [ConfigurationError] If any tool is unknown
126
+ #
127
+ # @example
128
+ # tools = Registry.create_all(definition)
129
+ def create_all(definition, memory_store: nil, subtask_depth: 0)
130
+ # Create shared read tracker for cross-tool enforcement
131
+ read_tracker = ReadTracker.new
132
+
133
+ context = {
134
+ agent_name: definition.name,
135
+ directory: definition.directory,
136
+ read_tracker: read_tracker,
137
+ memory_store: memory_store,
138
+ agent_definition: definition,
139
+ subtask_depth: subtask_depth,
140
+ }
141
+
142
+ config = Configuration.instance
143
+
144
+ # Start with the agent's declared tools + global default_tools
145
+ tool_names = definition.tools.dup
146
+ config.default_tools.each do |name|
147
+ tool_names << name.to_sym unless tool_names.include?(name.to_sym)
148
+ end
149
+
150
+ # Filter against registered_tools if configured
151
+ if config.registered_tools
152
+ allowed = config.registered_tools.map(&:to_sym)
153
+ tool_names.select! { |name| allowed.include?(name) }
154
+ end
155
+
156
+ tool_names.uniq.map { |name| create(name, **context) }
157
+ end
158
+
159
+ # Check if a tool exists (respects registered_tools filter)
160
+ #
161
+ # @param name [Symbol, String] Tool name
162
+ # @return [Boolean]
163
+ def exists?(name)
164
+ !get(name.to_sym).nil?
165
+ end
166
+
167
+ # Get all available tool names (respects registered_tools filter)
168
+ #
169
+ # @return [Array<Symbol>]
170
+ def available_names
171
+ allowed = Configuration.instance.registered_tools
172
+ names = builtin_tools.keys
173
+ names.select! { |n| allowed.map(&:to_sym).include?(n) } if allowed
174
+ names
175
+ end
176
+
177
+ # Validate tool names
178
+ #
179
+ # @param names [Array<Symbol, String>] Tool names to validate
180
+ # @return [Array<Symbol>] Invalid tool names
181
+ def validate(names)
182
+ names.map(&:to_sym).reject { |name| exists?(name) }
183
+ end
184
+
185
+ private
186
+
187
+ # Extract parameters from context for tool construction
188
+ #
189
+ # Includes each declared requirement that exists in the context.
190
+ # Missing keys are skipped — the tool constructor's own defaults
191
+ # and Ruby's `missing keyword` error handle validation naturally.
192
+ #
193
+ # @param requirements [Array<Symbol>] Parameter names the tool accepts
194
+ # @param context [Hash] Available context
195
+ # @param _tool_name [Symbol] Tool name (unused, kept for interface stability)
196
+ # @return [Hash] Parameters for constructor
197
+ def extract_params(requirements, context, _tool_name)
198
+ params = {}
199
+ requirements.each do |req|
200
+ params[req] = context[req] if context.key?(req)
201
+ end
202
+ params
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ # SubTask tool for spawning focused subtask agents
7
+ #
8
+ # Allows an agent to spawn a copy of itself for a focused subtask.
9
+ # The subtask agent shares the parent's memory (read-only), MCP
10
+ # connections, and skill access. It runs in the same process.
11
+ #
12
+ # SubTask is opt-in — agents must include :SubTask in their tools list.
13
+ # This prevents accidental cost explosion from unintended spawning.
14
+ #
15
+ # ## Depth control
16
+ #
17
+ # Subtasks track nesting depth. The maximum depth is controlled by
18
+ # {Configuration#max_subtask_depth} (default: 1, meaning the parent
19
+ # can spawn subtasks, but subtasks cannot spawn further subtasks).
20
+ #
21
+ # @example Agent definition with SubTask
22
+ # AgentDefinition.new(
23
+ # name: :researcher,
24
+ # description: "Research agent",
25
+ # tools: [:Read, :Grep, :Glob, :Think, :SubTask],
26
+ # )
27
+ class SubTask < Base
28
+ class << self
29
+ # @return [Array<Symbol>] Constructor requirements
30
+ def creation_requirements
31
+ [:agent_definition, :memory_store, :directory, :subtask_depth]
32
+ end
33
+ end
34
+
35
+ # @param agent_definition [AgentDefinition] Parent agent's definition
36
+ # @param memory_store [Memory::Store, nil] Parent's memory store
37
+ # @param directory [String] Working directory
38
+ # @param subtask_depth [Integer] Current nesting depth
39
+ def initialize(agent_definition:, memory_store: nil, directory: ".", subtask_depth: 0)
40
+ super()
41
+ @agent_definition = agent_definition
42
+ @memory_store = memory_store
43
+ @directory = directory
44
+ @subtask_depth = subtask_depth
45
+ end
46
+
47
+ description <<~DESC
48
+ Spawn a focused subtask to handle a specific piece of work.
49
+
50
+ The subtask runs as a copy of you with the same tools, skills, and MCP
51
+ connections, but in an **isolated context window**. Results are returned
52
+ as a summary, keeping your main context clean.
53
+
54
+ ## When to use SubTask
55
+
56
+ **Strongly prefer SubTask when:**
57
+ - Reading or analyzing multiple files (3+ files)
58
+ - Making multiple MCP tool calls (MCP operations are context-heavy)
59
+ - Searching across a codebase (grep/glob + read patterns)
60
+ - Performing any operation that would return large outputs
61
+ - Investigating tangential questions before returning to main task
62
+
63
+ **Use direct tools when:**
64
+ - Single, quick operations (one file read, one simple command)
65
+ - You need the raw output for immediate follow-up in the same turn
66
+
67
+ ## Context efficiency
68
+
69
+ Each subtask gets a fresh context window. Instead of accumulating
70
+ 50k+ tokens of tool outputs in your context, you receive a focused
71
+ summary. This keeps you effective over long conversations.
72
+
73
+ ## Example patterns
74
+
75
+ Instead of: Read file1, Read file2, Read file3, Grep for X, analyze
76
+ Do: SubTask("Analyze auth system", "Read the auth files and grep for
77
+ login patterns, summarize the authentication flow")
78
+
79
+ Instead of: Multiple MCP tool calls to external services
80
+ Do: SubTask("Gather external data", "Use the MCP tools to fetch X, Y, Z
81
+ and return the relevant findings")
82
+
83
+ ## Memory access
84
+
85
+ The subtask has read-only access to your memory — it can retrieve
86
+ context but cannot write new memories.
87
+
88
+ Be specific in your instructions. The subtask sees only what you
89
+ provide in the instructions field, plus any memory it retrieves.
90
+ DESC
91
+
92
+ param :title,
93
+ type: "string",
94
+ desc: "Short title for the subtask (used in logging)",
95
+ required: true
96
+
97
+ param :instructions,
98
+ type: "string",
99
+ desc: "Detailed instructions for the subtask. Be specific — the subtask only sees these instructions and its retrieved memory.",
100
+ required: true
101
+
102
+ # Execute the SubTask tool
103
+ #
104
+ # Spawns a SubTaskAgent, runs the instructions, and returns the result.
105
+ # The subtask agent is always cleaned up via ensure block.
106
+ #
107
+ # @param title [String] Short title for logging
108
+ # @param instructions [String] Detailed instructions for the subtask
109
+ # @return [String] Subtask result or error message
110
+ def execute(title:, instructions:, **_kwargs)
111
+ return validation_error("title is required") if title.to_s.strip.empty?
112
+ return validation_error("instructions are required") if instructions.to_s.strip.empty?
113
+
114
+ next_depth = @subtask_depth + 1
115
+ max_depth = Configuration.instance.max_subtask_depth
116
+
117
+ if next_depth > max_depth
118
+ return error(
119
+ "Maximum subtask depth (#{max_depth}) exceeded. " \
120
+ "Cannot spawn nested subtask at depth #{next_depth}.",
121
+ )
122
+ end
123
+
124
+ agent = SubTaskAgent.new(
125
+ @agent_definition,
126
+ parent_memory_store: @memory_store,
127
+ subtask_depth: next_depth,
128
+ )
129
+
130
+ EventStream.emit(
131
+ type: "subtask_spawned",
132
+ agent: @agent_definition.name,
133
+ subtask_agent: agent.id,
134
+ title: title,
135
+ depth: next_depth,
136
+ )
137
+
138
+ prompt = <<~PROMPT.strip
139
+ # SubTask: #{title}
140
+
141
+ #{instructions}
142
+ PROMPT
143
+
144
+ response = agent.ask(prompt)
145
+ format_result(title, response)
146
+ rescue StandardError => e
147
+ EventStream.emit(
148
+ type: "subtask_failed",
149
+ agent: @agent_definition.name,
150
+ subtask_agent: agent&.id,
151
+ title: title,
152
+ error: "#{e.class}: #{e.message}",
153
+ )
154
+ error("Subtask '#{title}' failed: #{e.class}: #{e.message}")
155
+ ensure
156
+ agent&.clear
157
+ end
158
+
159
+ private
160
+
161
+ # Format a successful subtask result
162
+ #
163
+ # @param title [String] Subtask title
164
+ # @param response [RubyLLM::Message] LLM response
165
+ # @return [String] Formatted result
166
+ def format_result(title, response)
167
+ EventStream.emit(
168
+ type: "subtask_completed",
169
+ agent: @agent_definition.name,
170
+ title: title,
171
+ success: true,
172
+ )
173
+
174
+ <<~RESULT.strip
175
+ ## SubTask Result: #{title}
176
+
177
+ #{response&.content || "(no response)"}
178
+ RESULT
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ # Think tool for explicit reasoning and planning
7
+ #
8
+ # Allows the agent to write down thoughts, plans, and intermediate
9
+ # calculations. These thoughts become part of the conversation context,
10
+ # enabling better reasoning through complex problems.
11
+ #
12
+ # When the agent has memory enabled, Think retrieves relevant memories
13
+ # for the thought content and returns them alongside "Thought noted."
14
+ # This gives the agent access to its long-term knowledge while reasoning.
15
+ # Without memory, Think behaves as a simple acknowledgment.
16
+ #
17
+ # Think does NOT write to memory — the existing turn-level async
18
+ # ingestion pipeline already captures tool call arguments (including
19
+ # Think's thoughts), so Think content is automatically persisted.
20
+ class Think < Base
21
+ class << self
22
+ # @return [Array<Symbol>] Constructor requirements
23
+ def creation_requirements
24
+ [:memory_store]
25
+ end
26
+ end
27
+
28
+ # @param memory_store [Memory::Store, nil] Memory store for retrieval
29
+ def initialize(memory_store: nil)
30
+ super()
31
+ @memory_store = memory_store
32
+ end
33
+
34
+ description <<~DESC
35
+ Use this tool to think through problems step by step before acting.
36
+
37
+ This is your working memory — record your thoughts, plans, strategies,
38
+ and intermediate calculations. Using this tool leads to significantly
39
+ better outcomes and more accurate solutions.
40
+
41
+ Use it frequently:
42
+ - Before starting any task
43
+ - When doing arithmetic or counting
44
+ - After reading files to process what you learned
45
+ - Between steps to track progress
46
+ - When debugging to trace issues
47
+
48
+ Your thoughts persist throughout the session as conversation history.
49
+ DESC
50
+
51
+ param :thoughts,
52
+ type: "string",
53
+ desc: "Your thoughts, plans, calculations, or any notes you want to record",
54
+ required: true
55
+
56
+ # Execute the Think tool
57
+ #
58
+ # When memory is available, searches for cards relevant to the thought
59
+ # content and returns them as context for the agent's reasoning.
60
+ #
61
+ # @param thoughts [String] The agent's thoughts
62
+ # @return [String] Acknowledgment with optional related memories
63
+ def execute(thoughts:, **_kwargs)
64
+ return "Thought noted." unless @memory_store
65
+
66
+ cards = @memory_store.search(thoughts, top_k: 5)
67
+ format_response(cards)
68
+ end
69
+
70
+ private
71
+
72
+ # Format the response with optional related memories
73
+ #
74
+ # @param cards [Array<Card>] Retrieved memory cards
75
+ # @return [String] Formatted response
76
+ def format_response(cards)
77
+ return "Thought noted." if cards.empty?
78
+
79
+ lines = ["Thought noted.", "", "Related memories:"]
80
+ cards.each do |card|
81
+ lines << "- [#{card.type.to_s.capitalize}] #{card.text}"
82
+ end
83
+ lines.join("\n")
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Tools
6
+ # Write tool for writing content to files
7
+ #
8
+ # Creates new files or overwrites existing files.
9
+ # Enforces read-before-write for existing files via the Read tool's tracking.
10
+ class Write < Base
11
+ class << self
12
+ # @return [Array<Symbol>] Constructor requirements
13
+ def creation_requirements
14
+ [:agent_name, :directory, :read_tracker]
15
+ end
16
+ end
17
+
18
+ description <<~DESC
19
+ Writes a file to the local filesystem.
20
+ Overwrites existing files. You MUST use Read first on existing files.
21
+ ALWAYS prefer editing existing files. NEVER write new files unless required.
22
+
23
+ Path handling:
24
+ - Relative paths resolve against your working directory
25
+ - Absolute paths (starting with /) are used as-is
26
+ DESC
27
+
28
+ param :file_path,
29
+ type: "string",
30
+ desc: "Path to the file to write",
31
+ required: true
32
+
33
+ param :content,
34
+ type: "string",
35
+ desc: "The content to write to the file",
36
+ required: true
37
+
38
+ # @param agent_name [Symbol, String] Agent identifier
39
+ # @param directory [String] Agent's working directory
40
+ # @param read_tracker [ReadTracker] Shared read tracker for enforcement
41
+ def initialize(agent_name:, directory:, read_tracker:)
42
+ super()
43
+ @agent_name = agent_name.to_sym
44
+ @directory = File.expand_path(directory)
45
+ @read_tracker = read_tracker
46
+ end
47
+
48
+ # Execute file write
49
+ #
50
+ # @param file_path [String] Path to the file
51
+ # @param content [String] Content to write
52
+ # @return [String] Success or error message
53
+ def execute(file_path:, content:)
54
+ return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
55
+ return validation_error("content is required") if content.nil?
56
+
57
+ resolved_path = resolve_path(file_path)
58
+ file_exists = File.exist?(resolved_path)
59
+
60
+ if file_exists && !@read_tracker.file_read?(@agent_name, resolved_path)
61
+ return validation_error(
62
+ "Cannot write to existing file without reading it first. " \
63
+ "Use the Read tool on '#{file_path}' before overwriting.",
64
+ )
65
+ end
66
+
67
+ parent_dir = File.dirname(resolved_path)
68
+ FileUtils.mkdir_p(parent_dir) unless File.directory?(parent_dir)
69
+
70
+ File.write(resolved_path, content, encoding: "UTF-8")
71
+
72
+ byte_size = content.bytesize
73
+ line_count = content.lines.count
74
+ action = file_exists ? "overwrote" : "created"
75
+
76
+ "Successfully #{action} file: #{file_path} (#{line_count} lines, #{byte_size} bytes)"
77
+ rescue Errno::EACCES
78
+ error("Permission denied: Cannot write to file '#{file_path}'")
79
+ rescue Errno::EISDIR
80
+ error("Path is a directory, not a file.")
81
+ rescue StandardError => e
82
+ error("Unexpected error writing file: #{e.class.name} - #{e.message}")
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end