claude_swarm 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +21 -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 -3
  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 +247 -4
  267. data/EXAMPLES.md +0 -164
@@ -0,0 +1,127 @@
1
+ # LLM Call Retry Logic
2
+
3
+ ## Feature
4
+
5
+ SwarmSDK automatically retries failed LLM API calls to handle transient failures.
6
+
7
+ ## Configuration
8
+
9
+ **Defaults:**
10
+ - Max retries: 10
11
+ - Delay: 10 seconds (fixed, no exponential backoff)
12
+ - Retries ALL StandardError exceptions
13
+
14
+ ## Implementation
15
+
16
+ **Location:** `lib/swarm_sdk/agent/chat.rb:768-801`
17
+
18
+ ```ruby
19
+ def call_llm_with_retry(max_retries: 10, delay: 10, &block)
20
+ attempts = 0
21
+ loop do
22
+ attempts += 1
23
+ begin
24
+ return yield
25
+ rescue StandardError => e
26
+ raise if attempts >= max_retries
27
+
28
+ RubyLLM.logger.warn("SwarmSDK: LLM call failed (attempt #{attempts}/#{max_retries})")
29
+ sleep(delay)
30
+ end
31
+ end
32
+ end
33
+ ```
34
+
35
+ ## Error Types Handled
36
+
37
+ - `Faraday::ConnectionFailed` - Network connection issues
38
+ - `Faraday::TimeoutError` - Request timeouts
39
+ - `RubyLLM::APIError` - API errors (500s, etc.)
40
+ - `RubyLLM::RateLimitError` - Rate limit errors
41
+ - `RubyLLM::BadRequestError` - Usually not transient, but retries anyway
42
+ - Any other `StandardError` - Catches proxy issues, DNS failures, etc.
43
+
44
+ ## Usage
45
+
46
+ **Automatic - No Configuration Needed:**
47
+
48
+ ```ruby
49
+ swarm = SwarmSDK.build do
50
+ agent :my_agent do
51
+ model "gpt-4"
52
+ base_url "http://proxy.example.com/v1" # Can fail
53
+ end
54
+ end
55
+
56
+ # Automatically retries on failure
57
+ response = swarm.execute("Do something")
58
+ ```
59
+
60
+ ## Logging
61
+
62
+ **On Retry:**
63
+ ```
64
+ WARN: SwarmSDK: LLM call failed (attempt 1/10): Faraday::ConnectionFailed: Connection failed
65
+ WARN: SwarmSDK: Retrying in 10 seconds...
66
+ ```
67
+
68
+ **On Max Retries:**
69
+ ```
70
+ ERROR: SwarmSDK: LLM call failed after 10 attempts: Faraday::ConnectionFailed: Connection failed
71
+ ```
72
+
73
+ ## Testing
74
+
75
+ Retry logic has been verified through:
76
+ - ✅ All 728 SwarmSDK tests passing
77
+ - ✅ Manual testing with failing proxies
78
+ - ✅ Evaluation harnesses (assistant/retrieval modes)
79
+
80
+ **Note:** Direct unit tests would require reflection (`instance_variable_set`) which violates security policy. The retry logic is tested implicitly through integration tests and real usage.
81
+
82
+ ## Behavior
83
+
84
+ **Scenario 1: Transient failure**
85
+ ```
86
+ Attempt 1: ConnectionFailed
87
+ → Wait 10s
88
+ Attempt 2: ConnectionFailed
89
+ → Wait 10s
90
+ Attempt 3: Success
91
+ → Returns response
92
+ ```
93
+
94
+ **Scenario 2: Persistent failure**
95
+ ```
96
+ Attempt 1-10: All fail
97
+ → Raises original error after attempt 10
98
+ ```
99
+
100
+ **Scenario 3: Immediate success**
101
+ ```
102
+ Attempt 1: Success
103
+ → Returns response (no retry needed)
104
+ ```
105
+
106
+ ## Why No Exponential Backoff
107
+
108
+ **Design Decision:** Fixed 10-second delay
109
+
110
+ **Rationale:**
111
+ - Simpler implementation
112
+ - Predictable retry duration (max 100 seconds)
113
+ - Transient proxy/network issues typically resolve within seconds
114
+ - Rate limit errors are caught by provider-specific handling
115
+ - User explicitly requested fixed delays
116
+
117
+ **Total max time:** 10 retries × 10 seconds = 100 seconds maximum
118
+
119
+ ## Future Enhancements (If Needed)
120
+
121
+ - [ ] Configurable retry count per agent
122
+ - [ ] Configurable delay per agent
123
+ - [ ] Selective retry based on error type
124
+ - [ ] Exponential backoff option
125
+ - [ ] Circuit breaker pattern
126
+
127
+ **Current State:** Production-ready with sensible defaults for proxy/network resilience.
@@ -0,0 +1,461 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ # Builder provides fluent API for configuring agents
6
+ #
7
+ # This class offers a Ruby DSL for defining agents with a clean, readable syntax.
8
+ # It collects configuration and then adds the agent to the swarm.
9
+ #
10
+ # @example
11
+ # agent :backend do
12
+ # model "gpt-5"
13
+ # prompt "You build APIs"
14
+ # tools :Read, :Write, :Bash
15
+ #
16
+ # hook :pre_tool_use, matcher: "Bash" do |ctx|
17
+ # SwarmSDK::Hooks::Result.halt("Blocked!") if dangerous?(ctx)
18
+ # end
19
+ # end
20
+ class Builder
21
+ # Expose default_permissions for Swarm::Builder to set from all_agents
22
+ attr_writer :default_permissions
23
+
24
+ # Expose mcp_servers for tests
25
+ attr_reader :mcp_servers
26
+
27
+ def initialize(name)
28
+ @name = name
29
+ @description = nil
30
+ @model = "gpt-5"
31
+ @provider = nil
32
+ @base_url = nil
33
+ @api_version = nil
34
+ @context_window = nil
35
+ @system_prompt = nil
36
+ # Use Set for tools to automatically handle duplicates when tools() is called multiple times.
37
+ # This ensures that if someone does: tools :Read; tools :Write; tools :Read
38
+ # the final set contains only [:Read, :Write] without duplicates.
39
+ # We convert to Array in to_definition for compatibility with Agent::Definition.
40
+ @tools = Set.new
41
+ @delegates_to = []
42
+ @directory = "."
43
+ @parameters = {}
44
+ @headers = {}
45
+ @timeout = nil
46
+ @mcp_servers = []
47
+ @disable_default_tools = nil # nil = include all default tools
48
+ @bypass_permissions = false
49
+ @coding_agent = nil # nil = not set (will default to false in Definition)
50
+ @assume_model_exists = nil
51
+ @hooks = []
52
+ @permissions_config = {}
53
+ @default_permissions = {} # Set by SwarmBuilder from all_agents
54
+ @memory_config = nil
55
+ end
56
+
57
+ # Set/get agent model
58
+ def model(model_name = :__not_provided__)
59
+ return @model if model_name == :__not_provided__
60
+
61
+ @model = model_name
62
+ end
63
+
64
+ # Set/get provider
65
+ def provider(provider_name = :__not_provided__)
66
+ return @provider if provider_name == :__not_provided__
67
+
68
+ @provider = provider_name
69
+ end
70
+
71
+ # Set/get base URL
72
+ def base_url(url = :__not_provided__)
73
+ return @base_url if url == :__not_provided__
74
+
75
+ @base_url = url
76
+ end
77
+
78
+ # Set/get API version (OpenAI-compatible providers only)
79
+ def api_version(version = :__not_provided__)
80
+ return @api_version if version == :__not_provided__
81
+
82
+ @api_version = version
83
+ end
84
+
85
+ # Set/get explicit context window override
86
+ def context_window(tokens = :__not_provided__)
87
+ return @context_window if tokens == :__not_provided__
88
+
89
+ @context_window = tokens
90
+ end
91
+
92
+ # Set/get LLM parameters
93
+ def parameters(params = :__not_provided__)
94
+ return @parameters if params == :__not_provided__
95
+
96
+ @parameters = params
97
+ end
98
+
99
+ # Set/get custom HTTP headers
100
+ def headers(header_hash = :__not_provided__)
101
+ return @headers if header_hash == :__not_provided__
102
+
103
+ @headers = header_hash
104
+ end
105
+
106
+ # Set/get timeout
107
+ def timeout(seconds = :__not_provided__)
108
+ return @timeout if seconds == :__not_provided__
109
+
110
+ @timeout = seconds
111
+ end
112
+
113
+ # Add an MCP server configuration
114
+ #
115
+ # @example stdio transport
116
+ # mcp_server :filesystem, type: :stdio, command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem"]
117
+ #
118
+ # @example SSE transport
119
+ # mcp_server :web, type: :sse, url: "https://example.com/mcp", headers: { authorization: "Bearer token" }
120
+ #
121
+ # @example HTTP/streamable transport
122
+ # mcp_server :api, type: :http, url: "https://api.example.com/mcp", timeout: 60
123
+ def mcp_server(name, **options)
124
+ server_config = { name: name }.merge(options)
125
+ @mcp_servers << server_config
126
+ end
127
+
128
+ # Disable default tools
129
+ #
130
+ # @param value [Boolean, Array<Symbol>]
131
+ # - true: Disable ALL default tools
132
+ # - Array of symbols: Disable specific tools (e.g., [:Think, :TodoWrite])
133
+ #
134
+ # @example Disable all default tools
135
+ # disable_default_tools true
136
+ #
137
+ # @example Disable specific tools (array)
138
+ # disable_default_tools [:Think, :TodoWrite]
139
+ #
140
+ # @example Disable specific tools (separate arguments)
141
+ # disable_default_tools :Think, :TodoWrite
142
+ def disable_default_tools(*tools)
143
+ # Handle different argument forms
144
+ @disable_default_tools = case tools.size
145
+ when 0
146
+ nil
147
+ when 1
148
+ # Single argument: could be true/false/array
149
+ tools.first
150
+ else
151
+ # Multiple arguments: treat as array of tool names
152
+ tools.map(&:to_sym)
153
+ end
154
+ end
155
+
156
+ # Set bypass_permissions flag
157
+ def bypass_permissions(enabled)
158
+ @bypass_permissions = enabled
159
+ end
160
+
161
+ # Set coding_agent flag
162
+ #
163
+ # When true, includes the base system prompt for coding tasks.
164
+ # When false (default), uses only the custom system prompt.
165
+ #
166
+ # @param enabled [Boolean] Whether to include base coding prompt
167
+ # @return [void]
168
+ #
169
+ # @example
170
+ # coding_agent true # Include base prompt for coding tasks
171
+ def coding_agent(enabled)
172
+ @coding_agent = enabled
173
+ end
174
+
175
+ # Set assume_model_exists flag
176
+ def assume_model_exists(enabled)
177
+ @assume_model_exists = enabled
178
+ end
179
+
180
+ # Set system prompt (matches YAML key)
181
+ def system_prompt(text)
182
+ @system_prompt = text
183
+ end
184
+
185
+ # Set description
186
+ def description(text)
187
+ @description = text
188
+ end
189
+
190
+ # Set or add tools
191
+ #
192
+ # Uses Set internally to automatically deduplicate tool names across multiple calls.
193
+ # This allows calling tools() multiple times without worrying about duplicates.
194
+ #
195
+ # @param tool_names [Array<Symbol>] Tool names to add
196
+ # @param include_default [Boolean] Whether to include default tools (Read, Grep, etc.)
197
+ # @param replace [Boolean] If true, replaces existing tools instead of merging (default: false)
198
+ #
199
+ # @example Basic usage with defaults
200
+ # tools :Grep, :Read # include_default: true is implicit
201
+ #
202
+ # @example Explicit tools only, no defaults
203
+ # tools :Grep, :Read, include_default: false
204
+ #
205
+ # @example Multiple calls (cumulative, automatic deduplication)
206
+ # tools :Read
207
+ # tools :Write, :Edit # @tools now contains Set[:Read, :Write, :Edit]
208
+ # tools :Read # Still Set[:Read, :Write, :Edit] - no duplicate
209
+ #
210
+ # @example Replace tools (for markdown overrides)
211
+ # tools :Read, :Write, replace: true # Replaces all existing tools
212
+ def tools(*tool_names, include_default: true, replace: false)
213
+ @tools = Set.new if replace
214
+ @tools.merge(tool_names.map(&:to_sym))
215
+ # When include_default is false, disable all default tools
216
+ @disable_default_tools = true unless include_default
217
+ end
218
+
219
+ # Add tools from all_agents configuration
220
+ #
221
+ # Used by Swarm::Builder to add all_agents tools.
222
+ # Since we use Set, order doesn't matter and duplicates are handled automatically.
223
+ #
224
+ # @param tool_names [Array] Tool names to add
225
+ # @return [void]
226
+ def prepend_tools(*tool_names)
227
+ @tools.merge(tool_names.map(&:to_sym))
228
+ end
229
+
230
+ # Set directory
231
+ def directory(dir)
232
+ @directory = dir
233
+ end
234
+
235
+ # Set delegation targets
236
+ def delegates_to(*agent_names)
237
+ @delegates_to.concat(agent_names)
238
+ end
239
+
240
+ # Add a hook (Ruby block OR shell command)
241
+ #
242
+ # @example Ruby block
243
+ # hook :pre_tool_use, matcher: "Bash" do |ctx|
244
+ # HookResult.halt("Blocked") if dangerous?(ctx)
245
+ # end
246
+ #
247
+ # @example Shell command
248
+ # hook :pre_tool_use, matcher: "Bash", command: "validate.sh"
249
+ def hook(event, matcher: nil, command: nil, timeout: nil, &block)
250
+ @hooks << {
251
+ event: event,
252
+ matcher: matcher,
253
+ command: command,
254
+ timeout: timeout,
255
+ block: block,
256
+ }
257
+ end
258
+
259
+ # Configure permissions for this agent
260
+ #
261
+ # @example
262
+ # permissions do
263
+ # Write.allow_paths "backend/**/*"
264
+ # Write.deny_paths "backend/secrets/**"
265
+ # end
266
+ def permissions(&block)
267
+ @permissions_config = PermissionsBuilder.build(&block)
268
+ end
269
+
270
+ # Check if model has been explicitly set (not default)
271
+ #
272
+ # Used by Swarm::Builder to determine if all_agents model should apply.
273
+ #
274
+ # @return [Boolean] true if model was explicitly set
275
+ def model_set?
276
+ @model != "gpt-5"
277
+ end
278
+
279
+ # Check if provider has been explicitly set
280
+ #
281
+ # Used by Swarm::Builder to determine if all_agents provider should apply.
282
+ #
283
+ # @return [Boolean] true if provider was explicitly set
284
+ def provider_set?
285
+ !@provider.nil?
286
+ end
287
+
288
+ # Check if base_url has been explicitly set
289
+ #
290
+ # Used by Swarm::Builder to determine if all_agents base_url should apply.
291
+ #
292
+ # @return [Boolean] true if base_url was explicitly set
293
+ def base_url_set?
294
+ !@base_url.nil?
295
+ end
296
+
297
+ # Check if api_version has been explicitly set
298
+ #
299
+ # Used by Swarm::Builder to determine if all_agents api_version should apply.
300
+ #
301
+ # @return [Boolean] true if api_version was explicitly set
302
+ def api_version_set?
303
+ !@api_version.nil?
304
+ end
305
+
306
+ # Check if timeout has been explicitly set
307
+ #
308
+ # Used by Swarm::Builder to determine if all_agents timeout should apply.
309
+ #
310
+ # @return [Boolean] true if timeout was explicitly set
311
+ def timeout_set?
312
+ !@timeout.nil?
313
+ end
314
+
315
+ # Check if coding_agent has been explicitly set
316
+ #
317
+ # Used by Swarm::Builder to determine if all_agents coding_agent should apply.
318
+ #
319
+ # @return [Boolean] true if coding_agent was explicitly set
320
+ def coding_agent_set?
321
+ !@coding_agent.nil?
322
+ end
323
+
324
+ # Check if parameters have been set
325
+ #
326
+ # Used by Swarm::Builder for merging all_agents parameters.
327
+ #
328
+ # @return [Boolean] true if parameters were set
329
+ def parameters_set?
330
+ @parameters.any?
331
+ end
332
+
333
+ # Check if headers have been set
334
+ #
335
+ # Used by Swarm::Builder for merging all_agents headers.
336
+ #
337
+ # @return [Boolean] true if headers were set
338
+ def headers_set?
339
+ @headers.any?
340
+ end
341
+
342
+ # Build and return an Agent::Definition
343
+ #
344
+ # This method converts the builder's configuration into a validated
345
+ # Agent::Definition object. The caller is responsible for adding it to a swarm.
346
+ #
347
+ # Converts @tools Set to Array here because Agent::Definition expects an array.
348
+ # The Set was only used during building to handle duplicates efficiently.
349
+ #
350
+ # @return [Agent::Definition] Fully configured and validated agent definition
351
+ def to_definition
352
+ agent_config = {
353
+ description: @description || "Agent #{@name}",
354
+ model: @model,
355
+ system_prompt: @system_prompt,
356
+ tools: @tools.to_a, # Convert Set to Array for Agent::Definition compatibility
357
+ delegates_to: @delegates_to,
358
+ directory: @directory,
359
+ }
360
+
361
+ # Add optional fields
362
+ agent_config[:provider] = @provider if @provider
363
+ agent_config[:base_url] = @base_url if @base_url
364
+ agent_config[:api_version] = @api_version if @api_version
365
+ agent_config[:context_window] = @context_window if @context_window
366
+ agent_config[:parameters] = @parameters if @parameters.any?
367
+ agent_config[:headers] = @headers if @headers.any?
368
+ agent_config[:timeout] = @timeout if @timeout
369
+ agent_config[:mcp_servers] = @mcp_servers if @mcp_servers.any?
370
+ agent_config[:disable_default_tools] = @disable_default_tools unless @disable_default_tools.nil?
371
+ agent_config[:bypass_permissions] = @bypass_permissions
372
+ agent_config[:coding_agent] = @coding_agent
373
+ agent_config[:assume_model_exists] = @assume_model_exists unless @assume_model_exists.nil?
374
+ agent_config[:permissions] = @permissions_config if @permissions_config.any?
375
+ agent_config[:default_permissions] = @default_permissions if @default_permissions.any?
376
+ agent_config[:memory] = @memory_config if @memory_config
377
+
378
+ # Convert DSL hooks to HookDefinition format
379
+ agent_config[:hooks] = convert_hooks_to_definitions if @hooks.any?
380
+
381
+ Agent::Definition.new(@name, agent_config)
382
+ end
383
+
384
+ private
385
+
386
+ # Convert DSL hooks to HookDefinition objects for Agent::Definition
387
+ #
388
+ # This converts the builder's hook configuration (Ruby blocks and shell commands)
389
+ # into HookDefinition objects that will be applied during agent initialization.
390
+ #
391
+ # @return [Hash] Hooks grouped by event type { event: [HookDefinition, ...] }
392
+ def convert_hooks_to_definitions
393
+ result = Hash.new { |h, k| h[k] = [] }
394
+
395
+ @hooks.each do |hook_config|
396
+ event = hook_config[:event]
397
+
398
+ # Create HookDefinition with proc or command
399
+ if hook_config[:block]
400
+ # Ruby block hook
401
+ hook_def = Hooks::Definition.new(
402
+ event: event,
403
+ matcher: hook_config[:matcher],
404
+ priority: 0,
405
+ proc: hook_config[:block],
406
+ )
407
+ elsif hook_config[:command]
408
+ # Shell command hook - wrap in a block that calls ShellExecutor
409
+ hook_def = Hooks::Definition.new(
410
+ event: event,
411
+ matcher: hook_config[:matcher],
412
+ priority: 0,
413
+ proc: create_shell_hook_proc(hook_config),
414
+ )
415
+ else
416
+ raise ConfigurationError, "Hook must have either :block or :command"
417
+ end
418
+
419
+ result[event] << hook_def
420
+ end
421
+
422
+ result
423
+ end
424
+
425
+ # Create a proc that executes a shell command hook
426
+ def create_shell_hook_proc(config)
427
+ command = config[:command]
428
+ timeout = config[:timeout] || 60
429
+ agent_name = @name
430
+
431
+ proc do |context|
432
+ input_json = build_hook_input(context, config[:event])
433
+ Hooks::ShellExecutor.execute(
434
+ command: command,
435
+ input_json: input_json,
436
+ timeout: timeout,
437
+ agent_name: agent_name,
438
+ swarm_name: context.swarm&.name,
439
+ event: config[:event],
440
+ )
441
+ end
442
+ end
443
+
444
+ # Build hook input JSON for shell command hooks
445
+ def build_hook_input(context, event)
446
+ base = { event: event.to_s, agent: @name.to_s }
447
+
448
+ case event
449
+ when :pre_tool_use
450
+ base.merge(tool: context.tool_call.name, parameters: context.tool_call.parameters)
451
+ when :post_tool_use
452
+ base.merge(result: context.tool_result.content, success: context.tool_result.success?)
453
+ when :user_prompt
454
+ base.merge(prompt: context.metadata[:prompt])
455
+ else
456
+ base
457
+ end
458
+ end
459
+ end
460
+ end
461
+ end