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.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +43 -22
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +6 -0
- data/lib/swarm_sdk/ruby_llm_patches/mcp_ssl_patch.rb +144 -0
- data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +3 -4
- data/lib/swarm_sdk/v3/agent.rb +1165 -0
- data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
- data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
- data/lib/swarm_sdk/v3/configuration.rb +490 -0
- data/lib/swarm_sdk/v3/debug_log.rb +86 -0
- data/lib/swarm_sdk/v3/event_stream.rb +130 -0
- data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
- data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
- data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
- data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
- data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
- data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
- data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
- data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
- data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
- data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
- data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
- data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
- data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
- data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
- data/lib/swarm_sdk/v3/memory/card.rb +206 -0
- data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
- data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
- data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
- data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
- data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
- data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
- data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
- data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
- data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
- data/lib/swarm_sdk/v3/memory/store.rb +489 -0
- data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
- data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
- data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
- data/lib/swarm_sdk/v3/tools/base.rb +80 -0
- data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
- data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
- data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
- data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
- data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
- data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
- data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
- data/lib/swarm_sdk/v3/tools/read.rb +181 -0
- data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
- data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
- data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
- data/lib/swarm_sdk/v3/tools/think.rb +88 -0
- data/lib/swarm_sdk/v3/tools/write.rb +87 -0
- data/lib/swarm_sdk/v3.rb +145 -0
- metadata +84 -148
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
- data/lib/swarm_sdk/agent/builder.rb +0 -680
- data/lib/swarm_sdk/agent/chat.rb +0 -1432
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
- data/lib/swarm_sdk/agent/context.rb +0 -115
- data/lib/swarm_sdk/agent/context_manager.rb +0 -315
- data/lib/swarm_sdk/agent/definition.rb +0 -581
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
- data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
- data/lib/swarm_sdk/agent_registry.rb +0 -146
- data/lib/swarm_sdk/builders/base_builder.rb +0 -553
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
- data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
- data/lib/swarm_sdk/concerns/validatable.rb +0 -55
- data/lib/swarm_sdk/config.rb +0 -367
- data/lib/swarm_sdk/configuration/parser.rb +0 -397
- data/lib/swarm_sdk/configuration/translator.rb +0 -283
- data/lib/swarm_sdk/configuration.rb +0 -165
- data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
- data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
- data/lib/swarm_sdk/context_compactor.rb +0 -335
- data/lib/swarm_sdk/context_management/builder.rb +0 -128
- data/lib/swarm_sdk/context_management/context.rb +0 -328
- data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
- data/lib/swarm_sdk/defaults.rb +0 -251
- data/lib/swarm_sdk/events_to_messages.rb +0 -199
- data/lib/swarm_sdk/hooks/adapter.rb +0 -359
- data/lib/swarm_sdk/hooks/context.rb +0 -197
- data/lib/swarm_sdk/hooks/definition.rb +0 -80
- data/lib/swarm_sdk/hooks/error.rb +0 -29
- data/lib/swarm_sdk/hooks/executor.rb +0 -146
- data/lib/swarm_sdk/hooks/registry.rb +0 -147
- data/lib/swarm_sdk/hooks/result.rb +0 -150
- data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
- data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
- data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
- data/lib/swarm_sdk/log_collector.rb +0 -227
- data/lib/swarm_sdk/log_stream.rb +0 -127
- data/lib/swarm_sdk/markdown_parser.rb +0 -75
- data/lib/swarm_sdk/model_aliases.json +0 -8
- data/lib/swarm_sdk/models.json +0 -44002
- data/lib/swarm_sdk/models.rb +0 -161
- data/lib/swarm_sdk/node_context.rb +0 -245
- data/lib/swarm_sdk/observer/builder.rb +0 -81
- data/lib/swarm_sdk/observer/config.rb +0 -45
- data/lib/swarm_sdk/observer/manager.rb +0 -236
- data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
- data/lib/swarm_sdk/permissions/config.rb +0 -239
- data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
- data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
- data/lib/swarm_sdk/permissions/validator.rb +0 -173
- data/lib/swarm_sdk/permissions_builder.rb +0 -122
- data/lib/swarm_sdk/plugin.rb +0 -309
- data/lib/swarm_sdk/plugin_registry.rb +0 -101
- data/lib/swarm_sdk/proc_helpers.rb +0 -53
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -212
- data/lib/swarm_sdk/snapshot.rb +0 -156
- data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
- data/lib/swarm_sdk/state_restorer.rb +0 -476
- data/lib/swarm_sdk/state_snapshot.rb +0 -334
- data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -195
- data/lib/swarm_sdk/swarm/builder.rb +0 -256
- data/lib/swarm_sdk/swarm/executor.rb +0 -290
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -151
- data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -360
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -270
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
- data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
- data/lib/swarm_sdk/swarm.rb +0 -843
- data/lib/swarm_sdk/swarm_loader.rb +0 -145
- data/lib/swarm_sdk/swarm_registry.rb +0 -136
- data/lib/swarm_sdk/tools/base.rb +0 -63
- data/lib/swarm_sdk/tools/bash.rb +0 -280
- data/lib/swarm_sdk/tools/clock.rb +0 -46
- data/lib/swarm_sdk/tools/delegate.rb +0 -389
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
- data/lib/swarm_sdk/tools/edit.rb +0 -145
- data/lib/swarm_sdk/tools/glob.rb +0 -166
- data/lib/swarm_sdk/tools/grep.rb +0 -235
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
- data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
- data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
- data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
- data/lib/swarm_sdk/tools/read.rb +0 -261
- data/lib/swarm_sdk/tools/registry.rb +0 -205
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
- data/lib/swarm_sdk/tools/think.rb +0 -100
- data/lib/swarm_sdk/tools/todo_write.rb +0 -237
- data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
- data/lib/swarm_sdk/tools/write.rb +0 -112
- data/lib/swarm_sdk/transcript_builder.rb +0 -278
- data/lib/swarm_sdk/utils.rb +0 -68
- data/lib/swarm_sdk/validation_result.rb +0 -33
- data/lib/swarm_sdk/version.rb +0 -5
- data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
- data/lib/swarm_sdk/workflow/builder.rb +0 -227
- data/lib/swarm_sdk/workflow/executor.rb +0 -497
- data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
- data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
- data/lib/swarm_sdk/workflow.rb +0 -589
- 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
|