claude_swarm 1.0.0 → 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.
- checksums.yaml +4 -4
- data/.claude/commands/release.md +1 -1
- data/.claude/hooks/lint-code-files.rb +65 -0
- data/.rubocop.yml +22 -2
- data/CHANGELOG.md +21 -1
- data/CLAUDE.md +1 -1
- data/CONTRIBUTING.md +69 -0
- data/README.md +27 -2
- data/Rakefile +71 -3
- data/analyze_coverage.rb +94 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
- data/docs/v2/README.md +308 -0
- data/docs/v2/guides/claude-code-agents.md +262 -0
- data/docs/v2/guides/complete-tutorial.md +3088 -0
- data/docs/v2/guides/getting-started.md +1456 -0
- data/docs/v2/guides/memory-adapters.md +998 -0
- data/docs/v2/guides/plugins.md +816 -0
- data/docs/v2/guides/quick-start-cli.md +1745 -0
- data/docs/v2/guides/rails-integration.md +1902 -0
- data/docs/v2/guides/swarm-memory.md +599 -0
- data/docs/v2/reference/cli.md +729 -0
- data/docs/v2/reference/ruby-dsl.md +2154 -0
- data/docs/v2/reference/yaml.md +1835 -0
- data/docs-team-swarm.yml +2222 -0
- data/examples/learning-assistant/assistant.md +7 -0
- data/examples/learning-assistant/example-memories/concept-example.md +90 -0
- data/examples/learning-assistant/example-memories/experience-example.md +66 -0
- data/examples/learning-assistant/example-memories/fact-example.md +76 -0
- data/examples/learning-assistant/example-memories/memory-index.md +78 -0
- data/examples/learning-assistant/example-memories/skill-example.md +168 -0
- data/examples/learning-assistant/learning_assistant.rb +34 -0
- data/examples/learning-assistant/learning_assistant.yml +20 -0
- data/examples/v2/dsl/01_basic.rb +44 -0
- data/examples/v2/dsl/02_core_parameters.rb +59 -0
- data/examples/v2/dsl/03_capabilities.rb +71 -0
- data/examples/v2/dsl/04_llm_parameters.rb +56 -0
- data/examples/v2/dsl/05_advanced_flags.rb +73 -0
- data/examples/v2/dsl/06_permissions.rb +80 -0
- data/examples/v2/dsl/07_mcp_server.rb +62 -0
- data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
- data/examples/v2/dsl/09_agent_hooks.rb +67 -0
- data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
- data/examples/v2/dsl/11_delegation.rb +60 -0
- data/examples/v2/dsl/12_complete_integration.rb +137 -0
- data/examples/v2/file_tools_swarm.yml +102 -0
- data/examples/v2/hooks/01_basic_hooks.rb +133 -0
- data/examples/v2/hooks/02_usage_tracking.rb +201 -0
- data/examples/v2/hooks/03_production_monitoring.rb +429 -0
- data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
- data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
- data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
- data/examples/v2/hooks/swarm_summary.sh +44 -0
- data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
- data/examples/v2/hooks/validate_bash.rb +59 -0
- data/examples/v2/multi_directory_permissions.yml +221 -0
- data/examples/v2/node_context_demo.rb +127 -0
- data/examples/v2/node_workflow.rb +173 -0
- data/examples/v2/path_resolution_demo.rb +216 -0
- data/examples/v2/simple-swarm-v2.rb +90 -0
- data/examples/v2/simple-swarm-v2.yml +62 -0
- data/examples/v2/swarm.yml +71 -0
- data/examples/v2/swarm_with_hooks.yml +61 -0
- data/examples/v2/swarm_with_hooks_simple.yml +25 -0
- data/examples/v2/think_tool_demo.rb +62 -0
- data/exe/swarm +6 -0
- data/lib/claude_swarm/claude_mcp_server.rb +0 -6
- data/lib/claude_swarm/cli.rb +10 -3
- data/lib/claude_swarm/commands/ps.rb +19 -20
- data/lib/claude_swarm/commands/show.rb +1 -1
- data/lib/claude_swarm/configuration.rb +10 -12
- data/lib/claude_swarm/mcp_generator.rb +10 -1
- data/lib/claude_swarm/orchestrator.rb +73 -49
- data/lib/claude_swarm/system_utils.rb +37 -11
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +1 -0
- data/lib/claude_swarm/yaml_loader.rb +22 -0
- data/lib/claude_swarm.rb +7 -3
- data/lib/swarm_cli/cli.rb +201 -0
- data/lib/swarm_cli/command_registry.rb +61 -0
- data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
- data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
- data/lib/swarm_cli/commands/migrate.rb +55 -0
- data/lib/swarm_cli/commands/run.rb +173 -0
- data/lib/swarm_cli/config_loader.rb +97 -0
- data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
- data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
- data/lib/swarm_cli/interactive_repl.rb +918 -0
- data/lib/swarm_cli/mcp_serve_options.rb +44 -0
- data/lib/swarm_cli/mcp_tools_options.rb +59 -0
- data/lib/swarm_cli/migrate_options.rb +54 -0
- data/lib/swarm_cli/migrator.rb +132 -0
- data/lib/swarm_cli/options.rb +151 -0
- data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
- data/lib/swarm_cli/ui/components/content_block.rb +120 -0
- data/lib/swarm_cli/ui/components/divider.rb +57 -0
- data/lib/swarm_cli/ui/components/panel.rb +62 -0
- data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
- data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
- data/lib/swarm_cli/ui/formatters/number.rb +58 -0
- data/lib/swarm_cli/ui/formatters/text.rb +77 -0
- data/lib/swarm_cli/ui/formatters/time.rb +73 -0
- data/lib/swarm_cli/ui/icons.rb +59 -0
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
- data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
- data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
- data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
- data/lib/swarm_cli/version.rb +5 -0
- data/lib/swarm_cli.rb +44 -0
- data/lib/swarm_memory/adapters/base.rb +141 -0
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
- data/lib/swarm_memory/chat_extension.rb +34 -0
- data/lib/swarm_memory/cli/commands.rb +306 -0
- data/lib/swarm_memory/core/entry.rb +37 -0
- data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
- data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
- data/lib/swarm_memory/core/path_normalizer.rb +75 -0
- data/lib/swarm_memory/core/semantic_index.rb +244 -0
- data/lib/swarm_memory/core/storage.rb +288 -0
- data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
- data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
- data/lib/swarm_memory/dsl/memory_config.rb +113 -0
- data/lib/swarm_memory/embeddings/embedder.rb +36 -0
- data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
- data/lib/swarm_memory/errors.rb +21 -0
- data/lib/swarm_memory/integration/cli_registration.rb +30 -0
- data/lib/swarm_memory/integration/configuration.rb +43 -0
- data/lib/swarm_memory/integration/registration.rb +31 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
- data/lib/swarm_memory/optimization/analyzer.rb +244 -0
- data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
- data/lib/swarm_memory/prompts/memory.md.erb +109 -0
- data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
- data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
- data/lib/swarm_memory/search/semantic_search.rb +112 -0
- data/lib/swarm_memory/search/text_search.rb +42 -0
- data/lib/swarm_memory/search/text_similarity.rb +80 -0
- data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
- data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
- data/lib/swarm_memory/tools/load_skill.rb +313 -0
- data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
- data/lib/swarm_memory/tools/memory_delete.rb +99 -0
- data/lib/swarm_memory/tools/memory_edit.rb +185 -0
- data/lib/swarm_memory/tools/memory_glob.rb +160 -0
- data/lib/swarm_memory/tools/memory_grep.rb +247 -0
- data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
- data/lib/swarm_memory/tools/memory_read.rb +123 -0
- data/lib/swarm_memory/tools/memory_write.rb +231 -0
- data/lib/swarm_memory/utils.rb +50 -0
- data/lib/swarm_memory/version.rb +5 -0
- data/lib/swarm_memory.rb +166 -0
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
- data/lib/swarm_sdk/agent/builder.rb +461 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
- data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
- data/lib/swarm_sdk/agent/chat.rb +1159 -0
- data/lib/swarm_sdk/agent/context.rb +112 -0
- data/lib/swarm_sdk/agent/context_manager.rb +309 -0
- data/lib/swarm_sdk/agent/definition.rb +556 -0
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
- data/lib/swarm_sdk/configuration.rb +296 -0
- data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
- data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
- data/lib/swarm_sdk/context_compactor.rb +340 -0
- data/lib/swarm_sdk/hooks/adapter.rb +359 -0
- data/lib/swarm_sdk/hooks/context.rb +197 -0
- data/lib/swarm_sdk/hooks/definition.rb +80 -0
- data/lib/swarm_sdk/hooks/error.rb +29 -0
- data/lib/swarm_sdk/hooks/executor.rb +146 -0
- data/lib/swarm_sdk/hooks/registry.rb +147 -0
- data/lib/swarm_sdk/hooks/result.rb +150 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
- data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
- data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
- data/lib/swarm_sdk/log_collector.rb +51 -0
- data/lib/swarm_sdk/log_stream.rb +69 -0
- data/lib/swarm_sdk/markdown_parser.rb +75 -0
- data/lib/swarm_sdk/model_aliases.json +5 -0
- data/lib/swarm_sdk/models.json +1 -0
- data/lib/swarm_sdk/models.rb +120 -0
- data/lib/swarm_sdk/node/agent_config.rb +49 -0
- data/lib/swarm_sdk/node/builder.rb +439 -0
- data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
- data/lib/swarm_sdk/node_context.rb +170 -0
- data/lib/swarm_sdk/node_orchestrator.rb +384 -0
- data/lib/swarm_sdk/permissions/config.rb +239 -0
- data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
- data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
- data/lib/swarm_sdk/permissions/validator.rb +173 -0
- data/lib/swarm_sdk/permissions_builder.rb +122 -0
- data/lib/swarm_sdk/plugin.rb +147 -0
- data/lib/swarm_sdk/plugin_registry.rb +101 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
- data/lib/swarm_sdk/result.rb +97 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
- data/lib/swarm_sdk/swarm/builder.rb +586 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
- data/lib/swarm_sdk/swarm.rb +982 -0
- data/lib/swarm_sdk/tools/bash.rb +274 -0
- data/lib/swarm_sdk/tools/clock.rb +44 -0
- data/lib/swarm_sdk/tools/delegate.rb +164 -0
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
- data/lib/swarm_sdk/tools/edit.rb +150 -0
- data/lib/swarm_sdk/tools/glob.rb +158 -0
- data/lib/swarm_sdk/tools/grep.rb +228 -0
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
- data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
- data/lib/swarm_sdk/tools/read.rb +251 -0
- data/lib/swarm_sdk/tools/registry.rb +93 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
- data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
- data/lib/swarm_sdk/tools/think.rb +95 -0
- data/lib/swarm_sdk/tools/todo_write.rb +216 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
- data/lib/swarm_sdk/tools/write.rb +117 -0
- data/lib/swarm_sdk/utils.rb +50 -0
- data/lib/swarm_sdk/version.rb +5 -0
- data/lib/swarm_sdk.rb +157 -0
- data/llm.v2.txt +13407 -0
- data/rubocop/cop/security/no_reflection_methods.rb +47 -0
- data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
- data/swarm_cli.gemspec +57 -0
- data/swarm_memory.gemspec +28 -0
- data/swarm_sdk.gemspec +41 -0
- data/team.yml +1 -1
- data/team_full.yml +1875 -0
- data/{team_v2.yml → team_sdk.yml} +121 -52
- metadata +247 -4
- data/EXAMPLES.md +0 -164
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
# Memory Adapter Development Guide
|
|
2
|
+
|
|
3
|
+
Learn how to build custom storage adapters for SwarmMemory using the plugin architecture.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
SwarmMemory uses a **plugin adapter pattern** for storage, allowing you to implement custom backends that fit your infrastructure:
|
|
10
|
+
|
|
11
|
+
- **FilesystemAdapter** (built-in) - Stores in `.md/.yml/.emb` files, Git-friendly
|
|
12
|
+
- **Custom Adapters** (via registry) - PostgreSQL, MySQL, MongoDB, Qdrant, or any storage you need
|
|
13
|
+
|
|
14
|
+
**Plugin Architecture Benefits:**
|
|
15
|
+
- ✅ SwarmMemory stays lightweight (no forced dependencies)
|
|
16
|
+
- ✅ Use your existing ORM (ActiveRecord, Sequel, etc.)
|
|
17
|
+
- ✅ Optimize for your infrastructure
|
|
18
|
+
- ✅ Community can publish adapters as gems
|
|
19
|
+
|
|
20
|
+
**Any adapter** that implements the `Adapters::Base` interface will work seamlessly with:
|
|
21
|
+
- All memory tools (MemoryWrite, MemoryRead, etc.)
|
|
22
|
+
- Semantic search
|
|
23
|
+
- Defrag operations
|
|
24
|
+
- CLI commands
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Adapter Registry
|
|
29
|
+
|
|
30
|
+
SwarmMemory provides a registry system for custom adapters:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# Register your adapter when your app boots
|
|
34
|
+
SwarmMemory.register_adapter(:my_adapter, MyCustomAdapter)
|
|
35
|
+
|
|
36
|
+
# Now use it in configuration
|
|
37
|
+
agent :researcher do
|
|
38
|
+
memory do
|
|
39
|
+
adapter :my_adapter
|
|
40
|
+
option :custom_option, "value"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check available adapters
|
|
45
|
+
SwarmMemory.available_adapters
|
|
46
|
+
# => [:filesystem, :my_adapter]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Adapter Interface
|
|
52
|
+
|
|
53
|
+
### Required Methods
|
|
54
|
+
|
|
55
|
+
All adapters MUST inherit from `SwarmMemory::Adapters::Base` and implement these methods:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
module SwarmMemory
|
|
59
|
+
module Adapters
|
|
60
|
+
class MyAdapter < Base
|
|
61
|
+
# Write an entry
|
|
62
|
+
#
|
|
63
|
+
# @param file_path [String] Logical path (e.g., "concept/ruby/classes.md")
|
|
64
|
+
# @param content [String] Markdown content
|
|
65
|
+
# @param title [String] Entry title
|
|
66
|
+
# @param embedding [Array<Float>, nil] Optional 384-dim embedding vector
|
|
67
|
+
# @param metadata [Hash, nil] Metadata (type, tags, confidence, etc.)
|
|
68
|
+
# @return [Core::Entry] The created entry
|
|
69
|
+
def write(file_path:, content:, title:, embedding: nil, metadata: nil)
|
|
70
|
+
# Implement storage logic
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Read entry content only
|
|
74
|
+
#
|
|
75
|
+
# @param file_path [String] Logical path
|
|
76
|
+
# @return [String] Content
|
|
77
|
+
# @raise [ArgumentError] If not found
|
|
78
|
+
def read(file_path:)
|
|
79
|
+
# Implement retrieval logic
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Read full entry with metadata
|
|
83
|
+
#
|
|
84
|
+
# @param file_path [String] Logical path
|
|
85
|
+
# @return [Core::Entry] Full entry object
|
|
86
|
+
# @raise [ArgumentError] If not found
|
|
87
|
+
def read_entry(file_path:)
|
|
88
|
+
# Implement full retrieval logic
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Delete an entry
|
|
92
|
+
#
|
|
93
|
+
# @param file_path [String] Logical path
|
|
94
|
+
# @return [void]
|
|
95
|
+
# @raise [ArgumentError] If not found
|
|
96
|
+
def delete(file_path:)
|
|
97
|
+
# Implement deletion logic
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# List all entries
|
|
101
|
+
#
|
|
102
|
+
# @param prefix [String, nil] Optional prefix filter
|
|
103
|
+
# @return [Array<Hash>] Array of {path:, title:, size:, updated_at:}
|
|
104
|
+
def list(prefix: nil)
|
|
105
|
+
# Implement listing logic
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Search by glob pattern
|
|
109
|
+
#
|
|
110
|
+
# @param pattern [String] Glob pattern (e.g., "skill/**/*.md")
|
|
111
|
+
# @return [Array<Hash>] Matching entries
|
|
112
|
+
def glob(pattern:)
|
|
113
|
+
# Implement glob search
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Search by content regex
|
|
117
|
+
#
|
|
118
|
+
# @param pattern [String] Regex pattern
|
|
119
|
+
# @param case_insensitive [Boolean] Case-insensitive flag
|
|
120
|
+
# @param output_mode [String] "files_with_matches", "content", or "count"
|
|
121
|
+
# @return [Array] Results in requested format
|
|
122
|
+
def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
|
|
123
|
+
# Implement grep search
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Clear all entries
|
|
127
|
+
#
|
|
128
|
+
# @return [void]
|
|
129
|
+
def clear
|
|
130
|
+
# Implement clear logic
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Get total storage size in bytes
|
|
134
|
+
#
|
|
135
|
+
# @return [Integer] Total size
|
|
136
|
+
def total_size
|
|
137
|
+
# Implement size calculation
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Get number of entries
|
|
141
|
+
#
|
|
142
|
+
# @return [Integer] Entry count
|
|
143
|
+
def size
|
|
144
|
+
# Implement count
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Get all entries (for defrag operations)
|
|
148
|
+
#
|
|
149
|
+
# @return [Hash<String, Core::Entry>] All entries keyed by path
|
|
150
|
+
def all_entries
|
|
151
|
+
# Implement bulk retrieval
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Semantic search by embedding vector (REQUIRED for semantic search)
|
|
155
|
+
#
|
|
156
|
+
# @param embedding [Array<Float>] Query embedding (384-dim)
|
|
157
|
+
# @param top_k [Integer] Number of results
|
|
158
|
+
# @param threshold [Float] Minimum similarity (0.0-1.0)
|
|
159
|
+
# @return [Array<Hash>] Results with :path, :similarity, :title, :metadata
|
|
160
|
+
def semantic_search(embedding:, top_k: 10, threshold: 0.0)
|
|
161
|
+
# Implement semantic search
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Configuration DSL
|
|
171
|
+
|
|
172
|
+
The memory configuration DSL supports passing options to custom adapters:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
# Filesystem adapter (built-in)
|
|
176
|
+
agent :researcher do
|
|
177
|
+
memory do
|
|
178
|
+
adapter :filesystem
|
|
179
|
+
directory ".swarm/memory/researcher"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Custom adapter with options
|
|
184
|
+
agent :researcher do
|
|
185
|
+
memory do
|
|
186
|
+
adapter :activerecord
|
|
187
|
+
option :namespace, "researcher"
|
|
188
|
+
option :table_name, "memory_entries"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Options are passed to adapter's initialize method
|
|
193
|
+
# MyAdapter.new(namespace: "researcher", table_name: "memory_entries")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Example: ActiveRecord Adapter (PostgreSQL/Rails)
|
|
199
|
+
|
|
200
|
+
Complete example using Rails with ActiveRecord and PostgreSQL:
|
|
201
|
+
|
|
202
|
+
### Step 1: Migration
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
# db/migrate/20250126_create_memory_entries.rb
|
|
206
|
+
class CreateMemoryEntries < ActiveRecord::Migration[7.1]
|
|
207
|
+
def change
|
|
208
|
+
# Enable pgvector extension for embeddings
|
|
209
|
+
enable_extension 'vector'
|
|
210
|
+
|
|
211
|
+
create_table :memory_entries, id: false do |t|
|
|
212
|
+
t.string :namespace, null: false
|
|
213
|
+
t.string :path, null: false
|
|
214
|
+
t.text :content, null: false
|
|
215
|
+
t.string :title, null: false
|
|
216
|
+
t.column :embedding, :vector, limit: 384
|
|
217
|
+
t.jsonb :metadata, default: {}
|
|
218
|
+
t.integer :size, null: false
|
|
219
|
+
t.integer :hits, default: 0
|
|
220
|
+
t.timestamps
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Primary key on namespace + path
|
|
224
|
+
add_index :memory_entries, [:namespace, :path], unique: true
|
|
225
|
+
|
|
226
|
+
# Indexes for performance
|
|
227
|
+
add_index :memory_entries, :namespace
|
|
228
|
+
add_index :memory_entries, :metadata, using: :gin
|
|
229
|
+
add_index :memory_entries, :embedding, using: :hnsw, opclass: :vector_cosine_ops
|
|
230
|
+
|
|
231
|
+
# Full-text search index
|
|
232
|
+
execute <<-SQL
|
|
233
|
+
CREATE INDEX index_memory_entries_content_tsvector
|
|
234
|
+
ON memory_entries
|
|
235
|
+
USING GIN(to_tsvector('english', content))
|
|
236
|
+
SQL
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Step 2: Model
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
# app/models/memory_entry.rb
|
|
245
|
+
class MemoryEntry < ApplicationRecord
|
|
246
|
+
validates :path, :namespace, :content, :title, presence: true
|
|
247
|
+
validates :path, uniqueness: { scope: :namespace }
|
|
248
|
+
|
|
249
|
+
scope :in_namespace, ->(ns) { where(namespace: ns) }
|
|
250
|
+
|
|
251
|
+
scope :matching_content, ->(pattern, case_insensitive) {
|
|
252
|
+
operator = case_insensitive ? "~*" : "~"
|
|
253
|
+
where("content #{operator} ?", pattern)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
def self.similar_to(embedding, threshold: 0.0, limit: 10)
|
|
257
|
+
where("embedding IS NOT NULL")
|
|
258
|
+
.where("1 - (embedding <=> ?) >= ?", embedding.to_s, threshold)
|
|
259
|
+
.order(Arel.sql("embedding <=> '#{embedding}'"))
|
|
260
|
+
.limit(limit)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def increment_hits!
|
|
264
|
+
increment!(:hits)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Step 3: Adapter Implementation
|
|
270
|
+
|
|
271
|
+
See the complete ActiveRecord adapter implementation in the separate documentation file:
|
|
272
|
+
**[docs/v2/swarm_memory_adapters.md](../swarm_memory_adapters.md)**
|
|
273
|
+
|
|
274
|
+
The adapter includes:
|
|
275
|
+
- Full CRUD operations using ActiveRecord
|
|
276
|
+
- PostgreSQL full-text search for fast grep
|
|
277
|
+
- pgvector integration for semantic search
|
|
278
|
+
- Proper error handling and validation
|
|
279
|
+
- ~300 lines of production-ready code
|
|
280
|
+
|
|
281
|
+
###Step 4: Register the Adapter
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
# config/initializers/swarm_memory.rb
|
|
285
|
+
require_relative '../../lib/activerecord_memory_adapter'
|
|
286
|
+
|
|
287
|
+
SwarmMemory.register_adapter(:activerecord, ActiveRecordMemoryAdapter)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Step 5: Configure Your Agent
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
# swarm.rb
|
|
294
|
+
agent :researcher do
|
|
295
|
+
memory do
|
|
296
|
+
adapter :activerecord
|
|
297
|
+
option :namespace, "researcher"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Legacy Example: Qdrant Adapter
|
|
305
|
+
|
|
306
|
+
> **Note**: This is a conceptual example. For production use with vector databases,
|
|
307
|
+
> implement and register your own adapter using the pattern above.
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
# Conceptual example - not maintained
|
|
311
|
+
class QdrantAdapter < SwarmMemory::Adapters::Base
|
|
312
|
+
def initialize(url:, collection:, api_key: nil)
|
|
313
|
+
super()
|
|
314
|
+
@client = Qdrant::Client.new(url: url, api_key: api_key)
|
|
315
|
+
@collection = collection
|
|
316
|
+
@total_size = 0
|
|
317
|
+
|
|
318
|
+
# Ensure collection exists
|
|
319
|
+
ensure_collection_exists
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def write(file_path:, content:, title:, embedding: nil, metadata: nil)
|
|
323
|
+
# Calculate size
|
|
324
|
+
content_size = content.bytesize
|
|
325
|
+
|
|
326
|
+
# Store in Qdrant
|
|
327
|
+
@client.upsert(
|
|
328
|
+
collection_name: @collection,
|
|
329
|
+
points: [{
|
|
330
|
+
id: file_path, # Use path as ID
|
|
331
|
+
vector: embedding || [],
|
|
332
|
+
payload: {
|
|
333
|
+
content: content,
|
|
334
|
+
title: title,
|
|
335
|
+
size: content_size,
|
|
336
|
+
updated_at: Time.now.to_i,
|
|
337
|
+
metadata: metadata || {}
|
|
338
|
+
}
|
|
339
|
+
}]
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
@total_size += content_size
|
|
343
|
+
|
|
344
|
+
# Return entry
|
|
345
|
+
Core::Entry.new(
|
|
346
|
+
content: content,
|
|
347
|
+
title: title,
|
|
348
|
+
updated_at: Time.now,
|
|
349
|
+
size: content_size,
|
|
350
|
+
embedding: embedding,
|
|
351
|
+
metadata: metadata
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def read(file_path:)
|
|
356
|
+
result = @client.retrieve(
|
|
357
|
+
collection_name: @collection,
|
|
358
|
+
ids: [file_path]
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
raise ArgumentError, "memory://#{file_path} not found" if result.empty?
|
|
362
|
+
|
|
363
|
+
result.first.payload["content"]
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def read_entry(file_path:)
|
|
367
|
+
result = @client.retrieve(
|
|
368
|
+
collection_name: @collection,
|
|
369
|
+
ids: [file_path]
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
raise ArgumentError, "memory://#{file_path} not found" if result.empty?
|
|
373
|
+
|
|
374
|
+
point = result.first
|
|
375
|
+
payload = point.payload
|
|
376
|
+
|
|
377
|
+
Core::Entry.new(
|
|
378
|
+
content: payload["content"],
|
|
379
|
+
title: payload["title"],
|
|
380
|
+
updated_at: Time.at(payload["updated_at"]),
|
|
381
|
+
size: payload["size"],
|
|
382
|
+
embedding: point.vector,
|
|
383
|
+
metadata: payload["metadata"]
|
|
384
|
+
)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def delete(file_path:)
|
|
388
|
+
result = @client.delete(
|
|
389
|
+
collection_name: @collection,
|
|
390
|
+
points: [file_path]
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
raise ArgumentError, "memory://#{file_path} not found" unless result.ok?
|
|
394
|
+
|
|
395
|
+
entry = read_entry(file_path: file_path)
|
|
396
|
+
@total_size -= entry.size
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def list(prefix: nil)
|
|
400
|
+
# Scroll through all points
|
|
401
|
+
results = @client.scroll(
|
|
402
|
+
collection_name: @collection,
|
|
403
|
+
limit: 1000
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
entries = results.points.map do |point|
|
|
407
|
+
{
|
|
408
|
+
path: point.id,
|
|
409
|
+
title: point.payload["title"],
|
|
410
|
+
size: point.payload["size"],
|
|
411
|
+
updated_at: Time.at(point.payload["updated_at"])
|
|
412
|
+
}
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Filter by prefix if provided
|
|
416
|
+
if prefix
|
|
417
|
+
entries.select { |e| e[:path].start_with?(prefix) }
|
|
418
|
+
else
|
|
419
|
+
entries
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def glob(pattern:)
|
|
424
|
+
# Convert glob to regex
|
|
425
|
+
regex = glob_to_regex(pattern)
|
|
426
|
+
|
|
427
|
+
list.select { |entry| regex.match?(entry[:path]) }
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
|
|
431
|
+
flags = case_insensitive ? Regexp::IGNORECASE : 0
|
|
432
|
+
regex = Regexp.new(pattern, flags)
|
|
433
|
+
|
|
434
|
+
all_entries = all_entries()
|
|
435
|
+
|
|
436
|
+
case output_mode
|
|
437
|
+
when "files_with_matches"
|
|
438
|
+
all_entries.keys.select { |path| regex.match?(all_entries[path].content) }
|
|
439
|
+
when "content"
|
|
440
|
+
# Return matching lines with line numbers
|
|
441
|
+
all_entries.map do |path, entry|
|
|
442
|
+
matches = entry.content.lines.each_with_index.select { |line, _| regex.match?(line) }
|
|
443
|
+
next if matches.empty?
|
|
444
|
+
|
|
445
|
+
{
|
|
446
|
+
path: path,
|
|
447
|
+
matches: matches.map { |line, idx| { line_number: idx + 1, content: line.chomp } }
|
|
448
|
+
}
|
|
449
|
+
end.compact
|
|
450
|
+
when "count"
|
|
451
|
+
all_entries.map do |path, entry|
|
|
452
|
+
count = entry.content.scan(regex).size
|
|
453
|
+
next if count <= 0
|
|
454
|
+
|
|
455
|
+
{ path: path, count: count }
|
|
456
|
+
end.compact
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def clear
|
|
461
|
+
@client.delete_collection(collection_name: @collection)
|
|
462
|
+
ensure_collection_exists
|
|
463
|
+
@total_size = 0
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def total_size
|
|
467
|
+
@total_size
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def size
|
|
471
|
+
list.size
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def all_entries
|
|
475
|
+
results = @client.scroll(
|
|
476
|
+
collection_name: @collection,
|
|
477
|
+
limit: 10000
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
results.points.each_with_object({}) do |point, hash|
|
|
481
|
+
hash[point.id] = Core::Entry.new(
|
|
482
|
+
content: point.payload["content"],
|
|
483
|
+
title: point.payload["title"],
|
|
484
|
+
updated_at: Time.at(point.payload["updated_at"]),
|
|
485
|
+
size: point.payload["size"],
|
|
486
|
+
embedding: point.vector,
|
|
487
|
+
metadata: point.payload["metadata"]
|
|
488
|
+
)
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Semantic search (Qdrant's strength!)
|
|
493
|
+
def semantic_search(embedding:, top_k: 10, threshold: 0.0)
|
|
494
|
+
result = @client.search(
|
|
495
|
+
collection_name: @collection,
|
|
496
|
+
vector: embedding,
|
|
497
|
+
limit: top_k,
|
|
498
|
+
score_threshold: threshold
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
result.map do |hit|
|
|
502
|
+
{
|
|
503
|
+
path: hit.id,
|
|
504
|
+
similarity: hit.score,
|
|
505
|
+
title: hit.payload["title"],
|
|
506
|
+
size: hit.payload["size"],
|
|
507
|
+
updated_at: Time.at(hit.payload["updated_at"]),
|
|
508
|
+
metadata: hit.payload["metadata"]
|
|
509
|
+
}
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
private
|
|
514
|
+
|
|
515
|
+
def ensure_collection_exists
|
|
516
|
+
@client.create_collection(
|
|
517
|
+
collection_name: @collection,
|
|
518
|
+
vectors: {
|
|
519
|
+
size: 384, # all-MiniLM-L6-v2 dimensions
|
|
520
|
+
distance: "Cosine"
|
|
521
|
+
}
|
|
522
|
+
)
|
|
523
|
+
rescue Qdrant::Errors::ApiError => e
|
|
524
|
+
# Collection already exists
|
|
525
|
+
raise unless e.message.include?("already exists")
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def glob_to_regex(pattern)
|
|
529
|
+
# Convert glob wildcards to regex
|
|
530
|
+
regex_pattern = pattern
|
|
531
|
+
.gsub("**", "DOUBLE_STAR")
|
|
532
|
+
.gsub("*", "[^/]*")
|
|
533
|
+
.gsub("DOUBLE_STAR", ".*")
|
|
534
|
+
.gsub("?", ".")
|
|
535
|
+
|
|
536
|
+
Regexp.new("^#{regex_pattern}$")
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## Adapter Comparison
|
|
546
|
+
|
|
547
|
+
| Feature | FilesystemAdapter (built-in) | ActiveRecord (example) | Vector DB (custom) |
|
|
548
|
+
|---------|------------------------------|------------------------|-------------------|
|
|
549
|
+
| **Storage** | Local files | PostgreSQL/MySQL | Qdrant/Milvus/Weaviate |
|
|
550
|
+
| **Semantic Search** | In-memory cosine | pgvector extension | Native vector search |
|
|
551
|
+
| **Scalability** | ~5K entries | Hundreds of thousands | Millions of entries |
|
|
552
|
+
| **Setup** | Zero config | Rails + migration | External service |
|
|
553
|
+
| **Dependencies** | None | activerecord, pg/mysql2 | qdrant-ruby, etc. |
|
|
554
|
+
| **Performance** | Good (<5K entries) | Good (with indexes) | Excellent (any size) |
|
|
555
|
+
| **Cost** | Free | Managed DB (~$10/mo) | Self-hosted or cloud |
|
|
556
|
+
| **Availability** | Built-in | You implement | You implement |
|
|
557
|
+
|
|
558
|
+
> **Note**: Only FilesystemAdapter is built-in. All other adapters are examples you implement and register using `SwarmMemory.register_adapter`.
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## Testing Adapters
|
|
563
|
+
|
|
564
|
+
### Unit Tests
|
|
565
|
+
|
|
566
|
+
```ruby
|
|
567
|
+
class MyAdapterTest < Minitest::Test
|
|
568
|
+
def setup
|
|
569
|
+
@adapter = MyAdapter.new(...)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def test_write_and_read
|
|
573
|
+
entry = @adapter.write(
|
|
574
|
+
file_path: "test/entry.md",
|
|
575
|
+
content: "Test content",
|
|
576
|
+
title: "Test",
|
|
577
|
+
metadata: { "type" => "concept" }
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
assert_equal "Test content", @adapter.read(file_path: "test/entry.md")
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def test_semantic_search
|
|
584
|
+
# Write entries with embeddings
|
|
585
|
+
@adapter.write(
|
|
586
|
+
file_path: "test/entry1.md",
|
|
587
|
+
content: "Ruby classes",
|
|
588
|
+
title: "Classes",
|
|
589
|
+
embedding: [0.1, 0.2, ...], # 384-dim vector
|
|
590
|
+
metadata: { "type" => "concept" }
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
# Search
|
|
594
|
+
query_embedding = [0.1, 0.2, ...] # Similar vector
|
|
595
|
+
results = @adapter.semantic_search(
|
|
596
|
+
embedding: query_embedding,
|
|
597
|
+
top_k: 5,
|
|
598
|
+
threshold: 0.5
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
assert_equal 1, results.size
|
|
602
|
+
assert_equal "test/entry1.md", results.first[:path]
|
|
603
|
+
assert results.first[:similarity] > 0.5
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def test_glob_search
|
|
607
|
+
@adapter.write(file_path: "concept/ruby/classes.md", ...)
|
|
608
|
+
@adapter.write(file_path: "concept/ruby/modules.md", ...)
|
|
609
|
+
|
|
610
|
+
results = @adapter.glob(pattern: "concept/ruby/*")
|
|
611
|
+
|
|
612
|
+
assert_equal 2, results.size
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### Integration Tests
|
|
618
|
+
|
|
619
|
+
```ruby
|
|
620
|
+
def test_adapter_works_with_storage
|
|
621
|
+
adapter = MyAdapter.new(...)
|
|
622
|
+
embedder = SwarmMemory::Embeddings::InformersEmbedder.new
|
|
623
|
+
storage = SwarmMemory::Core::Storage.new(adapter: adapter, embedder: embedder)
|
|
624
|
+
|
|
625
|
+
# Test via Storage API
|
|
626
|
+
storage.write(
|
|
627
|
+
file_path: "test/entry.md",
|
|
628
|
+
content: "Test",
|
|
629
|
+
title: "Test"
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
content = storage.read(file_path: "test/entry.md")
|
|
633
|
+
assert_equal "Test", content
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def test_adapter_works_with_memory_tools
|
|
637
|
+
adapter = MyAdapter.new(...)
|
|
638
|
+
storage = SwarmMemory::Core::Storage.new(adapter: adapter)
|
|
639
|
+
|
|
640
|
+
tool = SwarmMemory::Tools::MemoryWrite.new(storage: storage, agent_name: :test)
|
|
641
|
+
|
|
642
|
+
result = tool.execute(
|
|
643
|
+
file_path: "test/entry.md",
|
|
644
|
+
content: "Content",
|
|
645
|
+
title: "Title",
|
|
646
|
+
type: "concept",
|
|
647
|
+
# ... all required params
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
assert_match /Stored at memory/, result
|
|
651
|
+
end
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## FilesystemAdapter Deep Dive
|
|
657
|
+
|
|
658
|
+
Study the reference implementation:
|
|
659
|
+
|
|
660
|
+
### File Structure
|
|
661
|
+
|
|
662
|
+
```
|
|
663
|
+
.swarm/memory/
|
|
664
|
+
├── concept--ruby--classes.md # Markdown content
|
|
665
|
+
├── concept--ruby--classes.yml # Metadata (YAML)
|
|
666
|
+
├── concept--ruby--classes.emb # Embedding (binary)
|
|
667
|
+
└── .lock # File lock
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
**Path Flattening:**
|
|
671
|
+
- Logical: `concept/ruby/classes.md`
|
|
672
|
+
- Disk: `concept--ruby--classes.md`
|
|
673
|
+
- Why: Git-friendly, avoids nested directories
|
|
674
|
+
|
|
675
|
+
### Key Implementation Details
|
|
676
|
+
|
|
677
|
+
```ruby
|
|
678
|
+
class FilesystemAdapter < Base
|
|
679
|
+
def initialize(directory:)
|
|
680
|
+
@directory = File.expand_path(directory)
|
|
681
|
+
@semaphore = Async::Semaphore.new(1) # Fiber-safe locking
|
|
682
|
+
@lock_file_path = File.join(@directory, ".lock")
|
|
683
|
+
@index = build_index # In-memory index for fast lookups
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def write(file_path:, content:, title:, embedding: nil, metadata: nil)
|
|
687
|
+
with_write_lock do
|
|
688
|
+
@semaphore.acquire do
|
|
689
|
+
# Flatten path for disk storage
|
|
690
|
+
disk_path = flatten_path(file_path)
|
|
691
|
+
|
|
692
|
+
# Write content (.md file)
|
|
693
|
+
File.write(File.join(@directory, "#{disk_path}.md"), content)
|
|
694
|
+
|
|
695
|
+
# Write metadata (.yml file)
|
|
696
|
+
yaml_data = {
|
|
697
|
+
title: title,
|
|
698
|
+
file_path: file_path,
|
|
699
|
+
updated_at: Time.now,
|
|
700
|
+
size: content.bytesize,
|
|
701
|
+
metadata: metadata,
|
|
702
|
+
embedding_checksum: embedding ? checksum(embedding) : nil
|
|
703
|
+
}
|
|
704
|
+
File.write(File.join(@directory, "#{disk_path}.yml"), YAML.dump(yaml_data))
|
|
705
|
+
|
|
706
|
+
# Write embedding (.emb file, binary)
|
|
707
|
+
if embedding
|
|
708
|
+
File.write(File.join(@directory, "#{disk_path}.emb"), embedding.pack("f*"))
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# Update in-memory index
|
|
712
|
+
@index[file_path] = {...}
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Cross-process file locking
|
|
718
|
+
def with_write_lock
|
|
719
|
+
File.open(@lock_file_path, File::RDWR | File::CREAT) do |lock_file|
|
|
720
|
+
lock_file.flock(File::LOCK_EX) # Exclusive lock
|
|
721
|
+
yield
|
|
722
|
+
ensure
|
|
723
|
+
lock_file.flock(File::LOCK_UN) # Release
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
**Optimizations:**
|
|
730
|
+
- In-memory index for fast lookups
|
|
731
|
+
- File locking for concurrent access
|
|
732
|
+
- Binary embeddings (not JSON)
|
|
733
|
+
- Lazy loading (index built on init)
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
## Vector Database Adapters
|
|
738
|
+
|
|
739
|
+
### Qdrant Example (Production-Ready)
|
|
740
|
+
|
|
741
|
+
See full example above in "Example: QdrantAdapter" section.
|
|
742
|
+
|
|
743
|
+
**Benefits:**
|
|
744
|
+
- Native vector search (faster, more scalable)
|
|
745
|
+
- Built-in similarity algorithms
|
|
746
|
+
- Filtering by metadata
|
|
747
|
+
- Horizontal scaling
|
|
748
|
+
|
|
749
|
+
**Trade-offs:**
|
|
750
|
+
- Requires external service
|
|
751
|
+
- More complex setup
|
|
752
|
+
- Additional dependency
|
|
753
|
+
|
|
754
|
+
### Milvus Adapter
|
|
755
|
+
|
|
756
|
+
```ruby
|
|
757
|
+
class MilvusAdapter < Base
|
|
758
|
+
def initialize(host:, port:, collection:)
|
|
759
|
+
@client = Milvus::Client.new(host: host, port: port)
|
|
760
|
+
@collection = collection
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def semantic_search(embedding:, top_k:, threshold:)
|
|
764
|
+
@client.search(
|
|
765
|
+
collection_name: @collection,
|
|
766
|
+
vectors: [embedding],
|
|
767
|
+
top_k: top_k,
|
|
768
|
+
params: { nprobe: 10 }
|
|
769
|
+
).map do |result|
|
|
770
|
+
{
|
|
771
|
+
path: result.id,
|
|
772
|
+
similarity: result.distance,
|
|
773
|
+
# ... map other fields
|
|
774
|
+
}
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
---
|
|
781
|
+
|
|
782
|
+
## Relational Database Adapters
|
|
783
|
+
|
|
784
|
+
### PostgreSQL with pgvector
|
|
785
|
+
|
|
786
|
+
```ruby
|
|
787
|
+
class PostgresAdapter < Base
|
|
788
|
+
def initialize(connection_string:)
|
|
789
|
+
@conn = PG.connect(connection_string)
|
|
790
|
+
|
|
791
|
+
# Ensure pgvector extension and table exist
|
|
792
|
+
@conn.exec("CREATE EXTENSION IF NOT EXISTS vector")
|
|
793
|
+
@conn.exec(<<~SQL)
|
|
794
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
795
|
+
file_path TEXT PRIMARY KEY,
|
|
796
|
+
content TEXT NOT NULL,
|
|
797
|
+
title TEXT NOT NULL,
|
|
798
|
+
embedding vector(384),
|
|
799
|
+
metadata JSONB,
|
|
800
|
+
updated_at TIMESTAMP DEFAULT NOW()
|
|
801
|
+
)
|
|
802
|
+
SQL
|
|
803
|
+
|
|
804
|
+
# Create index for vector similarity search
|
|
805
|
+
@conn.exec("CREATE INDEX IF NOT EXISTS memories_embedding_idx ON memories USING ivfflat (embedding vector_cosine_ops)")
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
def write(file_path:, content:, title:, embedding: nil, metadata: nil)
|
|
809
|
+
@conn.exec_params(
|
|
810
|
+
"INSERT INTO memories (file_path, content, title, embedding, metadata, updated_at)
|
|
811
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
812
|
+
ON CONFLICT (file_path) DO UPDATE
|
|
813
|
+
SET content = $2, title = $3, embedding = $4, metadata = $5, updated_at = $6",
|
|
814
|
+
[file_path, content, title, embedding&.to_s, metadata.to_json, Time.now]
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
# Return entry
|
|
818
|
+
Core::Entry.new(...)
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
def semantic_search(embedding:, top_k:, threshold:)
|
|
822
|
+
# pgvector cosine similarity
|
|
823
|
+
result = @conn.exec_params(
|
|
824
|
+
"SELECT file_path, title, metadata,
|
|
825
|
+
1 - (embedding <=> $1::vector) AS similarity
|
|
826
|
+
FROM memories
|
|
827
|
+
WHERE (1 - (embedding <=> $1::vector)) >= $2
|
|
828
|
+
ORDER BY embedding <=> $1::vector
|
|
829
|
+
LIMIT $3",
|
|
830
|
+
[embedding.to_s, threshold, top_k]
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
result.map do |row|
|
|
834
|
+
{
|
|
835
|
+
path: row["file_path"],
|
|
836
|
+
similarity: row["similarity"].to_f,
|
|
837
|
+
title: row["title"],
|
|
838
|
+
metadata: JSON.parse(row["metadata"])
|
|
839
|
+
}
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def glob(pattern:)
|
|
844
|
+
# Convert glob to SQL LIKE pattern
|
|
845
|
+
like_pattern = pattern.gsub("**", "%").gsub("*", "%").gsub("?", "_")
|
|
846
|
+
|
|
847
|
+
result = @conn.exec_params(
|
|
848
|
+
"SELECT file_path, title, LENGTH(content) as size, updated_at
|
|
849
|
+
FROM memories
|
|
850
|
+
WHERE file_path LIKE $1",
|
|
851
|
+
[like_pattern]
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
result.map do |row|
|
|
855
|
+
{
|
|
856
|
+
path: row["file_path"],
|
|
857
|
+
title: row["title"],
|
|
858
|
+
size: row["size"].to_i,
|
|
859
|
+
updated_at: Time.parse(row["updated_at"])
|
|
860
|
+
}
|
|
861
|
+
end
|
|
862
|
+
end
|
|
863
|
+
end
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
---
|
|
867
|
+
|
|
868
|
+
## Adapter Checklist
|
|
869
|
+
|
|
870
|
+
When building an adapter, ensure:
|
|
871
|
+
|
|
872
|
+
### Functional Requirements
|
|
873
|
+
|
|
874
|
+
- [ ] All 14 required methods implemented
|
|
875
|
+
- [ ] Raises `ArgumentError` when entry not found
|
|
876
|
+
- [ ] Returns `Core::Entry` objects from read_entry
|
|
877
|
+
- [ ] Handles nil embeddings gracefully
|
|
878
|
+
- [ ] Handles nil metadata gracefully
|
|
879
|
+
- [ ] Supports prefix filtering in list()
|
|
880
|
+
- [ ] Glob patterns work correctly
|
|
881
|
+
- [ ] Grep supports all 3 output modes
|
|
882
|
+
- [ ] semantic_search returns sorted by similarity (descending)
|
|
883
|
+
|
|
884
|
+
### Performance Requirements
|
|
885
|
+
|
|
886
|
+
- [ ] Lookups use indexes (not full scans)
|
|
887
|
+
- [ ] Writes are atomic (no partial updates)
|
|
888
|
+
- [ ] Concurrent access handled safely
|
|
889
|
+
- [ ] Embeddings stored efficiently (binary, not JSON)
|
|
890
|
+
- [ ] list() is paginated or limited (for large datasets)
|
|
891
|
+
|
|
892
|
+
### Quality Requirements
|
|
893
|
+
|
|
894
|
+
- [ ] Thread-safe (or document as single-threaded only)
|
|
895
|
+
- [ ] Fiber-safe (if using Async)
|
|
896
|
+
- [ ] Errors have helpful messages
|
|
897
|
+
- [ ] Cleanup on adapter destruction
|
|
898
|
+
- [ ] Configuration validated on init
|
|
899
|
+
- [ ] Total size tracking (if possible)
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
## Using Custom Adapters
|
|
904
|
+
|
|
905
|
+
### Step 1: Implement Your Adapter
|
|
906
|
+
|
|
907
|
+
```ruby
|
|
908
|
+
# lib/my_custom_adapter.rb
|
|
909
|
+
class MyCustomAdapter < SwarmMemory::Adapters::Base
|
|
910
|
+
def initialize(url:, api_key: nil, **options)
|
|
911
|
+
super()
|
|
912
|
+
# Your initialization code
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
# Implement all required methods...
|
|
916
|
+
# See Adapters::Base for full interface
|
|
917
|
+
end
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
### Step 2: Register Your Adapter
|
|
921
|
+
|
|
922
|
+
```ruby
|
|
923
|
+
# config/initializers/swarm_memory.rb (Rails)
|
|
924
|
+
# or at the top of your swarm.rb file
|
|
925
|
+
|
|
926
|
+
require_relative 'lib/my_custom_adapter'
|
|
927
|
+
|
|
928
|
+
SwarmMemory.register_adapter(:my_adapter, MyCustomAdapter)
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
### Step 3: Configure Agent to Use It
|
|
932
|
+
|
|
933
|
+
```ruby
|
|
934
|
+
# swarm.rb
|
|
935
|
+
agent :assistant do
|
|
936
|
+
memory do
|
|
937
|
+
adapter :my_adapter # Uses registered adapter
|
|
938
|
+
option :url, "http://localhost:8080"
|
|
939
|
+
option :api_key, ENV["API_KEY"]
|
|
940
|
+
end
|
|
941
|
+
end
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
### With Storage Directly (Without SwarmSDK)
|
|
945
|
+
|
|
946
|
+
```ruby
|
|
947
|
+
require 'swarm_memory'
|
|
948
|
+
|
|
949
|
+
# Create your adapter
|
|
950
|
+
adapter = MyCustomAdapter.new(url: "http://localhost:6333")
|
|
951
|
+
|
|
952
|
+
# Create storage with embedder
|
|
953
|
+
embedder = SwarmMemory::Embeddings::InformersEmbedder.new
|
|
954
|
+
storage = SwarmMemory::Core::Storage.new(adapter: adapter, embedder: embedder)
|
|
955
|
+
|
|
956
|
+
# Use memory tools directly
|
|
957
|
+
tools = SwarmMemory.tools_for(storage: storage, agent_name: :test)
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
## Publishing Your Adapter
|
|
963
|
+
|
|
964
|
+
Consider publishing your adapter as a gem for community use:
|
|
965
|
+
|
|
966
|
+
```ruby
|
|
967
|
+
# my_adapter_gem.gemspec
|
|
968
|
+
Gem::Specification.new do |spec|
|
|
969
|
+
spec.name = "swarm_memory_my_adapter"
|
|
970
|
+
spec.version = "1.0.0"
|
|
971
|
+
spec.summary = "MyAdapter for SwarmMemory"
|
|
972
|
+
|
|
973
|
+
spec.add_dependency "swarm_memory", "~> 1.0"
|
|
974
|
+
spec.add_dependency "my_database_client", "~> 2.0"
|
|
975
|
+
end
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
Users can then:
|
|
979
|
+
```bash
|
|
980
|
+
gem install swarm_memory_my_adapter
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
```ruby
|
|
984
|
+
# In their app
|
|
985
|
+
require 'swarm_memory_my_adapter'
|
|
986
|
+
SwarmMemory.register_adapter(:my_adapter, MyCustomAdapter)
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
---
|
|
990
|
+
|
|
991
|
+
## See Also
|
|
992
|
+
|
|
993
|
+
- **Complete ActiveRecord Example:** [docs/v2/swarm_memory_adapters.md](../swarm_memory_adapters.md)
|
|
994
|
+
- **Base Class:** `lib/swarm_memory/adapters/base.rb` - Interface definition
|
|
995
|
+
- **Reference Implementation:** `lib/swarm_memory/adapters/filesystem_adapter.rb`
|
|
996
|
+
- **Entry Class:** `lib/swarm_memory/core/entry.rb` - Entry object spec
|
|
997
|
+
- **Storage Class:** `lib/swarm_memory/core/storage.rb` - Storage orchestration
|
|
998
|
+
- **Registry API:** `lib/swarm_memory.rb` - register_adapter, adapter_for, available_adapters
|