swarm_memory 2.1.4 → 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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. data/lib/swarm_memory.rb +7 -2
  4. metadata +6 -185
  5. data/lib/claude_swarm/base_executor.rb +0 -133
  6. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  7. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  8. data/lib/claude_swarm/cli.rb +0 -697
  9. data/lib/claude_swarm/commands/ps.rb +0 -215
  10. data/lib/claude_swarm/commands/show.rb +0 -139
  11. data/lib/claude_swarm/configuration.rb +0 -373
  12. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  13. data/lib/claude_swarm/json_handler.rb +0 -91
  14. data/lib/claude_swarm/mcp_generator.rb +0 -243
  15. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  16. data/lib/claude_swarm/openai/executor.rb +0 -256
  17. data/lib/claude_swarm/openai/responses.rb +0 -319
  18. data/lib/claude_swarm/orchestrator.rb +0 -878
  19. data/lib/claude_swarm/process_tracker.rb +0 -78
  20. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  21. data/lib/claude_swarm/session_path.rb +0 -42
  22. data/lib/claude_swarm/settings_generator.rb +0 -77
  23. data/lib/claude_swarm/system_utils.rb +0 -46
  24. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  25. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  27. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  28. data/lib/claude_swarm/version.rb +0 -5
  29. data/lib/claude_swarm/worktree_manager.rb +0 -475
  30. data/lib/claude_swarm/yaml_loader.rb +0 -22
  31. data/lib/claude_swarm.rb +0 -67
  32. data/lib/swarm_cli/cli.rb +0 -201
  33. data/lib/swarm_cli/command_registry.rb +0 -61
  34. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  35. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  36. data/lib/swarm_cli/commands/migrate.rb +0 -55
  37. data/lib/swarm_cli/commands/run.rb +0 -173
  38. data/lib/swarm_cli/config_loader.rb +0 -98
  39. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  40. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  41. data/lib/swarm_cli/interactive_repl.rb +0 -924
  42. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  43. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  44. data/lib/swarm_cli/migrate_options.rb +0 -54
  45. data/lib/swarm_cli/migrator.rb +0 -132
  46. data/lib/swarm_cli/options.rb +0 -151
  47. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  48. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  49. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  50. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  51. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  52. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  53. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  54. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  55. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  56. data/lib/swarm_cli/ui/icons.rb +0 -36
  57. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  58. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  59. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  60. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  61. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  62. data/lib/swarm_cli/version.rb +0 -5
  63. data/lib/swarm_cli.rb +0 -46
  64. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  65. data/lib/swarm_sdk/agent/builder.rb +0 -552
  66. data/lib/swarm_sdk/agent/chat.rb +0 -774
  67. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  68. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  69. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  70. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  71. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  72. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  73. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  75. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  76. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  77. data/lib/swarm_sdk/agent/context.rb +0 -116
  78. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  79. data/lib/swarm_sdk/agent/definition.rb +0 -477
  80. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  81. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  82. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  83. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  84. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  85. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  86. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  87. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  88. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  89. data/lib/swarm_sdk/configuration.rb +0 -135
  90. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  91. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  92. data/lib/swarm_sdk/context_compactor.rb +0 -335
  93. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  94. data/lib/swarm_sdk/context_management/context.rb +0 -328
  95. data/lib/swarm_sdk/defaults.rb +0 -196
  96. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  97. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  98. data/lib/swarm_sdk/hooks/context.rb +0 -197
  99. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  100. data/lib/swarm_sdk/hooks/error.rb +0 -29
  101. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  102. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  103. data/lib/swarm_sdk/hooks/result.rb +0 -150
  104. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  105. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  106. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  107. data/lib/swarm_sdk/log_collector.rb +0 -227
  108. data/lib/swarm_sdk/log_stream.rb +0 -127
  109. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  110. data/lib/swarm_sdk/model_aliases.json +0 -8
  111. data/lib/swarm_sdk/models.json +0 -1
  112. data/lib/swarm_sdk/models.rb +0 -120
  113. data/lib/swarm_sdk/node_context.rb +0 -245
  114. data/lib/swarm_sdk/observer/builder.rb +0 -81
  115. data/lib/swarm_sdk/observer/config.rb +0 -45
  116. data/lib/swarm_sdk/observer/manager.rb +0 -236
  117. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  118. data/lib/swarm_sdk/permissions/config.rb +0 -239
  119. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  120. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  121. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  122. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  123. data/lib/swarm_sdk/plugin.rb +0 -309
  124. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  125. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  126. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  127. data/lib/swarm_sdk/restore_result.rb +0 -65
  128. data/lib/swarm_sdk/result.rb +0 -123
  129. data/lib/swarm_sdk/snapshot.rb +0 -156
  130. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  131. data/lib/swarm_sdk/state_restorer.rb +0 -476
  132. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  133. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  134. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  135. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  136. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  137. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  138. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  139. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  140. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  141. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  142. data/lib/swarm_sdk/swarm.rb +0 -717
  143. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  144. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  145. data/lib/swarm_sdk/tools/bash.rb +0 -282
  146. data/lib/swarm_sdk/tools/clock.rb +0 -44
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  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 -163
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  160. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  161. data/lib/swarm_sdk/tools/read.rb +0 -261
  162. data/lib/swarm_sdk/tools/registry.rb +0 -205
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  166. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  167. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
  168. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  169. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  170. data/lib/swarm_sdk/tools/think.rb +0 -98
  171. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  172. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  173. data/lib/swarm_sdk/tools/write.rb +0 -112
  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 -79
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  182. data/lib/swarm_sdk/workflow.rb +0 -554
  183. data/lib/swarm_sdk.rb +0 -524
  184. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -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