swarm_sdk 2.7.13 → 3.0.0.alpha1

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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +43 -22
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +6 -0
  4. data/lib/swarm_sdk/ruby_llm_patches/mcp_ssl_patch.rb +144 -0
  5. data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +3 -4
  6. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  7. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  8. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  9. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  10. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  11. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  12. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  13. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  14. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  15. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  16. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  17. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  18. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  19. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  20. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  24. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  25. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  26. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  27. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  28. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  29. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  30. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  31. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  32. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  33. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  34. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  35. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  36. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  37. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  38. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  39. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  40. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  41. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  42. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  43. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  45. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  46. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  47. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  48. data/lib/swarm_sdk/v3/tools/read.rb +181 -0
  49. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  50. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  51. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  52. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  53. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  54. data/lib/swarm_sdk/v3.rb +145 -0
  55. metadata +84 -148
  56. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  57. data/lib/swarm_sdk/agent/builder.rb +0 -680
  58. data/lib/swarm_sdk/agent/chat.rb +0 -1432
  59. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  60. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  61. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  62. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  63. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  64. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  65. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  66. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  67. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  68. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  69. data/lib/swarm_sdk/agent/context.rb +0 -115
  70. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  71. data/lib/swarm_sdk/agent/definition.rb +0 -581
  72. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  73. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  74. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  75. data/lib/swarm_sdk/agent_registry.rb +0 -146
  76. data/lib/swarm_sdk/builders/base_builder.rb +0 -553
  77. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  78. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  79. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  80. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  81. data/lib/swarm_sdk/config.rb +0 -367
  82. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  83. data/lib/swarm_sdk/configuration/translator.rb +0 -283
  84. data/lib/swarm_sdk/configuration.rb +0 -165
  85. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  86. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  87. data/lib/swarm_sdk/context_compactor.rb +0 -335
  88. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  89. data/lib/swarm_sdk/context_management/context.rb +0 -328
  90. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  91. data/lib/swarm_sdk/defaults.rb +0 -251
  92. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  93. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  94. data/lib/swarm_sdk/hooks/context.rb +0 -197
  95. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  96. data/lib/swarm_sdk/hooks/error.rb +0 -29
  97. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  98. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  99. data/lib/swarm_sdk/hooks/result.rb +0 -150
  100. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  101. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  102. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  103. data/lib/swarm_sdk/log_collector.rb +0 -227
  104. data/lib/swarm_sdk/log_stream.rb +0 -127
  105. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  106. data/lib/swarm_sdk/model_aliases.json +0 -8
  107. data/lib/swarm_sdk/models.json +0 -44002
  108. data/lib/swarm_sdk/models.rb +0 -161
  109. data/lib/swarm_sdk/node_context.rb +0 -245
  110. data/lib/swarm_sdk/observer/builder.rb +0 -81
  111. data/lib/swarm_sdk/observer/config.rb +0 -45
  112. data/lib/swarm_sdk/observer/manager.rb +0 -236
  113. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  114. data/lib/swarm_sdk/permissions/config.rb +0 -239
  115. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  116. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  117. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  118. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  119. data/lib/swarm_sdk/plugin.rb +0 -309
  120. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  121. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  122. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  123. data/lib/swarm_sdk/restore_result.rb +0 -65
  124. data/lib/swarm_sdk/result.rb +0 -212
  125. data/lib/swarm_sdk/snapshot.rb +0 -156
  126. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  127. data/lib/swarm_sdk/state_restorer.rb +0 -476
  128. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  129. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  130. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -195
  131. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  132. data/lib/swarm_sdk/swarm/executor.rb +0 -290
  133. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -151
  134. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  135. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -360
  136. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -270
  137. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  138. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  139. data/lib/swarm_sdk/swarm.rb +0 -843
  140. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  141. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  142. data/lib/swarm_sdk/tools/base.rb +0 -63
  143. data/lib/swarm_sdk/tools/bash.rb +0 -280
  144. data/lib/swarm_sdk/tools/clock.rb +0 -46
  145. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  146. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  147. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  148. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  149. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  150. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  151. data/lib/swarm_sdk/tools/edit.rb +0 -145
  152. data/lib/swarm_sdk/tools/glob.rb +0 -166
  153. data/lib/swarm_sdk/tools/grep.rb +0 -235
  154. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  155. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  156. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  157. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  158. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  159. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  160. data/lib/swarm_sdk/tools/read.rb +0 -261
  161. data/lib/swarm_sdk/tools/registry.rb +0 -205
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  165. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  166. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  167. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  168. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  169. data/lib/swarm_sdk/tools/think.rb +0 -100
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  174. data/lib/swarm_sdk/utils.rb +0 -68
  175. data/lib/swarm_sdk/validation_result.rb +0 -33
  176. data/lib/swarm_sdk/version.rb +0 -5
  177. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  182. data/lib/swarm_sdk/workflow.rb +0 -589
  183. data/lib/swarm_sdk.rb +0 -718
@@ -1,581 +0,0 @@
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
- attr_reader :name,
22
- :description,
23
- :model,
24
- :context_window,
25
- :directory,
26
- :tools,
27
- :delegation_configs, # Full delegation config with tool names
28
- :system_prompt,
29
- :provider,
30
- :base_url,
31
- :api_version,
32
- :mcp_servers,
33
- :parameters,
34
- :headers,
35
- :request_timeout,
36
- :turn_timeout,
37
- :disable_default_tools,
38
- :coding_agent,
39
- :default_permissions,
40
- :agent_permissions,
41
- :assume_model_exists,
42
- :hooks,
43
- :plugin_configs,
44
- :shared_across_delegations,
45
- :streaming,
46
- :thinking
47
-
48
- attr_accessor :bypass_permissions, :max_concurrent_tools
49
-
50
- def initialize(name, config = {})
51
- @name = name.to_sym
52
-
53
- # Validate name doesn't contain '@' (reserved for delegation instances)
54
- if @name.to_s.include?("@")
55
- raise ConfigurationError,
56
- "Agent names cannot contain '@' character (reserved for delegation instance naming). " \
57
- "Agent: #{@name}"
58
- end
59
-
60
- # BREAKING CHANGE: Hard error for plural form
61
- if config[:directories]
62
- raise ConfigurationError,
63
- "The 'directories' (plural) configuration is no longer supported in SwarmSDK 1.0+.\n\n" \
64
- "Change 'directories:' to 'directory:' (singular).\n\n" \
65
- "If you need access to multiple directories, use permissions:\n\n " \
66
- "directory: 'backend/'\n " \
67
- "permissions do\n " \
68
- "tool(:Read).allow_paths('../shared/**')\n " \
69
- "end"
70
- end
71
-
72
- @description = config[:description]
73
- @model = config[:model] || SwarmSDK.config.default_model
74
- @provider = config[:provider] || SwarmSDK.config.default_provider
75
- @base_url = config[:base_url]
76
- @api_version = config[:api_version]
77
- @context_window = coerce_to_integer(config[:context_window]) # Explicit context window override
78
- @parameters = config[:parameters] || {}
79
- @headers = Utils.stringify_keys(config[:headers] || {})
80
- @request_timeout = config[:request_timeout] || SwarmSDK.config.agent_request_timeout
81
- @bypass_permissions = config[:bypass_permissions] || false
82
- @max_concurrent_tools = config[:max_concurrent_tools]
83
-
84
- # Use default from config unless explicitly set (including nil to disable)
85
- @turn_timeout = if config.key?(:turn_timeout)
86
- config[:turn_timeout] # Could be a number OR nil (to disable)
87
- else
88
- SwarmSDK.config.default_turn_timeout
89
- end
90
- # Always assume model exists - SwarmSDK validates models separately using models.json
91
- # This prevents RubyLLM from trying to validate models in its registry
92
- @assume_model_exists = true
93
-
94
- # disable_default_tools can be:
95
- # - nil/not set: include all default tools (default behavior)
96
- # - true: disable ALL default tools
97
- # - Array of symbols: disable specific tools (e.g., [:Think, :TodoWrite])
98
- @disable_default_tools = config[:disable_default_tools]
99
-
100
- # coding_agent defaults to false if not specified
101
- # When true, includes the base system prompt for coding tasks
102
- # When false, uses only the custom system prompt (no base prompt)
103
- @coding_agent = config.key?(:coding_agent) ? config[:coding_agent] : false
104
-
105
- # Parse directory first so it can be used in system prompt rendering
106
- @directory = parse_directory(config[:directory])
107
-
108
- # Extract plugin configurations (generic bucket for all plugin-specific keys)
109
- # This allows plugins to store their config without SDK knowing about them
110
- @plugin_configs = extract_plugin_configs(config)
111
-
112
- # Delegation isolation mode (default: false = isolated instances per delegation)
113
- @shared_across_delegations = config[:shared_across_delegations] || false
114
-
115
- # Streaming configuration (default: true from global config)
116
- @streaming = config.fetch(:streaming, SwarmSDK.config.streaming)
117
-
118
- # Extended thinking configuration (nil = disabled)
119
- @thinking = config[:thinking]
120
-
121
- # Build system prompt after directory and memory are set
122
- @system_prompt = build_full_system_prompt(config[:system_prompt])
123
-
124
- # Parse tools with permissions support
125
- @default_permissions = config[:default_permissions] || {}
126
- @agent_permissions = config[:permissions] || {}
127
- @tools = parse_tools_with_permissions(
128
- config[:tools],
129
- @default_permissions,
130
- @agent_permissions,
131
- )
132
-
133
- # Inject default write restrictions for security
134
- @tools = inject_default_write_permissions(@tools)
135
-
136
- # Parse delegation configuration (supports both simple arrays and custom tool names)
137
- @delegation_configs = parse_delegation_config(config[:delegates_to])
138
- @mcp_servers = Array(config[:mcp_servers] || [])
139
-
140
- # Parse hooks configuration
141
- # Handles both DSL (HookDefinition objects) and YAML (raw hash) formats
142
- @hooks = parse_hooks(config[:hooks])
143
-
144
- validate!
145
- end
146
-
147
- # Get agent names that this agent delegates to (backwards compatible)
148
- #
149
- # Returns an array of agent name symbols. This maintains backwards compatibility
150
- # with existing code that expects delegates_to to be a simple array.
151
- #
152
- # @return [Array<Symbol>] Delegate agent names
153
- #
154
- # @example
155
- # agent_definition.delegates_to
156
- # # => [:frontend, :backend, :qa]
157
- def delegates_to
158
- @delegation_configs.map { |config| config[:agent] }
159
- end
160
-
161
- # Get plugin-specific configuration
162
- #
163
- # Plugins store their configuration in the generic plugin_configs hash.
164
- # This allows SDK to remain plugin-agnostic while plugins can store
165
- # arbitrary configuration.
166
- #
167
- # @param plugin_name [Symbol] Plugin name (e.g., :memory)
168
- # @return [Object, nil] Plugin configuration or nil if not present
169
- #
170
- # @example
171
- # agent_definition.plugin_config(:memory)
172
- # # => { directory: "tmp/memory", mode: :full_access }
173
- def plugin_config(plugin_name)
174
- @plugin_configs[plugin_name.to_sym] || @plugin_configs[plugin_name.to_s]
175
- end
176
-
177
- def to_h
178
- # Core SDK configuration (always serialized)
179
- base_config = {
180
- name: @name,
181
- description: @description,
182
- model: SwarmSDK::Models.resolve_alias(@model), # Resolve model aliases
183
- context_window: @context_window,
184
- directory: @directory,
185
- tools: @tools,
186
- delegates_to: @delegation_configs, # Serialize full config
187
- system_prompt: @system_prompt,
188
- provider: @provider,
189
- base_url: @base_url,
190
- api_version: @api_version,
191
- mcp_servers: @mcp_servers,
192
- parameters: @parameters,
193
- headers: @headers,
194
- request_timeout: @request_timeout,
195
- turn_timeout: @turn_timeout,
196
- bypass_permissions: @bypass_permissions,
197
- disable_default_tools: @disable_default_tools,
198
- coding_agent: @coding_agent,
199
- assume_model_exists: @assume_model_exists,
200
- max_concurrent_tools: @max_concurrent_tools,
201
- hooks: @hooks,
202
- shared_across_delegations: @shared_across_delegations,
203
- streaming: @streaming,
204
- # Permissions are core SDK functionality (not plugin-specific)
205
- default_permissions: @default_permissions,
206
- permissions: @agent_permissions,
207
- }.compact
208
-
209
- # Allow plugins to contribute their config for serialization
210
- # This enables plugin features (memory, skills, etc.) to be preserved
211
- # when cloning agents without SwarmSDK knowing about plugin-specific fields
212
- plugin_configs = SwarmSDK::PluginRegistry.all.map do |plugin|
213
- plugin.serialize_config(agent_definition: self)
214
- end
215
-
216
- # Merge plugin configs into base config
217
- # Later plugins override earlier ones if they have conflicting keys
218
- plugin_configs.reduce(base_config) { |acc, config| acc.merge(config) }
219
- end
220
-
221
- # Validate agent configuration and return warnings (non-fatal issues)
222
- #
223
- # Unlike validate! which raises exceptions for critical errors, this method
224
- # returns an array of warning hashes for non-fatal issues like:
225
- # - Model not found in registry (informs user, suggests alternatives)
226
- # - Context tracking unavailable (useful even with assume_model_exists)
227
- #
228
- # Note: Validation ALWAYS runs, even with assume_model_exists: true or base_url set.
229
- # The purpose is to inform the user about potential issues and suggest corrections,
230
- # not to block execution.
231
- #
232
- # @return [Array<Hash>] Array of warning hashes
233
- def validate
234
- warnings = []
235
-
236
- # Always validate model (even with assume_model_exists)
237
- # Warnings inform user about typos and context tracking limitations
238
- model_warning = validate_model
239
- warnings << model_warning if model_warning
240
-
241
- # Future: could add tool validation, delegate validation, etc.
242
-
243
- warnings
244
- end
245
-
246
- private
247
-
248
- # Validate that model exists in SwarmSDK's model registry
249
- #
250
- # Uses SwarmSDK's static models.json instead of RubyLLM's dynamic registry.
251
- # This provides stable, offline model validation without network calls.
252
- #
253
- # Process:
254
- # 1. Try to find model directly in models.json
255
- # 2. If not found, try to resolve as alias and find again
256
- # 3. If still not found, return warning with suggestions
257
- #
258
- # @return [Hash, nil] Warning hash if model not found, nil otherwise
259
- def validate_model
260
- # Try direct lookup first
261
- model_data = SwarmSDK::Models.all.find { |m| (m["id"] || m[:id]) == @model }
262
-
263
- # If not found, try alias resolution
264
- unless model_data
265
- resolved_id = SwarmSDK::Models.resolve_alias(@model)
266
- # Only search again if alias was different
267
- if resolved_id != @model
268
- model_data = SwarmSDK::Models.all.find { |m| (m["id"] || m[:id]) == resolved_id }
269
- end
270
- end
271
-
272
- if model_data
273
- nil # Model exists (either directly or via alias)
274
- else
275
- # Model not found - return warning with suggestions
276
- {
277
- type: :model_not_found,
278
- agent: @name,
279
- model: @model,
280
- error_message: "Unknown model: #{@model}",
281
- suggestions: SwarmSDK::Models.suggest_similar(@model),
282
- }
283
- end
284
- rescue StandardError => e
285
- # Return warning on error
286
- {
287
- type: :model_not_found,
288
- agent: @name,
289
- model: @model,
290
- error_message: e.message,
291
- suggestions: [],
292
- }
293
- end
294
-
295
- def build_full_system_prompt(custom_prompt)
296
- # Delegate to SystemPromptBuilder for all prompt construction logic
297
- # This keeps Definition focused on data storage while extracting complex logic
298
- SystemPromptBuilder.build(
299
- custom_prompt: custom_prompt,
300
- coding_agent: @coding_agent,
301
- disable_default_tools: @disable_default_tools,
302
- directory: @directory,
303
- definition: self,
304
- )
305
- end
306
-
307
- def parse_directory(directory_config)
308
- directory_config ||= "."
309
- File.expand_path(directory_config.to_s)
310
- end
311
-
312
- # Parse delegation configuration
313
- #
314
- # Supports multiple formats for backwards compatibility and new features:
315
- # 1. Simple array (backwards compatible): [:frontend, :backend]
316
- # 2. Hash with custom tool names: { frontend: "AskFrontend", backend: nil }
317
- # 3. Array of hashes: [{ agent: :frontend, tool_name: "AskFrontend" }]
318
- #
319
- # Returns normalized format: [{agent: :name, tool_name: "Custom" or nil}]
320
- #
321
- # @param delegation_config [nil, Array, Hash] Delegation configuration
322
- # @return [Array<Hash>] Normalized delegation config
323
- def parse_delegation_config(delegation_config)
324
- return [] if delegation_config.nil?
325
- return [] if delegation_config.respond_to?(:empty?) && delegation_config.empty?
326
-
327
- # Handle array format (could be symbols or hashes)
328
- if delegation_config.is_a?(Array)
329
- delegation_config.flat_map do |item|
330
- case item
331
- when Symbol, String
332
- # Simple format: :frontend → {agent: :frontend, tool_name: nil, preserve_context: true}
333
- [{ agent: item.to_sym, tool_name: nil, preserve_context: true }]
334
- when Hash
335
- # Could be already normalized or hash format
336
- if item.key?(:agent)
337
- # Already normalized: {agent: :frontend, tool_name: "Custom", preserve_context: false}
338
- [{
339
- agent: item[:agent].to_sym,
340
- tool_name: item[:tool_name],
341
- preserve_context: item.fetch(:preserve_context, true),
342
- }]
343
- else
344
- # Hash format in array: {frontend: "AskFrontend"}
345
- item.map { |agent, tool_name| { agent: agent.to_sym, tool_name: tool_name, preserve_context: true } }
346
- end
347
- else
348
- raise ConfigurationError, "Invalid delegation config format: #{item.inspect}"
349
- end
350
- end.uniq { |config| config[:agent] } # Remove duplicates by agent name
351
- elsif delegation_config.is_a?(Hash)
352
- # Hash format: {frontend: "AskFrontend", backend: nil}
353
- delegation_config.map do |agent, tool_name|
354
- { agent: agent.to_sym, tool_name: tool_name, preserve_context: true }
355
- end
356
- else
357
- raise ConfigurationError, "delegates_to must be an Array or Hash, got #{delegation_config.class}"
358
- end
359
- end
360
-
361
- # Extract plugin-specific configuration keys from the config hash
362
- #
363
- # Standard SDK keys are filtered out, leaving only plugin-specific keys.
364
- # This allows plugins to add their own configuration without SDK modifications.
365
- #
366
- # @param config [Hash] Full agent configuration
367
- # @return [Hash] Plugin-specific configuration (keys not recognized by SDK)
368
- def extract_plugin_configs(config)
369
- standard_keys = [
370
- :name,
371
- :description,
372
- :model,
373
- :provider,
374
- :base_url,
375
- :api_version,
376
- :context_window,
377
- :parameters,
378
- :headers,
379
- :request_timeout,
380
- :turn_timeout,
381
- :bypass_permissions,
382
- :max_concurrent_tools,
383
- :assume_model_exists,
384
- :disable_default_tools,
385
- :coding_agent,
386
- :directory,
387
- :system_prompt,
388
- :tools,
389
- :delegates_to,
390
- :mcp_servers,
391
- :hooks,
392
- :default_permissions,
393
- :permissions,
394
- :shared_across_delegations,
395
- :streaming,
396
- :directories,
397
- ]
398
-
399
- config.reject { |k, _| standard_keys.include?(k.to_sym) }
400
- end
401
-
402
- # Parse tools configuration with permissions support
403
- #
404
- # Tools can be specified as:
405
- # - Symbol: :Write (no permissions)
406
- # - Hash: { Write: { allowed_paths: [...] } } (with permissions)
407
- #
408
- # Returns array of tool configs:
409
- # [
410
- # { name: :Read, permissions: nil },
411
- # { name: :Write, permissions: { allowed_paths: [...] } }
412
- # ]
413
- def parse_tools_with_permissions(tools_config, default_permissions, agent_permissions)
414
- tools_array = Array(tools_config || [])
415
-
416
- tools_array.map do |tool_spec|
417
- case tool_spec
418
- when Symbol, String
419
- # Simple tool: :Write or "Write"
420
- tool_name = tool_spec.to_sym
421
- permissions = resolve_permissions(tool_name, default_permissions, agent_permissions)
422
-
423
- { name: tool_name, permissions: permissions }
424
- when Hash
425
- # Check if already in parsed format: { name: :Write, permissions: {...} }
426
- if tool_spec.key?(:name)
427
- # Already parsed - pass through as-is
428
- tool_spec
429
- else
430
- # Tool with inline permissions: { Write: { allowed_paths: [...] } }
431
- tool_name = tool_spec.keys.first.to_sym
432
- inline_permissions = tool_spec.values.first
433
-
434
- # Inline permissions override defaults
435
- { name: tool_name, permissions: inline_permissions }
436
- end
437
- else
438
- raise ConfigurationError, "Invalid tool specification: #{tool_spec.inspect}"
439
- end
440
- end
441
- end
442
-
443
- # Resolve permissions for a tool from defaults and agent-level overrides
444
- def resolve_permissions(tool_name, default_permissions, agent_permissions)
445
- # Agent-level permissions override defaults
446
- agent_permissions[tool_name] || default_permissions[tool_name]
447
- end
448
-
449
- # Inject default write permissions for security
450
- #
451
- # Write, Edit, and MultiEdit tools without explicit permissions are automatically
452
- # restricted to only write within the agent's directory. This prevents accidental
453
- # writes outside the agent's working scope.
454
- #
455
- # Default permission: { allowed_paths: ["**/*"] }
456
- # This is resolved relative to the agent's directory by the permissions system.
457
- #
458
- # Users can override by explicitly setting permissions for these tools.
459
- def inject_default_write_permissions(tools)
460
- write_tools = [:Write, :Edit, :MultiEdit]
461
-
462
- tools.map do |tool_config|
463
- tool_name = tool_config[:name]
464
-
465
- # If it's a write tool and has no permissions, inject default
466
- if write_tools.include?(tool_name) && tool_config[:permissions].nil?
467
- tool_config.merge(permissions: { allowed_paths: ["**/*"] })
468
- else
469
- tool_config
470
- end
471
- end
472
- end
473
-
474
- # Parse hooks configuration
475
- #
476
- # Handles two input formats:
477
- #
478
- # 1. DSL format (from Agent::Builder): Pre-parsed HookDefinition objects
479
- # { event_type: [HookDefinition, ...] }
480
- # These are applied directly in pass_4_configure_hooks
481
- #
482
- # 2. YAML format: Raw hash with shell command specifications
483
- # hooks:
484
- # pre_tool_use:
485
- # - matcher: "Write|Edit"
486
- # type: command
487
- # command: "validate.sh"
488
- # These are kept raw and processed by Hooks::Adapter in pass_5
489
- #
490
- # Returns:
491
- # - DSL: { event_type: [HookDefinition, ...] }
492
- # - YAML: Raw hash (for Hooks::Adapter)
493
- def parse_hooks(hooks_config)
494
- return {} if hooks_config.nil? || hooks_config.empty?
495
-
496
- # If already parsed from DSL (HookDefinition objects), return as-is
497
- if hooks_config.is_a?(Hash) && hooks_config.values.all? { |v| v.is_a?(Array) && v.all? { |item| item.is_a?(Hooks::Definition) } }
498
- return hooks_config
499
- end
500
-
501
- # For YAML hooks: validate structure but keep raw for Hooks::Adapter
502
- validate_yaml_hooks(hooks_config)
503
-
504
- # Return raw YAML - Hooks::Adapter will process in pass_5
505
- hooks_config
506
- end
507
-
508
- # Validate YAML hooks structure
509
- #
510
- # @param hooks_config [Hash] YAML hooks configuration
511
- # @return [void]
512
- def validate_yaml_hooks(hooks_config)
513
- hooks_config.each do |event_name, hook_specs|
514
- event_sym = event_name.to_sym
515
-
516
- # Validate event type
517
- unless Hooks::Registry::VALID_EVENTS.include?(event_sym)
518
- raise ConfigurationError,
519
- "Invalid hook event '#{event_name}' for agent '#{@name}'. " \
520
- "Valid events: #{Hooks::Registry::VALID_EVENTS.join(", ")}"
521
- end
522
-
523
- # Validate each hook spec structure
524
- Array(hook_specs).each do |spec|
525
- hook_type = spec[:type] || spec["type"]
526
- command = spec[:command] || spec["command"]
527
-
528
- raise ConfigurationError, "Hook missing 'type' field for event #{event_name}" unless hook_type
529
- raise ConfigurationError, "Hook missing 'command' field for event #{event_name}" if hook_type.to_s == "command" && !command
530
- end
531
- end
532
- end
533
-
534
- # Coerce value to integer if it's a numeric string
535
- #
536
- # YAML sometimes parses numbers as strings (especially when quoted).
537
- # This ensures numeric values are properly converted.
538
- #
539
- # @param value [String, Integer, nil] Value to coerce
540
- # @return [Integer, nil] Coerced integer or nil
541
- def coerce_to_integer(value)
542
- return if value.nil?
543
- return value if value.is_a?(Integer)
544
- return value.to_i if value.is_a?(String) && value.match?(/\A\d+\z/)
545
-
546
- value
547
- end
548
-
549
- def validate!
550
- raise ConfigurationError, "Agent '#{@name}' missing required 'description' field" unless @description
551
-
552
- # Validate turn_timeout is positive if set
553
- if @turn_timeout && @turn_timeout <= 0
554
- raise ConfigurationError, "Agent '#{@name}' turn_timeout must be positive (got #{@turn_timeout})"
555
- end
556
-
557
- # Validate api_version can only be set for OpenAI-compatible providers
558
- if @api_version
559
- openai_compatible = ["openai", "deepseek", "perplexity", "mistral", "openrouter"]
560
- unless openai_compatible.include?(@provider.to_s)
561
- raise ConfigurationError,
562
- "Agent '#{@name}' has api_version set, but provider is '#{@provider}'. " \
563
- "api_version can only be used with OpenAI-compatible providers: #{openai_compatible.join(", ")}"
564
- end
565
-
566
- # Validate api_version value
567
- valid_versions = ["v1/chat/completions", "v1/responses"]
568
- unless valid_versions.include?(@api_version)
569
- raise ConfigurationError,
570
- "Agent '#{@name}' has invalid api_version '#{@api_version}'. " \
571
- "Valid values: #{valid_versions.join(", ")}"
572
- end
573
- end
574
-
575
- unless File.directory?(@directory)
576
- raise ConfigurationError, "Directory '#{@directory}' for agent '#{@name}' does not exist"
577
- end
578
- end
579
- end
580
- end
581
- end