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,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:
@@ -22,8 +22,8 @@ module SwarmSDK
22
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>
23
23
  REMINDER
24
24
 
25
- # Number of messages between TodoWrite reminders
26
- TODOWRITE_REMINDER_INTERVAL = 8
25
+ # Backward compatibility alias - use Defaults module for new code
26
+ TODOWRITE_REMINDER_INTERVAL = Defaults::Context::TODOWRITE_REMINDER_INTERVAL
27
27
 
28
28
  class << self
29
29
  # Check if this is the first user message in the conversation
@@ -31,7 +31,7 @@ module SwarmSDK
31
31
  # @param chat [Agent::Chat] The chat instance
32
32
  # @return [Boolean] true if no user messages exist yet
33
33
  def first_message?(chat)
34
- chat.messages.none? { |msg| msg.role == :user }
34
+ !chat.has_user_message?
35
35
  end
36
36
 
37
37
  # Inject first message reminders
@@ -55,7 +55,7 @@ module SwarmSDK
55
55
  ]
56
56
 
57
57
  # Only include todo list reminder if agent has TodoWrite tool
58
- parts << AFTER_FIRST_MESSAGE_REMINDER if chat.tools.key?("TodoWrite")
58
+ parts << AFTER_FIRST_MESSAGE_REMINDER if chat.has_tool?("TodoWrite")
59
59
 
60
60
  full_content = parts.join("\n\n")
61
61
 
@@ -68,7 +68,7 @@ module SwarmSDK
68
68
 
69
69
  # Track reminders to embed in this message when sending to LLM
70
70
  reminders.each do |reminder|
71
- chat.context_manager.add_ephemeral_reminder(reminder, messages_array: chat.messages)
71
+ chat.add_ephemeral_reminder(reminder)
72
72
  end
73
73
  end
74
74
 
@@ -77,7 +77,7 @@ module SwarmSDK
77
77
  # @param chat [Agent::Chat] The chat instance
78
78
  # @return [String] System reminder with tool list
79
79
  def build_toolset_reminder(chat)
80
- tools_list = chat.tools.values.map(&:name).sort
80
+ tools_list = chat.tool_names
81
81
 
82
82
  reminder = "<system-reminder>\n"
83
83
  reminder += "Tools available: #{tools_list.join(", ")}\n\n"
@@ -98,23 +98,23 @@ module SwarmSDK
98
98
  # @return [Boolean] true if reminder should be injected
99
99
  def should_inject_todowrite_reminder?(chat, last_todowrite_index)
100
100
  # Need at least a few messages before reminding
101
- return false if chat.messages.count < 5
101
+ return false if chat.message_count < 5
102
102
 
103
103
  # Find the last message that contains TodoWrite tool usage
104
- last_todo_index = chat.messages.rindex do |msg|
104
+ last_todo_index = chat.find_last_message_index do |msg|
105
105
  msg.role == :tool && msg.content.to_s.include?("TodoWrite")
106
106
  end
107
107
 
108
108
  # Check if enough messages have passed since last TodoWrite
109
109
  if last_todo_index.nil? && last_todowrite_index.nil?
110
110
  # Never used TodoWrite - check if we've exceeded interval
111
- chat.messages.count >= TODOWRITE_REMINDER_INTERVAL
111
+ chat.message_count >= TODOWRITE_REMINDER_INTERVAL
112
112
  elsif last_todo_index
113
113
  # Recently used - don't remind
114
114
  false
115
115
  elsif last_todowrite_index
116
116
  # Used before - check if interval has passed
117
- chat.messages.count - last_todowrite_index >= TODOWRITE_REMINDER_INTERVAL
117
+ chat.message_count - last_todowrite_index >= TODOWRITE_REMINDER_INTERVAL
118
118
  else
119
119
  false
120
120
  end
@@ -125,7 +125,7 @@ module SwarmSDK
125
125
  # @param chat [Agent::Chat] The chat instance
126
126
  # @return [Integer, nil] Index of last TodoWrite usage, or nil
127
127
  def find_last_todowrite_index(chat)
128
- chat.messages.rindex do |msg|
128
+ chat.find_last_message_index do |msg|
129
129
  msg.role == :tool && msg.content.to_s.include?("TodoWrite")
130
130
  end
131
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
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ module ChatHelpers
6
+ # Token usage tracking and context limit management
7
+ #
8
+ # Extracted from Chat to reduce class size and centralize token metrics.
9
+ module TokenTracking
10
+ # Get context window limit for the current model
11
+ #
12
+ # @return [Integer, nil] Maximum context tokens
13
+ def context_limit
14
+ return @explicit_context_window if @explicit_context_window
15
+ return @real_model_info.context_window if @real_model_info&.context_window
16
+
17
+ model_context_window
18
+ rescue StandardError
19
+ nil
20
+ end
21
+
22
+ # Calculate cumulative input tokens for the conversation
23
+ #
24
+ # Gets input_tokens from the most recent assistant message, which represents
25
+ # the total context size sent to the model (not sum of all messages).
26
+ #
27
+ # @return [Integer] Total input tokens used
28
+ def cumulative_input_tokens
29
+ find_last_message { |msg| msg.role == :assistant && msg.input_tokens }&.input_tokens || 0
30
+ end
31
+
32
+ # Calculate cumulative output tokens across all assistant messages
33
+ #
34
+ # @return [Integer] Total output tokens used
35
+ def cumulative_output_tokens
36
+ assistant_messages.sum { |msg| msg.output_tokens || 0 }
37
+ end
38
+
39
+ # Calculate cumulative cached tokens
40
+ #
41
+ # @return [Integer] Total cached tokens used
42
+ def cumulative_cached_tokens
43
+ assistant_messages.sum { |msg| msg.cached_tokens || 0 }
44
+ end
45
+
46
+ # Calculate cumulative cache creation tokens
47
+ #
48
+ # @return [Integer] Total tokens written to cache
49
+ def cumulative_cache_creation_tokens
50
+ assistant_messages.sum { |msg| msg.cache_creation_tokens || 0 }
51
+ end
52
+
53
+ # Calculate effective input tokens (excluding cache hits)
54
+ #
55
+ # @return [Integer] Actual input tokens charged
56
+ def effective_input_tokens
57
+ cumulative_input_tokens - cumulative_cached_tokens
58
+ end
59
+
60
+ # Calculate total tokens used (input + output)
61
+ #
62
+ # @return [Integer] Total tokens used
63
+ def cumulative_total_tokens
64
+ cumulative_input_tokens + cumulative_output_tokens
65
+ end
66
+
67
+ # Calculate percentage of context window used
68
+ #
69
+ # @return [Float] Percentage (0.0 to 100.0)
70
+ def context_usage_percentage
71
+ limit = context_limit
72
+ return 0.0 if limit.nil? || limit.zero?
73
+
74
+ (cumulative_total_tokens.to_f / limit * 100).round(2)
75
+ end
76
+
77
+ # Calculate remaining tokens in context window
78
+ #
79
+ # @return [Integer, nil] Tokens remaining
80
+ def tokens_remaining
81
+ limit = context_limit
82
+ return if limit.nil?
83
+
84
+ limit - cumulative_total_tokens
85
+ end
86
+
87
+ # Compact the conversation history to reduce token usage
88
+ #
89
+ # @param options [Hash] Compression options
90
+ # @return [ContextCompactor::Metrics] Compression statistics
91
+ def compact_context(**options)
92
+ compactor = ContextCompactor.new(self, options)
93
+ compactor.compact
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -30,8 +30,8 @@ module SwarmSDK
30
30
  # 60% triggers automatic compression, 80%/90% are informational warnings
31
31
  CONTEXT_WARNING_THRESHOLDS = [60, 80, 90].freeze
32
32
 
33
- # Threshold at which automatic compression is triggered
34
- COMPRESSION_THRESHOLD = 60
33
+ # Backward compatibility alias - use Defaults module for new code
34
+ COMPRESSION_THRESHOLD = Defaults::Context::COMPRESSION_THRESHOLD_PERCENT
35
35
 
36
36
  attr_reader :name, :delegation_tools, :metadata, :warning_thresholds_hit, :swarm_id, :parent_swarm_id
37
37