claude_swarm 1.0.1 → 1.0.4
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 +7 -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 +249 -6
- data/EXAMPLES.md +0 -164
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Agent
|
|
5
|
+
# Agent definition encapsulates agent configuration and builds system prompts
|
|
6
|
+
#
|
|
7
|
+
# This class is responsible for:
|
|
8
|
+
# - Parsing and validating agent configuration
|
|
9
|
+
# - Building the full system prompt (base + custom)
|
|
10
|
+
# - Handling tool permissions
|
|
11
|
+
# - Managing hooks (both DSL Ruby blocks and YAML shell commands)
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# definition = Agent::Definition.new(:backend, {
|
|
15
|
+
# description: "Backend API developer",
|
|
16
|
+
# model: "gpt-5",
|
|
17
|
+
# tools: [:Read, :Write, :Bash],
|
|
18
|
+
# system_prompt: "You build APIs"
|
|
19
|
+
# })
|
|
20
|
+
class Definition
|
|
21
|
+
DEFAULT_MODEL = "gpt-5"
|
|
22
|
+
DEFAULT_PROVIDER = "openai"
|
|
23
|
+
DEFAULT_TIMEOUT = 300 # 5 minutes - reasoning models can take a while
|
|
24
|
+
BASE_SYSTEM_PROMPT_PATH = File.expand_path("../prompts/base_system_prompt.md.erb", __dir__)
|
|
25
|
+
|
|
26
|
+
attr_reader :name,
|
|
27
|
+
:description,
|
|
28
|
+
:model,
|
|
29
|
+
:context_window,
|
|
30
|
+
:directory,
|
|
31
|
+
:tools,
|
|
32
|
+
:delegates_to,
|
|
33
|
+
:system_prompt,
|
|
34
|
+
:provider,
|
|
35
|
+
:base_url,
|
|
36
|
+
:api_version,
|
|
37
|
+
:mcp_servers,
|
|
38
|
+
:parameters,
|
|
39
|
+
:headers,
|
|
40
|
+
:timeout,
|
|
41
|
+
:disable_default_tools,
|
|
42
|
+
:coding_agent,
|
|
43
|
+
:default_permissions,
|
|
44
|
+
:agent_permissions,
|
|
45
|
+
:assume_model_exists,
|
|
46
|
+
:hooks,
|
|
47
|
+
:memory
|
|
48
|
+
|
|
49
|
+
attr_accessor :bypass_permissions, :max_concurrent_tools
|
|
50
|
+
|
|
51
|
+
def initialize(name, config = {})
|
|
52
|
+
@name = name.to_sym
|
|
53
|
+
|
|
54
|
+
# BREAKING CHANGE: Hard error for plural form
|
|
55
|
+
if config[:directories]
|
|
56
|
+
raise ConfigurationError,
|
|
57
|
+
"The 'directories' (plural) configuration is no longer supported in SwarmSDK 1.0+.\n\n" \
|
|
58
|
+
"Change 'directories:' to 'directory:' (singular).\n\n" \
|
|
59
|
+
"If you need access to multiple directories, use permissions:\n\n " \
|
|
60
|
+
"directory: 'backend/'\n " \
|
|
61
|
+
"permissions do\n " \
|
|
62
|
+
"tool(:Read).allow_paths('../shared/**')\n " \
|
|
63
|
+
"end"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@description = config[:description]
|
|
67
|
+
@model = config[:model] || DEFAULT_MODEL
|
|
68
|
+
@provider = config[:provider] || DEFAULT_PROVIDER
|
|
69
|
+
@base_url = config[:base_url]
|
|
70
|
+
@api_version = config[:api_version]
|
|
71
|
+
@context_window = config[:context_window] # Explicit context window override
|
|
72
|
+
@parameters = config[:parameters] || {}
|
|
73
|
+
@headers = Utils.stringify_keys(config[:headers] || {})
|
|
74
|
+
@timeout = config[:timeout] || DEFAULT_TIMEOUT
|
|
75
|
+
@bypass_permissions = config[:bypass_permissions] || false
|
|
76
|
+
@max_concurrent_tools = config[:max_concurrent_tools]
|
|
77
|
+
# Always assume model exists - SwarmSDK validates models separately using models.json
|
|
78
|
+
# This prevents RubyLLM from trying to validate models in its registry
|
|
79
|
+
@assume_model_exists = true
|
|
80
|
+
|
|
81
|
+
# disable_default_tools can be:
|
|
82
|
+
# - nil/not set: include all default tools (default behavior)
|
|
83
|
+
# - true: disable ALL default tools
|
|
84
|
+
# - Array of symbols: disable specific tools (e.g., [:Think, :TodoWrite])
|
|
85
|
+
@disable_default_tools = config[:disable_default_tools]
|
|
86
|
+
|
|
87
|
+
# coding_agent defaults to false if not specified
|
|
88
|
+
# When true, includes the base system prompt for coding tasks
|
|
89
|
+
# When false, uses only the custom system prompt (no base prompt)
|
|
90
|
+
@coding_agent = config.key?(:coding_agent) ? config[:coding_agent] : false
|
|
91
|
+
|
|
92
|
+
# Parse directory first so it can be used in system prompt rendering
|
|
93
|
+
@directory = parse_directory(config[:directory])
|
|
94
|
+
|
|
95
|
+
# Parse memory configuration BEFORE building system prompt
|
|
96
|
+
# (memory prompt needs to be appended if memory is enabled)
|
|
97
|
+
@memory = parse_memory_config(config[:memory])
|
|
98
|
+
|
|
99
|
+
# Build system prompt after directory and memory are set
|
|
100
|
+
@system_prompt = build_full_system_prompt(config[:system_prompt])
|
|
101
|
+
|
|
102
|
+
# Parse tools with permissions support
|
|
103
|
+
@default_permissions = config[:default_permissions] || {}
|
|
104
|
+
@agent_permissions = config[:permissions] || {}
|
|
105
|
+
@tools = parse_tools_with_permissions(
|
|
106
|
+
config[:tools],
|
|
107
|
+
@default_permissions,
|
|
108
|
+
@agent_permissions,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Inject default write restrictions for security
|
|
112
|
+
@tools = inject_default_write_permissions(@tools)
|
|
113
|
+
|
|
114
|
+
@delegates_to = Array(config[:delegates_to] || []).map(&:to_sym)
|
|
115
|
+
@mcp_servers = Array(config[:mcp_servers] || [])
|
|
116
|
+
|
|
117
|
+
# Parse hooks configuration
|
|
118
|
+
# Handles both DSL (HookDefinition objects) and YAML (raw hash) formats
|
|
119
|
+
@hooks = parse_hooks(config[:hooks])
|
|
120
|
+
|
|
121
|
+
validate!
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check if memory is enabled for this agent
|
|
125
|
+
#
|
|
126
|
+
# @return [Boolean]
|
|
127
|
+
def memory_enabled?
|
|
128
|
+
return false if @memory.nil?
|
|
129
|
+
|
|
130
|
+
# MemoryConfig object (from DSL)
|
|
131
|
+
return @memory.enabled? if @memory.respond_to?(:enabled?)
|
|
132
|
+
|
|
133
|
+
# Hash (from YAML) - check for directory key
|
|
134
|
+
if @memory.is_a?(Hash)
|
|
135
|
+
directory = @memory[:directory] || @memory["directory"]
|
|
136
|
+
return !directory.nil? && !directory.to_s.strip.empty?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Parse memory configuration from Hash or MemoryConfig object
|
|
143
|
+
#
|
|
144
|
+
# @param memory_config [Hash, Object, nil] Memory configuration
|
|
145
|
+
# @return [Object, Hash, nil] Memory config (could be MemoryConfig from swarm_memory or Hash)
|
|
146
|
+
def parse_memory_config(memory_config)
|
|
147
|
+
return if memory_config.nil?
|
|
148
|
+
|
|
149
|
+
# If it's a MemoryConfig object (duck typing - has directory, adapter, mode methods)
|
|
150
|
+
# return as-is. This could be SwarmMemory::DSL::MemoryConfig or any compatible object.
|
|
151
|
+
return memory_config if memory_config.respond_to?(:directory) &&
|
|
152
|
+
memory_config.respond_to?(:adapter) &&
|
|
153
|
+
memory_config.respond_to?(:enabled?)
|
|
154
|
+
|
|
155
|
+
# If it's a hash (from YAML), keep it as a hash
|
|
156
|
+
# Plugin will create storage adapter based on the hash values
|
|
157
|
+
memory_config
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def to_h
|
|
161
|
+
{
|
|
162
|
+
name: @name,
|
|
163
|
+
description: @description,
|
|
164
|
+
model: SwarmSDK::Models.resolve_alias(@model), # Resolve model aliases
|
|
165
|
+
directory: @directory,
|
|
166
|
+
tools: @tools,
|
|
167
|
+
delegates_to: @delegates_to,
|
|
168
|
+
system_prompt: @system_prompt,
|
|
169
|
+
provider: @provider,
|
|
170
|
+
base_url: @base_url,
|
|
171
|
+
api_version: @api_version,
|
|
172
|
+
mcp_servers: @mcp_servers,
|
|
173
|
+
parameters: @parameters,
|
|
174
|
+
headers: @headers,
|
|
175
|
+
timeout: @timeout,
|
|
176
|
+
bypass_permissions: @bypass_permissions,
|
|
177
|
+
disable_default_tools: @disable_default_tools,
|
|
178
|
+
coding_agent: @coding_agent,
|
|
179
|
+
assume_model_exists: @assume_model_exists,
|
|
180
|
+
max_concurrent_tools: @max_concurrent_tools,
|
|
181
|
+
hooks: @hooks,
|
|
182
|
+
}.compact
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Validate agent configuration and return warnings (non-fatal issues)
|
|
186
|
+
#
|
|
187
|
+
# Unlike validate! which raises exceptions for critical errors, this method
|
|
188
|
+
# returns an array of warning hashes for non-fatal issues like:
|
|
189
|
+
# - Model not found in registry (informs user, suggests alternatives)
|
|
190
|
+
# - Context tracking unavailable (useful even with assume_model_exists)
|
|
191
|
+
#
|
|
192
|
+
# Note: Validation ALWAYS runs, even with assume_model_exists: true or base_url set.
|
|
193
|
+
# The purpose is to inform the user about potential issues and suggest corrections,
|
|
194
|
+
# not to block execution.
|
|
195
|
+
#
|
|
196
|
+
# @return [Array<Hash>] Array of warning hashes
|
|
197
|
+
def validate
|
|
198
|
+
warnings = []
|
|
199
|
+
|
|
200
|
+
# Always validate model (even with assume_model_exists)
|
|
201
|
+
# Warnings inform user about typos and context tracking limitations
|
|
202
|
+
model_warning = validate_model
|
|
203
|
+
warnings << model_warning if model_warning
|
|
204
|
+
|
|
205
|
+
# Future: could add tool validation, delegate validation, etc.
|
|
206
|
+
|
|
207
|
+
warnings
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
private
|
|
211
|
+
|
|
212
|
+
# Validate that model exists in SwarmSDK's model registry
|
|
213
|
+
#
|
|
214
|
+
# Uses SwarmSDK's static models.json instead of RubyLLM's dynamic registry.
|
|
215
|
+
# This provides stable, offline model validation without network calls.
|
|
216
|
+
#
|
|
217
|
+
# Process:
|
|
218
|
+
# 1. Try to find model directly in models.json
|
|
219
|
+
# 2. If not found, try to resolve as alias and find again
|
|
220
|
+
# 3. If still not found, return warning with suggestions
|
|
221
|
+
#
|
|
222
|
+
# @return [Hash, nil] Warning hash if model not found, nil otherwise
|
|
223
|
+
def validate_model
|
|
224
|
+
# Try direct lookup first
|
|
225
|
+
model_data = SwarmSDK::Models.all.find { |m| (m["id"] || m[:id]) == @model }
|
|
226
|
+
|
|
227
|
+
# If not found, try alias resolution
|
|
228
|
+
unless model_data
|
|
229
|
+
resolved_id = SwarmSDK::Models.resolve_alias(@model)
|
|
230
|
+
# Only search again if alias was different
|
|
231
|
+
if resolved_id != @model
|
|
232
|
+
model_data = SwarmSDK::Models.all.find { |m| (m["id"] || m[:id]) == resolved_id }
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
if model_data
|
|
237
|
+
nil # Model exists (either directly or via alias)
|
|
238
|
+
else
|
|
239
|
+
# Model not found - return warning with suggestions
|
|
240
|
+
{
|
|
241
|
+
type: :model_not_found,
|
|
242
|
+
agent: @name,
|
|
243
|
+
model: @model,
|
|
244
|
+
error_message: "Unknown model: #{@model}",
|
|
245
|
+
suggestions: SwarmSDK::Models.suggest_similar(@model),
|
|
246
|
+
}
|
|
247
|
+
end
|
|
248
|
+
rescue StandardError => e
|
|
249
|
+
# Return warning on error
|
|
250
|
+
{
|
|
251
|
+
type: :model_not_found,
|
|
252
|
+
agent: @name,
|
|
253
|
+
model: @model,
|
|
254
|
+
error_message: e.message,
|
|
255
|
+
suggestions: [],
|
|
256
|
+
}
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def build_full_system_prompt(custom_prompt)
|
|
260
|
+
# Build the base prompt based on coding_agent setting
|
|
261
|
+
prompt = if @coding_agent
|
|
262
|
+
# Coding agent: include full base prompt
|
|
263
|
+
rendered_base = render_base_system_prompt
|
|
264
|
+
|
|
265
|
+
if custom_prompt && !custom_prompt.strip.empty?
|
|
266
|
+
"#{rendered_base}\n\n#{custom_prompt}"
|
|
267
|
+
else
|
|
268
|
+
rendered_base
|
|
269
|
+
end
|
|
270
|
+
elsif default_tools_enabled?
|
|
271
|
+
# Non-coding agent: optionally include TODO/Scratchpad sections if default tools available
|
|
272
|
+
non_coding_base = render_non_coding_base_prompt
|
|
273
|
+
|
|
274
|
+
if custom_prompt && !custom_prompt.strip.empty?
|
|
275
|
+
# Prepend TODO/Scratchpad info before custom prompt
|
|
276
|
+
"#{non_coding_base}\n\n#{custom_prompt}"
|
|
277
|
+
else
|
|
278
|
+
# No custom prompt: just return TODO/Scratchpad info
|
|
279
|
+
non_coding_base
|
|
280
|
+
end
|
|
281
|
+
else
|
|
282
|
+
# No default tools: return only custom prompt
|
|
283
|
+
(custom_prompt || "").to_s
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Append plugin contributions to system prompt
|
|
287
|
+
plugin_contributions = collect_plugin_prompt_contributions
|
|
288
|
+
if plugin_contributions.any?
|
|
289
|
+
combined_contributions = plugin_contributions.join("\n\n")
|
|
290
|
+
prompt = if prompt && !prompt.strip.empty?
|
|
291
|
+
"#{prompt}\n\n#{combined_contributions}"
|
|
292
|
+
else
|
|
293
|
+
combined_contributions
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
prompt
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Check if default tools are enabled (i.e., not disabled)
|
|
301
|
+
#
|
|
302
|
+
# @return [Boolean] True if default tools should be included
|
|
303
|
+
def default_tools_enabled?
|
|
304
|
+
@disable_default_tools != true
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def render_base_system_prompt
|
|
308
|
+
cwd = @directory || Dir.pwd
|
|
309
|
+
platform = RUBY_PLATFORM
|
|
310
|
+
os_version = begin
|
|
311
|
+
%x(uname -sr 2>/dev/null).strip
|
|
312
|
+
rescue
|
|
313
|
+
RUBY_PLATFORM
|
|
314
|
+
end
|
|
315
|
+
date = Time.now.strftime("%Y-%m-%d")
|
|
316
|
+
|
|
317
|
+
template_content = File.read(BASE_SYSTEM_PROMPT_PATH)
|
|
318
|
+
ERB.new(template_content).result(binding)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Collect system prompt contributions from all plugins
|
|
322
|
+
#
|
|
323
|
+
# Asks each registered plugin if it wants to contribute to the system prompt.
|
|
324
|
+
# Plugins can return custom instructions based on their configuration.
|
|
325
|
+
#
|
|
326
|
+
# @return [Array<String>] Array of prompt contributions from plugins
|
|
327
|
+
def collect_plugin_prompt_contributions
|
|
328
|
+
contributions = []
|
|
329
|
+
|
|
330
|
+
PluginRegistry.all.each do |plugin|
|
|
331
|
+
# Check if plugin has storage enabled for this agent
|
|
332
|
+
next unless plugin.storage_enabled?(self)
|
|
333
|
+
|
|
334
|
+
# Ask plugin for prompt contribution
|
|
335
|
+
# Note: storage is not available yet at this point, so we pass nil
|
|
336
|
+
contribution = plugin.system_prompt_contribution(agent_definition: self, storage: nil)
|
|
337
|
+
contributions << contribution if contribution && !contribution.strip.empty?
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
contributions
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def render_non_coding_base_prompt
|
|
344
|
+
# Simplified base prompt for non-coding agents
|
|
345
|
+
# Includes environment info, TODO, and Scratchpad tool information
|
|
346
|
+
# Does not steer towards coding tasks
|
|
347
|
+
cwd = @directory || Dir.pwd
|
|
348
|
+
platform = RUBY_PLATFORM
|
|
349
|
+
os_version = begin
|
|
350
|
+
%x(uname -sr 2>/dev/null).strip
|
|
351
|
+
rescue
|
|
352
|
+
RUBY_PLATFORM
|
|
353
|
+
end
|
|
354
|
+
date = Time.now.strftime("%Y-%m-%d")
|
|
355
|
+
|
|
356
|
+
<<~PROMPT.strip
|
|
357
|
+
# Today's date
|
|
358
|
+
|
|
359
|
+
<today-date>
|
|
360
|
+
#{date}
|
|
361
|
+
#</today-date>
|
|
362
|
+
|
|
363
|
+
# Current Environment
|
|
364
|
+
|
|
365
|
+
<env>
|
|
366
|
+
Working directory: #{cwd}
|
|
367
|
+
Platform: #{platform}
|
|
368
|
+
OS Version: #{os_version}
|
|
369
|
+
</env>
|
|
370
|
+
|
|
371
|
+
# Task Management
|
|
372
|
+
|
|
373
|
+
You have access to the TodoWrite tool to help you manage and plan tasks. Use this tool to track your progress and give visibility into your work.
|
|
374
|
+
|
|
375
|
+
When working on multi-step tasks:
|
|
376
|
+
1. Create a todo list with all known tasks before starting work
|
|
377
|
+
2. Mark each task as in_progress when you start it
|
|
378
|
+
3. Mark each task as completed IMMEDIATELY after finishing it
|
|
379
|
+
4. Complete ALL pending todos before finishing your response
|
|
380
|
+
|
|
381
|
+
# Scratchpad Storage
|
|
382
|
+
|
|
383
|
+
You have access to Scratchpad tools for storing and retrieving information:
|
|
384
|
+
- **ScratchpadWrite**: Store detailed outputs, analysis, or results that are too long for direct responses
|
|
385
|
+
- **ScratchpadRead**: Retrieve previously stored content
|
|
386
|
+
- **ScratchpadList**: List available scratchpad entries
|
|
387
|
+
|
|
388
|
+
Use the scratchpad to share information that would otherwise clutter your responses.
|
|
389
|
+
PROMPT
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def parse_directory(directory_config)
|
|
393
|
+
directory_config ||= "."
|
|
394
|
+
File.expand_path(directory_config.to_s)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Parse tools configuration with permissions support
|
|
398
|
+
#
|
|
399
|
+
# Tools can be specified as:
|
|
400
|
+
# - Symbol: :Write (no permissions)
|
|
401
|
+
# - Hash: { Write: { allowed_paths: [...] } } (with permissions)
|
|
402
|
+
#
|
|
403
|
+
# Returns array of tool configs:
|
|
404
|
+
# [
|
|
405
|
+
# { name: :Read, permissions: nil },
|
|
406
|
+
# { name: :Write, permissions: { allowed_paths: [...] } }
|
|
407
|
+
# ]
|
|
408
|
+
def parse_tools_with_permissions(tools_config, default_permissions, agent_permissions)
|
|
409
|
+
tools_array = Array(tools_config || [])
|
|
410
|
+
|
|
411
|
+
tools_array.map do |tool_spec|
|
|
412
|
+
case tool_spec
|
|
413
|
+
when Symbol, String
|
|
414
|
+
# Simple tool: :Write or "Write"
|
|
415
|
+
tool_name = tool_spec.to_sym
|
|
416
|
+
permissions = resolve_permissions(tool_name, default_permissions, agent_permissions)
|
|
417
|
+
|
|
418
|
+
{ name: tool_name, permissions: permissions }
|
|
419
|
+
when Hash
|
|
420
|
+
# Check if already in parsed format: { name: :Write, permissions: {...} }
|
|
421
|
+
if tool_spec.key?(:name)
|
|
422
|
+
# Already parsed - pass through as-is
|
|
423
|
+
tool_spec
|
|
424
|
+
else
|
|
425
|
+
# Tool with inline permissions: { Write: { allowed_paths: [...] } }
|
|
426
|
+
tool_name = tool_spec.keys.first.to_sym
|
|
427
|
+
inline_permissions = tool_spec.values.first
|
|
428
|
+
|
|
429
|
+
# Inline permissions override defaults
|
|
430
|
+
{ name: tool_name, permissions: inline_permissions }
|
|
431
|
+
end
|
|
432
|
+
else
|
|
433
|
+
raise ConfigurationError, "Invalid tool specification: #{tool_spec.inspect}"
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Resolve permissions for a tool from defaults and agent-level overrides
|
|
439
|
+
def resolve_permissions(tool_name, default_permissions, agent_permissions)
|
|
440
|
+
# Agent-level permissions override defaults
|
|
441
|
+
agent_permissions[tool_name] || default_permissions[tool_name]
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Inject default write permissions for security
|
|
445
|
+
#
|
|
446
|
+
# Write, Edit, and MultiEdit tools without explicit permissions are automatically
|
|
447
|
+
# restricted to only write within the agent's directory. This prevents accidental
|
|
448
|
+
# writes outside the agent's working scope.
|
|
449
|
+
#
|
|
450
|
+
# Default permission: { allowed_paths: ["**/*"] }
|
|
451
|
+
# This is resolved relative to the agent's directory by the permissions system.
|
|
452
|
+
#
|
|
453
|
+
# Users can override by explicitly setting permissions for these tools.
|
|
454
|
+
def inject_default_write_permissions(tools)
|
|
455
|
+
write_tools = [:Write, :Edit, :MultiEdit]
|
|
456
|
+
|
|
457
|
+
tools.map do |tool_config|
|
|
458
|
+
tool_name = tool_config[:name]
|
|
459
|
+
|
|
460
|
+
# If it's a write tool and has no permissions, inject default
|
|
461
|
+
if write_tools.include?(tool_name) && tool_config[:permissions].nil?
|
|
462
|
+
tool_config.merge(permissions: { allowed_paths: ["**/*"] })
|
|
463
|
+
else
|
|
464
|
+
tool_config
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Parse hooks configuration
|
|
470
|
+
#
|
|
471
|
+
# Handles two input formats:
|
|
472
|
+
#
|
|
473
|
+
# 1. DSL format (from Agent::Builder): Pre-parsed HookDefinition objects
|
|
474
|
+
# { event_type: [HookDefinition, ...] }
|
|
475
|
+
# These are applied directly in pass_4_configure_hooks
|
|
476
|
+
#
|
|
477
|
+
# 2. YAML format: Raw hash with shell command specifications
|
|
478
|
+
# hooks:
|
|
479
|
+
# pre_tool_use:
|
|
480
|
+
# - matcher: "Write|Edit"
|
|
481
|
+
# type: command
|
|
482
|
+
# command: "validate.sh"
|
|
483
|
+
# These are kept raw and processed by Hooks::Adapter in pass_5
|
|
484
|
+
#
|
|
485
|
+
# Returns:
|
|
486
|
+
# - DSL: { event_type: [HookDefinition, ...] }
|
|
487
|
+
# - YAML: Raw hash (for Hooks::Adapter)
|
|
488
|
+
def parse_hooks(hooks_config)
|
|
489
|
+
return {} if hooks_config.nil? || hooks_config.empty?
|
|
490
|
+
|
|
491
|
+
# If already parsed from DSL (HookDefinition objects), return as-is
|
|
492
|
+
if hooks_config.is_a?(Hash) && hooks_config.values.all? { |v| v.is_a?(Array) && v.all? { |item| item.is_a?(Hooks::Definition) } }
|
|
493
|
+
return hooks_config
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# For YAML hooks: validate structure but keep raw for Hooks::Adapter
|
|
497
|
+
validate_yaml_hooks(hooks_config)
|
|
498
|
+
|
|
499
|
+
# Return raw YAML - Hooks::Adapter will process in pass_5
|
|
500
|
+
hooks_config
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Validate YAML hooks structure
|
|
504
|
+
#
|
|
505
|
+
# @param hooks_config [Hash] YAML hooks configuration
|
|
506
|
+
# @return [void]
|
|
507
|
+
def validate_yaml_hooks(hooks_config)
|
|
508
|
+
hooks_config.each do |event_name, hook_specs|
|
|
509
|
+
event_sym = event_name.to_sym
|
|
510
|
+
|
|
511
|
+
# Validate event type
|
|
512
|
+
unless Hooks::Registry::VALID_EVENTS.include?(event_sym)
|
|
513
|
+
raise ConfigurationError,
|
|
514
|
+
"Invalid hook event '#{event_name}' for agent '#{@name}'. " \
|
|
515
|
+
"Valid events: #{Hooks::Registry::VALID_EVENTS.join(", ")}"
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Validate each hook spec structure
|
|
519
|
+
Array(hook_specs).each do |spec|
|
|
520
|
+
hook_type = spec[:type] || spec["type"]
|
|
521
|
+
command = spec[:command] || spec["command"]
|
|
522
|
+
|
|
523
|
+
raise ConfigurationError, "Hook missing 'type' field for event #{event_name}" unless hook_type
|
|
524
|
+
raise ConfigurationError, "Hook missing 'command' field for event #{event_name}" if hook_type.to_s == "command" && !command
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def validate!
|
|
530
|
+
raise ConfigurationError, "Agent '#{@name}' missing required 'description' field" unless @description
|
|
531
|
+
|
|
532
|
+
# Validate api_version can only be set for OpenAI-compatible providers
|
|
533
|
+
if @api_version
|
|
534
|
+
openai_compatible = ["openai", "deepseek", "perplexity", "mistral", "openrouter"]
|
|
535
|
+
unless openai_compatible.include?(@provider.to_s)
|
|
536
|
+
raise ConfigurationError,
|
|
537
|
+
"Agent '#{@name}' has api_version set, but provider is '#{@provider}'. " \
|
|
538
|
+
"api_version can only be used with OpenAI-compatible providers: #{openai_compatible.join(", ")}"
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# Validate api_version value
|
|
542
|
+
valid_versions = ["v1/chat/completions", "v1/responses"]
|
|
543
|
+
unless valid_versions.include?(@api_version)
|
|
544
|
+
raise ConfigurationError,
|
|
545
|
+
"Agent '#{@name}' has invalid api_version '#{@api_version}'. " \
|
|
546
|
+
"Valid values: #{valid_versions.join(", ")}"
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
unless File.directory?(@directory)
|
|
551
|
+
raise ConfigurationError, "Directory '#{@directory}' for agent '#{@name}' does not exist"
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
end
|