swarm_sdk 2.2.0 → 2.3.0
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_sdk/agent/builder.rb +58 -0
- data/lib/swarm_sdk/agent/chat.rb +527 -1059
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
- 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 +12 -12
- 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 +2 -2
- data/lib/swarm_sdk/agent/definition.rb +66 -154
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
- 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 +65 -543
- 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 +18 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
- data/lib/swarm_sdk/log_collector.rb +179 -29
- data/lib/swarm_sdk/log_stream.rb +29 -0
- 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/snapshot.rb +6 -6
- data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
- data/lib/swarm_sdk/state_restorer.rb +136 -151
- data/lib/swarm_sdk/state_snapshot.rb +65 -100
- data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
- data/lib/swarm_sdk/swarm/builder.rb +44 -578
- 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/tool_configurator.rb +42 -138
- data/lib/swarm_sdk/swarm.rb +137 -679
- data/lib/swarm_sdk/tools/bash.rb +11 -3
- data/lib/swarm_sdk/tools/delegate.rb +61 -43
- 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 +11 -13
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/todo_write.rb +7 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
- 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} +3 -3
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
- data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
- data/lib/swarm_sdk.rb +33 -3
- metadata +67 -15
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
|
@@ -44,6 +44,7 @@ module SwarmSDK
|
|
|
44
44
|
# - `agent_step`: Reconstructs assistant message with tool calls
|
|
45
45
|
# - `agent_stop`: Reconstructs final assistant message
|
|
46
46
|
# - `tool_result`: Reconstructs tool result message
|
|
47
|
+
# - `delegation_result`: Reconstructs tool result message from delegation
|
|
47
48
|
class EventsToMessages
|
|
48
49
|
class << self
|
|
49
50
|
# Reconstruct messages for an agent from event stream
|
|
@@ -90,6 +91,8 @@ module SwarmSDK
|
|
|
90
91
|
reconstruct_assistant_message(event)
|
|
91
92
|
when "tool_result"
|
|
92
93
|
reconstruct_tool_result_message(event)
|
|
94
|
+
when "delegation_result"
|
|
95
|
+
reconstruct_delegation_result_message(event)
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
messages << message if message
|
|
@@ -158,6 +161,21 @@ module SwarmSDK
|
|
|
158
161
|
)
|
|
159
162
|
end
|
|
160
163
|
|
|
164
|
+
# Reconstruct tool result message from delegation_result event
|
|
165
|
+
#
|
|
166
|
+
# delegation_result events are emitted when a delegation completes,
|
|
167
|
+
# and they should be converted to tool result messages in the conversation.
|
|
168
|
+
#
|
|
169
|
+
# @param event [Hash] delegation_result event
|
|
170
|
+
# @return [RubyLLM::Message] Tool result message
|
|
171
|
+
def reconstruct_delegation_result_message(event)
|
|
172
|
+
RubyLLM::Message.new(
|
|
173
|
+
role: :tool,
|
|
174
|
+
content: event[:result].to_s,
|
|
175
|
+
tool_call_id: event[:tool_call_id],
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
161
179
|
# Parse timestamp string to Time object
|
|
162
180
|
#
|
|
163
181
|
# @param timestamp [String, nil] ISO 8601 timestamp
|
|
@@ -47,7 +47,8 @@ module SwarmSDK
|
|
|
47
47
|
# )
|
|
48
48
|
# # => Result (continue or halt based on exit code)
|
|
49
49
|
class ShellExecutor
|
|
50
|
-
|
|
50
|
+
# Backward compatibility alias - use Defaults module for new code
|
|
51
|
+
DEFAULT_TIMEOUT = Defaults::Timeouts::HOOK_SHELL_SECONDS
|
|
51
52
|
|
|
52
53
|
class << self
|
|
53
54
|
# Execute a shell command hook
|
|
@@ -1,76 +1,226 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SwarmSDK
|
|
4
|
-
# LogCollector manages subscriber callbacks for log events.
|
|
4
|
+
# LogCollector manages subscriber callbacks for log events with filtering support.
|
|
5
5
|
#
|
|
6
6
|
# This module acts as an emitter implementation that forwards events
|
|
7
7
|
# to user-registered callbacks. It's designed to be set as the LogStream
|
|
8
8
|
# emitter during swarm execution.
|
|
9
9
|
#
|
|
10
|
+
# ## Features
|
|
11
|
+
#
|
|
12
|
+
# - **Filtered Subscriptions**: Subscribe to specific agents, event types, or swarm IDs
|
|
13
|
+
# - **Unsubscribe Support**: Remove subscriptions by ID to prevent memory leaks
|
|
14
|
+
# - **Error Isolation**: One subscriber's error doesn't break others
|
|
15
|
+
# - **Thread Safety**: Fiber-local storage for multi-threaded environments
|
|
16
|
+
#
|
|
10
17
|
# ## Thread Safety for Multi-Threaded Environments (Puma, Sidekiq)
|
|
11
18
|
#
|
|
12
|
-
#
|
|
19
|
+
# Subscriptions are stored in Fiber-local storage (Fiber[:log_subscriptions]) instead
|
|
13
20
|
# of class instance variables. This ensures callbacks registered in the parent
|
|
14
21
|
# thread/fiber are accessible to child fibers created by Async reactor.
|
|
15
22
|
#
|
|
16
|
-
# Why: In Puma/Sidekiq, class instance variables (@
|
|
23
|
+
# Why: In Puma/Sidekiq, class instance variables (@subscriptions) are thread-isolated
|
|
17
24
|
# and don't properly propagate to child fibers. Using Fiber-local storage ensures
|
|
18
|
-
# events emitted from within Async blocks can reach registered
|
|
25
|
+
# events emitted from within Async blocks can reach registered subscriptions.
|
|
19
26
|
#
|
|
20
27
|
# Child fibers inherit parent fiber-local storage automatically, so events
|
|
21
28
|
# emitted from agent callbacks (on_tool_call, on_end_message, etc.) executing
|
|
22
|
-
# in child fibers can still reach the parent's registered
|
|
29
|
+
# in child fibers can still reach the parent's registered subscriptions.
|
|
23
30
|
#
|
|
24
31
|
# ## Usage
|
|
25
32
|
#
|
|
26
|
-
# #
|
|
27
|
-
# LogCollector.
|
|
28
|
-
#
|
|
29
|
-
#
|
|
33
|
+
# # Subscribe to all events
|
|
34
|
+
# sub_id = LogCollector.subscribe { |event| puts event }
|
|
35
|
+
#
|
|
36
|
+
# # Subscribe to specific agent
|
|
37
|
+
# sub_id = LogCollector.subscribe(filter: { agent: :backend }) { |event|
|
|
38
|
+
# puts "Backend: #{event}"
|
|
39
|
+
# }
|
|
30
40
|
#
|
|
31
|
-
# #
|
|
32
|
-
# LogCollector.
|
|
41
|
+
# # Subscribe to specific event types
|
|
42
|
+
# sub_id = LogCollector.subscribe(filter: { type: ["tool_call", "tool_result"] }) { |event|
|
|
43
|
+
# log_tool_activity(event)
|
|
44
|
+
# }
|
|
45
|
+
#
|
|
46
|
+
# # Subscribe with regex matching
|
|
47
|
+
# sub_id = LogCollector.subscribe(filter: { type: /^tool_/ }) { |event|
|
|
48
|
+
# track_tool_usage(event)
|
|
49
|
+
# }
|
|
50
|
+
#
|
|
51
|
+
# # Unsubscribe when done
|
|
52
|
+
# LogCollector.unsubscribe(sub_id)
|
|
33
53
|
#
|
|
34
54
|
# # After execution, reset for next use
|
|
35
55
|
# LogCollector.reset!
|
|
36
56
|
#
|
|
37
57
|
module LogCollector
|
|
58
|
+
# Subscription object with filtering capabilities
|
|
59
|
+
#
|
|
60
|
+
# Encapsulates a callback with optional filters. Filters can match
|
|
61
|
+
# against agent name, event type, swarm_id, or any event field.
|
|
62
|
+
class Subscription
|
|
63
|
+
attr_reader :id, :filter, :callback
|
|
64
|
+
|
|
65
|
+
# Initialize a subscription
|
|
66
|
+
#
|
|
67
|
+
# @param filter [Hash] Filter criteria
|
|
68
|
+
# @param callback [Proc] Block to call when event matches
|
|
69
|
+
def initialize(filter: {}, &callback)
|
|
70
|
+
@id = SecureRandom.uuid
|
|
71
|
+
@filter = normalize_filter(filter)
|
|
72
|
+
@callback = callback
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if event matches filter criteria
|
|
76
|
+
#
|
|
77
|
+
# Empty filter matches all events. Multiple filter keys are AND'd together.
|
|
78
|
+
#
|
|
79
|
+
# @param event [Hash] Event entry with :type, :agent, etc.
|
|
80
|
+
# @return [Boolean] True if event matches filter
|
|
81
|
+
def matches?(event)
|
|
82
|
+
return true if @filter.empty?
|
|
83
|
+
|
|
84
|
+
@filter.all? do |key, matcher|
|
|
85
|
+
value = event[key]
|
|
86
|
+
case matcher
|
|
87
|
+
when Array
|
|
88
|
+
# Match if value is in array (handles symbols/strings)
|
|
89
|
+
matcher.include?(value) || matcher.map(&:to_s).include?(value.to_s)
|
|
90
|
+
when Regexp
|
|
91
|
+
# Regex matching
|
|
92
|
+
value.to_s.match?(matcher)
|
|
93
|
+
when Proc
|
|
94
|
+
# Custom matcher
|
|
95
|
+
matcher.call(value)
|
|
96
|
+
else
|
|
97
|
+
# Exact match (handles symbols/strings)
|
|
98
|
+
matcher == value || matcher.to_s == value.to_s
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Normalize filter keys to symbols for consistent matching
|
|
106
|
+
#
|
|
107
|
+
# @param filter [Hash] Raw filter
|
|
108
|
+
# @return [Hash] Normalized filter with symbol keys
|
|
109
|
+
def normalize_filter(filter)
|
|
110
|
+
filter.transform_keys(&:to_sym)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
38
114
|
class << self
|
|
39
|
-
#
|
|
115
|
+
# Subscribe to log events with optional filtering
|
|
40
116
|
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
117
|
+
# Registers a callback that will receive events matching the filter criteria.
|
|
118
|
+
# Returns a subscription ID that can be used to unsubscribe later.
|
|
43
119
|
#
|
|
120
|
+
# @param filter [Hash] Filter criteria (empty = all events)
|
|
121
|
+
# - :agent [Symbol, String, Array, Regexp] Agent name(s) to observe
|
|
122
|
+
# - :type [String, Array, Regexp] Event type(s) to receive
|
|
123
|
+
# - :swarm_id [String] Specific swarm instance
|
|
124
|
+
# - Any other key matches against event fields
|
|
44
125
|
# @yield [Hash] Log event entry
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
126
|
+
# @return [String] Subscription ID for unsubscribe
|
|
127
|
+
#
|
|
128
|
+
# @example Subscribe to all events
|
|
129
|
+
# LogCollector.subscribe { |event| puts event }
|
|
130
|
+
#
|
|
131
|
+
# @example Subscribe to specific agent's tool calls
|
|
132
|
+
# sub_id = LogCollector.subscribe(
|
|
133
|
+
# filter: { agent: :backend, type: /^tool_/ }
|
|
134
|
+
# ) do |event|
|
|
135
|
+
# puts "Backend used tool: #{event[:tool]}"
|
|
136
|
+
# end
|
|
137
|
+
#
|
|
138
|
+
# @example Subscribe to multiple agents
|
|
139
|
+
# LogCollector.subscribe(
|
|
140
|
+
# filter: { agent: [:backend, :frontend], type: "agent_stop" }
|
|
141
|
+
# ) { |e| record_completion(e) }
|
|
142
|
+
def subscribe(filter: {}, &block)
|
|
143
|
+
subscription = Subscription.new(filter: filter, &block)
|
|
144
|
+
subscriptions << subscription
|
|
145
|
+
subscription.id
|
|
48
146
|
end
|
|
49
147
|
|
|
50
|
-
#
|
|
148
|
+
# Unsubscribe by ID
|
|
149
|
+
#
|
|
150
|
+
# Removes a subscription to prevent memory leaks and stop receiving events.
|
|
151
|
+
#
|
|
152
|
+
# @param subscription_id [String] ID returned from subscribe
|
|
153
|
+
# @return [Subscription, nil] Removed subscription or nil if not found
|
|
154
|
+
def unsubscribe(subscription_id)
|
|
155
|
+
index = subscriptions.find_index { |s| s.id == subscription_id }
|
|
156
|
+
return unless index
|
|
157
|
+
|
|
158
|
+
subscriptions.delete_at(index)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Clear all subscriptions
|
|
162
|
+
#
|
|
163
|
+
# Removes all subscriptions. Useful for testing or execution cleanup.
|
|
164
|
+
#
|
|
165
|
+
# @return [void]
|
|
166
|
+
def clear_subscriptions
|
|
167
|
+
subscriptions.clear
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Get current subscription count
|
|
171
|
+
#
|
|
172
|
+
# @return [Integer] Number of active subscriptions
|
|
173
|
+
def subscription_count
|
|
174
|
+
subscriptions.size
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Emit an event to all matching subscribers
|
|
51
178
|
#
|
|
52
179
|
# Automatically adds a timestamp if one doesn't exist.
|
|
53
|
-
#
|
|
180
|
+
# Errors in individual subscribers are isolated - one bad subscriber
|
|
181
|
+
# won't prevent others from receiving events.
|
|
54
182
|
#
|
|
55
183
|
# @param entry [Hash] Log event entry
|
|
56
184
|
# @return [void]
|
|
57
185
|
def emit(entry)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
186
|
+
entry_with_timestamp = ensure_timestamp(entry)
|
|
187
|
+
|
|
188
|
+
subscriptions.each do |subscription|
|
|
189
|
+
next unless subscription.matches?(entry_with_timestamp)
|
|
190
|
+
|
|
191
|
+
begin
|
|
192
|
+
subscription.callback.call(entry_with_timestamp)
|
|
193
|
+
rescue StandardError => e
|
|
194
|
+
# Error isolation - don't let one subscriber break others
|
|
195
|
+
RubyLLM.logger.error("SwarmSDK: Subscription #{subscription.id} error: #{e.message}")
|
|
196
|
+
end
|
|
66
197
|
end
|
|
67
198
|
end
|
|
68
199
|
|
|
69
|
-
# Reset the collector (clears
|
|
200
|
+
# Reset the collector (clears subscriptions for next execution)
|
|
70
201
|
#
|
|
71
202
|
# @return [void]
|
|
72
203
|
def reset!
|
|
73
|
-
Fiber[:
|
|
204
|
+
Fiber[:log_subscriptions] = []
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
# Get subscriptions from Fiber-local storage
|
|
210
|
+
#
|
|
211
|
+
# @return [Array<Subscription>] Current subscriptions
|
|
212
|
+
def subscriptions
|
|
213
|
+
Fiber[:log_subscriptions] ||= []
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Ensure event has a timestamp
|
|
217
|
+
#
|
|
218
|
+
# @param entry [Hash] Event entry
|
|
219
|
+
# @return [Hash] Entry with timestamp
|
|
220
|
+
def ensure_timestamp(entry)
|
|
221
|
+
return entry if entry.key?(:timestamp)
|
|
222
|
+
|
|
223
|
+
entry.merge(timestamp: Time.now.utc.iso8601(6))
|
|
74
224
|
end
|
|
75
225
|
end
|
|
76
226
|
end
|
data/lib/swarm_sdk/log_stream.rb
CHANGED
|
@@ -93,6 +93,35 @@ module SwarmSDK
|
|
|
93
93
|
def enabled?
|
|
94
94
|
!Fiber[:log_stream_emitter].nil?
|
|
95
95
|
end
|
|
96
|
+
|
|
97
|
+
# Emit an internal error event
|
|
98
|
+
#
|
|
99
|
+
# Provides consistent error event emission for all internal errors.
|
|
100
|
+
# These are errors that occur during execution but are handled gracefully
|
|
101
|
+
# (with fallback behavior) rather than causing failures.
|
|
102
|
+
#
|
|
103
|
+
# @param error [Exception] The caught exception
|
|
104
|
+
# @param source [String] Source module/class (e.g., "hook_triggers", "context_compactor")
|
|
105
|
+
# @param context [String] Specific operation context (e.g., "swarm_stop", "summarization")
|
|
106
|
+
# @param agent [Symbol, String, nil] Agent name if applicable
|
|
107
|
+
# @param metadata [Hash] Additional context data
|
|
108
|
+
# @return [void]
|
|
109
|
+
def emit_error(error, source:, context:, agent: nil, **metadata)
|
|
110
|
+
emit(
|
|
111
|
+
type: "internal_error",
|
|
112
|
+
source: source,
|
|
113
|
+
context: context,
|
|
114
|
+
agent: agent,
|
|
115
|
+
error_class: error.class.name,
|
|
116
|
+
error_message: error.message,
|
|
117
|
+
backtrace: error.backtrace&.first(5),
|
|
118
|
+
**metadata,
|
|
119
|
+
)
|
|
120
|
+
rescue StandardError
|
|
121
|
+
# Absolute fallback - if emit_error itself fails, don't break execution
|
|
122
|
+
# This should never happen, but we must be defensive
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
96
125
|
end
|
|
97
126
|
end
|
|
98
127
|
end
|
|
@@ -168,7 +168,7 @@ module SwarmSDK
|
|
|
168
168
|
end
|
|
169
169
|
|
|
170
170
|
# Control flow methods for transformers
|
|
171
|
-
# These return special hashes that
|
|
171
|
+
# These return special hashes that Workflow recognizes
|
|
172
172
|
|
|
173
173
|
# Skip current node's LLM execution and return content immediately
|
|
174
174
|
#
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Observer
|
|
5
|
+
# DSL for configuring observer agents
|
|
6
|
+
#
|
|
7
|
+
# Used by Swarm::Builder#observer to provide a clean DSL for defining
|
|
8
|
+
# event handlers and observer configuration options.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage
|
|
11
|
+
# observer :profiler do
|
|
12
|
+
# on :swarm_start do |event|
|
|
13
|
+
# "Analyze this prompt: #{event[:prompt]}"
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# timeout 120
|
|
17
|
+
# max_concurrent 2
|
|
18
|
+
# end
|
|
19
|
+
class Builder
|
|
20
|
+
# Initialize builder with agent name and config
|
|
21
|
+
#
|
|
22
|
+
# @param agent_name [Symbol] Name of the observer agent
|
|
23
|
+
# @param config [Observer::Config] Configuration object to populate
|
|
24
|
+
def initialize(agent_name, config)
|
|
25
|
+
@agent_name = agent_name
|
|
26
|
+
@config = config
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Register an event handler
|
|
30
|
+
#
|
|
31
|
+
# The block receives the event hash and should return:
|
|
32
|
+
# - A prompt string to trigger the observer agent
|
|
33
|
+
# - nil to skip execution for this event
|
|
34
|
+
#
|
|
35
|
+
# @param event_type [Symbol] Type of event to handle (e.g., :swarm_start, :tool_call)
|
|
36
|
+
# @yield [Hash] Event hash
|
|
37
|
+
# @yieldreturn [String, nil] Prompt or nil to skip
|
|
38
|
+
# @return [void]
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# on :tool_call do |event|
|
|
42
|
+
# next unless event[:tool_name] == "Bash"
|
|
43
|
+
# "Check this command: #{event[:arguments][:command]}"
|
|
44
|
+
# end
|
|
45
|
+
def on(event_type, &block)
|
|
46
|
+
@config.add_handler(event_type, &block)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Set maximum concurrent executions for this observer
|
|
50
|
+
#
|
|
51
|
+
# Limits how many instances of this observer agent can run simultaneously.
|
|
52
|
+
# Useful for resource-intensive observers.
|
|
53
|
+
#
|
|
54
|
+
# @param n [Integer] Maximum concurrent executions
|
|
55
|
+
# @return [void]
|
|
56
|
+
def max_concurrent(n)
|
|
57
|
+
@config.options[:max_concurrent] = n
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Set timeout for observer execution
|
|
61
|
+
#
|
|
62
|
+
# Observer tasks will be cancelled after this duration.
|
|
63
|
+
#
|
|
64
|
+
# @param seconds [Integer] Timeout in seconds (default: 60)
|
|
65
|
+
# @return [void]
|
|
66
|
+
def timeout(seconds)
|
|
67
|
+
@config.options[:timeout] = seconds
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Wait for observer to complete before swarm execution ends
|
|
71
|
+
#
|
|
72
|
+
# By default, observers are fire-and-forget. This option causes
|
|
73
|
+
# the main execution to wait for this observer to complete.
|
|
74
|
+
#
|
|
75
|
+
# @return [void]
|
|
76
|
+
def wait_for_completion!
|
|
77
|
+
@config.options[:fire_and_forget] = false
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -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
|