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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/lib/claude_swarm/claude_mcp_server.rb +1 -0
  3. data/lib/claude_swarm/cli.rb +5 -18
  4. data/lib/claude_swarm/configuration.rb +2 -15
  5. data/lib/claude_swarm/mcp_generator.rb +1 -0
  6. data/lib/claude_swarm/openai/chat_completion.rb +4 -12
  7. data/lib/claude_swarm/openai/executor.rb +3 -1
  8. data/lib/claude_swarm/openai/responses.rb +13 -32
  9. data/lib/claude_swarm/version.rb +1 -1
  10. data/lib/swarm_cli/commands/run.rb +2 -2
  11. data/lib/swarm_cli/config_loader.rb +11 -11
  12. data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
  13. data/lib/swarm_cli/interactive_repl.rb +11 -5
  14. data/lib/swarm_cli/ui/icons.rb +0 -23
  15. data/lib/swarm_cli/version.rb +1 -1
  16. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  17. data/lib/swarm_memory/integration/sdk_plugin.rb +87 -7
  18. data/lib/swarm_memory/version.rb +1 -1
  19. data/lib/swarm_memory.rb +1 -1
  20. data/lib/swarm_sdk/agent/builder.rb +58 -0
  21. data/lib/swarm_sdk/agent/chat.rb +527 -1059
  22. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
  23. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  24. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
  25. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  26. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  27. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  28. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  29. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +12 -12
  30. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  31. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  32. data/lib/swarm_sdk/agent/context.rb +2 -2
  33. data/lib/swarm_sdk/agent/definition.rb +66 -154
  34. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  35. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  36. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  37. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  38. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  39. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  40. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  41. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  42. data/lib/swarm_sdk/configuration.rb +65 -543
  43. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  44. data/lib/swarm_sdk/context_compactor.rb +6 -11
  45. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  46. data/lib/swarm_sdk/context_management/context.rb +328 -0
  47. data/lib/swarm_sdk/defaults.rb +196 -0
  48. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  49. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  50. data/lib/swarm_sdk/log_collector.rb +179 -29
  51. data/lib/swarm_sdk/log_stream.rb +29 -0
  52. data/lib/swarm_sdk/node_context.rb +1 -1
  53. data/lib/swarm_sdk/observer/builder.rb +81 -0
  54. data/lib/swarm_sdk/observer/config.rb +45 -0
  55. data/lib/swarm_sdk/observer/manager.rb +236 -0
  56. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  57. data/lib/swarm_sdk/plugin.rb +93 -3
  58. data/lib/swarm_sdk/snapshot.rb +6 -6
  59. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  60. data/lib/swarm_sdk/state_restorer.rb +136 -151
  61. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  62. data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
  63. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  64. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  65. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  66. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  67. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  68. data/lib/swarm_sdk/swarm/tool_configurator.rb +42 -138
  69. data/lib/swarm_sdk/swarm.rb +137 -679
  70. data/lib/swarm_sdk/tools/bash.rb +11 -3
  71. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  72. data/lib/swarm_sdk/tools/edit.rb +8 -13
  73. data/lib/swarm_sdk/tools/glob.rb +9 -1
  74. data/lib/swarm_sdk/tools/grep.rb +7 -0
  75. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  76. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  77. data/lib/swarm_sdk/tools/read.rb +11 -13
  78. data/lib/swarm_sdk/tools/registry.rb +122 -10
  79. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -5
  80. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  81. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  82. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  83. data/lib/swarm_sdk/tools/write.rb +8 -13
  84. data/lib/swarm_sdk/version.rb +1 -1
  85. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  86. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  87. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  88. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +3 -3
  89. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  90. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  91. data/lib/swarm_sdk.rb +33 -3
  92. metadata +37 -14
  93. data/lib/swarm_memory/chat_extension.rb +0 -34
  94. 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