swarm_memory 2.1.3 → 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 +2 -15
- data/lib/claude_swarm/mcp_generator.rb +1 -0
- 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/run.rb +2 -2
- data/lib/swarm_cli/config_loader.rb +11 -11
- 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/filesystem_adapter.rb +11 -34
- data/lib/swarm_memory/integration/sdk_plugin.rb +87 -7
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +1 -1
- 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 +37 -14
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
class Swarm
|
|
5
|
+
# Handles swarm execution orchestration
|
|
6
|
+
#
|
|
7
|
+
# Extracted from Swarm#execute to reduce complexity and eliminate code duplication.
|
|
8
|
+
# The core execution loop, error handling, and cleanup logic are unified here.
|
|
9
|
+
class Executor
|
|
10
|
+
def initialize(swarm)
|
|
11
|
+
@swarm = swarm
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Execute the swarm with a prompt
|
|
15
|
+
#
|
|
16
|
+
# @param prompt [String] User prompt
|
|
17
|
+
# @param wait [Boolean] Block until completion (true) or return task (false)
|
|
18
|
+
# @param logs [Array] Log collection array
|
|
19
|
+
# @param has_logging [Boolean] Whether logging is enabled
|
|
20
|
+
# @param original_fiber_storage [Hash] Original Fiber storage values to restore
|
|
21
|
+
# @return [Async::Task] The execution task
|
|
22
|
+
def run(prompt, wait:, logs:, has_logging:, original_fiber_storage:)
|
|
23
|
+
@original_fiber_storage = original_fiber_storage
|
|
24
|
+
if wait
|
|
25
|
+
run_blocking(prompt, logs: logs, has_logging: has_logging)
|
|
26
|
+
else
|
|
27
|
+
run_async(prompt, logs: logs, has_logging: has_logging)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Blocking execution using Sync
|
|
34
|
+
def run_blocking(prompt, logs:, has_logging:)
|
|
35
|
+
Sync do |task|
|
|
36
|
+
execute_in_task(prompt, logs: logs, has_logging: has_logging) do |lead, current_prompt|
|
|
37
|
+
task.async(finished: false) { lead.ask(current_prompt) }.wait
|
|
38
|
+
end
|
|
39
|
+
ensure
|
|
40
|
+
# Always wait for observer tasks, even if main execution raises
|
|
41
|
+
# This is INSIDE Sync block, so async tasks can still complete
|
|
42
|
+
@swarm.wait_for_observers
|
|
43
|
+
end
|
|
44
|
+
ensure
|
|
45
|
+
# Restore original fiber storage (preserves parent context for nested swarms)
|
|
46
|
+
restore_fiber_storage
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Non-blocking execution using parent async task
|
|
50
|
+
def run_async(prompt, logs:, has_logging:)
|
|
51
|
+
parent = Async::Task.current
|
|
52
|
+
raise ConfigurationError, "wait: false requires an async context. Use Sync { swarm.execute(..., wait: false) }" unless parent
|
|
53
|
+
|
|
54
|
+
parent.async(finished: false) do
|
|
55
|
+
execute_in_task(prompt, logs: logs, has_logging: has_logging) do |lead, current_prompt|
|
|
56
|
+
Async(finished: false) { lead.ask(current_prompt) }.wait
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Core execution logic (unified, no duplication)
|
|
62
|
+
#
|
|
63
|
+
# @param prompt [String] Initial prompt
|
|
64
|
+
# @param logs [Array] Log collection
|
|
65
|
+
# @param has_logging [Boolean] Whether logging is enabled
|
|
66
|
+
# @yield [lead, current_prompt] Block to execute LLM call
|
|
67
|
+
# @return [Result] Execution result
|
|
68
|
+
def execute_in_task(prompt, logs:, has_logging:, &block)
|
|
69
|
+
start_time = Time.now
|
|
70
|
+
result = nil
|
|
71
|
+
swarm_stop_triggered = false
|
|
72
|
+
current_prompt = prompt
|
|
73
|
+
|
|
74
|
+
begin
|
|
75
|
+
# Notify plugins that swarm is starting
|
|
76
|
+
PluginRegistry.emit_event(:on_swarm_started, swarm: @swarm)
|
|
77
|
+
|
|
78
|
+
result = execution_loop(current_prompt, logs, start_time, &block)
|
|
79
|
+
swarm_stop_triggered = true
|
|
80
|
+
rescue ConfigurationError, AgentNotFoundError
|
|
81
|
+
# Re-raise configuration errors - these should be fixed, not caught
|
|
82
|
+
raise
|
|
83
|
+
rescue TypeError => e
|
|
84
|
+
result = handle_type_error(e, logs, start_time)
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
result = handle_standard_error(e, logs, start_time)
|
|
87
|
+
ensure
|
|
88
|
+
# Notify plugins that swarm is stopping (called even on error)
|
|
89
|
+
PluginRegistry.emit_event(:on_swarm_stopped, swarm: @swarm)
|
|
90
|
+
|
|
91
|
+
cleanup_after_execution(result, start_time, logs, swarm_stop_triggered, has_logging)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
result
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Main execution loop with reprompting support
|
|
98
|
+
def execution_loop(initial_prompt, logs, start_time)
|
|
99
|
+
current_prompt = initial_prompt
|
|
100
|
+
|
|
101
|
+
loop do
|
|
102
|
+
lead = @swarm.agents[@swarm.lead_agent]
|
|
103
|
+
response = yield(lead, current_prompt)
|
|
104
|
+
|
|
105
|
+
# Check if swarm was finished by a hook (finish_swarm)
|
|
106
|
+
if response.is_a?(Hash) && response[:__finish_swarm__]
|
|
107
|
+
result = build_result(response[:message], logs, start_time)
|
|
108
|
+
@swarm.trigger_swarm_stop(result)
|
|
109
|
+
return result
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
result = build_result(response.content, logs, start_time)
|
|
113
|
+
|
|
114
|
+
# Trigger swarm_stop hooks (for reprompt check and event emission)
|
|
115
|
+
hook_result = @swarm.trigger_swarm_stop(result)
|
|
116
|
+
|
|
117
|
+
# Check if hook requests reprompting
|
|
118
|
+
if hook_result&.reprompt?
|
|
119
|
+
current_prompt = hook_result.value
|
|
120
|
+
# Continue loop with new prompt
|
|
121
|
+
else
|
|
122
|
+
# Exit loop - execution complete
|
|
123
|
+
return result
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Build a Result object
|
|
129
|
+
def build_result(content, logs, start_time)
|
|
130
|
+
Result.new(
|
|
131
|
+
content: content,
|
|
132
|
+
agent: @swarm.lead_agent.to_s,
|
|
133
|
+
logs: logs,
|
|
134
|
+
duration: Time.now - start_time,
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Handle TypeError (e.g., "String does not have #dig method")
|
|
139
|
+
def handle_type_error(error, logs, start_time)
|
|
140
|
+
if error.message.include?("does not have #dig method")
|
|
141
|
+
agent_definition = @swarm.agent_definitions[@swarm.lead_agent]
|
|
142
|
+
error_msg = if agent_definition.base_url
|
|
143
|
+
"LLM API request failed: The proxy/server at '#{agent_definition.base_url}' returned an invalid response. " \
|
|
144
|
+
"This usually means the proxy is unreachable, requires authentication, or returned an error in non-JSON format. " \
|
|
145
|
+
"Original error: #{error.message}"
|
|
146
|
+
else
|
|
147
|
+
"LLM API request failed with unexpected response format. Original error: #{error.message}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
Result.new(
|
|
151
|
+
content: nil,
|
|
152
|
+
agent: @swarm.lead_agent.to_s,
|
|
153
|
+
error: LLMError.new(error_msg),
|
|
154
|
+
logs: logs,
|
|
155
|
+
duration: Time.now - start_time,
|
|
156
|
+
)
|
|
157
|
+
else
|
|
158
|
+
Result.new(
|
|
159
|
+
content: nil,
|
|
160
|
+
agent: @swarm.lead_agent.to_s,
|
|
161
|
+
error: error,
|
|
162
|
+
logs: logs,
|
|
163
|
+
duration: Time.now - start_time,
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Handle StandardError
|
|
169
|
+
def handle_standard_error(error, logs, start_time)
|
|
170
|
+
Result.new(
|
|
171
|
+
content: nil,
|
|
172
|
+
agent: @swarm.lead_agent&.to_s || "unknown",
|
|
173
|
+
error: error,
|
|
174
|
+
logs: logs,
|
|
175
|
+
duration: Time.now - start_time,
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Cleanup after execution (ensure block logic)
|
|
180
|
+
def cleanup_after_execution(result, start_time, logs, swarm_stop_triggered, has_logging)
|
|
181
|
+
# Trigger swarm_stop if not already triggered (handles error cases)
|
|
182
|
+
unless swarm_stop_triggered
|
|
183
|
+
@swarm.trigger_swarm_stop_final(result, start_time, logs)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Cleanup MCP clients after execution
|
|
187
|
+
@swarm.cleanup
|
|
188
|
+
|
|
189
|
+
# Cleanup observer subscriptions (matches MCP cleanup pattern)
|
|
190
|
+
@swarm.cleanup_observers
|
|
191
|
+
|
|
192
|
+
# Restore original Fiber storage (preserves parent context for nested swarms)
|
|
193
|
+
restore_fiber_storage
|
|
194
|
+
|
|
195
|
+
# Reset logging state for next execution if we set it up
|
|
196
|
+
reset_logging if has_logging
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Restore Fiber-local storage to original values (preserves parent context)
|
|
200
|
+
def restore_fiber_storage
|
|
201
|
+
Fiber[:execution_id] = @original_fiber_storage[:execution_id]
|
|
202
|
+
Fiber[:swarm_id] = @original_fiber_storage[:swarm_id]
|
|
203
|
+
Fiber[:parent_swarm_id] = @original_fiber_storage[:parent_swarm_id]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Reset logging state
|
|
207
|
+
def reset_logging
|
|
208
|
+
LogCollector.reset!
|
|
209
|
+
LogStream.reset!
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
class Swarm
|
|
5
|
+
# Hook triggering methods for swarm lifecycle events
|
|
6
|
+
#
|
|
7
|
+
# Extracted from Swarm to reduce class size and centralize hook execution logic.
|
|
8
|
+
# These methods build contexts and execute hooks via the hook registry.
|
|
9
|
+
module HookTriggers
|
|
10
|
+
# Add a default callback for an event
|
|
11
|
+
#
|
|
12
|
+
# @param event [Symbol] Event type (:pre_tool_use, :post_tool_use, etc.)
|
|
13
|
+
# @param matcher [Hash, nil] Optional matcher to filter events
|
|
14
|
+
# @param priority [Integer] Callback priority (higher = later)
|
|
15
|
+
# @param block [Proc] Hook implementation
|
|
16
|
+
# @return [self]
|
|
17
|
+
def add_default_callback(event, matcher: nil, priority: 0, &block)
|
|
18
|
+
@hook_registry.add_default(event, matcher: matcher, priority: priority, &block)
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Trigger swarm_stop hooks and check for reprompt
|
|
23
|
+
#
|
|
24
|
+
# @param result [Result] The execution result
|
|
25
|
+
# @return [Hooks::Result, nil] Hook result (reprompt action if applicable)
|
|
26
|
+
def trigger_swarm_stop(result)
|
|
27
|
+
context = build_swarm_stop_context(result)
|
|
28
|
+
executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
|
|
29
|
+
executor.execute_safe(event: :swarm_stop, context: context, callbacks: [])
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
LogStream.emit_error(e, source: "hook_triggers", context: "swarm_stop", agent: @lead_agent)
|
|
32
|
+
RubyLLM.logger.debug("SwarmSDK: Error in swarm_stop hook: #{e.message}")
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Trigger swarm_stop for final event emission (called in ensure block)
|
|
37
|
+
#
|
|
38
|
+
# @param result [Result, nil] Execution result
|
|
39
|
+
# @param start_time [Time] Execution start time
|
|
40
|
+
# @param logs [Array] Collected logs
|
|
41
|
+
# @return [void]
|
|
42
|
+
def trigger_swarm_stop_final(result, start_time, logs)
|
|
43
|
+
result ||= Result.new(
|
|
44
|
+
content: nil,
|
|
45
|
+
agent: @lead_agent&.to_s || "unknown",
|
|
46
|
+
logs: logs,
|
|
47
|
+
duration: Time.now - start_time,
|
|
48
|
+
error: StandardError.new("Unknown error"),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
context = build_swarm_stop_context(result)
|
|
52
|
+
executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
|
|
53
|
+
executor.execute_safe(event: :swarm_stop, context: context, callbacks: [])
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
LogStream.emit_error(e, source: "hook_triggers", context: "swarm_stop_final", agent: @lead_agent)
|
|
56
|
+
RubyLLM.logger.debug("SwarmSDK: Error in swarm_stop final emission: #{e.message}")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Build swarm_stop context (DRY - used by both trigger methods)
|
|
62
|
+
#
|
|
63
|
+
# @param result [Result] Execution result
|
|
64
|
+
# @return [Hooks::Context] Hook context for swarm_stop event
|
|
65
|
+
def build_swarm_stop_context(result)
|
|
66
|
+
Hooks::Context.new(
|
|
67
|
+
event: :swarm_stop,
|
|
68
|
+
agent_name: @lead_agent.to_s,
|
|
69
|
+
swarm: self,
|
|
70
|
+
metadata: {
|
|
71
|
+
swarm_name: @name,
|
|
72
|
+
lead_agent: @lead_agent,
|
|
73
|
+
last_agent: result.agent,
|
|
74
|
+
content: result.content,
|
|
75
|
+
success: result.success?,
|
|
76
|
+
duration: result.duration,
|
|
77
|
+
total_cost: result.total_cost,
|
|
78
|
+
total_tokens: result.total_tokens,
|
|
79
|
+
agents_involved: result.agents_involved,
|
|
80
|
+
result: result,
|
|
81
|
+
timestamp: Time.now.utc.iso8601,
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Trigger swarm_start hooks when swarm execution begins
|
|
87
|
+
#
|
|
88
|
+
# @param prompt [String] The user's task prompt
|
|
89
|
+
# @return [Hooks::Result, nil] Result with stdout to append (if exit 0) or nil
|
|
90
|
+
# @raise [Hooks::Error] If hook halts execution
|
|
91
|
+
def trigger_swarm_start(prompt)
|
|
92
|
+
context = Hooks::Context.new(
|
|
93
|
+
event: :swarm_start,
|
|
94
|
+
agent_name: @lead_agent.to_s,
|
|
95
|
+
swarm: self,
|
|
96
|
+
metadata: {
|
|
97
|
+
swarm_name: @name,
|
|
98
|
+
lead_agent: @lead_agent,
|
|
99
|
+
prompt: prompt,
|
|
100
|
+
timestamp: Time.now.utc.iso8601,
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
|
|
105
|
+
result = executor.execute_safe(event: :swarm_start, context: context, callbacks: [])
|
|
106
|
+
|
|
107
|
+
# Halt execution if hook requests it
|
|
108
|
+
raise Hooks::Error, "Swarm start halted by hook: #{result.value}" if result.halt?
|
|
109
|
+
|
|
110
|
+
# Return result so caller can check for replace (stdout injection)
|
|
111
|
+
result
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
LogStream.emit_error(e, source: "hook_triggers", context: "swarm_start", agent: @lead_agent)
|
|
114
|
+
RubyLLM.logger.debug("SwarmSDK: Error in swarm_start hook: #{e.message}")
|
|
115
|
+
raise
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Trigger first_message hooks when first user message is sent
|
|
119
|
+
#
|
|
120
|
+
# @param prompt [String] The first user message
|
|
121
|
+
# @return [void]
|
|
122
|
+
# @raise [Hooks::Error] If hook halts execution
|
|
123
|
+
def trigger_first_message(prompt)
|
|
124
|
+
return if @hook_registry.get_defaults(:first_message).empty?
|
|
125
|
+
|
|
126
|
+
context = Hooks::Context.new(
|
|
127
|
+
event: :first_message,
|
|
128
|
+
agent_name: @lead_agent.to_s,
|
|
129
|
+
swarm: self,
|
|
130
|
+
metadata: {
|
|
131
|
+
swarm_name: @name,
|
|
132
|
+
lead_agent: @lead_agent,
|
|
133
|
+
prompt: prompt,
|
|
134
|
+
timestamp: Time.now.utc.iso8601,
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
|
|
139
|
+
result = executor.execute_safe(event: :first_message, context: context, callbacks: [])
|
|
140
|
+
|
|
141
|
+
# Halt execution if hook requests it
|
|
142
|
+
raise Hooks::Error, "First message halted by hook: #{result.value}" if result.halt?
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
LogStream.emit_error(e, source: "hook_triggers", context: "first_message", agent: @lead_agent)
|
|
145
|
+
RubyLLM.logger.debug("SwarmSDK: Error in first_message hook: #{e.message}")
|
|
146
|
+
raise
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
class Swarm
|
|
5
|
+
# Logging callbacks for swarm events
|
|
6
|
+
#
|
|
7
|
+
# Extracted from Swarm to reduce class size and eliminate repetitive callback patterns.
|
|
8
|
+
# These callbacks emit structured log events to LogStream for monitoring and debugging.
|
|
9
|
+
module LoggingCallbacks
|
|
10
|
+
# Register default logging callbacks for all swarm events
|
|
11
|
+
#
|
|
12
|
+
# Sets up low-priority callbacks that emit structured events to LogStream.
|
|
13
|
+
# These callbacks only fire when LogStream.emitter is set (logging enabled).
|
|
14
|
+
def register_default_logging_callbacks
|
|
15
|
+
register_swarm_lifecycle_callbacks
|
|
16
|
+
register_agent_lifecycle_callbacks
|
|
17
|
+
register_tool_execution_callbacks
|
|
18
|
+
register_context_warning_callback
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Setup logging infrastructure for an execution
|
|
22
|
+
#
|
|
23
|
+
# @param logs [Array] Log collection array
|
|
24
|
+
# @yield [entry] Block called for each log entry
|
|
25
|
+
def setup_logging(logs)
|
|
26
|
+
# Force fresh subscription array for this execution
|
|
27
|
+
Fiber[:log_subscriptions] = []
|
|
28
|
+
|
|
29
|
+
# Subscribe to collect logs and forward to user's block
|
|
30
|
+
LogCollector.subscribe do |entry|
|
|
31
|
+
logs << entry
|
|
32
|
+
yield(entry) if block_given?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Set LogStream to use LogCollector as emitter
|
|
36
|
+
LogStream.emitter = LogCollector
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Emit agent_start events if agents were initialized before logging was set up
|
|
40
|
+
#
|
|
41
|
+
# When agents are initialized BEFORE logging (e.g., via restore()),
|
|
42
|
+
# we need to retroactively set up logging callbacks and emit agent_start events.
|
|
43
|
+
def emit_retroactive_agent_start_events
|
|
44
|
+
return if !@agents_initialized || @agent_start_events_emitted
|
|
45
|
+
|
|
46
|
+
# Setup logging callbacks for all agents (they were skipped during initialization)
|
|
47
|
+
setup_logging_for_all_agents
|
|
48
|
+
|
|
49
|
+
# Emit agent_start events now that logging is ready
|
|
50
|
+
emit_agent_start_events
|
|
51
|
+
@agent_start_events_emitted = true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Setup logging callbacks for all initialized agents
|
|
55
|
+
#
|
|
56
|
+
# Called after restore() when logging is enabled. Sets up logging callbacks
|
|
57
|
+
# for each agent so that subsequent events are captured.
|
|
58
|
+
def setup_logging_for_all_agents
|
|
59
|
+
# Setup for PRIMARY agents
|
|
60
|
+
@agents.each_value do |chat|
|
|
61
|
+
chat.setup_logging if chat.respond_to?(:setup_logging)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Setup for DELEGATION instances
|
|
65
|
+
@delegation_instances.each_value do |chat|
|
|
66
|
+
chat.setup_logging if chat.respond_to?(:setup_logging)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Emit agent_start events for all initialized agents
|
|
71
|
+
#
|
|
72
|
+
# Called retroactively when agents were initialized before logging was enabled.
|
|
73
|
+
# Emits agent_start events so log stream captures complete agent lifecycle.
|
|
74
|
+
def emit_agent_start_events
|
|
75
|
+
return unless LogStream.emitter
|
|
76
|
+
|
|
77
|
+
# Emit for PRIMARY agents
|
|
78
|
+
@agents.each do |agent_name, chat|
|
|
79
|
+
emit_agent_start_for(agent_name, chat, is_delegation: false)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Emit for DELEGATION instances
|
|
83
|
+
@delegation_instances.each do |instance_name, chat|
|
|
84
|
+
base_name = extract_base_name(instance_name)
|
|
85
|
+
emit_agent_start_for(instance_name.to_sym, chat, is_delegation: true, base_name: base_name)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Mark as emitted to prevent duplicate emissions
|
|
89
|
+
@agent_start_events_emitted = true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Emit a single agent_start event
|
|
93
|
+
#
|
|
94
|
+
# @param agent_name [Symbol] Agent name (or instance name for delegations)
|
|
95
|
+
# @param chat [Agent::Chat] Agent chat instance
|
|
96
|
+
# @param is_delegation [Boolean] Whether this is a delegation instance
|
|
97
|
+
# @param base_name [String, nil] Base agent name for delegations
|
|
98
|
+
def emit_agent_start_for(agent_name, chat, is_delegation:, base_name: nil)
|
|
99
|
+
base_name ||= agent_name
|
|
100
|
+
agent_def = @agent_definitions[base_name]
|
|
101
|
+
|
|
102
|
+
# Build plugin storage info using base name
|
|
103
|
+
plugin_storage_info = {}
|
|
104
|
+
@plugin_storages.each do |plugin_name, agent_storages|
|
|
105
|
+
next unless agent_storages.key?(base_name)
|
|
106
|
+
|
|
107
|
+
plugin_storage_info[plugin_name] = {
|
|
108
|
+
enabled: true,
|
|
109
|
+
config: agent_def.respond_to?(plugin_name) ? extract_plugin_config_info(agent_def.public_send(plugin_name)) : nil,
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
LogStream.emit(
|
|
114
|
+
type: "agent_start",
|
|
115
|
+
agent: agent_name,
|
|
116
|
+
swarm_id: @swarm_id,
|
|
117
|
+
parent_swarm_id: @parent_swarm_id,
|
|
118
|
+
swarm_name: @name,
|
|
119
|
+
model: agent_def.model,
|
|
120
|
+
provider: agent_def.provider || "openai",
|
|
121
|
+
directory: agent_def.directory,
|
|
122
|
+
system_prompt: agent_def.system_prompt,
|
|
123
|
+
tools: chat.tool_names,
|
|
124
|
+
delegates_to: agent_def.delegates_to,
|
|
125
|
+
plugin_storages: plugin_storage_info,
|
|
126
|
+
is_delegation_instance: is_delegation,
|
|
127
|
+
base_agent: (base_name if is_delegation),
|
|
128
|
+
timestamp: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Register swarm lifecycle callbacks (swarm_start, swarm_stop)
|
|
135
|
+
def register_swarm_lifecycle_callbacks
|
|
136
|
+
add_default_callback(:swarm_start, priority: -100) do |context|
|
|
137
|
+
emit_swarm_start_event(context)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
add_default_callback(:swarm_stop, priority: -100) do |context|
|
|
141
|
+
emit_swarm_stop_event(context)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Register agent lifecycle callbacks (user_prompt, agent_step, agent_stop)
|
|
146
|
+
def register_agent_lifecycle_callbacks
|
|
147
|
+
add_default_callback(:user_prompt, priority: -100) do |context|
|
|
148
|
+
emit_user_prompt_event(context)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
add_default_callback(:agent_step, priority: -100) do |context|
|
|
152
|
+
emit_agent_step_event(context)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
add_default_callback(:agent_stop, priority: -100) do |context|
|
|
156
|
+
emit_agent_stop_event(context)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Register tool execution callbacks (pre_tool_use, post_tool_use)
|
|
161
|
+
def register_tool_execution_callbacks
|
|
162
|
+
add_default_callback(:pre_tool_use, priority: -100) do |context|
|
|
163
|
+
emit_tool_call_event(context)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
add_default_callback(:post_tool_use, priority: -100) do |context|
|
|
167
|
+
emit_tool_result_event(context)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Register context warning callback
|
|
172
|
+
def register_context_warning_callback
|
|
173
|
+
add_default_callback(:context_warning, priority: -100) do |context|
|
|
174
|
+
emit_context_warning_event(context)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Emit swarm_start event
|
|
179
|
+
def emit_swarm_start_event(context)
|
|
180
|
+
return unless LogStream.emitter
|
|
181
|
+
|
|
182
|
+
LogStream.emit(
|
|
183
|
+
type: "swarm_start",
|
|
184
|
+
agent: context.metadata[:lead_agent],
|
|
185
|
+
swarm_id: @swarm_id,
|
|
186
|
+
parent_swarm_id: @parent_swarm_id,
|
|
187
|
+
swarm_name: context.metadata[:swarm_name],
|
|
188
|
+
lead_agent: context.metadata[:lead_agent],
|
|
189
|
+
prompt: context.metadata[:prompt],
|
|
190
|
+
timestamp: context.metadata[:timestamp],
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Emit swarm_stop event
|
|
195
|
+
def emit_swarm_stop_event(context)
|
|
196
|
+
return unless LogStream.emitter
|
|
197
|
+
|
|
198
|
+
LogStream.emit(
|
|
199
|
+
type: "swarm_stop",
|
|
200
|
+
swarm_id: @swarm_id,
|
|
201
|
+
parent_swarm_id: @parent_swarm_id,
|
|
202
|
+
swarm_name: context.metadata[:swarm_name],
|
|
203
|
+
lead_agent: context.metadata[:lead_agent],
|
|
204
|
+
last_agent: context.metadata[:last_agent],
|
|
205
|
+
content: context.metadata[:content],
|
|
206
|
+
success: context.metadata[:success],
|
|
207
|
+
duration: context.metadata[:duration],
|
|
208
|
+
total_cost: context.metadata[:total_cost],
|
|
209
|
+
total_tokens: context.metadata[:total_tokens],
|
|
210
|
+
agents_involved: context.metadata[:agents_involved],
|
|
211
|
+
timestamp: context.metadata[:timestamp],
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Emit user_prompt event
|
|
216
|
+
def emit_user_prompt_event(context)
|
|
217
|
+
return unless LogStream.emitter
|
|
218
|
+
|
|
219
|
+
LogStream.emit(
|
|
220
|
+
type: "user_prompt",
|
|
221
|
+
agent: context.agent_name,
|
|
222
|
+
swarm_id: @swarm_id,
|
|
223
|
+
parent_swarm_id: @parent_swarm_id,
|
|
224
|
+
model: context.metadata[:model] || "unknown",
|
|
225
|
+
provider: context.metadata[:provider] || "unknown",
|
|
226
|
+
message_count: context.metadata[:message_count] || 0,
|
|
227
|
+
tools: context.metadata[:tools] || [],
|
|
228
|
+
delegates_to: context.metadata[:delegates_to] || [],
|
|
229
|
+
source: context.metadata[:source] || "user",
|
|
230
|
+
metadata: context.metadata,
|
|
231
|
+
)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Emit agent_step event (intermediate response with tool calls)
|
|
235
|
+
def emit_agent_step_event(context)
|
|
236
|
+
return unless LogStream.emitter
|
|
237
|
+
|
|
238
|
+
metadata_without_duplicates = context.metadata.except(
|
|
239
|
+
:model, :content, :tool_calls, :finish_reason, :usage, :tool_executions
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
LogStream.emit(
|
|
243
|
+
type: "agent_step",
|
|
244
|
+
agent: context.agent_name,
|
|
245
|
+
swarm_id: @swarm_id,
|
|
246
|
+
parent_swarm_id: @parent_swarm_id,
|
|
247
|
+
model: context.metadata[:model],
|
|
248
|
+
content: context.metadata[:content],
|
|
249
|
+
tool_calls: context.metadata[:tool_calls],
|
|
250
|
+
finish_reason: context.metadata[:finish_reason],
|
|
251
|
+
usage: context.metadata[:usage],
|
|
252
|
+
tool_executions: context.metadata[:tool_executions],
|
|
253
|
+
metadata: metadata_without_duplicates,
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Emit agent_stop event (final response)
|
|
258
|
+
def emit_agent_stop_event(context)
|
|
259
|
+
return unless LogStream.emitter
|
|
260
|
+
|
|
261
|
+
metadata_without_duplicates = context.metadata.except(
|
|
262
|
+
:model, :content, :tool_calls, :finish_reason, :usage, :tool_executions
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
LogStream.emit(
|
|
266
|
+
type: "agent_stop",
|
|
267
|
+
agent: context.agent_name,
|
|
268
|
+
swarm_id: @swarm_id,
|
|
269
|
+
parent_swarm_id: @parent_swarm_id,
|
|
270
|
+
model: context.metadata[:model],
|
|
271
|
+
content: context.metadata[:content],
|
|
272
|
+
tool_calls: context.metadata[:tool_calls],
|
|
273
|
+
finish_reason: context.metadata[:finish_reason],
|
|
274
|
+
usage: context.metadata[:usage],
|
|
275
|
+
tool_executions: context.metadata[:tool_executions],
|
|
276
|
+
metadata: metadata_without_duplicates,
|
|
277
|
+
)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Emit tool_call event (pre_tool_use)
|
|
281
|
+
def emit_tool_call_event(context)
|
|
282
|
+
return unless LogStream.emitter
|
|
283
|
+
|
|
284
|
+
LogStream.emit(
|
|
285
|
+
type: "tool_call",
|
|
286
|
+
agent: context.agent_name,
|
|
287
|
+
swarm_id: @swarm_id,
|
|
288
|
+
parent_swarm_id: @parent_swarm_id,
|
|
289
|
+
tool_call_id: context.tool_call.id,
|
|
290
|
+
tool: context.tool_call.name,
|
|
291
|
+
arguments: context.tool_call.parameters,
|
|
292
|
+
metadata: context.metadata,
|
|
293
|
+
)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Emit tool_result event (post_tool_use)
|
|
297
|
+
def emit_tool_result_event(context)
|
|
298
|
+
return unless LogStream.emitter
|
|
299
|
+
|
|
300
|
+
LogStream.emit(
|
|
301
|
+
type: "tool_result",
|
|
302
|
+
agent: context.agent_name,
|
|
303
|
+
swarm_id: @swarm_id,
|
|
304
|
+
parent_swarm_id: @parent_swarm_id,
|
|
305
|
+
tool_call_id: context.tool_result.tool_call_id,
|
|
306
|
+
tool: context.tool_result.tool_name,
|
|
307
|
+
result: context.tool_result.content,
|
|
308
|
+
metadata: context.metadata,
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Emit context_limit_warning event
|
|
313
|
+
def emit_context_warning_event(context)
|
|
314
|
+
return unless LogStream.emitter
|
|
315
|
+
|
|
316
|
+
LogStream.emit(
|
|
317
|
+
type: "context_limit_warning",
|
|
318
|
+
agent: context.agent_name,
|
|
319
|
+
swarm_id: @swarm_id,
|
|
320
|
+
parent_swarm_id: @parent_swarm_id,
|
|
321
|
+
model: context.metadata[:model] || "unknown",
|
|
322
|
+
threshold: "#{context.metadata[:threshold]}%",
|
|
323
|
+
current_usage: "#{context.metadata[:percentage]}%",
|
|
324
|
+
tokens_used: context.metadata[:tokens_used],
|
|
325
|
+
tokens_remaining: context.metadata[:tokens_remaining],
|
|
326
|
+
context_limit: context.metadata[:context_limit],
|
|
327
|
+
metadata: context.metadata,
|
|
328
|
+
)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Extract base name from delegation instance name
|
|
332
|
+
#
|
|
333
|
+
# @param instance_name [String, Symbol] Instance name (e.g., "agent@1234")
|
|
334
|
+
# @return [Symbol] Base agent name (e.g., :agent)
|
|
335
|
+
def extract_base_name(instance_name)
|
|
336
|
+
instance_name.to_s.split("@").first.to_sym
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|