swarm_memory 2.1.2 → 2.1.4
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/claude_swarm/claude_mcp_server.rb +1 -0
- data/lib/claude_swarm/cli.rb +5 -18
- data/lib/claude_swarm/configuration.rb +30 -19
- data/lib/claude_swarm/mcp_generator.rb +5 -10
- data/lib/claude_swarm/openai/chat_completion.rb +4 -12
- data/lib/claude_swarm/openai/executor.rb +3 -1
- data/lib/claude_swarm/openai/responses.rb +13 -32
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
- data/lib/swarm_cli/commands/run.rb +2 -2
- data/lib/swarm_cli/config_loader.rb +14 -14
- data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
- data/lib/swarm_cli/interactive_repl.rb +11 -5
- data/lib/swarm_cli/ui/icons.rb +0 -23
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/adapters/base.rb +4 -4
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
- data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
- data/lib/swarm_memory/integration/cli_registration.rb +3 -2
- data/lib/swarm_memory/integration/sdk_plugin.rb +98 -12
- data/lib/swarm_memory/tools/memory_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_read.rb +3 -3
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +6 -1
- data/lib/swarm_sdk/agent/builder.rb +91 -0
- data/lib/swarm_sdk/agent/chat.rb +540 -925
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +33 -79
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +147 -39
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +22 -38
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
- data/lib/swarm_sdk/agent/context.rb +8 -4
- data/lib/swarm_sdk/agent/context_manager.rb +6 -0
- data/lib/swarm_sdk/agent/definition.rb +79 -174
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +182 -0
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
- data/lib/swarm_sdk/builders/base_builder.rb +409 -0
- data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
- data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
- data/lib/swarm_sdk/concerns/validatable.rb +55 -0
- data/lib/swarm_sdk/configuration/parser.rb +353 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +100 -261
- data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
- data/lib/swarm_sdk/context_compactor.rb +6 -11
- data/lib/swarm_sdk/context_management/builder.rb +128 -0
- data/lib/swarm_sdk/context_management/context.rb +328 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +199 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
- data/lib/swarm_sdk/log_collector.rb +192 -16
- data/lib/swarm_sdk/log_stream.rb +66 -8
- data/lib/swarm_sdk/model_aliases.json +4 -1
- data/lib/swarm_sdk/node_context.rb +1 -1
- data/lib/swarm_sdk/observer/builder.rb +81 -0
- data/lib/swarm_sdk/observer/config.rb +45 -0
- data/lib/swarm_sdk/observer/manager.rb +236 -0
- data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
- data/lib/swarm_sdk/plugin.rb +93 -3
- data/lib/swarm_sdk/proc_helpers.rb +53 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
- data/lib/swarm_sdk/restore_result.rb +65 -0
- data/lib/swarm_sdk/snapshot.rb +156 -0
- data/lib/swarm_sdk/snapshot_from_events.rb +397 -0
- data/lib/swarm_sdk/state_restorer.rb +476 -0
- data/lib/swarm_sdk/state_snapshot.rb +334 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +428 -79
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
- data/lib/swarm_sdk/swarm/builder.rb +69 -407
- data/lib/swarm_sdk/swarm/executor.rb +213 -0
- data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +88 -149
- data/lib/swarm_sdk/swarm.rb +366 -631
- data/lib/swarm_sdk/swarm_loader.rb +145 -0
- data/lib/swarm_sdk/swarm_registry.rb +136 -0
- data/lib/swarm_sdk/tools/bash.rb +11 -3
- data/lib/swarm_sdk/tools/delegate.rb +127 -24
- data/lib/swarm_sdk/tools/edit.rb +8 -13
- data/lib/swarm_sdk/tools/glob.rb +9 -1
- data/lib/swarm_sdk/tools/grep.rb +7 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
- data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
- data/lib/swarm_sdk/tools/read.rb +28 -18
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +53 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/think.rb +4 -1
- data/lib/swarm_sdk/tools/todo_write.rb +27 -8
- data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/utils.rb +18 -0
- data/lib/swarm_sdk/validation_result.rb +33 -0
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +34 -9
- data/lib/swarm_sdk/workflow/builder.rb +143 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +42 -21
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
- data/lib/swarm_sdk/workflow.rb +554 -0
- data/lib/swarm_sdk.rb +393 -22
- metadata +51 -16
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_sdk/node_orchestrator.rb +0 -591
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -582
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Observer
|
|
5
|
+
# Configuration for an observer agent
|
|
6
|
+
#
|
|
7
|
+
# Holds the agent name, event handlers (blocks that return prompts or nil),
|
|
8
|
+
# and execution options.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# config = Observer::Config.new(:profiler)
|
|
12
|
+
# config.add_handler(:swarm_start) { |event| "Analyze: #{event[:prompt]}" }
|
|
13
|
+
# config.options[:timeout] = 120
|
|
14
|
+
class Config
|
|
15
|
+
attr_reader :agent_name, :event_handlers, :options
|
|
16
|
+
|
|
17
|
+
# Initialize a new observer configuration
|
|
18
|
+
#
|
|
19
|
+
# @param agent_name [Symbol] Name of the agent to use as observer
|
|
20
|
+
def initialize(agent_name)
|
|
21
|
+
@agent_name = agent_name
|
|
22
|
+
@event_handlers = {} # { event_type => block }
|
|
23
|
+
@options = {
|
|
24
|
+
max_concurrent: nil,
|
|
25
|
+
timeout: 60,
|
|
26
|
+
fire_and_forget: true,
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Add an event handler for a specific event type
|
|
31
|
+
#
|
|
32
|
+
# The block receives the event hash and should return:
|
|
33
|
+
# - A prompt string to trigger the observer agent
|
|
34
|
+
# - nil to skip execution for this event
|
|
35
|
+
#
|
|
36
|
+
# @param event_type [Symbol] Type of event to handle (e.g., :swarm_start, :tool_call)
|
|
37
|
+
# @yield [Hash] Event hash with type, agent, and other data
|
|
38
|
+
# @yieldreturn [String, nil] Prompt to execute or nil to skip
|
|
39
|
+
# @return [void]
|
|
40
|
+
def add_handler(event_type, &block)
|
|
41
|
+
@event_handlers[event_type] = block
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,236 @@
|
|
|
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
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
data/lib/swarm_sdk/plugin.rb
CHANGED
|
@@ -10,7 +10,7 @@ module SwarmSDK
|
|
|
10
10
|
# ## Adding Custom Attributes to Agents
|
|
11
11
|
#
|
|
12
12
|
# Plugins can add custom attributes to Agent::Definition that are preserved
|
|
13
|
-
# when agents are cloned (e.g., in
|
|
13
|
+
# when agents are cloned (e.g., in Workflow). To do this:
|
|
14
14
|
#
|
|
15
15
|
# 1. Add attr_reader to Agent::Definition for your attribute
|
|
16
16
|
# 2. Parse the attribute in Agent::Definition#initialize
|
|
@@ -62,7 +62,7 @@ module SwarmSDK
|
|
|
62
62
|
# my_custom_config { option: "value" }
|
|
63
63
|
# end
|
|
64
64
|
#
|
|
65
|
-
# And it will be preserved when
|
|
65
|
+
# And it will be preserved when Workflow clones the agent!
|
|
66
66
|
#
|
|
67
67
|
# @example Real-world: SwarmMemory plugin
|
|
68
68
|
# # SwarmMemory adds 'memory' attribute to agents
|
|
@@ -197,7 +197,7 @@ module SwarmSDK
|
|
|
197
197
|
# Contribute to agent serialization (optional)
|
|
198
198
|
#
|
|
199
199
|
# Called when Agent::Definition.to_h is invoked (e.g., for cloning agents
|
|
200
|
-
# in
|
|
200
|
+
# in Workflow). Plugins can return config keys that should be
|
|
201
201
|
# included in the serialized hash to preserve their state.
|
|
202
202
|
#
|
|
203
203
|
# This allows plugins to maintain their configuration when agents are
|
|
@@ -215,5 +215,95 @@ module SwarmSDK
|
|
|
215
215
|
def serialize_config(agent_definition:)
|
|
216
216
|
{}
|
|
217
217
|
end
|
|
218
|
+
|
|
219
|
+
# Snapshot plugin-specific state for an agent
|
|
220
|
+
#
|
|
221
|
+
# Called during state snapshot creation (e.g., session persistence).
|
|
222
|
+
# Return any state your plugin needs to persist for this agent.
|
|
223
|
+
# The returned hash will be JSON serialized.
|
|
224
|
+
#
|
|
225
|
+
# @param agent_name [Symbol] Agent identifier
|
|
226
|
+
# @return [Hash] Plugin-specific state (empty hash if nothing to snapshot)
|
|
227
|
+
#
|
|
228
|
+
# @example Memory read tracking
|
|
229
|
+
# def snapshot_agent_state(agent_name)
|
|
230
|
+
# entries = StorageReadTracker.get_read_entries(agent_name)
|
|
231
|
+
# return {} if entries.empty?
|
|
232
|
+
#
|
|
233
|
+
# { read_entries: entries }
|
|
234
|
+
# end
|
|
235
|
+
def snapshot_agent_state(agent_name)
|
|
236
|
+
{}
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Restore plugin-specific state for an agent
|
|
240
|
+
#
|
|
241
|
+
# Called during state restoration. Restore any persisted state.
|
|
242
|
+
# This method is idempotent - calling it multiple times with
|
|
243
|
+
# the same state should produce the same result.
|
|
244
|
+
#
|
|
245
|
+
# @param agent_name [Symbol] Agent identifier
|
|
246
|
+
# @param state [Hash] Previously snapshotted state (with symbol keys)
|
|
247
|
+
# @return [void]
|
|
248
|
+
#
|
|
249
|
+
# @example Memory read tracking
|
|
250
|
+
# def restore_agent_state(agent_name, state)
|
|
251
|
+
# entries = state[:read_entries] || state["read_entries"]
|
|
252
|
+
# return unless entries
|
|
253
|
+
#
|
|
254
|
+
# StorageReadTracker.restore_read_entries(agent_name, entries)
|
|
255
|
+
# end
|
|
256
|
+
def restore_agent_state(agent_name, state)
|
|
257
|
+
# Override if needed
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Get digest for a tool result (e.g., file hash, memory entry hash)
|
|
261
|
+
#
|
|
262
|
+
# Called during tool result metadata collection. Returns a digest
|
|
263
|
+
# that can be used to detect if the resource has changed since
|
|
264
|
+
# it was last read. This enables change detection hooks.
|
|
265
|
+
#
|
|
266
|
+
# @param agent_name [Symbol] Agent identifier
|
|
267
|
+
# @param tool_name [String] Name of the tool (e.g., "MemoryRead")
|
|
268
|
+
# @param path [String] Path or identifier of the resource
|
|
269
|
+
# @return [String, nil] Digest string or nil if not tracked by this plugin
|
|
270
|
+
#
|
|
271
|
+
# @example Memory read tracking
|
|
272
|
+
# def get_tool_result_digest(agent_name:, tool_name:, path:)
|
|
273
|
+
# return unless tool_name == "MemoryRead"
|
|
274
|
+
#
|
|
275
|
+
# StorageReadTracker.get_read_entries(agent_name)[path]
|
|
276
|
+
# end
|
|
277
|
+
def get_tool_result_digest(agent_name:, tool_name:, path:)
|
|
278
|
+
nil
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Translate YAML configuration into DSL calls
|
|
282
|
+
#
|
|
283
|
+
# Called during YAML-to-DSL translation. Plugins can translate their
|
|
284
|
+
# specific YAML configuration keys into DSL method calls on the builder.
|
|
285
|
+
# This allows SDK to remain plugin-agnostic while plugins can add
|
|
286
|
+
# YAML configuration support.
|
|
287
|
+
#
|
|
288
|
+
# @param builder [Agent::Builder] Builder instance (self in DSL context)
|
|
289
|
+
# @param agent_config [Hash] Full agent config from YAML
|
|
290
|
+
# @return [void]
|
|
291
|
+
#
|
|
292
|
+
# @example Memory plugin YAML translation
|
|
293
|
+
# def translate_yaml_config(builder, agent_config)
|
|
294
|
+
# memory_config = agent_config[:memory]
|
|
295
|
+
# return unless memory_config
|
|
296
|
+
#
|
|
297
|
+
# builder.instance_eval do
|
|
298
|
+
# memory do
|
|
299
|
+
# directory(memory_config[:directory])
|
|
300
|
+
# adapter(memory_config[:adapter]) if memory_config[:adapter]
|
|
301
|
+
# mode(memory_config[:mode]) if memory_config[:mode]
|
|
302
|
+
# end
|
|
303
|
+
# end
|
|
304
|
+
# end
|
|
305
|
+
def translate_yaml_config(builder, agent_config)
|
|
306
|
+
# Override if plugin needs YAML configuration support
|
|
307
|
+
end
|
|
218
308
|
end
|
|
219
309
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Helper methods for working with Procs and Lambdas
|
|
5
|
+
#
|
|
6
|
+
# Provides functionality to convert regular Proc objects into Lambdas to enable
|
|
7
|
+
# safe use of return statements in DSL blocks (like input/output transformers).
|
|
8
|
+
module ProcHelpers
|
|
9
|
+
class << self
|
|
10
|
+
# Convert a Proc to a Lambda
|
|
11
|
+
#
|
|
12
|
+
# The fundamental difference between a Proc and a Lambda is in how they handle
|
|
13
|
+
# return statements. In a Proc, return exits the enclosing method (or program),
|
|
14
|
+
# while in a Lambda, return only exits the lambda itself.
|
|
15
|
+
#
|
|
16
|
+
# This method converts a Proc to a Lambda by:
|
|
17
|
+
# 1. Converting the proc to an unbound method via define_method
|
|
18
|
+
# 2. Wrapping it in a lambda that binds and calls the method
|
|
19
|
+
# 3. In the method, return exits the method (not the original scope)
|
|
20
|
+
#
|
|
21
|
+
# This allows users to write natural control flow with return statements:
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# my_proc = proc { |x| return x * 2 if x > 0; 0 }
|
|
25
|
+
# my_lambda = ProcHelpers.to_lambda(my_proc)
|
|
26
|
+
# my_lambda.call(5) # => 10 (return works safely!)
|
|
27
|
+
#
|
|
28
|
+
# @param proc [Proc] The proc to convert
|
|
29
|
+
# @return [Proc] A lambda with the same behavior but safe return semantics
|
|
30
|
+
def to_lambda(proc)
|
|
31
|
+
return proc if proc.lambda?
|
|
32
|
+
|
|
33
|
+
# Save local reference to proc so we can use it in module_exec/lambda scopes
|
|
34
|
+
source_proc = proc
|
|
35
|
+
|
|
36
|
+
# Convert proc to unbound method
|
|
37
|
+
# define_method with a block converts the block to a method, where return
|
|
38
|
+
# exits the method (not the original scope)
|
|
39
|
+
unbound_method = Module.new.module_exec do
|
|
40
|
+
instance_method(define_method(:_proc_call, &source_proc))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Return lambda which binds our unbound method to correct receiver and calls it
|
|
44
|
+
lambda do |*args, **kwargs, &block|
|
|
45
|
+
# Bind method to the original proc's receiver (the context where it was defined)
|
|
46
|
+
# This preserves access to instance variables, local variables via closure, etc.
|
|
47
|
+
receiver = source_proc.binding.eval("self")
|
|
48
|
+
unbound_method.bind(receiver).call(*args, **kwargs, &block)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|