swarm_sdk 2.7.13 → 3.0.0.alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +43 -22
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +6 -0
  4. data/lib/swarm_sdk/ruby_llm_patches/mcp_ssl_patch.rb +144 -0
  5. data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +3 -4
  6. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  7. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  8. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  9. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  10. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  11. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  12. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  13. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  14. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  15. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  16. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  17. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  18. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  19. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  20. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  24. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  25. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  26. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  27. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  28. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  29. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  30. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  31. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  32. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  33. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  34. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  35. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  36. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  37. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  38. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  39. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  40. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  41. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  42. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  43. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  45. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  46. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  47. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  48. data/lib/swarm_sdk/v3/tools/read.rb +181 -0
  49. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  50. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  51. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  52. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  53. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  54. data/lib/swarm_sdk/v3.rb +145 -0
  55. metadata +84 -148
  56. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  57. data/lib/swarm_sdk/agent/builder.rb +0 -680
  58. data/lib/swarm_sdk/agent/chat.rb +0 -1432
  59. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  60. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  61. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  62. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  63. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  64. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  65. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  66. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  67. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  68. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  69. data/lib/swarm_sdk/agent/context.rb +0 -115
  70. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  71. data/lib/swarm_sdk/agent/definition.rb +0 -581
  72. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  73. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  74. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  75. data/lib/swarm_sdk/agent_registry.rb +0 -146
  76. data/lib/swarm_sdk/builders/base_builder.rb +0 -553
  77. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  78. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  79. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  80. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  81. data/lib/swarm_sdk/config.rb +0 -367
  82. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  83. data/lib/swarm_sdk/configuration/translator.rb +0 -283
  84. data/lib/swarm_sdk/configuration.rb +0 -165
  85. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  86. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  87. data/lib/swarm_sdk/context_compactor.rb +0 -335
  88. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  89. data/lib/swarm_sdk/context_management/context.rb +0 -328
  90. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  91. data/lib/swarm_sdk/defaults.rb +0 -251
  92. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  93. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  94. data/lib/swarm_sdk/hooks/context.rb +0 -197
  95. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  96. data/lib/swarm_sdk/hooks/error.rb +0 -29
  97. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  98. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  99. data/lib/swarm_sdk/hooks/result.rb +0 -150
  100. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  101. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  102. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  103. data/lib/swarm_sdk/log_collector.rb +0 -227
  104. data/lib/swarm_sdk/log_stream.rb +0 -127
  105. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  106. data/lib/swarm_sdk/model_aliases.json +0 -8
  107. data/lib/swarm_sdk/models.json +0 -44002
  108. data/lib/swarm_sdk/models.rb +0 -161
  109. data/lib/swarm_sdk/node_context.rb +0 -245
  110. data/lib/swarm_sdk/observer/builder.rb +0 -81
  111. data/lib/swarm_sdk/observer/config.rb +0 -45
  112. data/lib/swarm_sdk/observer/manager.rb +0 -236
  113. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  114. data/lib/swarm_sdk/permissions/config.rb +0 -239
  115. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  116. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  117. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  118. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  119. data/lib/swarm_sdk/plugin.rb +0 -309
  120. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  121. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  122. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  123. data/lib/swarm_sdk/restore_result.rb +0 -65
  124. data/lib/swarm_sdk/result.rb +0 -212
  125. data/lib/swarm_sdk/snapshot.rb +0 -156
  126. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  127. data/lib/swarm_sdk/state_restorer.rb +0 -476
  128. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  129. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  130. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -195
  131. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  132. data/lib/swarm_sdk/swarm/executor.rb +0 -290
  133. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -151
  134. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  135. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -360
  136. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -270
  137. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  138. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  139. data/lib/swarm_sdk/swarm.rb +0 -843
  140. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  141. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  142. data/lib/swarm_sdk/tools/base.rb +0 -63
  143. data/lib/swarm_sdk/tools/bash.rb +0 -280
  144. data/lib/swarm_sdk/tools/clock.rb +0 -46
  145. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  146. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  147. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  148. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  149. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  150. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  151. data/lib/swarm_sdk/tools/edit.rb +0 -145
  152. data/lib/swarm_sdk/tools/glob.rb +0 -166
  153. data/lib/swarm_sdk/tools/grep.rb +0 -235
  154. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  155. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  156. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  157. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  158. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  159. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  160. data/lib/swarm_sdk/tools/read.rb +0 -261
  161. data/lib/swarm_sdk/tools/registry.rb +0 -205
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  165. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  166. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  167. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  168. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  169. data/lib/swarm_sdk/tools/think.rb +0 -100
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  174. data/lib/swarm_sdk/utils.rb +0 -68
  175. data/lib/swarm_sdk/validation_result.rb +0 -33
  176. data/lib/swarm_sdk/version.rb +0 -5
  177. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  182. data/lib/swarm_sdk/workflow.rb +0 -589
  183. data/lib/swarm_sdk.rb +0 -718
@@ -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