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
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
module Memory
|
|
6
|
-
# Tool for writing content to memory storage
|
|
7
|
-
#
|
|
8
|
-
# Stores content in persistent, per-agent memory storage with metadata.
|
|
9
|
-
# Each agent has its own isolated memory storage that persists across sessions.
|
|
10
|
-
class MemoryWrite < RubyLLM::Tool
|
|
11
|
-
define_method(:name) { "MemoryWrite" }
|
|
12
|
-
|
|
13
|
-
description <<~DESC
|
|
14
|
-
Store content in memory for later retrieval.
|
|
15
|
-
Use this to save detailed outputs, analysis, or results that would
|
|
16
|
-
otherwise bloat tool responses. Only you (this agent) can access your memory.
|
|
17
|
-
|
|
18
|
-
IMPORTANT: You must determine the appropriate file_path based on the task you're performing.
|
|
19
|
-
Choose a logical, descriptive path that reflects the content type and purpose.
|
|
20
|
-
Examples: 'analysis/code_review', 'research/findings', 'parallel/batch_1/results', 'logs/debug_trace'
|
|
21
|
-
DESC
|
|
22
|
-
|
|
23
|
-
param :file_path,
|
|
24
|
-
desc: "File-path-like address you determine based on the task (e.g., 'analysis/report', 'parallel/batch1/task_0')",
|
|
25
|
-
required: true
|
|
26
|
-
|
|
27
|
-
param :content,
|
|
28
|
-
desc: "Content to store in memory (max 1MB per entry)",
|
|
29
|
-
required: true
|
|
30
|
-
|
|
31
|
-
param :title,
|
|
32
|
-
desc: "Brief title describing the content (shown in listings)",
|
|
33
|
-
required: true
|
|
34
|
-
|
|
35
|
-
class << self
|
|
36
|
-
# Create a MemoryWrite tool for a specific memory storage instance
|
|
37
|
-
#
|
|
38
|
-
# @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
|
|
39
|
-
# @return [MemoryWrite] Tool instance
|
|
40
|
-
def create_for_memory(memory_storage)
|
|
41
|
-
new(memory_storage)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Initialize with memory storage instance
|
|
46
|
-
#
|
|
47
|
-
# @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
|
|
48
|
-
def initialize(memory_storage)
|
|
49
|
-
super() # Call RubyLLM::Tool's initialize
|
|
50
|
-
@memory_storage = memory_storage
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Execute the tool
|
|
54
|
-
#
|
|
55
|
-
# @param file_path [String] Path to store content
|
|
56
|
-
# @param content [String] Content to store
|
|
57
|
-
# @param title [String] Brief title
|
|
58
|
-
# @return [String] Success message with path and size
|
|
59
|
-
def execute(file_path:, content:, title:)
|
|
60
|
-
entry = memory_storage.write(file_path: file_path, content: content, title: title)
|
|
61
|
-
"Stored at memory://#{file_path} (#{format_bytes(entry.size)})"
|
|
62
|
-
rescue ArgumentError => e
|
|
63
|
-
validation_error(e.message)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
attr_reader :memory_storage
|
|
69
|
-
|
|
70
|
-
def validation_error(message)
|
|
71
|
-
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Format bytes to human-readable size
|
|
75
|
-
#
|
|
76
|
-
# @param bytes [Integer] Number of bytes
|
|
77
|
-
# @return [String] Formatted size
|
|
78
|
-
def format_bytes(bytes)
|
|
79
|
-
if bytes >= 1_000_000
|
|
80
|
-
"#{(bytes.to_f / 1_000_000).round(1)}MB"
|
|
81
|
-
elsif bytes >= 1_000
|
|
82
|
-
"#{(bytes.to_f / 1_000).round(1)}KB"
|
|
83
|
-
else
|
|
84
|
-
"#{bytes}B"
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
module Stores
|
|
6
|
-
# MemoryStorage provides persistent, per-agent storage
|
|
7
|
-
#
|
|
8
|
-
# Features:
|
|
9
|
-
# - Per-agent: Each agent has its own isolated storage
|
|
10
|
-
# - Persistent: ALWAYS saves to JSON file
|
|
11
|
-
# - Path-based: Hierarchical organization using file-path-like addresses
|
|
12
|
-
# - Metadata-rich: Stores content + title + timestamp + size
|
|
13
|
-
# - Thread-safe: Mutex-protected operations
|
|
14
|
-
class MemoryStorage < Storage
|
|
15
|
-
# Initialize memory storage with required persistence
|
|
16
|
-
#
|
|
17
|
-
# @param persist_to [String] Path to JSON file for persistence (REQUIRED)
|
|
18
|
-
# @raise [ArgumentError] If persist_to is not provided
|
|
19
|
-
def initialize(persist_to:)
|
|
20
|
-
super() # Initialize parent Storage class
|
|
21
|
-
raise ArgumentError, "persist_to is required for MemoryStorage" if persist_to.nil? || persist_to.to_s.strip.empty?
|
|
22
|
-
|
|
23
|
-
@entries = {}
|
|
24
|
-
@total_size = 0
|
|
25
|
-
@persist_to = persist_to
|
|
26
|
-
@mutex = Mutex.new
|
|
27
|
-
|
|
28
|
-
# Load existing data if file exists
|
|
29
|
-
load_from_file if File.exist?(@persist_to)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Write content to memory storage
|
|
33
|
-
#
|
|
34
|
-
# @param file_path [String] Path to store content
|
|
35
|
-
# @param content [String] Content to store
|
|
36
|
-
# @param title [String] Brief title describing the content
|
|
37
|
-
# @raise [ArgumentError] If size limits are exceeded
|
|
38
|
-
# @return [Entry] The created entry
|
|
39
|
-
def write(file_path:, content:, title:)
|
|
40
|
-
@mutex.synchronize do
|
|
41
|
-
raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
|
|
42
|
-
raise ArgumentError, "content is required" if content.nil?
|
|
43
|
-
raise ArgumentError, "title is required" if title.nil? || title.to_s.strip.empty?
|
|
44
|
-
|
|
45
|
-
content_size = content.bytesize
|
|
46
|
-
|
|
47
|
-
# Check entry size limit
|
|
48
|
-
if content_size > MAX_ENTRY_SIZE
|
|
49
|
-
raise ArgumentError, "Content exceeds maximum size (#{format_bytes(MAX_ENTRY_SIZE)}). " \
|
|
50
|
-
"Current: #{format_bytes(content_size)}"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Calculate new total size
|
|
54
|
-
existing_entry = @entries[file_path]
|
|
55
|
-
existing_size = existing_entry ? existing_entry.size : 0
|
|
56
|
-
new_total_size = @total_size - existing_size + content_size
|
|
57
|
-
|
|
58
|
-
# Check total size limit
|
|
59
|
-
if new_total_size > MAX_TOTAL_SIZE
|
|
60
|
-
raise ArgumentError, "Memory storage full (#{format_bytes(MAX_TOTAL_SIZE)} limit). " \
|
|
61
|
-
"Current: #{format_bytes(@total_size)}, " \
|
|
62
|
-
"Would be: #{format_bytes(new_total_size)}. " \
|
|
63
|
-
"Clear old entries or use smaller content."
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Create entry
|
|
67
|
-
entry = Entry.new(
|
|
68
|
-
content: content,
|
|
69
|
-
title: title,
|
|
70
|
-
updated_at: Time.now,
|
|
71
|
-
size: content_size,
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
# Update storage
|
|
75
|
-
@entries[file_path] = entry
|
|
76
|
-
@total_size = new_total_size
|
|
77
|
-
|
|
78
|
-
# Always persist to file
|
|
79
|
-
save_to_file
|
|
80
|
-
|
|
81
|
-
entry
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Read content from memory storage
|
|
86
|
-
#
|
|
87
|
-
# @param file_path [String] Path to read from
|
|
88
|
-
# @raise [ArgumentError] If path not found
|
|
89
|
-
# @return [String] Content at the path
|
|
90
|
-
def read(file_path:)
|
|
91
|
-
raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
|
|
92
|
-
|
|
93
|
-
entry = @entries[file_path]
|
|
94
|
-
raise ArgumentError, "memory://#{file_path} not found" unless entry
|
|
95
|
-
|
|
96
|
-
entry.content
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Delete a specific entry
|
|
100
|
-
#
|
|
101
|
-
# @param file_path [String] Path to delete
|
|
102
|
-
# @raise [ArgumentError] If path not found
|
|
103
|
-
# @return [void]
|
|
104
|
-
def delete(file_path:)
|
|
105
|
-
@mutex.synchronize do
|
|
106
|
-
raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
|
|
107
|
-
|
|
108
|
-
entry = @entries[file_path]
|
|
109
|
-
raise ArgumentError, "memory://#{file_path} not found" unless entry
|
|
110
|
-
|
|
111
|
-
# Update total size
|
|
112
|
-
@total_size -= entry.size
|
|
113
|
-
|
|
114
|
-
# Remove entry
|
|
115
|
-
@entries.delete(file_path)
|
|
116
|
-
|
|
117
|
-
# Always persist to file
|
|
118
|
-
save_to_file
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# List memory storage entries, optionally filtered by prefix
|
|
123
|
-
#
|
|
124
|
-
# @param prefix [String, nil] Filter by path prefix
|
|
125
|
-
# @return [Array<Hash>] Array of entry metadata (path, title, size, updated_at)
|
|
126
|
-
def list(prefix: nil)
|
|
127
|
-
entries = @entries
|
|
128
|
-
|
|
129
|
-
# Filter by prefix if provided
|
|
130
|
-
if prefix && !prefix.empty?
|
|
131
|
-
entries = entries.select { |path, _| path.start_with?(prefix) }
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Return metadata sorted by path
|
|
135
|
-
entries.map do |path, entry|
|
|
136
|
-
{
|
|
137
|
-
path: path,
|
|
138
|
-
title: entry.title,
|
|
139
|
-
size: entry.size,
|
|
140
|
-
updated_at: entry.updated_at,
|
|
141
|
-
}
|
|
142
|
-
end.sort_by { |e| e[:path] }
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Search entries by glob pattern
|
|
146
|
-
#
|
|
147
|
-
# @param pattern [String] Glob pattern (e.g., "**/*.txt", "parallel/*/task_*")
|
|
148
|
-
# @return [Array<Hash>] Array of matching entry metadata, sorted by most recent first
|
|
149
|
-
def glob(pattern:)
|
|
150
|
-
raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?
|
|
151
|
-
|
|
152
|
-
# Convert glob pattern to regex
|
|
153
|
-
regex = glob_to_regex(pattern)
|
|
154
|
-
|
|
155
|
-
# Filter entries by pattern
|
|
156
|
-
matching_entries = @entries.select { |path, _| regex.match?(path) }
|
|
157
|
-
|
|
158
|
-
# Return metadata sorted by most recent first
|
|
159
|
-
matching_entries.map do |path, entry|
|
|
160
|
-
{
|
|
161
|
-
path: path,
|
|
162
|
-
title: entry.title,
|
|
163
|
-
size: entry.size,
|
|
164
|
-
updated_at: entry.updated_at,
|
|
165
|
-
}
|
|
166
|
-
end.sort_by { |e| -e[:updated_at].to_f }
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
# Search entry content by pattern
|
|
170
|
-
#
|
|
171
|
-
# @param pattern [String] Regular expression pattern to search for
|
|
172
|
-
# @param case_insensitive [Boolean] Whether to perform case-insensitive search
|
|
173
|
-
# @param output_mode [String] Output mode: "files_with_matches" (default), "content", or "count"
|
|
174
|
-
# @return [Array<Hash>, String] Results based on output_mode
|
|
175
|
-
def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
|
|
176
|
-
raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?
|
|
177
|
-
|
|
178
|
-
# Create regex from pattern
|
|
179
|
-
flags = case_insensitive ? Regexp::IGNORECASE : 0
|
|
180
|
-
regex = Regexp.new(pattern, flags)
|
|
181
|
-
|
|
182
|
-
case output_mode
|
|
183
|
-
when "files_with_matches"
|
|
184
|
-
# Return just the paths that match
|
|
185
|
-
matching_paths = @entries.select { |_path, entry| regex.match?(entry.content) }
|
|
186
|
-
.map { |path, _| path }
|
|
187
|
-
.sort
|
|
188
|
-
matching_paths
|
|
189
|
-
when "content"
|
|
190
|
-
# Return paths with matching lines, sorted by most recent first
|
|
191
|
-
results = []
|
|
192
|
-
@entries.each do |path, entry|
|
|
193
|
-
matching_lines = []
|
|
194
|
-
entry.content.each_line.with_index(1) do |line, line_num|
|
|
195
|
-
matching_lines << { line_number: line_num, content: line.chomp } if regex.match?(line)
|
|
196
|
-
end
|
|
197
|
-
results << { path: path, matches: matching_lines, updated_at: entry.updated_at } unless matching_lines.empty?
|
|
198
|
-
end
|
|
199
|
-
results.sort_by { |r| -r[:updated_at].to_f }.map { |r| r.except(:updated_at) }
|
|
200
|
-
when "count"
|
|
201
|
-
# Return paths with match counts, sorted by most recent first
|
|
202
|
-
results = []
|
|
203
|
-
@entries.each do |path, entry|
|
|
204
|
-
count = entry.content.scan(regex).size
|
|
205
|
-
results << { path: path, count: count, updated_at: entry.updated_at } if count > 0
|
|
206
|
-
end
|
|
207
|
-
results.sort_by { |r| -r[:updated_at].to_f }.map { |r| r.except(:updated_at) }
|
|
208
|
-
else
|
|
209
|
-
raise ArgumentError, "Invalid output_mode: #{output_mode}. Must be 'files_with_matches', 'content', or 'count'"
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# Clear all entries
|
|
214
|
-
#
|
|
215
|
-
# @return [void]
|
|
216
|
-
def clear
|
|
217
|
-
@mutex.synchronize do
|
|
218
|
-
@entries.clear
|
|
219
|
-
@total_size = 0
|
|
220
|
-
save_to_file
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
# Get current total size
|
|
225
|
-
#
|
|
226
|
-
# @return [Integer] Total size in bytes
|
|
227
|
-
attr_reader :total_size
|
|
228
|
-
|
|
229
|
-
# Get number of entries
|
|
230
|
-
#
|
|
231
|
-
# @return [Integer] Number of entries
|
|
232
|
-
def size
|
|
233
|
-
@entries.size
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
private
|
|
237
|
-
|
|
238
|
-
# Save memory storage data to JSON file
|
|
239
|
-
#
|
|
240
|
-
# @return [void]
|
|
241
|
-
def save_to_file
|
|
242
|
-
# Convert entries to serializable format
|
|
243
|
-
data = {
|
|
244
|
-
version: 1,
|
|
245
|
-
total_size: @total_size,
|
|
246
|
-
entries: @entries.transform_values do |entry|
|
|
247
|
-
{
|
|
248
|
-
content: entry.content,
|
|
249
|
-
title: entry.title,
|
|
250
|
-
updated_at: entry.updated_at.iso8601,
|
|
251
|
-
size: entry.size,
|
|
252
|
-
}
|
|
253
|
-
end,
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
# Ensure directory exists
|
|
257
|
-
dir = File.dirname(@persist_to)
|
|
258
|
-
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
259
|
-
|
|
260
|
-
# Write to file atomically (write to temp file, then rename)
|
|
261
|
-
temp_file = "#{@persist_to}.tmp"
|
|
262
|
-
File.write(temp_file, JSON.pretty_generate(data))
|
|
263
|
-
File.rename(temp_file, @persist_to)
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# Load memory storage data from JSON file
|
|
267
|
-
#
|
|
268
|
-
# @return [void]
|
|
269
|
-
def load_from_file
|
|
270
|
-
return unless File.exist?(@persist_to)
|
|
271
|
-
|
|
272
|
-
data = JSON.parse(File.read(@persist_to))
|
|
273
|
-
|
|
274
|
-
# Restore entries
|
|
275
|
-
@entries = data["entries"].transform_values do |entry_data|
|
|
276
|
-
Entry.new(
|
|
277
|
-
content: entry_data["content"],
|
|
278
|
-
title: entry_data["title"],
|
|
279
|
-
updated_at: Time.parse(entry_data["updated_at"]),
|
|
280
|
-
size: entry_data["size"],
|
|
281
|
-
)
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
# Restore total size
|
|
285
|
-
@total_size = data["total_size"]
|
|
286
|
-
rescue JSON::ParserError => e
|
|
287
|
-
# If file is corrupted, log warning and start fresh
|
|
288
|
-
warn("Warning: Failed to load memory storage from #{@persist_to}: #{e.message}. Starting with empty storage.")
|
|
289
|
-
@entries = {}
|
|
290
|
-
@total_size = 0
|
|
291
|
-
rescue StandardError => e
|
|
292
|
-
# If any other error occurs, log warning and start fresh
|
|
293
|
-
warn("Warning: Failed to load memory storage from #{@persist_to}: #{e.message}. Starting with empty storage.")
|
|
294
|
-
@entries = {}
|
|
295
|
-
@total_size = 0
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
end
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Tools
|
|
5
|
-
module Stores
|
|
6
|
-
# StorageReadTracker manages read-entry tracking for all agents
|
|
7
|
-
#
|
|
8
|
-
# This module maintains a global registry of which memory entries each agent
|
|
9
|
-
# has read during their conversation. This enables enforcement of the
|
|
10
|
-
# "read-before-edit" rule that ensures agents have context before modifying entries.
|
|
11
|
-
#
|
|
12
|
-
# Each agent maintains an independent set of read entries, keyed by agent identifier.
|
|
13
|
-
module StorageReadTracker
|
|
14
|
-
@read_entries = {}
|
|
15
|
-
@mutex = Mutex.new
|
|
16
|
-
|
|
17
|
-
class << self
|
|
18
|
-
# Register that an agent has read a storage entry
|
|
19
|
-
#
|
|
20
|
-
# @param agent_id [Symbol] The agent identifier
|
|
21
|
-
# @param entry_path [String] The storage entry path
|
|
22
|
-
def register_read(agent_id, entry_path)
|
|
23
|
-
@mutex.synchronize do
|
|
24
|
-
@read_entries[agent_id] ||= Set.new
|
|
25
|
-
@read_entries[agent_id] << entry_path
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Check if an agent has read a storage entry
|
|
30
|
-
#
|
|
31
|
-
# @param agent_id [Symbol] The agent identifier
|
|
32
|
-
# @param entry_path [String] The storage entry path
|
|
33
|
-
# @return [Boolean] true if the agent has read this entry
|
|
34
|
-
def entry_read?(agent_id, entry_path)
|
|
35
|
-
@mutex.synchronize do
|
|
36
|
-
return false unless @read_entries[agent_id]
|
|
37
|
-
|
|
38
|
-
@read_entries[agent_id].include?(entry_path)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Clear read history for an agent (useful for testing)
|
|
43
|
-
#
|
|
44
|
-
# @param agent_id [Symbol] The agent identifier
|
|
45
|
-
def clear(agent_id)
|
|
46
|
-
@mutex.synchronize do
|
|
47
|
-
@read_entries.delete(agent_id)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Clear all read history (useful for testing)
|
|
52
|
-
def clear_all
|
|
53
|
-
@mutex.synchronize do
|
|
54
|
-
@read_entries.clear
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|