claude_swarm 1.0.1 ā 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +14 -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 +6 -2
- 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,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Agent
|
|
5
|
+
# AgentContext encapsulates per-agent state and metadata
|
|
6
|
+
#
|
|
7
|
+
# Each agent has its own context that tracks:
|
|
8
|
+
# - Agent identity (name)
|
|
9
|
+
# - Delegation relationships (which tool calls are delegations)
|
|
10
|
+
# - Context window warnings (which thresholds have been hit)
|
|
11
|
+
# - Optional metadata
|
|
12
|
+
#
|
|
13
|
+
# This class replaces the per-agent hash maps that were previously
|
|
14
|
+
# stored in UnifiedLogger.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# context = Agent::Context.new(
|
|
18
|
+
# name: :backend,
|
|
19
|
+
# delegation_tools: ["DelegateToDatabase", "DelegateToAuth"],
|
|
20
|
+
# metadata: { role: "backend" }
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
# # Track a delegation
|
|
24
|
+
# context.track_delegation(call_id: "call_123", target: "DelegateToDatabase")
|
|
25
|
+
#
|
|
26
|
+
# # Check if a tool call is a delegation
|
|
27
|
+
# context.delegation?(call_id: "call_123") # => true
|
|
28
|
+
class Context
|
|
29
|
+
# Thresholds for context limit warnings (in percentage)
|
|
30
|
+
# 60% triggers automatic compression, 80%/90% are informational warnings
|
|
31
|
+
CONTEXT_WARNING_THRESHOLDS = [60, 80, 90].freeze
|
|
32
|
+
|
|
33
|
+
# Threshold at which automatic compression is triggered
|
|
34
|
+
COMPRESSION_THRESHOLD = 60
|
|
35
|
+
|
|
36
|
+
attr_reader :name, :delegation_tools, :metadata, :warning_thresholds_hit
|
|
37
|
+
|
|
38
|
+
# Initialize a new agent context
|
|
39
|
+
#
|
|
40
|
+
# @param name [Symbol, String] Agent name
|
|
41
|
+
# @param delegation_tools [Array<String>] Names of tools that are delegations
|
|
42
|
+
# @param metadata [Hash] Optional metadata about the agent
|
|
43
|
+
def initialize(name:, delegation_tools: [], metadata: {})
|
|
44
|
+
@name = name.to_sym
|
|
45
|
+
@delegation_tools = Set.new(delegation_tools.map(&:to_s))
|
|
46
|
+
@metadata = metadata
|
|
47
|
+
@delegation_call_ids = Set.new
|
|
48
|
+
@delegation_targets = {}
|
|
49
|
+
@warning_thresholds_hit = Set.new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Track a delegation tool call
|
|
53
|
+
#
|
|
54
|
+
# @param call_id [String] Tool call ID
|
|
55
|
+
# @param target [String] Target agent/tool name
|
|
56
|
+
# @return [void]
|
|
57
|
+
def track_delegation(call_id:, target:)
|
|
58
|
+
@delegation_call_ids.add(call_id)
|
|
59
|
+
@delegation_targets[call_id] = target
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check if a tool call is a delegation
|
|
63
|
+
#
|
|
64
|
+
# @param call_id [String] Tool call ID
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
def delegation?(call_id:)
|
|
67
|
+
@delegation_call_ids.include?(call_id)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Get the delegation target for a tool call
|
|
71
|
+
#
|
|
72
|
+
# @param call_id [String] Tool call ID
|
|
73
|
+
# @return [String, nil] Target agent/tool name, or nil if not a delegation
|
|
74
|
+
def delegation_target(call_id:)
|
|
75
|
+
@delegation_targets[call_id]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Remove a delegation from tracking (after it completes)
|
|
79
|
+
#
|
|
80
|
+
# @param call_id [String] Tool call ID
|
|
81
|
+
# @return [void]
|
|
82
|
+
def clear_delegation(call_id:)
|
|
83
|
+
@delegation_targets.delete(call_id)
|
|
84
|
+
@delegation_call_ids.delete(call_id)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check if a tool name is a delegation tool
|
|
88
|
+
#
|
|
89
|
+
# @param tool_name [String] Tool name
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
def delegation_tool?(tool_name)
|
|
92
|
+
@delegation_tools.include?(tool_name.to_s)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Record that a context warning threshold has been hit
|
|
96
|
+
#
|
|
97
|
+
# @param threshold [Integer] Threshold percentage (80, 90, etc)
|
|
98
|
+
# @return [Boolean] true if this is the first time hitting this threshold
|
|
99
|
+
def hit_warning_threshold?(threshold)
|
|
100
|
+
!@warning_thresholds_hit.add?(threshold).nil?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Check if a warning threshold has been hit
|
|
104
|
+
#
|
|
105
|
+
# @param threshold [Integer] Threshold percentage
|
|
106
|
+
# @return [Boolean]
|
|
107
|
+
def warning_threshold_hit?(threshold)
|
|
108
|
+
@warning_thresholds_hit.include?(threshold)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Agent
|
|
5
|
+
# Manages conversation context and message optimization
|
|
6
|
+
#
|
|
7
|
+
# Responsibilities:
|
|
8
|
+
# - Handle ephemeral messages (sent to LLM but not persisted)
|
|
9
|
+
# - Extract and strip system reminders
|
|
10
|
+
# - Prepare messages for LLM API calls
|
|
11
|
+
# - Future: Context window management, summarization, truncation
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# manager = ContextManager.new
|
|
15
|
+
# manager.add_ephemeral_reminder("<system-reminder>Use caution</system-reminder>")
|
|
16
|
+
# messages_for_llm = manager.prepare_for_llm(persistent_messages)
|
|
17
|
+
# manager.clear_ephemeral # After LLM call
|
|
18
|
+
class ContextManager
|
|
19
|
+
SYSTEM_REMINDER_REGEX = %r{<system-reminder>.*?</system-reminder>}m
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
# Ephemeral content to append to messages for this turn only
|
|
23
|
+
# Format: { message_index => [array of reminder strings] }
|
|
24
|
+
@ephemeral_content = {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Track ephemeral content to append to a specific message
|
|
28
|
+
#
|
|
29
|
+
# Reminders will be embedded in the message content when sent to LLM,
|
|
30
|
+
# but are NOT persisted in the message history.
|
|
31
|
+
#
|
|
32
|
+
# @param message_index [Integer] Index of message to append to
|
|
33
|
+
# @param content [String] Reminder content to append
|
|
34
|
+
# @return [void]
|
|
35
|
+
def add_ephemeral_content_for_message(message_index, content)
|
|
36
|
+
@ephemeral_content[message_index] ||= []
|
|
37
|
+
@ephemeral_content[message_index] << content
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Add ephemeral reminder to the most recent message
|
|
41
|
+
#
|
|
42
|
+
# This will append the reminder to the last message in the array when
|
|
43
|
+
# preparing for LLM, but won't modify the stored message.
|
|
44
|
+
#
|
|
45
|
+
# @param content [String] Reminder content
|
|
46
|
+
# @param messages_array [Array<RubyLLM::Message>] Message array to get index from
|
|
47
|
+
# @return [void]
|
|
48
|
+
def add_ephemeral_reminder(content, messages_array:)
|
|
49
|
+
message_index = messages_array.size - 1
|
|
50
|
+
return if message_index < 0
|
|
51
|
+
|
|
52
|
+
add_ephemeral_content_for_message(message_index, content)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Prepare messages for LLM API call
|
|
56
|
+
#
|
|
57
|
+
# Embeds ephemeral content into messages for this turn only.
|
|
58
|
+
# Does NOT modify the persistent messages array.
|
|
59
|
+
#
|
|
60
|
+
# @param persistent_messages [Array<RubyLLM::Message>] Messages from @messages
|
|
61
|
+
# @return [Array<RubyLLM::Message>] Messages with ephemeral content embedded
|
|
62
|
+
def prepare_for_llm(persistent_messages)
|
|
63
|
+
return persistent_messages.dup if @ephemeral_content.empty?
|
|
64
|
+
|
|
65
|
+
# Clone messages and embed ephemeral content
|
|
66
|
+
messages_for_llm = persistent_messages.map.with_index do |msg, index|
|
|
67
|
+
ephemeral_for_this_msg = @ephemeral_content[index]
|
|
68
|
+
|
|
69
|
+
# No ephemeral content for this message - use as-is
|
|
70
|
+
next msg unless ephemeral_for_this_msg&.any?
|
|
71
|
+
|
|
72
|
+
# Embed ephemeral content in this message
|
|
73
|
+
original_content = msg.content.is_a?(RubyLLM::Content) ? msg.content.text : msg.content.to_s
|
|
74
|
+
embedded_content = [original_content, *ephemeral_for_this_msg].join("\n\n")
|
|
75
|
+
|
|
76
|
+
# Create new message with embedded content
|
|
77
|
+
if msg.content.is_a?(RubyLLM::Content)
|
|
78
|
+
RubyLLM::Message.new(
|
|
79
|
+
role: msg.role,
|
|
80
|
+
content: RubyLLM::Content.new(embedded_content, msg.content.attachments),
|
|
81
|
+
tool_call_id: msg.tool_call_id,
|
|
82
|
+
)
|
|
83
|
+
else
|
|
84
|
+
RubyLLM::Message.new(
|
|
85
|
+
role: msg.role,
|
|
86
|
+
content: embedded_content,
|
|
87
|
+
tool_call_id: msg.tool_call_id,
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
messages_for_llm
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Clear all ephemeral content
|
|
96
|
+
#
|
|
97
|
+
# Should be called after LLM response is received.
|
|
98
|
+
#
|
|
99
|
+
# @return [void]
|
|
100
|
+
def clear_ephemeral
|
|
101
|
+
@ephemeral_content.clear
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Check if there is pending ephemeral content
|
|
105
|
+
#
|
|
106
|
+
# @return [Boolean] True if ephemeral content exists
|
|
107
|
+
def has_ephemeral?
|
|
108
|
+
@ephemeral_content.any?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Get count of messages with ephemeral content
|
|
112
|
+
#
|
|
113
|
+
# @return [Integer] Number of messages with ephemeral content attached
|
|
114
|
+
def ephemeral_count
|
|
115
|
+
@ephemeral_content.size
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Extract all <system-reminder> blocks from content
|
|
119
|
+
#
|
|
120
|
+
# @param content [String] Content to extract from
|
|
121
|
+
# @return [Array<String>] Array of system reminder blocks
|
|
122
|
+
def extract_system_reminders(content)
|
|
123
|
+
return [] if content.nil? || content.empty?
|
|
124
|
+
|
|
125
|
+
content.scan(SYSTEM_REMINDER_REGEX)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Strip all <system-reminder> blocks from content
|
|
129
|
+
#
|
|
130
|
+
# Returns clean content without system reminders.
|
|
131
|
+
#
|
|
132
|
+
# @param content [String] Content to strip from
|
|
133
|
+
# @return [String] Clean content
|
|
134
|
+
def strip_system_reminders(content)
|
|
135
|
+
return content if content.nil? || content.empty?
|
|
136
|
+
|
|
137
|
+
content.gsub(SYSTEM_REMINDER_REGEX, "").strip
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Check if content contains system reminders
|
|
141
|
+
#
|
|
142
|
+
# @param content [String] Content to check
|
|
143
|
+
# @return [Boolean] True if reminders found
|
|
144
|
+
def has_system_reminders?(content)
|
|
145
|
+
return false if content.nil? || content.empty?
|
|
146
|
+
|
|
147
|
+
SYSTEM_REMINDER_REGEX.match?(content)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# ============================================================================
|
|
151
|
+
# FUTURE: Context Optimization Methods (Hooks for Later Implementation)
|
|
152
|
+
# ============================================================================
|
|
153
|
+
|
|
154
|
+
# Future: Summarize old messages to save context window space
|
|
155
|
+
#
|
|
156
|
+
# @param messages [Array<RubyLLM::Message>] Messages to potentially summarize
|
|
157
|
+
# @param before_index [Integer] Summarize messages before this index
|
|
158
|
+
# @param strategy [Symbol] Summarization strategy (:llm, :truncate, :remove)
|
|
159
|
+
# @return [Array<RubyLLM::Message>] Optimized message array
|
|
160
|
+
def summarize_old_messages(messages, before_index:, strategy: :truncate)
|
|
161
|
+
# TODO: Implement when needed
|
|
162
|
+
messages
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Future: Truncate messages to fit within context window
|
|
166
|
+
#
|
|
167
|
+
# @param messages [Array<RubyLLM::Message>] Messages to fit
|
|
168
|
+
# @param max_tokens [Integer] Maximum token budget
|
|
169
|
+
# @param keep_recent [Integer] Number of recent messages to always keep
|
|
170
|
+
# @return [Array<RubyLLM::Message>] Truncated messages
|
|
171
|
+
def truncate_to_fit(messages, max_tokens:, keep_recent: 10)
|
|
172
|
+
# TODO: Implement when needed
|
|
173
|
+
messages
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Compress verbose tool results for older messages
|
|
177
|
+
#
|
|
178
|
+
# Uses progressive compression: older messages are compressed more aggressively.
|
|
179
|
+
# Preserves user/assistant messages at full detail (conversational context).
|
|
180
|
+
#
|
|
181
|
+
# @param messages [Array<RubyLLM::Message>] Messages to compress
|
|
182
|
+
# @param keep_recent [Integer] Number of recent messages to keep at full detail
|
|
183
|
+
# @return [Array<RubyLLM::Message>] Compressed messages
|
|
184
|
+
def compress_tool_results(messages, keep_recent: 10)
|
|
185
|
+
messages.map.with_index do |msg, i|
|
|
186
|
+
# Keep recent messages at full detail
|
|
187
|
+
next msg if i >= messages.size - keep_recent
|
|
188
|
+
|
|
189
|
+
# Keep user/assistant messages (conversational flow is important)
|
|
190
|
+
next msg if [:user, :assistant].include?(msg.role)
|
|
191
|
+
|
|
192
|
+
# Compress old tool results
|
|
193
|
+
if msg.role == :tool
|
|
194
|
+
compress_tool_message(msg, age: messages.size - i)
|
|
195
|
+
else
|
|
196
|
+
msg
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Compress a single tool message based on age
|
|
202
|
+
#
|
|
203
|
+
# Progressive compression: older messages get compressed more.
|
|
204
|
+
# For re-runnable tools (Read, Grep, Glob, etc.), adds instruction to re-run if needed.
|
|
205
|
+
#
|
|
206
|
+
# @param msg [RubyLLM::Message] Tool message to compress
|
|
207
|
+
# @param age [Integer] How many messages ago (higher = older)
|
|
208
|
+
# @return [RubyLLM::Message] Compressed message
|
|
209
|
+
def compress_tool_message(msg, age:)
|
|
210
|
+
content = msg.content.to_s
|
|
211
|
+
|
|
212
|
+
# Progressive compression based on age
|
|
213
|
+
max_length = case age
|
|
214
|
+
when 0..10 then return msg # Recent: keep full detail
|
|
215
|
+
when 11..20 then 1000 # Medium age: light compression
|
|
216
|
+
when 21..40 then 500 # Old: moderate compression
|
|
217
|
+
when 41..60 then 200 # Very old: heavy compression
|
|
218
|
+
else 100 # Ancient: minimal summary
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
return msg if content.length <= max_length
|
|
222
|
+
|
|
223
|
+
# Compress while preserving structure
|
|
224
|
+
compressed = content.slice(0, max_length)
|
|
225
|
+
truncated_chars = content.length - max_length
|
|
226
|
+
compressed += "\n...[#{truncated_chars} chars truncated for context management]"
|
|
227
|
+
|
|
228
|
+
# Detect if this is a re-runnable tool and add helpful instruction
|
|
229
|
+
tool_name = detect_tool_name(content)
|
|
230
|
+
if rerunnable_tool?(tool_name)
|
|
231
|
+
compressed += "\n\nš” If you need the full output, re-run the #{tool_name} tool with the same parameters."
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
RubyLLM::Message.new(
|
|
235
|
+
role: :tool,
|
|
236
|
+
content: compressed,
|
|
237
|
+
tool_call_id: msg.tool_call_id,
|
|
238
|
+
)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Detect tool name from content
|
|
242
|
+
#
|
|
243
|
+
# @param content [String] Tool result content
|
|
244
|
+
# @return [String, nil] Tool name or nil
|
|
245
|
+
def detect_tool_name(content)
|
|
246
|
+
# Many tool results start with patterns we can detect
|
|
247
|
+
case content
|
|
248
|
+
when /^\s*\d+ā/ # Line numbers (Read, MemoryRead)
|
|
249
|
+
content.include?("memory://") ? "MemoryRead" : "Read"
|
|
250
|
+
when /^Memory entries matching/ # MemoryGlob
|
|
251
|
+
"MemoryGlob"
|
|
252
|
+
when /^Found \d+ files? matching/ # Glob
|
|
253
|
+
"Glob"
|
|
254
|
+
when /matches in \d+ files?|No matches found/ # Grep, MemoryGrep
|
|
255
|
+
content.include?("memory://") ? "MemoryGrep" : "Grep"
|
|
256
|
+
when %r{^Stored at memory://} # MemoryWrite (not re-runnable but identifiable)
|
|
257
|
+
"MemoryWrite"
|
|
258
|
+
when %r{^Deleted memory://} # MemoryDelete
|
|
259
|
+
"MemoryDelete"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Check if a tool is re-runnable (idempotent, can get same data again)
|
|
264
|
+
#
|
|
265
|
+
# @param tool_name [String, nil] Tool name
|
|
266
|
+
# @return [Boolean] True if tool can be re-run safely
|
|
267
|
+
def rerunnable_tool?(tool_name)
|
|
268
|
+
return false if tool_name.nil?
|
|
269
|
+
|
|
270
|
+
# These tools are idempotent - re-running gives same/current data
|
|
271
|
+
["Read", "MemoryRead", "Grep", "MemoryGrep", "Glob", "MemoryGlob"].include?(tool_name)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Automatically compress messages when context threshold is hit
|
|
275
|
+
#
|
|
276
|
+
# This is called automatically when context usage crosses 60% threshold.
|
|
277
|
+
# Returns compressed messages array for immediate use.
|
|
278
|
+
#
|
|
279
|
+
# @param messages [Array<RubyLLM::Message>] Current message array
|
|
280
|
+
# @param keep_recent [Integer] Number of recent messages to keep full
|
|
281
|
+
# @return [Array<RubyLLM::Message>] Compressed messages
|
|
282
|
+
def auto_compress_on_threshold(messages, keep_recent: 10)
|
|
283
|
+
return messages if @compression_applied
|
|
284
|
+
|
|
285
|
+
# Mark as applied to avoid compressing multiple times
|
|
286
|
+
@compression_applied = true
|
|
287
|
+
|
|
288
|
+
compress_tool_results(messages, keep_recent: keep_recent)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Reset compression flag (when conversation is reset)
|
|
292
|
+
#
|
|
293
|
+
# @return [void]
|
|
294
|
+
def reset_compression
|
|
295
|
+
@compression_applied = false
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Future: Detect if context is becoming bloated
|
|
299
|
+
#
|
|
300
|
+
# @param messages [Array<RubyLLM::Message>] Messages to analyze
|
|
301
|
+
# @param threshold [Float] Bloat threshold (0.0-1.0)
|
|
302
|
+
# @return [Hash] Bloat analysis with recommendations
|
|
303
|
+
def analyze_context_bloat(messages, threshold: 0.7)
|
|
304
|
+
# TODO: Implement when needed
|
|
305
|
+
{ bloated: false, recommendations: [] }
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|