claude_swarm 1.0.1 → 1.0.2

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 +6 -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 +247 -4
  267. data/EXAMPLES.md +0 -164
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmMemory
4
+ module Core
5
+ # Semantic search abstraction layer
6
+ #
7
+ # Provides embedding computation and semantic search operations
8
+ # that work with any storage adapter. Easily replaceable with
9
+ # vector database implementations (Qdrant, Milvus, Chroma, etc.)
10
+ #
11
+ # Uses hybrid search: combines semantic similarity with keyword matching
12
+ # for better recall accuracy.
13
+ #
14
+ # @example
15
+ # index = SemanticIndex.new(adapter: adapter, embedder: embedder)
16
+ # results = index.search(query: "how to debug", top_k: 5, threshold: 0.7)
17
+ class SemanticIndex
18
+ # Default weights for hybrid scoring (optimal: 50/50 discovered via systematic evaluation)
19
+ # Configurable via ENV vars: SWARM_MEMORY_SEMANTIC_WEIGHT, SWARM_MEMORY_KEYWORD_WEIGHT
20
+ DEFAULT_SEMANTIC_WEIGHT = (ENV["SWARM_MEMORY_SEMANTIC_WEIGHT"] || "0.5").to_f
21
+ DEFAULT_KEYWORD_WEIGHT = (ENV["SWARM_MEMORY_KEYWORD_WEIGHT"] || "0.5").to_f
22
+
23
+ # @param adapter [Adapters::Base] Storage adapter
24
+ # @param embedder [Embeddings::Embedder] Embedding model
25
+ # @param semantic_weight [Float] Weight for semantic similarity (0.0-1.0)
26
+ # @param keyword_weight [Float] Weight for keyword matching (0.0-1.0)
27
+ def initialize(adapter:, embedder:, semantic_weight: DEFAULT_SEMANTIC_WEIGHT, keyword_weight: DEFAULT_KEYWORD_WEIGHT)
28
+ @adapter = adapter
29
+ @embedder = embedder
30
+ @semantic_weight = semantic_weight
31
+ @keyword_weight = keyword_weight
32
+ end
33
+
34
+ # Compute embedding for text
35
+ #
36
+ # @param text [String] Text to embed
37
+ # @return [Array<Float>] Embedding vector
38
+ def compute_embedding(text)
39
+ @embedder.embed(text)
40
+ end
41
+
42
+ # Semantic search by text query
43
+ #
44
+ # @param query [String] Search query
45
+ # @param top_k [Integer] Number of results to return
46
+ # @param threshold [Float] Minimum similarity score (0.0-1.0)
47
+ # @param filter [Hash, nil] Optional metadata filters (e.g., { "type" => "skill" })
48
+ # @return [Array<Hash>] Results with similarity scores, sorted by similarity descending
49
+ #
50
+ # @example
51
+ # results = index.search(
52
+ # query: "how to create a swarm",
53
+ # top_k: 3,
54
+ # threshold: 0.65,
55
+ # filter: { "type" => "skill" }
56
+ # )
57
+ #
58
+ # results.each do |result|
59
+ # puts "#{result[:path]} (#{result[:similarity]})"
60
+ # puts result[:title]
61
+ # end
62
+ def search(query:, top_k: 10, threshold: 0.0, filter: nil)
63
+ # Extract keywords from query for keyword matching
64
+ query_keywords = extract_keywords(query)
65
+
66
+ # Compute query embedding
67
+ query_embedding = compute_embedding(query)
68
+
69
+ # Delegate to adapter-specific search (gets semantic similarity only)
70
+ # Use threshold of 0.0 to get all results, we'll filter after hybrid scoring
71
+ results = @adapter.semantic_search(
72
+ embedding: query_embedding,
73
+ top_k: top_k * 3, # Get extra for reranking
74
+ threshold: 0.0, # No threshold yet - will apply after hybrid scoring
75
+ )
76
+
77
+ # Calculate hybrid scores (semantic + keyword)
78
+ results = calculate_hybrid_scores(results, query_keywords)
79
+
80
+ # Apply metadata filters if provided
81
+ results = apply_filters(results, filter) if filter
82
+
83
+ # Filter by threshold on hybrid score
84
+ results = results.select { |r| r[:similarity] >= threshold }
85
+
86
+ # Return top K after filtering and reranking
87
+ results.take(top_k)
88
+ end
89
+
90
+ # Find similar entries by embedding vector
91
+ #
92
+ # @param embedding [Array<Float>] Embedding vector
93
+ # @param top_k [Integer] Number of results to return
94
+ # @param threshold [Float] Minimum similarity score (0.0-1.0)
95
+ # @param filter [Hash, nil] Optional metadata filters
96
+ # @return [Array<Hash>] Similar entries sorted by similarity descending
97
+ def find_similar(embedding:, top_k: 10, threshold: 0.0, filter: nil)
98
+ results = @adapter.semantic_search(
99
+ embedding: embedding,
100
+ top_k: top_k * 2,
101
+ threshold: threshold,
102
+ )
103
+
104
+ # Apply metadata filters if provided
105
+ results = apply_filters(results, filter) if filter
106
+
107
+ # Return top K after filtering
108
+ results.take(top_k)
109
+ end
110
+
111
+ private
112
+
113
+ # Extract keywords from query text
114
+ #
115
+ # Removes common words and punctuation, lowercases everything.
116
+ #
117
+ # @param text [String] Query text
118
+ # @return [Array<String>] Extracted keywords
119
+ def extract_keywords(text)
120
+ # Common stop words to ignore
121
+ stop_words = [
122
+ "a",
123
+ "an",
124
+ "and",
125
+ "are",
126
+ "as",
127
+ "at",
128
+ "be",
129
+ "by",
130
+ "for",
131
+ "from",
132
+ "has",
133
+ "have",
134
+ "in",
135
+ "is",
136
+ "it",
137
+ "of",
138
+ "on",
139
+ "that",
140
+ "the",
141
+ "this",
142
+ "to",
143
+ "was",
144
+ "will",
145
+ "with",
146
+ "how",
147
+ "what",
148
+ "when",
149
+ "where",
150
+ "why",
151
+ "who",
152
+ "which",
153
+ "do",
154
+ "does",
155
+ "did",
156
+ "can",
157
+ "could",
158
+ "should",
159
+ "would",
160
+ "may",
161
+ "might",
162
+ "must",
163
+ "me",
164
+ "my",
165
+ "you",
166
+ "your",
167
+ "we",
168
+ "us",
169
+ "our",
170
+ ]
171
+
172
+ # Extract words (lowercase, alphanumeric only)
173
+ words = text.downcase
174
+ .gsub(/[^a-z0-9\s\-]/, " ") # Remove punctuation except hyphens
175
+ .split(/\s+/)
176
+ .reject { |w| w.length < 2 } # Skip single chars
177
+ .reject { |w| stop_words.include?(w) } # Skip stop words
178
+
179
+ words.uniq
180
+ end
181
+
182
+ # Calculate hybrid scores combining semantic similarity and keyword matching
183
+ #
184
+ # @param results [Array<Hash>] Results with semantic :similarity scores
185
+ # @param query_keywords [Array<String>] Keywords from query
186
+ # @return [Array<Hash>] Results with updated :similarity (hybrid score) and debug info
187
+ def calculate_hybrid_scores(results, query_keywords)
188
+ results.map do |result|
189
+ semantic_score = result[:similarity]
190
+ keyword_score = calculate_keyword_score(result, query_keywords)
191
+
192
+ # Hybrid score: weighted combination
193
+ hybrid_score = (@semantic_weight * semantic_score) + (@keyword_weight * keyword_score)
194
+
195
+ # Update result with hybrid score and debug info
196
+ result.merge(
197
+ similarity: hybrid_score,
198
+ semantic_score: semantic_score,
199
+ keyword_score: keyword_score,
200
+ )
201
+ end.sort_by { |r| -r[:similarity] }
202
+ end
203
+
204
+ # Calculate keyword matching score based on tag overlap
205
+ #
206
+ # @param result [Hash] Search result with :metadata containing tags
207
+ # @param query_keywords [Array<String>] Keywords from query
208
+ # @return [Float] Keyword score (0.0-1.0)
209
+ def calculate_keyword_score(result, query_keywords)
210
+ return 0.0 if query_keywords.empty?
211
+
212
+ # Get tags from metadata
213
+ tags = result.dig(:metadata, "tags") || result.dig(:metadata, :tags) || []
214
+ return 0.0 if tags.empty?
215
+
216
+ # Normalize tags to lowercase
217
+ normalized_tags = tags.map(&:downcase)
218
+
219
+ # Count keyword matches (fuzzy matching - substring or contains)
220
+ matches = query_keywords.count do |keyword|
221
+ normalized_tags.any? { |tag| tag.include?(keyword) || keyword.include?(tag) }
222
+ end
223
+
224
+ # Normalize to 0-1 scale
225
+ # Use min(query_keywords.size, 5) as denominator to avoid penalizing long queries
226
+ denominator = [query_keywords.size, 5].min
227
+ matches.to_f / denominator
228
+ end
229
+
230
+ # Apply metadata filters to results
231
+ #
232
+ # @param results [Array<Hash>] Search results
233
+ # @param filter [Hash] Metadata filters
234
+ # @return [Array<Hash>] Filtered results
235
+ def apply_filters(results, filter)
236
+ results.select do |result|
237
+ filter.all? do |key, value|
238
+ result.dig(:metadata, key) == value || result.dig(:metadata, key.to_s) == value
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmMemory
4
+ module Core
5
+ # High-level storage orchestration
6
+ #
7
+ # Coordinates adapter operations, path normalization, embedding generation,
8
+ # and metadata extraction.
9
+ #
10
+ # @example
11
+ # adapter = Adapters::FilesystemAdapter.new(persist_to: ".swarm/memory.json")
12
+ # storage = Storage.new(adapter: adapter)
13
+ # storage.write(file_path: "concepts/ruby", content: "...", title: "Ruby Classes")
14
+ class Storage
15
+ attr_reader :adapter
16
+
17
+ # Initialize storage with an adapter
18
+ #
19
+ # @param adapter [Adapters::Base] Storage adapter
20
+ # @param embedder [Embeddings::Embedder, nil] Optional embedder for semantic search
21
+ def initialize(adapter:, embedder: nil)
22
+ raise ArgumentError, "adapter is required" unless adapter.is_a?(Adapters::Base)
23
+
24
+ @adapter = adapter
25
+ @embedder = embedder
26
+
27
+ # Create semantic index if embedder is provided
28
+ @semantic_index = if embedder
29
+ SemanticIndex.new(adapter: adapter, embedder: embedder)
30
+ end
31
+ end
32
+
33
+ # Get semantic index for semantic search operations
34
+ #
35
+ # @return [SemanticIndex, nil] Semantic index instance or nil if no embedder
36
+ attr_reader :semantic_index
37
+
38
+ # Write content to storage
39
+ #
40
+ # @param file_path [String] Path to store content (with .md extension)
41
+ # @param content [String] Content to store (pure markdown)
42
+ # @param title [String] Brief title
43
+ # @param metadata [Hash, nil] Optional metadata
44
+ # @param generate_embedding [Boolean] Whether to generate embedding (default: true if embedder present)
45
+ # @return [Entry] The created entry
46
+ def write(file_path:, content:, title:, metadata: nil, generate_embedding: nil)
47
+ # Normalize path
48
+ normalized_path = PathNormalizer.normalize(file_path)
49
+
50
+ # Generate embedding if requested and embedder available
51
+ embedding = nil
52
+ should_embed = generate_embedding.nil? ? !@embedder.nil? : generate_embedding
53
+
54
+ if should_embed && @embedder
55
+ begin
56
+ # Build searchable text for better semantic matching
57
+ # Uses title + tags + first paragraph instead of full content
58
+ searchable_text = build_searchable_text(content, title, metadata)
59
+
60
+ # ALWAYS emit to LogStream (create if needed for debugging)
61
+ # This ensures we can see what's being embedded
62
+ begin
63
+ if defined?(SwarmSDK::LogStream)
64
+ SwarmSDK::LogStream.emit(
65
+ type: "memory_embedding_generated",
66
+ file_path: normalized_path,
67
+ title: title,
68
+ searchable_text_length: searchable_text.length,
69
+ searchable_text_preview: searchable_text.slice(0, 300),
70
+ full_searchable_text: searchable_text,
71
+ metadata_tags: metadata&.dig("tags"),
72
+ metadata_domain: metadata&.dig("domain"),
73
+ )
74
+ end
75
+ rescue StandardError => e
76
+ # Don't fail if logging fails
77
+ warn("Failed to log embedding: #{e.message}")
78
+ end
79
+
80
+ embedding = @embedder.embed(searchable_text)
81
+ rescue StandardError => e
82
+ # Don't fail write if embedding generation fails
83
+ warn("Warning: Failed to generate embedding for #{normalized_path}: #{e.message}")
84
+ embedding = nil
85
+ end
86
+ end
87
+
88
+ # Write to adapter (metadata passed from tool parameters)
89
+ @adapter.write(
90
+ file_path: normalized_path,
91
+ content: content,
92
+ title: title,
93
+ embedding: embedding,
94
+ metadata: metadata,
95
+ )
96
+ end
97
+
98
+ # Read content from storage
99
+ #
100
+ # @param file_path [String] Path to read from
101
+ # @return [String] Content at the path
102
+ def read(file_path:)
103
+ normalized_path = PathNormalizer.normalize(file_path)
104
+ @adapter.read(file_path: normalized_path)
105
+ end
106
+
107
+ # Read full entry with metadata
108
+ #
109
+ # @param file_path [String] Path to read from
110
+ # @return [Entry] Full entry object
111
+ def read_entry(file_path:)
112
+ normalized_path = PathNormalizer.normalize(file_path)
113
+ @adapter.read_entry(file_path: normalized_path)
114
+ end
115
+
116
+ # Delete an entry
117
+ #
118
+ # @param file_path [String] Path to delete
119
+ # @return [void]
120
+ def delete(file_path:)
121
+ normalized_path = PathNormalizer.normalize(file_path)
122
+ @adapter.delete(file_path: normalized_path)
123
+ end
124
+
125
+ # List all entries
126
+ #
127
+ # @param prefix [String, nil] Optional prefix filter
128
+ # @return [Array<Hash>] Entry metadata
129
+ def list(prefix: nil)
130
+ @adapter.list(prefix: prefix)
131
+ end
132
+
133
+ # Search by glob pattern
134
+ #
135
+ # @param pattern [String] Glob pattern
136
+ # @return [Array<Hash>] Matching entries
137
+ def glob(pattern:)
138
+ @adapter.glob(pattern: pattern)
139
+ end
140
+
141
+ # Search by content pattern
142
+ #
143
+ # @param pattern [String] Regex pattern
144
+ # @param case_insensitive [Boolean] Case-insensitive search
145
+ # @param output_mode [String] Output mode
146
+ # @param path [String, nil] Optional path prefix filter
147
+ # @return [Array<Hash>] Search results
148
+ def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches", path: nil)
149
+ @adapter.grep(
150
+ pattern: pattern,
151
+ case_insensitive: case_insensitive,
152
+ output_mode: output_mode,
153
+ path: path,
154
+ )
155
+ end
156
+
157
+ # Clear all entries
158
+ #
159
+ # @return [void]
160
+ def clear
161
+ @adapter.clear
162
+ end
163
+
164
+ # Get total storage size
165
+ #
166
+ # @return [Integer] Size in bytes
167
+ def total_size
168
+ @adapter.total_size
169
+ end
170
+
171
+ # Get number of entries
172
+ #
173
+ # @return [Integer] Entry count
174
+ def size
175
+ @adapter.size
176
+ end
177
+
178
+ # Get all entries (for optimization/analysis)
179
+ #
180
+ # @return [Hash<String, Entry>] All entries
181
+ def all_entries
182
+ @adapter.all_entries
183
+ end
184
+
185
+ private
186
+
187
+ # Build searchable text for embedding
188
+ #
189
+ # Creates a condensed representation optimized for semantic search.
190
+ # Uses title + tags + first paragraph instead of full content.
191
+ #
192
+ # @param content [String] Full entry content
193
+ # @param title [String] Entry title
194
+ # @param metadata [Hash, nil] Entry metadata
195
+ # @return [String] Searchable text for embedding
196
+ def build_searchable_text(content, title, metadata)
197
+ parts = []
198
+
199
+ # 1. Title (most important for matching)
200
+ parts << "Title: #{title}"
201
+
202
+ # 2. Tags (critical keywords that users would search for)
203
+ if metadata && metadata["tags"]&.any?
204
+ parts << "Tags: #{metadata["tags"].join(", ")}"
205
+ end
206
+
207
+ # 3. Domain (additional context)
208
+ if metadata && metadata["domain"]
209
+ parts << "Domain: #{metadata["domain"]}"
210
+ end
211
+
212
+ # 4. First paragraph (summary/description)
213
+ first_para = extract_first_paragraph(content)
214
+ parts << "Summary: #{first_para}" if first_para
215
+
216
+ parts.join("\n\n")
217
+ end
218
+
219
+ # Extract first meaningful paragraph from content
220
+ #
221
+ # Skips YAML frontmatter, headings, and empty lines to find
222
+ # the first substantive paragraph.
223
+ #
224
+ # Character limit can be controlled via SWARM_MEMORY_EMBEDDING_MAX_CHARS:
225
+ # - Default: 300
226
+ # - -1: Unlimited (use full content)
227
+ # - Any positive number: Custom limit
228
+ #
229
+ # @param content [String] Full content
230
+ # @return [String, nil] First paragraph (max chars based on env var) or nil
231
+ def extract_first_paragraph(content)
232
+ return if content.nil? || content.strip.empty?
233
+
234
+ # Get character limit from environment variable
235
+ # Default: 300, -1 = unlimited
236
+ max_chars = (ENV["SWARM_MEMORY_EMBEDDING_MAX_CHARS"] || "1200").to_i
237
+ unlimited = max_chars == -1
238
+
239
+ lines = content.lines
240
+
241
+ # Skip YAML frontmatter (--- ... ---)
242
+ in_frontmatter = false
243
+ lines = lines.drop_while do |line|
244
+ if line.strip == "---"
245
+ in_frontmatter = !in_frontmatter
246
+ true
247
+ else
248
+ in_frontmatter
249
+ end
250
+ end
251
+
252
+ # Find first non-heading, non-empty paragraph(s)
253
+ paragraph = []
254
+ lines.each do |line|
255
+ stripped = line.strip
256
+
257
+ # Skip empty lines
258
+ next if stripped.empty?
259
+
260
+ # Stop if we hit a heading after collecting some text (unless unlimited)
261
+ if stripped.start_with?("#") && paragraph.any? && !unlimited
262
+ break
263
+ end
264
+
265
+ # Skip headings
266
+ next if stripped.start_with?("#")
267
+
268
+ # Skip code blocks
269
+ next if stripped.start_with?("```")
270
+
271
+ # Add line to paragraph
272
+ paragraph << stripped
273
+
274
+ # Stop if we have enough text (unless unlimited)
275
+ unless unlimited
276
+ break if paragraph.join(" ").length > (max_chars - 100)
277
+ end
278
+ end
279
+
280
+ return if paragraph.empty?
281
+
282
+ # Join and cap at max_chars (or don't cap if unlimited)
283
+ text = paragraph.join(" ")
284
+ unlimited ? text : text.slice(0, max_chars)
285
+ end
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmMemory
4
+ module Core
5
+ # StorageReadTracker manages read-entry tracking for all agents
6
+ #
7
+ # This module maintains a global registry of which memory entries each agent
8
+ # has read during their conversation. This enables enforcement of the
9
+ # "read-before-edit" rule that ensures agents have context before modifying entries.
10
+ #
11
+ # Each agent maintains an independent set of read entries, keyed by agent identifier.
12
+ module StorageReadTracker
13
+ @read_entries = {}
14
+ @mutex = Mutex.new
15
+
16
+ class << self
17
+ # Register that an agent has read a storage entry
18
+ #
19
+ # @param agent_id [Symbol] The agent identifier
20
+ # @param entry_path [String] The storage entry path
21
+ # @return [void]
22
+ def register_read(agent_id, entry_path)
23
+ @mutex.synchronize do
24
+ @read_entries[agent_id] ||= Set.new
25
+ @read_entries[agent_id] << entry_path
26
+ end
27
+ end
28
+
29
+ # Check if an agent has read a storage entry
30
+ #
31
+ # @param agent_id [Symbol] The agent identifier
32
+ # @param entry_path [String] The storage entry path
33
+ # @return [Boolean] true if the agent has read this entry
34
+ def entry_read?(agent_id, entry_path)
35
+ @mutex.synchronize do
36
+ return false unless @read_entries[agent_id]
37
+
38
+ @read_entries[agent_id].include?(entry_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
+ # @return [void]
46
+ def clear(agent_id)
47
+ @mutex.synchronize do
48
+ @read_entries.delete(agent_id)
49
+ end
50
+ end
51
+
52
+ # Clear all read history (useful for testing)
53
+ #
54
+ # @return [void]
55
+ def clear_all
56
+ @mutex.synchronize do
57
+ @read_entries.clear
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmMemory
4
+ module DSL
5
+ # Extension module that injects memory DSL into SwarmSDK::Agent::Builder
6
+ #
7
+ # This module is included into Agent::Builder when swarm_memory is required,
8
+ # adding the `memory` configuration method.
9
+ module BuilderExtension
10
+ # Configure persistent memory for this agent
11
+ #
12
+ # @example Interactive mode (default) - Learn and retrieve
13
+ # memory do
14
+ # directory ".swarm/agent-memory"
15
+ # end
16
+ #
17
+ # @example Retrieval mode - Read-only Q&A
18
+ # memory do
19
+ # directory "team-knowledge/"
20
+ # mode :retrieval
21
+ # end
22
+ #
23
+ # @example Researcher mode - Knowledge extraction
24
+ # memory do
25
+ # directory "team-knowledge/"
26
+ # mode :researcher
27
+ # end
28
+ def memory(&block)
29
+ @memory_config = SwarmMemory::DSL::MemoryConfig.new
30
+ @memory_config.instance_eval(&block) if block_given?
31
+ @memory_config
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ # Inject memory DSL into Agent::Builder when this file is loaded
38
+ if defined?(SwarmSDK::Agent::Builder)
39
+ SwarmSDK::Agent::Builder.include(SwarmMemory::DSL::BuilderExtension)
40
+ end