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
|
@@ -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
|
data/lib/swarm_sdk/snapshot.rb
CHANGED
|
@@ -102,7 +102,7 @@ module SwarmSDK
|
|
|
102
102
|
@data[:version] || @data["version"]
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
-
# Get snapshot type (swarm or
|
|
105
|
+
# Get snapshot type (swarm or workflow)
|
|
106
106
|
#
|
|
107
107
|
# @return [String] Snapshot type
|
|
108
108
|
def type
|
|
@@ -139,18 +139,18 @@ module SwarmSDK
|
|
|
139
139
|
delegations ? delegations.keys.map(&:to_s) : []
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
-
# Check if snapshot is for a swarm (vs
|
|
142
|
+
# Check if snapshot is for a swarm (vs workflow)
|
|
143
143
|
#
|
|
144
144
|
# @return [Boolean] true if swarm snapshot
|
|
145
145
|
def swarm?
|
|
146
146
|
type == "swarm"
|
|
147
147
|
end
|
|
148
148
|
|
|
149
|
-
# Check if snapshot is for a
|
|
149
|
+
# Check if snapshot is for a workflow
|
|
150
150
|
#
|
|
151
|
-
# @return [Boolean] true if
|
|
152
|
-
def
|
|
153
|
-
type == "
|
|
151
|
+
# @return [Boolean] true if workflow snapshot
|
|
152
|
+
def workflow?
|
|
153
|
+
type == "workflow"
|
|
154
154
|
end
|
|
155
155
|
end
|
|
156
156
|
end
|
|
@@ -69,16 +69,17 @@ module SwarmSDK
|
|
|
69
69
|
# @return [Hash] StateSnapshot hash
|
|
70
70
|
def reconstruct
|
|
71
71
|
{
|
|
72
|
-
version: "1.0
|
|
72
|
+
version: "2.1.0",
|
|
73
73
|
type: "swarm",
|
|
74
74
|
snapshot_at: @events.last&.fetch(:timestamp, Time.now.utc.iso8601),
|
|
75
75
|
swarm_sdk_version: SwarmSDK::VERSION,
|
|
76
|
-
|
|
76
|
+
metadata: reconstruct_swarm_metadata,
|
|
77
77
|
agents: reconstruct_all_agents,
|
|
78
78
|
delegation_instances: reconstruct_all_delegations,
|
|
79
79
|
scratchpad: reconstruct_scratchpad,
|
|
80
80
|
read_tracking: reconstruct_read_tracking,
|
|
81
81
|
memory_read_tracking: reconstruct_memory_read_tracking,
|
|
82
|
+
plugin_states: reconstruct_plugin_states,
|
|
82
83
|
}
|
|
83
84
|
end
|
|
84
85
|
|
|
@@ -363,6 +364,16 @@ module SwarmSDK
|
|
|
363
364
|
tracking
|
|
364
365
|
end
|
|
365
366
|
|
|
367
|
+
# Reconstruct plugin states
|
|
368
|
+
#
|
|
369
|
+
# Plugin states cannot be fully reconstructed from events alone as they
|
|
370
|
+
# contain internal plugin data. Returns empty hash for compatibility.
|
|
371
|
+
#
|
|
372
|
+
# @return [Hash] Empty plugin states hash
|
|
373
|
+
def reconstruct_plugin_states
|
|
374
|
+
{}
|
|
375
|
+
end
|
|
376
|
+
|
|
366
377
|
# Parse timestamp string to Time object
|
|
367
378
|
#
|
|
368
379
|
# @param timestamp [String, nil] ISO 8601 timestamp
|