Class: SwarmSDK::Agent::Chat
- Inherits:
-
Object
- Object
- SwarmSDK::Agent::Chat
- Includes:
- SwarmSDK::Agent::ChatHelpers::EventEmitter, SwarmSDK::Agent::ChatHelpers::HookIntegration, SwarmSDK::Agent::ChatHelpers::Instrumentation, SwarmSDK::Agent::ChatHelpers::LlmConfiguration, SwarmSDK::Agent::ChatHelpers::LoggingHelpers, SwarmSDK::Agent::ChatHelpers::Serialization, SwarmSDK::Agent::ChatHelpers::SystemReminders, SwarmSDK::Agent::ChatHelpers::TokenTracking
- Defined in:
- lib/swarm_sdk/agent/chat.rb
Overview
Chat wraps RubyLLM::Chat to provide SwarmSDK orchestration capabilities
Architecture
This class uses composition with RubyLLM::Chat:
- RubyLLM::Chat handles: LLM API, messages, tools, concurrent execution
- SwarmSDK::Agent::Chat adds: hooks, reminders, semaphores, event enrichment
ChatHelpers Module Architecture
Chat is decomposed into 8 focused helper modules to manage complexity:
Core Functionality
- EventEmitter: Multi-subscriber event callbacks for tool/lifecycle events.
Provides
subscribe,emit_event,clear_subscribersfor observable behavior. - LoggingHelpers: Formatting tool call information for structured JSON logs. Converts tool calls/results to loggable hashes with sanitization.
- LlmConfiguration: Model selection, provider setup, and API configuration. Resolves provider from model, handles model aliases, builds connection config.
- SystemReminders: Dynamic system message injection based on agent state. Collects reminders from plugins, context trackers, and other sources.
Cross-Cutting Concerns
- Instrumentation: LLM API request/response logging via Faraday middleware. Wraps HTTP calls to capture timing, tokens, and error information.
- HookIntegration: Pre/post tool execution callbacks and delegation hooks. Integrates with SwarmSDK Hooks::Registry for lifecycle events.
- TokenTracking: Usage statistics and cost calculation per conversation. Accumulates input/output tokens across all LLM calls.
State Management
- Serialization: Snapshot/restore for session persistence. Saves/restores message history, tool states, and agent context.
Module Dependencies
EventEmitter <-- HookIntegration (event emission for hooks)
TokenTracking <-- Instrumentation (usage data collection)
SystemReminders <-- uses ContextTracker instance (not a module)
LoggingHelpers <-- EventEmitter (log event formatting)
Design Rationale
This decomposition follows Single Responsibility Principle. Each module handles one concern. They access shared Chat internals (@llm_chat, @messages, etc.) which makes them tightly coupled to Chat, but this keeps the main Chat class focused on orchestration rather than implementation details. The modules are intentionally NOT standalone - they augment Chat with specific capabilities.
Rate Limiting Strategy
Two-level semaphore system prevents API quota exhaustion in hierarchical agent trees:
- Global semaphore - Serializes ask() calls across entire swarm
- Local semaphore - Limits concurrent tool calls per agent (via RubyLLM)
Event Flow
RubyLLM events → SwarmSDK subscribes → enriches with context → emits SwarmSDK events This allows hooks to fire on SwarmSDK events with full agent context.
Instance Attribute Summary collapse
-
#active_skill_path ⇒ Object
SwarmSDK-specific accessors.
-
#agent_context ⇒ Object
readonly
SwarmSDK-specific accessors.
-
#context_manager ⇒ Object
readonly
SwarmSDK-specific accessors.
-
#context_tracker ⇒ Object
readonly
SwarmSDK-specific accessors.
-
#global_semaphore ⇒ Object
readonly
SwarmSDK-specific accessors.
-
#last_todowrite_message_index ⇒ Object
SwarmSDK-specific accessors.
-
#provider ⇒ Object
readonly
SwarmSDK-specific accessors.
-
#real_model_info ⇒ Object
readonly
SwarmSDK-specific accessors.
Attributes included from SwarmSDK::Agent::ChatHelpers::HookIntegration
#hook_agent_hooks, #hook_executor, #hook_swarm
Instance Method Summary collapse
-
#add_ephemeral_reminder(reminder) ⇒ void
Add an ephemeral reminder to the most recent message.
-
#add_message(message_or_attributes) ⇒ RubyLLM::Message
Add a message to the conversation history.
-
#add_tool(tool) ⇒ self
Add a tool to this chat.
-
#ask(prompt, **options) ⇒ RubyLLM::Message
Send a message to the LLM and get a response.
-
#assistant_messages ⇒ Array<RubyLLM::Message>
Get all assistant messages.
-
#clear_conversation ⇒ void
Clear conversation history.
-
#complete(**_options, &block) ⇒ RubyLLM::Message
Complete the current conversation (no additional prompt).
-
#configure_system_prompt(prompt, replace: false) ⇒ self
Configure system prompt for the conversation.
-
#emit_model_lookup_warning(agent_name) ⇒ Object
Emit model lookup warning if one occurred during initialization.
-
#find_last_message {|msg| ... } ⇒ RubyLLM::Message?
Find the last message matching a condition.
-
#find_last_message_index {|msg| ... } ⇒ Integer?
Find the index of last message matching a condition.
-
#has_tool?(name) ⇒ Boolean
Tool introspection.
- #has_user_message? ⇒ Boolean
-
#initialize(definition:, agent_name: nil, global_semaphore: nil, **options) ⇒ Chat
constructor
Initialize AgentChat with RubyLLM::Chat wrapper.
- #last_assistant_message ⇒ Object
-
#mark_skill_loaded(file_path) ⇒ Object
Mark skill as loaded (tracking for debugging/logging).
-
#mark_tools_immutable(*tool_names) ⇒ Object
Mark tools as immutable (cannot be removed by dynamic tool swapping).
-
#message_count ⇒ Object
Message introspection.
-
#messages ⇒ Array<RubyLLM::Message>
Read-only access to conversation messages.
- #model_context_window ⇒ Object
-
#model_id ⇒ Object
Model information.
- #model_provider ⇒ Object
-
#non_delegation_tool_names ⇒ Array<String>
Get tool names that are NOT delegation tools.
-
#remove_mutable_tools ⇒ void
Remove all mutable tools (keeps immutable tools).
- #remove_tool(name) ⇒ Object
-
#replace_messages(new_messages) ⇒ self
Atomically replace all conversation messages.
-
#setup_context(context) ⇒ Object
Setup agent context.
-
#setup_logging ⇒ void
Setup logging callbacks.
-
#skill_loaded? ⇒ Boolean
Check if a skill is currently loaded.
- #tool_count ⇒ Object
- #tool_names ⇒ Object
-
#tools ⇒ Hash
Direct access to tools hash for advanced operations.
Methods included from SwarmSDK::Agent::ChatHelpers::Serialization
#conversation_snapshot, #restore_conversation
Methods included from SwarmSDK::Agent::ChatHelpers::TokenTracking
#compact_context, #context_limit, #context_usage_percentage, #cumulative_cache_creation_tokens, #cumulative_cached_tokens, #cumulative_input_cost, #cumulative_input_tokens, #cumulative_output_cost, #cumulative_output_tokens, #cumulative_total_cost, #cumulative_total_tokens, #effective_input_tokens, #tokens_remaining
Methods included from SwarmSDK::Agent::ChatHelpers::SystemReminders
#collect_plugin_reminders, #collect_system_reminders
Methods included from SwarmSDK::Agent::ChatHelpers::HookIntegration
#add_hook, #check_context_warnings, #setup_hooks, #trigger_post_tool_use, #trigger_pre_tool_use
Methods included from SwarmSDK::Agent::ChatHelpers::LoggingHelpers
#calculate_cost, #format_tool_calls, #serialize_result, #zero_cost
Methods included from SwarmSDK::Agent::ChatHelpers::EventEmitter
#callback_count, #clear_callbacks, #initialize_event_emitter, #on_end_message, #on_new_message, #on_tool_call, #on_tool_result, #once, #subscribe
Constructor Details
#initialize(definition:, agent_name: nil, global_semaphore: nil, **options) ⇒ Chat
Initialize AgentChat with RubyLLM::Chat wrapper
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/swarm_sdk/agent/chat.rb', line 114 def initialize(definition:, agent_name: nil, global_semaphore: nil, **) # Initialize event emitter system initialize_event_emitter # Extract configuration from definition model_id = definition[:model] provider_name = definition[:provider] context_window = definition[:context_window] max_concurrent_tools = definition[:max_concurrent_tools] base_url = definition[:base_url] api_version = definition[:api_version] timeout = definition[:timeout] || SwarmSDK.config.agent_request_timeout assume_model_exists = definition[:assume_model_exists] system_prompt = definition[:system_prompt] parameters = definition[:parameters] custom_headers = definition[:headers] # Agent identifier (for plugin callbacks) @agent_name = agent_name # Context manager for ephemeral messages @context_manager = ContextManager.new # Rate limiting @global_semaphore = global_semaphore @explicit_context_window = context_window # Serialize ask() calls to prevent message corruption @ask_semaphore = Async::Semaphore.new(1) # Track TodoWrite usage for periodic reminders @last_todowrite_message_index = nil # Agent context for logging (set via setup_context) @agent_context = nil # Context tracker (created after agent_context is set) @context_tracker = nil # Track immutable tools @immutable_tool_names = Set.new(["Think", "Clock", "TodoWrite"]) # Track active skill (only used if memory enabled) @active_skill_path = nil # Create internal RubyLLM::Chat instance @llm_chat = create_llm_chat( model_id: model_id, provider_name: provider_name, base_url: base_url, api_version: api_version, timeout: timeout, assume_model_exists: assume_model_exists, max_concurrent_tools: max_concurrent_tools, ) # Extract provider from RubyLLM::Chat for instrumentation # Must be done after create_llm_chat since with_responses_api() may swap provider # NOTE: RubyLLM doesn't expose provider publicly, but we need it for Faraday middleware # rubocop:disable Security/NoReflectionMethods @provider = @llm_chat.instance_variable_get(:@provider) # rubocop:enable Security/NoReflectionMethods # Try to fetch real model info for accurate context tracking fetch_real_model_info(model_id) # Configure system prompt, parameters, and headers configure_system_prompt(system_prompt) if system_prompt configure_parameters(parameters) configure_headers(custom_headers) # Setup around_tool_execution hook for SwarmSDK orchestration setup_tool_execution_hook # Setup around_llm_request hook for ephemeral message injection setup_llm_request_hook # Setup event bridging from RubyLLM to SwarmSDK setup_event_bridging end |
Instance Attribute Details
#active_skill_path ⇒ Object
SwarmSDK-specific accessors
96 97 98 |
# File 'lib/swarm_sdk/agent/chat.rb', line 96 def active_skill_path @active_skill_path end |
#agent_context ⇒ Object (readonly)
SwarmSDK-specific accessors
96 97 98 |
# File 'lib/swarm_sdk/agent/chat.rb', line 96 def agent_context @agent_context end |
#context_manager ⇒ Object (readonly)
SwarmSDK-specific accessors
96 97 98 |
# File 'lib/swarm_sdk/agent/chat.rb', line 96 def context_manager @context_manager end |
#context_tracker ⇒ Object (readonly)
SwarmSDK-specific accessors
96 97 98 |
# File 'lib/swarm_sdk/agent/chat.rb', line 96 def context_tracker @context_tracker end |
#global_semaphore ⇒ Object (readonly)
SwarmSDK-specific accessors
96 97 98 |
# File 'lib/swarm_sdk/agent/chat.rb', line 96 def global_semaphore @global_semaphore end |
#last_todowrite_message_index ⇒ Object
SwarmSDK-specific accessors
96 97 98 |
# File 'lib/swarm_sdk/agent/chat.rb', line 96 def @last_todowrite_message_index end |
#provider ⇒ Object (readonly)
SwarmSDK-specific accessors
96 97 98 |
# File 'lib/swarm_sdk/agent/chat.rb', line 96 def provider @provider end |
#real_model_info ⇒ Object (readonly)
SwarmSDK-specific accessors
96 97 98 |
# File 'lib/swarm_sdk/agent/chat.rb', line 96 def real_model_info @real_model_info end |
Instance Method Details
#add_ephemeral_reminder(reminder) ⇒ void
This method returns an undefined value.
Add an ephemeral reminder to the most recent message
The reminder will be sent to the LLM but not persisted in message history. This encapsulates the internal message array access.
317 318 319 |
# File 'lib/swarm_sdk/agent/chat.rb', line 317 def add_ephemeral_reminder(reminder) @context_manager&.add_ephemeral_reminder(reminder, messages_array: @llm_chat.) end |
#add_message(message_or_attributes) ⇒ RubyLLM::Message
Add a message to the conversation history
Automatically extracts and strips system reminders, tracking them as ephemeral.
515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 |
# File 'lib/swarm_sdk/agent/chat.rb', line 515 def () = if .is_a?(RubyLLM::Message) else RubyLLM::Message.new() end # Extract system reminders if present content_str = .content.is_a?(RubyLLM::Content) ? .content.text : .content.to_s if @context_manager.has_system_reminders?(content_str) reminders = @context_manager.extract_system_reminders(content_str) clean_content_str = @context_manager.strip_system_reminders(content_str) clean_content = if .content.is_a?(RubyLLM::Content) RubyLLM::Content.new(clean_content_str, .content.) else clean_content_str end = RubyLLM::Message.new( role: .role, content: clean_content, tool_call_id: .tool_call_id, tool_calls: .tool_calls, model_id: .model_id, input_tokens: .input_tokens, output_tokens: .output_tokens, cached_tokens: .cached_tokens, cache_creation_tokens: .cache_creation_tokens, ) @llm_chat.() # Track reminders as ephemeral reminders.each do |reminder| @context_manager.add_ephemeral_reminder(reminder, messages_array: ) end else @llm_chat.() end end |
#add_tool(tool) ⇒ self
Add a tool to this chat
374 375 376 377 |
# File 'lib/swarm_sdk/agent/chat.rb', line 374 def add_tool(tool) @llm_chat.with_tool(tool) self end |
#ask(prompt, **options) ⇒ RubyLLM::Message
Send a message to the LLM and get a response
This method:
- Serializes concurrent asks via @ask_semaphore
- Adds CLEAN user message to history (no reminders)
- Injects system reminders as ephemeral content (sent to LLM but not stored)
- Triggers user_prompt hooks
- Acquires global semaphore for LLM call
- Delegates to RubyLLM::Chat for actual execution
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 |
# File 'lib/swarm_sdk/agent/chat.rb', line 462 def ask(prompt, **) @ask_semaphore.acquire do is_first = # Collect system reminders to inject as ephemeral content reminders = collect_system_reminders(prompt, is_first) # Trigger user_prompt hook (with clean prompt, not reminders) source = .delete(:source) || "user" final_prompt = prompt if @hook_executor hook_result = trigger_user_prompt(prompt, source: source) if hook_result[:halted] return RubyLLM::Message.new( role: :assistant, content: hook_result[:halt_message], model_id: model_id, ) end final_prompt = hook_result[:modified_prompt] if hook_result[:modified_prompt] end # Add CLEAN user message to history (no reminders embedded) @llm_chat.(role: :user, content: final_prompt) # Track reminders as ephemeral content for this LLM call only # They'll be injected by around_llm_request hook but not stored reminders.each do |reminder| @context_manager.add_ephemeral_reminder(reminder, messages_array: @llm_chat.) end # Execute complete() which handles tool loop and ephemeral injection response = execute_with_global_semaphore do catch(:finish_agent) do catch(:finish_swarm) do @llm_chat.complete(**) end end end # Handle finish markers from hooks handle_finish_marker(response) end end |
#assistant_messages ⇒ Array<RubyLLM::Message>
Get all assistant messages
279 280 281 |
# File 'lib/swarm_sdk/agent/chat.rb', line 279 def @llm_chat..select { |msg| msg.role == :assistant } end |
#clear_conversation ⇒ void
This method returns an undefined value.
Clear conversation history
442 443 444 445 |
# File 'lib/swarm_sdk/agent/chat.rb', line 442 def clear_conversation @llm_chat. @context_manager&.clear_ephemeral end |
#complete(**_options, &block) ⇒ RubyLLM::Message
Complete the current conversation (no additional prompt)
Delegates to RubyLLM::Chat#complete() which handles:
- LLM API calls (with around_llm_request hook for ephemeral injection)
- Tool execution (with around_tool_execution hook for SwarmSDK hooks)
- Automatic tool loop (continues until no more tool calls)
SwarmSDK adds:
- Semaphore rate limiting (ask + global)
- Finish marker handling (finish_agent, finish_swarm)
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 |
# File 'lib/swarm_sdk/agent/chat.rb', line 393 def complete(**, &block) @ask_semaphore.acquire do execute_with_global_semaphore do result = catch(:finish_agent) do catch(:finish_swarm) do # Delegate to RubyLLM::Chat#complete() # Hooks handle ephemeral injection and tool orchestration @llm_chat.complete(&block) end end # Handle finish markers thrown by hooks handle_finish_marker(result) end end end |
#configure_system_prompt(prompt, replace: false) ⇒ self
Configure system prompt for the conversation
365 366 367 368 |
# File 'lib/swarm_sdk/agent/chat.rb', line 365 def configure_system_prompt(prompt, replace: false) @llm_chat.with_instructions(prompt, replace: replace) self end |
#emit_model_lookup_warning(agent_name) ⇒ Object
Emit model lookup warning if one occurred during initialization
344 345 346 347 348 349 350 351 352 353 354 355 356 |
# File 'lib/swarm_sdk/agent/chat.rb', line 344 def emit_model_lookup_warning(agent_name) return unless @model_lookup_error LogStream.emit( type: "model_lookup_warning", agent: agent_name, swarm_id: @agent_context&.swarm_id, parent_swarm_id: @agent_context&.parent_swarm_id, model: @model_lookup_error[:model], error_message: @model_lookup_error[:error_message], suggestions: @model_lookup_error[:suggestions].map { |s| { id: s.id, name: s.name, context_window: s.context_window } }, ) end |
#find_last_message {|msg| ... } ⇒ RubyLLM::Message?
Find the last message matching a condition
287 288 289 |
# File 'lib/swarm_sdk/agent/chat.rb', line 287 def (&block) @llm_chat..reverse.find(&block) end |
#find_last_message_index {|msg| ... } ⇒ Integer?
Find the index of last message matching a condition
295 296 297 |
# File 'lib/swarm_sdk/agent/chat.rb', line 295 def (&block) @llm_chat..rindex(&block) end |
#has_tool?(name) ⇒ Boolean
Tool introspection
212 213 214 |
# File 'lib/swarm_sdk/agent/chat.rb', line 212 def has_tool?(name) @llm_chat.tools.key?(name.to_s) || @llm_chat.tools.key?(name.to_sym) end |
#has_user_message? ⇒ Boolean
245 246 247 |
# File 'lib/swarm_sdk/agent/chat.rb', line 245 def @llm_chat..any? { |msg| msg.role == :user } end |
#last_assistant_message ⇒ Object
249 250 251 |
# File 'lib/swarm_sdk/agent/chat.rb', line 249 def @llm_chat..reverse.find { |msg| msg.role == :assistant } end |
#mark_skill_loaded(file_path) ⇒ Object
Mark skill as loaded (tracking for debugging/logging)
428 429 430 |
# File 'lib/swarm_sdk/agent/chat.rb', line 428 def mark_skill_loaded(file_path) @active_skill_path = file_path end |
#mark_tools_immutable(*tool_names) ⇒ Object
Mark tools as immutable (cannot be removed by dynamic tool swapping)
413 414 415 |
# File 'lib/swarm_sdk/agent/chat.rb', line 413 def mark_tools_immutable(*tool_names) @immutable_tool_names.merge(tool_names.flatten.map(&:to_s)) end |
#message_count ⇒ Object
Message introspection
241 242 243 |
# File 'lib/swarm_sdk/agent/chat.rb', line 241 def @llm_chat..size end |
#messages ⇒ Array<RubyLLM::Message>
Read-only access to conversation messages
Returns a copy of the message array for safe enumeration. External code should use this instead of internal_messages.
259 260 261 |
# File 'lib/swarm_sdk/agent/chat.rb', line 259 def @llm_chat..dup end |
#model_context_window ⇒ Object
207 208 209 |
# File 'lib/swarm_sdk/agent/chat.rb', line 207 def model_context_window @real_model_info&.context_window || @llm_chat.model.context_window end |
#model_id ⇒ Object
Model information
199 200 201 |
# File 'lib/swarm_sdk/agent/chat.rb', line 199 def model_id @llm_chat.model.id end |
#model_provider ⇒ Object
203 204 205 |
# File 'lib/swarm_sdk/agent/chat.rb', line 203 def model_provider @llm_chat.model.provider end |
#non_delegation_tool_names ⇒ Array<String>
Get tool names that are NOT delegation tools
302 303 304 305 306 307 308 |
# File 'lib/swarm_sdk/agent/chat.rb', line 302 def non_delegation_tool_names if @agent_context @llm_chat.tools.keys.reject { |name| @agent_context.delegation_tool?(name.to_s) } else @llm_chat.tools.keys end end |
#remove_mutable_tools ⇒ void
This method returns an undefined value.
Remove all mutable tools (keeps immutable tools)
420 421 422 423 |
# File 'lib/swarm_sdk/agent/chat.rb', line 420 def remove_mutable_tools mutable_tool_names = tools.keys.reject { |name| @immutable_tool_names.include?(name.to_s) } mutable_tool_names.each { |name| tools.delete(name) } end |
#remove_tool(name) ⇒ Object
224 225 226 |
# File 'lib/swarm_sdk/agent/chat.rb', line 224 def remove_tool(name) @llm_chat.tools.delete(name.to_s) || @llm_chat.tools.delete(name.to_sym) end |
#replace_messages(new_messages) ⇒ self
Atomically replace all conversation messages
Used for context compaction and state restoration. This is the safe way to manipulate messages from external code.
270 271 272 273 274 |
# File 'lib/swarm_sdk/agent/chat.rb', line 270 def () @llm_chat..clear .each { |msg| @llm_chat. << msg } self end |
#setup_context(context) ⇒ Object
Setup agent context
326 327 328 329 |
# File 'lib/swarm_sdk/agent/chat.rb', line 326 def setup_context(context) @agent_context = context @context_tracker = ChatHelpers::ContextTracker.new(self, context) end |
#setup_logging ⇒ void
This method returns an undefined value.
Setup logging callbacks
334 335 336 337 338 339 |
# File 'lib/swarm_sdk/agent/chat.rb', line 334 def setup_logging raise StateError, "Agent context not set. Call setup_context first." unless @agent_context @context_tracker.setup_logging inject_llm_instrumentation end |
#skill_loaded? ⇒ Boolean
Check if a skill is currently loaded
435 436 437 |
# File 'lib/swarm_sdk/agent/chat.rb', line 435 def skill_loaded? !@active_skill_path.nil? end |
#tool_count ⇒ Object
220 221 222 |
# File 'lib/swarm_sdk/agent/chat.rb', line 220 def tool_count @llm_chat.tools.size end |
#tool_names ⇒ Object
216 217 218 |
# File 'lib/swarm_sdk/agent/chat.rb', line 216 def tool_names @llm_chat.tools.values.map(&:name).sort end |
#tools ⇒ Hash
Direct access to tools hash for advanced operations
Use with caution - prefer has_tool?, tool_names, remove_tool for most cases. This is provided for:
- Direct tool execution in tests
- Advanced tool manipulation (remove_mutable_tools)
236 237 238 |
# File 'lib/swarm_sdk/agent/chat.rb', line 236 def tools @llm_chat.tools end |