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.
Files changed (267) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/release.md +1 -1
  3. data/.claude/hooks/lint-code-files.rb +65 -0
  4. data/.rubocop.yml +22 -2
  5. data/CHANGELOG.md +14 -1
  6. data/CLAUDE.md +1 -1
  7. data/CONTRIBUTING.md +69 -0
  8. data/README.md +27 -2
  9. data/Rakefile +71 -3
  10. data/analyze_coverage.rb +94 -0
  11. data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
  12. data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
  13. data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
  14. data/docs/v2/README.md +308 -0
  15. data/docs/v2/guides/claude-code-agents.md +262 -0
  16. data/docs/v2/guides/complete-tutorial.md +3088 -0
  17. data/docs/v2/guides/getting-started.md +1456 -0
  18. data/docs/v2/guides/memory-adapters.md +998 -0
  19. data/docs/v2/guides/plugins.md +816 -0
  20. data/docs/v2/guides/quick-start-cli.md +1745 -0
  21. data/docs/v2/guides/rails-integration.md +1902 -0
  22. data/docs/v2/guides/swarm-memory.md +599 -0
  23. data/docs/v2/reference/cli.md +729 -0
  24. data/docs/v2/reference/ruby-dsl.md +2154 -0
  25. data/docs/v2/reference/yaml.md +1835 -0
  26. data/docs-team-swarm.yml +2222 -0
  27. data/examples/learning-assistant/assistant.md +7 -0
  28. data/examples/learning-assistant/example-memories/concept-example.md +90 -0
  29. data/examples/learning-assistant/example-memories/experience-example.md +66 -0
  30. data/examples/learning-assistant/example-memories/fact-example.md +76 -0
  31. data/examples/learning-assistant/example-memories/memory-index.md +78 -0
  32. data/examples/learning-assistant/example-memories/skill-example.md +168 -0
  33. data/examples/learning-assistant/learning_assistant.rb +34 -0
  34. data/examples/learning-assistant/learning_assistant.yml +20 -0
  35. data/examples/v2/dsl/01_basic.rb +44 -0
  36. data/examples/v2/dsl/02_core_parameters.rb +59 -0
  37. data/examples/v2/dsl/03_capabilities.rb +71 -0
  38. data/examples/v2/dsl/04_llm_parameters.rb +56 -0
  39. data/examples/v2/dsl/05_advanced_flags.rb +73 -0
  40. data/examples/v2/dsl/06_permissions.rb +80 -0
  41. data/examples/v2/dsl/07_mcp_server.rb +62 -0
  42. data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
  43. data/examples/v2/dsl/09_agent_hooks.rb +67 -0
  44. data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
  45. data/examples/v2/dsl/11_delegation.rb +60 -0
  46. data/examples/v2/dsl/12_complete_integration.rb +137 -0
  47. data/examples/v2/file_tools_swarm.yml +102 -0
  48. data/examples/v2/hooks/01_basic_hooks.rb +133 -0
  49. data/examples/v2/hooks/02_usage_tracking.rb +201 -0
  50. data/examples/v2/hooks/03_production_monitoring.rb +429 -0
  51. data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
  52. data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
  53. data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
  54. data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
  55. data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
  56. data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
  57. data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
  58. data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
  59. data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
  60. data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
  61. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
  62. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
  63. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
  64. data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
  65. data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
  66. data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
  67. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
  68. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
  69. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
  70. data/examples/v2/hooks/swarm_summary.sh +44 -0
  71. data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
  72. data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
  73. data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
  74. data/examples/v2/hooks/validate_bash.rb +59 -0
  75. data/examples/v2/multi_directory_permissions.yml +221 -0
  76. data/examples/v2/node_context_demo.rb +127 -0
  77. data/examples/v2/node_workflow.rb +173 -0
  78. data/examples/v2/path_resolution_demo.rb +216 -0
  79. data/examples/v2/simple-swarm-v2.rb +90 -0
  80. data/examples/v2/simple-swarm-v2.yml +62 -0
  81. data/examples/v2/swarm.yml +71 -0
  82. data/examples/v2/swarm_with_hooks.yml +61 -0
  83. data/examples/v2/swarm_with_hooks_simple.yml +25 -0
  84. data/examples/v2/think_tool_demo.rb +62 -0
  85. data/exe/swarm +6 -0
  86. data/lib/claude_swarm/claude_mcp_server.rb +0 -6
  87. data/lib/claude_swarm/cli.rb +10 -3
  88. data/lib/claude_swarm/commands/ps.rb +19 -20
  89. data/lib/claude_swarm/commands/show.rb +1 -1
  90. data/lib/claude_swarm/configuration.rb +10 -12
  91. data/lib/claude_swarm/mcp_generator.rb +10 -1
  92. data/lib/claude_swarm/orchestrator.rb +73 -49
  93. data/lib/claude_swarm/system_utils.rb +37 -11
  94. data/lib/claude_swarm/version.rb +1 -1
  95. data/lib/claude_swarm/worktree_manager.rb +1 -0
  96. data/lib/claude_swarm/yaml_loader.rb +22 -0
  97. data/lib/claude_swarm.rb +7 -2
  98. data/lib/swarm_cli/cli.rb +201 -0
  99. data/lib/swarm_cli/command_registry.rb +61 -0
  100. data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
  101. data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
  102. data/lib/swarm_cli/commands/migrate.rb +55 -0
  103. data/lib/swarm_cli/commands/run.rb +173 -0
  104. data/lib/swarm_cli/config_loader.rb +97 -0
  105. data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
  106. data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
  107. data/lib/swarm_cli/interactive_repl.rb +918 -0
  108. data/lib/swarm_cli/mcp_serve_options.rb +44 -0
  109. data/lib/swarm_cli/mcp_tools_options.rb +59 -0
  110. data/lib/swarm_cli/migrate_options.rb +54 -0
  111. data/lib/swarm_cli/migrator.rb +132 -0
  112. data/lib/swarm_cli/options.rb +151 -0
  113. data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
  114. data/lib/swarm_cli/ui/components/content_block.rb +120 -0
  115. data/lib/swarm_cli/ui/components/divider.rb +57 -0
  116. data/lib/swarm_cli/ui/components/panel.rb +62 -0
  117. data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
  118. data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
  119. data/lib/swarm_cli/ui/formatters/number.rb +58 -0
  120. data/lib/swarm_cli/ui/formatters/text.rb +77 -0
  121. data/lib/swarm_cli/ui/formatters/time.rb +73 -0
  122. data/lib/swarm_cli/ui/icons.rb +59 -0
  123. data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
  124. data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
  125. data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
  126. data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
  127. data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
  128. data/lib/swarm_cli/version.rb +5 -0
  129. data/lib/swarm_cli.rb +44 -0
  130. data/lib/swarm_memory/adapters/base.rb +141 -0
  131. data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
  132. data/lib/swarm_memory/chat_extension.rb +34 -0
  133. data/lib/swarm_memory/cli/commands.rb +306 -0
  134. data/lib/swarm_memory/core/entry.rb +37 -0
  135. data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
  136. data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
  137. data/lib/swarm_memory/core/path_normalizer.rb +75 -0
  138. data/lib/swarm_memory/core/semantic_index.rb +244 -0
  139. data/lib/swarm_memory/core/storage.rb +288 -0
  140. data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
  141. data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
  142. data/lib/swarm_memory/dsl/memory_config.rb +113 -0
  143. data/lib/swarm_memory/embeddings/embedder.rb +36 -0
  144. data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
  145. data/lib/swarm_memory/errors.rb +21 -0
  146. data/lib/swarm_memory/integration/cli_registration.rb +30 -0
  147. data/lib/swarm_memory/integration/configuration.rb +43 -0
  148. data/lib/swarm_memory/integration/registration.rb +31 -0
  149. data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
  150. data/lib/swarm_memory/optimization/analyzer.rb +244 -0
  151. data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
  152. data/lib/swarm_memory/prompts/memory.md.erb +109 -0
  153. data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
  154. data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
  155. data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
  156. data/lib/swarm_memory/search/semantic_search.rb +112 -0
  157. data/lib/swarm_memory/search/text_search.rb +42 -0
  158. data/lib/swarm_memory/search/text_similarity.rb +80 -0
  159. data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
  160. data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
  161. data/lib/swarm_memory/tools/load_skill.rb +313 -0
  162. data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
  163. data/lib/swarm_memory/tools/memory_delete.rb +99 -0
  164. data/lib/swarm_memory/tools/memory_edit.rb +185 -0
  165. data/lib/swarm_memory/tools/memory_glob.rb +160 -0
  166. data/lib/swarm_memory/tools/memory_grep.rb +247 -0
  167. data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
  168. data/lib/swarm_memory/tools/memory_read.rb +123 -0
  169. data/lib/swarm_memory/tools/memory_write.rb +231 -0
  170. data/lib/swarm_memory/utils.rb +50 -0
  171. data/lib/swarm_memory/version.rb +5 -0
  172. data/lib/swarm_memory.rb +166 -0
  173. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  174. data/lib/swarm_sdk/agent/builder.rb +461 -0
  175. data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
  176. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  177. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
  178. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
  179. data/lib/swarm_sdk/agent/chat.rb +1159 -0
  180. data/lib/swarm_sdk/agent/context.rb +112 -0
  181. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  182. data/lib/swarm_sdk/agent/definition.rb +556 -0
  183. data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
  184. data/lib/swarm_sdk/configuration.rb +296 -0
  185. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  186. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  187. data/lib/swarm_sdk/context_compactor.rb +340 -0
  188. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  189. data/lib/swarm_sdk/hooks/context.rb +197 -0
  190. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  191. data/lib/swarm_sdk/hooks/error.rb +29 -0
  192. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  193. data/lib/swarm_sdk/hooks/registry.rb +147 -0
  194. data/lib/swarm_sdk/hooks/result.rb +150 -0
  195. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  196. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  197. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  198. data/lib/swarm_sdk/log_collector.rb +51 -0
  199. data/lib/swarm_sdk/log_stream.rb +69 -0
  200. data/lib/swarm_sdk/markdown_parser.rb +75 -0
  201. data/lib/swarm_sdk/model_aliases.json +5 -0
  202. data/lib/swarm_sdk/models.json +1 -0
  203. data/lib/swarm_sdk/models.rb +120 -0
  204. data/lib/swarm_sdk/node/agent_config.rb +49 -0
  205. data/lib/swarm_sdk/node/builder.rb +439 -0
  206. data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
  207. data/lib/swarm_sdk/node_context.rb +170 -0
  208. data/lib/swarm_sdk/node_orchestrator.rb +384 -0
  209. data/lib/swarm_sdk/permissions/config.rb +239 -0
  210. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  211. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  212. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  213. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  214. data/lib/swarm_sdk/plugin.rb +147 -0
  215. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  216. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
  217. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  218. data/lib/swarm_sdk/result.rb +97 -0
  219. data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
  220. data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
  221. data/lib/swarm_sdk/swarm/builder.rb +586 -0
  222. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  223. data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
  224. data/lib/swarm_sdk/swarm.rb +982 -0
  225. data/lib/swarm_sdk/tools/bash.rb +274 -0
  226. data/lib/swarm_sdk/tools/clock.rb +44 -0
  227. data/lib/swarm_sdk/tools/delegate.rb +164 -0
  228. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  229. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  230. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  231. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  232. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  233. data/lib/swarm_sdk/tools/edit.rb +150 -0
  234. data/lib/swarm_sdk/tools/glob.rb +158 -0
  235. data/lib/swarm_sdk/tools/grep.rb +228 -0
  236. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  237. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  238. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  239. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  240. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  241. data/lib/swarm_sdk/tools/read.rb +251 -0
  242. data/lib/swarm_sdk/tools/registry.rb +93 -0
  243. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  244. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  245. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  246. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  247. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  248. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  249. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  250. data/lib/swarm_sdk/tools/think.rb +95 -0
  251. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  252. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  253. data/lib/swarm_sdk/tools/write.rb +117 -0
  254. data/lib/swarm_sdk/utils.rb +50 -0
  255. data/lib/swarm_sdk/version.rb +5 -0
  256. data/lib/swarm_sdk.rb +157 -0
  257. data/llm.v2.txt +13407 -0
  258. data/rubocop/cop/security/no_reflection_methods.rb +47 -0
  259. data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
  260. data/swarm_cli.gemspec +57 -0
  261. data/swarm_memory.gemspec +28 -0
  262. data/swarm_sdk.gemspec +41 -0
  263. data/team.yml +1 -1
  264. data/team_full.yml +1875 -0
  265. data/{team_v2.yml → team_sdk.yml} +121 -52
  266. metadata +249 -6
  267. 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