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,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Adapter for converting Claude Code agent markdown files to SwarmSDK format
|
|
5
|
+
#
|
|
6
|
+
# Claude Code agent files use a different syntax and conventions than SwarmSDK:
|
|
7
|
+
# - Tools are comma-separated strings instead of arrays
|
|
8
|
+
# - Model shortcuts like 'sonnet', 'opus', 'haiku' instead of full model IDs
|
|
9
|
+
# - Tool permissions like 'Write(src/**)' instead of SwarmSDK's permission system
|
|
10
|
+
# - Required 'name' field in frontmatter
|
|
11
|
+
#
|
|
12
|
+
# This adapter:
|
|
13
|
+
# - Detects Claude Code format by checking frontmatter markers
|
|
14
|
+
# - Converts tools from comma-separated strings to arrays
|
|
15
|
+
# - Maps model shortcuts to canonical model IDs
|
|
16
|
+
# - Strips unsupported tool permission syntax with warnings
|
|
17
|
+
# - Sets coding_agent: true by default
|
|
18
|
+
# - Warns about unsupported fields
|
|
19
|
+
#
|
|
20
|
+
# @example Parse a Claude Code agent file
|
|
21
|
+
# content = File.read('.claude/agents/reviewer.md')
|
|
22
|
+
# config = ClaudeCodeAgentAdapter.parse(content, :reviewer)
|
|
23
|
+
# agent = Agent::Definition.new(:reviewer, config)
|
|
24
|
+
#
|
|
25
|
+
class ClaudeCodeAgentAdapter
|
|
26
|
+
# Fields supported in Claude Code agent frontmatter
|
|
27
|
+
SUPPORTED_FIELDS = ["name", "description", "tools", "model"].freeze
|
|
28
|
+
|
|
29
|
+
# SwarmSDK documentation URL for reference
|
|
30
|
+
SWARM_SDK_DOCS_URL = "https://github.com/parruda/claude-swarm/blob/main/docs/v2/README.md"
|
|
31
|
+
|
|
32
|
+
# Pattern to detect tool permission syntax like Write(src/**)
|
|
33
|
+
TOOL_PERMISSION_PATTERN = /^([A-Za-z_]+)\([^)]+\)$/
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# Detect if content appears to be in Claude Code agent format
|
|
37
|
+
#
|
|
38
|
+
# Detection is based on tools field type:
|
|
39
|
+
# - Claude Code: tools is a comma-separated string (e.g., "Read, Write, Bash")
|
|
40
|
+
# - SwarmSDK: tools is an array (e.g., [Read, Write, Bash])
|
|
41
|
+
#
|
|
42
|
+
# Note: The 'name' field alone is not sufficient since SwarmSDK also supports it
|
|
43
|
+
#
|
|
44
|
+
# @param content [String] Markdown content with YAML frontmatter
|
|
45
|
+
# @return [Boolean] true if content appears to be Claude Code format
|
|
46
|
+
def claude_code_format?(content)
|
|
47
|
+
return false unless content =~ /\A---\s*\n(.*?)\n---\s*\n/m
|
|
48
|
+
|
|
49
|
+
frontmatter_yaml = Regexp.last_match(1)
|
|
50
|
+
frontmatter = YAML.safe_load(frontmatter_yaml, permitted_classes: [Symbol], aliases: true)
|
|
51
|
+
|
|
52
|
+
return false unless frontmatter.is_a?(Hash)
|
|
53
|
+
|
|
54
|
+
# Only detect as Claude Code if tools field is a comma-separated string
|
|
55
|
+
# This is the most reliable indicator since SwarmSDK always uses arrays
|
|
56
|
+
frontmatter.key?("tools") && frontmatter["tools"].is_a?(String)
|
|
57
|
+
rescue Psych::SyntaxError
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Parse Claude Code agent markdown and convert to SwarmSDK format
|
|
62
|
+
#
|
|
63
|
+
# @param content [String] Markdown content with YAML frontmatter
|
|
64
|
+
# @param agent_name [Symbol, String] Name of the agent
|
|
65
|
+
# @param inherit_model [String, nil] Model to use when frontmatter has 'inherit'
|
|
66
|
+
# @return [Hash] Configuration hash suitable for Agent::Definition.new
|
|
67
|
+
# @raise [ConfigurationError] if content format is invalid
|
|
68
|
+
def parse(content, agent_name, inherit_model: nil)
|
|
69
|
+
new(inherit_model: inherit_model).parse(content, agent_name)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Initialize adapter with optional context
|
|
74
|
+
#
|
|
75
|
+
# @param inherit_model [String, nil] Model to use when frontmatter has 'inherit'
|
|
76
|
+
def initialize(inherit_model: nil)
|
|
77
|
+
@inherit_model = inherit_model
|
|
78
|
+
@warnings = []
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Parse Claude Code agent content
|
|
82
|
+
#
|
|
83
|
+
# @param content [String] Markdown content with YAML frontmatter
|
|
84
|
+
# @param agent_name [Symbol, String] Name of the agent
|
|
85
|
+
# @return [Hash] Configuration hash for Agent::Definition
|
|
86
|
+
# @raise [ConfigurationError] if format is invalid
|
|
87
|
+
def parse(content, agent_name)
|
|
88
|
+
unless content =~ /\A---\s*\n(.*?)\n---\s*\n(.*)\z/m
|
|
89
|
+
raise ConfigurationError, "Invalid Claude Code agent format. Expected YAML frontmatter followed by prompt content."
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
frontmatter_yaml = Regexp.last_match(1)
|
|
93
|
+
prompt_content = Regexp.last_match(2).strip
|
|
94
|
+
|
|
95
|
+
frontmatter = YAML.safe_load(frontmatter_yaml, permitted_classes: [Symbol], aliases: true)
|
|
96
|
+
|
|
97
|
+
unless frontmatter.is_a?(Hash)
|
|
98
|
+
raise ConfigurationError, "Invalid frontmatter format in Claude Code agent file"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
config = build_config(frontmatter, prompt_content, agent_name)
|
|
102
|
+
emit_warnings(agent_name)
|
|
103
|
+
config
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# Build SwarmSDK configuration from Claude Code frontmatter
|
|
109
|
+
def build_config(frontmatter, prompt_content, agent_name)
|
|
110
|
+
warn_unknown_fields(frontmatter)
|
|
111
|
+
|
|
112
|
+
config = {
|
|
113
|
+
description: frontmatter["description"],
|
|
114
|
+
system_prompt: prompt_content,
|
|
115
|
+
coding_agent: true, # Default for Claude Code agents
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Parse tools if present
|
|
119
|
+
if frontmatter["tools"]
|
|
120
|
+
config[:tools] = parse_tools(frontmatter["tools"])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Parse model if present
|
|
124
|
+
if frontmatter["model"]
|
|
125
|
+
config[:model] = resolve_model(frontmatter["model"])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
config
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Parse tools field - handles both comma-separated string and array
|
|
132
|
+
#
|
|
133
|
+
# @param tools_field [String, Array] Tools from frontmatter
|
|
134
|
+
# @return [Array<String>] Array of tool names
|
|
135
|
+
def parse_tools(tools_field)
|
|
136
|
+
tools_array = if tools_field.is_a?(String)
|
|
137
|
+
tools_field.split(",").map(&:strip)
|
|
138
|
+
else
|
|
139
|
+
Array(tools_field).map(&:to_s)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Clean tool permissions and collect warnings
|
|
143
|
+
tools_array.map { |tool| clean_tool_permissions(tool) }.compact
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Strip tool permission syntax and warn if detected
|
|
147
|
+
#
|
|
148
|
+
# @param tool_string [String] Tool name, possibly with permissions like 'Write(src/**)'
|
|
149
|
+
# @return [String, nil] Clean tool name, or nil if invalid
|
|
150
|
+
def clean_tool_permissions(tool_string)
|
|
151
|
+
if tool_string =~ TOOL_PERMISSION_PATTERN
|
|
152
|
+
tool_name = Regexp.last_match(1)
|
|
153
|
+
@warnings << "Tool permission syntax '#{tool_string}' detected in agent file. SwarmSDK supports permissions but uses different syntax. Using '#{tool_name}' without restrictions for now. See SwarmSDK documentation for permission configuration: #{SWARM_SDK_DOCS_URL}"
|
|
154
|
+
tool_name
|
|
155
|
+
else
|
|
156
|
+
tool_string
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Resolve model shortcuts to canonical model IDs
|
|
161
|
+
#
|
|
162
|
+
# Uses SwarmSDK::Models.resolve_alias to map shortcuts like 'sonnet'
|
|
163
|
+
# to the latest model IDs from model_aliases.json.
|
|
164
|
+
#
|
|
165
|
+
# @param model_field [String] Model from frontmatter
|
|
166
|
+
# @return [String, Symbol] Canonical model ID or :inherit symbol
|
|
167
|
+
def resolve_model(model_field)
|
|
168
|
+
model_str = model_field.to_s.strip
|
|
169
|
+
|
|
170
|
+
# Handle 'inherit' keyword
|
|
171
|
+
return :inherit if model_str == "inherit"
|
|
172
|
+
|
|
173
|
+
# Resolve using SwarmSDK model aliases
|
|
174
|
+
# This maps 'sonnet' → 'claude-sonnet-4-5-20250929', etc.
|
|
175
|
+
SwarmSDK::Models.resolve_alias(model_str)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Warn about unknown frontmatter fields
|
|
179
|
+
def warn_unknown_fields(frontmatter)
|
|
180
|
+
unknown_fields = frontmatter.keys - SUPPORTED_FIELDS
|
|
181
|
+
|
|
182
|
+
unknown_fields.each do |field|
|
|
183
|
+
@warnings << case field
|
|
184
|
+
when "hooks"
|
|
185
|
+
"Hooks configuration detected in agent frontmatter. SwarmSDK handles hooks at the swarm level. See: #{SWARM_SDK_DOCS_URL}"
|
|
186
|
+
else
|
|
187
|
+
"Unknown field '#{field}' in Claude Code agent file. Ignoring. Supported fields: #{SUPPORTED_FIELDS.join(", ")}"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Emit all collected warnings via LogCollector
|
|
193
|
+
def emit_warnings(agent_name)
|
|
194
|
+
return if @warnings.empty?
|
|
195
|
+
|
|
196
|
+
@warnings.each do |warning|
|
|
197
|
+
LogCollector.emit(
|
|
198
|
+
type: "claude_code_conversion_warning",
|
|
199
|
+
agent: agent_name,
|
|
200
|
+
message: warning,
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
class Configuration
|
|
5
|
+
ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
|
|
6
|
+
|
|
7
|
+
attr_reader :config_path, :swarm_name, :lead_agent, :agents, :all_agents_config, :swarm_hooks, :all_agents_hooks, :scratchpad_enabled
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def load(path)
|
|
11
|
+
new(path).tap(&:load_and_validate)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(config_path)
|
|
16
|
+
@config_path = Pathname.new(config_path).expand_path
|
|
17
|
+
@config_dir = @config_path.dirname
|
|
18
|
+
@agents = {}
|
|
19
|
+
@all_agents_config = {} # Settings applied to all agents
|
|
20
|
+
@swarm_hooks = {} # Swarm-level hooks (swarm_start, swarm_stop)
|
|
21
|
+
@all_agents_hooks = {} # Hooks applied to all agents
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def load_and_validate
|
|
25
|
+
@config = YAML.load_file(@config_path, aliases: true)
|
|
26
|
+
|
|
27
|
+
unless @config.is_a?(Hash)
|
|
28
|
+
raise ConfigurationError, "Invalid YAML syntax: configuration must be a Hash"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@config = Utils.symbolize_keys(@config)
|
|
32
|
+
interpolate_env_vars!(@config)
|
|
33
|
+
validate_version
|
|
34
|
+
load_all_agents_config
|
|
35
|
+
load_hooks_config
|
|
36
|
+
validate_swarm
|
|
37
|
+
load_agents
|
|
38
|
+
detect_circular_dependencies
|
|
39
|
+
self
|
|
40
|
+
rescue Errno::ENOENT
|
|
41
|
+
raise ConfigurationError, "Configuration file not found: #{@config_path}"
|
|
42
|
+
rescue Psych::SyntaxError => e
|
|
43
|
+
raise ConfigurationError, "Invalid YAML syntax: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def agent_names
|
|
47
|
+
@agents.keys
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def connections_for(agent_name)
|
|
51
|
+
@agents[agent_name]&.delegates_to || []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Convert configuration to Swarm instance using Ruby API
|
|
55
|
+
#
|
|
56
|
+
# This method bridges YAML configuration to the Ruby API, making YAML
|
|
57
|
+
# a thin convenience layer over the programmatic interface.
|
|
58
|
+
#
|
|
59
|
+
# @return [Swarm] Configured swarm instance
|
|
60
|
+
def to_swarm
|
|
61
|
+
swarm = Swarm.new(
|
|
62
|
+
name: @swarm_name,
|
|
63
|
+
global_concurrency: Swarm::DEFAULT_GLOBAL_CONCURRENCY,
|
|
64
|
+
default_local_concurrency: Swarm::DEFAULT_LOCAL_CONCURRENCY,
|
|
65
|
+
scratchpad_enabled: @scratchpad_enabled,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Add all agents - pass definitions directly
|
|
69
|
+
@agents.each do |_name, agent_def|
|
|
70
|
+
swarm.add_agent(agent_def)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Set lead agent
|
|
74
|
+
swarm.lead = @lead_agent
|
|
75
|
+
|
|
76
|
+
swarm
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def interpolate_env_vars!(obj)
|
|
82
|
+
case obj
|
|
83
|
+
when String
|
|
84
|
+
interpolate_env_string(obj)
|
|
85
|
+
when Hash
|
|
86
|
+
obj.transform_values! { |v| interpolate_env_vars!(v) }
|
|
87
|
+
when Array
|
|
88
|
+
obj.map! { |v| interpolate_env_vars!(v) }
|
|
89
|
+
else
|
|
90
|
+
obj
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def interpolate_env_string(str)
|
|
95
|
+
str.gsub(ENV_VAR_WITH_DEFAULT_PATTERN) do |_match|
|
|
96
|
+
env_var = Regexp.last_match(1)
|
|
97
|
+
has_default = Regexp.last_match(2)
|
|
98
|
+
default_value = Regexp.last_match(3)
|
|
99
|
+
|
|
100
|
+
if ENV.key?(env_var)
|
|
101
|
+
ENV[env_var]
|
|
102
|
+
elsif has_default
|
|
103
|
+
default_value || ""
|
|
104
|
+
else
|
|
105
|
+
raise ConfigurationError, "Environment variable '#{env_var}' is not set"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def validate_version
|
|
111
|
+
version = @config[:version]
|
|
112
|
+
raise ConfigurationError, "Missing 'version' field in configuration" unless version
|
|
113
|
+
raise ConfigurationError, "SwarmSDK requires version: 2 configuration. Got version: #{version}" unless version == 2
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def load_all_agents_config
|
|
117
|
+
return unless @config[:swarm]
|
|
118
|
+
|
|
119
|
+
@all_agents_config = @config[:swarm][:all_agents] || {}
|
|
120
|
+
|
|
121
|
+
# Convert disable_default_tools array elements to symbols
|
|
122
|
+
if @all_agents_config[:disable_default_tools].is_a?(Array)
|
|
123
|
+
@all_agents_config[:disable_default_tools] = @all_agents_config[:disable_default_tools].map(&:to_sym)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def load_hooks_config
|
|
128
|
+
return unless @config[:swarm]
|
|
129
|
+
|
|
130
|
+
# Load swarm-level hooks (only swarm_start, swarm_stop allowed)
|
|
131
|
+
@swarm_hooks = Utils.symbolize_keys(@config[:swarm][:hooks] || {})
|
|
132
|
+
|
|
133
|
+
# Load all_agents hooks (applied as swarm defaults)
|
|
134
|
+
if @config[:swarm][:all_agents]
|
|
135
|
+
@all_agents_hooks = Utils.symbolize_keys(@config[:swarm][:all_agents][:hooks] || {})
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def validate_swarm
|
|
140
|
+
raise ConfigurationError, "Missing 'swarm' field in configuration" unless @config[:swarm]
|
|
141
|
+
|
|
142
|
+
swarm = @config[:swarm]
|
|
143
|
+
raise ConfigurationError, "Missing 'name' field in swarm configuration" unless swarm[:name]
|
|
144
|
+
raise ConfigurationError, "Missing 'agents' field in swarm configuration" unless swarm[:agents]
|
|
145
|
+
raise ConfigurationError, "Missing 'lead' field in swarm configuration" unless swarm[:lead]
|
|
146
|
+
raise ConfigurationError, "No agents defined" if swarm[:agents].empty?
|
|
147
|
+
|
|
148
|
+
@swarm_name = swarm[:name]
|
|
149
|
+
@lead_agent = swarm[:lead].to_sym # Convert to symbol for consistency
|
|
150
|
+
@scratchpad_enabled = swarm[:use_scratchpad].nil? ? true : swarm[:use_scratchpad] # Default: enabled
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def load_agents
|
|
154
|
+
swarm_agents = @config[:swarm][:agents]
|
|
155
|
+
|
|
156
|
+
swarm_agents.each do |name, agent_config|
|
|
157
|
+
# Support three formats:
|
|
158
|
+
# 1. String: assistant: "agents/assistant.md" (file path)
|
|
159
|
+
# 2. Hash with agent_file: assistant: { agent_file: "..." }
|
|
160
|
+
# 3. Hash with inline definition: assistant: { description: "...", model: "..." }
|
|
161
|
+
|
|
162
|
+
if agent_config.is_a?(String)
|
|
163
|
+
# Format 1: Direct file path as string
|
|
164
|
+
file_path = agent_config
|
|
165
|
+
merged_config = merge_all_agents_config({})
|
|
166
|
+
@agents[name] = load_agent_from_file(name, file_path, merged_config)
|
|
167
|
+
else
|
|
168
|
+
# Format 2 or 3: Hash configuration
|
|
169
|
+
agent_config ||= {}
|
|
170
|
+
|
|
171
|
+
# Merge all_agents_config into agent config
|
|
172
|
+
# Agent-specific config overrides all_agents config
|
|
173
|
+
merged_config = merge_all_agents_config(agent_config)
|
|
174
|
+
|
|
175
|
+
@agents[name] = if agent_config[:agent_file]
|
|
176
|
+
# Format 2: Hash with agent_file key
|
|
177
|
+
load_agent_from_file(name, agent_config[:agent_file], merged_config)
|
|
178
|
+
else
|
|
179
|
+
# Format 3: Inline definition
|
|
180
|
+
Agent::Definition.new(name, merged_config)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
unless @agents.key?(@lead_agent)
|
|
186
|
+
raise ConfigurationError, "Lead agent '#{@lead_agent}' not found in agents"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Merge all_agents config with agent-specific config
|
|
191
|
+
# Agent config takes precedence over all_agents config
|
|
192
|
+
#
|
|
193
|
+
# Merge strategy:
|
|
194
|
+
# - Arrays (tools, delegates_to): Concatenate
|
|
195
|
+
# - Hashes (parameters, headers): Merge (agent values override)
|
|
196
|
+
# - Scalars (model, provider, base_url, timeout, coding_agent): Agent overrides
|
|
197
|
+
#
|
|
198
|
+
# @param agent_config [Hash] Agent-specific configuration
|
|
199
|
+
# @return [Hash] Merged configuration
|
|
200
|
+
def merge_all_agents_config(agent_config)
|
|
201
|
+
merged = @all_agents_config.dup
|
|
202
|
+
|
|
203
|
+
# For arrays, concatenate
|
|
204
|
+
# For hashes, merge (agent values override)
|
|
205
|
+
# For scalars, agent value overrides
|
|
206
|
+
agent_config.each do |key, value|
|
|
207
|
+
case key
|
|
208
|
+
when :tools
|
|
209
|
+
# Concatenate tools: all_agents.tools + agent.tools
|
|
210
|
+
merged[:tools] = Array(merged[:tools]) + Array(value)
|
|
211
|
+
when :delegates_to
|
|
212
|
+
# Concatenate delegates_to
|
|
213
|
+
merged[:delegates_to] = Array(merged[:delegates_to]) + Array(value)
|
|
214
|
+
when :parameters
|
|
215
|
+
# Merge parameters: all_agents.parameters + agent.parameters
|
|
216
|
+
# Agent values override all_agents values for same keys
|
|
217
|
+
merged[:parameters] = (merged[:parameters] || {}).merge(value || {})
|
|
218
|
+
when :headers
|
|
219
|
+
# Merge headers: all_agents.headers + agent.headers
|
|
220
|
+
# Agent values override all_agents values for same keys
|
|
221
|
+
merged[:headers] = (merged[:headers] || {}).merge(value || {})
|
|
222
|
+
when :disable_default_tools
|
|
223
|
+
# Convert array elements to symbols if it's an array
|
|
224
|
+
merged[key] = value.is_a?(Array) ? value.map(&:to_sym) : value
|
|
225
|
+
else
|
|
226
|
+
# For everything else (model, provider, base_url, timeout, coding_agent, etc.),
|
|
227
|
+
# agent value overrides all_agents value
|
|
228
|
+
merged[key] = value
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Pass all_agents permissions as default_permissions for backward compat with AgentDefinition
|
|
233
|
+
if @all_agents_config[:permissions]
|
|
234
|
+
merged[:default_permissions] = @all_agents_config[:permissions]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
merged
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def load_agent_from_file(name, file_path, merged_config)
|
|
241
|
+
agent_file_path = resolve_agent_file_path(file_path)
|
|
242
|
+
|
|
243
|
+
unless File.exist?(agent_file_path)
|
|
244
|
+
raise ConfigurationError, "Agent file not found: #{agent_file_path}"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
content = File.read(agent_file_path)
|
|
248
|
+
# Parse markdown and merge with YAML config
|
|
249
|
+
agent_def_from_file = MarkdownParser.parse(content, name)
|
|
250
|
+
|
|
251
|
+
# Merge: YAML config overrides markdown file (YAML takes precedence)
|
|
252
|
+
# This allows YAML to override any settings from the markdown file
|
|
253
|
+
final_config = agent_def_from_file.to_h.compact.merge(merged_config.compact)
|
|
254
|
+
|
|
255
|
+
Agent::Definition.new(name, final_config)
|
|
256
|
+
rescue StandardError => e
|
|
257
|
+
raise ConfigurationError, "Error loading agent '#{name}' from file '#{file_path}': #{e.message}"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def resolve_agent_file_path(file_path)
|
|
261
|
+
return file_path if Pathname.new(file_path).absolute?
|
|
262
|
+
|
|
263
|
+
@config_dir.join(file_path).to_s
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def detect_circular_dependencies
|
|
267
|
+
@agents.each_key do |agent_name|
|
|
268
|
+
visited = Set.new
|
|
269
|
+
path = []
|
|
270
|
+
detect_cycle_from(agent_name, visited, path)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def detect_cycle_from(agent_name, visited, path)
|
|
275
|
+
return if visited.include?(agent_name)
|
|
276
|
+
|
|
277
|
+
if path.include?(agent_name)
|
|
278
|
+
cycle_start = path.index(agent_name)
|
|
279
|
+
cycle = path[cycle_start..] + [agent_name]
|
|
280
|
+
raise CircularDependencyError, "Circular dependency detected: #{cycle.join(" -> ")}"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
path.push(agent_name)
|
|
284
|
+
connections_for(agent_name).each do |connection|
|
|
285
|
+
connection_sym = connection.to_sym # Convert to symbol for lookup
|
|
286
|
+
unless @agents.key?(connection_sym)
|
|
287
|
+
raise ConfigurationError, "Agent '#{agent_name}' has connection to unknown agent '#{connection}'"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
detect_cycle_from(connection_sym, visited, path)
|
|
291
|
+
end
|
|
292
|
+
path.pop
|
|
293
|
+
visited.add(agent_name)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
class ContextCompactor
|
|
5
|
+
# Metrics tracks compression statistics
|
|
6
|
+
#
|
|
7
|
+
# Provides detailed information about the compression operation:
|
|
8
|
+
# - Message counts (before/after)
|
|
9
|
+
# - Token counts (before/after)
|
|
10
|
+
# - Compression ratio
|
|
11
|
+
# - Time taken
|
|
12
|
+
# - Summary of changes
|
|
13
|
+
#
|
|
14
|
+
# ## Usage
|
|
15
|
+
#
|
|
16
|
+
# metrics = agent.compact_context
|
|
17
|
+
# puts metrics.summary
|
|
18
|
+
# puts "Compressed from #{metrics.original_tokens} to #{metrics.compressed_tokens} tokens"
|
|
19
|
+
# puts "Compression ratio: #{(metrics.compression_ratio * 100).round(1)}%"
|
|
20
|
+
#
|
|
21
|
+
class Metrics
|
|
22
|
+
attr_reader :original_messages, :compressed_messages, :time_taken
|
|
23
|
+
|
|
24
|
+
# Initialize metrics from compression operation
|
|
25
|
+
#
|
|
26
|
+
# @param original_messages [Array<RubyLLM::Message>] Messages before compression
|
|
27
|
+
# @param compressed_messages [Array<RubyLLM::Message>] Messages after compression
|
|
28
|
+
# @param time_taken [Float] Time taken in seconds
|
|
29
|
+
def initialize(original_messages:, compressed_messages:, time_taken:)
|
|
30
|
+
@original_messages = original_messages
|
|
31
|
+
@compressed_messages = compressed_messages
|
|
32
|
+
@time_taken = time_taken
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Number of messages before compression
|
|
36
|
+
#
|
|
37
|
+
# @return [Integer] Original message count
|
|
38
|
+
def original_message_count
|
|
39
|
+
@original_messages.size
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Number of messages after compression
|
|
43
|
+
#
|
|
44
|
+
# @return [Integer] Compressed message count
|
|
45
|
+
def compressed_message_count
|
|
46
|
+
@compressed_messages.size
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Number of messages removed
|
|
50
|
+
#
|
|
51
|
+
# @return [Integer] Messages removed
|
|
52
|
+
def messages_removed
|
|
53
|
+
original_message_count - compressed_message_count
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Number of checkpoint summary messages created
|
|
57
|
+
#
|
|
58
|
+
# @return [Integer] Checkpoint messages
|
|
59
|
+
def messages_summarized
|
|
60
|
+
@compressed_messages.count do |msg|
|
|
61
|
+
msg.role == :system && msg.content.to_s.include?("CONVERSATION CHECKPOINT")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Estimated tokens before compression
|
|
66
|
+
#
|
|
67
|
+
# @return [Integer] Original token count
|
|
68
|
+
def original_tokens
|
|
69
|
+
@original_tokens ||= TokenCounter.estimate_messages(@original_messages)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Estimated tokens after compression
|
|
73
|
+
#
|
|
74
|
+
# @return [Integer] Compressed token count
|
|
75
|
+
def compressed_tokens
|
|
76
|
+
@compressed_tokens ||= TokenCounter.estimate_messages(@compressed_messages)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Number of tokens removed
|
|
80
|
+
#
|
|
81
|
+
# @return [Integer] Tokens removed
|
|
82
|
+
def tokens_removed
|
|
83
|
+
original_tokens - compressed_tokens
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Compression ratio (compressed / original)
|
|
87
|
+
#
|
|
88
|
+
# @return [Float] Ratio between 0.0 and 1.0
|
|
89
|
+
def compression_ratio
|
|
90
|
+
return 0.0 if original_tokens.zero?
|
|
91
|
+
|
|
92
|
+
compressed_tokens.to_f / original_tokens
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Compression factor (original / compressed)
|
|
96
|
+
#
|
|
97
|
+
# e.g., 5.0 means compressed to 1/5th of original size
|
|
98
|
+
#
|
|
99
|
+
# @return [Float] Compression factor
|
|
100
|
+
def compression_factor
|
|
101
|
+
return 0.0 if compressed_tokens.zero?
|
|
102
|
+
|
|
103
|
+
original_tokens.to_f / compressed_tokens
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Compression percentage
|
|
107
|
+
#
|
|
108
|
+
# @return [Float] Percentage of original size (0-100)
|
|
109
|
+
def compression_percentage
|
|
110
|
+
(compression_ratio * 100).round(2)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Generate a human-readable summary
|
|
114
|
+
#
|
|
115
|
+
# @return [String] Summary text
|
|
116
|
+
def summary
|
|
117
|
+
<<~SUMMARY
|
|
118
|
+
Context Compression Results:
|
|
119
|
+
- Messages: #{original_message_count} → #{compressed_message_count} (-#{messages_removed})
|
|
120
|
+
- Estimated tokens: #{original_tokens} → #{compressed_tokens} (-#{tokens_removed})
|
|
121
|
+
- Compression ratio: #{compression_factor.round(1)}:1 (#{compression_percentage}%)
|
|
122
|
+
- Checkpoints created: #{messages_summarized}
|
|
123
|
+
- Time taken: #{time_taken.round(3)}s
|
|
124
|
+
SUMMARY
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Convert metrics to hash for logging
|
|
128
|
+
#
|
|
129
|
+
# @return [Hash] Metrics as hash
|
|
130
|
+
def to_h
|
|
131
|
+
{
|
|
132
|
+
original_message_count: original_message_count,
|
|
133
|
+
compressed_message_count: compressed_message_count,
|
|
134
|
+
messages_removed: messages_removed,
|
|
135
|
+
messages_summarized: messages_summarized,
|
|
136
|
+
original_tokens: original_tokens,
|
|
137
|
+
compressed_tokens: compressed_tokens,
|
|
138
|
+
tokens_removed: tokens_removed,
|
|
139
|
+
compression_ratio: compression_ratio.round(4),
|
|
140
|
+
compression_factor: compression_factor.round(2),
|
|
141
|
+
compression_percentage: compression_percentage,
|
|
142
|
+
time_taken: time_taken.round(3),
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|