swarm_memory 2.1.2 → 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 (118) 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 +30 -19
  5. data/lib/claude_swarm/mcp_generator.rb +5 -10
  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/mcp_serve.rb +2 -2
  11. data/lib/swarm_cli/commands/run.rb +2 -2
  12. data/lib/swarm_cli/config_loader.rb +14 -14
  13. data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
  14. data/lib/swarm_cli/interactive_repl.rb +11 -5
  15. data/lib/swarm_cli/ui/icons.rb +0 -23
  16. data/lib/swarm_cli/version.rb +1 -1
  17. data/lib/swarm_memory/adapters/base.rb +4 -4
  18. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  19. data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
  20. data/lib/swarm_memory/integration/cli_registration.rb +3 -2
  21. data/lib/swarm_memory/integration/sdk_plugin.rb +98 -12
  22. data/lib/swarm_memory/tools/memory_edit.rb +2 -2
  23. data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
  24. data/lib/swarm_memory/tools/memory_read.rb +3 -3
  25. data/lib/swarm_memory/version.rb +1 -1
  26. data/lib/swarm_memory.rb +6 -1
  27. data/lib/swarm_sdk/agent/builder.rb +91 -0
  28. data/lib/swarm_sdk/agent/chat.rb +540 -925
  29. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +33 -79
  30. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  31. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +147 -39
  32. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  33. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  34. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  35. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  36. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +22 -38
  37. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  38. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  39. data/lib/swarm_sdk/agent/context.rb +8 -4
  40. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  41. data/lib/swarm_sdk/agent/definition.rb +79 -174
  42. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +182 -0
  43. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  44. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  45. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  46. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  47. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  48. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  49. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  50. data/lib/swarm_sdk/configuration.rb +100 -261
  51. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  52. data/lib/swarm_sdk/context_compactor.rb +6 -11
  53. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  54. data/lib/swarm_sdk/context_management/context.rb +328 -0
  55. data/lib/swarm_sdk/defaults.rb +196 -0
  56. data/lib/swarm_sdk/events_to_messages.rb +199 -0
  57. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  58. data/lib/swarm_sdk/log_collector.rb +192 -16
  59. data/lib/swarm_sdk/log_stream.rb +66 -8
  60. data/lib/swarm_sdk/model_aliases.json +4 -1
  61. data/lib/swarm_sdk/node_context.rb +1 -1
  62. data/lib/swarm_sdk/observer/builder.rb +81 -0
  63. data/lib/swarm_sdk/observer/config.rb +45 -0
  64. data/lib/swarm_sdk/observer/manager.rb +236 -0
  65. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  66. data/lib/swarm_sdk/plugin.rb +93 -3
  67. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  68. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  69. data/lib/swarm_sdk/restore_result.rb +65 -0
  70. data/lib/swarm_sdk/snapshot.rb +156 -0
  71. data/lib/swarm_sdk/snapshot_from_events.rb +397 -0
  72. data/lib/swarm_sdk/state_restorer.rb +476 -0
  73. data/lib/swarm_sdk/state_snapshot.rb +334 -0
  74. data/lib/swarm_sdk/swarm/agent_initializer.rb +428 -79
  75. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  76. data/lib/swarm_sdk/swarm/builder.rb +69 -407
  77. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  78. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  79. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  80. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  81. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  82. data/lib/swarm_sdk/swarm/tool_configurator.rb +88 -149
  83. data/lib/swarm_sdk/swarm.rb +366 -631
  84. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  85. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  86. data/lib/swarm_sdk/tools/bash.rb +11 -3
  87. data/lib/swarm_sdk/tools/delegate.rb +127 -24
  88. data/lib/swarm_sdk/tools/edit.rb +8 -13
  89. data/lib/swarm_sdk/tools/glob.rb +9 -1
  90. data/lib/swarm_sdk/tools/grep.rb +7 -0
  91. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  92. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  93. data/lib/swarm_sdk/tools/read.rb +28 -18
  94. data/lib/swarm_sdk/tools/registry.rb +122 -10
  95. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  96. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  97. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  98. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  99. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +53 -5
  100. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  101. data/lib/swarm_sdk/tools/think.rb +4 -1
  102. data/lib/swarm_sdk/tools/todo_write.rb +27 -8
  103. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  104. data/lib/swarm_sdk/tools/write.rb +8 -13
  105. data/lib/swarm_sdk/utils.rb +18 -0
  106. data/lib/swarm_sdk/validation_result.rb +33 -0
  107. data/lib/swarm_sdk/version.rb +1 -1
  108. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +34 -9
  109. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  110. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  111. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +42 -21
  112. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  113. data/lib/swarm_sdk/workflow.rb +554 -0
  114. data/lib/swarm_sdk.rb +393 -22
  115. metadata +51 -16
  116. data/lib/swarm_memory/chat_extension.rb +0 -34
  117. data/lib/swarm_sdk/node_orchestrator.rb +0 -591
  118. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -582
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ module ChatHelpers
6
+ # LLM instrumentation for API request/response logging
7
+ #
8
+ # Extracted from Chat to reduce class size and centralize observability logic.
9
+ module Instrumentation
10
+ private
11
+
12
+ # Inject LLM instrumentation middleware for API request/response logging
13
+ #
14
+ # @return [void]
15
+ def inject_llm_instrumentation
16
+ return unless @provider
17
+
18
+ faraday_conn = @provider.connection&.connection
19
+ return unless faraday_conn
20
+ return if @llm_instrumentation_injected
21
+
22
+ provider_name = @provider.class.name.split("::").last.downcase
23
+
24
+ faraday_conn.builder.insert(
25
+ 0,
26
+ SwarmSDK::Agent::LLMInstrumentationMiddleware,
27
+ on_request: method(:handle_llm_api_request),
28
+ on_response: method(:handle_llm_api_response),
29
+ provider_name: provider_name,
30
+ )
31
+
32
+ @llm_instrumentation_injected = true
33
+
34
+ RubyLLM.logger.debug("SwarmSDK: Injected LLM instrumentation middleware for agent #{@agent_name}")
35
+ rescue StandardError => e
36
+ LogStream.emit_error(e, source: "instrumentation", context: "inject_middleware", agent: @agent_name)
37
+ RubyLLM.logger.debug("SwarmSDK: Failed to inject LLM instrumentation: #{e.message}")
38
+ end
39
+
40
+ # Handle LLM API request event
41
+ #
42
+ # @param data [Hash] Request data from middleware
43
+ def handle_llm_api_request(data)
44
+ return unless LogStream.emitter
45
+
46
+ LogStream.emit(
47
+ type: "llm_api_request",
48
+ agent: @agent_name,
49
+ swarm_id: @agent_context&.swarm_id,
50
+ parent_swarm_id: @agent_context&.parent_swarm_id,
51
+ **data,
52
+ )
53
+ rescue StandardError => e
54
+ LogStream.emit_error(e, source: "instrumentation", context: "emit_llm_api_request", agent: @agent_name)
55
+ RubyLLM.logger.debug("SwarmSDK: Error emitting llm_api_request event: #{e.message}")
56
+ end
57
+
58
+ # Handle LLM API response event
59
+ #
60
+ # @param data [Hash] Response data from middleware
61
+ def handle_llm_api_response(data)
62
+ return unless LogStream.emitter
63
+
64
+ LogStream.emit(
65
+ type: "llm_api_response",
66
+ agent: @agent_name,
67
+ swarm_id: @agent_context&.swarm_id,
68
+ parent_swarm_id: @agent_context&.parent_swarm_id,
69
+ **data,
70
+ )
71
+ rescue StandardError => e
72
+ LogStream.emit_error(e, source: "instrumentation", context: "emit_llm_api_response", agent: @agent_name)
73
+ RubyLLM.logger.debug("SwarmSDK: Error emitting llm_api_response event: #{e.message}")
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ module ChatHelpers
6
+ # LLM configuration and provider setup
7
+ #
8
+ # Extracted from Chat to reduce class size and centralize RubyLLM setup logic.
9
+ module LlmConfiguration
10
+ private
11
+
12
+ # Create the internal RubyLLM::Chat instance
13
+ #
14
+ # @return [RubyLLM::Chat] Chat instance
15
+ def create_llm_chat(model_id:, provider_name:, base_url:, api_version:, timeout:, assume_model_exists:, max_concurrent_tools:)
16
+ chat_options = build_chat_options(max_concurrent_tools)
17
+
18
+ chat = instantiate_chat(
19
+ model_id: model_id,
20
+ provider_name: provider_name,
21
+ base_url: base_url,
22
+ timeout: timeout,
23
+ assume_model_exists: assume_model_exists,
24
+ chat_options: chat_options,
25
+ )
26
+
27
+ # Enable RubyLLM's native Responses API if configured
28
+ enable_responses_api(chat, api_version, base_url) if api_version == "v1/responses"
29
+
30
+ chat
31
+ end
32
+
33
+ # Build chat options hash
34
+ #
35
+ # @param max_concurrent_tools [Integer, nil] Max concurrent tool executions
36
+ # @return [Hash] Chat options
37
+ def build_chat_options(max_concurrent_tools)
38
+ return {} unless max_concurrent_tools
39
+
40
+ {
41
+ tool_concurrency: :async,
42
+ max_concurrency: max_concurrent_tools,
43
+ }
44
+ end
45
+
46
+ # Instantiate RubyLLM::Chat with appropriate configuration
47
+ #
48
+ # @return [RubyLLM::Chat] Chat instance
49
+ def instantiate_chat(model_id:, provider_name:, base_url:, timeout:, assume_model_exists:, chat_options:)
50
+ if base_url || timeout != Defaults::Timeouts::AGENT_REQUEST_SECONDS
51
+ instantiate_with_custom_context(
52
+ model_id: model_id,
53
+ provider_name: provider_name,
54
+ base_url: base_url,
55
+ timeout: timeout,
56
+ assume_model_exists: assume_model_exists,
57
+ chat_options: chat_options,
58
+ )
59
+ elsif provider_name
60
+ instantiate_with_provider(
61
+ model_id: model_id,
62
+ provider_name: provider_name,
63
+ assume_model_exists: assume_model_exists,
64
+ chat_options: chat_options,
65
+ )
66
+ else
67
+ instantiate_default(
68
+ model_id: model_id,
69
+ assume_model_exists: assume_model_exists,
70
+ chat_options: chat_options,
71
+ )
72
+ end
73
+ end
74
+
75
+ # Instantiate chat with custom context (base_url/timeout overrides)
76
+ def instantiate_with_custom_context(model_id:, provider_name:, base_url:, timeout:, assume_model_exists:, chat_options:)
77
+ raise ArgumentError, "Provider must be specified when base_url is set" if base_url && !provider_name
78
+
79
+ context = build_custom_context(provider: provider_name, base_url: base_url, timeout: timeout)
80
+ assume_model_exists = base_url ? true : false if assume_model_exists.nil?
81
+
82
+ RubyLLM.chat(
83
+ model: model_id,
84
+ provider: provider_name,
85
+ assume_model_exists: assume_model_exists,
86
+ context: context,
87
+ **chat_options,
88
+ )
89
+ end
90
+
91
+ # Instantiate chat with explicit provider
92
+ def instantiate_with_provider(model_id:, provider_name:, assume_model_exists:, chat_options:)
93
+ assume_model_exists = false if assume_model_exists.nil?
94
+
95
+ RubyLLM.chat(
96
+ model: model_id,
97
+ provider: provider_name,
98
+ assume_model_exists: assume_model_exists,
99
+ **chat_options,
100
+ )
101
+ end
102
+
103
+ # Instantiate chat with default configuration
104
+ def instantiate_default(model_id:, assume_model_exists:, chat_options:)
105
+ assume_model_exists = false if assume_model_exists.nil?
106
+
107
+ RubyLLM.chat(
108
+ model: model_id,
109
+ assume_model_exists: assume_model_exists,
110
+ **chat_options,
111
+ )
112
+ end
113
+
114
+ # Build custom RubyLLM context for base_url/timeout overrides
115
+ #
116
+ # @return [RubyLLM::Context] Configured context
117
+ def build_custom_context(provider:, base_url:, timeout:)
118
+ RubyLLM.context do |config|
119
+ config.request_timeout = timeout
120
+
121
+ configure_provider_base_url(config, provider, base_url) if base_url
122
+ end
123
+ end
124
+
125
+ # Configure provider-specific base URL
126
+ def configure_provider_base_url(config, provider, base_url)
127
+ case provider.to_s
128
+ when "openai", "deepseek", "perplexity", "mistral", "openrouter"
129
+ config.openai_api_base = base_url
130
+ config.openai_api_key = ENV["OPENAI_API_KEY"] || "dummy-key-for-local"
131
+ config.openai_use_system_role = true
132
+ when "ollama"
133
+ config.ollama_api_base = base_url
134
+ when "gpustack"
135
+ config.gpustack_api_base = base_url
136
+ config.gpustack_api_key = ENV["GPUSTACK_API_KEY"] || "dummy-key"
137
+ else
138
+ raise ArgumentError, "Provider '#{provider}' doesn't support custom base_url."
139
+ end
140
+ end
141
+
142
+ # Fetch real model info for accurate context tracking
143
+ #
144
+ # @param model_id [String] Model ID to lookup
145
+ def fetch_real_model_info(model_id)
146
+ @model_lookup_error = nil
147
+ @real_model_info = begin
148
+ RubyLLM.models.find(model_id)
149
+ rescue StandardError => e
150
+ suggestions = suggest_similar_models(model_id)
151
+ @model_lookup_error = {
152
+ model: model_id,
153
+ error_message: e.message,
154
+ suggestions: suggestions,
155
+ }
156
+ nil
157
+ end
158
+ end
159
+
160
+ # Enable RubyLLM's native Responses API on the chat instance
161
+ #
162
+ # Uses RubyLLM's built-in support for OpenAI's Responses API (v1/responses endpoint)
163
+ # which provides automatic stateful conversation tracking with 5-minute TTL.
164
+ #
165
+ # @param chat [RubyLLM::Chat] Chat instance to configure
166
+ # @param api_version [String] API version (should be "v1/responses")
167
+ # @param base_url [String, nil] Custom endpoint URL if any
168
+ def enable_responses_api(chat, api_version, base_url)
169
+ return unless api_version == "v1/responses"
170
+
171
+ # Warn if using custom endpoint (typically doesn't support Responses API)
172
+ if base_url && !base_url.include?("api.openai.com")
173
+ RubyLLM.logger.warn(
174
+ "SwarmSDK: Responses API requested but using custom endpoint #{base_url}. " \
175
+ "Custom endpoints typically don't support /v1/responses.",
176
+ )
177
+ end
178
+
179
+ # Enable native RubyLLM Responses API support
180
+ # - stateful: true enables automatic previous_response_id tracking
181
+ # - store: true enables server-side conversation storage
182
+ chat.with_responses_api(stateful: true, store: true)
183
+ RubyLLM.logger.debug("SwarmSDK: Enabled native Responses API support")
184
+ end
185
+
186
+ # Configure LLM parameters with proper temperature normalization
187
+ #
188
+ # @param params [Hash] Parameter hash
189
+ # @return [self]
190
+ def configure_parameters(params)
191
+ return self if params.nil? || params.empty?
192
+
193
+ if params[:temperature]
194
+ @llm_chat.with_temperature(params[:temperature])
195
+ params = params.except(:temperature)
196
+ end
197
+
198
+ @llm_chat.with_params(**params) if params.any?
199
+
200
+ self
201
+ end
202
+
203
+ # Configure custom HTTP headers for LLM requests
204
+ #
205
+ # @param headers [Hash, nil] Custom HTTP headers
206
+ # @return [self]
207
+ def configure_headers(custom_headers)
208
+ return self if custom_headers.nil? || custom_headers.empty?
209
+
210
+ @llm_chat.with_headers(**custom_headers)
211
+
212
+ self
213
+ end
214
+
215
+ # Suggest similar models when a model is not found
216
+ #
217
+ # @param query [String] Model name to search for
218
+ # @return [Array<RubyLLM::Model::Info>] Up to 3 similar models
219
+ def suggest_similar_models(query)
220
+ normalized_query = query.to_s.downcase.gsub(/[.\-_]/, "")
221
+
222
+ RubyLLM.models.all.select do |model_info|
223
+ normalized_id = model_info.id.downcase.gsub(/[.\-_]/, "")
224
+ normalized_id.include?(normalized_query) ||
225
+ model_info.name&.downcase&.gsub(/[.\-_]/, "")&.include?(normalized_query)
226
+ end.first(3)
227
+ rescue StandardError
228
+ []
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module SwarmSDK
4
4
  module Agent
5
- class Chat < RubyLLM::Chat
5
+ module ChatHelpers
6
6
  # Helper methods for logging and serialization of tool calls and results
7
7
  #
8
8
  # Responsibilities:
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ module ChatHelpers
6
+ # Message serialization and deserialization for snapshots
7
+ #
8
+ # Extracted from Chat to reduce class size and centralize persistence logic.
9
+ module Serialization
10
+ # Create snapshot of current conversation state
11
+ #
12
+ # @return [Hash] Serialized conversation data
13
+ def conversation_snapshot
14
+ {
15
+ messages: @llm_chat.messages.map { |msg| serialize_message(msg) },
16
+ model_id: model_id,
17
+ provider: model_provider,
18
+ timestamp: Time.now.utc.iso8601,
19
+ }
20
+ end
21
+
22
+ # Restore conversation from snapshot
23
+ #
24
+ # @param snapshot [Hash] Serialized conversation data
25
+ # @return [self]
26
+ def restore_conversation(snapshot)
27
+ raise ArgumentError, "Invalid snapshot: missing messages" unless snapshot[:messages]
28
+
29
+ @llm_chat.messages.clear
30
+ snapshot[:messages].each do |msg_data|
31
+ @llm_chat.messages << deserialize_message(msg_data)
32
+ end
33
+
34
+ self
35
+ end
36
+
37
+ private
38
+
39
+ # Serialize a RubyLLM::Message to a plain hash
40
+ #
41
+ # @param message [RubyLLM::Message] Message to serialize
42
+ # @return [Hash] Serialized message data
43
+ def serialize_message(message)
44
+ data = message.to_h
45
+
46
+ # Convert tool_calls to plain hashes (they're ToolCall objects)
47
+ if data[:tool_calls]
48
+ data[:tool_calls] = data[:tool_calls].transform_values(&:to_h)
49
+ end
50
+
51
+ # Handle Content objects
52
+ if data[:content].respond_to?(:to_h)
53
+ data[:content] = data[:content].to_h
54
+ end
55
+
56
+ data
57
+ end
58
+
59
+ # Deserialize a hash back to a RubyLLM::Message
60
+ #
61
+ # @param data [Hash] Serialized message data
62
+ # @return [RubyLLM::Message] Reconstructed message
63
+ def deserialize_message(data)
64
+ data = data.transform_keys(&:to_sym)
65
+
66
+ # Convert tool_calls back to ToolCall objects
67
+ if data[:tool_calls]
68
+ data[:tool_calls] = data[:tool_calls].transform_values do |tc_data|
69
+ tc_data = tc_data.transform_keys(&:to_sym)
70
+ RubyLLM::ToolCall.new(
71
+ id: tc_data[:id],
72
+ name: tc_data[:name],
73
+ arguments: tc_data[:arguments] || {},
74
+ )
75
+ end
76
+ end
77
+
78
+ RubyLLM::Message.new(**data)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module SwarmSDK
4
4
  module Agent
5
- class Chat < RubyLLM::Chat
5
+ module ChatHelpers
6
6
  # Handles injection of system reminders at strategic points in the conversation
7
7
  #
8
8
  # Responsibilities:
@@ -12,23 +12,6 @@ module SwarmSDK
12
12
  #
13
13
  # This class is stateless - it operates on the chat's message history.
14
14
  class SystemReminderInjector
15
- # System reminder to inject BEFORE the first user message
16
- BEFORE_FIRST_MESSAGE_REMINDER = <<~REMINDER.strip
17
- <system-reminder>
18
- As you answer the user's questions, you can use the following context:
19
-
20
- # important-instruction-reminders
21
-
22
- Do what has been asked; nothing more, nothing less.
23
- NEVER create files unless they're absolutely necessary for achieving your goal.
24
- ALWAYS prefer editing an existing file to creating a new one.
25
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
26
-
27
- IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.
28
-
29
- </system-reminder>
30
- REMINDER
31
-
32
15
  # System reminder to inject AFTER the first user message
33
16
  AFTER_FIRST_MESSAGE_REMINDER = <<~REMINDER.strip
34
17
  <system-reminder>Your todo list is currently empty. DO NOT mention this to the user. If this task requires multiple steps: (1) FIRST analyze the scope by searching/reading files, (2) SECOND create a COMPLETE todo list with ALL tasks before starting work, (3) THIRD execute tasks one by one. Only skip the todo list for simple single-step tasks. Do not mention this message to the user.</system-reminder>
@@ -39,8 +22,8 @@ module SwarmSDK
39
22
  <system-reminder>The TodoWrite tool hasn't been used recently. If you're working on tasks that would benefit from tracking progress, consider using the TodoWrite tool to track progress. Also consider cleaning up the todo list if has become stale and no longer matches what you are working on. Only use it if it's relevant to the current work. This is just a gentle reminder - ignore if not applicable.</system-reminder>
40
23
  REMINDER
41
24
 
42
- # Number of messages between TodoWrite reminders
43
- TODOWRITE_REMINDER_INTERVAL = 8
25
+ # Backward compatibility alias - use Defaults module for new code
26
+ TODOWRITE_REMINDER_INTERVAL = Defaults::Context::TODOWRITE_REMINDER_INTERVAL
44
27
 
45
28
  class << self
46
29
  # Check if this is the first user message in the conversation
@@ -48,19 +31,17 @@ module SwarmSDK
48
31
  # @param chat [Agent::Chat] The chat instance
49
32
  # @return [Boolean] true if no user messages exist yet
50
33
  def first_message?(chat)
51
- chat.messages.none? { |msg| msg.role == :user }
34
+ !chat.has_user_message?
52
35
  end
53
36
 
54
- # Inject first message reminders (before + after user message)
37
+ # Inject first message reminders
55
38
  #
56
- # This manually constructs the first message sequence with system reminders
57
- # sandwiching the actual user prompt.
39
+ # This manually constructs the first message sequence with system reminders.
58
40
  #
59
41
  # Sequence:
60
- # 1. BEFORE_FIRST_MESSAGE_REMINDER (general reminders)
42
+ # 1. User's actual prompt
61
43
  # 2. Toolset reminder (list of available tools)
62
- # 3. User's actual prompt
63
- # 4. AFTER_FIRST_MESSAGE_REMINDER (todo list reminder)
44
+ # 3. AFTER_FIRST_MESSAGE_REMINDER (todo list reminder - only if TodoWrite available)
64
45
  #
65
46
  # @param chat [Agent::Chat] The chat instance
66
47
  # @param prompt [String] The user's actual prompt
@@ -68,12 +49,15 @@ module SwarmSDK
68
49
  def inject_first_message_reminders(chat, prompt)
69
50
  # Build user message with embedded reminders
70
51
  # Reminders are embedded in the content, not separate messages
71
- full_content = [
52
+ parts = [
72
53
  prompt,
73
- BEFORE_FIRST_MESSAGE_REMINDER,
74
54
  build_toolset_reminder(chat),
75
- AFTER_FIRST_MESSAGE_REMINDER,
76
- ].join("\n\n")
55
+ ]
56
+
57
+ # Only include todo list reminder if agent has TodoWrite tool
58
+ parts << AFTER_FIRST_MESSAGE_REMINDER if chat.has_tool?("TodoWrite")
59
+
60
+ full_content = parts.join("\n\n")
77
61
 
78
62
  # Extract reminders and add clean prompt to persistent history
79
63
  reminders = chat.context_manager.extract_system_reminders(full_content)
@@ -84,7 +68,7 @@ module SwarmSDK
84
68
 
85
69
  # Track reminders to embed in this message when sending to LLM
86
70
  reminders.each do |reminder|
87
- chat.context_manager.add_ephemeral_reminder(reminder, messages_array: chat.messages)
71
+ chat.add_ephemeral_reminder(reminder)
88
72
  end
89
73
  end
90
74
 
@@ -93,7 +77,7 @@ module SwarmSDK
93
77
  # @param chat [Agent::Chat] The chat instance
94
78
  # @return [String] System reminder with tool list
95
79
  def build_toolset_reminder(chat)
96
- tools_list = chat.tools.values.map(&:name).sort
80
+ tools_list = chat.tool_names
97
81
 
98
82
  reminder = "<system-reminder>\n"
99
83
  reminder += "Tools available: #{tools_list.join(", ")}\n\n"
@@ -114,23 +98,23 @@ module SwarmSDK
114
98
  # @return [Boolean] true if reminder should be injected
115
99
  def should_inject_todowrite_reminder?(chat, last_todowrite_index)
116
100
  # Need at least a few messages before reminding
117
- return false if chat.messages.count < 5
101
+ return false if chat.message_count < 5
118
102
 
119
103
  # Find the last message that contains TodoWrite tool usage
120
- last_todo_index = chat.messages.rindex do |msg|
104
+ last_todo_index = chat.find_last_message_index do |msg|
121
105
  msg.role == :tool && msg.content.to_s.include?("TodoWrite")
122
106
  end
123
107
 
124
108
  # Check if enough messages have passed since last TodoWrite
125
109
  if last_todo_index.nil? && last_todowrite_index.nil?
126
110
  # Never used TodoWrite - check if we've exceeded interval
127
- chat.messages.count >= TODOWRITE_REMINDER_INTERVAL
111
+ chat.message_count >= TODOWRITE_REMINDER_INTERVAL
128
112
  elsif last_todo_index
129
113
  # Recently used - don't remind
130
114
  false
131
115
  elsif last_todowrite_index
132
116
  # Used before - check if interval has passed
133
- chat.messages.count - last_todowrite_index >= TODOWRITE_REMINDER_INTERVAL
117
+ chat.message_count - last_todowrite_index >= TODOWRITE_REMINDER_INTERVAL
134
118
  else
135
119
  false
136
120
  end
@@ -141,7 +125,7 @@ module SwarmSDK
141
125
  # @param chat [Agent::Chat] The chat instance
142
126
  # @return [Integer, nil] Index of last TodoWrite usage, or nil
143
127
  def find_last_todowrite_index(chat)
144
- chat.messages.rindex do |msg|
128
+ chat.find_last_message_index do |msg|
145
129
  msg.role == :tool && msg.content.to_s.include?("TodoWrite")
146
130
  end
147
131
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ module ChatHelpers
6
+ # System reminder collection and injection
7
+ #
8
+ # Extracted from Chat to reduce class size and centralize reminder logic.
9
+ module SystemReminders
10
+ # Collect reminders from all plugins
11
+ #
12
+ # @param prompt [String] User's message
13
+ # @param is_first_message [Boolean] True if first message
14
+ # @return [Array<String>] Array of reminder strings
15
+ def collect_plugin_reminders(prompt, is_first_message:)
16
+ return [] unless @agent_name
17
+
18
+ PluginRegistry.all.flat_map do |plugin|
19
+ plugin.on_user_message(
20
+ agent_name: @agent_name,
21
+ prompt: prompt,
22
+ is_first_message: is_first_message,
23
+ )
24
+ end.compact
25
+ end
26
+
27
+ # Collect all system reminders for this message
28
+ #
29
+ # Returns an array of reminder strings that should be injected as ephemeral content.
30
+ # These are sent to the LLM but not stored in message history.
31
+ #
32
+ # @param prompt [String] User prompt
33
+ # @param is_first [Boolean] Whether this is the first message
34
+ # @return [Array<String>] Array of reminder strings
35
+ def collect_system_reminders(prompt, is_first)
36
+ reminders = []
37
+
38
+ if is_first
39
+ # Add toolset reminder on first message
40
+ reminders << build_toolset_reminder
41
+
42
+ # Add todo list reminder if agent has TodoWrite tool
43
+ reminders << SystemReminderInjector::AFTER_FIRST_MESSAGE_REMINDER if has_tool?(:TodoWrite)
44
+
45
+ # Collect plugin reminders
46
+ reminders.concat(collect_plugin_reminders(prompt, is_first_message: true))
47
+ else
48
+ # Add periodic TodoWrite reminder if needed
49
+ if has_tool?(:TodoWrite) && SystemReminderInjector.should_inject_todowrite_reminder?(self, @last_todowrite_message_index)
50
+ reminders << SystemReminderInjector::TODOWRITE_PERIODIC_REMINDER
51
+ @last_todowrite_message_index = SystemReminderInjector.find_last_todowrite_index(self)
52
+ end
53
+
54
+ # Collect plugin reminders
55
+ reminders.concat(collect_plugin_reminders(prompt, is_first_message: false))
56
+ end
57
+
58
+ reminders
59
+ end
60
+
61
+ private
62
+
63
+ # Build toolset reminder listing all available tools
64
+ #
65
+ # @return [String] System reminder with tool list
66
+ def build_toolset_reminder
67
+ tools_list = tool_names
68
+
69
+ reminder = "<system-reminder>\n"
70
+ reminder += "Tools available: #{tools_list.join(", ")}\n\n"
71
+ reminder += "Only use tools from this list. Do not attempt to use tools that are not listed here.\n"
72
+ reminder += "</system-reminder>"
73
+
74
+ reminder
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end