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.
- checksums.yaml +4 -4
- data/lib/swarm_memory/version.rb +1 -1
- metadata +5 -184
- data/lib/claude_swarm/base_executor.rb +0 -133
- data/lib/claude_swarm/claude_code_executor.rb +0 -349
- data/lib/claude_swarm/claude_mcp_server.rb +0 -78
- data/lib/claude_swarm/cli.rb +0 -697
- data/lib/claude_swarm/commands/ps.rb +0 -215
- data/lib/claude_swarm/commands/show.rb +0 -139
- data/lib/claude_swarm/configuration.rb +0 -373
- data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
- data/lib/claude_swarm/json_handler.rb +0 -91
- data/lib/claude_swarm/mcp_generator.rb +0 -230
- data/lib/claude_swarm/openai/chat_completion.rb +0 -256
- data/lib/claude_swarm/openai/executor.rb +0 -256
- data/lib/claude_swarm/openai/responses.rb +0 -319
- data/lib/claude_swarm/orchestrator.rb +0 -878
- data/lib/claude_swarm/process_tracker.rb +0 -78
- data/lib/claude_swarm/session_cost_calculator.rb +0 -209
- data/lib/claude_swarm/session_path.rb +0 -42
- data/lib/claude_swarm/settings_generator.rb +0 -77
- data/lib/claude_swarm/system_utils.rb +0 -46
- data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
- data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
- data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
- data/lib/claude_swarm/tools/task_tool.rb +0 -63
- data/lib/claude_swarm/version.rb +0 -5
- data/lib/claude_swarm/worktree_manager.rb +0 -475
- data/lib/claude_swarm/yaml_loader.rb +0 -22
- data/lib/claude_swarm.rb +0 -67
- data/lib/swarm_cli/cli.rb +0 -201
- data/lib/swarm_cli/command_registry.rb +0 -61
- data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
- data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
- data/lib/swarm_cli/commands/migrate.rb +0 -55
- data/lib/swarm_cli/commands/run.rb +0 -173
- data/lib/swarm_cli/config_loader.rb +0 -98
- data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
- data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
- data/lib/swarm_cli/interactive_repl.rb +0 -924
- data/lib/swarm_cli/mcp_serve_options.rb +0 -44
- data/lib/swarm_cli/mcp_tools_options.rb +0 -59
- data/lib/swarm_cli/migrate_options.rb +0 -54
- data/lib/swarm_cli/migrator.rb +0 -132
- data/lib/swarm_cli/options.rb +0 -151
- data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
- data/lib/swarm_cli/ui/components/content_block.rb +0 -120
- data/lib/swarm_cli/ui/components/divider.rb +0 -57
- data/lib/swarm_cli/ui/components/panel.rb +0 -62
- data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
- data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
- data/lib/swarm_cli/ui/formatters/number.rb +0 -58
- data/lib/swarm_cli/ui/formatters/text.rb +0 -77
- data/lib/swarm_cli/ui/formatters/time.rb +0 -73
- data/lib/swarm_cli/ui/icons.rb +0 -36
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
- data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
- data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
- data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
- data/lib/swarm_cli/version.rb +0 -5
- data/lib/swarm_cli.rb +0 -46
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
- data/lib/swarm_sdk/agent/builder.rb +0 -552
- data/lib/swarm_sdk/agent/chat.rb +0 -774
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
- data/lib/swarm_sdk/agent/context.rb +0 -116
- data/lib/swarm_sdk/agent/context_manager.rb +0 -315
- data/lib/swarm_sdk/agent/definition.rb +0 -477
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
- data/lib/swarm_sdk/builders/base_builder.rb +0 -409
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
- data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
- data/lib/swarm_sdk/concerns/validatable.rb +0 -55
- data/lib/swarm_sdk/configuration/parser.rb +0 -353
- data/lib/swarm_sdk/configuration/translator.rb +0 -255
- data/lib/swarm_sdk/configuration.rb +0 -135
- data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
- data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
- data/lib/swarm_sdk/context_compactor.rb +0 -335
- data/lib/swarm_sdk/context_management/builder.rb +0 -128
- data/lib/swarm_sdk/context_management/context.rb +0 -328
- data/lib/swarm_sdk/defaults.rb +0 -196
- data/lib/swarm_sdk/events_to_messages.rb +0 -199
- data/lib/swarm_sdk/hooks/adapter.rb +0 -359
- data/lib/swarm_sdk/hooks/context.rb +0 -197
- data/lib/swarm_sdk/hooks/definition.rb +0 -80
- data/lib/swarm_sdk/hooks/error.rb +0 -29
- data/lib/swarm_sdk/hooks/executor.rb +0 -146
- data/lib/swarm_sdk/hooks/registry.rb +0 -147
- data/lib/swarm_sdk/hooks/result.rb +0 -150
- data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
- data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
- data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
- data/lib/swarm_sdk/log_collector.rb +0 -227
- data/lib/swarm_sdk/log_stream.rb +0 -127
- data/lib/swarm_sdk/markdown_parser.rb +0 -75
- data/lib/swarm_sdk/model_aliases.json +0 -8
- data/lib/swarm_sdk/models.json +0 -1
- data/lib/swarm_sdk/models.rb +0 -120
- data/lib/swarm_sdk/node_context.rb +0 -245
- data/lib/swarm_sdk/observer/builder.rb +0 -81
- data/lib/swarm_sdk/observer/config.rb +0 -45
- data/lib/swarm_sdk/observer/manager.rb +0 -236
- data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
- data/lib/swarm_sdk/permissions/config.rb +0 -239
- data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
- data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
- data/lib/swarm_sdk/permissions/validator.rb +0 -173
- data/lib/swarm_sdk/permissions_builder.rb +0 -122
- data/lib/swarm_sdk/plugin.rb +0 -309
- data/lib/swarm_sdk/plugin_registry.rb +0 -101
- data/lib/swarm_sdk/proc_helpers.rb +0 -53
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -123
- data/lib/swarm_sdk/snapshot.rb +0 -156
- data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
- data/lib/swarm_sdk/state_restorer.rb +0 -476
- data/lib/swarm_sdk/state_snapshot.rb +0 -334
- data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
- data/lib/swarm_sdk/swarm/builder.rb +0 -249
- data/lib/swarm_sdk/swarm/executor.rb +0 -213
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
- data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
- data/lib/swarm_sdk/swarm.rb +0 -717
- data/lib/swarm_sdk/swarm_loader.rb +0 -145
- data/lib/swarm_sdk/swarm_registry.rb +0 -136
- data/lib/swarm_sdk/tools/bash.rb +0 -282
- data/lib/swarm_sdk/tools/clock.rb +0 -44
- data/lib/swarm_sdk/tools/delegate.rb +0 -267
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
- data/lib/swarm_sdk/tools/edit.rb +0 -145
- data/lib/swarm_sdk/tools/glob.rb +0 -166
- data/lib/swarm_sdk/tools/grep.rb +0 -235
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
- data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
- data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
- data/lib/swarm_sdk/tools/read.rb +0 -261
- data/lib/swarm_sdk/tools/registry.rb +0 -205
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
- data/lib/swarm_sdk/tools/think.rb +0 -98
- data/lib/swarm_sdk/tools/todo_write.rb +0 -235
- data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
- data/lib/swarm_sdk/tools/write.rb +0 -112
- data/lib/swarm_sdk/utils.rb +0 -68
- data/lib/swarm_sdk/validation_result.rb +0 -33
- data/lib/swarm_sdk/version.rb +0 -5
- data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
- data/lib/swarm_sdk/workflow/builder.rb +0 -143
- data/lib/swarm_sdk/workflow/executor.rb +0 -497
- data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
- data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
- data/lib/swarm_sdk/workflow.rb +0 -554
- 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
|