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,382 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmMemory
|
|
4
|
+
module Tools
|
|
5
|
+
# Tool for analyzing and optimizing memory storage
|
|
6
|
+
#
|
|
7
|
+
# Provides defragmentation operations to maintain memory quality.
|
|
8
|
+
# Each agent has its own isolated memory storage.
|
|
9
|
+
class MemoryDefrag < RubyLLM::Tool
|
|
10
|
+
description <<~DESC
|
|
11
|
+
Analyze and optimize your memory storage for better precision, recall, and organization.
|
|
12
|
+
|
|
13
|
+
THINK BEFORE CALLING: This tool has many actions and parameters. Choose the right action for your goal.
|
|
14
|
+
|
|
15
|
+
**When to Run Defrag:**
|
|
16
|
+
- Every 15-20 new memory entries created
|
|
17
|
+
- Memory searches returning too many irrelevant results
|
|
18
|
+
- Trouble finding specific information
|
|
19
|
+
- Before major new tasks (check memory health)
|
|
20
|
+
- Periodically as maintenance (every ~50 entries minimum)
|
|
21
|
+
|
|
22
|
+
## READ-ONLY ANALYSIS ACTIONS (Safe - No Changes)
|
|
23
|
+
|
|
24
|
+
**1. analyze** - Get overall memory health report
|
|
25
|
+
```
|
|
26
|
+
MemoryDefrag(action: "analyze")
|
|
27
|
+
```
|
|
28
|
+
- Shows entry counts, quality scores, metadata coverage
|
|
29
|
+
- Provides health score (0-100) - aim for 70+
|
|
30
|
+
- ALWAYS run this first to understand memory state
|
|
31
|
+
- No parameters needed
|
|
32
|
+
|
|
33
|
+
**2. find_duplicates** - Identify similar/duplicate entries
|
|
34
|
+
```
|
|
35
|
+
MemoryDefrag(action: "find_duplicates", similarity_threshold: 0.85)
|
|
36
|
+
```
|
|
37
|
+
- Uses text and semantic similarity to find near-duplicates
|
|
38
|
+
- similarity_threshold: 0.0-1.0 (default: 0.85) - higher = more strict
|
|
39
|
+
- Start with 0.85, adjust based on results
|
|
40
|
+
- Helps identify entries to merge
|
|
41
|
+
|
|
42
|
+
**3. find_low_quality** - Find entries with poor metadata
|
|
43
|
+
```
|
|
44
|
+
MemoryDefrag(action: "find_low_quality", confidence_filter: "low")
|
|
45
|
+
```
|
|
46
|
+
- confidence_filter: 'low', 'medium', or 'high' (default: 'low')
|
|
47
|
+
- Identifies entries missing metadata, tags, or with low confidence
|
|
48
|
+
- Helps identify entries to improve or delete
|
|
49
|
+
|
|
50
|
+
**4. find_archival_candidates** - Find old, unused entries
|
|
51
|
+
```
|
|
52
|
+
MemoryDefrag(action: "find_archival_candidates", age_days: 90)
|
|
53
|
+
```
|
|
54
|
+
- age_days: Threshold in days (default: 90)
|
|
55
|
+
- Lists entries not updated in N days
|
|
56
|
+
- Candidates for deletion or archival
|
|
57
|
+
|
|
58
|
+
**5. find_related** - Discover entries that should be cross-linked
|
|
59
|
+
```
|
|
60
|
+
MemoryDefrag(action: "find_related", min_similarity: 0.60, max_similarity: 0.85)
|
|
61
|
+
```
|
|
62
|
+
- Finds pairs with 60-85% semantic similarity (related but not duplicates)
|
|
63
|
+
- Shows current linking status (unlinked, one-way, bidirectional)
|
|
64
|
+
- Suggests which links to create
|
|
65
|
+
- Pure semantic similarity (no keyword boost) - finds content relationships
|
|
66
|
+
|
|
67
|
+
## ACTIVE OPTIMIZATION ACTIONS (Modify Memory)
|
|
68
|
+
|
|
69
|
+
**IMPORTANT:** All active operations default to dry_run=true for safety. Set dry_run=false to actually perform changes.
|
|
70
|
+
|
|
71
|
+
**6. link_related** - Create bidirectional links between related entries
|
|
72
|
+
```
|
|
73
|
+
# Preview first
|
|
74
|
+
MemoryDefrag(action: "link_related", min_similarity: 0.60, max_similarity: 0.85, dry_run: true)
|
|
75
|
+
|
|
76
|
+
# Execute after reviewing
|
|
77
|
+
MemoryDefrag(action: "link_related", min_similarity: 0.60, max_similarity: 0.85, dry_run: false)
|
|
78
|
+
```
|
|
79
|
+
Parameters:
|
|
80
|
+
- min_similarity: 0.0-1.0 (default: 0.60) - minimum relationship threshold
|
|
81
|
+
- max_similarity: 0.0-1.0 (default: 0.85) - maximum (above = duplicates)
|
|
82
|
+
- dry_run: true (preview) or false (execute)
|
|
83
|
+
|
|
84
|
+
What it does:
|
|
85
|
+
- Finds related but not duplicate entries (60-85% similarity)
|
|
86
|
+
- Creates bidirectional links in 'related' metadata
|
|
87
|
+
- Skips already-linked pairs
|
|
88
|
+
- Builds your knowledge graph automatically
|
|
89
|
+
|
|
90
|
+
**7. merge_duplicates** - Merge similar entries
|
|
91
|
+
```
|
|
92
|
+
# Preview first (dry_run defaults to true)
|
|
93
|
+
MemoryDefrag(action: "merge_duplicates", similarity_threshold: 0.85, dry_run: true)
|
|
94
|
+
|
|
95
|
+
# Execute after reviewing preview
|
|
96
|
+
MemoryDefrag(action: "merge_duplicates", similarity_threshold: 0.85, dry_run: false)
|
|
97
|
+
```
|
|
98
|
+
Parameters:
|
|
99
|
+
- similarity_threshold: 0.0-1.0 (default: 0.85) - matching threshold
|
|
100
|
+
- merge_strategy: 'keep_newer', 'keep_larger', 'combine' (default: 'keep_newer')
|
|
101
|
+
- dry_run: true (preview) or false (execute)
|
|
102
|
+
|
|
103
|
+
What it does:
|
|
104
|
+
- Merges similar entries to reduce duplication
|
|
105
|
+
- Creates stub files with auto-redirect to merged entry
|
|
106
|
+
- Preserves access to old paths via redirects
|
|
107
|
+
|
|
108
|
+
**8. cleanup_stubs** - Remove old redirect stub files
|
|
109
|
+
```
|
|
110
|
+
MemoryDefrag(action: "cleanup_stubs", age_days: 30, max_hits: 3, dry_run: false)
|
|
111
|
+
```
|
|
112
|
+
Parameters:
|
|
113
|
+
- age_days: Minimum age to delete (default: 90)
|
|
114
|
+
- max_hits: Maximum access count to delete (default: 10)
|
|
115
|
+
- dry_run: true (preview) or false (execute)
|
|
116
|
+
|
|
117
|
+
What it does:
|
|
118
|
+
- Deletes stub files that are old AND rarely accessed
|
|
119
|
+
- Keeps frequently-accessed stubs even if old
|
|
120
|
+
- Reduces clutter from obsolete redirects
|
|
121
|
+
|
|
122
|
+
**9. compact** - Delete low-value entries
|
|
123
|
+
```
|
|
124
|
+
MemoryDefrag(action: "compact", min_quality_score: 20, min_age_days: 30, max_hits: 0, dry_run: false)
|
|
125
|
+
```
|
|
126
|
+
Parameters:
|
|
127
|
+
- min_quality_score: Minimum quality threshold (default: 20)
|
|
128
|
+
- min_age_days: Minimum age in days (default: 30)
|
|
129
|
+
- max_hits: Maximum access count (default: 10)
|
|
130
|
+
- dry_run: true (preview) or false (execute)
|
|
131
|
+
|
|
132
|
+
What it does:
|
|
133
|
+
- Permanently deletes entries matching ALL criteria
|
|
134
|
+
- Targets low-quality, old, unused entries
|
|
135
|
+
- Frees up memory space
|
|
136
|
+
|
|
137
|
+
**10. full** - Complete optimization workflow
|
|
138
|
+
```
|
|
139
|
+
# Preview full optimization
|
|
140
|
+
MemoryDefrag(action: "full", dry_run: true)
|
|
141
|
+
|
|
142
|
+
# Execute full optimization
|
|
143
|
+
MemoryDefrag(action: "full", dry_run: false)
|
|
144
|
+
```
|
|
145
|
+
What it does:
|
|
146
|
+
- Runs: merge_duplicates → cleanup_stubs → compact
|
|
147
|
+
- Shows health score improvement (before/after)
|
|
148
|
+
- Most comprehensive optimization
|
|
149
|
+
- ALWAYS preview first!
|
|
150
|
+
- Does NOT include link_related (run separately if desired)
|
|
151
|
+
|
|
152
|
+
## BEST PRACTICES
|
|
153
|
+
|
|
154
|
+
**1. Always Preview First:**
|
|
155
|
+
- Use dry_run=true to see what would happen
|
|
156
|
+
- Review the preview carefully
|
|
157
|
+
- Only proceed if changes look correct
|
|
158
|
+
|
|
159
|
+
**2. Start with Analysis:**
|
|
160
|
+
```
|
|
161
|
+
# Step 1: Check health
|
|
162
|
+
MemoryDefrag(action: "analyze")
|
|
163
|
+
|
|
164
|
+
# Step 2: Find issues
|
|
165
|
+
MemoryDefrag(action: "find_duplicates")
|
|
166
|
+
MemoryDefrag(action: "find_low_quality")
|
|
167
|
+
|
|
168
|
+
# Step 3: Preview fixes
|
|
169
|
+
MemoryDefrag(action: "merge_duplicates", dry_run: true)
|
|
170
|
+
|
|
171
|
+
# Step 4: Execute if preview looks good
|
|
172
|
+
MemoryDefrag(action: "merge_duplicates", dry_run: false)
|
|
173
|
+
|
|
174
|
+
# Step 5: Verify improvement
|
|
175
|
+
MemoryDefrag(action: "analyze")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**3. Conservative Thresholds:**
|
|
179
|
+
- similarity_threshold: Start at 0.85 or higher
|
|
180
|
+
- age_days: Start at 90+ days (don't delete recent entries)
|
|
181
|
+
- min_quality_score: Start at 20 or lower (only worst entries)
|
|
182
|
+
|
|
183
|
+
**4. Regular Maintenance Schedule:**
|
|
184
|
+
- Light check: Every 15-20 new entries (just analyze)
|
|
185
|
+
- Medium check: Every 50 entries (analyze + find_*)
|
|
186
|
+
- Heavy maintenance: Every 100 entries (full optimization)
|
|
187
|
+
|
|
188
|
+
**5. Safety Checklist:**
|
|
189
|
+
- ✓ Preview with dry_run=true first
|
|
190
|
+
- ✓ Review what will be changed
|
|
191
|
+
- ✓ Use conservative thresholds initially
|
|
192
|
+
- ✓ Re-analyze after to verify improvement
|
|
193
|
+
- ✓ Don't delete recent or frequently-accessed entries
|
|
194
|
+
|
|
195
|
+
## PARAMETER REFERENCE
|
|
196
|
+
|
|
197
|
+
All parameters are optional and have sensible defaults:
|
|
198
|
+
|
|
199
|
+
- action: 'analyze', 'find_duplicates', 'find_low_quality', 'find_archival_candidates', 'find_related', 'link_related', 'merge_duplicates', 'cleanup_stubs', 'compact', 'full' (default: 'analyze')
|
|
200
|
+
- dry_run: true (preview) or false (execute) - default: true for safety
|
|
201
|
+
- similarity_threshold: 0.0-1.0 (default: 0.85) - for merge_duplicates
|
|
202
|
+
- min_similarity: 0.0-1.0 (default: 0.60) - for find_related/link_related
|
|
203
|
+
- max_similarity: 0.0-1.0 (default: 0.85) - for find_related/link_related
|
|
204
|
+
- merge_strategy: 'keep_newer', 'keep_larger', 'combine' (default: 'keep_newer')
|
|
205
|
+
- age_days: Days threshold (default: 90)
|
|
206
|
+
- max_hits: Access count threshold (default: 10)
|
|
207
|
+
- min_quality_score: Quality threshold (default: 20)
|
|
208
|
+
- confidence_filter: 'low', 'medium', 'high' (default: 'low')
|
|
209
|
+
DESC
|
|
210
|
+
|
|
211
|
+
param :action,
|
|
212
|
+
desc: "Action: 'analyze', 'find_duplicates', 'find_low_quality', 'find_archival_candidates', 'find_related', 'link_related', 'merge_duplicates', 'cleanup_stubs', 'compact', 'full' (default: 'analyze')",
|
|
213
|
+
required: false
|
|
214
|
+
|
|
215
|
+
param :dry_run,
|
|
216
|
+
desc: "Preview mode - show what would be done without doing it (default: true for safety)",
|
|
217
|
+
required: false
|
|
218
|
+
|
|
219
|
+
param :similarity_threshold,
|
|
220
|
+
desc: "Similarity threshold for duplicate detection 0.0-1.0 (default: 0.85)",
|
|
221
|
+
required: false
|
|
222
|
+
|
|
223
|
+
param :min_similarity,
|
|
224
|
+
desc: "Minimum similarity for find_related/link_related 0.0-1.0 (default: 0.60)",
|
|
225
|
+
required: false
|
|
226
|
+
|
|
227
|
+
param :max_similarity,
|
|
228
|
+
desc: "Maximum similarity for find_related/link_related 0.0-1.0 (default: 0.85)",
|
|
229
|
+
required: false
|
|
230
|
+
|
|
231
|
+
param :merge_strategy,
|
|
232
|
+
desc: "Merge strategy: 'keep_newer', 'keep_larger', 'combine' (default: 'keep_newer')",
|
|
233
|
+
required: false
|
|
234
|
+
|
|
235
|
+
param :age_days,
|
|
236
|
+
desc: "Age threshold for archival/cleanup in days (default: 90)",
|
|
237
|
+
required: false
|
|
238
|
+
|
|
239
|
+
param :max_hits,
|
|
240
|
+
desc: "Maximum hits threshold for cleanup/archive (default: 10)",
|
|
241
|
+
required: false
|
|
242
|
+
|
|
243
|
+
param :min_quality_score,
|
|
244
|
+
desc: "Minimum quality score for compact (default: 20)",
|
|
245
|
+
required: false
|
|
246
|
+
|
|
247
|
+
param :confidence_filter,
|
|
248
|
+
desc: "Confidence level to filter: 'low', 'medium', 'high' (default: 'low')",
|
|
249
|
+
required: false
|
|
250
|
+
|
|
251
|
+
# Initialize with storage instance
|
|
252
|
+
#
|
|
253
|
+
# @param storage [Core::Storage] Storage instance
|
|
254
|
+
def initialize(storage:)
|
|
255
|
+
super()
|
|
256
|
+
@storage = storage
|
|
257
|
+
@defragmenter = nil # Lazy load
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Override name to return simple "MemoryDefrag"
|
|
261
|
+
def name
|
|
262
|
+
"MemoryDefrag"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Execute the tool
|
|
266
|
+
#
|
|
267
|
+
# @param action [String] Action to perform
|
|
268
|
+
# @param dry_run [Boolean] Preview mode (default: true)
|
|
269
|
+
# @param similarity_threshold [Float] Duplicate detection threshold
|
|
270
|
+
# @param min_similarity [Float] Minimum for relationship detection
|
|
271
|
+
# @param max_similarity [Float] Maximum for relationship detection
|
|
272
|
+
# @param merge_strategy [String] Merge strategy
|
|
273
|
+
# @param age_days [Integer] Age threshold
|
|
274
|
+
# @param max_hits [Integer] Maximum hits threshold
|
|
275
|
+
# @param min_quality_score [Integer] Minimum quality score
|
|
276
|
+
# @param confidence_filter [String] Confidence level filter
|
|
277
|
+
# @return [String] Operation report
|
|
278
|
+
def execute(
|
|
279
|
+
action: "analyze",
|
|
280
|
+
dry_run: true,
|
|
281
|
+
similarity_threshold: 0.85,
|
|
282
|
+
min_similarity: 0.60,
|
|
283
|
+
max_similarity: 0.85,
|
|
284
|
+
merge_strategy: "keep_newer",
|
|
285
|
+
age_days: 90,
|
|
286
|
+
max_hits: 10,
|
|
287
|
+
min_quality_score: 20,
|
|
288
|
+
confidence_filter: "low"
|
|
289
|
+
)
|
|
290
|
+
ensure_defragmenter_loaded
|
|
291
|
+
|
|
292
|
+
case action.to_s.downcase
|
|
293
|
+
# Read-only operations
|
|
294
|
+
when "analyze"
|
|
295
|
+
@defragmenter.health_report
|
|
296
|
+
when "find_duplicates"
|
|
297
|
+
@defragmenter.find_duplicates_report(threshold: similarity_threshold.to_f)
|
|
298
|
+
when "find_low_quality"
|
|
299
|
+
@defragmenter.find_low_quality_report(confidence_filter: confidence_filter.to_s.downcase)
|
|
300
|
+
when "find_archival_candidates"
|
|
301
|
+
@defragmenter.find_archival_candidates_report(age_days: age_days.to_i)
|
|
302
|
+
when "find_related"
|
|
303
|
+
@defragmenter.find_related_report(min_threshold: min_similarity.to_f, max_threshold: max_similarity.to_f)
|
|
304
|
+
|
|
305
|
+
# Active operations (modify memory)
|
|
306
|
+
when "link_related"
|
|
307
|
+
@defragmenter.link_related_active(
|
|
308
|
+
min_threshold: min_similarity.to_f,
|
|
309
|
+
max_threshold: max_similarity.to_f,
|
|
310
|
+
dry_run: dry_run,
|
|
311
|
+
)
|
|
312
|
+
when "merge_duplicates"
|
|
313
|
+
@defragmenter.merge_duplicates_active(
|
|
314
|
+
threshold: similarity_threshold.to_f,
|
|
315
|
+
strategy: merge_strategy.to_sym,
|
|
316
|
+
dry_run: dry_run,
|
|
317
|
+
)
|
|
318
|
+
when "cleanup_stubs"
|
|
319
|
+
@defragmenter.cleanup_stubs_active(
|
|
320
|
+
min_age_days: age_days.to_i,
|
|
321
|
+
max_hits: max_hits.to_i,
|
|
322
|
+
dry_run: dry_run,
|
|
323
|
+
)
|
|
324
|
+
when "compact"
|
|
325
|
+
@defragmenter.compact_active(
|
|
326
|
+
min_quality_score: min_quality_score.to_i,
|
|
327
|
+
min_age_days: age_days.to_i,
|
|
328
|
+
max_hits: max_hits.to_i,
|
|
329
|
+
dry_run: dry_run,
|
|
330
|
+
)
|
|
331
|
+
when "full"
|
|
332
|
+
# Full can be read-only analysis OR active optimization
|
|
333
|
+
if dry_run
|
|
334
|
+
# Just analysis
|
|
335
|
+
@defragmenter.full_analysis(
|
|
336
|
+
similarity_threshold: similarity_threshold.to_f,
|
|
337
|
+
age_days: age_days.to_i,
|
|
338
|
+
confidence_filter: confidence_filter.to_s.downcase,
|
|
339
|
+
)
|
|
340
|
+
else
|
|
341
|
+
# Active optimization
|
|
342
|
+
@defragmenter.full_optimization(dry_run: dry_run)
|
|
343
|
+
end
|
|
344
|
+
else
|
|
345
|
+
validation_error("Invalid action: #{action}. Must be one of: analyze, find_duplicates, find_low_quality, find_archival_candidates, find_related, link_related, merge_duplicates, cleanup_stubs, compact, full")
|
|
346
|
+
end
|
|
347
|
+
rescue ArgumentError => e
|
|
348
|
+
validation_error(e.message)
|
|
349
|
+
rescue StandardError => e
|
|
350
|
+
# Provide detailed error information
|
|
351
|
+
error_msg = "Defrag error: #{e.class.name} - #{e.message}\n\n"
|
|
352
|
+
error_msg += "Backtrace:\n"
|
|
353
|
+
error_msg += e.backtrace.first(10).join("\n")
|
|
354
|
+
validation_error(error_msg)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
private
|
|
358
|
+
|
|
359
|
+
def validation_error(message)
|
|
360
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Lazy load defragmenter with embedder if available
|
|
364
|
+
#
|
|
365
|
+
# @return [void]
|
|
366
|
+
def ensure_defragmenter_loaded
|
|
367
|
+
return if @defragmenter
|
|
368
|
+
|
|
369
|
+
# Don't load embedder automatically - it can be slow (model download)
|
|
370
|
+
# Defrag works fine without embeddings (uses text similarity instead)
|
|
371
|
+
embedder = nil
|
|
372
|
+
|
|
373
|
+
@defragmenter = Optimization::Defragmenter.new(
|
|
374
|
+
adapter: @storage.adapter,
|
|
375
|
+
embedder: embedder,
|
|
376
|
+
)
|
|
377
|
+
rescue StandardError => e
|
|
378
|
+
raise EmbeddingError, "Failed to initialize defragmenter: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmMemory
|
|
4
|
+
module Tools
|
|
5
|
+
# Tool for deleting content from memory storage
|
|
6
|
+
#
|
|
7
|
+
# Removes entries that are no longer relevant.
|
|
8
|
+
# Each agent has its own isolated memory storage.
|
|
9
|
+
class MemoryDelete < RubyLLM::Tool
|
|
10
|
+
description <<~DESC
|
|
11
|
+
Delete entries from your memory storage when they're no longer needed.
|
|
12
|
+
|
|
13
|
+
REQUIRED: Provide the file_path parameter - the path to the entry you want to delete.
|
|
14
|
+
|
|
15
|
+
**Parameters:**
|
|
16
|
+
- file_path (REQUIRED): Path to memory entry - MUST start with concept/, fact/, skill/, or experience/
|
|
17
|
+
|
|
18
|
+
**MEMORY STRUCTURE (4 Fixed Categories Only):**
|
|
19
|
+
- concept/{domain}/{name}.md - Abstract ideas
|
|
20
|
+
- fact/{subfolder}/{name}.md - Concrete information
|
|
21
|
+
- skill/{domain}/{name}.md - Procedures
|
|
22
|
+
- experience/{name}.md - Lessons
|
|
23
|
+
INVALID: documentation/, reference/, analysis/, parallel/, temp/, notes/
|
|
24
|
+
|
|
25
|
+
**When to Delete:**
|
|
26
|
+
- Outdated information that's been superseded
|
|
27
|
+
- Completed tasks that are no longer relevant
|
|
28
|
+
- Duplicate entries (after consolidating)
|
|
29
|
+
- Test data or temporary calculations
|
|
30
|
+
- Low-quality entries with minimal value
|
|
31
|
+
|
|
32
|
+
**IMPORTANT WARNINGS:**
|
|
33
|
+
- Deletion is PERMANENT - content cannot be recovered
|
|
34
|
+
- Think carefully before deleting - consider if it might be useful later
|
|
35
|
+
- Use MemoryDefrag to identify candidates for deletion (find_archival_candidates, compact)
|
|
36
|
+
- Consider reading the entry first to verify you're deleting the right thing
|
|
37
|
+
|
|
38
|
+
**Examples:**
|
|
39
|
+
```
|
|
40
|
+
# Delete outdated concept
|
|
41
|
+
MemoryDelete(file_path: "concept/old-api/deprecated.md")
|
|
42
|
+
|
|
43
|
+
# Delete completed experience
|
|
44
|
+
MemoryDelete(file_path: "experience/temp-experiment.md")
|
|
45
|
+
|
|
46
|
+
# Delete obsolete fact
|
|
47
|
+
MemoryDelete(file_path: "fact/orgs/defunct-company.md")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Best Practices:**
|
|
51
|
+
1. Read entry first with MemoryRead to confirm it's the right one
|
|
52
|
+
2. Use MemoryDefrag(action: "find_archival_candidates") to find old, unused entries
|
|
53
|
+
3. Delete in batches during memory maintenance sessions
|
|
54
|
+
4. Keep entries that might provide historical context
|
|
55
|
+
5. Don't delete skills unless you're certain they won't be needed
|
|
56
|
+
|
|
57
|
+
**Alternative to Deletion:**
|
|
58
|
+
Instead of deleting, consider:
|
|
59
|
+
- Updating entries to mark them as archived
|
|
60
|
+
- Consolidating multiple entries into one comprehensive entry
|
|
61
|
+
- Moving entries to an "archive/" hierarchy for later reference
|
|
62
|
+
DESC
|
|
63
|
+
|
|
64
|
+
param :file_path,
|
|
65
|
+
desc: "Path to delete from memory - MUST start with concept/, fact/, skill/, or experience/ (e.g., 'concept/old-api/deprecated.md', 'experience/temp.md')",
|
|
66
|
+
required: true
|
|
67
|
+
|
|
68
|
+
# Initialize with storage instance
|
|
69
|
+
#
|
|
70
|
+
# @param storage [Core::Storage] Storage instance
|
|
71
|
+
def initialize(storage:)
|
|
72
|
+
super()
|
|
73
|
+
@storage = storage
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Override name to return simple "MemoryDelete"
|
|
77
|
+
def name
|
|
78
|
+
"MemoryDelete"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Execute the tool
|
|
82
|
+
#
|
|
83
|
+
# @param file_path [String] Path to delete from
|
|
84
|
+
# @return [String] Success message
|
|
85
|
+
def execute(file_path:)
|
|
86
|
+
@storage.delete(file_path: file_path)
|
|
87
|
+
"Deleted memory://#{file_path}"
|
|
88
|
+
rescue ArgumentError => e
|
|
89
|
+
validation_error(e.message)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def validation_error(message)
|
|
95
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmMemory
|
|
4
|
+
module Tools
|
|
5
|
+
# Tool for editing memory entries with exact string replacement
|
|
6
|
+
#
|
|
7
|
+
# Performs exact string replacements in memory content.
|
|
8
|
+
# Each agent has its own isolated memory storage.
|
|
9
|
+
class MemoryEdit < RubyLLM::Tool
|
|
10
|
+
description <<~DESC
|
|
11
|
+
Perform exact string replacements in memory entries (works like Edit tool but for memory content).
|
|
12
|
+
|
|
13
|
+
REQUIRED: Provide ALL THREE parameters - file_path, old_string, and new_string.
|
|
14
|
+
|
|
15
|
+
**Required Parameters:**
|
|
16
|
+
- file_path (REQUIRED): Path to memory entry - MUST start with concept/, fact/, skill/, or experience/
|
|
17
|
+
- old_string (REQUIRED): The exact text to replace - must match exactly including all whitespace and indentation
|
|
18
|
+
- new_string (REQUIRED): The replacement text - must be different from old_string
|
|
19
|
+
|
|
20
|
+
**MEMORY STRUCTURE (4 Fixed Categories Only):**
|
|
21
|
+
- concept/{domain}/{name}.md - Abstract ideas
|
|
22
|
+
- fact/{subfolder}/{name}.md - Concrete information
|
|
23
|
+
- skill/{domain}/{name}.md - Procedures
|
|
24
|
+
- experience/{name}.md - Lessons
|
|
25
|
+
INVALID: documentation/, reference/, analysis/, parallel/, tutorial/
|
|
26
|
+
|
|
27
|
+
**Optional Parameters:**
|
|
28
|
+
- replace_all: Set to true to replace all occurrences (default: false, replaces only first occurrence)
|
|
29
|
+
|
|
30
|
+
**CRITICAL - Before Using This Tool:**
|
|
31
|
+
1. You MUST use MemoryRead on the entry first - edits without reading will FAIL
|
|
32
|
+
2. Copy text exactly from MemoryRead output, EXCLUDING the line number prefix
|
|
33
|
+
3. Line number format: " 123→actual content" - only use text AFTER the arrow
|
|
34
|
+
4. Preserve exact indentation and whitespace from the original content
|
|
35
|
+
|
|
36
|
+
**How It Works:**
|
|
37
|
+
- If old_string appears once: replacement succeeds
|
|
38
|
+
- If old_string appears multiple times: FAILS unless replace_all=true
|
|
39
|
+
- If old_string not found: FAILS with helpful error
|
|
40
|
+
- Make old_string unique by including more surrounding context
|
|
41
|
+
|
|
42
|
+
**Examples:**
|
|
43
|
+
```
|
|
44
|
+
# Update a fact
|
|
45
|
+
MemoryEdit(
|
|
46
|
+
file_path: "fact/people/john.md",
|
|
47
|
+
old_string: "status: active",
|
|
48
|
+
new_string: "status: inactive"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Update a skill
|
|
52
|
+
MemoryEdit(
|
|
53
|
+
file_path: "skill/debugging/api-errors.md",
|
|
54
|
+
old_string: "TODO: Add error codes",
|
|
55
|
+
new_string: "Common error codes: 401, 403, 404, 500",
|
|
56
|
+
replace_all: true
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Update a concept
|
|
60
|
+
MemoryEdit(
|
|
61
|
+
file_path: "concept/ruby/classes.md",
|
|
62
|
+
old_string: "def calculate_total(items)\n return sum(items)\nend",
|
|
63
|
+
new_string: "def compute_sum(items)\n items.sum\nend"
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Common Mistakes to Avoid:**
|
|
68
|
+
- Including line numbers in old_string or new_string
|
|
69
|
+
- Not reading the entry first with MemoryRead
|
|
70
|
+
- Not matching whitespace exactly
|
|
71
|
+
- Trying to replace non-unique text without replace_all
|
|
72
|
+
DESC
|
|
73
|
+
|
|
74
|
+
param :file_path,
|
|
75
|
+
desc: "Path to memory entry - MUST start with concept/, fact/, skill/, or experience/ (e.g., 'concept/ruby/classes.md', 'skill/debugging/api.md')",
|
|
76
|
+
required: true
|
|
77
|
+
|
|
78
|
+
param :old_string,
|
|
79
|
+
desc: "The exact text to replace (must match exactly including whitespace)",
|
|
80
|
+
required: true
|
|
81
|
+
|
|
82
|
+
param :new_string,
|
|
83
|
+
desc: "The text to replace it with (must be different from old_string)",
|
|
84
|
+
required: true
|
|
85
|
+
|
|
86
|
+
param :replace_all,
|
|
87
|
+
desc: "Replace all occurrences of old_string (default false)",
|
|
88
|
+
required: false
|
|
89
|
+
|
|
90
|
+
# Initialize with storage instance and agent name
|
|
91
|
+
#
|
|
92
|
+
# @param storage [Core::Storage] Storage instance
|
|
93
|
+
# @param agent_name [String, Symbol] Agent identifier
|
|
94
|
+
def initialize(storage:, agent_name:)
|
|
95
|
+
super()
|
|
96
|
+
@storage = storage
|
|
97
|
+
@agent_name = agent_name.to_sym
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Override name to return simple "MemoryEdit"
|
|
101
|
+
def name
|
|
102
|
+
"MemoryEdit"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Execute the tool
|
|
106
|
+
#
|
|
107
|
+
# @param file_path [String] Path to memory entry
|
|
108
|
+
# @param old_string [String] Text to replace
|
|
109
|
+
# @param new_string [String] Replacement text
|
|
110
|
+
# @param replace_all [Boolean] Replace all occurrences
|
|
111
|
+
# @return [String] Success message or error
|
|
112
|
+
def execute(file_path:, old_string:, new_string:, replace_all: false)
|
|
113
|
+
# Validate inputs
|
|
114
|
+
return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
|
|
115
|
+
return validation_error("old_string is required") if old_string.nil? || old_string.empty?
|
|
116
|
+
return validation_error("new_string is required") if new_string.nil?
|
|
117
|
+
|
|
118
|
+
# old_string and new_string must be different
|
|
119
|
+
if old_string == new_string
|
|
120
|
+
return validation_error("old_string and new_string must be different. They are currently identical.")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Read current content (this will raise ArgumentError if entry doesn't exist)
|
|
124
|
+
content = @storage.read(file_path: file_path)
|
|
125
|
+
|
|
126
|
+
# Enforce read-before-edit
|
|
127
|
+
unless Core::StorageReadTracker.entry_read?(@agent_name, file_path)
|
|
128
|
+
return validation_error(
|
|
129
|
+
"Cannot edit memory entry without reading it first. " \
|
|
130
|
+
"You must use MemoryRead on 'memory://#{file_path}' before editing it. " \
|
|
131
|
+
"This ensures you have the current content to match against.",
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Check if old_string exists in content
|
|
136
|
+
unless content.include?(old_string)
|
|
137
|
+
return validation_error(<<~ERROR.chomp)
|
|
138
|
+
old_string not found in memory entry. Make sure it matches exactly, including all whitespace and indentation.
|
|
139
|
+
Do not include line number prefixes from MemoryRead tool output.
|
|
140
|
+
ERROR
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Count occurrences
|
|
144
|
+
occurrences = content.scan(old_string).count
|
|
145
|
+
|
|
146
|
+
# If not replace_all and multiple occurrences, error
|
|
147
|
+
if !replace_all && occurrences > 1
|
|
148
|
+
return validation_error(<<~ERROR.chomp)
|
|
149
|
+
Found #{occurrences} occurrences of old_string.
|
|
150
|
+
Either provide more surrounding context to make the match unique, or use replace_all: true to replace all occurrences.
|
|
151
|
+
ERROR
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Perform replacement
|
|
155
|
+
new_content = if replace_all
|
|
156
|
+
content.gsub(old_string, new_string)
|
|
157
|
+
else
|
|
158
|
+
content.sub(old_string, new_string)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Get existing entry metadata
|
|
162
|
+
entry = @storage.read_entry(file_path: file_path)
|
|
163
|
+
|
|
164
|
+
# Write updated content back (preserving the title)
|
|
165
|
+
@storage.write(
|
|
166
|
+
file_path: file_path,
|
|
167
|
+
content: new_content,
|
|
168
|
+
title: entry.title,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Build success message
|
|
172
|
+
replaced_count = replace_all ? occurrences : 1
|
|
173
|
+
"Successfully replaced #{replaced_count} occurrence(s) in memory://#{file_path}"
|
|
174
|
+
rescue ArgumentError => e
|
|
175
|
+
validation_error(e.message)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def validation_error(message)
|
|
181
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|