swarm_sdk 2.0.6 → 2.0.7
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/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
- data/lib/swarm_sdk/agent/builder.rb +16 -42
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +43 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +41 -3
- data/lib/swarm_sdk/agent/chat.rb +426 -61
- data/lib/swarm_sdk/agent/context.rb +5 -1
- data/lib/swarm_sdk/agent/context_manager.rb +309 -0
- data/lib/swarm_sdk/agent/definition.rb +57 -24
- data/lib/swarm_sdk/plugin.rb +147 -0
- data/lib/swarm_sdk/plugin_registry.rb +101 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +7 -1
- data/lib/swarm_sdk/swarm/agent_initializer.rb +80 -12
- data/lib/swarm_sdk/swarm/tool_configurator.rb +116 -44
- data/lib/swarm_sdk/swarm.rb +44 -8
- data/lib/swarm_sdk/tools/clock.rb +44 -0
- data/lib/swarm_sdk/tools/grep.rb +16 -19
- data/lib/swarm_sdk/tools/registry.rb +23 -12
- data/lib/swarm_sdk/tools/todo_write.rb +1 -1
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +4 -0
- metadata +7 -12
- data/lib/swarm_sdk/prompts/memory.md.erb +0 -480
- data/lib/swarm_sdk/tools/memory/memory_delete.rb +0 -64
- data/lib/swarm_sdk/tools/memory/memory_edit.rb +0 -145
- data/lib/swarm_sdk/tools/memory/memory_glob.rb +0 -94
- data/lib/swarm_sdk/tools/memory/memory_grep.rb +0 -147
- data/lib/swarm_sdk/tools/memory/memory_multi_edit.rb +0 -228
- data/lib/swarm_sdk/tools/memory/memory_read.rb +0 -82
- data/lib/swarm_sdk/tools/memory/memory_write.rb +0 -90
- data/lib/swarm_sdk/tools/stores/memory_storage.rb +0 -300
- data/lib/swarm_sdk/tools/stores/storage_read_tracker.rb +0 -61
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 782e917bdddafcaa3ac68b97b674f4a1ccc6bfcb7299be6f51103e0ea1cfc94e
|
|
4
|
+
data.tar.gz: ea68356da094da208e4ec5dadab749cf9abf31b40412ed1be149377c91fc6c95
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0909fd0fb175fcede89e24f57da40176eea673c228c9a29da7ba00a9be65316c059d21f86d389e8662aeeaa593a743a5abd15f9988dfa99d38fb4b663285a755'
|
|
7
|
+
data.tar.gz: 23bf74e22fbf31b8a094193b818935e53ec93b25745d3f815f54205a104cc22ce9450ba15f006920d5f540945486afffdcee6c44c4bc9a42c080072ee00a7563
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# LLM Call Retry Logic
|
|
2
|
+
|
|
3
|
+
## Feature
|
|
4
|
+
|
|
5
|
+
SwarmSDK automatically retries failed LLM API calls to handle transient failures.
|
|
6
|
+
|
|
7
|
+
## Configuration
|
|
8
|
+
|
|
9
|
+
**Defaults:**
|
|
10
|
+
- Max retries: 10
|
|
11
|
+
- Delay: 10 seconds (fixed, no exponential backoff)
|
|
12
|
+
- Retries ALL StandardError exceptions
|
|
13
|
+
|
|
14
|
+
## Implementation
|
|
15
|
+
|
|
16
|
+
**Location:** `lib/swarm_sdk/agent/chat.rb:768-801`
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
def call_llm_with_retry(max_retries: 10, delay: 10, &block)
|
|
20
|
+
attempts = 0
|
|
21
|
+
loop do
|
|
22
|
+
attempts += 1
|
|
23
|
+
begin
|
|
24
|
+
return yield
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
raise if attempts >= max_retries
|
|
27
|
+
|
|
28
|
+
RubyLLM.logger.warn("SwarmSDK: LLM call failed (attempt #{attempts}/#{max_retries})")
|
|
29
|
+
sleep(delay)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Error Types Handled
|
|
36
|
+
|
|
37
|
+
- `Faraday::ConnectionFailed` - Network connection issues
|
|
38
|
+
- `Faraday::TimeoutError` - Request timeouts
|
|
39
|
+
- `RubyLLM::APIError` - API errors (500s, etc.)
|
|
40
|
+
- `RubyLLM::RateLimitError` - Rate limit errors
|
|
41
|
+
- `RubyLLM::BadRequestError` - Usually not transient, but retries anyway
|
|
42
|
+
- Any other `StandardError` - Catches proxy issues, DNS failures, etc.
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
**Automatic - No Configuration Needed:**
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
swarm = SwarmSDK.build do
|
|
50
|
+
agent :my_agent do
|
|
51
|
+
model "gpt-4"
|
|
52
|
+
base_url "http://proxy.example.com/v1" # Can fail
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Automatically retries on failure
|
|
57
|
+
response = swarm.execute("Do something")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Logging
|
|
61
|
+
|
|
62
|
+
**On Retry:**
|
|
63
|
+
```
|
|
64
|
+
WARN: SwarmSDK: LLM call failed (attempt 1/10): Faraday::ConnectionFailed: Connection failed
|
|
65
|
+
WARN: SwarmSDK: Retrying in 10 seconds...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**On Max Retries:**
|
|
69
|
+
```
|
|
70
|
+
ERROR: SwarmSDK: LLM call failed after 10 attempts: Faraday::ConnectionFailed: Connection failed
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Testing
|
|
74
|
+
|
|
75
|
+
Retry logic has been verified through:
|
|
76
|
+
- ✅ All 728 SwarmSDK tests passing
|
|
77
|
+
- ✅ Manual testing with failing proxies
|
|
78
|
+
- ✅ Evaluation harnesses (assistant/retrieval modes)
|
|
79
|
+
|
|
80
|
+
**Note:** Direct unit tests would require reflection (`instance_variable_set`) which violates security policy. The retry logic is tested implicitly through integration tests and real usage.
|
|
81
|
+
|
|
82
|
+
## Behavior
|
|
83
|
+
|
|
84
|
+
**Scenario 1: Transient failure**
|
|
85
|
+
```
|
|
86
|
+
Attempt 1: ConnectionFailed
|
|
87
|
+
→ Wait 10s
|
|
88
|
+
Attempt 2: ConnectionFailed
|
|
89
|
+
→ Wait 10s
|
|
90
|
+
Attempt 3: Success
|
|
91
|
+
→ Returns response
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Scenario 2: Persistent failure**
|
|
95
|
+
```
|
|
96
|
+
Attempt 1-10: All fail
|
|
97
|
+
→ Raises original error after attempt 10
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Scenario 3: Immediate success**
|
|
101
|
+
```
|
|
102
|
+
Attempt 1: Success
|
|
103
|
+
→ Returns response (no retry needed)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Why No Exponential Backoff
|
|
107
|
+
|
|
108
|
+
**Design Decision:** Fixed 10-second delay
|
|
109
|
+
|
|
110
|
+
**Rationale:**
|
|
111
|
+
- Simpler implementation
|
|
112
|
+
- Predictable retry duration (max 100 seconds)
|
|
113
|
+
- Transient proxy/network issues typically resolve within seconds
|
|
114
|
+
- Rate limit errors are caught by provider-specific handling
|
|
115
|
+
- User explicitly requested fixed delays
|
|
116
|
+
|
|
117
|
+
**Total max time:** 10 retries × 10 seconds = 100 seconds maximum
|
|
118
|
+
|
|
119
|
+
## Future Enhancements (If Needed)
|
|
120
|
+
|
|
121
|
+
- [ ] Configurable retry count per agent
|
|
122
|
+
- [ ] Configurable delay per agent
|
|
123
|
+
- [ ] Selective retry based on error type
|
|
124
|
+
- [ ] Exponential backoff option
|
|
125
|
+
- [ ] Circuit breaker pattern
|
|
126
|
+
|
|
127
|
+
**Current State:** Production-ready with sensible defaults for proxy/network resilience.
|
|
@@ -2,32 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module SwarmSDK
|
|
4
4
|
module Agent
|
|
5
|
-
# Configuration for agent memory
|
|
6
|
-
class MemoryConfig
|
|
7
|
-
def initialize
|
|
8
|
-
@adapter = :filesystem # Default adapter
|
|
9
|
-
@directory = nil
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# DSL method to set/get adapter
|
|
13
|
-
def adapter(value = nil)
|
|
14
|
-
return @adapter if value.nil?
|
|
15
|
-
|
|
16
|
-
@adapter = value.to_sym
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# DSL method to set/get directory
|
|
20
|
-
def directory(value = nil)
|
|
21
|
-
return @directory if value.nil?
|
|
22
|
-
|
|
23
|
-
@directory = value
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def enabled?
|
|
27
|
-
!@directory.nil?
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
5
|
# Builder provides fluent API for configuring agents
|
|
32
6
|
#
|
|
33
7
|
# This class offers a Ruby DSL for defining agents with a clean, readable syntax.
|
|
@@ -160,10 +134,23 @@ module SwarmSDK
|
|
|
160
134
|
# @example Disable all default tools
|
|
161
135
|
# disable_default_tools true
|
|
162
136
|
#
|
|
163
|
-
# @example Disable specific tools
|
|
137
|
+
# @example Disable specific tools (array)
|
|
164
138
|
# disable_default_tools [:Think, :TodoWrite]
|
|
165
|
-
|
|
166
|
-
|
|
139
|
+
#
|
|
140
|
+
# @example Disable specific tools (separate arguments)
|
|
141
|
+
# disable_default_tools :Think, :TodoWrite
|
|
142
|
+
def disable_default_tools(*tools)
|
|
143
|
+
# Handle different argument forms
|
|
144
|
+
@disable_default_tools = case tools.size
|
|
145
|
+
when 0
|
|
146
|
+
nil
|
|
147
|
+
when 1
|
|
148
|
+
# Single argument: could be true/false/array
|
|
149
|
+
tools.first
|
|
150
|
+
else
|
|
151
|
+
# Multiple arguments: treat as array of tool names
|
|
152
|
+
tools.map(&:to_sym)
|
|
153
|
+
end
|
|
167
154
|
end
|
|
168
155
|
|
|
169
156
|
# Set bypass_permissions flag
|
|
@@ -245,19 +232,6 @@ module SwarmSDK
|
|
|
245
232
|
@directory = dir
|
|
246
233
|
end
|
|
247
234
|
|
|
248
|
-
# Configure persistent memory for this agent
|
|
249
|
-
#
|
|
250
|
-
# @example
|
|
251
|
-
# memory do
|
|
252
|
-
# adapter :filesystem # default
|
|
253
|
-
# directory ".swarm/agent-memory"
|
|
254
|
-
# end
|
|
255
|
-
def memory(&block)
|
|
256
|
-
@memory_config = MemoryConfig.new
|
|
257
|
-
@memory_config.instance_eval(&block) if block_given?
|
|
258
|
-
@memory_config
|
|
259
|
-
end
|
|
260
|
-
|
|
261
235
|
# Set delegation targets
|
|
262
236
|
def delegates_to(*agent_names)
|
|
263
237
|
@delegates_to.concat(agent_names)
|
|
@@ -74,6 +74,11 @@ module SwarmSDK
|
|
|
74
74
|
# Mark threshold as hit and emit warning
|
|
75
75
|
@agent_context.hit_warning_threshold?(threshold)
|
|
76
76
|
|
|
77
|
+
# Trigger automatic compression at 60% threshold
|
|
78
|
+
if threshold == Context::COMPRESSION_THRESHOLD
|
|
79
|
+
trigger_automatic_compression
|
|
80
|
+
end
|
|
81
|
+
|
|
77
82
|
LogStream.emit(
|
|
78
83
|
type: "context_limit_warning",
|
|
79
84
|
agent: @agent_context.name,
|
|
@@ -84,6 +89,7 @@ module SwarmSDK
|
|
|
84
89
|
tokens_remaining: @chat.tokens_remaining,
|
|
85
90
|
context_limit: @chat.context_limit,
|
|
86
91
|
metadata: @agent_context.metadata,
|
|
92
|
+
compression_triggered: threshold == Context::COMPRESSION_THRESHOLD,
|
|
87
93
|
)
|
|
88
94
|
end
|
|
89
95
|
end
|
|
@@ -266,6 +272,43 @@ module SwarmSDK
|
|
|
266
272
|
)
|
|
267
273
|
end
|
|
268
274
|
end
|
|
275
|
+
|
|
276
|
+
# Trigger automatic message compression
|
|
277
|
+
#
|
|
278
|
+
# Called when context usage crosses 60% threshold. Compresses old tool
|
|
279
|
+
# results to save context window space while preserving accuracy.
|
|
280
|
+
#
|
|
281
|
+
# @return [void]
|
|
282
|
+
def trigger_automatic_compression
|
|
283
|
+
return unless @chat.respond_to?(:context_manager)
|
|
284
|
+
|
|
285
|
+
# Calculate tokens before compression
|
|
286
|
+
tokens_before = @chat.cumulative_total_tokens
|
|
287
|
+
|
|
288
|
+
# Get compressed messages from ContextManager
|
|
289
|
+
compressed = @chat.context_manager.auto_compress_on_threshold(@chat.messages, keep_recent: 10)
|
|
290
|
+
|
|
291
|
+
# Count how many messages were actually compressed
|
|
292
|
+
messages_compressed = compressed.count do |msg|
|
|
293
|
+
msg.content.to_s.include?("[truncated for context management]")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Replace messages array with compressed version
|
|
297
|
+
@chat.messages.clear
|
|
298
|
+
compressed.each { |msg| @chat.messages << msg }
|
|
299
|
+
|
|
300
|
+
# Log compression event
|
|
301
|
+
LogStream.emit(
|
|
302
|
+
type: "context_compression",
|
|
303
|
+
agent: @agent_context.name,
|
|
304
|
+
total_messages: @chat.messages.size,
|
|
305
|
+
messages_compressed: messages_compressed,
|
|
306
|
+
tokens_before: tokens_before,
|
|
307
|
+
current_usage: "#{@chat.context_usage_percentage}%",
|
|
308
|
+
compression_strategy: "progressive_tool_result_compression",
|
|
309
|
+
keep_recent: 10,
|
|
310
|
+
) if LogStream.enabled?
|
|
311
|
+
end
|
|
269
312
|
end
|
|
270
313
|
end
|
|
271
314
|
end
|
|
@@ -56,13 +56,51 @@ module SwarmSDK
|
|
|
56
56
|
# This manually constructs the first message sequence with system reminders
|
|
57
57
|
# sandwiching the actual user prompt.
|
|
58
58
|
#
|
|
59
|
+
# Sequence:
|
|
60
|
+
# 1. BEFORE_FIRST_MESSAGE_REMINDER (general reminders)
|
|
61
|
+
# 2. Toolset reminder (list of available tools)
|
|
62
|
+
# 3. User's actual prompt
|
|
63
|
+
# 4. AFTER_FIRST_MESSAGE_REMINDER (todo list reminder)
|
|
64
|
+
#
|
|
59
65
|
# @param chat [Agent::Chat] The chat instance
|
|
60
66
|
# @param prompt [String] The user's actual prompt
|
|
61
67
|
# @return [void]
|
|
62
68
|
def inject_first_message_reminders(chat, prompt)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
# Build user message with embedded reminders
|
|
70
|
+
# Reminders are embedded in the content, not separate messages
|
|
71
|
+
full_content = [
|
|
72
|
+
prompt,
|
|
73
|
+
BEFORE_FIRST_MESSAGE_REMINDER,
|
|
74
|
+
build_toolset_reminder(chat),
|
|
75
|
+
AFTER_FIRST_MESSAGE_REMINDER,
|
|
76
|
+
].join("\n\n")
|
|
77
|
+
|
|
78
|
+
# Extract reminders and add clean prompt to persistent history
|
|
79
|
+
reminders = chat.context_manager.extract_system_reminders(full_content)
|
|
80
|
+
clean_prompt = chat.context_manager.strip_system_reminders(full_content)
|
|
81
|
+
|
|
82
|
+
# Store clean prompt (without reminders) in conversation history
|
|
83
|
+
chat.add_message(role: :user, content: clean_prompt)
|
|
84
|
+
|
|
85
|
+
# Track reminders to embed in this message when sending to LLM
|
|
86
|
+
reminders.each do |reminder|
|
|
87
|
+
chat.context_manager.add_ephemeral_reminder(reminder, messages_array: chat.messages)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Build toolset reminder listing all available tools
|
|
92
|
+
#
|
|
93
|
+
# @param chat [Agent::Chat] The chat instance
|
|
94
|
+
# @return [String] System reminder with tool list
|
|
95
|
+
def build_toolset_reminder(chat)
|
|
96
|
+
tools_list = chat.tools.values.map(&:name).sort
|
|
97
|
+
|
|
98
|
+
reminder = "<system-reminder>\n"
|
|
99
|
+
reminder += "Tools available: #{tools_list.join(", ")}\n\n"
|
|
100
|
+
reminder += "Only use tools from this list. Do not attempt to use tools that are not listed here.\n"
|
|
101
|
+
reminder += "</system-reminder>"
|
|
102
|
+
|
|
103
|
+
reminder
|
|
66
104
|
end
|
|
67
105
|
|
|
68
106
|
# Check if we should inject a periodic TodoWrite reminder
|