swarm_sdk 2.7.14 → 3.0.0.alpha2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
- 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/document_converters/base.rb +84 -0
- data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
- data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
- data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -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 +213 -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 +88 -149
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
- data/lib/swarm_sdk/agent/builder.rb +0 -705
- data/lib/swarm_sdk/agent/chat.rb +0 -1438
- 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 -588
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
- 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 -558
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
- 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 -368
- data/lib/swarm_sdk/configuration/parser.rb +0 -397
- data/lib/swarm_sdk/configuration/translator.rb +0 -285
- 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 -248
- 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 -119
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -241
- 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 -204
- data/lib/swarm_sdk/swarm/builder.rb +0 -256
- data/lib/swarm_sdk/swarm/executor.rb +0 -446
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
- data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
- 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 -973
- 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 -721
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
module Scratchpad
|
|
6
|
-
# Tool for writing content to scratchpad storage
|
|
7
|
-
#
|
|
8
|
-
# Stores content in volatile, shared storage for temporary communication.
|
|
9
|
-
# All agents in the swarm share the same scratchpad.
|
|
10
|
-
# Data is lost when the process ends (not persisted).
|
|
11
|
-
class ScratchpadWrite < Base
|
|
12
|
-
define_method(:name) { "ScratchpadWrite" }
|
|
13
|
-
|
|
14
|
-
description <<~DESC
|
|
15
|
-
Store content in scratchpad for temporary cross-agent communication.
|
|
16
|
-
|
|
17
|
-
## When to Use Scratchpad
|
|
18
|
-
|
|
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'
|
|
39
|
-
DESC
|
|
40
|
-
|
|
41
|
-
param :file_path,
|
|
42
|
-
desc: "Simple path for the content (e.g., 'status', 'result', 'notes/agent_x')",
|
|
43
|
-
required: true
|
|
44
|
-
|
|
45
|
-
param :content,
|
|
46
|
-
desc: "Content to store in scratchpad (max 1MB per entry)",
|
|
47
|
-
required: true
|
|
48
|
-
|
|
49
|
-
param :title,
|
|
50
|
-
desc: "Brief title describing the content",
|
|
51
|
-
required: true
|
|
52
|
-
|
|
53
|
-
class << self
|
|
54
|
-
# Create a ScratchpadWrite tool for a specific scratchpad storage instance
|
|
55
|
-
#
|
|
56
|
-
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
|
57
|
-
# @return [ScratchpadWrite] Tool instance
|
|
58
|
-
def create_for_scratchpad(scratchpad_storage)
|
|
59
|
-
new(scratchpad_storage)
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Initialize with scratchpad storage instance
|
|
64
|
-
#
|
|
65
|
-
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
|
66
|
-
def initialize(scratchpad_storage)
|
|
67
|
-
super() # Call RubyLLM::Tool's initialize
|
|
68
|
-
@scratchpad_storage = scratchpad_storage
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Execute the tool
|
|
72
|
-
#
|
|
73
|
-
# @param file_path [String] Path to store content
|
|
74
|
-
# @param content [String] Content to store
|
|
75
|
-
# @param title [String] Brief title
|
|
76
|
-
# @return [String] Success message with path and size
|
|
77
|
-
def execute(file_path:, content:, title:)
|
|
78
|
-
entry = scratchpad_storage.write(file_path: file_path, content: content, title: title)
|
|
79
|
-
"Stored at scratchpad://#{file_path} (#{format_bytes(entry.size)})"
|
|
80
|
-
rescue ArgumentError => e
|
|
81
|
-
validation_error(e.message)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
private
|
|
85
|
-
|
|
86
|
-
attr_reader :scratchpad_storage
|
|
87
|
-
|
|
88
|
-
def validation_error(message)
|
|
89
|
-
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Format bytes to human-readable size
|
|
93
|
-
#
|
|
94
|
-
# @param bytes [Integer] Number of bytes
|
|
95
|
-
# @return [String] Formatted size
|
|
96
|
-
def format_bytes(bytes)
|
|
97
|
-
if bytes >= 1_000_000
|
|
98
|
-
"#{(bytes.to_f / 1_000_000).round(1)}MB"
|
|
99
|
-
elsif bytes >= 1_000
|
|
100
|
-
"#{(bytes.to_f / 1_000).round(1)}KB"
|
|
101
|
-
else
|
|
102
|
-
"#{bytes}B"
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
module Stores
|
|
6
|
-
# ReadTracker manages read-file tracking for all agents with content digest verification
|
|
7
|
-
#
|
|
8
|
-
# This module maintains a global registry of which files each agent has read
|
|
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.
|
|
13
|
-
#
|
|
14
|
-
# Each agent maintains an independent map of read files to content digests.
|
|
15
|
-
module ReadTracker
|
|
16
|
-
@read_files = {} # { agent_id => { file_path => sha256_digest } }
|
|
17
|
-
@mutex = Mutex.new
|
|
18
|
-
|
|
19
|
-
class << self
|
|
20
|
-
# Register that an agent has read a file with content digest
|
|
21
|
-
#
|
|
22
|
-
# @param agent_id [Symbol] The agent identifier
|
|
23
|
-
# @param file_path [String] The absolute path to the file
|
|
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)
|
|
27
|
-
@mutex.synchronize do
|
|
28
|
-
@read_files[agent_id] ||= {}
|
|
29
|
-
digest = Digest::SHA256.hexdigest(content)
|
|
30
|
-
@read_files[agent_id][File.expand_path(file_path)] = digest
|
|
31
|
-
digest
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Check if an agent has read a file AND content hasn't changed
|
|
36
|
-
#
|
|
37
|
-
# @param agent_id [Symbol] The agent identifier
|
|
38
|
-
# @param file_path [String] The absolute path to the file
|
|
39
|
-
# @return [Boolean] true if agent read file and content matches
|
|
40
|
-
def file_read?(agent_id, file_path)
|
|
41
|
-
@mutex.synchronize do
|
|
42
|
-
return false unless @read_files[agent_id]
|
|
43
|
-
|
|
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
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Clear read history for an agent (useful for testing)
|
|
78
|
-
#
|
|
79
|
-
# @param agent_id [Symbol] The agent identifier
|
|
80
|
-
def clear(agent_id)
|
|
81
|
-
@mutex.synchronize do
|
|
82
|
-
@read_files.delete(agent_id)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Clear all read history (useful for testing)
|
|
87
|
-
def clear_all
|
|
88
|
-
@mutex.synchronize do
|
|
89
|
-
@read_files.clear
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
module Stores
|
|
6
|
-
# ScratchpadStorage provides volatile, shared storage
|
|
7
|
-
#
|
|
8
|
-
# Features:
|
|
9
|
-
# - Shared: All agents share the same scratchpad
|
|
10
|
-
# - Volatile: NEVER persists - all data lost when process ends
|
|
11
|
-
# - Path-based: Hierarchical organization using file-path-like addresses
|
|
12
|
-
# - Metadata-rich: Stores content + title + timestamp + size
|
|
13
|
-
# - Thread-safe: Mutex-protected operations
|
|
14
|
-
#
|
|
15
|
-
# Use for temporary, cross-agent communication within a single session.
|
|
16
|
-
class ScratchpadStorage < Storage
|
|
17
|
-
# Initialize scratchpad storage (always volatile)
|
|
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)
|
|
21
|
-
super() # Initialize parent Storage class
|
|
22
|
-
@entries = {}
|
|
23
|
-
@total_size = 0
|
|
24
|
-
@total_size_limit = total_size_limit || SwarmSDK.config.scratchpad_total_size_limit
|
|
25
|
-
@mutex = Mutex.new
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Write content to scratchpad
|
|
29
|
-
#
|
|
30
|
-
# @param file_path [String] Path to store content
|
|
31
|
-
# @param content [String] Content to store
|
|
32
|
-
# @param title [String] Brief title describing the content
|
|
33
|
-
# @raise [ArgumentError] If size limits are exceeded
|
|
34
|
-
# @return [Entry] The created entry
|
|
35
|
-
def write(file_path:, content:, title:)
|
|
36
|
-
@mutex.synchronize do
|
|
37
|
-
raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
|
|
38
|
-
raise ArgumentError, "content is required" if content.nil?
|
|
39
|
-
raise ArgumentError, "title is required" if title.nil? || title.to_s.strip.empty?
|
|
40
|
-
|
|
41
|
-
content_size = content.bytesize
|
|
42
|
-
|
|
43
|
-
# Check entry size limit
|
|
44
|
-
entry_size_limit = SwarmSDK.config.scratchpad_entry_size_limit
|
|
45
|
-
if content_size > entry_size_limit
|
|
46
|
-
raise ArgumentError, "Content exceeds maximum size (#{format_bytes(entry_size_limit)}). " \
|
|
47
|
-
"Current: #{format_bytes(content_size)}"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Calculate new total size
|
|
51
|
-
existing_entry = @entries[file_path]
|
|
52
|
-
existing_size = existing_entry ? existing_entry.size : 0
|
|
53
|
-
new_total_size = @total_size - existing_size + content_size
|
|
54
|
-
|
|
55
|
-
# Check total size limit
|
|
56
|
-
if new_total_size > @total_size_limit
|
|
57
|
-
raise ArgumentError, "Scratchpad full (#{format_bytes(@total_size_limit)} limit). " \
|
|
58
|
-
"Current: #{format_bytes(@total_size)}, " \
|
|
59
|
-
"Would be: #{format_bytes(new_total_size)}. " \
|
|
60
|
-
"Clear old entries or use smaller content."
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Create entry
|
|
64
|
-
entry = Entry.new(
|
|
65
|
-
content: content,
|
|
66
|
-
title: title,
|
|
67
|
-
updated_at: Time.now,
|
|
68
|
-
size: content_size,
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
# Update storage
|
|
72
|
-
@entries[file_path] = entry
|
|
73
|
-
@total_size = new_total_size
|
|
74
|
-
|
|
75
|
-
entry
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Read content from scratchpad
|
|
80
|
-
#
|
|
81
|
-
# @param file_path [String] Path to read from
|
|
82
|
-
# @raise [ArgumentError] If path not found
|
|
83
|
-
# @return [String] Content at the path
|
|
84
|
-
def read(file_path:)
|
|
85
|
-
raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
|
|
86
|
-
|
|
87
|
-
entry = @entries[file_path]
|
|
88
|
-
raise ArgumentError, "scratchpad://#{file_path} not found" unless entry
|
|
89
|
-
|
|
90
|
-
entry.content
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Delete a specific entry
|
|
94
|
-
#
|
|
95
|
-
# @param file_path [String] Path to delete
|
|
96
|
-
# @raise [ArgumentError] If path not found
|
|
97
|
-
# @return [void]
|
|
98
|
-
def delete(file_path:)
|
|
99
|
-
@mutex.synchronize do
|
|
100
|
-
raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
|
|
101
|
-
|
|
102
|
-
entry = @entries[file_path]
|
|
103
|
-
raise ArgumentError, "scratchpad://#{file_path} not found" unless entry
|
|
104
|
-
|
|
105
|
-
# Update total size
|
|
106
|
-
@total_size -= entry.size
|
|
107
|
-
|
|
108
|
-
# Remove entry
|
|
109
|
-
@entries.delete(file_path)
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# List scratchpad entries, optionally filtered by prefix
|
|
114
|
-
#
|
|
115
|
-
# @param prefix [String, nil] Filter by path prefix
|
|
116
|
-
# @return [Array<Hash>] Array of entry metadata (path, title, size, updated_at)
|
|
117
|
-
def list(prefix: nil)
|
|
118
|
-
entries = @entries
|
|
119
|
-
|
|
120
|
-
# Filter by prefix if provided
|
|
121
|
-
if prefix && !prefix.empty?
|
|
122
|
-
entries = entries.select { |path, _| path.start_with?(prefix) }
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Return metadata sorted by path
|
|
126
|
-
entries.map do |path, entry|
|
|
127
|
-
{
|
|
128
|
-
path: path,
|
|
129
|
-
title: entry.title,
|
|
130
|
-
size: entry.size,
|
|
131
|
-
updated_at: entry.updated_at,
|
|
132
|
-
}
|
|
133
|
-
end.sort_by { |e| e[:path] }
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Search entries by glob pattern
|
|
137
|
-
#
|
|
138
|
-
# @param pattern [String] Glob pattern (e.g., "**/*.txt", "parallel/*/task_*")
|
|
139
|
-
# @return [Array<Hash>] Array of matching entry metadata, sorted by most recent first
|
|
140
|
-
def glob(pattern:)
|
|
141
|
-
raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?
|
|
142
|
-
|
|
143
|
-
# Convert glob pattern to regex
|
|
144
|
-
regex = glob_to_regex(pattern)
|
|
145
|
-
|
|
146
|
-
# Filter entries by pattern
|
|
147
|
-
matching_entries = @entries.select { |path, _| regex.match?(path) }
|
|
148
|
-
|
|
149
|
-
# Return metadata sorted by most recent first
|
|
150
|
-
matching_entries.map do |path, entry|
|
|
151
|
-
{
|
|
152
|
-
path: path,
|
|
153
|
-
title: entry.title,
|
|
154
|
-
size: entry.size,
|
|
155
|
-
updated_at: entry.updated_at,
|
|
156
|
-
}
|
|
157
|
-
end.sort_by { |e| -e[:updated_at].to_f }
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
# Search entry content by pattern
|
|
161
|
-
#
|
|
162
|
-
# @param pattern [String] Regular expression pattern to search for
|
|
163
|
-
# @param case_insensitive [Boolean] Whether to perform case-insensitive search
|
|
164
|
-
# @param output_mode [String] Output mode: "files_with_matches" (default), "content", or "count"
|
|
165
|
-
# @return [Array<Hash>, String] Results based on output_mode
|
|
166
|
-
def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
|
|
167
|
-
raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?
|
|
168
|
-
|
|
169
|
-
# Create regex from pattern
|
|
170
|
-
flags = case_insensitive ? Regexp::IGNORECASE : 0
|
|
171
|
-
regex = Regexp.new(pattern, flags)
|
|
172
|
-
|
|
173
|
-
case output_mode
|
|
174
|
-
when "files_with_matches"
|
|
175
|
-
# Return just the paths that match
|
|
176
|
-
matching_paths = @entries.select { |_path, entry| regex.match?(entry.content) }
|
|
177
|
-
.map { |path, _| path }
|
|
178
|
-
.sort
|
|
179
|
-
matching_paths
|
|
180
|
-
when "content"
|
|
181
|
-
# Return paths with matching lines, sorted by most recent first
|
|
182
|
-
results = []
|
|
183
|
-
@entries.each do |path, entry|
|
|
184
|
-
matching_lines = []
|
|
185
|
-
entry.content.each_line.with_index(1) do |line, line_num|
|
|
186
|
-
matching_lines << { line_number: line_num, content: line.chomp } if regex.match?(line)
|
|
187
|
-
end
|
|
188
|
-
results << { path: path, matches: matching_lines, updated_at: entry.updated_at } unless matching_lines.empty?
|
|
189
|
-
end
|
|
190
|
-
results.sort_by { |r| -r[:updated_at].to_f }.map { |r| r.except(:updated_at) }
|
|
191
|
-
when "count"
|
|
192
|
-
# Return paths with match counts, sorted by most recent first
|
|
193
|
-
results = []
|
|
194
|
-
@entries.each do |path, entry|
|
|
195
|
-
count = entry.content.scan(regex).size
|
|
196
|
-
results << { path: path, count: count, updated_at: entry.updated_at } if count > 0
|
|
197
|
-
end
|
|
198
|
-
results.sort_by { |r| -r[:updated_at].to_f }.map { |r| r.except(:updated_at) }
|
|
199
|
-
else
|
|
200
|
-
raise ArgumentError, "Invalid output_mode: #{output_mode}. Must be 'files_with_matches', 'content', or 'count'"
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
# Clear all entries
|
|
205
|
-
#
|
|
206
|
-
# @return [void]
|
|
207
|
-
def clear
|
|
208
|
-
@mutex.synchronize do
|
|
209
|
-
@entries.clear
|
|
210
|
-
@total_size = 0
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# Get current total size
|
|
215
|
-
#
|
|
216
|
-
# @return [Integer] Total size in bytes
|
|
217
|
-
attr_reader :total_size
|
|
218
|
-
|
|
219
|
-
# Get number of entries
|
|
220
|
-
#
|
|
221
|
-
# @return [Integer] Number of entries
|
|
222
|
-
def size
|
|
223
|
-
@entries.size
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Get all entries with content for snapshot
|
|
227
|
-
#
|
|
228
|
-
# Thread-safe method that returns a copy of all entries.
|
|
229
|
-
# Used by snapshot/restore functionality.
|
|
230
|
-
#
|
|
231
|
-
# @return [Hash] { path => Entry }
|
|
232
|
-
def all_entries
|
|
233
|
-
@mutex.synchronize do
|
|
234
|
-
@entries.dup
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
# Restore entries from snapshot
|
|
239
|
-
#
|
|
240
|
-
# Restores entries directly without using write() to preserve timestamps.
|
|
241
|
-
# This ensures entry ordering and metadata accuracy after restore.
|
|
242
|
-
#
|
|
243
|
-
# @param entries_data [Hash] { path => { content:, title:, updated_at:, size: } }
|
|
244
|
-
# @return [void]
|
|
245
|
-
def restore_entries(entries_data)
|
|
246
|
-
@mutex.synchronize do
|
|
247
|
-
entries_data.each do |path, data|
|
|
248
|
-
# Handle both symbol and string keys from JSON
|
|
249
|
-
content = data[:content] || data["content"]
|
|
250
|
-
title = data[:title] || data["title"]
|
|
251
|
-
updated_at_str = data[:updated_at] || data["updated_at"]
|
|
252
|
-
|
|
253
|
-
# Parse timestamp from ISO8601 string
|
|
254
|
-
updated_at = Time.parse(updated_at_str)
|
|
255
|
-
|
|
256
|
-
# Create entry with preserved timestamp
|
|
257
|
-
entry = Entry.new(
|
|
258
|
-
content: content,
|
|
259
|
-
title: title,
|
|
260
|
-
updated_at: updated_at,
|
|
261
|
-
size: content.bytesize,
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
# Update storage
|
|
265
|
-
@entries[path] = entry
|
|
266
|
-
@total_size += entry.size
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
end
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
|
-
end
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
module Stores
|
|
6
|
-
# Abstract base class for hierarchical key-value storage with metadata
|
|
7
|
-
#
|
|
8
|
-
# Provides session-scoped storage for agents with path-based organization.
|
|
9
|
-
# Subclasses implement persistence behavior (volatile vs persistent).
|
|
10
|
-
#
|
|
11
|
-
# Features:
|
|
12
|
-
# - Path-based: Hierarchical organization using file-path-like addresses
|
|
13
|
-
# - Metadata-rich: Stores content + title + timestamp + size
|
|
14
|
-
# - Search capabilities: Glob patterns and grep-style content search
|
|
15
|
-
# - Thread-safe: Mutex-protected operations
|
|
16
|
-
class Storage
|
|
17
|
-
# Represents a single storage entry with metadata
|
|
18
|
-
Entry = Struct.new(:content, :title, :updated_at, :size, keyword_init: true)
|
|
19
|
-
|
|
20
|
-
# Initialize storage
|
|
21
|
-
#
|
|
22
|
-
# Subclasses should call super() in their initialize method.
|
|
23
|
-
# This base implementation does nothing - it exists only to satisfy RuboCop.
|
|
24
|
-
def initialize
|
|
25
|
-
# Base class initialization - subclasses implement their own logic
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Write content to storage
|
|
29
|
-
#
|
|
30
|
-
# @param file_path [String] Path to store content
|
|
31
|
-
# @param content [String] Content to store
|
|
32
|
-
# @param title [String] Brief title describing the content
|
|
33
|
-
# @raise [ArgumentError] If size limits are exceeded
|
|
34
|
-
# @return [Entry] The created entry
|
|
35
|
-
def write(file_path:, content:, title:)
|
|
36
|
-
raise NotImplementedError, "Subclass must implement #write"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Read content from storage
|
|
40
|
-
#
|
|
41
|
-
# @param file_path [String] Path to read from
|
|
42
|
-
# @raise [ArgumentError] If path not found
|
|
43
|
-
# @return [String] Content at the path
|
|
44
|
-
def read(file_path:)
|
|
45
|
-
raise NotImplementedError, "Subclass must implement #read"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Delete a specific entry
|
|
49
|
-
#
|
|
50
|
-
# @param file_path [String] Path to delete
|
|
51
|
-
# @raise [ArgumentError] If path not found
|
|
52
|
-
# @return [void]
|
|
53
|
-
def delete(file_path:)
|
|
54
|
-
raise NotImplementedError, "Subclass must implement #delete"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# List entries, optionally filtered by prefix
|
|
58
|
-
#
|
|
59
|
-
# @param prefix [String, nil] Filter by path prefix
|
|
60
|
-
# @return [Array<Hash>] Array of entry metadata (path, title, size, updated_at)
|
|
61
|
-
def list(prefix: nil)
|
|
62
|
-
raise NotImplementedError, "Subclass must implement #list"
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Search entries by glob pattern
|
|
66
|
-
#
|
|
67
|
-
# @param pattern [String] Glob pattern (e.g., "**/*.txt", "parallel/*/task_*")
|
|
68
|
-
# @return [Array<Hash>] Array of matching entry metadata, sorted by most recent first
|
|
69
|
-
def glob(pattern:)
|
|
70
|
-
raise NotImplementedError, "Subclass must implement #glob"
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Search entry content by pattern
|
|
74
|
-
#
|
|
75
|
-
# @param pattern [String] Regular expression pattern to search for
|
|
76
|
-
# @param case_insensitive [Boolean] Whether to perform case-insensitive search
|
|
77
|
-
# @param output_mode [String] Output mode: "files_with_matches" (default), "content", or "count"
|
|
78
|
-
# @return [Array<Hash>, String] Results based on output_mode
|
|
79
|
-
def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
|
|
80
|
-
raise NotImplementedError, "Subclass must implement #grep"
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Clear all entries
|
|
84
|
-
#
|
|
85
|
-
# @return [void]
|
|
86
|
-
def clear
|
|
87
|
-
raise NotImplementedError, "Subclass must implement #clear"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Get current total size
|
|
91
|
-
#
|
|
92
|
-
# @return [Integer] Total size in bytes
|
|
93
|
-
def total_size
|
|
94
|
-
raise NotImplementedError, "Subclass must implement #total_size"
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Get number of entries
|
|
98
|
-
#
|
|
99
|
-
# @return [Integer] Number of entries
|
|
100
|
-
def size
|
|
101
|
-
raise NotImplementedError, "Subclass must implement #size"
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
protected
|
|
105
|
-
|
|
106
|
-
# Format bytes to human-readable size
|
|
107
|
-
#
|
|
108
|
-
# @param bytes [Integer] Number of bytes
|
|
109
|
-
# @return [String] Formatted size (e.g., "1.5MB", "500.0KB")
|
|
110
|
-
def format_bytes(bytes)
|
|
111
|
-
if bytes >= 1_000_000
|
|
112
|
-
"#{(bytes.to_f / 1_000_000).round(1)}MB"
|
|
113
|
-
elsif bytes >= 1_000
|
|
114
|
-
"#{(bytes.to_f / 1_000).round(1)}KB"
|
|
115
|
-
else
|
|
116
|
-
"#{bytes}B"
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Convert glob pattern to regex
|
|
121
|
-
#
|
|
122
|
-
# @param pattern [String] Glob pattern
|
|
123
|
-
# @return [Regexp] Regular expression
|
|
124
|
-
def glob_to_regex(pattern)
|
|
125
|
-
# Escape special regex characters except glob wildcards
|
|
126
|
-
escaped = Regexp.escape(pattern)
|
|
127
|
-
|
|
128
|
-
# Convert glob wildcards to regex
|
|
129
|
-
# ** matches any number of directories (including zero)
|
|
130
|
-
escaped = escaped.gsub('\*\*', ".*")
|
|
131
|
-
# * matches anything except directory separator
|
|
132
|
-
escaped = escaped.gsub('\*', "[^/]*")
|
|
133
|
-
# ? matches single character except directory separator
|
|
134
|
-
escaped = escaped.gsub('\?', "[^/]")
|
|
135
|
-
|
|
136
|
-
# Anchor to start and end
|
|
137
|
-
Regexp.new("\\A#{escaped}\\z")
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
end
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
module Stores
|
|
6
|
-
# TodoManager provides per-agent todo list storage
|
|
7
|
-
#
|
|
8
|
-
# Each agent maintains its own independent todo list that persists
|
|
9
|
-
# throughout the agent's execution session. This allows agents to
|
|
10
|
-
# track progress on complex multi-step tasks.
|
|
11
|
-
class TodoManager
|
|
12
|
-
@storage = {}
|
|
13
|
-
@mutex = Mutex.new
|
|
14
|
-
|
|
15
|
-
class << self
|
|
16
|
-
# Get the current todo list for an agent
|
|
17
|
-
#
|
|
18
|
-
# @param agent_id [Symbol, String] Unique agent identifier
|
|
19
|
-
# @return [Array<Hash>] Array of todo items
|
|
20
|
-
def get_todos(agent_id)
|
|
21
|
-
@mutex.synchronize do
|
|
22
|
-
@storage[agent_id.to_sym] ||= []
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Set the todo list for an agent
|
|
27
|
-
#
|
|
28
|
-
# @param agent_id [Symbol, String] Unique agent identifier
|
|
29
|
-
# @param todos [Array<Hash>] Array of todo items
|
|
30
|
-
# @return [Array<Hash>] The stored todos
|
|
31
|
-
def set_todos(agent_id, todos)
|
|
32
|
-
@mutex.synchronize do
|
|
33
|
-
@storage[agent_id.to_sym] = todos
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Clear all todos for an agent
|
|
38
|
-
#
|
|
39
|
-
# @param agent_id [Symbol, String] Unique agent identifier
|
|
40
|
-
def clear_todos(agent_id)
|
|
41
|
-
@mutex.synchronize do
|
|
42
|
-
@storage.delete(agent_id.to_sym)
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Clear all todos for all agents
|
|
47
|
-
def clear_all
|
|
48
|
-
@mutex.synchronize do
|
|
49
|
-
@storage.clear
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Get summary of all agent todo lists
|
|
54
|
-
#
|
|
55
|
-
# @return [Hash] Map of agent_id => todo count
|
|
56
|
-
def summary
|
|
57
|
-
@mutex.synchronize do
|
|
58
|
-
@storage.transform_values(&:size)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|