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