swarm_memory 2.1.2 → 2.1.4
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/claude_swarm/claude_mcp_server.rb +1 -0
- data/lib/claude_swarm/cli.rb +5 -18
- data/lib/claude_swarm/configuration.rb +30 -19
- data/lib/claude_swarm/mcp_generator.rb +5 -10
- data/lib/claude_swarm/openai/chat_completion.rb +4 -12
- data/lib/claude_swarm/openai/executor.rb +3 -1
- data/lib/claude_swarm/openai/responses.rb +13 -32
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
- data/lib/swarm_cli/commands/run.rb +2 -2
- data/lib/swarm_cli/config_loader.rb +14 -14
- data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
- data/lib/swarm_cli/interactive_repl.rb +11 -5
- data/lib/swarm_cli/ui/icons.rb +0 -23
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/adapters/base.rb +4 -4
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
- data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
- data/lib/swarm_memory/integration/cli_registration.rb +3 -2
- data/lib/swarm_memory/integration/sdk_plugin.rb +98 -12
- data/lib/swarm_memory/tools/memory_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_read.rb +3 -3
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +6 -1
- data/lib/swarm_sdk/agent/builder.rb +91 -0
- data/lib/swarm_sdk/agent/chat.rb +540 -925
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +33 -79
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +147 -39
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +22 -38
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
- data/lib/swarm_sdk/agent/context.rb +8 -4
- data/lib/swarm_sdk/agent/context_manager.rb +6 -0
- data/lib/swarm_sdk/agent/definition.rb +79 -174
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +182 -0
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
- data/lib/swarm_sdk/builders/base_builder.rb +409 -0
- data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
- data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
- data/lib/swarm_sdk/concerns/validatable.rb +55 -0
- data/lib/swarm_sdk/configuration/parser.rb +353 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +100 -261
- data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
- data/lib/swarm_sdk/context_compactor.rb +6 -11
- data/lib/swarm_sdk/context_management/builder.rb +128 -0
- data/lib/swarm_sdk/context_management/context.rb +328 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +199 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
- data/lib/swarm_sdk/log_collector.rb +192 -16
- data/lib/swarm_sdk/log_stream.rb +66 -8
- data/lib/swarm_sdk/model_aliases.json +4 -1
- data/lib/swarm_sdk/node_context.rb +1 -1
- data/lib/swarm_sdk/observer/builder.rb +81 -0
- data/lib/swarm_sdk/observer/config.rb +45 -0
- data/lib/swarm_sdk/observer/manager.rb +236 -0
- data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
- data/lib/swarm_sdk/plugin.rb +93 -3
- data/lib/swarm_sdk/proc_helpers.rb +53 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
- data/lib/swarm_sdk/restore_result.rb +65 -0
- data/lib/swarm_sdk/snapshot.rb +156 -0
- data/lib/swarm_sdk/snapshot_from_events.rb +397 -0
- data/lib/swarm_sdk/state_restorer.rb +476 -0
- data/lib/swarm_sdk/state_snapshot.rb +334 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +428 -79
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
- data/lib/swarm_sdk/swarm/builder.rb +69 -407
- data/lib/swarm_sdk/swarm/executor.rb +213 -0
- data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +88 -149
- data/lib/swarm_sdk/swarm.rb +366 -631
- data/lib/swarm_sdk/swarm_loader.rb +145 -0
- data/lib/swarm_sdk/swarm_registry.rb +136 -0
- data/lib/swarm_sdk/tools/bash.rb +11 -3
- data/lib/swarm_sdk/tools/delegate.rb +127 -24
- data/lib/swarm_sdk/tools/edit.rb +8 -13
- data/lib/swarm_sdk/tools/glob.rb +9 -1
- data/lib/swarm_sdk/tools/grep.rb +7 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
- data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
- data/lib/swarm_sdk/tools/read.rb +28 -18
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +53 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/think.rb +4 -1
- data/lib/swarm_sdk/tools/todo_write.rb +27 -8
- data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/utils.rb +18 -0
- data/lib/swarm_sdk/validation_result.rb +33 -0
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +34 -9
- data/lib/swarm_sdk/workflow/builder.rb +143 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +42 -21
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
- data/lib/swarm_sdk/workflow.rb +554 -0
- data/lib/swarm_sdk.rb +393 -22
- metadata +51 -16
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_sdk/node_orchestrator.rb +0 -591
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -582
|
@@ -11,6 +11,13 @@ module SwarmSDK
|
|
|
11
11
|
class MultiEdit < RubyLLM::Tool
|
|
12
12
|
include PathResolver
|
|
13
13
|
|
|
14
|
+
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
15
|
+
class << self
|
|
16
|
+
def creation_requirements
|
|
17
|
+
[:agent_name, :directory]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
14
21
|
description <<~DESC
|
|
15
22
|
Performs multiple exact string replacements in a single file.
|
|
16
23
|
Edits are applied sequentially, so later edits see the results of earlier ones.
|
|
@@ -53,8 +60,7 @@ module SwarmSDK
|
|
|
53
60
|
# @param directory [String] Agent's working directory
|
|
54
61
|
def initialize(agent_name:, directory:)
|
|
55
62
|
super()
|
|
56
|
-
|
|
57
|
-
@directory = File.expand_path(directory)
|
|
63
|
+
initialize_agent_context(agent_name: agent_name, directory: directory)
|
|
58
64
|
end
|
|
59
65
|
|
|
60
66
|
# Override name to return simple "MultiEdit" instead of full class path
|
|
@@ -204,15 +210,13 @@ module SwarmSDK
|
|
|
204
210
|
|
|
205
211
|
private
|
|
206
212
|
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
end
|
|
215
|
-
|
|
213
|
+
# Format an error that includes partial results
|
|
214
|
+
#
|
|
215
|
+
# Shows what edits succeeded before the error occurred.
|
|
216
|
+
#
|
|
217
|
+
# @param message [String] Error description
|
|
218
|
+
# @param results [Array<Hash>] Successful edit results before failure
|
|
219
|
+
# @return [String] Formatted error message with results summary
|
|
216
220
|
def error_with_results(message, results)
|
|
217
221
|
output = "<tool_use_error>InputValidationError: #{message}\n\n"
|
|
218
222
|
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module SwarmSDK
|
|
4
4
|
module Tools
|
|
5
|
-
# Shared path resolution logic for
|
|
5
|
+
# Shared path resolution and agent context logic for file tools
|
|
6
|
+
#
|
|
7
|
+
# This module provides:
|
|
8
|
+
# - Path resolution (relative to agent's working directory)
|
|
9
|
+
# - Agent context initialization (agent_name, directory expansion)
|
|
10
|
+
# - Standard error message formatting
|
|
6
11
|
#
|
|
7
12
|
# Tools resolve relative paths against the agent's directory.
|
|
8
13
|
# Absolute paths are used as-is.
|
|
@@ -12,17 +17,41 @@ module SwarmSDK
|
|
|
12
17
|
# include PathResolver
|
|
13
18
|
#
|
|
14
19
|
# def initialize(agent_name:, directory:)
|
|
15
|
-
#
|
|
20
|
+
# super()
|
|
21
|
+
# initialize_agent_context(agent_name: agent_name, directory: directory)
|
|
16
22
|
# end
|
|
17
23
|
#
|
|
18
24
|
# def execute(file_path:)
|
|
19
25
|
# resolved_path = resolve_path(file_path)
|
|
20
26
|
# File.read(resolved_path)
|
|
27
|
+
# rescue StandardError => e
|
|
28
|
+
# error("Failed to read: #{e.message}")
|
|
21
29
|
# end
|
|
22
30
|
# end
|
|
23
31
|
module PathResolver
|
|
32
|
+
# Agent context attributes
|
|
33
|
+
# @return [Symbol] The agent identifier
|
|
34
|
+
attr_reader :agent_name
|
|
35
|
+
|
|
36
|
+
# @return [String] Absolute path to agent's working directory
|
|
37
|
+
attr_reader :directory
|
|
38
|
+
|
|
24
39
|
private
|
|
25
40
|
|
|
41
|
+
# Initialize agent context for file tools
|
|
42
|
+
#
|
|
43
|
+
# Sets up the common agent context needed by file tools:
|
|
44
|
+
# - Normalizes agent_name to symbol
|
|
45
|
+
# - Expands directory to absolute path
|
|
46
|
+
#
|
|
47
|
+
# @param agent_name [Symbol, String] The agent identifier
|
|
48
|
+
# @param directory [String] Agent's working directory (will be expanded)
|
|
49
|
+
# @return [void]
|
|
50
|
+
def initialize_agent_context(agent_name:, directory:)
|
|
51
|
+
@agent_name = agent_name.to_sym
|
|
52
|
+
@directory = File.expand_path(directory)
|
|
53
|
+
end
|
|
54
|
+
|
|
26
55
|
# Resolve a path relative to the agent's directory
|
|
27
56
|
#
|
|
28
57
|
# - Absolute paths (starting with /) are returned as-is
|
|
@@ -38,6 +67,26 @@ module SwarmSDK
|
|
|
38
67
|
|
|
39
68
|
File.expand_path(path, @directory)
|
|
40
69
|
end
|
|
70
|
+
|
|
71
|
+
# Format a validation error response
|
|
72
|
+
#
|
|
73
|
+
# Used for input validation failures (missing required params, invalid formats, etc.)
|
|
74
|
+
#
|
|
75
|
+
# @param message [String] Error description
|
|
76
|
+
# @return [String] Formatted error message wrapped in tool_use_error tags
|
|
77
|
+
def validation_error(message)
|
|
78
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Format a general error response
|
|
82
|
+
#
|
|
83
|
+
# Used for runtime errors (permission denied, file not found, etc.)
|
|
84
|
+
#
|
|
85
|
+
# @param message [String] Error description
|
|
86
|
+
# @return [String] Formatted error message prefixed with "Error:"
|
|
87
|
+
def error(message)
|
|
88
|
+
"Error: #{message}"
|
|
89
|
+
end
|
|
41
90
|
end
|
|
42
91
|
end
|
|
43
92
|
end
|
data/lib/swarm_sdk/tools/read.rb
CHANGED
|
@@ -10,8 +10,9 @@ module SwarmSDK
|
|
|
10
10
|
class Read < RubyLLM::Tool
|
|
11
11
|
include PathResolver
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# Backward compatibility aliases - use Defaults module for new code
|
|
14
|
+
MAX_LINE_LENGTH = Defaults::Limits::LINE_CHARACTERS
|
|
15
|
+
DEFAULT_LIMIT = Defaults::Limits::READ_LINES
|
|
15
16
|
|
|
16
17
|
# List of available document converters
|
|
17
18
|
CONVERTERS = [
|
|
@@ -28,6 +29,13 @@ module SwarmSDK
|
|
|
28
29
|
""
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
33
|
+
class << self
|
|
34
|
+
def creation_requirements
|
|
35
|
+
[:agent_name, :directory]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
31
39
|
description <<~DESC
|
|
32
40
|
Reads a file from the local filesystem. You can access any file directly by using this tool.
|
|
33
41
|
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid.
|
|
@@ -67,8 +75,7 @@ module SwarmSDK
|
|
|
67
75
|
# @param directory [String] Agent's working directory
|
|
68
76
|
def initialize(agent_name:, directory:)
|
|
69
77
|
super()
|
|
70
|
-
|
|
71
|
-
@directory = File.expand_path(directory)
|
|
78
|
+
initialize_agent_context(agent_name: agent_name, directory: directory)
|
|
72
79
|
end
|
|
73
80
|
|
|
74
81
|
# Override name to return simple "Read" instead of full class path
|
|
@@ -92,25 +99,37 @@ module SwarmSDK
|
|
|
92
99
|
return validation_error("Path is a directory, not a file. Use Bash with ls to read directories.")
|
|
93
100
|
end
|
|
94
101
|
|
|
95
|
-
# Register this read in the tracker (use resolved path)
|
|
96
|
-
Stores::ReadTracker.register_read(@agent_name, resolved_path)
|
|
97
|
-
|
|
98
102
|
# Check if it's a document and try to convert it
|
|
99
103
|
converter = find_converter_for_file(resolved_path)
|
|
100
104
|
if converter
|
|
101
105
|
result = converter.new.convert(resolved_path)
|
|
106
|
+
# For document files, register the converted text content
|
|
107
|
+
# Extract text from result (may be wrapped in system-reminder tags)
|
|
108
|
+
if result.is_a?(String)
|
|
109
|
+
# Remove system-reminder wrapper if present to get clean text for digest
|
|
110
|
+
text_content = result.gsub(%r{<system-reminder>.*?</system-reminder>}m, "").strip
|
|
111
|
+
Stores::ReadTracker.register_read(@agent_name, resolved_path, text_content)
|
|
112
|
+
end
|
|
102
113
|
return result
|
|
103
114
|
end
|
|
104
115
|
|
|
105
116
|
# Try to read as text, handle binary files separately
|
|
106
117
|
content = read_file_content(resolved_path)
|
|
107
118
|
|
|
108
|
-
# If content is a Content object (binary file),
|
|
109
|
-
|
|
119
|
+
# If content is a Content object (binary file), track with binary digest and return
|
|
120
|
+
if content.is_a?(RubyLLM::Content)
|
|
121
|
+
# For binary files, read raw bytes for digest
|
|
122
|
+
binary_content = File.binread(resolved_path)
|
|
123
|
+
Stores::ReadTracker.register_read(@agent_name, resolved_path, binary_content)
|
|
124
|
+
return content
|
|
125
|
+
end
|
|
110
126
|
|
|
111
127
|
# Return early if we got an error message or system reminder
|
|
112
128
|
return content if content.is_a?(String) && (content.start_with?("Error:") || content.start_with?("<system-reminder>"))
|
|
113
129
|
|
|
130
|
+
# At this point, we have valid text content - register the read with digest
|
|
131
|
+
Stores::ReadTracker.register_read(@agent_name, resolved_path, content)
|
|
132
|
+
|
|
114
133
|
# Check if file is empty
|
|
115
134
|
if content.empty?
|
|
116
135
|
return format_with_reminder(
|
|
@@ -170,15 +189,6 @@ module SwarmSDK
|
|
|
170
189
|
CONVERTERS.find { |converter| converter.extensions.include?(ext) }
|
|
171
190
|
end
|
|
172
191
|
|
|
173
|
-
# Helper methods
|
|
174
|
-
def validation_error(message)
|
|
175
|
-
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def error(message)
|
|
179
|
-
"Error: #{message}"
|
|
180
|
-
end
|
|
181
|
-
|
|
182
192
|
def format_with_reminder(content, reminder)
|
|
183
193
|
return content if reminder.nil? || reminder.empty?
|
|
184
194
|
|
|
@@ -5,24 +5,51 @@ module SwarmSDK
|
|
|
5
5
|
# Registry for built-in SwarmSDK tools
|
|
6
6
|
#
|
|
7
7
|
# Maps tool names (symbols) to their RubyLLM::Tool classes.
|
|
8
|
-
# Provides validation and
|
|
8
|
+
# Provides validation, lookup, and factory functionality for tool registration.
|
|
9
|
+
#
|
|
10
|
+
# ## Tool Creation Pattern
|
|
11
|
+
#
|
|
12
|
+
# Tools register themselves with their creation requirements via the `tool_factory` method.
|
|
13
|
+
# This eliminates the need for a giant case statement in ToolConfigurator.
|
|
14
|
+
#
|
|
15
|
+
# Tools fall into three categories:
|
|
16
|
+
# 1. **No params**: Simple tools with no initialization requirements (Think, Clock)
|
|
17
|
+
# 2. **Directory only**: Tools needing working directory (Bash, Grep, Glob)
|
|
18
|
+
# 3. **Agent context**: Tools needing agent tracking (Read, Write, Edit, MultiEdit)
|
|
19
|
+
# 4. **Scratchpad**: Tools needing scratchpad storage instance
|
|
20
|
+
#
|
|
21
|
+
# @example Adding a new tool with creation requirements
|
|
22
|
+
# # In the tool class:
|
|
23
|
+
# class MyTool < RubyLLM::Tool
|
|
24
|
+
# def self.creation_requirements
|
|
25
|
+
# [:agent_name, :directory]
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # In registry:
|
|
30
|
+
# BUILTIN_TOOLS = {
|
|
31
|
+
# MyTool: SwarmSDK::Tools::MyTool,
|
|
32
|
+
# }
|
|
9
33
|
#
|
|
10
34
|
# Note: Plugin-provided tools (e.g., memory tools) are NOT in this registry.
|
|
11
35
|
# They are registered via SwarmSDK::PluginRegistry instead.
|
|
12
36
|
class Registry
|
|
13
37
|
# All available built-in tools
|
|
38
|
+
#
|
|
39
|
+
# Maps tool names to their classes. The class must respond to `creation_requirements`
|
|
40
|
+
# to specify what parameters are needed for instantiation.
|
|
14
41
|
BUILTIN_TOOLS = {
|
|
15
|
-
Read:
|
|
16
|
-
Write:
|
|
17
|
-
Edit:
|
|
42
|
+
Read: SwarmSDK::Tools::Read,
|
|
43
|
+
Write: SwarmSDK::Tools::Write,
|
|
44
|
+
Edit: SwarmSDK::Tools::Edit,
|
|
45
|
+
MultiEdit: SwarmSDK::Tools::MultiEdit,
|
|
18
46
|
Bash: SwarmSDK::Tools::Bash,
|
|
19
47
|
Grep: SwarmSDK::Tools::Grep,
|
|
20
48
|
Glob: SwarmSDK::Tools::Glob,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
ScratchpadList: :special, # Requires scratchpad storage instance
|
|
49
|
+
TodoWrite: SwarmSDK::Tools::TodoWrite,
|
|
50
|
+
ScratchpadWrite: :scratchpad, # Requires scratchpad storage instance
|
|
51
|
+
ScratchpadRead: :scratchpad, # Requires scratchpad storage instance
|
|
52
|
+
ScratchpadList: :scratchpad, # Requires scratchpad storage instance
|
|
26
53
|
Think: SwarmSDK::Tools::Think,
|
|
27
54
|
WebFetch: SwarmSDK::Tools::WebFetch,
|
|
28
55
|
Clock: SwarmSDK::Tools::Clock,
|
|
@@ -35,12 +62,49 @@ module SwarmSDK
|
|
|
35
62
|
# They are managed by SwarmSDK::PluginRegistry instead.
|
|
36
63
|
#
|
|
37
64
|
# @param name [Symbol, String] Tool name
|
|
38
|
-
# @return [Class, Symbol, nil] Tool class, :
|
|
65
|
+
# @return [Class, Symbol, nil] Tool class, :scratchpad marker, or nil if not found
|
|
39
66
|
def get(name)
|
|
40
67
|
name_sym = name.to_sym
|
|
41
68
|
BUILTIN_TOOLS[name_sym]
|
|
42
69
|
end
|
|
43
70
|
|
|
71
|
+
# Create a tool instance using the Factory Pattern
|
|
72
|
+
#
|
|
73
|
+
# Uses the tool's `creation_requirements` class method to determine
|
|
74
|
+
# what parameters to pass to the constructor.
|
|
75
|
+
#
|
|
76
|
+
# @param name [Symbol, String] Tool name
|
|
77
|
+
# @param context [Hash] Available context for tool creation
|
|
78
|
+
# @option context [Symbol] :agent_name Agent identifier
|
|
79
|
+
# @option context [String] :directory Agent's working directory
|
|
80
|
+
# @option context [Object] :scratchpad_storage Scratchpad storage instance
|
|
81
|
+
# @return [RubyLLM::Tool] Instantiated tool
|
|
82
|
+
# @raise [ConfigurationError] If tool is unknown or has unmet requirements
|
|
83
|
+
def create(name, context = {})
|
|
84
|
+
name_sym = name.to_sym
|
|
85
|
+
tool_entry = BUILTIN_TOOLS[name_sym]
|
|
86
|
+
|
|
87
|
+
raise ConfigurationError, "Unknown tool: #{name}" unless tool_entry
|
|
88
|
+
|
|
89
|
+
# Handle scratchpad tools specially (they use factory methods)
|
|
90
|
+
if tool_entry == :scratchpad
|
|
91
|
+
return create_scratchpad_tool(name_sym, context[:scratchpad_storage])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Get the tool class and its requirements
|
|
95
|
+
tool_class = tool_entry
|
|
96
|
+
|
|
97
|
+
# Check if tool defines creation requirements
|
|
98
|
+
if tool_class.respond_to?(:creation_requirements)
|
|
99
|
+
requirements = tool_class.creation_requirements
|
|
100
|
+
params = extract_params(requirements, context, name)
|
|
101
|
+
tool_class.new(**params)
|
|
102
|
+
else
|
|
103
|
+
# No requirements - simple instantiation
|
|
104
|
+
tool_class.new
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
44
108
|
# Get multiple tool classes by names
|
|
45
109
|
#
|
|
46
110
|
# @param names [Array<Symbol, String>] Tool names
|
|
@@ -87,6 +151,54 @@ module SwarmSDK
|
|
|
87
151
|
def validate(names)
|
|
88
152
|
names.reject { |name| exists?(name) }
|
|
89
153
|
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
# Extract required parameters from context
|
|
158
|
+
#
|
|
159
|
+
# @param requirements [Array<Symbol>] Required parameter names
|
|
160
|
+
# @param context [Hash] Available context
|
|
161
|
+
# @param tool_name [Symbol] Tool name for error messages
|
|
162
|
+
# @return [Hash] Parameters to pass to tool constructor
|
|
163
|
+
# @raise [ConfigurationError] If required parameter is missing
|
|
164
|
+
def extract_params(requirements, context, tool_name)
|
|
165
|
+
params = {}
|
|
166
|
+
|
|
167
|
+
requirements.each do |req|
|
|
168
|
+
unless context.key?(req)
|
|
169
|
+
raise ConfigurationError,
|
|
170
|
+
"Tool #{tool_name} requires #{req} but it was not provided in context"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
params[req] = context[req]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
params
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Create a scratchpad tool using its factory method
|
|
180
|
+
#
|
|
181
|
+
# @param name [Symbol] Scratchpad tool name
|
|
182
|
+
# @param storage [Object] Scratchpad storage instance
|
|
183
|
+
# @return [RubyLLM::Tool] Instantiated scratchpad tool
|
|
184
|
+
# @raise [ConfigurationError] If storage is not provided
|
|
185
|
+
def create_scratchpad_tool(name, storage)
|
|
186
|
+
unless storage
|
|
187
|
+
raise ConfigurationError,
|
|
188
|
+
"Scratchpad tool #{name} requires scratchpad_storage in context"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
case name
|
|
192
|
+
when :ScratchpadWrite
|
|
193
|
+
Tools::Scratchpad::ScratchpadWrite.create_for_scratchpad(storage)
|
|
194
|
+
when :ScratchpadRead
|
|
195
|
+
Tools::Scratchpad::ScratchpadRead.create_for_scratchpad(storage)
|
|
196
|
+
when :ScratchpadList
|
|
197
|
+
Tools::Scratchpad::ScratchpadList.create_for_scratchpad(storage)
|
|
198
|
+
else
|
|
199
|
+
raise ConfigurationError, "Unknown scratchpad tool: #{name}"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
90
202
|
end
|
|
91
203
|
end
|
|
92
204
|
end
|
|
@@ -12,8 +12,29 @@ module SwarmSDK
|
|
|
12
12
|
|
|
13
13
|
description <<~DESC
|
|
14
14
|
List all entries in scratchpad with their metadata.
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
|
|
16
|
+
## When to Use ScratchpadList
|
|
17
|
+
|
|
18
|
+
Use ScratchpadList to:
|
|
19
|
+
- Discover what content is available in the scratchpad
|
|
20
|
+
- Check what other agents have stored
|
|
21
|
+
- Find relevant entries before reading them
|
|
22
|
+
- Review all stored outputs and analysis
|
|
23
|
+
- Check entry sizes and last update times
|
|
24
|
+
|
|
25
|
+
## Best Practices
|
|
26
|
+
|
|
27
|
+
- Use this before ScratchpadRead if you don't know what's stored
|
|
28
|
+
- Filter by prefix to narrow down results (e.g., 'notes/' lists all notes)
|
|
29
|
+
- Shows path, title, size, and last updated time for each entry
|
|
30
|
+
- Any agent can see all scratchpad entries
|
|
31
|
+
- Helps coordinate multi-agent workflows
|
|
32
|
+
|
|
33
|
+
## Examples
|
|
34
|
+
|
|
35
|
+
- List all entries: (no prefix parameter)
|
|
36
|
+
- List notes only: prefix='notes/'
|
|
37
|
+
- List analysis results: prefix='analysis/'
|
|
17
38
|
DESC
|
|
18
39
|
|
|
19
40
|
param :prefix,
|
|
@@ -12,8 +12,29 @@ module SwarmSDK
|
|
|
12
12
|
|
|
13
13
|
description <<~DESC
|
|
14
14
|
Read content from scratchpad.
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
|
|
16
|
+
## When to Use ScratchpadRead
|
|
17
|
+
|
|
18
|
+
Use ScratchpadRead to:
|
|
19
|
+
- Retrieve previously stored content and outputs
|
|
20
|
+
- Access detailed analysis or results from earlier steps
|
|
21
|
+
- Read messages or notes left by other agents
|
|
22
|
+
- Access cached computed data
|
|
23
|
+
- Retrieve content that was too long for direct responses
|
|
24
|
+
|
|
25
|
+
## Best Practices
|
|
26
|
+
|
|
27
|
+
- Any agent can read any scratchpad content
|
|
28
|
+
- Content is returned with line numbers for easy reference
|
|
29
|
+
- Use ScratchpadList first if you don't know what's stored
|
|
30
|
+
- Scratchpad data is temporary and lost when swarm ends
|
|
31
|
+
- For persistent data, use MemoryRead instead
|
|
32
|
+
|
|
33
|
+
## Examples
|
|
34
|
+
|
|
35
|
+
- Read status: file_path='status'
|
|
36
|
+
- Read analysis: file_path='api_analysis'
|
|
37
|
+
- Read agent notes: file_path='notes/backend'
|
|
17
38
|
DESC
|
|
18
39
|
|
|
19
40
|
param :file_path,
|
|
@@ -13,12 +13,29 @@ module SwarmSDK
|
|
|
13
13
|
|
|
14
14
|
description <<~DESC
|
|
15
15
|
Store content in scratchpad for temporary cross-agent communication.
|
|
16
|
-
Use this for quick notes, intermediate results, or coordination messages.
|
|
17
|
-
Any agent can read this content. Data is lost when the swarm ends.
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
## When to Use Scratchpad
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
Use ScratchpadWrite to:
|
|
20
|
+
- Store detailed outputs, analysis, or results that are too long for direct responses
|
|
21
|
+
- Share information that would otherwise clutter your responses
|
|
22
|
+
- Store intermediate results during multi-step tasks
|
|
23
|
+
- Leave coordination messages for other agents
|
|
24
|
+
- Cache computed data for quick retrieval
|
|
25
|
+
|
|
26
|
+
## Best Practices
|
|
27
|
+
|
|
28
|
+
- Choose simple, descriptive paths: 'status', 'result', 'notes/agent_x'
|
|
29
|
+
- Use hierarchical paths for organization: 'analysis/step1', 'analysis/step2'
|
|
30
|
+
- Keep entries focused - one piece of information per entry
|
|
31
|
+
- Any agent can read scratchpad content
|
|
32
|
+
- Data is lost when the swarm ends (use MemoryWrite for persistent storage)
|
|
33
|
+
- Maximum 1MB per entry
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
Good paths: 'status', 'api_analysis', 'test_results', 'notes/backend'
|
|
38
|
+
Bad paths: 'scratch/temp/file123.txt', 'output.log'
|
|
22
39
|
DESC
|
|
23
40
|
|
|
24
41
|
param :file_path,
|
|
@@ -3,39 +3,74 @@
|
|
|
3
3
|
module SwarmSDK
|
|
4
4
|
module Tools
|
|
5
5
|
module Stores
|
|
6
|
-
# ReadTracker manages read-file tracking for all agents
|
|
6
|
+
# ReadTracker manages read-file tracking for all agents with content digest verification
|
|
7
7
|
#
|
|
8
8
|
# This module maintains a global registry of which files each agent has read
|
|
9
|
-
# during their conversation
|
|
10
|
-
# and "read-before-edit" rules that ensure
|
|
9
|
+
# during their conversation along with SHA256 digests of the content. This enables
|
|
10
|
+
# enforcement of the "read-before-write" and "read-before-edit" rules that ensure
|
|
11
|
+
# agents have context before modifying files, AND prevents editing files that have
|
|
12
|
+
# changed externally since being read.
|
|
11
13
|
#
|
|
12
|
-
# Each agent maintains an independent
|
|
14
|
+
# Each agent maintains an independent map of read files to content digests.
|
|
13
15
|
module ReadTracker
|
|
14
|
-
@read_files = {}
|
|
16
|
+
@read_files = {} # { agent_id => { file_path => sha256_digest } }
|
|
15
17
|
@mutex = Mutex.new
|
|
16
18
|
|
|
17
19
|
class << self
|
|
18
|
-
# Register that an agent has read a file
|
|
20
|
+
# Register that an agent has read a file with content digest
|
|
19
21
|
#
|
|
20
22
|
# @param agent_id [Symbol] The agent identifier
|
|
21
23
|
# @param file_path [String] The absolute path to the file
|
|
22
|
-
|
|
24
|
+
# @param content [String] File content (for digest calculation)
|
|
25
|
+
# @return [String] The calculated SHA256 digest
|
|
26
|
+
def register_read(agent_id, file_path, content)
|
|
23
27
|
@mutex.synchronize do
|
|
24
|
-
@read_files[agent_id] ||=
|
|
25
|
-
|
|
28
|
+
@read_files[agent_id] ||= {}
|
|
29
|
+
digest = Digest::SHA256.hexdigest(content)
|
|
30
|
+
@read_files[agent_id][File.expand_path(file_path)] = digest
|
|
31
|
+
digest
|
|
26
32
|
end
|
|
27
33
|
end
|
|
28
34
|
|
|
29
|
-
# Check if an agent has read a file
|
|
35
|
+
# Check if an agent has read a file AND content hasn't changed
|
|
30
36
|
#
|
|
31
37
|
# @param agent_id [Symbol] The agent identifier
|
|
32
38
|
# @param file_path [String] The absolute path to the file
|
|
33
|
-
# @return [Boolean] true if
|
|
39
|
+
# @return [Boolean] true if agent read file and content matches
|
|
34
40
|
def file_read?(agent_id, file_path)
|
|
35
41
|
@mutex.synchronize do
|
|
36
42
|
return false unless @read_files[agent_id]
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
expanded_path = File.expand_path(file_path)
|
|
45
|
+
stored_digest = @read_files[agent_id][expanded_path]
|
|
46
|
+
return false unless stored_digest
|
|
47
|
+
|
|
48
|
+
# Check if file still exists and matches stored digest
|
|
49
|
+
return false unless File.exist?(expanded_path)
|
|
50
|
+
|
|
51
|
+
current_digest = Digest::SHA256.hexdigest(File.read(expanded_path))
|
|
52
|
+
current_digest == stored_digest
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get all read files with digests for snapshot
|
|
57
|
+
#
|
|
58
|
+
# @param agent_id [Symbol] The agent identifier
|
|
59
|
+
# @return [Hash] { file_path => digest }
|
|
60
|
+
def get_read_files(agent_id)
|
|
61
|
+
@mutex.synchronize do
|
|
62
|
+
@read_files[agent_id]&.dup || {}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Restore read files with digests from snapshot
|
|
67
|
+
#
|
|
68
|
+
# @param agent_id [Symbol] The agent identifier
|
|
69
|
+
# @param files_with_digests [Hash] { file_path => digest }
|
|
70
|
+
# @return [void]
|
|
71
|
+
def restore_read_files(agent_id, files_with_digests)
|
|
72
|
+
@mutex.synchronize do
|
|
73
|
+
@read_files[agent_id] = files_with_digests.dup
|
|
39
74
|
end
|
|
40
75
|
end
|
|
41
76
|
|
|
@@ -15,10 +15,13 @@ module SwarmSDK
|
|
|
15
15
|
# Use for temporary, cross-agent communication within a single session.
|
|
16
16
|
class ScratchpadStorage < Storage
|
|
17
17
|
# Initialize scratchpad storage (always volatile)
|
|
18
|
-
|
|
18
|
+
#
|
|
19
|
+
# @param total_size_limit [Integer, nil] Maximum total size in bytes (defaults to Defaults::Storage::TOTAL_SIZE_BYTES)
|
|
20
|
+
def initialize(total_size_limit: nil)
|
|
19
21
|
super() # Initialize parent Storage class
|
|
20
22
|
@entries = {}
|
|
21
23
|
@total_size = 0
|
|
24
|
+
@total_size_limit = total_size_limit || Defaults::Storage::TOTAL_SIZE_BYTES
|
|
22
25
|
@mutex = Mutex.new
|
|
23
26
|
end
|
|
24
27
|
|
|
@@ -38,8 +41,8 @@ module SwarmSDK
|
|
|
38
41
|
content_size = content.bytesize
|
|
39
42
|
|
|
40
43
|
# Check entry size limit
|
|
41
|
-
if content_size >
|
|
42
|
-
raise ArgumentError, "Content exceeds maximum size (#{format_bytes(
|
|
44
|
+
if content_size > Defaults::Storage::ENTRY_SIZE_BYTES
|
|
45
|
+
raise ArgumentError, "Content exceeds maximum size (#{format_bytes(Defaults::Storage::ENTRY_SIZE_BYTES)}). " \
|
|
43
46
|
"Current: #{format_bytes(content_size)}"
|
|
44
47
|
end
|
|
45
48
|
|
|
@@ -49,8 +52,8 @@ module SwarmSDK
|
|
|
49
52
|
new_total_size = @total_size - existing_size + content_size
|
|
50
53
|
|
|
51
54
|
# Check total size limit
|
|
52
|
-
if new_total_size >
|
|
53
|
-
raise ArgumentError, "Scratchpad full (#{format_bytes(
|
|
55
|
+
if new_total_size > @total_size_limit
|
|
56
|
+
raise ArgumentError, "Scratchpad full (#{format_bytes(@total_size_limit)} limit). " \
|
|
54
57
|
"Current: #{format_bytes(@total_size)}, " \
|
|
55
58
|
"Would be: #{format_bytes(new_total_size)}. " \
|
|
56
59
|
"Clear old entries or use smaller content."
|
|
@@ -218,6 +221,51 @@ module SwarmSDK
|
|
|
218
221
|
def size
|
|
219
222
|
@entries.size
|
|
220
223
|
end
|
|
224
|
+
|
|
225
|
+
# Get all entries with content for snapshot
|
|
226
|
+
#
|
|
227
|
+
# Thread-safe method that returns a copy of all entries.
|
|
228
|
+
# Used by snapshot/restore functionality.
|
|
229
|
+
#
|
|
230
|
+
# @return [Hash] { path => Entry }
|
|
231
|
+
def all_entries
|
|
232
|
+
@mutex.synchronize do
|
|
233
|
+
@entries.dup
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Restore entries from snapshot
|
|
238
|
+
#
|
|
239
|
+
# Restores entries directly without using write() to preserve timestamps.
|
|
240
|
+
# This ensures entry ordering and metadata accuracy after restore.
|
|
241
|
+
#
|
|
242
|
+
# @param entries_data [Hash] { path => { content:, title:, updated_at:, size: } }
|
|
243
|
+
# @return [void]
|
|
244
|
+
def restore_entries(entries_data)
|
|
245
|
+
@mutex.synchronize do
|
|
246
|
+
entries_data.each do |path, data|
|
|
247
|
+
# Handle both symbol and string keys from JSON
|
|
248
|
+
content = data[:content] || data["content"]
|
|
249
|
+
title = data[:title] || data["title"]
|
|
250
|
+
updated_at_str = data[:updated_at] || data["updated_at"]
|
|
251
|
+
|
|
252
|
+
# Parse timestamp from ISO8601 string
|
|
253
|
+
updated_at = Time.parse(updated_at_str)
|
|
254
|
+
|
|
255
|
+
# Create entry with preserved timestamp
|
|
256
|
+
entry = Entry.new(
|
|
257
|
+
content: content,
|
|
258
|
+
title: title,
|
|
259
|
+
updated_at: updated_at,
|
|
260
|
+
size: content.bytesize,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Update storage
|
|
264
|
+
@entries[path] = entry
|
|
265
|
+
@total_size += entry.size
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
221
269
|
end
|
|
222
270
|
end
|
|
223
271
|
end
|
|
@@ -14,12 +14,6 @@ module SwarmSDK
|
|
|
14
14
|
# - Search capabilities: Glob patterns and grep-style content search
|
|
15
15
|
# - Thread-safe: Mutex-protected operations
|
|
16
16
|
class Storage
|
|
17
|
-
# Maximum size per entry (1MB)
|
|
18
|
-
MAX_ENTRY_SIZE = 1_000_000
|
|
19
|
-
|
|
20
|
-
# Maximum total storage size (100MB)
|
|
21
|
-
MAX_TOTAL_SIZE = 100_000_000
|
|
22
|
-
|
|
23
17
|
# Represents a single storage entry with metadata
|
|
24
18
|
Entry = Struct.new(:content, :title, :updated_at, :size, keyword_init: true)
|
|
25
19
|
|