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,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
|