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.
- checksums.yaml +4 -4
- data/lib/claude_swarm/claude_mcp_server.rb +1 -0
- data/lib/claude_swarm/cli.rb +5 -18
- data/lib/claude_swarm/configuration.rb +30 -19
- data/lib/claude_swarm/mcp_generator.rb +5 -10
- data/lib/claude_swarm/openai/chat_completion.rb +4 -12
- data/lib/claude_swarm/openai/executor.rb +3 -1
- data/lib/claude_swarm/openai/responses.rb +13 -32
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
- data/lib/swarm_cli/commands/run.rb +2 -2
- data/lib/swarm_cli/config_loader.rb +14 -14
- data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
- data/lib/swarm_cli/interactive_repl.rb +11 -5
- data/lib/swarm_cli/ui/icons.rb +0 -23
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/adapters/base.rb +4 -4
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
- data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
- data/lib/swarm_memory/integration/cli_registration.rb +3 -2
- data/lib/swarm_memory/integration/sdk_plugin.rb +98 -12
- data/lib/swarm_memory/tools/memory_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_read.rb +3 -3
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +6 -1
- data/lib/swarm_sdk/agent/builder.rb +91 -0
- data/lib/swarm_sdk/agent/chat.rb +540 -925
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +33 -79
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +147 -39
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +22 -38
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
- data/lib/swarm_sdk/agent/context.rb +8 -4
- data/lib/swarm_sdk/agent/context_manager.rb +6 -0
- data/lib/swarm_sdk/agent/definition.rb +79 -174
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +182 -0
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
- data/lib/swarm_sdk/builders/base_builder.rb +409 -0
- data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
- data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
- data/lib/swarm_sdk/concerns/validatable.rb +55 -0
- data/lib/swarm_sdk/configuration/parser.rb +353 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +100 -261
- data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
- data/lib/swarm_sdk/context_compactor.rb +6 -11
- data/lib/swarm_sdk/context_management/builder.rb +128 -0
- data/lib/swarm_sdk/context_management/context.rb +328 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +199 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
- data/lib/swarm_sdk/log_collector.rb +192 -16
- data/lib/swarm_sdk/log_stream.rb +66 -8
- data/lib/swarm_sdk/model_aliases.json +4 -1
- data/lib/swarm_sdk/node_context.rb +1 -1
- data/lib/swarm_sdk/observer/builder.rb +81 -0
- data/lib/swarm_sdk/observer/config.rb +45 -0
- data/lib/swarm_sdk/observer/manager.rb +236 -0
- data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
- data/lib/swarm_sdk/plugin.rb +93 -3
- data/lib/swarm_sdk/proc_helpers.rb +53 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
- data/lib/swarm_sdk/restore_result.rb +65 -0
- data/lib/swarm_sdk/snapshot.rb +156 -0
- data/lib/swarm_sdk/snapshot_from_events.rb +397 -0
- data/lib/swarm_sdk/state_restorer.rb +476 -0
- data/lib/swarm_sdk/state_snapshot.rb +334 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +428 -79
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
- data/lib/swarm_sdk/swarm/builder.rb +69 -407
- data/lib/swarm_sdk/swarm/executor.rb +213 -0
- data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +88 -149
- data/lib/swarm_sdk/swarm.rb +366 -631
- data/lib/swarm_sdk/swarm_loader.rb +145 -0
- data/lib/swarm_sdk/swarm_registry.rb +136 -0
- data/lib/swarm_sdk/tools/bash.rb +11 -3
- data/lib/swarm_sdk/tools/delegate.rb +127 -24
- data/lib/swarm_sdk/tools/edit.rb +8 -13
- data/lib/swarm_sdk/tools/glob.rb +9 -1
- data/lib/swarm_sdk/tools/grep.rb +7 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
- data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
- data/lib/swarm_sdk/tools/read.rb +28 -18
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +53 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/think.rb +4 -1
- data/lib/swarm_sdk/tools/todo_write.rb +27 -8
- data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/utils.rb +18 -0
- data/lib/swarm_sdk/validation_result.rb +33 -0
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +34 -9
- data/lib/swarm_sdk/workflow/builder.rb +143 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +42 -21
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
- data/lib/swarm_sdk/workflow.rb +554 -0
- data/lib/swarm_sdk.rb +393 -22
- metadata +51 -16
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_sdk/node_orchestrator.rb +0 -591
- 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
|
|
@@ -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
|
-
|
|
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
|
-
#
|
|
43
|
-
TODOWRITE_REMINDER_INTERVAL =
|
|
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.
|
|
34
|
+
!chat.has_user_message?
|
|
52
35
|
end
|
|
53
36
|
|
|
54
|
-
# Inject first message reminders
|
|
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.
|
|
42
|
+
# 1. User's actual prompt
|
|
61
43
|
# 2. Toolset reminder (list of available tools)
|
|
62
|
-
# 3.
|
|
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
|
-
|
|
52
|
+
parts = [
|
|
72
53
|
prompt,
|
|
73
|
-
BEFORE_FIRST_MESSAGE_REMINDER,
|
|
74
54
|
build_toolset_reminder(chat),
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|