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