swarm_sdk 2.7.14 → 3.0.0.alpha2

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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
  4. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  5. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  6. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  7. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  8. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  9. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  10. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  11. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  12. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  13. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  14. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  15. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  16. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  17. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  18. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  19. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  20. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  24. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  25. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  26. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  27. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  28. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  29. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  30. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  31. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  32. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  33. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  34. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  35. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  36. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  37. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  38. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  39. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  40. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  41. data/lib/swarm_sdk/v3/tools/document_converters/base.rb +84 -0
  42. data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
  43. data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -0
  45. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  46. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  47. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  48. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  49. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  50. data/lib/swarm_sdk/v3/tools/read.rb +213 -0
  51. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  52. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  53. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  54. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  55. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  56. data/lib/swarm_sdk/v3.rb +145 -0
  57. metadata +88 -149
  58. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  59. data/lib/swarm_sdk/agent/builder.rb +0 -705
  60. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  61. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  62. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  63. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  64. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  65. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  66. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  67. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  68. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  69. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  70. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  71. data/lib/swarm_sdk/agent/context.rb +0 -115
  72. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  73. data/lib/swarm_sdk/agent/definition.rb +0 -588
  74. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  75. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  76. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  77. data/lib/swarm_sdk/agent_registry.rb +0 -146
  78. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  79. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  80. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  81. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  82. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  83. data/lib/swarm_sdk/config.rb +0 -368
  84. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  85. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  86. data/lib/swarm_sdk/configuration.rb +0 -165
  87. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  88. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  89. data/lib/swarm_sdk/context_compactor.rb +0 -335
  90. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  91. data/lib/swarm_sdk/context_management/context.rb +0 -328
  92. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  93. data/lib/swarm_sdk/defaults.rb +0 -251
  94. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  95. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  96. data/lib/swarm_sdk/hooks/context.rb +0 -197
  97. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  98. data/lib/swarm_sdk/hooks/error.rb +0 -29
  99. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  100. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  101. data/lib/swarm_sdk/hooks/result.rb +0 -150
  102. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  103. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  104. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  105. data/lib/swarm_sdk/log_collector.rb +0 -227
  106. data/lib/swarm_sdk/log_stream.rb +0 -127
  107. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  108. data/lib/swarm_sdk/model_aliases.json +0 -8
  109. data/lib/swarm_sdk/models.json +0 -44002
  110. data/lib/swarm_sdk/models.rb +0 -161
  111. data/lib/swarm_sdk/node_context.rb +0 -245
  112. data/lib/swarm_sdk/observer/builder.rb +0 -81
  113. data/lib/swarm_sdk/observer/config.rb +0 -45
  114. data/lib/swarm_sdk/observer/manager.rb +0 -248
  115. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  116. data/lib/swarm_sdk/permissions/config.rb +0 -239
  117. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  118. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  119. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  120. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  121. data/lib/swarm_sdk/plugin.rb +0 -309
  122. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  123. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  124. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  125. data/lib/swarm_sdk/restore_result.rb +0 -65
  126. data/lib/swarm_sdk/result.rb +0 -241
  127. data/lib/swarm_sdk/snapshot.rb +0 -156
  128. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  129. data/lib/swarm_sdk/state_restorer.rb +0 -476
  130. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  131. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  132. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  133. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  134. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  135. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  136. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  141. data/lib/swarm_sdk/swarm.rb +0 -973
  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/base.rb +0 -63
  145. data/lib/swarm_sdk/tools/bash.rb +0 -280
  146. data/lib/swarm_sdk/tools/clock.rb +0 -46
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  148. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  149. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  150. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  151. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  152. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  153. data/lib/swarm_sdk/tools/edit.rb +0 -145
  154. data/lib/swarm_sdk/tools/glob.rb +0 -166
  155. data/lib/swarm_sdk/tools/grep.rb +0 -235
  156. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  157. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  160. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  161. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  162. data/lib/swarm_sdk/tools/read.rb +0 -261
  163. data/lib/swarm_sdk/tools/registry.rb +0 -205
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  166. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  167. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  168. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  169. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  170. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  171. data/lib/swarm_sdk/tools/think.rb +0 -100
  172. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  173. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  174. data/lib/swarm_sdk/tools/write.rb +0 -112
  175. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  176. data/lib/swarm_sdk/utils.rb +0 -68
  177. data/lib/swarm_sdk/validation_result.rb +0 -33
  178. data/lib/swarm_sdk/version.rb +0 -5
  179. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  180. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  181. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  182. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  183. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  184. data/lib/swarm_sdk/workflow.rb +0 -589
  185. data/lib/swarm_sdk.rb +0 -721
@@ -1,248 +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
- # Stop all observer tasks immediately
79
- #
80
- # Interrupts in-flight observer LLM calls by stopping the barrier.
81
- # Called during swarm interruption instead of wait_for_completion.
82
- #
83
- # @return [void]
84
- def stop
85
- @barrier&.stop
86
- rescue StandardError => e
87
- RubyLLM.logger.debug("SwarmSDK: Error stopping observer barrier: #{e.message}")
88
- end
89
-
90
- # Cleanup all subscriptions
91
- #
92
- # Unsubscribes from LogCollector to prevent memory leaks.
93
- # Called by Executor.cleanup_after_execution.
94
- #
95
- # @return [void]
96
- def cleanup
97
- @subscription_ids.each { |id| LogCollector.unsubscribe(id) }
98
- @subscription_ids.clear
99
- end
100
-
101
- private
102
-
103
- # Handle an incoming event
104
- #
105
- # Checks self-consumption protection, calls handler block,
106
- # and spawns execution if handler returns a prompt.
107
- #
108
- # @param config [Observer::Config] Observer configuration
109
- # @param handler [Proc] Event handler block
110
- # @param event [Hash] Event data
111
- # @return [void]
112
- def handle_event(config, handler, event)
113
- # CRITICAL: Prevent self-consumption - observer must not consume its own events
114
- # This prevents infinite loops where an observer triggers itself
115
- return if event[:agent] == config.agent_name
116
-
117
- prompt = handler.call(event)
118
- return unless prompt # nil means skip
119
-
120
- spawn_execution(config, prompt, event)
121
- end
122
-
123
- # Spawn an async task for observer execution
124
- #
125
- # Creates a child async task via barrier for the observer agent.
126
- # Sets observer-specific Fiber context.
127
- #
128
- # @param config [Observer::Config] Observer configuration
129
- # @param prompt [String] Prompt to send to observer agent
130
- # @param trigger_event [Hash] Event that triggered this execution
131
- # @return [void]
132
- def spawn_execution(config, prompt, trigger_event)
133
- @barrier.async do
134
- # Set observer-specific context in child fiber
135
- # No need to restore - child fiber dies when task completes
136
- Fiber[:swarm_id] = "#{Fiber[:swarm_id]}/observer:#{config.agent_name}"
137
-
138
- execute_observer_agent(config, prompt, trigger_event)
139
- end
140
- end
141
-
142
- # Execute the observer agent with the prompt
143
- #
144
- # Creates an isolated chat instance and sends the prompt.
145
- # Emits lifecycle events (start, complete, error).
146
- #
147
- # @param config [Observer::Config] Observer configuration
148
- # @param prompt [String] Prompt to execute
149
- # @param trigger_event [Hash] Event that triggered this execution
150
- # @return [RubyLLM::Message, nil] Response or nil on error
151
- def execute_observer_agent(config, prompt, trigger_event)
152
- agent_chat = create_isolated_chat(config.agent_name)
153
-
154
- start_time = Time.now
155
- emit_observer_start(config, trigger_event)
156
-
157
- result = agent_chat.ask(prompt)
158
-
159
- emit_observer_complete(config, trigger_event, result, Time.now - start_time)
160
- result
161
- rescue StandardError => e
162
- emit_observer_error(config, trigger_event, e)
163
- nil
164
- end
165
-
166
- # Create an isolated chat instance for the observer agent
167
- #
168
- # Uses AgentInitializer to create a fully configured agent chat
169
- # without delegation tools (observers don't delegate).
170
- #
171
- # @param agent_name [Symbol] Name of the observer agent
172
- # @return [Agent::Chat] Isolated chat instance
173
- def create_isolated_chat(agent_name)
174
- initializer = Swarm::AgentInitializer.new(@swarm)
175
- initializer.initialize_isolated_agent(agent_name)
176
- end
177
-
178
- # Emit observer_agent_start event
179
- #
180
- # @param config [Observer::Config] Observer configuration
181
- # @param trigger_event [Hash] Triggering event
182
- # @return [void]
183
- def emit_observer_start(config, trigger_event)
184
- return unless LogStream.emitter
185
-
186
- LogStream.emit(
187
- type: "observer_agent_start",
188
- agent: config.agent_name,
189
- trigger_event: trigger_event[:type],
190
- trigger_timestamp: trigger_event[:timestamp],
191
- task_id: generate_task_id(config),
192
- timestamp: Time.now.utc.iso8601,
193
- )
194
- end
195
-
196
- # Emit observer_agent_complete event
197
- #
198
- # @param config [Observer::Config] Observer configuration
199
- # @param trigger_event [Hash] Triggering event
200
- # @param result [RubyLLM::Message] Agent response
201
- # @param duration [Float] Execution duration in seconds
202
- # @return [void]
203
- def emit_observer_complete(config, trigger_event, result, duration)
204
- return unless LogStream.emitter
205
-
206
- LogStream.emit(
207
- type: "observer_agent_complete",
208
- agent: config.agent_name,
209
- trigger_event: trigger_event[:type],
210
- task_id: generate_task_id(config),
211
- duration: duration.round(3),
212
- success: true,
213
- timestamp: Time.now.utc.iso8601,
214
- )
215
- end
216
-
217
- # Emit observer_agent_error event
218
- #
219
- # @param config [Observer::Config] Observer configuration
220
- # @param trigger_event [Hash] Triggering event
221
- # @param error [StandardError] Error that occurred
222
- # @return [void]
223
- def emit_observer_error(config, trigger_event, error)
224
- return unless LogStream.emitter
225
-
226
- LogStream.emit(
227
- type: "observer_agent_error",
228
- agent: config.agent_name,
229
- trigger_event: trigger_event[:type],
230
- task_id: generate_task_id(config),
231
- error: error.message,
232
- backtrace: error.backtrace&.first(5),
233
- timestamp: Time.now.utc.iso8601,
234
- )
235
- end
236
-
237
- # Generate a unique task ID for an observer
238
- #
239
- # Cached per observer agent name for correlation.
240
- #
241
- # @param config [Observer::Config] Observer configuration
242
- # @return [String] Task ID
243
- def generate_task_id(config)
244
- @task_ids[config.agent_name] ||= "observer_#{SecureRandom.hex(6)}"
245
- end
246
- end
247
- end
248
- 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