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,236 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Observer
5
- # Manages observer agent executions
6
- #
7
- # Handles:
8
- # - Event subscription via LogCollector
9
- # - Spawning async tasks for observer agents
10
- # - Self-consumption protection (observers don't trigger themselves)
11
- # - Task lifecycle and cleanup
12
- #
13
- # @example
14
- # manager = Observer::Manager.new(swarm)
15
- # manager.add_config(profiler_config)
16
- # manager.setup
17
- # # ... main execution happens ...
18
- # manager.wait_for_completion
19
- # manager.cleanup
20
- class Manager
21
- # Initialize manager with swarm reference
22
- #
23
- # @param swarm [Swarm] Parent swarm instance
24
- def initialize(swarm)
25
- @swarm = swarm
26
- @configs = []
27
- @subscription_ids = []
28
- @barrier = nil
29
- @task_ids = {}
30
- end
31
-
32
- # Add an observer configuration
33
- #
34
- # @param config [Observer::Config] Observer configuration
35
- # @return [void]
36
- def add_config(config)
37
- @configs << config
38
- end
39
-
40
- # Setup event subscriptions for all observer configs
41
- #
42
- # Creates LogCollector subscriptions for each event type, filtered by type.
43
- # Must be called after setup_logging() in Swarm.execute().
44
- #
45
- # @return [void]
46
- def setup
47
- @barrier = Async::Barrier.new
48
-
49
- @configs.each do |config|
50
- config.event_handlers.each do |event_type, handler|
51
- sub_id = LogCollector.subscribe(filter: { type: event_type.to_s }) do |event|
52
- handle_event(config, handler, event)
53
- end
54
- @subscription_ids << sub_id
55
- end
56
- end
57
- end
58
-
59
- # Wait for all observer tasks to complete
60
- #
61
- # Uses Async::Barrier.wait to wait for all spawned tasks.
62
- # Handles errors gracefully without stopping other observers.
63
- #
64
- # @return [void]
65
- def wait_for_completion
66
- return unless @barrier
67
-
68
- # Wait for all tasks, handling errors gracefully
69
- # Barrier.wait re-raises first exception by default, so we use block form
70
- @barrier.wait do |task|
71
- task.wait
72
- rescue StandardError => error
73
- # Log but don't stop waiting for other observers
74
- RubyLLM.logger.error("Observer task failed: #{error.message}")
75
- end
76
- end
77
-
78
- # Cleanup all subscriptions
79
- #
80
- # Unsubscribes from LogCollector to prevent memory leaks.
81
- # Called by Executor.cleanup_after_execution.
82
- #
83
- # @return [void]
84
- def cleanup
85
- @subscription_ids.each { |id| LogCollector.unsubscribe(id) }
86
- @subscription_ids.clear
87
- end
88
-
89
- private
90
-
91
- # Handle an incoming event
92
- #
93
- # Checks self-consumption protection, calls handler block,
94
- # and spawns execution if handler returns a prompt.
95
- #
96
- # @param config [Observer::Config] Observer configuration
97
- # @param handler [Proc] Event handler block
98
- # @param event [Hash] Event data
99
- # @return [void]
100
- def handle_event(config, handler, event)
101
- # CRITICAL: Prevent self-consumption - observer must not consume its own events
102
- # This prevents infinite loops where an observer triggers itself
103
- return if event[:agent] == config.agent_name
104
-
105
- prompt = handler.call(event)
106
- return unless prompt # nil means skip
107
-
108
- spawn_execution(config, prompt, event)
109
- end
110
-
111
- # Spawn an async task for observer execution
112
- #
113
- # Creates a child async task via barrier for the observer agent.
114
- # Sets observer-specific Fiber context.
115
- #
116
- # @param config [Observer::Config] Observer configuration
117
- # @param prompt [String] Prompt to send to observer agent
118
- # @param trigger_event [Hash] Event that triggered this execution
119
- # @return [void]
120
- def spawn_execution(config, prompt, trigger_event)
121
- @barrier.async do
122
- # Set observer-specific context in child fiber
123
- # No need to restore - child fiber dies when task completes
124
- Fiber[:swarm_id] = "#{Fiber[:swarm_id]}/observer:#{config.agent_name}"
125
-
126
- execute_observer_agent(config, prompt, trigger_event)
127
- end
128
- end
129
-
130
- # Execute the observer agent with the prompt
131
- #
132
- # Creates an isolated chat instance and sends the prompt.
133
- # Emits lifecycle events (start, complete, error).
134
- #
135
- # @param config [Observer::Config] Observer configuration
136
- # @param prompt [String] Prompt to execute
137
- # @param trigger_event [Hash] Event that triggered this execution
138
- # @return [RubyLLM::Message, nil] Response or nil on error
139
- def execute_observer_agent(config, prompt, trigger_event)
140
- agent_chat = create_isolated_chat(config.agent_name)
141
-
142
- start_time = Time.now
143
- emit_observer_start(config, trigger_event)
144
-
145
- result = agent_chat.ask(prompt)
146
-
147
- emit_observer_complete(config, trigger_event, result, Time.now - start_time)
148
- result
149
- rescue StandardError => e
150
- emit_observer_error(config, trigger_event, e)
151
- nil
152
- end
153
-
154
- # Create an isolated chat instance for the observer agent
155
- #
156
- # Uses AgentInitializer to create a fully configured agent chat
157
- # without delegation tools (observers don't delegate).
158
- #
159
- # @param agent_name [Symbol] Name of the observer agent
160
- # @return [Agent::Chat] Isolated chat instance
161
- def create_isolated_chat(agent_name)
162
- initializer = Swarm::AgentInitializer.new(@swarm)
163
- initializer.initialize_isolated_agent(agent_name)
164
- end
165
-
166
- # Emit observer_agent_start event
167
- #
168
- # @param config [Observer::Config] Observer configuration
169
- # @param trigger_event [Hash] Triggering event
170
- # @return [void]
171
- def emit_observer_start(config, trigger_event)
172
- return unless LogStream.emitter
173
-
174
- LogStream.emit(
175
- type: "observer_agent_start",
176
- agent: config.agent_name,
177
- trigger_event: trigger_event[:type],
178
- trigger_timestamp: trigger_event[:timestamp],
179
- task_id: generate_task_id(config),
180
- timestamp: Time.now.utc.iso8601,
181
- )
182
- end
183
-
184
- # Emit observer_agent_complete event
185
- #
186
- # @param config [Observer::Config] Observer configuration
187
- # @param trigger_event [Hash] Triggering event
188
- # @param result [RubyLLM::Message] Agent response
189
- # @param duration [Float] Execution duration in seconds
190
- # @return [void]
191
- def emit_observer_complete(config, trigger_event, result, duration)
192
- return unless LogStream.emitter
193
-
194
- LogStream.emit(
195
- type: "observer_agent_complete",
196
- agent: config.agent_name,
197
- trigger_event: trigger_event[:type],
198
- task_id: generate_task_id(config),
199
- duration: duration.round(3),
200
- success: true,
201
- timestamp: Time.now.utc.iso8601,
202
- )
203
- end
204
-
205
- # Emit observer_agent_error event
206
- #
207
- # @param config [Observer::Config] Observer configuration
208
- # @param trigger_event [Hash] Triggering event
209
- # @param error [StandardError] Error that occurred
210
- # @return [void]
211
- def emit_observer_error(config, trigger_event, error)
212
- return unless LogStream.emitter
213
-
214
- LogStream.emit(
215
- type: "observer_agent_error",
216
- agent: config.agent_name,
217
- trigger_event: trigger_event[:type],
218
- task_id: generate_task_id(config),
219
- error: error.message,
220
- backtrace: error.backtrace&.first(5),
221
- timestamp: Time.now.utc.iso8601,
222
- )
223
- end
224
-
225
- # Generate a unique task ID for an observer
226
- #
227
- # Cached per observer agent name for correlation.
228
- #
229
- # @param config [Observer::Config] Observer configuration
230
- # @return [String] Task ID
231
- def generate_task_id(config)
232
- @task_ids[config.agent_name] ||= "observer_#{SecureRandom.hex(6)}"
233
- end
234
- end
235
- end
236
- end
@@ -1,160 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Patterns
5
- # Observes another agent's actions with optional real-time processing
6
- #
7
- # @example Basic observation
8
- # observer = AgentObserver.new(target: :backend)
9
- # observer.start
10
- # swarm.execute("task")
11
- # observer.stop
12
- # puts observer.observations
13
- #
14
- # @example Real-time analysis
15
- # observer = AgentObserver.new(
16
- # target: :backend,
17
- # on_event: ->(e) { analyze_security(e) }
18
- # )
19
- #
20
- # @example Filter specific event types
21
- # observer = AgentObserver.new(
22
- # target: :backend,
23
- # event_types: ["tool_call", "tool_result"]
24
- # )
25
- class AgentObserver
26
- attr_reader :observations, :target_agent
27
-
28
- # Initialize observer
29
- #
30
- # @param target [Symbol] Agent to observe
31
- # @param event_types [Array<String>] Event types to capture (default: all)
32
- # @param on_event [Proc] Optional callback for real-time processing
33
- def initialize(target:, event_types: nil, on_event: nil)
34
- @target_agent = target
35
- @event_types = event_types
36
- @on_event = on_event
37
- @observations = []
38
- @subscription_id = nil
39
- @started_at = nil
40
- end
41
-
42
- # Start observing
43
- #
44
- # @return [void]
45
- def start
46
- return if @subscription_id
47
-
48
- @started_at = Time.now
49
- @observations.clear
50
-
51
- filter = { agent: @target_agent }
52
- filter[:type] = @event_types if @event_types
53
-
54
- @subscription_id = LogCollector.subscribe(filter: filter) do |event|
55
- @observations << event.merge(observed_at: Time.now)
56
- @on_event&.call(event)
57
- end
58
- end
59
-
60
- # Stop observing
61
- #
62
- # @return [void]
63
- def stop
64
- return unless @subscription_id
65
-
66
- LogCollector.unsubscribe(@subscription_id)
67
- @subscription_id = nil
68
- end
69
-
70
- # Check if currently observing
71
- #
72
- # @return [Boolean] true if actively observing
73
- def observing?
74
- !@subscription_id.nil?
75
- end
76
-
77
- # Get summary of observations
78
- #
79
- # @return [Hash] Summary statistics
80
- def summary
81
- {
82
- target: @target_agent,
83
- started_at: @started_at,
84
- duration_seconds: @started_at ? (Time.now - @started_at).round(2) : 0,
85
- total_events: @observations.size,
86
- event_breakdown: @observations.group_by { |e| e[:type] }.transform_values(&:count),
87
- tool_calls: @observations.select { |e| e[:type] == "tool_call" }.map { |e| e[:tool_name] },
88
- errors: @observations.select { |e| e[:type] == "internal_error" },
89
- }
90
- end
91
-
92
- # Format observations for LLM consumption
93
- #
94
- # Useful for providing observation data to another agent for analysis
95
- #
96
- # @return [String] Formatted observation log
97
- def to_llm_context
98
- @observations.map do |event|
99
- case event[:type]
100
- when "tool_call"
101
- "- Called #{event[:tool_name]} with: #{truncate_json(event[:arguments])}"
102
- when "tool_result"
103
- "- #{event[:tool_name]} returned: #{truncate(event[:result])}"
104
- when "agent_step"
105
- "- Thinking: #{truncate(event[:content])}"
106
- when "agent_stop"
107
- "- Final response: #{truncate(event[:content])}"
108
- else
109
- "- [#{event[:type]}] #{event.except(:type, :timestamp, :observed_at).to_json}"
110
- end
111
- end.join("\n")
112
- end
113
-
114
- # Clear collected observations
115
- #
116
- # @return [void]
117
- def clear_observations
118
- @observations.clear
119
- end
120
-
121
- # Execute block while observing
122
- #
123
- # Automatically starts and stops observation around the block
124
- #
125
- # @example
126
- # observer = AgentObserver.new(target: :backend)
127
- # observer.observe do
128
- # swarm.execute("Build API")
129
- # end
130
- # puts observer.summary
131
- #
132
- # @yield Block to execute while observing
133
- # @return [Object] Result from the block
134
- def observe
135
- start
136
- yield
137
- ensure
138
- stop
139
- end
140
-
141
- private
142
-
143
- def truncate(text, max_length = 200)
144
- return "" if text.nil?
145
-
146
- text = text.to_s
147
- return text if text.length <= max_length
148
-
149
- "#{text[0...max_length]}..."
150
- end
151
-
152
- def truncate_json(obj, max_length = 100)
153
- return "{}" if obj.nil?
154
-
155
- json = obj.to_json
156
- truncate(json, max_length)
157
- end
158
- end
159
- end
160
- end
@@ -1,239 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Permissions
5
- # Config parses and validates permission configuration for tools
6
- #
7
- # Handles:
8
- # - Allowed path patterns (allowlist)
9
- # - Denied path patterns (explicit denylist)
10
- # - Allowed command patterns (regex for Bash tool)
11
- # - Denied command patterns (regex for Bash tool)
12
- # - Relative paths converted to absolute based on agent directory
13
- # - Glob pattern matching with absolute paths
14
- #
15
- # All paths and patterns are converted to absolute:
16
- # - Patterns starting with / are kept as-is
17
- # - Relative patterns are expanded against the agent's base directory
18
- # - Paths starting with / are kept as-is
19
- # - Relative paths are expanded against the agent's base directory
20
- #
21
- # Example:
22
- # config = Config.new(
23
- # {
24
- # allowed_paths: ["tmp/**/*"],
25
- # denied_paths: ["tmp/secrets/**"],
26
- # allowed_commands: ["^git (status|diff|log)$"],
27
- # denied_commands: ["^rm -rf"]
28
- # },
29
- # base_directories: ["/home/user/project"]
30
- # )
31
- # config.allowed?("tmp/file.txt") # => true (checks /home/user/project/tmp/file.txt)
32
- # config.allowed?("tmp/secrets/key.pem") # => false (denied takes precedence)
33
- # config.command_allowed?("git status") # => true
34
- # config.command_allowed?("rm -rf /") # => false (denied takes precedence)
35
- class Config
36
- attr_reader :allowed_patterns, :denied_patterns, :allowed_commands, :denied_commands
37
-
38
- # Initialize permission configuration
39
- #
40
- # @param config_hash [Hash] Permission configuration with :allowed_paths, :denied_paths, :allowed_commands, :denied_commands
41
- # @param base_directory [String] Base directory for the agent
42
- def initialize(config_hash, base_directory:)
43
- # Use agent's directory as the base for path resolution
44
- @base_directory = File.expand_path(base_directory)
45
-
46
- # Expand all patterns to absolute paths
47
- @allowed_patterns = expand_patterns(config_hash[:allowed_paths] || [])
48
- @denied_patterns = expand_patterns(config_hash[:denied_paths] || [])
49
-
50
- # Parse command patterns (regex strings)
51
- @allowed_commands = compile_regex_patterns(config_hash[:allowed_commands] || [])
52
- @denied_commands = compile_regex_patterns(config_hash[:denied_commands] || [])
53
- end
54
-
55
- # Check if a path is allowed according to this configuration
56
- #
57
- # Rules:
58
- # 1. Denied patterns take precedence and always block
59
- # 2. If allowed_paths specified: must match at least one pattern (allowlist)
60
- # 3. If allowed_paths NOT specified: allow everything (except denied)
61
- # 4. All paths are converted to absolute for consistent matching
62
- # 5. For directories used as search bases (Glob/Grep), allow if any pattern would match inside
63
- #
64
- # @param path [String] Path to check (relative or absolute)
65
- # @param directory_search [Boolean] True if this is a directory search base (Glob/Grep)
66
- # @return [Boolean] True if path is allowed
67
- def allowed?(path, directory_search: false)
68
- # Convert path to absolute
69
- absolute_path = to_absolute_path(path)
70
-
71
- # Denied patterns take precedence - check first
72
- return false if matches_any?(@denied_patterns, absolute_path)
73
-
74
- # If no allowed patterns, allow everything (except denied)
75
- return true if @allowed_patterns.empty?
76
-
77
- # For directory searches, check if directory is a prefix of any allowed pattern
78
- # Don't check if directory exists - allow non-existent directories as search bases
79
- if directory_search
80
- return true if allowed_as_search_base?(absolute_path)
81
- end
82
-
83
- # Must match at least one allowed pattern
84
- matches_any?(@allowed_patterns, absolute_path)
85
- end
86
-
87
- # Find the specific pattern that denies or doesn't allow a path
88
- #
89
- # @param path [String] Path to check (relative or absolute)
90
- # @param directory_search [Boolean] True if this is a directory search base (Glob/Grep)
91
- # @return [String, nil] The pattern that blocks this path, or nil if allowed
92
- def find_blocking_pattern(path, directory_search: false)
93
- absolute_path = to_absolute_path(path)
94
-
95
- # Check denied patterns first
96
- denied_match = @denied_patterns.find { |pattern| PathMatcher.matches?(pattern, absolute_path) }
97
- return denied_match if denied_match
98
-
99
- # Check allowed patterns
100
- if @allowed_patterns.any?
101
- # For directory searches, check if allowed as search base
102
- # Don't check if directory exists - allow non-existent directories as search bases
103
- if directory_search
104
- return if allowed_as_search_base?(absolute_path)
105
- end
106
-
107
- # Check if path matches any allowed pattern
108
- return if @allowed_patterns.any? { |pattern| PathMatcher.matches?(pattern, absolute_path) }
109
-
110
- # Path doesn't match any allowed pattern
111
- return "(not in allowed list)"
112
- end
113
-
114
- nil
115
- end
116
-
117
- # Convert a path to absolute form
118
- #
119
- # @param path [String] Path to convert
120
- # @return [String] Absolute path
121
- def to_absolute(path)
122
- to_absolute_path(path)
123
- end
124
-
125
- # Check if a command is allowed according to this configuration
126
- #
127
- # Rules:
128
- # 1. Denied command patterns take precedence and always block
129
- # 2. If allowed_commands specified: must match at least one pattern (allowlist)
130
- # 3. If allowed_commands NOT specified: allow everything (except denied)
131
- #
132
- # @param command [String] Command to check
133
- # @return [Boolean] True if command is allowed
134
- def command_allowed?(command)
135
- # Denied patterns take precedence - check first
136
- return false if matches_any_regex?(@denied_commands, command)
137
-
138
- # If no allowed patterns, allow everything (except denied)
139
- return true if @allowed_commands.empty?
140
-
141
- # Must match at least one allowed pattern
142
- matches_any_regex?(@allowed_commands, command)
143
- end
144
-
145
- # Find the specific pattern that denies or doesn't allow a command
146
- #
147
- # @param command [String] Command to check
148
- # @return [String, nil] The pattern that blocks this command, or nil if allowed
149
- def find_blocking_command_pattern(command)
150
- # Check denied patterns first
151
- denied_match = @denied_commands.find { |pattern| pattern.match?(command) }
152
- return denied_match.source if denied_match
153
-
154
- # Check allowed patterns
155
- if @allowed_commands.any?
156
- # Check if command matches any allowed pattern
157
- return if @allowed_commands.any? { |pattern| pattern.match?(command) }
158
-
159
- # Command doesn't match any allowed pattern
160
- return "(not in allowed list)"
161
- end
162
-
163
- nil
164
- end
165
-
166
- private
167
-
168
- # Expand patterns to absolute paths
169
- #
170
- # Patterns starting with / are kept as-is
171
- # Relative patterns are joined with base directory
172
- def expand_patterns(patterns)
173
- Array(patterns).map do |pattern|
174
- if pattern.to_s.start_with?("/")
175
- pattern.to_s
176
- else
177
- File.join(@base_directory, pattern.to_s)
178
- end
179
- end
180
- end
181
-
182
- # Convert path to absolute
183
- #
184
- # Paths starting with / are kept as-is
185
- # Relative paths are expanded against base directory
186
- def to_absolute_path(path)
187
- if path.start_with?("/")
188
- path
189
- else
190
- File.expand_path(path, @base_directory)
191
- end
192
- end
193
-
194
- # Check if path matches any pattern in the list
195
- def matches_any?(patterns, path)
196
- patterns.any? { |pattern| PathMatcher.matches?(pattern, path) }
197
- end
198
-
199
- # Check if a directory is allowed as a search base
200
- #
201
- # A directory is allowed as a search base if any allowed pattern
202
- # would match files or directories inside it.
203
- #
204
- # @param directory_path [String] Absolute path to directory
205
- # @return [Boolean] True if directory can be used as search base
206
- def allowed_as_search_base?(directory_path)
207
- # Normalize directory path (ensure trailing slash for comparison)
208
- dir_with_slash = directory_path.end_with?("/") ? directory_path : "#{directory_path}/"
209
-
210
- @allowed_patterns.any? do |pattern|
211
- # Check if the pattern starts with this directory
212
- # This means files inside this directory would match the pattern
213
- pattern.start_with?(dir_with_slash) || pattern == directory_path
214
- end
215
- end
216
-
217
- # Compile regex patterns from strings
218
- #
219
- # @param patterns [Array<String>] Array of regex pattern strings
220
- # @return [Array<Regexp>] Array of compiled regex objects
221
- def compile_regex_patterns(patterns)
222
- Array(patterns).map do |pattern|
223
- Regexp.new(pattern)
224
- rescue RegexpError => e
225
- raise ConfigurationError, "Invalid regex pattern '#{pattern}': #{e.message}"
226
- end
227
- end
228
-
229
- # Check if command matches any regex pattern in the list
230
- #
231
- # @param patterns [Array<Regexp>] Array of compiled regex patterns
232
- # @param command [String] Command to check
233
- # @return [Boolean] True if command matches any pattern
234
- def matches_any_regex?(patterns, command)
235
- patterns.any? { |pattern| pattern.match?(command) }
236
- end
237
- end
238
- end
239
- end