swarm_memory 2.1.5 → 2.1.6

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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. metadata +5 -184
  4. data/lib/claude_swarm/base_executor.rb +0 -133
  5. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  6. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  7. data/lib/claude_swarm/cli.rb +0 -697
  8. data/lib/claude_swarm/commands/ps.rb +0 -215
  9. data/lib/claude_swarm/commands/show.rb +0 -139
  10. data/lib/claude_swarm/configuration.rb +0 -373
  11. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  12. data/lib/claude_swarm/json_handler.rb +0 -91
  13. data/lib/claude_swarm/mcp_generator.rb +0 -230
  14. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  15. data/lib/claude_swarm/openai/executor.rb +0 -256
  16. data/lib/claude_swarm/openai/responses.rb +0 -319
  17. data/lib/claude_swarm/orchestrator.rb +0 -878
  18. data/lib/claude_swarm/process_tracker.rb +0 -78
  19. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  20. data/lib/claude_swarm/session_path.rb +0 -42
  21. data/lib/claude_swarm/settings_generator.rb +0 -77
  22. data/lib/claude_swarm/system_utils.rb +0 -46
  23. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  24. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  25. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  27. data/lib/claude_swarm/version.rb +0 -5
  28. data/lib/claude_swarm/worktree_manager.rb +0 -475
  29. data/lib/claude_swarm/yaml_loader.rb +0 -22
  30. data/lib/claude_swarm.rb +0 -67
  31. data/lib/swarm_cli/cli.rb +0 -201
  32. data/lib/swarm_cli/command_registry.rb +0 -61
  33. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  34. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  35. data/lib/swarm_cli/commands/migrate.rb +0 -55
  36. data/lib/swarm_cli/commands/run.rb +0 -173
  37. data/lib/swarm_cli/config_loader.rb +0 -98
  38. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  39. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  40. data/lib/swarm_cli/interactive_repl.rb +0 -924
  41. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  42. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  43. data/lib/swarm_cli/migrate_options.rb +0 -54
  44. data/lib/swarm_cli/migrator.rb +0 -132
  45. data/lib/swarm_cli/options.rb +0 -151
  46. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  47. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  48. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  49. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  50. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  51. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  52. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  53. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  54. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  55. data/lib/swarm_cli/ui/icons.rb +0 -36
  56. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  57. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  58. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  59. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  60. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  61. data/lib/swarm_cli/version.rb +0 -5
  62. data/lib/swarm_cli.rb +0 -46
  63. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  64. data/lib/swarm_sdk/agent/builder.rb +0 -552
  65. data/lib/swarm_sdk/agent/chat.rb +0 -774
  66. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  67. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  68. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  69. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  70. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  71. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  72. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  73. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  75. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  76. data/lib/swarm_sdk/agent/context.rb +0 -116
  77. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  78. data/lib/swarm_sdk/agent/definition.rb +0 -477
  79. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  80. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  81. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  82. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  83. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  84. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  85. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  86. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  87. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  88. data/lib/swarm_sdk/configuration.rb +0 -135
  89. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  90. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  91. data/lib/swarm_sdk/context_compactor.rb +0 -335
  92. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  93. data/lib/swarm_sdk/context_management/context.rb +0 -328
  94. data/lib/swarm_sdk/defaults.rb +0 -196
  95. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  96. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  97. data/lib/swarm_sdk/hooks/context.rb +0 -197
  98. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  99. data/lib/swarm_sdk/hooks/error.rb +0 -29
  100. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  101. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  102. data/lib/swarm_sdk/hooks/result.rb +0 -150
  103. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  104. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  105. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  106. data/lib/swarm_sdk/log_collector.rb +0 -227
  107. data/lib/swarm_sdk/log_stream.rb +0 -127
  108. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  109. data/lib/swarm_sdk/model_aliases.json +0 -8
  110. data/lib/swarm_sdk/models.json +0 -1
  111. data/lib/swarm_sdk/models.rb +0 -120
  112. data/lib/swarm_sdk/node_context.rb +0 -245
  113. data/lib/swarm_sdk/observer/builder.rb +0 -81
  114. data/lib/swarm_sdk/observer/config.rb +0 -45
  115. data/lib/swarm_sdk/observer/manager.rb +0 -236
  116. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  117. data/lib/swarm_sdk/permissions/config.rb +0 -239
  118. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  119. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  120. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  121. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  122. data/lib/swarm_sdk/plugin.rb +0 -309
  123. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  124. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  125. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  126. data/lib/swarm_sdk/restore_result.rb +0 -65
  127. data/lib/swarm_sdk/result.rb +0 -123
  128. data/lib/swarm_sdk/snapshot.rb +0 -156
  129. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  130. data/lib/swarm_sdk/state_restorer.rb +0 -476
  131. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  132. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  133. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  134. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  135. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  136. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  141. data/lib/swarm_sdk/swarm.rb +0 -717
  142. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  143. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  144. data/lib/swarm_sdk/tools/bash.rb +0 -282
  145. data/lib/swarm_sdk/tools/clock.rb +0 -44
  146. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  147. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  148. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  149. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  150. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  151. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  152. data/lib/swarm_sdk/tools/edit.rb +0 -145
  153. data/lib/swarm_sdk/tools/glob.rb +0 -166
  154. data/lib/swarm_sdk/tools/grep.rb +0 -235
  155. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  156. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
  157. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  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 -272
  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 -98
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/utils.rb +0 -68
  174. data/lib/swarm_sdk/validation_result.rb +0 -33
  175. data/lib/swarm_sdk/version.rb +0 -5
  176. data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
  177. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  178. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  179. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  180. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  181. data/lib/swarm_sdk/workflow.rb +0 -554
  182. data/lib/swarm_sdk.rb +0 -524
@@ -1,477 +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
- :delegates_to,
28
- :system_prompt,
29
- :provider,
30
- :base_url,
31
- :api_version,
32
- :mcp_servers,
33
- :parameters,
34
- :headers,
35
- :timeout,
36
- :disable_default_tools,
37
- :coding_agent,
38
- :default_permissions,
39
- :agent_permissions,
40
- :assume_model_exists,
41
- :hooks,
42
- :plugin_configs,
43
- :shared_across_delegations
44
-
45
- attr_accessor :bypass_permissions, :max_concurrent_tools
46
-
47
- def initialize(name, config = {})
48
- @name = name.to_sym
49
-
50
- # Validate name doesn't contain '@' (reserved for delegation instances)
51
- if @name.to_s.include?("@")
52
- raise ConfigurationError,
53
- "Agent names cannot contain '@' character (reserved for delegation instance naming). " \
54
- "Agent: #{@name}"
55
- end
56
-
57
- # BREAKING CHANGE: Hard error for plural form
58
- if config[:directories]
59
- raise ConfigurationError,
60
- "The 'directories' (plural) configuration is no longer supported in SwarmSDK 1.0+.\n\n" \
61
- "Change 'directories:' to 'directory:' (singular).\n\n" \
62
- "If you need access to multiple directories, use permissions:\n\n " \
63
- "directory: 'backend/'\n " \
64
- "permissions do\n " \
65
- "tool(:Read).allow_paths('../shared/**')\n " \
66
- "end"
67
- end
68
-
69
- @description = config[:description]
70
- @model = config[:model] || Defaults::Agent::MODEL
71
- @provider = config[:provider] || Defaults::Agent::PROVIDER
72
- @base_url = config[:base_url]
73
- @api_version = config[:api_version]
74
- @context_window = config[:context_window] # Explicit context window override
75
- @parameters = config[:parameters] || {}
76
- @headers = Utils.stringify_keys(config[:headers] || {})
77
- @timeout = config[:timeout] || Defaults::Timeouts::AGENT_REQUEST_SECONDS
78
- @bypass_permissions = config[:bypass_permissions] || false
79
- @max_concurrent_tools = config[:max_concurrent_tools]
80
- # Always assume model exists - SwarmSDK validates models separately using models.json
81
- # This prevents RubyLLM from trying to validate models in its registry
82
- @assume_model_exists = true
83
-
84
- # disable_default_tools can be:
85
- # - nil/not set: include all default tools (default behavior)
86
- # - true: disable ALL default tools
87
- # - Array of symbols: disable specific tools (e.g., [:Think, :TodoWrite])
88
- @disable_default_tools = config[:disable_default_tools]
89
-
90
- # coding_agent defaults to false if not specified
91
- # When true, includes the base system prompt for coding tasks
92
- # When false, uses only the custom system prompt (no base prompt)
93
- @coding_agent = config.key?(:coding_agent) ? config[:coding_agent] : false
94
-
95
- # Parse directory first so it can be used in system prompt rendering
96
- @directory = parse_directory(config[:directory])
97
-
98
- # Extract plugin configurations (generic bucket for all plugin-specific keys)
99
- # This allows plugins to store their config without SDK knowing about them
100
- @plugin_configs = extract_plugin_configs(config)
101
-
102
- # Delegation isolation mode (default: false = isolated instances per delegation)
103
- @shared_across_delegations = config[:shared_across_delegations] || false
104
-
105
- # Build system prompt after directory and memory are set
106
- @system_prompt = build_full_system_prompt(config[:system_prompt])
107
-
108
- # Parse tools with permissions support
109
- @default_permissions = config[:default_permissions] || {}
110
- @agent_permissions = config[:permissions] || {}
111
- @tools = parse_tools_with_permissions(
112
- config[:tools],
113
- @default_permissions,
114
- @agent_permissions,
115
- )
116
-
117
- # Inject default write restrictions for security
118
- @tools = inject_default_write_permissions(@tools)
119
-
120
- @delegates_to = Array(config[:delegates_to] || []).map(&:to_sym).uniq
121
- @mcp_servers = Array(config[:mcp_servers] || [])
122
-
123
- # Parse hooks configuration
124
- # Handles both DSL (HookDefinition objects) and YAML (raw hash) formats
125
- @hooks = parse_hooks(config[:hooks])
126
-
127
- validate!
128
- end
129
-
130
- # Get plugin-specific configuration
131
- #
132
- # Plugins store their configuration in the generic plugin_configs hash.
133
- # This allows SDK to remain plugin-agnostic while plugins can store
134
- # arbitrary configuration.
135
- #
136
- # @param plugin_name [Symbol] Plugin name (e.g., :memory)
137
- # @return [Object, nil] Plugin configuration or nil if not present
138
- #
139
- # @example
140
- # agent_definition.plugin_config(:memory)
141
- # # => { directory: "tmp/memory", mode: :researcher }
142
- def plugin_config(plugin_name)
143
- @plugin_configs[plugin_name.to_sym] || @plugin_configs[plugin_name.to_s]
144
- end
145
-
146
- def to_h
147
- # Core SDK configuration (always serialized)
148
- base_config = {
149
- name: @name,
150
- description: @description,
151
- model: SwarmSDK::Models.resolve_alias(@model), # Resolve model aliases
152
- context_window: @context_window,
153
- directory: @directory,
154
- tools: @tools,
155
- delegates_to: @delegates_to,
156
- system_prompt: @system_prompt,
157
- provider: @provider,
158
- base_url: @base_url,
159
- api_version: @api_version,
160
- mcp_servers: @mcp_servers,
161
- parameters: @parameters,
162
- headers: @headers,
163
- timeout: @timeout,
164
- bypass_permissions: @bypass_permissions,
165
- disable_default_tools: @disable_default_tools,
166
- coding_agent: @coding_agent,
167
- assume_model_exists: @assume_model_exists,
168
- max_concurrent_tools: @max_concurrent_tools,
169
- hooks: @hooks,
170
- shared_across_delegations: @shared_across_delegations,
171
- # Permissions are core SDK functionality (not plugin-specific)
172
- default_permissions: @default_permissions,
173
- permissions: @agent_permissions,
174
- }.compact
175
-
176
- # Allow plugins to contribute their config for serialization
177
- # This enables plugin features (memory, skills, etc.) to be preserved
178
- # when cloning agents without SwarmSDK knowing about plugin-specific fields
179
- plugin_configs = SwarmSDK::PluginRegistry.all.map do |plugin|
180
- plugin.serialize_config(agent_definition: self)
181
- end
182
-
183
- # Merge plugin configs into base config
184
- # Later plugins override earlier ones if they have conflicting keys
185
- plugin_configs.reduce(base_config) { |acc, config| acc.merge(config) }
186
- end
187
-
188
- # Validate agent configuration and return warnings (non-fatal issues)
189
- #
190
- # Unlike validate! which raises exceptions for critical errors, this method
191
- # returns an array of warning hashes for non-fatal issues like:
192
- # - Model not found in registry (informs user, suggests alternatives)
193
- # - Context tracking unavailable (useful even with assume_model_exists)
194
- #
195
- # Note: Validation ALWAYS runs, even with assume_model_exists: true or base_url set.
196
- # The purpose is to inform the user about potential issues and suggest corrections,
197
- # not to block execution.
198
- #
199
- # @return [Array<Hash>] Array of warning hashes
200
- def validate
201
- warnings = []
202
-
203
- # Always validate model (even with assume_model_exists)
204
- # Warnings inform user about typos and context tracking limitations
205
- model_warning = validate_model
206
- warnings << model_warning if model_warning
207
-
208
- # Future: could add tool validation, delegate validation, etc.
209
-
210
- warnings
211
- end
212
-
213
- private
214
-
215
- # Validate that model exists in SwarmSDK's model registry
216
- #
217
- # Uses SwarmSDK's static models.json instead of RubyLLM's dynamic registry.
218
- # This provides stable, offline model validation without network calls.
219
- #
220
- # Process:
221
- # 1. Try to find model directly in models.json
222
- # 2. If not found, try to resolve as alias and find again
223
- # 3. If still not found, return warning with suggestions
224
- #
225
- # @return [Hash, nil] Warning hash if model not found, nil otherwise
226
- def validate_model
227
- # Try direct lookup first
228
- model_data = SwarmSDK::Models.all.find { |m| (m["id"] || m[:id]) == @model }
229
-
230
- # If not found, try alias resolution
231
- unless model_data
232
- resolved_id = SwarmSDK::Models.resolve_alias(@model)
233
- # Only search again if alias was different
234
- if resolved_id != @model
235
- model_data = SwarmSDK::Models.all.find { |m| (m["id"] || m[:id]) == resolved_id }
236
- end
237
- end
238
-
239
- if model_data
240
- nil # Model exists (either directly or via alias)
241
- else
242
- # Model not found - return warning with suggestions
243
- {
244
- type: :model_not_found,
245
- agent: @name,
246
- model: @model,
247
- error_message: "Unknown model: #{@model}",
248
- suggestions: SwarmSDK::Models.suggest_similar(@model),
249
- }
250
- end
251
- rescue StandardError => e
252
- # Return warning on error
253
- {
254
- type: :model_not_found,
255
- agent: @name,
256
- model: @model,
257
- error_message: e.message,
258
- suggestions: [],
259
- }
260
- end
261
-
262
- def build_full_system_prompt(custom_prompt)
263
- # Delegate to SystemPromptBuilder for all prompt construction logic
264
- # This keeps Definition focused on data storage while extracting complex logic
265
- SystemPromptBuilder.build(
266
- custom_prompt: custom_prompt,
267
- coding_agent: @coding_agent,
268
- disable_default_tools: @disable_default_tools,
269
- directory: @directory,
270
- definition: self,
271
- )
272
- end
273
-
274
- def parse_directory(directory_config)
275
- directory_config ||= "."
276
- File.expand_path(directory_config.to_s)
277
- end
278
-
279
- # Extract plugin-specific configuration keys from the config hash
280
- #
281
- # Standard SDK keys are filtered out, leaving only plugin-specific keys.
282
- # This allows plugins to add their own configuration without SDK modifications.
283
- #
284
- # @param config [Hash] Full agent configuration
285
- # @return [Hash] Plugin-specific configuration (keys not recognized by SDK)
286
- def extract_plugin_configs(config)
287
- standard_keys = [
288
- :name,
289
- :description,
290
- :model,
291
- :provider,
292
- :base_url,
293
- :api_version,
294
- :context_window,
295
- :parameters,
296
- :headers,
297
- :timeout,
298
- :bypass_permissions,
299
- :max_concurrent_tools,
300
- :assume_model_exists,
301
- :disable_default_tools,
302
- :coding_agent,
303
- :directory,
304
- :system_prompt,
305
- :tools,
306
- :delegates_to,
307
- :mcp_servers,
308
- :hooks,
309
- :default_permissions,
310
- :permissions,
311
- :shared_across_delegations,
312
- :directories,
313
- ]
314
-
315
- config.reject { |k, _| standard_keys.include?(k.to_sym) }
316
- end
317
-
318
- # Parse tools configuration with permissions support
319
- #
320
- # Tools can be specified as:
321
- # - Symbol: :Write (no permissions)
322
- # - Hash: { Write: { allowed_paths: [...] } } (with permissions)
323
- #
324
- # Returns array of tool configs:
325
- # [
326
- # { name: :Read, permissions: nil },
327
- # { name: :Write, permissions: { allowed_paths: [...] } }
328
- # ]
329
- def parse_tools_with_permissions(tools_config, default_permissions, agent_permissions)
330
- tools_array = Array(tools_config || [])
331
-
332
- tools_array.map do |tool_spec|
333
- case tool_spec
334
- when Symbol, String
335
- # Simple tool: :Write or "Write"
336
- tool_name = tool_spec.to_sym
337
- permissions = resolve_permissions(tool_name, default_permissions, agent_permissions)
338
-
339
- { name: tool_name, permissions: permissions }
340
- when Hash
341
- # Check if already in parsed format: { name: :Write, permissions: {...} }
342
- if tool_spec.key?(:name)
343
- # Already parsed - pass through as-is
344
- tool_spec
345
- else
346
- # Tool with inline permissions: { Write: { allowed_paths: [...] } }
347
- tool_name = tool_spec.keys.first.to_sym
348
- inline_permissions = tool_spec.values.first
349
-
350
- # Inline permissions override defaults
351
- { name: tool_name, permissions: inline_permissions }
352
- end
353
- else
354
- raise ConfigurationError, "Invalid tool specification: #{tool_spec.inspect}"
355
- end
356
- end
357
- end
358
-
359
- # Resolve permissions for a tool from defaults and agent-level overrides
360
- def resolve_permissions(tool_name, default_permissions, agent_permissions)
361
- # Agent-level permissions override defaults
362
- agent_permissions[tool_name] || default_permissions[tool_name]
363
- end
364
-
365
- # Inject default write permissions for security
366
- #
367
- # Write, Edit, and MultiEdit tools without explicit permissions are automatically
368
- # restricted to only write within the agent's directory. This prevents accidental
369
- # writes outside the agent's working scope.
370
- #
371
- # Default permission: { allowed_paths: ["**/*"] }
372
- # This is resolved relative to the agent's directory by the permissions system.
373
- #
374
- # Users can override by explicitly setting permissions for these tools.
375
- def inject_default_write_permissions(tools)
376
- write_tools = [:Write, :Edit, :MultiEdit]
377
-
378
- tools.map do |tool_config|
379
- tool_name = tool_config[:name]
380
-
381
- # If it's a write tool and has no permissions, inject default
382
- if write_tools.include?(tool_name) && tool_config[:permissions].nil?
383
- tool_config.merge(permissions: { allowed_paths: ["**/*"] })
384
- else
385
- tool_config
386
- end
387
- end
388
- end
389
-
390
- # Parse hooks configuration
391
- #
392
- # Handles two input formats:
393
- #
394
- # 1. DSL format (from Agent::Builder): Pre-parsed HookDefinition objects
395
- # { event_type: [HookDefinition, ...] }
396
- # These are applied directly in pass_4_configure_hooks
397
- #
398
- # 2. YAML format: Raw hash with shell command specifications
399
- # hooks:
400
- # pre_tool_use:
401
- # - matcher: "Write|Edit"
402
- # type: command
403
- # command: "validate.sh"
404
- # These are kept raw and processed by Hooks::Adapter in pass_5
405
- #
406
- # Returns:
407
- # - DSL: { event_type: [HookDefinition, ...] }
408
- # - YAML: Raw hash (for Hooks::Adapter)
409
- def parse_hooks(hooks_config)
410
- return {} if hooks_config.nil? || hooks_config.empty?
411
-
412
- # If already parsed from DSL (HookDefinition objects), return as-is
413
- if hooks_config.is_a?(Hash) && hooks_config.values.all? { |v| v.is_a?(Array) && v.all? { |item| item.is_a?(Hooks::Definition) } }
414
- return hooks_config
415
- end
416
-
417
- # For YAML hooks: validate structure but keep raw for Hooks::Adapter
418
- validate_yaml_hooks(hooks_config)
419
-
420
- # Return raw YAML - Hooks::Adapter will process in pass_5
421
- hooks_config
422
- end
423
-
424
- # Validate YAML hooks structure
425
- #
426
- # @param hooks_config [Hash] YAML hooks configuration
427
- # @return [void]
428
- def validate_yaml_hooks(hooks_config)
429
- hooks_config.each do |event_name, hook_specs|
430
- event_sym = event_name.to_sym
431
-
432
- # Validate event type
433
- unless Hooks::Registry::VALID_EVENTS.include?(event_sym)
434
- raise ConfigurationError,
435
- "Invalid hook event '#{event_name}' for agent '#{@name}'. " \
436
- "Valid events: #{Hooks::Registry::VALID_EVENTS.join(", ")}"
437
- end
438
-
439
- # Validate each hook spec structure
440
- Array(hook_specs).each do |spec|
441
- hook_type = spec[:type] || spec["type"]
442
- command = spec[:command] || spec["command"]
443
-
444
- raise ConfigurationError, "Hook missing 'type' field for event #{event_name}" unless hook_type
445
- raise ConfigurationError, "Hook missing 'command' field for event #{event_name}" if hook_type.to_s == "command" && !command
446
- end
447
- end
448
- end
449
-
450
- def validate!
451
- raise ConfigurationError, "Agent '#{@name}' missing required 'description' field" unless @description
452
-
453
- # Validate api_version can only be set for OpenAI-compatible providers
454
- if @api_version
455
- openai_compatible = ["openai", "deepseek", "perplexity", "mistral", "openrouter"]
456
- unless openai_compatible.include?(@provider.to_s)
457
- raise ConfigurationError,
458
- "Agent '#{@name}' has api_version set, but provider is '#{@provider}'. " \
459
- "api_version can only be used with OpenAI-compatible providers: #{openai_compatible.join(", ")}"
460
- end
461
-
462
- # Validate api_version value
463
- valid_versions = ["v1/chat/completions", "v1/responses"]
464
- unless valid_versions.include?(@api_version)
465
- raise ConfigurationError,
466
- "Agent '#{@name}' has invalid api_version '#{@api_version}'. " \
467
- "Valid values: #{valid_versions.join(", ")}"
468
- end
469
- end
470
-
471
- unless File.directory?(@directory)
472
- raise ConfigurationError, "Directory '#{@directory}' for agent '#{@name}' does not exist"
473
- end
474
- end
475
- end
476
- end
477
- end
@@ -1,182 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Agent
5
- # Faraday middleware for capturing LLM API requests and responses
6
- #
7
- # This middleware intercepts HTTP calls to LLM providers and emits
8
- # structured events via LogStream for logging and monitoring.
9
- #
10
- # Events emitted:
11
- # - llm_api_request: Before sending request to LLM API
12
- # - llm_api_response: After receiving response from LLM API
13
- #
14
- # The middleware is injected at runtime into the provider's Faraday
15
- # connection stack (see Agent::Chat#inject_llm_instrumentation).
16
- class LLMInstrumentationMiddleware < Faraday::Middleware
17
- # Initialize middleware
18
- #
19
- # @param app [Faraday::Connection] Faraday app
20
- # @param on_request [Proc] Callback for request events
21
- # @param on_response [Proc] Callback for response events
22
- # @param provider_name [String] Provider name for logging
23
- def initialize(app, on_request:, on_response:, provider_name:)
24
- super(app)
25
- @on_request = on_request
26
- @on_response = on_response
27
- @provider_name = provider_name
28
- end
29
-
30
- # Intercept HTTP call
31
- #
32
- # @param env [Faraday::Env] Request environment
33
- # @return [Faraday::Response] HTTP response
34
- def call(env)
35
- start_time = Time.now
36
-
37
- # Emit request event
38
- emit_request_event(env, start_time)
39
-
40
- # Execute request
41
- @app.call(env).on_complete do |response_env|
42
- end_time = Time.now
43
- duration = end_time - start_time
44
-
45
- # Emit response event
46
- emit_response_event(response_env, start_time, end_time, duration)
47
- end
48
- end
49
-
50
- private
51
-
52
- # Emit request event
53
- #
54
- # @param env [Faraday::Env] Request environment
55
- # @param timestamp [Time] Request timestamp
56
- # @return [void]
57
- def emit_request_event(env, timestamp)
58
- request_data = {
59
- provider: @provider_name,
60
- body: parse_body(env.body),
61
- timestamp: timestamp.utc.iso8601,
62
- }
63
-
64
- @on_request.call(request_data)
65
- rescue StandardError => e
66
- # Don't let logging errors break the request
67
- LogStream.emit_error(e, source: "llm_instrumentation_middleware", context: "emit_request_event", provider: @provider_name)
68
- RubyLLM.logger.debug("LLM instrumentation request error: #{e.message}")
69
- end
70
-
71
- # Emit response event
72
- #
73
- # @param env [Faraday::Env] Response environment
74
- # @param start_time [Time] Request start time
75
- # @param end_time [Time] Request end time
76
- # @param duration [Float] Request duration in seconds
77
- # @return [void]
78
- def emit_response_event(env, start_time, end_time, duration)
79
- response_data = {
80
- provider: @provider_name,
81
- body: parse_body(env.body),
82
- duration_seconds: duration.round(3),
83
- timestamp: end_time.utc.iso8601,
84
- }
85
-
86
- # Extract usage information from response body if available
87
- if env.body.is_a?(String) && !env.body.empty?
88
- begin
89
- parsed = JSON.parse(env.body)
90
- response_data[:usage] = extract_usage(parsed) if parsed.is_a?(Hash)
91
- response_data[:model] = parsed["model"] if parsed.is_a?(Hash)
92
- response_data[:finish_reason] = extract_finish_reason(parsed) if parsed.is_a?(Hash)
93
- rescue JSON::ParserError
94
- # Not JSON, skip usage extraction
95
- end
96
- end
97
-
98
- @on_response.call(response_data)
99
- rescue StandardError => e
100
- # Don't let logging errors break the response
101
- LogStream.emit_error(e, source: "llm_instrumentation_middleware", context: "emit_response_event", provider: @provider_name)
102
- RubyLLM.logger.debug("LLM instrumentation response error: #{e.message}")
103
- end
104
-
105
- # Sanitize headers by removing sensitive data
106
- #
107
- # @param headers [Hash] HTTP headers
108
- # @return [Hash] Sanitized headers
109
- def sanitize_headers(headers)
110
- return {} unless headers
111
-
112
- headers.transform_keys(&:to_s).transform_values do |value|
113
- # Redact authorization headers
114
- if value.to_s.match?(/bearer|token|key/i)
115
- "[REDACTED]"
116
- else
117
- value.to_s
118
- end
119
- end
120
- rescue StandardError
121
- {}
122
- end
123
-
124
- # Parse request/response body
125
- #
126
- # @param body [String, Hash, nil] HTTP body
127
- # @return [Hash, String, nil] Parsed body
128
- def parse_body(body)
129
- return if body.nil? || body == ""
130
-
131
- # Already parsed
132
- return body if body.is_a?(Hash)
133
-
134
- # Try to parse JSON
135
- JSON.parse(body)
136
- rescue JSON::ParserError
137
- # Return truncated string if not JSON
138
- body.to_s[0..1000]
139
- rescue StandardError
140
- nil
141
- end
142
-
143
- # Extract usage statistics from response
144
- #
145
- # Handles different provider formats (OpenAI, Anthropic, etc.)
146
- #
147
- # @param parsed [Hash] Parsed response body
148
- # @return [Hash, nil] Usage statistics
149
- def extract_usage(parsed)
150
- usage = parsed["usage"] || parsed.dig("usage")
151
- return unless usage
152
-
153
- {
154
- input_tokens: usage["input_tokens"] || usage["prompt_tokens"],
155
- output_tokens: usage["output_tokens"] || usage["completion_tokens"],
156
- total_tokens: usage["total_tokens"],
157
- }.compact
158
- rescue StandardError
159
- nil
160
- end
161
-
162
- # Extract finish reason from response
163
- #
164
- # Handles different provider formats
165
- #
166
- # @param parsed [Hash] Parsed response body
167
- # @return [String, nil] Finish reason
168
- def extract_finish_reason(parsed)
169
- # Anthropic format
170
- return parsed["stop_reason"] if parsed["stop_reason"]
171
-
172
- # OpenAI format
173
- choices = parsed["choices"]
174
- return unless choices&.is_a?(Array) && !choices.empty?
175
-
176
- choices.first["finish_reason"]
177
- rescue StandardError
178
- nil
179
- end
180
- end
181
- end
182
- end