swarm_memory 2.1.5 → 2.1.6

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