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.
Files changed (185) 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/document_converters/base.rb +84 -0
  42. data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
  43. data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -0
  45. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  46. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  47. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  48. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  49. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  50. data/lib/swarm_sdk/v3/tools/read.rb +213 -0
  51. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  52. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  53. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  54. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  55. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  56. data/lib/swarm_sdk/v3.rb +145 -0
  57. metadata +88 -149
  58. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  59. data/lib/swarm_sdk/agent/builder.rb +0 -705
  60. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  61. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  62. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  63. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  64. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  65. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  66. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  67. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  68. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  69. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  70. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  71. data/lib/swarm_sdk/agent/context.rb +0 -115
  72. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  73. data/lib/swarm_sdk/agent/definition.rb +0 -588
  74. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  75. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  76. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  77. data/lib/swarm_sdk/agent_registry.rb +0 -146
  78. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  79. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  80. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  81. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  82. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  83. data/lib/swarm_sdk/config.rb +0 -368
  84. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  85. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  86. data/lib/swarm_sdk/configuration.rb +0 -165
  87. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  88. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  89. data/lib/swarm_sdk/context_compactor.rb +0 -335
  90. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  91. data/lib/swarm_sdk/context_management/context.rb +0 -328
  92. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  93. data/lib/swarm_sdk/defaults.rb +0 -251
  94. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  95. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  96. data/lib/swarm_sdk/hooks/context.rb +0 -197
  97. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  98. data/lib/swarm_sdk/hooks/error.rb +0 -29
  99. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  100. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  101. data/lib/swarm_sdk/hooks/result.rb +0 -150
  102. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  103. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  104. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  105. data/lib/swarm_sdk/log_collector.rb +0 -227
  106. data/lib/swarm_sdk/log_stream.rb +0 -127
  107. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  108. data/lib/swarm_sdk/model_aliases.json +0 -8
  109. data/lib/swarm_sdk/models.json +0 -44002
  110. data/lib/swarm_sdk/models.rb +0 -161
  111. data/lib/swarm_sdk/node_context.rb +0 -245
  112. data/lib/swarm_sdk/observer/builder.rb +0 -81
  113. data/lib/swarm_sdk/observer/config.rb +0 -45
  114. data/lib/swarm_sdk/observer/manager.rb +0 -248
  115. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  116. data/lib/swarm_sdk/permissions/config.rb +0 -239
  117. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  118. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  119. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  120. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  121. data/lib/swarm_sdk/plugin.rb +0 -309
  122. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  123. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  124. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  125. data/lib/swarm_sdk/restore_result.rb +0 -65
  126. data/lib/swarm_sdk/result.rb +0 -241
  127. data/lib/swarm_sdk/snapshot.rb +0 -156
  128. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  129. data/lib/swarm_sdk/state_restorer.rb +0 -476
  130. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  131. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  132. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  133. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  134. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  135. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  136. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  141. data/lib/swarm_sdk/swarm.rb +0 -973
  142. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  143. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  144. data/lib/swarm_sdk/tools/base.rb +0 -63
  145. data/lib/swarm_sdk/tools/bash.rb +0 -280
  146. data/lib/swarm_sdk/tools/clock.rb +0 -46
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  148. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  149. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  150. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  151. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  152. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  153. data/lib/swarm_sdk/tools/edit.rb +0 -145
  154. data/lib/swarm_sdk/tools/glob.rb +0 -166
  155. data/lib/swarm_sdk/tools/grep.rb +0 -235
  156. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  157. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  160. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  161. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  162. data/lib/swarm_sdk/tools/read.rb +0 -261
  163. data/lib/swarm_sdk/tools/registry.rb +0 -205
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  166. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  167. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  168. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  169. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  170. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  171. data/lib/swarm_sdk/tools/think.rb +0 -100
  172. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  173. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  174. data/lib/swarm_sdk/tools/write.rb +0 -112
  175. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  176. data/lib/swarm_sdk/utils.rb +0 -68
  177. data/lib/swarm_sdk/validation_result.rb +0 -33
  178. data/lib/swarm_sdk/version.rb +0 -5
  179. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  180. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  181. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  182. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  183. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  184. data/lib/swarm_sdk/workflow.rb +0 -589
  185. 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