claude_swarm 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/release.md +1 -1
  3. data/.claude/hooks/lint-code-files.rb +65 -0
  4. data/.rubocop.yml +22 -2
  5. data/CHANGELOG.md +14 -1
  6. data/CLAUDE.md +1 -1
  7. data/CONTRIBUTING.md +69 -0
  8. data/README.md +27 -2
  9. data/Rakefile +71 -3
  10. data/analyze_coverage.rb +94 -0
  11. data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
  12. data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
  13. data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
  14. data/docs/v2/README.md +308 -0
  15. data/docs/v2/guides/claude-code-agents.md +262 -0
  16. data/docs/v2/guides/complete-tutorial.md +3088 -0
  17. data/docs/v2/guides/getting-started.md +1456 -0
  18. data/docs/v2/guides/memory-adapters.md +998 -0
  19. data/docs/v2/guides/plugins.md +816 -0
  20. data/docs/v2/guides/quick-start-cli.md +1745 -0
  21. data/docs/v2/guides/rails-integration.md +1902 -0
  22. data/docs/v2/guides/swarm-memory.md +599 -0
  23. data/docs/v2/reference/cli.md +729 -0
  24. data/docs/v2/reference/ruby-dsl.md +2154 -0
  25. data/docs/v2/reference/yaml.md +1835 -0
  26. data/docs-team-swarm.yml +2222 -0
  27. data/examples/learning-assistant/assistant.md +7 -0
  28. data/examples/learning-assistant/example-memories/concept-example.md +90 -0
  29. data/examples/learning-assistant/example-memories/experience-example.md +66 -0
  30. data/examples/learning-assistant/example-memories/fact-example.md +76 -0
  31. data/examples/learning-assistant/example-memories/memory-index.md +78 -0
  32. data/examples/learning-assistant/example-memories/skill-example.md +168 -0
  33. data/examples/learning-assistant/learning_assistant.rb +34 -0
  34. data/examples/learning-assistant/learning_assistant.yml +20 -0
  35. data/examples/v2/dsl/01_basic.rb +44 -0
  36. data/examples/v2/dsl/02_core_parameters.rb +59 -0
  37. data/examples/v2/dsl/03_capabilities.rb +71 -0
  38. data/examples/v2/dsl/04_llm_parameters.rb +56 -0
  39. data/examples/v2/dsl/05_advanced_flags.rb +73 -0
  40. data/examples/v2/dsl/06_permissions.rb +80 -0
  41. data/examples/v2/dsl/07_mcp_server.rb +62 -0
  42. data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
  43. data/examples/v2/dsl/09_agent_hooks.rb +67 -0
  44. data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
  45. data/examples/v2/dsl/11_delegation.rb +60 -0
  46. data/examples/v2/dsl/12_complete_integration.rb +137 -0
  47. data/examples/v2/file_tools_swarm.yml +102 -0
  48. data/examples/v2/hooks/01_basic_hooks.rb +133 -0
  49. data/examples/v2/hooks/02_usage_tracking.rb +201 -0
  50. data/examples/v2/hooks/03_production_monitoring.rb +429 -0
  51. data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
  52. data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
  53. data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
  54. data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
  55. data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
  56. data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
  57. data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
  58. data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
  59. data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
  60. data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
  61. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
  62. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
  63. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
  64. data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
  65. data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
  66. data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
  67. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
  68. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
  69. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
  70. data/examples/v2/hooks/swarm_summary.sh +44 -0
  71. data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
  72. data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
  73. data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
  74. data/examples/v2/hooks/validate_bash.rb +59 -0
  75. data/examples/v2/multi_directory_permissions.yml +221 -0
  76. data/examples/v2/node_context_demo.rb +127 -0
  77. data/examples/v2/node_workflow.rb +173 -0
  78. data/examples/v2/path_resolution_demo.rb +216 -0
  79. data/examples/v2/simple-swarm-v2.rb +90 -0
  80. data/examples/v2/simple-swarm-v2.yml +62 -0
  81. data/examples/v2/swarm.yml +71 -0
  82. data/examples/v2/swarm_with_hooks.yml +61 -0
  83. data/examples/v2/swarm_with_hooks_simple.yml +25 -0
  84. data/examples/v2/think_tool_demo.rb +62 -0
  85. data/exe/swarm +6 -0
  86. data/lib/claude_swarm/claude_mcp_server.rb +0 -6
  87. data/lib/claude_swarm/cli.rb +10 -3
  88. data/lib/claude_swarm/commands/ps.rb +19 -20
  89. data/lib/claude_swarm/commands/show.rb +1 -1
  90. data/lib/claude_swarm/configuration.rb +10 -12
  91. data/lib/claude_swarm/mcp_generator.rb +10 -1
  92. data/lib/claude_swarm/orchestrator.rb +73 -49
  93. data/lib/claude_swarm/system_utils.rb +37 -11
  94. data/lib/claude_swarm/version.rb +1 -1
  95. data/lib/claude_swarm/worktree_manager.rb +1 -0
  96. data/lib/claude_swarm/yaml_loader.rb +22 -0
  97. data/lib/claude_swarm.rb +7 -2
  98. data/lib/swarm_cli/cli.rb +201 -0
  99. data/lib/swarm_cli/command_registry.rb +61 -0
  100. data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
  101. data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
  102. data/lib/swarm_cli/commands/migrate.rb +55 -0
  103. data/lib/swarm_cli/commands/run.rb +173 -0
  104. data/lib/swarm_cli/config_loader.rb +97 -0
  105. data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
  106. data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
  107. data/lib/swarm_cli/interactive_repl.rb +918 -0
  108. data/lib/swarm_cli/mcp_serve_options.rb +44 -0
  109. data/lib/swarm_cli/mcp_tools_options.rb +59 -0
  110. data/lib/swarm_cli/migrate_options.rb +54 -0
  111. data/lib/swarm_cli/migrator.rb +132 -0
  112. data/lib/swarm_cli/options.rb +151 -0
  113. data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
  114. data/lib/swarm_cli/ui/components/content_block.rb +120 -0
  115. data/lib/swarm_cli/ui/components/divider.rb +57 -0
  116. data/lib/swarm_cli/ui/components/panel.rb +62 -0
  117. data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
  118. data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
  119. data/lib/swarm_cli/ui/formatters/number.rb +58 -0
  120. data/lib/swarm_cli/ui/formatters/text.rb +77 -0
  121. data/lib/swarm_cli/ui/formatters/time.rb +73 -0
  122. data/lib/swarm_cli/ui/icons.rb +59 -0
  123. data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
  124. data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
  125. data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
  126. data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
  127. data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
  128. data/lib/swarm_cli/version.rb +5 -0
  129. data/lib/swarm_cli.rb +44 -0
  130. data/lib/swarm_memory/adapters/base.rb +141 -0
  131. data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
  132. data/lib/swarm_memory/chat_extension.rb +34 -0
  133. data/lib/swarm_memory/cli/commands.rb +306 -0
  134. data/lib/swarm_memory/core/entry.rb +37 -0
  135. data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
  136. data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
  137. data/lib/swarm_memory/core/path_normalizer.rb +75 -0
  138. data/lib/swarm_memory/core/semantic_index.rb +244 -0
  139. data/lib/swarm_memory/core/storage.rb +288 -0
  140. data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
  141. data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
  142. data/lib/swarm_memory/dsl/memory_config.rb +113 -0
  143. data/lib/swarm_memory/embeddings/embedder.rb +36 -0
  144. data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
  145. data/lib/swarm_memory/errors.rb +21 -0
  146. data/lib/swarm_memory/integration/cli_registration.rb +30 -0
  147. data/lib/swarm_memory/integration/configuration.rb +43 -0
  148. data/lib/swarm_memory/integration/registration.rb +31 -0
  149. data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
  150. data/lib/swarm_memory/optimization/analyzer.rb +244 -0
  151. data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
  152. data/lib/swarm_memory/prompts/memory.md.erb +109 -0
  153. data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
  154. data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
  155. data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
  156. data/lib/swarm_memory/search/semantic_search.rb +112 -0
  157. data/lib/swarm_memory/search/text_search.rb +42 -0
  158. data/lib/swarm_memory/search/text_similarity.rb +80 -0
  159. data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
  160. data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
  161. data/lib/swarm_memory/tools/load_skill.rb +313 -0
  162. data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
  163. data/lib/swarm_memory/tools/memory_delete.rb +99 -0
  164. data/lib/swarm_memory/tools/memory_edit.rb +185 -0
  165. data/lib/swarm_memory/tools/memory_glob.rb +160 -0
  166. data/lib/swarm_memory/tools/memory_grep.rb +247 -0
  167. data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
  168. data/lib/swarm_memory/tools/memory_read.rb +123 -0
  169. data/lib/swarm_memory/tools/memory_write.rb +231 -0
  170. data/lib/swarm_memory/utils.rb +50 -0
  171. data/lib/swarm_memory/version.rb +5 -0
  172. data/lib/swarm_memory.rb +166 -0
  173. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  174. data/lib/swarm_sdk/agent/builder.rb +461 -0
  175. data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
  176. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  177. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
  178. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
  179. data/lib/swarm_sdk/agent/chat.rb +1159 -0
  180. data/lib/swarm_sdk/agent/context.rb +112 -0
  181. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  182. data/lib/swarm_sdk/agent/definition.rb +556 -0
  183. data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
  184. data/lib/swarm_sdk/configuration.rb +296 -0
  185. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  186. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  187. data/lib/swarm_sdk/context_compactor.rb +340 -0
  188. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  189. data/lib/swarm_sdk/hooks/context.rb +197 -0
  190. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  191. data/lib/swarm_sdk/hooks/error.rb +29 -0
  192. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  193. data/lib/swarm_sdk/hooks/registry.rb +147 -0
  194. data/lib/swarm_sdk/hooks/result.rb +150 -0
  195. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  196. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  197. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  198. data/lib/swarm_sdk/log_collector.rb +51 -0
  199. data/lib/swarm_sdk/log_stream.rb +69 -0
  200. data/lib/swarm_sdk/markdown_parser.rb +75 -0
  201. data/lib/swarm_sdk/model_aliases.json +5 -0
  202. data/lib/swarm_sdk/models.json +1 -0
  203. data/lib/swarm_sdk/models.rb +120 -0
  204. data/lib/swarm_sdk/node/agent_config.rb +49 -0
  205. data/lib/swarm_sdk/node/builder.rb +439 -0
  206. data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
  207. data/lib/swarm_sdk/node_context.rb +170 -0
  208. data/lib/swarm_sdk/node_orchestrator.rb +384 -0
  209. data/lib/swarm_sdk/permissions/config.rb +239 -0
  210. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  211. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  212. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  213. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  214. data/lib/swarm_sdk/plugin.rb +147 -0
  215. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  216. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
  217. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  218. data/lib/swarm_sdk/result.rb +97 -0
  219. data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
  220. data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
  221. data/lib/swarm_sdk/swarm/builder.rb +586 -0
  222. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  223. data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
  224. data/lib/swarm_sdk/swarm.rb +982 -0
  225. data/lib/swarm_sdk/tools/bash.rb +274 -0
  226. data/lib/swarm_sdk/tools/clock.rb +44 -0
  227. data/lib/swarm_sdk/tools/delegate.rb +164 -0
  228. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  229. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  230. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  231. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  232. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  233. data/lib/swarm_sdk/tools/edit.rb +150 -0
  234. data/lib/swarm_sdk/tools/glob.rb +158 -0
  235. data/lib/swarm_sdk/tools/grep.rb +228 -0
  236. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  237. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  238. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  239. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  240. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  241. data/lib/swarm_sdk/tools/read.rb +251 -0
  242. data/lib/swarm_sdk/tools/registry.rb +93 -0
  243. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  244. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  245. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  246. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  247. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  248. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  249. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  250. data/lib/swarm_sdk/tools/think.rb +95 -0
  251. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  252. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  253. data/lib/swarm_sdk/tools/write.rb +117 -0
  254. data/lib/swarm_sdk/utils.rb +50 -0
  255. data/lib/swarm_sdk/version.rb +5 -0
  256. data/lib/swarm_sdk.rb +157 -0
  257. data/llm.v2.txt +13407 -0
  258. data/rubocop/cop/security/no_reflection_methods.rb +47 -0
  259. data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
  260. data/swarm_cli.gemspec +57 -0
  261. data/swarm_memory.gemspec +28 -0
  262. data/swarm_sdk.gemspec +41 -0
  263. data/team.yml +1 -1
  264. data/team_full.yml +1875 -0
  265. data/{team_v2.yml → team_sdk.yml} +121 -52
  266. metadata +249 -6
  267. data/EXAMPLES.md +0 -164
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Tools
5
+ module Scratchpad
6
+ # Tool for listing scratchpad entries
7
+ #
8
+ # Shows all entries in the shared scratchpad with their metadata.
9
+ # All agents in the swarm share the same scratchpad.
10
+ class ScratchpadList < RubyLLM::Tool
11
+ define_method(:name) { "ScratchpadList" }
12
+
13
+ description <<~DESC
14
+ List all entries in scratchpad with their metadata.
15
+ Shows path, title, size, and last updated time for each entry.
16
+ Use this to discover what's stored in the scratchpad.
17
+ DESC
18
+
19
+ param :prefix,
20
+ desc: "Optional prefix to filter entries (e.g., 'notes/' to list all entries under notes/)",
21
+ required: false
22
+
23
+ class << self
24
+ # Create a ScratchpadList tool for a specific scratchpad storage instance
25
+ #
26
+ # @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
27
+ # @return [ScratchpadList] Tool instance
28
+ def create_for_scratchpad(scratchpad_storage)
29
+ new(scratchpad_storage)
30
+ end
31
+ end
32
+
33
+ # Initialize with scratchpad storage instance
34
+ #
35
+ # @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
36
+ def initialize(scratchpad_storage)
37
+ super() # Call RubyLLM::Tool's initialize
38
+ @scratchpad_storage = scratchpad_storage
39
+ end
40
+
41
+ # Execute the tool
42
+ #
43
+ # @param prefix [String, nil] Optional prefix to filter entries
44
+ # @return [String] Formatted list of entries
45
+ def execute(prefix: nil)
46
+ entries = scratchpad_storage.list(prefix: prefix)
47
+
48
+ if entries.empty?
49
+ prefix_msg = prefix ? " with prefix '#{prefix}'" : ""
50
+ return "No entries found in scratchpad#{prefix_msg}"
51
+ end
52
+
53
+ result = []
54
+ prefix_msg = prefix ? " with prefix '#{prefix}'" : ""
55
+ result << "Scratchpad entries#{prefix_msg} (#{entries.size} #{entries.size == 1 ? "entry" : "entries"}):"
56
+ result << ""
57
+
58
+ entries.each do |entry|
59
+ time_str = entry[:updated_at].strftime("%Y-%m-%d %H:%M:%S")
60
+ result << " scratchpad://#{entry[:path]}"
61
+ result << " Title: #{entry[:title]}"
62
+ result << " Size: #{format_bytes(entry[:size])}"
63
+ result << " Updated: #{time_str}"
64
+ result << ""
65
+ end
66
+
67
+ result.join("\n").rstrip
68
+ rescue ArgumentError => e
69
+ validation_error(e.message)
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :scratchpad_storage
75
+
76
+ def validation_error(message)
77
+ "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
78
+ end
79
+
80
+ # Format bytes to human-readable size
81
+ #
82
+ # @param bytes [Integer] Number of bytes
83
+ # @return [String] Formatted size
84
+ def format_bytes(bytes)
85
+ if bytes >= 1_000_000
86
+ "#{(bytes.to_f / 1_000_000).round(1)}MB"
87
+ elsif bytes >= 1_000
88
+ "#{(bytes.to_f / 1_000).round(1)}KB"
89
+ else
90
+ "#{bytes}B"
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Tools
5
+ module Scratchpad
6
+ # Tool for reading content from scratchpad storage
7
+ #
8
+ # Retrieves content stored by any agent using scratchpad_write.
9
+ # All agents in the swarm share the same scratchpad.
10
+ class ScratchpadRead < RubyLLM::Tool
11
+ define_method(:name) { "ScratchpadRead" }
12
+
13
+ description <<~DESC
14
+ Read content from scratchpad.
15
+ Use this to retrieve temporary notes, results, or messages stored by any agent.
16
+ Any agent can read any scratchpad content.
17
+ DESC
18
+
19
+ param :file_path,
20
+ desc: "Path to read from scratchpad (e.g., 'status', 'result', 'notes/agent_x')",
21
+ required: true
22
+
23
+ class << self
24
+ # Create a ScratchpadRead tool for a specific scratchpad storage instance
25
+ #
26
+ # @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
27
+ # @return [ScratchpadRead] Tool instance
28
+ def create_for_scratchpad(scratchpad_storage)
29
+ new(scratchpad_storage)
30
+ end
31
+ end
32
+
33
+ # Initialize with scratchpad storage instance
34
+ #
35
+ # @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
36
+ def initialize(scratchpad_storage)
37
+ super() # Call RubyLLM::Tool's initialize
38
+ @scratchpad_storage = scratchpad_storage
39
+ end
40
+
41
+ # Execute the tool
42
+ #
43
+ # @param file_path [String] Path to read from
44
+ # @return [String] Content at the path with line numbers, or error message
45
+ def execute(file_path:)
46
+ content = scratchpad_storage.read(file_path: file_path)
47
+ format_with_line_numbers(content)
48
+ rescue ArgumentError => e
49
+ validation_error(e.message)
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :scratchpad_storage
55
+
56
+ def validation_error(message)
57
+ "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
58
+ end
59
+
60
+ # Format content with line numbers (same format as Read tool)
61
+ #
62
+ # @param content [String] Content to format
63
+ # @return [String] Content with line numbers
64
+ def format_with_line_numbers(content)
65
+ lines = content.lines
66
+ output_lines = lines.each_with_index.map do |line, idx|
67
+ line_number = idx + 1
68
+ display_line = line.chomp
69
+ "#{line_number.to_s.rjust(6)}→#{display_line}"
70
+ end
71
+ output_lines.join("\n")
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,91 @@
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 < RubyLLM::Tool
12
+ define_method(:name) { "ScratchpadWrite" }
13
+
14
+ description <<~DESC
15
+ Store content in scratchpad for temporary cross-agent communication.
16
+ Use this for quick notes, intermediate results, or coordination messages.
17
+ Any agent can read this content. Data is lost when the swarm ends.
18
+
19
+ For persistent storage that survives across sessions, use MemoryWrite instead.
20
+
21
+ Choose a simple, descriptive path. Examples: 'status', 'result', 'notes/agent_x'
22
+ DESC
23
+
24
+ param :file_path,
25
+ desc: "Simple path for the content (e.g., 'status', 'result', 'notes/agent_x')",
26
+ required: true
27
+
28
+ param :content,
29
+ desc: "Content to store in scratchpad (max 1MB per entry)",
30
+ required: true
31
+
32
+ param :title,
33
+ desc: "Brief title describing the content",
34
+ required: true
35
+
36
+ class << self
37
+ # Create a ScratchpadWrite tool for a specific scratchpad storage instance
38
+ #
39
+ # @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
40
+ # @return [ScratchpadWrite] Tool instance
41
+ def create_for_scratchpad(scratchpad_storage)
42
+ new(scratchpad_storage)
43
+ end
44
+ end
45
+
46
+ # Initialize with scratchpad storage instance
47
+ #
48
+ # @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
49
+ def initialize(scratchpad_storage)
50
+ super() # Call RubyLLM::Tool's initialize
51
+ @scratchpad_storage = scratchpad_storage
52
+ end
53
+
54
+ # Execute the tool
55
+ #
56
+ # @param file_path [String] Path to store content
57
+ # @param content [String] Content to store
58
+ # @param title [String] Brief title
59
+ # @return [String] Success message with path and size
60
+ def execute(file_path:, content:, title:)
61
+ entry = scratchpad_storage.write(file_path: file_path, content: content, title: title)
62
+ "Stored at scratchpad://#{file_path} (#{format_bytes(entry.size)})"
63
+ rescue ArgumentError => e
64
+ validation_error(e.message)
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :scratchpad_storage
70
+
71
+ def validation_error(message)
72
+ "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
73
+ end
74
+
75
+ # Format bytes to human-readable size
76
+ #
77
+ # @param bytes [Integer] Number of bytes
78
+ # @return [String] Formatted size
79
+ def format_bytes(bytes)
80
+ if bytes >= 1_000_000
81
+ "#{(bytes.to_f / 1_000_000).round(1)}MB"
82
+ elsif bytes >= 1_000
83
+ "#{(bytes.to_f / 1_000).round(1)}KB"
84
+ else
85
+ "#{bytes}B"
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Tools
5
+ module Stores
6
+ # ReadTracker manages read-file tracking for all agents
7
+ #
8
+ # This module maintains a global registry of which files each agent has read
9
+ # during their conversation. This enables enforcement of the "read-before-write"
10
+ # and "read-before-edit" rules that ensure agents have context before modifying files.
11
+ #
12
+ # Each agent maintains an independent set of read files, keyed by agent identifier.
13
+ module ReadTracker
14
+ @read_files = {}
15
+ @mutex = Mutex.new
16
+
17
+ class << self
18
+ # Register that an agent has read a file
19
+ #
20
+ # @param agent_id [Symbol] The agent identifier
21
+ # @param file_path [String] The absolute path to the file
22
+ def register_read(agent_id, file_path)
23
+ @mutex.synchronize do
24
+ @read_files[agent_id] ||= Set.new
25
+ @read_files[agent_id] << File.expand_path(file_path)
26
+ end
27
+ end
28
+
29
+ # Check if an agent has read a file
30
+ #
31
+ # @param agent_id [Symbol] The agent identifier
32
+ # @param file_path [String] The absolute path to the file
33
+ # @return [Boolean] true if the agent has read this file
34
+ def file_read?(agent_id, file_path)
35
+ @mutex.synchronize do
36
+ return false unless @read_files[agent_id]
37
+
38
+ @read_files[agent_id].include?(File.expand_path(file_path))
39
+ end
40
+ end
41
+
42
+ # Clear read history for an agent (useful for testing)
43
+ #
44
+ # @param agent_id [Symbol] The agent identifier
45
+ def clear(agent_id)
46
+ @mutex.synchronize do
47
+ @read_files.delete(agent_id)
48
+ end
49
+ end
50
+
51
+ # Clear all read history (useful for testing)
52
+ def clear_all
53
+ @mutex.synchronize do
54
+ @read_files.clear
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,224 @@
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
+ def initialize
19
+ super() # Initialize parent Storage class
20
+ @entries = {}
21
+ @total_size = 0
22
+ @mutex = Mutex.new
23
+ end
24
+
25
+ # Write content to scratchpad
26
+ #
27
+ # @param file_path [String] Path to store content
28
+ # @param content [String] Content to store
29
+ # @param title [String] Brief title describing the content
30
+ # @raise [ArgumentError] If size limits are exceeded
31
+ # @return [Entry] The created entry
32
+ def write(file_path:, content:, title:)
33
+ @mutex.synchronize do
34
+ raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
35
+ raise ArgumentError, "content is required" if content.nil?
36
+ raise ArgumentError, "title is required" if title.nil? || title.to_s.strip.empty?
37
+
38
+ content_size = content.bytesize
39
+
40
+ # Check entry size limit
41
+ if content_size > MAX_ENTRY_SIZE
42
+ raise ArgumentError, "Content exceeds maximum size (#{format_bytes(MAX_ENTRY_SIZE)}). " \
43
+ "Current: #{format_bytes(content_size)}"
44
+ end
45
+
46
+ # Calculate new total size
47
+ existing_entry = @entries[file_path]
48
+ existing_size = existing_entry ? existing_entry.size : 0
49
+ new_total_size = @total_size - existing_size + content_size
50
+
51
+ # Check total size limit
52
+ if new_total_size > MAX_TOTAL_SIZE
53
+ raise ArgumentError, "Scratchpad full (#{format_bytes(MAX_TOTAL_SIZE)} limit). " \
54
+ "Current: #{format_bytes(@total_size)}, " \
55
+ "Would be: #{format_bytes(new_total_size)}. " \
56
+ "Clear old entries or use smaller content."
57
+ end
58
+
59
+ # Create entry
60
+ entry = Entry.new(
61
+ content: content,
62
+ title: title,
63
+ updated_at: Time.now,
64
+ size: content_size,
65
+ )
66
+
67
+ # Update storage
68
+ @entries[file_path] = entry
69
+ @total_size = new_total_size
70
+
71
+ entry
72
+ end
73
+ end
74
+
75
+ # Read content from scratchpad
76
+ #
77
+ # @param file_path [String] Path to read from
78
+ # @raise [ArgumentError] If path not found
79
+ # @return [String] Content at the path
80
+ def read(file_path:)
81
+ raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
82
+
83
+ entry = @entries[file_path]
84
+ raise ArgumentError, "scratchpad://#{file_path} not found" unless entry
85
+
86
+ entry.content
87
+ end
88
+
89
+ # Delete a specific entry
90
+ #
91
+ # @param file_path [String] Path to delete
92
+ # @raise [ArgumentError] If path not found
93
+ # @return [void]
94
+ def delete(file_path:)
95
+ @mutex.synchronize do
96
+ raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
97
+
98
+ entry = @entries[file_path]
99
+ raise ArgumentError, "scratchpad://#{file_path} not found" unless entry
100
+
101
+ # Update total size
102
+ @total_size -= entry.size
103
+
104
+ # Remove entry
105
+ @entries.delete(file_path)
106
+ end
107
+ end
108
+
109
+ # List scratchpad entries, optionally filtered by prefix
110
+ #
111
+ # @param prefix [String, nil] Filter by path prefix
112
+ # @return [Array<Hash>] Array of entry metadata (path, title, size, updated_at)
113
+ def list(prefix: nil)
114
+ entries = @entries
115
+
116
+ # Filter by prefix if provided
117
+ if prefix && !prefix.empty?
118
+ entries = entries.select { |path, _| path.start_with?(prefix) }
119
+ end
120
+
121
+ # Return metadata sorted by path
122
+ entries.map do |path, entry|
123
+ {
124
+ path: path,
125
+ title: entry.title,
126
+ size: entry.size,
127
+ updated_at: entry.updated_at,
128
+ }
129
+ end.sort_by { |e| e[:path] }
130
+ end
131
+
132
+ # Search entries by glob pattern
133
+ #
134
+ # @param pattern [String] Glob pattern (e.g., "**/*.txt", "parallel/*/task_*")
135
+ # @return [Array<Hash>] Array of matching entry metadata, sorted by most recent first
136
+ def glob(pattern:)
137
+ raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?
138
+
139
+ # Convert glob pattern to regex
140
+ regex = glob_to_regex(pattern)
141
+
142
+ # Filter entries by pattern
143
+ matching_entries = @entries.select { |path, _| regex.match?(path) }
144
+
145
+ # Return metadata sorted by most recent first
146
+ matching_entries.map do |path, entry|
147
+ {
148
+ path: path,
149
+ title: entry.title,
150
+ size: entry.size,
151
+ updated_at: entry.updated_at,
152
+ }
153
+ end.sort_by { |e| -e[:updated_at].to_f }
154
+ end
155
+
156
+ # Search entry content by pattern
157
+ #
158
+ # @param pattern [String] Regular expression pattern to search for
159
+ # @param case_insensitive [Boolean] Whether to perform case-insensitive search
160
+ # @param output_mode [String] Output mode: "files_with_matches" (default), "content", or "count"
161
+ # @return [Array<Hash>, String] Results based on output_mode
162
+ def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
163
+ raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?
164
+
165
+ # Create regex from pattern
166
+ flags = case_insensitive ? Regexp::IGNORECASE : 0
167
+ regex = Regexp.new(pattern, flags)
168
+
169
+ case output_mode
170
+ when "files_with_matches"
171
+ # Return just the paths that match
172
+ matching_paths = @entries.select { |_path, entry| regex.match?(entry.content) }
173
+ .map { |path, _| path }
174
+ .sort
175
+ matching_paths
176
+ when "content"
177
+ # Return paths with matching lines, sorted by most recent first
178
+ results = []
179
+ @entries.each do |path, entry|
180
+ matching_lines = []
181
+ entry.content.each_line.with_index(1) do |line, line_num|
182
+ matching_lines << { line_number: line_num, content: line.chomp } if regex.match?(line)
183
+ end
184
+ results << { path: path, matches: matching_lines, updated_at: entry.updated_at } unless matching_lines.empty?
185
+ end
186
+ results.sort_by { |r| -r[:updated_at].to_f }.map { |r| r.except(:updated_at) }
187
+ when "count"
188
+ # Return paths with match counts, sorted by most recent first
189
+ results = []
190
+ @entries.each do |path, entry|
191
+ count = entry.content.scan(regex).size
192
+ results << { path: path, count: count, updated_at: entry.updated_at } if count > 0
193
+ end
194
+ results.sort_by { |r| -r[:updated_at].to_f }.map { |r| r.except(:updated_at) }
195
+ else
196
+ raise ArgumentError, "Invalid output_mode: #{output_mode}. Must be 'files_with_matches', 'content', or 'count'"
197
+ end
198
+ end
199
+
200
+ # Clear all entries
201
+ #
202
+ # @return [void]
203
+ def clear
204
+ @mutex.synchronize do
205
+ @entries.clear
206
+ @total_size = 0
207
+ end
208
+ end
209
+
210
+ # Get current total size
211
+ #
212
+ # @return [Integer] Total size in bytes
213
+ attr_reader :total_size
214
+
215
+ # Get number of entries
216
+ #
217
+ # @return [Integer] Number of entries
218
+ def size
219
+ @entries.size
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,148 @@
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
+ # Maximum size per entry (1MB)
18
+ MAX_ENTRY_SIZE = 1_000_000
19
+
20
+ # Maximum total storage size (100MB)
21
+ MAX_TOTAL_SIZE = 100_000_000
22
+
23
+ # Represents a single storage entry with metadata
24
+ Entry = Struct.new(:content, :title, :updated_at, :size, keyword_init: true)
25
+
26
+ # Initialize storage
27
+ #
28
+ # Subclasses should call super() in their initialize method.
29
+ # This base implementation does nothing - it exists only to satisfy RuboCop.
30
+ def initialize
31
+ # Base class initialization - subclasses implement their own logic
32
+ end
33
+
34
+ # Write content to storage
35
+ #
36
+ # @param file_path [String] Path to store content
37
+ # @param content [String] Content to store
38
+ # @param title [String] Brief title describing the content
39
+ # @raise [ArgumentError] If size limits are exceeded
40
+ # @return [Entry] The created entry
41
+ def write(file_path:, content:, title:)
42
+ raise NotImplementedError, "Subclass must implement #write"
43
+ end
44
+
45
+ # Read content from storage
46
+ #
47
+ # @param file_path [String] Path to read from
48
+ # @raise [ArgumentError] If path not found
49
+ # @return [String] Content at the path
50
+ def read(file_path:)
51
+ raise NotImplementedError, "Subclass must implement #read"
52
+ end
53
+
54
+ # Delete a specific entry
55
+ #
56
+ # @param file_path [String] Path to delete
57
+ # @raise [ArgumentError] If path not found
58
+ # @return [void]
59
+ def delete(file_path:)
60
+ raise NotImplementedError, "Subclass must implement #delete"
61
+ end
62
+
63
+ # List entries, optionally filtered by prefix
64
+ #
65
+ # @param prefix [String, nil] Filter by path prefix
66
+ # @return [Array<Hash>] Array of entry metadata (path, title, size, updated_at)
67
+ def list(prefix: nil)
68
+ raise NotImplementedError, "Subclass must implement #list"
69
+ end
70
+
71
+ # Search entries by glob pattern
72
+ #
73
+ # @param pattern [String] Glob pattern (e.g., "**/*.txt", "parallel/*/task_*")
74
+ # @return [Array<Hash>] Array of matching entry metadata, sorted by most recent first
75
+ def glob(pattern:)
76
+ raise NotImplementedError, "Subclass must implement #glob"
77
+ end
78
+
79
+ # Search entry content by pattern
80
+ #
81
+ # @param pattern [String] Regular expression pattern to search for
82
+ # @param case_insensitive [Boolean] Whether to perform case-insensitive search
83
+ # @param output_mode [String] Output mode: "files_with_matches" (default), "content", or "count"
84
+ # @return [Array<Hash>, String] Results based on output_mode
85
+ def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
86
+ raise NotImplementedError, "Subclass must implement #grep"
87
+ end
88
+
89
+ # Clear all entries
90
+ #
91
+ # @return [void]
92
+ def clear
93
+ raise NotImplementedError, "Subclass must implement #clear"
94
+ end
95
+
96
+ # Get current total size
97
+ #
98
+ # @return [Integer] Total size in bytes
99
+ def total_size
100
+ raise NotImplementedError, "Subclass must implement #total_size"
101
+ end
102
+
103
+ # Get number of entries
104
+ #
105
+ # @return [Integer] Number of entries
106
+ def size
107
+ raise NotImplementedError, "Subclass must implement #size"
108
+ end
109
+
110
+ protected
111
+
112
+ # Format bytes to human-readable size
113
+ #
114
+ # @param bytes [Integer] Number of bytes
115
+ # @return [String] Formatted size (e.g., "1.5MB", "500.0KB")
116
+ def format_bytes(bytes)
117
+ if bytes >= 1_000_000
118
+ "#{(bytes.to_f / 1_000_000).round(1)}MB"
119
+ elsif bytes >= 1_000
120
+ "#{(bytes.to_f / 1_000).round(1)}KB"
121
+ else
122
+ "#{bytes}B"
123
+ end
124
+ end
125
+
126
+ # Convert glob pattern to regex
127
+ #
128
+ # @param pattern [String] Glob pattern
129
+ # @return [Regexp] Regular expression
130
+ def glob_to_regex(pattern)
131
+ # Escape special regex characters except glob wildcards
132
+ escaped = Regexp.escape(pattern)
133
+
134
+ # Convert glob wildcards to regex
135
+ # ** matches any number of directories (including zero)
136
+ escaped = escaped.gsub('\*\*', ".*")
137
+ # * matches anything except directory separator
138
+ escaped = escaped.gsub('\*', "[^/]*")
139
+ # ? matches single character except directory separator
140
+ escaped = escaped.gsub('\?', "[^/]")
141
+
142
+ # Anchor to start and end
143
+ Regexp.new("\\A#{escaped}\\z")
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end