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,531 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmMemory
4
+ module Integration
5
+ # SwarmSDK plugin implementation for SwarmMemory
6
+ #
7
+ # This plugin integrates SwarmMemory with SwarmSDK, providing:
8
+ # - Persistent memory storage for agents
9
+ # - Memory tools (MemoryWrite, MemoryRead, MemoryEdit, etc.)
10
+ # - LoadSkill tool for dynamic tool swapping
11
+ # - System prompt contributions for memory guidance
12
+ # - Semantic skill discovery on user messages
13
+ #
14
+ # The plugin automatically registers itself when SwarmMemory is loaded
15
+ # alongside SwarmSDK.
16
+ class SDKPlugin < SwarmSDK::Plugin
17
+ def initialize
18
+ super
19
+ # Track storages for each agent: { agent_name => storage }
20
+ # Needed for semantic skill discovery in on_user_message
21
+ @storages = {}
22
+ # Track memory mode for each agent: { agent_name => mode }
23
+ # Modes: :assistant (default), :retrieval, :researcher
24
+ @modes = {}
25
+ end
26
+
27
+ # Plugin identifier
28
+ #
29
+ # @return [Symbol] Plugin name
30
+ def name
31
+ :memory
32
+ end
33
+
34
+ # Tools provided by this plugin
35
+ #
36
+ # Returns all memory tools for PluginRegistry mapping.
37
+ # Tools are auto-registered by ToolConfigurator, then filtered
38
+ # by mode in on_agent_initialized using remove_tool.
39
+ #
40
+ # Note: LoadSkill is NOT included here because it requires special handling.
41
+ # It's registered separately in on_agent_initialized lifecycle hook because
42
+ # it needs chat, tool_configurator, and agent_definition parameters.
43
+ #
44
+ # @return [Array<Symbol>] All memory tool names
45
+ def tools
46
+ [
47
+ :MemoryRead,
48
+ :MemoryGlob,
49
+ :MemoryGrep,
50
+ :MemoryWrite,
51
+ :MemoryEdit,
52
+ :MemoryMultiEdit,
53
+ :MemoryDelete,
54
+ :MemoryDefrag,
55
+ ]
56
+ end
57
+
58
+ # Get tools for a specific mode
59
+ #
60
+ # @param mode [Symbol] Memory mode
61
+ # @return [Array<Symbol>] Tool names for this mode
62
+ def tools_for_mode(mode)
63
+ case mode
64
+ when :retrieval
65
+ # Read-only tools for Q&A agents
66
+ [:MemoryRead, :MemoryGlob, :MemoryGrep]
67
+ when :assistant
68
+ # Read + Write + Edit for learning assistants (need edit for corrections)
69
+ [:MemoryRead, :MemoryGlob, :MemoryGrep, :MemoryWrite, :MemoryEdit]
70
+ when :researcher
71
+ # All tools for knowledge extraction
72
+ [
73
+ :MemoryRead,
74
+ :MemoryGlob,
75
+ :MemoryGrep,
76
+ :MemoryWrite,
77
+ :MemoryEdit,
78
+ :MemoryMultiEdit,
79
+ :MemoryDelete,
80
+ :MemoryDefrag,
81
+ ]
82
+ else
83
+ # Default to assistant
84
+ [:MemoryRead, :MemoryGlob, :MemoryGrep, :MemoryWrite, :MemoryEdit]
85
+ end
86
+ end
87
+
88
+ # Create a tool instance
89
+ #
90
+ # @param tool_name [Symbol] Tool name
91
+ # @param context [Hash] Creation context with :storage, :agent_name, :chat, etc.
92
+ # @return [RubyLLM::Tool] Tool instance
93
+ def create_tool(tool_name, context)
94
+ storage = context[:storage]
95
+ agent_name = context[:agent_name]
96
+
97
+ # Delegate to SwarmMemory's tool factory
98
+ SwarmMemory.create_tool(tool_name, storage: storage, agent_name: agent_name)
99
+ end
100
+
101
+ # Create plugin storage for an agent
102
+ #
103
+ # @param agent_name [Symbol] Agent identifier
104
+ # @param config [Object] Memory configuration (MemoryConfig or Hash)
105
+ # @return [Core::Storage] Storage instance with embeddings enabled
106
+ def create_storage(agent_name:, config:)
107
+ # Extract adapter type and options from config
108
+ adapter_type, adapter_options = if config.respond_to?(:adapter_type)
109
+ # MemoryConfig object (from DSL)
110
+ [config.adapter_type, config.adapter_options]
111
+ elsif config.is_a?(Hash)
112
+ # Hash (from YAML)
113
+ adapter = (config[:adapter] || config["adapter"] || :filesystem).to_sym
114
+ options = config.reject { |k, _v| k == :adapter || k == "adapter" || k == :mode || k == "mode" }
115
+ [adapter, options]
116
+ else
117
+ raise SwarmSDK::ConfigurationError, "Invalid memory configuration for #{agent_name}"
118
+ end
119
+
120
+ # Get adapter class from registry
121
+ begin
122
+ adapter_class = SwarmMemory.adapter_for(adapter_type)
123
+ rescue ArgumentError => e
124
+ raise SwarmSDK::ConfigurationError, "#{e.message} for agent #{agent_name}"
125
+ end
126
+
127
+ # Instantiate adapter with options
128
+ # Note: Adapter is responsible for validating its own requirements
129
+ begin
130
+ adapter = adapter_class.new(**adapter_options)
131
+ rescue ArgumentError => e
132
+ raise SwarmSDK::ConfigurationError,
133
+ "Failed to initialize #{adapter_type} adapter for #{agent_name}: #{e.message}"
134
+ end
135
+
136
+ # Create embedder for semantic search
137
+ embedder = Embeddings::InformersEmbedder.new
138
+
139
+ # Create storage with embedder (enables semantic features)
140
+ Core::Storage.new(adapter: adapter, embedder: embedder)
141
+ end
142
+
143
+ # Parse memory configuration
144
+ #
145
+ # @param raw_config [Object] Raw config (MemoryConfig or Hash)
146
+ # @return [Object] Parsed configuration
147
+ def parse_config(raw_config)
148
+ # Already parsed by Agent::Definition, just return as-is
149
+ raw_config
150
+ end
151
+
152
+ # Contribute to agent system prompt
153
+ #
154
+ # @param agent_definition [Agent::Definition] Agent definition
155
+ # @param storage [Core::Storage, nil] Storage instance (may be nil during prompt building)
156
+ # @return [String] Memory prompt contribution
157
+ def system_prompt_contribution(agent_definition:, storage:)
158
+ # Extract mode from memory config
159
+ memory_config = agent_definition.memory
160
+ mode = if memory_config.is_a?(SwarmMemory::DSL::MemoryConfig)
161
+ memory_config.mode # MemoryConfig object from DSL
162
+ elsif memory_config.respond_to?(:mode)
163
+ memory_config.mode # Other object with mode method
164
+ elsif memory_config.is_a?(Hash)
165
+ (memory_config[:mode] || memory_config["mode"] || :assistant).to_sym
166
+ else
167
+ :assistant # Default mode
168
+ end
169
+
170
+ # Select prompt template based on mode
171
+ prompt_filename = case mode
172
+ when :retrieval then "memory_retrieval.md.erb"
173
+ when :researcher then "memory_researcher.md.erb"
174
+ else "memory_assistant.md.erb" # Default
175
+ end
176
+
177
+ memory_prompt_path = File.expand_path("../prompts/#{prompt_filename}", __dir__)
178
+ template_content = File.read(memory_prompt_path)
179
+
180
+ # Render with agent_definition binding
181
+ ERB.new(template_content).result(agent_definition.instance_eval { binding })
182
+ end
183
+
184
+ # Tools that should be marked immutable (mode-aware)
185
+ #
186
+ # Memory tools for the current mode plus LoadSkill (if applicable) are immutable.
187
+ # This prevents LoadSkill from accidentally removing memory tools.
188
+ #
189
+ # @param mode [Symbol] Memory mode
190
+ # @return [Array<Symbol>] Immutable tool names for this mode
191
+ def immutable_tools_for_mode(mode)
192
+ base_tools = tools_for_mode(mode)
193
+
194
+ # LoadSkill only for assistant and researcher modes (not retrieval)
195
+ if mode == :retrieval
196
+ base_tools
197
+ else
198
+ base_tools + [:LoadSkill]
199
+ end
200
+ end
201
+
202
+ # Check if storage should be created for this agent
203
+ #
204
+ # @param agent_definition [Agent::Definition] Agent definition
205
+ # @return [Boolean] True if agent has memory configuration
206
+ def storage_enabled?(agent_definition)
207
+ agent_definition.memory_enabled?
208
+ end
209
+
210
+ # Lifecycle: Agent initialized
211
+ #
212
+ # Filters tools by mode (removing non-mode tools), registers LoadSkill,
213
+ # and marks memory tools as immutable.
214
+ #
215
+ # LoadSkill needs special handling because it requires chat, tool_configurator,
216
+ # and agent_definition to perform dynamic tool swapping.
217
+ #
218
+ # @param agent_name [Symbol] Agent identifier
219
+ # @param agent [Agent::Chat] Chat instance
220
+ # @param context [Hash] Initialization context
221
+ def on_agent_initialized(agent_name:, agent:, context:)
222
+ storage = context[:storage]
223
+ agent_definition = context[:agent_definition]
224
+ tool_configurator = context[:tool_configurator]
225
+
226
+ return unless storage # Only proceed if memory is enabled for this agent
227
+
228
+ # Extract mode from memory config
229
+ memory_config = agent_definition.memory
230
+ mode = if memory_config.is_a?(SwarmMemory::DSL::MemoryConfig)
231
+ memory_config.mode # MemoryConfig object from DSL
232
+ elsif memory_config.respond_to?(:mode)
233
+ memory_config.mode # Other object with mode method
234
+ elsif memory_config.is_a?(Hash)
235
+ (memory_config[:mode] || memory_config["mode"] || :interactive).to_sym
236
+ else
237
+ :interactive # Default
238
+ end
239
+
240
+ # Store storage and mode for this agent
241
+ @storages[agent_name] = storage
242
+ @modes[agent_name] = mode
243
+
244
+ # Get mode-specific tools
245
+ allowed_tools = tools_for_mode(mode)
246
+
247
+ # Get all registered memory tool names
248
+ all_memory_tools = tools # Returns all possible memory tools
249
+
250
+ # Remove tools not allowed in this mode
251
+ tools_to_remove = all_memory_tools - allowed_tools
252
+
253
+ tools_to_remove.each do |tool_name|
254
+ agent.remove_tool(tool_name)
255
+ end
256
+
257
+ # Create and register LoadSkill tool (NOT for retrieval mode - read-only)
258
+ unless mode == :retrieval
259
+ load_skill_tool = SwarmMemory.create_tool(
260
+ :LoadSkill,
261
+ storage: storage,
262
+ agent_name: agent_name,
263
+ chat: agent,
264
+ tool_configurator: tool_configurator,
265
+ agent_definition: agent_definition,
266
+ )
267
+
268
+ agent.with_tool(load_skill_tool)
269
+ end
270
+
271
+ # Mark mode-specific memory tools + LoadSkill as immutable
272
+ agent.mark_tools_immutable(immutable_tools_for_mode(mode).map(&:to_s))
273
+ end
274
+
275
+ # Lifecycle: User message
276
+ #
277
+ # Performs TWO semantic searches:
278
+ # 1. Skills - For loadable procedures with LoadSkill
279
+ # 2. Memories - For concepts/facts/experiences that provide context
280
+ #
281
+ # Returns system reminders for both if high-confidence matches found.
282
+ #
283
+ # @param agent_name [Symbol] Agent identifier
284
+ # @param prompt [String] User's message
285
+ # @param is_first_message [Boolean] True if first message
286
+ # @return [Array<String>] System reminders (0-2 reminders)
287
+ def on_user_message(agent_name:, prompt:, is_first_message:)
288
+ storage = @storages[agent_name]
289
+ return [] unless storage&.semantic_index
290
+
291
+ # Adaptive threshold based on query length
292
+ # Short queries use lower threshold as they have less semantic richness
293
+ # Optimal: cutoff=10 words, short=0.25, normal=0.35 (discovered via systematic evaluation)
294
+ word_count = prompt.split.size
295
+ word_cutoff = (ENV["SWARM_MEMORY_ADAPTIVE_WORD_CUTOFF"] || "10").to_i
296
+
297
+ threshold = if word_count < word_cutoff
298
+ (ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD_SHORT"] || "0.25").to_f
299
+ else
300
+ (ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD"] || "0.35").to_f
301
+ end
302
+ reminders = []
303
+
304
+ # Run both searches in parallel with Async
305
+ Async do |task|
306
+ # Search 1: Skills (type = "skill")
307
+ skills_task = task.async do
308
+ storage.semantic_index.search(
309
+ query: prompt,
310
+ top_k: 3,
311
+ threshold: threshold,
312
+ filter: { "type" => "skill" },
313
+ )
314
+ end
315
+
316
+ # Search 2: All results (for memories + logging)
317
+ all_results_task = task.async do
318
+ storage.semantic_index.search(
319
+ query: prompt,
320
+ top_k: 10,
321
+ threshold: 0.0, # Get all for logging
322
+ filter: nil,
323
+ )
324
+ end
325
+
326
+ # Wait for both searches to complete
327
+ skills = skills_task.wait
328
+ all_results = all_results_task.wait
329
+
330
+ # Filter to concepts, facts, experiences (not skills)
331
+ memories = all_results
332
+ .select { |r| ["concept", "fact", "experience"].include?(r.dig(:metadata, "type")) }
333
+ .select { |r| r[:similarity] >= threshold }
334
+ .take(3)
335
+
336
+ # Emit log events (include word count for adaptive threshold analysis)
337
+ search_context = { threshold: threshold, word_count: word_count, word_cutoff: word_cutoff }
338
+ emit_skill_search_log(agent_name, prompt, skills, all_results, search_context)
339
+ emit_memory_search_log(agent_name, prompt, memories, all_results, search_context)
340
+
341
+ # Build skill reminder if found
342
+ if skills.any?
343
+ reminders << build_skill_discovery_reminder(skills)
344
+ end
345
+
346
+ # Build memory reminder if found
347
+ if memories.any?
348
+ reminders << build_memory_discovery_reminder(memories)
349
+ end
350
+ end.wait
351
+
352
+ reminders
353
+ end
354
+
355
+ private
356
+
357
+ # Emit log event for semantic skill search
358
+ #
359
+ # @param agent_name [Symbol] Agent identifier
360
+ # @param prompt [String] User's message
361
+ # @param skills [Array<Hash>] Found skills (filtered)
362
+ # @param all_results [Array<Hash>] All search results (unfiltered)
363
+ # @param search_context [Hash] Search context with :threshold and :word_count
364
+ # @return [void]
365
+ def emit_skill_search_log(agent_name, prompt, skills, all_results, search_context)
366
+ return unless SwarmSDK::LogStream.enabled?
367
+
368
+ threshold = search_context[:threshold]
369
+ word_count = search_context[:word_count]
370
+ word_cutoff = search_context[:word_cutoff]
371
+
372
+ # Include top 5 results for debugging (even if below threshold or wrong type)
373
+ all_entries_debug = all_results.take(5).map do |result|
374
+ {
375
+ path: result[:path],
376
+ title: result[:title],
377
+ hybrid_score: result[:similarity].round(3),
378
+ semantic_score: result[:semantic_score]&.round(3),
379
+ keyword_score: result[:keyword_score]&.round(3),
380
+ type: result.dig(:metadata, "type"),
381
+ tags: result.dig(:metadata, "tags"),
382
+ }
383
+ end
384
+
385
+ # Get actual weights being used (from ENV or defaults)
386
+ semantic_weight = (ENV["SWARM_MEMORY_SEMANTIC_WEIGHT"] || "0.5").to_f
387
+ keyword_weight = (ENV["SWARM_MEMORY_KEYWORD_WEIGHT"] || "0.5").to_f
388
+
389
+ SwarmSDK::LogStream.emit(
390
+ type: "semantic_skill_search",
391
+ agent: agent_name,
392
+ query: prompt,
393
+ query_word_count: word_count,
394
+ threshold: threshold,
395
+ threshold_type: word_count < word_cutoff ? "short_query" : "normal_query",
396
+ adaptive_cutoff: word_cutoff,
397
+ skills_found: skills.size,
398
+ total_entries_searched: all_results.size,
399
+ search_mode: "hybrid",
400
+ weights: { semantic: semantic_weight, keyword: keyword_weight },
401
+ skills: skills.map do |skill|
402
+ {
403
+ path: skill[:path],
404
+ title: skill[:title],
405
+ hybrid_score: skill[:similarity].round(3),
406
+ semantic_score: skill[:semantic_score]&.round(3),
407
+ keyword_score: skill[:keyword_score]&.round(3),
408
+ }
409
+ end,
410
+ debug_top_results: all_entries_debug,
411
+ )
412
+ end
413
+
414
+ # Emit log event for semantic memory search
415
+ #
416
+ # @param agent_name [Symbol] Agent identifier
417
+ # @param prompt [String] User's message
418
+ # @param memories [Array<Hash>] Found memories (concepts/facts/experiences)
419
+ # @param all_results [Array<Hash>] All search results (unfiltered)
420
+ # @param search_context [Hash] Search context with :threshold and :word_count
421
+ # @return [void]
422
+ def emit_memory_search_log(agent_name, prompt, memories, all_results, search_context)
423
+ return unless SwarmSDK::LogStream.enabled?
424
+
425
+ threshold = search_context[:threshold]
426
+ word_count = search_context[:word_count]
427
+ word_cutoff = search_context[:word_cutoff]
428
+
429
+ # Filter all_results to only concept/fact/experience types for debug output
430
+ memory_entries = all_results.select do |r|
431
+ ["concept", "fact", "experience"].include?(r.dig(:metadata, "type"))
432
+ end
433
+
434
+ # Include top 10 memory entries for debugging (even if below threshold)
435
+ debug_all_memories = memory_entries.take(10).map do |result|
436
+ {
437
+ path: result[:path],
438
+ title: result[:title],
439
+ hybrid_score: result[:similarity].round(3),
440
+ semantic_score: result[:semantic_score]&.round(3),
441
+ keyword_score: result[:keyword_score]&.round(3),
442
+ type: result.dig(:metadata, "type"),
443
+ tags: result.dig(:metadata, "tags"),
444
+ domain: result.dig(:metadata, "domain"),
445
+ }
446
+ end
447
+
448
+ # Get actual weights being used (from ENV or defaults)
449
+ semantic_weight = (ENV["SWARM_MEMORY_SEMANTIC_WEIGHT"] || "0.5").to_f
450
+ keyword_weight = (ENV["SWARM_MEMORY_KEYWORD_WEIGHT"] || "0.5").to_f
451
+
452
+ SwarmSDK::LogStream.emit(
453
+ type: "semantic_memory_search",
454
+ agent: agent_name,
455
+ query: prompt,
456
+ query_word_count: word_count,
457
+ threshold: threshold,
458
+ threshold_type: word_count < word_cutoff ? "short_query" : "normal_query",
459
+ adaptive_cutoff: word_cutoff,
460
+ memories_found: memories.size,
461
+ total_memory_entries_searched: memory_entries.size,
462
+ search_mode: "hybrid",
463
+ weights: { semantic: semantic_weight, keyword: keyword_weight },
464
+ memories: memories.map do |memory|
465
+ {
466
+ path: memory[:path],
467
+ title: memory[:title],
468
+ type: memory.dig(:metadata, "type"),
469
+ hybrid_score: memory[:similarity].round(3),
470
+ semantic_score: memory[:semantic_score]&.round(3),
471
+ keyword_score: memory[:keyword_score]&.round(3),
472
+ }
473
+ end,
474
+ debug_top_results: debug_all_memories,
475
+ )
476
+ end
477
+
478
+ # Build system reminder for discovered skills
479
+ #
480
+ # @param skills [Array<Hash>] Skill search results
481
+ # @return [String] Formatted system reminder
482
+ def build_skill_discovery_reminder(skills)
483
+ reminder = "<system-reminder>\n"
484
+ reminder += "🎯 Found #{skills.size} skill(s) in memory that may be relevant:\n\n"
485
+
486
+ skills.each do |skill|
487
+ match_pct = (skill[:similarity] * 100).round
488
+ reminder += "**#{skill[:title]}** (#{match_pct}% match)\n"
489
+ reminder += "Path: `#{skill[:path]}`\n"
490
+ reminder += "To use: `LoadSkill(file_path: \"#{skill[:path]}\")`\n\n"
491
+ end
492
+
493
+ reminder += "**If a skill matches your task:** Load it to get step-by-step instructions and adapted tools.\n"
494
+ reminder += "**If none match (false positive):** Ignore and proceed normally.\n"
495
+ reminder += "</system-reminder>"
496
+
497
+ reminder
498
+ end
499
+
500
+ # Build system reminder for discovered memories
501
+ #
502
+ # @param memories [Array<Hash>] Memory search results (concepts/facts/experiences)
503
+ # @return [String] Formatted system reminder
504
+ def build_memory_discovery_reminder(memories)
505
+ reminder = "<system-reminder>\n"
506
+ reminder += "📚 Found #{memories.size} memory entr#{memories.size == 1 ? "y" : "ies"} that may provide context:\n\n"
507
+
508
+ memories.each do |memory|
509
+ match_pct = (memory[:similarity] * 100).round
510
+ type = memory.dig(:metadata, "type")
511
+ type_emoji = case type
512
+ when "concept" then "💡"
513
+ when "fact" then "📋"
514
+ when "experience" then "🔍"
515
+ else "📄"
516
+ end
517
+
518
+ reminder += "#{type_emoji} **#{memory[:title]}** (#{type}, #{match_pct}% match)\n"
519
+ reminder += "Path: `#{memory[:path]}`\n"
520
+ reminder += "Read with: `MemoryRead(file_path: \"#{memory[:path]}\")`\n\n"
521
+ end
522
+
523
+ reminder += "**These entries may contain relevant knowledge for your task.**\n"
524
+ reminder += "Read them to inform your approach, or ignore if not helpful.\n"
525
+ reminder += "</system-reminder>"
526
+
527
+ reminder
528
+ end
529
+ end
530
+ end
531
+ end