swarm_memory 2.1.5 → 2.1.6

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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. metadata +5 -184
  4. data/lib/claude_swarm/base_executor.rb +0 -133
  5. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  6. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  7. data/lib/claude_swarm/cli.rb +0 -697
  8. data/lib/claude_swarm/commands/ps.rb +0 -215
  9. data/lib/claude_swarm/commands/show.rb +0 -139
  10. data/lib/claude_swarm/configuration.rb +0 -373
  11. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  12. data/lib/claude_swarm/json_handler.rb +0 -91
  13. data/lib/claude_swarm/mcp_generator.rb +0 -230
  14. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  15. data/lib/claude_swarm/openai/executor.rb +0 -256
  16. data/lib/claude_swarm/openai/responses.rb +0 -319
  17. data/lib/claude_swarm/orchestrator.rb +0 -878
  18. data/lib/claude_swarm/process_tracker.rb +0 -78
  19. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  20. data/lib/claude_swarm/session_path.rb +0 -42
  21. data/lib/claude_swarm/settings_generator.rb +0 -77
  22. data/lib/claude_swarm/system_utils.rb +0 -46
  23. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  24. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  25. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  27. data/lib/claude_swarm/version.rb +0 -5
  28. data/lib/claude_swarm/worktree_manager.rb +0 -475
  29. data/lib/claude_swarm/yaml_loader.rb +0 -22
  30. data/lib/claude_swarm.rb +0 -67
  31. data/lib/swarm_cli/cli.rb +0 -201
  32. data/lib/swarm_cli/command_registry.rb +0 -61
  33. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  34. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  35. data/lib/swarm_cli/commands/migrate.rb +0 -55
  36. data/lib/swarm_cli/commands/run.rb +0 -173
  37. data/lib/swarm_cli/config_loader.rb +0 -98
  38. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  39. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  40. data/lib/swarm_cli/interactive_repl.rb +0 -924
  41. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  42. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  43. data/lib/swarm_cli/migrate_options.rb +0 -54
  44. data/lib/swarm_cli/migrator.rb +0 -132
  45. data/lib/swarm_cli/options.rb +0 -151
  46. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  47. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  48. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  49. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  50. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  51. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  52. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  53. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  54. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  55. data/lib/swarm_cli/ui/icons.rb +0 -36
  56. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  57. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  58. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  59. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  60. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  61. data/lib/swarm_cli/version.rb +0 -5
  62. data/lib/swarm_cli.rb +0 -46
  63. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  64. data/lib/swarm_sdk/agent/builder.rb +0 -552
  65. data/lib/swarm_sdk/agent/chat.rb +0 -774
  66. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  67. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  68. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  69. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  70. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  71. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  72. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  73. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  75. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  76. data/lib/swarm_sdk/agent/context.rb +0 -116
  77. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  78. data/lib/swarm_sdk/agent/definition.rb +0 -477
  79. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  80. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  81. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  82. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  83. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  84. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  85. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  86. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  87. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  88. data/lib/swarm_sdk/configuration.rb +0 -135
  89. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  90. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  91. data/lib/swarm_sdk/context_compactor.rb +0 -335
  92. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  93. data/lib/swarm_sdk/context_management/context.rb +0 -328
  94. data/lib/swarm_sdk/defaults.rb +0 -196
  95. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  96. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  97. data/lib/swarm_sdk/hooks/context.rb +0 -197
  98. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  99. data/lib/swarm_sdk/hooks/error.rb +0 -29
  100. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  101. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  102. data/lib/swarm_sdk/hooks/result.rb +0 -150
  103. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  104. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  105. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  106. data/lib/swarm_sdk/log_collector.rb +0 -227
  107. data/lib/swarm_sdk/log_stream.rb +0 -127
  108. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  109. data/lib/swarm_sdk/model_aliases.json +0 -8
  110. data/lib/swarm_sdk/models.json +0 -1
  111. data/lib/swarm_sdk/models.rb +0 -120
  112. data/lib/swarm_sdk/node_context.rb +0 -245
  113. data/lib/swarm_sdk/observer/builder.rb +0 -81
  114. data/lib/swarm_sdk/observer/config.rb +0 -45
  115. data/lib/swarm_sdk/observer/manager.rb +0 -236
  116. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  117. data/lib/swarm_sdk/permissions/config.rb +0 -239
  118. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  119. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  120. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  121. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  122. data/lib/swarm_sdk/plugin.rb +0 -309
  123. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  124. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  125. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  126. data/lib/swarm_sdk/restore_result.rb +0 -65
  127. data/lib/swarm_sdk/result.rb +0 -123
  128. data/lib/swarm_sdk/snapshot.rb +0 -156
  129. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  130. data/lib/swarm_sdk/state_restorer.rb +0 -476
  131. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  132. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  133. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  134. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  135. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  136. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  141. data/lib/swarm_sdk/swarm.rb +0 -717
  142. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  143. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  144. data/lib/swarm_sdk/tools/bash.rb +0 -282
  145. data/lib/swarm_sdk/tools/clock.rb +0 -44
  146. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  147. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  148. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  149. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  150. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  151. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  152. data/lib/swarm_sdk/tools/edit.rb +0 -145
  153. data/lib/swarm_sdk/tools/glob.rb +0 -166
  154. data/lib/swarm_sdk/tools/grep.rb +0 -235
  155. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  156. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
  157. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  158. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  159. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  160. data/lib/swarm_sdk/tools/read.rb +0 -261
  161. data/lib/swarm_sdk/tools/registry.rb +0 -205
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  165. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  166. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
  167. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  168. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  169. data/lib/swarm_sdk/tools/think.rb +0 -98
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/utils.rb +0 -68
  174. data/lib/swarm_sdk/validation_result.rb +0 -33
  175. data/lib/swarm_sdk/version.rb +0 -5
  176. data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
  177. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  178. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  179. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  180. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  181. data/lib/swarm_sdk/workflow.rb +0 -554
  182. data/lib/swarm_sdk.rb +0 -524
@@ -1,774 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Agent
5
- # Chat wraps RubyLLM::Chat to provide SwarmSDK orchestration capabilities
6
- #
7
- # ## Architecture
8
- #
9
- # This class uses **composition** with RubyLLM::Chat:
10
- # - RubyLLM::Chat handles: LLM API, messages, tools, concurrent execution
11
- # - SwarmSDK::Agent::Chat adds: hooks, reminders, semaphores, event enrichment
12
- #
13
- # ## ChatHelpers Module Architecture
14
- #
15
- # Chat is decomposed into 8 focused helper modules to manage complexity:
16
- #
17
- # ### Core Functionality
18
- # - **EventEmitter**: Multi-subscriber event callbacks for tool/lifecycle events.
19
- # Provides `subscribe`, `emit_event`, `clear_subscribers` for observable behavior.
20
- # - **LoggingHelpers**: Formatting tool call information for structured JSON logs.
21
- # Converts tool calls/results to loggable hashes with sanitization.
22
- # - **LlmConfiguration**: Model selection, provider setup, and API configuration.
23
- # Resolves provider from model, handles model aliases, builds connection config.
24
- # - **SystemReminders**: Dynamic system message injection based on agent state.
25
- # Collects reminders from plugins, context trackers, and other sources.
26
- #
27
- # ### Cross-Cutting Concerns
28
- # - **Instrumentation**: LLM API request/response logging via Faraday middleware.
29
- # Wraps HTTP calls to capture timing, tokens, and error information.
30
- # - **HookIntegration**: Pre/post tool execution callbacks and delegation hooks.
31
- # Integrates with SwarmSDK Hooks::Registry for lifecycle events.
32
- # - **TokenTracking**: Usage statistics and cost calculation per conversation.
33
- # Accumulates input/output tokens across all LLM calls.
34
- #
35
- # ### State Management
36
- # - **Serialization**: Snapshot/restore for session persistence.
37
- # Saves/restores message history, tool states, and agent context.
38
- #
39
- # ## Module Dependencies
40
- #
41
- # EventEmitter <-- HookIntegration (event emission for hooks)
42
- # TokenTracking <-- Instrumentation (usage data collection)
43
- # SystemReminders <-- uses ContextTracker instance (not a module)
44
- # LoggingHelpers <-- EventEmitter (log event formatting)
45
- #
46
- # ## Design Rationale
47
- #
48
- # This decomposition follows Single Responsibility Principle. Each module
49
- # handles one concern. They access shared Chat internals (@llm_chat,
50
- # @messages, etc.) which makes them tightly coupled to Chat, but this keeps
51
- # the main Chat class focused on orchestration rather than implementation
52
- # details. The modules are intentionally NOT standalone - they augment
53
- # Chat with specific capabilities.
54
- #
55
- # ## Rate Limiting Strategy
56
- #
57
- # Two-level semaphore system prevents API quota exhaustion in hierarchical agent trees:
58
- # 1. **Global semaphore** - Serializes ask() calls across entire swarm
59
- # 2. **Local semaphore** - Limits concurrent tool calls per agent (via RubyLLM)
60
- #
61
- # ## Event Flow
62
- #
63
- # RubyLLM events → SwarmSDK subscribes → enriches with context → emits SwarmSDK events
64
- # This allows hooks to fire on SwarmSDK events with full agent context.
65
- #
66
- # @see ChatHelpers::EventEmitter Event subscription and emission
67
- # @see ChatHelpers::Instrumentation API logging via Faraday middleware
68
- # @see ChatHelpers::Serialization State persistence (snapshot/restore)
69
- # @see ChatHelpers::HookIntegration Pre/post tool execution callbacks
70
- class Chat
71
- # Include event emitter for multi-subscriber callbacks
72
- include ChatHelpers::EventEmitter
73
-
74
- # Include logging helpers for tool call formatting
75
- include ChatHelpers::LoggingHelpers
76
-
77
- # Include hook integration for pre/post tool hooks
78
- include ChatHelpers::HookIntegration
79
-
80
- # Include LLM configuration helpers
81
- include ChatHelpers::LlmConfiguration
82
-
83
- # Include system reminder collection
84
- include ChatHelpers::SystemReminders
85
-
86
- # Include token tracking methods
87
- include ChatHelpers::TokenTracking
88
-
89
- # Include message serialization
90
- include ChatHelpers::Serialization
91
-
92
- # Include LLM instrumentation
93
- include ChatHelpers::Instrumentation
94
-
95
- # SwarmSDK-specific accessors
96
- attr_reader :global_semaphore,
97
- :real_model_info,
98
- :context_tracker,
99
- :context_manager,
100
- :agent_context,
101
- :last_todowrite_message_index,
102
- :active_skill_path,
103
- :provider # Extracted from RubyLLM::Chat for instrumentation (not publicly accessible)
104
-
105
- # Setters for snapshot/restore
106
- attr_writer :last_todowrite_message_index, :active_skill_path
107
-
108
- # Initialize AgentChat with RubyLLM::Chat wrapper
109
- #
110
- # @param definition [Hash] Agent definition containing all configuration
111
- # @param agent_name [Symbol, nil] Agent identifier (for plugin callbacks)
112
- # @param global_semaphore [Async::Semaphore, nil] Shared across all agents
113
- # @param options [Hash] Additional options
114
- def initialize(definition:, agent_name: nil, global_semaphore: nil, **options)
115
- # Initialize event emitter system
116
- initialize_event_emitter
117
-
118
- # Extract configuration from definition
119
- model_id = definition[:model]
120
- provider_name = definition[:provider]
121
- context_window = definition[:context_window]
122
- max_concurrent_tools = definition[:max_concurrent_tools]
123
- base_url = definition[:base_url]
124
- api_version = definition[:api_version]
125
- timeout = definition[:timeout] || Defaults::Timeouts::AGENT_REQUEST_SECONDS
126
- assume_model_exists = definition[:assume_model_exists]
127
- system_prompt = definition[:system_prompt]
128
- parameters = definition[:parameters]
129
- custom_headers = definition[:headers]
130
-
131
- # Agent identifier (for plugin callbacks)
132
- @agent_name = agent_name
133
-
134
- # Context manager for ephemeral messages
135
- @context_manager = ContextManager.new
136
-
137
- # Rate limiting
138
- @global_semaphore = global_semaphore
139
- @explicit_context_window = context_window
140
-
141
- # Serialize ask() calls to prevent message corruption
142
- @ask_semaphore = Async::Semaphore.new(1)
143
-
144
- # Track TodoWrite usage for periodic reminders
145
- @last_todowrite_message_index = nil
146
-
147
- # Agent context for logging (set via setup_context)
148
- @agent_context = nil
149
-
150
- # Context tracker (created after agent_context is set)
151
- @context_tracker = nil
152
-
153
- # Track immutable tools
154
- @immutable_tool_names = Set.new(["Think", "Clock", "TodoWrite"])
155
-
156
- # Track active skill (only used if memory enabled)
157
- @active_skill_path = nil
158
-
159
- # Create internal RubyLLM::Chat instance
160
- @llm_chat = create_llm_chat(
161
- model_id: model_id,
162
- provider_name: provider_name,
163
- base_url: base_url,
164
- api_version: api_version,
165
- timeout: timeout,
166
- assume_model_exists: assume_model_exists,
167
- max_concurrent_tools: max_concurrent_tools,
168
- )
169
-
170
- # Extract provider from RubyLLM::Chat for instrumentation
171
- # Must be done after create_llm_chat since with_responses_api() may swap provider
172
- # NOTE: RubyLLM doesn't expose provider publicly, but we need it for Faraday middleware
173
- # rubocop:disable Security/NoReflectionMethods
174
- @provider = @llm_chat.instance_variable_get(:@provider)
175
- # rubocop:enable Security/NoReflectionMethods
176
-
177
- # Try to fetch real model info for accurate context tracking
178
- fetch_real_model_info(model_id)
179
-
180
- # Configure system prompt, parameters, and headers
181
- configure_system_prompt(system_prompt) if system_prompt
182
- configure_parameters(parameters)
183
- configure_headers(custom_headers)
184
-
185
- # Setup around_tool_execution hook for SwarmSDK orchestration
186
- setup_tool_execution_hook
187
-
188
- # Setup around_llm_request hook for ephemeral message injection
189
- setup_llm_request_hook
190
-
191
- # Setup event bridging from RubyLLM to SwarmSDK
192
- setup_event_bridging
193
- end
194
-
195
- # --- SwarmSDK Abstraction API ---
196
- # These methods provide SwarmSDK-specific semantics without exposing RubyLLM internals
197
-
198
- # Model information
199
- def model_id
200
- @llm_chat.model.id
201
- end
202
-
203
- def model_provider
204
- @llm_chat.model.provider
205
- end
206
-
207
- def model_context_window
208
- @real_model_info&.context_window || @llm_chat.model.context_window
209
- end
210
-
211
- # Tool introspection
212
- def has_tool?(name)
213
- @llm_chat.tools.key?(name.to_s) || @llm_chat.tools.key?(name.to_sym)
214
- end
215
-
216
- def tool_names
217
- @llm_chat.tools.values.map(&:name).sort
218
- end
219
-
220
- def tool_count
221
- @llm_chat.tools.size
222
- end
223
-
224
- def remove_tool(name)
225
- @llm_chat.tools.delete(name.to_s) || @llm_chat.tools.delete(name.to_sym)
226
- end
227
-
228
- # Direct access to tools hash for advanced operations
229
- #
230
- # Use with caution - prefer has_tool?, tool_names, remove_tool for most cases.
231
- # This is provided for:
232
- # - Direct tool execution in tests
233
- # - Advanced tool manipulation (remove_mutable_tools)
234
- #
235
- # @return [Hash] Tool name to tool instance mapping
236
- def tools
237
- @llm_chat.tools
238
- end
239
-
240
- # Message introspection
241
- def message_count
242
- @llm_chat.messages.size
243
- end
244
-
245
- def has_user_message?
246
- @llm_chat.messages.any? { |msg| msg.role == :user }
247
- end
248
-
249
- def last_assistant_message
250
- @llm_chat.messages.reverse.find { |msg| msg.role == :assistant }
251
- end
252
-
253
- # Read-only access to conversation messages
254
- #
255
- # Returns a copy of the message array for safe enumeration.
256
- # External code should use this instead of internal_messages.
257
- #
258
- # @return [Array<RubyLLM::Message>] Copy of message array
259
- def messages
260
- @llm_chat.messages.dup
261
- end
262
-
263
- # Atomically replace all conversation messages
264
- #
265
- # Used for context compaction and state restoration.
266
- # This is the safe way to manipulate messages from external code.
267
- #
268
- # @param new_messages [Array<RubyLLM::Message>] New message array
269
- # @return [self] for chaining
270
- def replace_messages(new_messages)
271
- @llm_chat.messages.clear
272
- new_messages.each { |msg| @llm_chat.messages << msg }
273
- self
274
- end
275
-
276
- # Get all assistant messages
277
- #
278
- # @return [Array<RubyLLM::Message>] All assistant messages
279
- def assistant_messages
280
- @llm_chat.messages.select { |msg| msg.role == :assistant }
281
- end
282
-
283
- # Find the last message matching a condition
284
- #
285
- # @yield [msg] Block to test each message
286
- # @return [RubyLLM::Message, nil] Last matching message or nil
287
- def find_last_message(&block)
288
- @llm_chat.messages.reverse.find(&block)
289
- end
290
-
291
- # Find the index of last message matching a condition
292
- #
293
- # @yield [msg] Block to test each message
294
- # @return [Integer, nil] Index of last matching message or nil
295
- def find_last_message_index(&block)
296
- @llm_chat.messages.rindex(&block)
297
- end
298
-
299
- # Get tool names that are NOT delegation tools
300
- #
301
- # @return [Array<String>] Non-delegation tool names
302
- def non_delegation_tool_names
303
- if @agent_context
304
- @llm_chat.tools.keys.reject { |name| @agent_context.delegation_tool?(name.to_s) }
305
- else
306
- @llm_chat.tools.keys
307
- end
308
- end
309
-
310
- # Add an ephemeral reminder to the most recent message
311
- #
312
- # The reminder will be sent to the LLM but not persisted in message history.
313
- # This encapsulates the internal message array access.
314
- #
315
- # @param reminder [String] Reminder content to add
316
- # @return [void]
317
- def add_ephemeral_reminder(reminder)
318
- @context_manager&.add_ephemeral_reminder(reminder, messages_array: @llm_chat.messages)
319
- end
320
-
321
- # --- Setup Methods ---
322
-
323
- # Setup agent context
324
- #
325
- # @param context [Agent::Context] Agent context for this chat
326
- def setup_context(context)
327
- @agent_context = context
328
- @context_tracker = ChatHelpers::ContextTracker.new(self, context)
329
- end
330
-
331
- # Setup logging callbacks
332
- #
333
- # @return [void]
334
- def setup_logging
335
- raise StateError, "Agent context not set. Call setup_context first." unless @agent_context
336
-
337
- @context_tracker.setup_logging
338
- inject_llm_instrumentation
339
- end
340
-
341
- # Emit model lookup warning if one occurred during initialization
342
- #
343
- # @param agent_name [Symbol, String] The agent name for logging context
344
- def emit_model_lookup_warning(agent_name)
345
- return unless @model_lookup_error
346
-
347
- LogStream.emit(
348
- type: "model_lookup_warning",
349
- agent: agent_name,
350
- swarm_id: @agent_context&.swarm_id,
351
- parent_swarm_id: @agent_context&.parent_swarm_id,
352
- model: @model_lookup_error[:model],
353
- error_message: @model_lookup_error[:error_message],
354
- suggestions: @model_lookup_error[:suggestions].map { |s| { id: s.id, name: s.name, context_window: s.context_window } },
355
- )
356
- end
357
-
358
- # --- Adapter API (SwarmSDK-stable interface) ---
359
-
360
- # Configure system prompt for the conversation
361
- #
362
- # @param prompt [String] System prompt
363
- # @param replace [Boolean] Replace existing system messages if true
364
- # @return [self] for chaining
365
- def configure_system_prompt(prompt, replace: false)
366
- @llm_chat.with_instructions(prompt, replace: replace)
367
- self
368
- end
369
-
370
- # Add a tool to this chat
371
- #
372
- # @param tool [Class, RubyLLM::Tool] Tool class or instance
373
- # @return [self] for chaining
374
- def add_tool(tool)
375
- @llm_chat.with_tool(tool)
376
- self
377
- end
378
-
379
- # Complete the current conversation (no additional prompt)
380
- #
381
- # Delegates to RubyLLM::Chat#complete() which handles:
382
- # - LLM API calls (with around_llm_request hook for ephemeral injection)
383
- # - Tool execution (with around_tool_execution hook for SwarmSDK hooks)
384
- # - Automatic tool loop (continues until no more tool calls)
385
- #
386
- # SwarmSDK adds:
387
- # - Semaphore rate limiting (ask + global)
388
- # - Finish marker handling (finish_agent, finish_swarm)
389
- #
390
- # @param options [Hash] Additional options (currently unused, for future compatibility)
391
- # @param block [Proc] Optional streaming block
392
- # @return [RubyLLM::Message] LLM response
393
- def complete(**_options, &block)
394
- @ask_semaphore.acquire do
395
- execute_with_global_semaphore do
396
- result = catch(:finish_agent) do
397
- catch(:finish_swarm) do
398
- # Delegate to RubyLLM::Chat#complete()
399
- # Hooks handle ephemeral injection and tool orchestration
400
- @llm_chat.complete(&block)
401
- end
402
- end
403
-
404
- # Handle finish markers thrown by hooks
405
- handle_finish_marker(result)
406
- end
407
- end
408
- end
409
-
410
- # Mark tools as immutable (cannot be removed by dynamic tool swapping)
411
- #
412
- # @param tool_names [Array<String>] Tool names to mark as immutable
413
- def mark_tools_immutable(*tool_names)
414
- @immutable_tool_names.merge(tool_names.flatten.map(&:to_s))
415
- end
416
-
417
- # Remove all mutable tools (keeps immutable tools)
418
- #
419
- # @return [void]
420
- def remove_mutable_tools
421
- mutable_tool_names = tools.keys.reject { |name| @immutable_tool_names.include?(name.to_s) }
422
- mutable_tool_names.each { |name| tools.delete(name) }
423
- end
424
-
425
- # Mark skill as loaded (tracking for debugging/logging)
426
- #
427
- # @param file_path [String] Path to loaded skill
428
- def mark_skill_loaded(file_path)
429
- @active_skill_path = file_path
430
- end
431
-
432
- # Check if a skill is currently loaded
433
- #
434
- # @return [Boolean] True if a skill has been loaded
435
- def skill_loaded?
436
- !@active_skill_path.nil?
437
- end
438
-
439
- # Clear conversation history
440
- #
441
- # @return [void]
442
- def clear_conversation
443
- @llm_chat.reset_messages!
444
- @context_manager&.clear_ephemeral
445
- end
446
-
447
- # --- Core Conversation Methods ---
448
-
449
- # Send a message to the LLM and get a response
450
- #
451
- # This method:
452
- # 1. Serializes concurrent asks via @ask_semaphore
453
- # 2. Adds CLEAN user message to history (no reminders)
454
- # 3. Injects system reminders as ephemeral content (sent to LLM but not stored)
455
- # 4. Triggers user_prompt hooks
456
- # 5. Acquires global semaphore for LLM call
457
- # 6. Delegates to RubyLLM::Chat for actual execution
458
- #
459
- # @param prompt [String] User prompt
460
- # @param options [Hash] Additional options (source: for hooks)
461
- # @return [RubyLLM::Message] LLM response
462
- def ask(prompt, **options)
463
- @ask_semaphore.acquire do
464
- is_first = first_message?
465
-
466
- # Collect system reminders to inject as ephemeral content
467
- reminders = collect_system_reminders(prompt, is_first)
468
-
469
- # Trigger user_prompt hook (with clean prompt, not reminders)
470
- source = options.delete(:source) || "user"
471
- final_prompt = prompt
472
- if @hook_executor
473
- hook_result = trigger_user_prompt(prompt, source: source)
474
-
475
- if hook_result[:halted]
476
- return RubyLLM::Message.new(
477
- role: :assistant,
478
- content: hook_result[:halt_message],
479
- model_id: model_id,
480
- )
481
- end
482
-
483
- final_prompt = hook_result[:modified_prompt] if hook_result[:modified_prompt]
484
- end
485
-
486
- # Add CLEAN user message to history (no reminders embedded)
487
- @llm_chat.add_message(role: :user, content: final_prompt)
488
-
489
- # Track reminders as ephemeral content for this LLM call only
490
- # They'll be injected by around_llm_request hook but not stored
491
- reminders.each do |reminder|
492
- @context_manager.add_ephemeral_reminder(reminder, messages_array: @llm_chat.messages)
493
- end
494
-
495
- # Execute complete() which handles tool loop and ephemeral injection
496
- response = execute_with_global_semaphore do
497
- catch(:finish_agent) do
498
- catch(:finish_swarm) do
499
- @llm_chat.complete(**options)
500
- end
501
- end
502
- end
503
-
504
- # Handle finish markers from hooks
505
- handle_finish_marker(response)
506
- end
507
- end
508
-
509
- # Add a message to the conversation history
510
- #
511
- # Automatically extracts and strips system reminders, tracking them as ephemeral.
512
- #
513
- # @param message_or_attributes [RubyLLM::Message, Hash] Message object or attributes hash
514
- # @return [RubyLLM::Message] The added message
515
- def add_message(message_or_attributes)
516
- message = if message_or_attributes.is_a?(RubyLLM::Message)
517
- message_or_attributes
518
- else
519
- RubyLLM::Message.new(message_or_attributes)
520
- end
521
-
522
- # Extract system reminders if present
523
- content_str = message.content.is_a?(RubyLLM::Content) ? message.content.text : message.content.to_s
524
-
525
- if @context_manager.has_system_reminders?(content_str)
526
- reminders = @context_manager.extract_system_reminders(content_str)
527
- clean_content_str = @context_manager.strip_system_reminders(content_str)
528
-
529
- clean_content = if message.content.is_a?(RubyLLM::Content)
530
- RubyLLM::Content.new(clean_content_str, message.content.attachments)
531
- else
532
- clean_content_str
533
- end
534
-
535
- clean_message = RubyLLM::Message.new(
536
- role: message.role,
537
- content: clean_content,
538
- tool_call_id: message.tool_call_id,
539
- tool_calls: message.tool_calls,
540
- model_id: message.model_id,
541
- input_tokens: message.input_tokens,
542
- output_tokens: message.output_tokens,
543
- cached_tokens: message.cached_tokens,
544
- cache_creation_tokens: message.cache_creation_tokens,
545
- )
546
-
547
- @llm_chat.add_message(clean_message)
548
-
549
- # Track reminders as ephemeral
550
- reminders.each do |reminder|
551
- @context_manager.add_ephemeral_reminder(reminder, messages_array: messages)
552
- end
553
-
554
- clean_message
555
- else
556
- @llm_chat.add_message(message)
557
- end
558
- end
559
-
560
- private
561
-
562
- # --- Tool Execution Hook ---
563
-
564
- # Setup around_tool_execution hook for SwarmSDK orchestration
565
- #
566
- # This hook intercepts all tool executions to:
567
- # - Trigger pre_tool_use hooks (can block, replace, or finish)
568
- # - Trigger post_tool_use hooks (can transform results)
569
- # - Handle finish markers
570
- def setup_tool_execution_hook
571
- @llm_chat.around_tool_execution do |tool_call, _tool_instance, execute|
572
- # Skip hooks for delegation tools (they have their own events)
573
- if delegation_tool_call?(tool_call)
574
- execute.call
575
- else
576
- # PRE-HOOK
577
- pre_result = trigger_pre_tool_use(tool_call)
578
-
579
- case pre_result
580
- when Hash
581
- if pre_result[:finish_agent]
582
- throw(:finish_agent, { __finish_agent__: true, message: pre_result[:custom_result] })
583
- elsif pre_result[:finish_swarm]
584
- throw(:finish_swarm, { __finish_swarm__: true, message: pre_result[:custom_result] })
585
- elsif !pre_result[:proceed]
586
- # Blocked - return custom result without executing
587
- next pre_result[:custom_result] || "Tool execution blocked by hook"
588
- end
589
- end
590
-
591
- # EXECUTE tool (no retry - failures are returned to LLM)
592
- result = execute.call
593
-
594
- # POST-HOOK
595
- post_result = trigger_post_tool_use(result, tool_call: tool_call)
596
-
597
- # Check for finish markers from post-hook
598
- if post_result.is_a?(Hash)
599
- if post_result[:__finish_agent__]
600
- throw(:finish_agent, post_result)
601
- elsif post_result[:__finish_swarm__]
602
- throw(:finish_swarm, post_result)
603
- end
604
- end
605
-
606
- post_result
607
- end
608
- end
609
- end
610
-
611
- # --- Event Bridging ---
612
-
613
- # Setup event bridging from RubyLLM to SwarmSDK
614
- #
615
- # Subscribes to RubyLLM events and emits enriched SwarmSDK events.
616
- def setup_event_bridging
617
- # Bridge tool_call events
618
- @llm_chat.on_tool_call do |tool_call|
619
- emit(:tool_call, tool_call)
620
- end
621
-
622
- # Bridge tool_result events
623
- @llm_chat.on_tool_result do |_tool_call, result|
624
- emit(:tool_result, result)
625
- end
626
-
627
- # Bridge new_message events
628
- @llm_chat.on_new_message do
629
- emit(:new_message)
630
- end
631
-
632
- # Bridge end_message events (used for agent_step/agent_stop)
633
- @llm_chat.on_end_message do |message|
634
- emit(:end_message, message)
635
- end
636
- end
637
-
638
- # --- LLM Request Hook ---
639
-
640
- # Setup around_llm_request hook for ephemeral message injection
641
- #
642
- # This hook intercepts all LLM API calls to:
643
- # - Inject ephemeral content (system reminders) that shouldn't be persisted
644
- # - Clear ephemeral content after each LLM call
645
- # - Add retry logic for transient failures
646
- def setup_llm_request_hook
647
- @llm_chat.around_llm_request do |messages, &send_request|
648
- # Inject ephemeral content (system reminders, etc.)
649
- # These are sent to LLM but NOT persisted in message history
650
- prepared_messages = @context_manager.prepare_for_llm(messages)
651
-
652
- # Make the actual LLM API call with retry logic
653
- response = call_llm_with_retry do
654
- send_request.call(prepared_messages)
655
- end
656
-
657
- # Clear ephemeral content after successful call
658
- @context_manager.clear_ephemeral
659
-
660
- response
661
- end
662
- end
663
-
664
- # --- Semaphore and Reminder Management ---
665
-
666
- # Execute block with global semaphore
667
- #
668
- # @yield Block to execute
669
- # @return [Object] Result from block
670
- def execute_with_global_semaphore(&block)
671
- if @global_semaphore
672
- @global_semaphore.acquire(&block)
673
- else
674
- yield
675
- end
676
- end
677
-
678
- # Check if this is the first user message
679
- #
680
- # @return [Boolean] true if no user messages exist yet
681
- def first_message?
682
- !has_user_message?
683
- end
684
-
685
- # Handle finish markers from hooks
686
- #
687
- # @param response [Object] Response from ask (may be a finish marker hash)
688
- # @return [RubyLLM::Message] Final message
689
- def handle_finish_marker(response)
690
- if response.is_a?(Hash)
691
- if response[:__finish_agent__]
692
- message = RubyLLM::Message.new(
693
- role: :assistant,
694
- content: response[:message],
695
- model_id: model_id,
696
- )
697
- @context_tracker.finish_reason_override = "finish_agent" if @context_tracker
698
- emit(:end_message, message)
699
- message
700
- elsif response[:__finish_swarm__]
701
- # Propagate finish_swarm marker up
702
- response
703
- else
704
- # Regular response
705
- response
706
- end
707
- else
708
- response
709
- end
710
- end
711
-
712
- # --- LLM Call Retry Logic ---
713
-
714
- # Call LLM provider with retry logic for transient failures
715
- #
716
- # @param max_retries [Integer] Maximum retry attempts
717
- # @param delay [Integer] Delay between retries in seconds
718
- # @yield Block that performs the LLM call
719
- # @return [Object] Result from block
720
- def call_llm_with_retry(max_retries: 10, delay: 10, &block)
721
- attempts = 0
722
-
723
- loop do
724
- attempts += 1
725
-
726
- begin
727
- return yield
728
- rescue StandardError => e
729
- if attempts >= max_retries
730
- LogStream.emit(
731
- type: "llm_retry_exhausted",
732
- agent: @agent_name,
733
- swarm_id: @agent_context&.swarm_id,
734
- parent_swarm_id: @agent_context&.parent_swarm_id,
735
- model: model_id,
736
- attempts: attempts,
737
- error_class: e.class.name,
738
- error_message: e.message,
739
- error_backtrace: e.backtrace,
740
- )
741
- raise
742
- end
743
-
744
- LogStream.emit(
745
- type: "llm_retry_attempt",
746
- agent: @agent_name,
747
- swarm_id: @agent_context&.swarm_id,
748
- parent_swarm_id: @agent_context&.parent_swarm_id,
749
- model: model_id,
750
- attempt: attempts,
751
- max_retries: max_retries,
752
- error_class: e.class.name,
753
- error_message: e.message,
754
- error_backtrace: e.backtrace,
755
- retry_delay: delay,
756
- )
757
-
758
- sleep(delay)
759
- end
760
- end
761
- end
762
-
763
- # Check if a tool call is a delegation tool
764
- #
765
- # @param tool_call [RubyLLM::ToolCall] Tool call to check
766
- # @return [Boolean] true if this is a delegation tool
767
- def delegation_tool_call?(tool_call)
768
- return false unless @agent_context
769
-
770
- @agent_context.delegation_tool?(tool_call.name)
771
- end
772
- end
773
- end
774
- end