swarm_sdk 2.7.13 → 3.0.0.alpha1
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/ruby_llm_patches/chat_callbacks_patch.rb +43 -22
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +6 -0
- data/lib/swarm_sdk/ruby_llm_patches/mcp_ssl_patch.rb +144 -0
- data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +3 -4
- data/lib/swarm_sdk/v3/agent.rb +1165 -0
- data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
- data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
- data/lib/swarm_sdk/v3/configuration.rb +490 -0
- data/lib/swarm_sdk/v3/debug_log.rb +86 -0
- data/lib/swarm_sdk/v3/event_stream.rb +130 -0
- data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
- data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
- data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
- data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
- data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
- data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
- data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
- data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
- data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
- data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
- data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
- data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
- data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
- data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
- data/lib/swarm_sdk/v3/memory/card.rb +206 -0
- data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
- data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
- data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
- data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
- data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
- data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
- data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
- data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
- data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
- data/lib/swarm_sdk/v3/memory/store.rb +489 -0
- data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
- data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
- data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
- data/lib/swarm_sdk/v3/tools/base.rb +80 -0
- data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
- data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
- data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
- data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
- data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
- data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
- data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
- data/lib/swarm_sdk/v3/tools/read.rb +181 -0
- data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
- data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
- data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
- data/lib/swarm_sdk/v3/tools/think.rb +88 -0
- data/lib/swarm_sdk/v3/tools/write.rb +87 -0
- data/lib/swarm_sdk/v3.rb +145 -0
- metadata +84 -148
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
- data/lib/swarm_sdk/agent/builder.rb +0 -680
- data/lib/swarm_sdk/agent/chat.rb +0 -1432
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
- data/lib/swarm_sdk/agent/context.rb +0 -115
- data/lib/swarm_sdk/agent/context_manager.rb +0 -315
- data/lib/swarm_sdk/agent/definition.rb +0 -581
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
- data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
- data/lib/swarm_sdk/agent_registry.rb +0 -146
- data/lib/swarm_sdk/builders/base_builder.rb +0 -553
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
- data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
- data/lib/swarm_sdk/concerns/validatable.rb +0 -55
- data/lib/swarm_sdk/config.rb +0 -367
- data/lib/swarm_sdk/configuration/parser.rb +0 -397
- data/lib/swarm_sdk/configuration/translator.rb +0 -283
- data/lib/swarm_sdk/configuration.rb +0 -165
- data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
- data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
- data/lib/swarm_sdk/context_compactor.rb +0 -335
- data/lib/swarm_sdk/context_management/builder.rb +0 -128
- data/lib/swarm_sdk/context_management/context.rb +0 -328
- data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
- data/lib/swarm_sdk/defaults.rb +0 -251
- data/lib/swarm_sdk/events_to_messages.rb +0 -199
- data/lib/swarm_sdk/hooks/adapter.rb +0 -359
- data/lib/swarm_sdk/hooks/context.rb +0 -197
- data/lib/swarm_sdk/hooks/definition.rb +0 -80
- data/lib/swarm_sdk/hooks/error.rb +0 -29
- data/lib/swarm_sdk/hooks/executor.rb +0 -146
- data/lib/swarm_sdk/hooks/registry.rb +0 -147
- data/lib/swarm_sdk/hooks/result.rb +0 -150
- data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
- data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
- data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
- data/lib/swarm_sdk/log_collector.rb +0 -227
- data/lib/swarm_sdk/log_stream.rb +0 -127
- data/lib/swarm_sdk/markdown_parser.rb +0 -75
- data/lib/swarm_sdk/model_aliases.json +0 -8
- data/lib/swarm_sdk/models.json +0 -44002
- data/lib/swarm_sdk/models.rb +0 -161
- data/lib/swarm_sdk/node_context.rb +0 -245
- data/lib/swarm_sdk/observer/builder.rb +0 -81
- data/lib/swarm_sdk/observer/config.rb +0 -45
- data/lib/swarm_sdk/observer/manager.rb +0 -236
- data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
- data/lib/swarm_sdk/permissions/config.rb +0 -239
- data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
- data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
- data/lib/swarm_sdk/permissions/validator.rb +0 -173
- data/lib/swarm_sdk/permissions_builder.rb +0 -122
- data/lib/swarm_sdk/plugin.rb +0 -309
- data/lib/swarm_sdk/plugin_registry.rb +0 -101
- data/lib/swarm_sdk/proc_helpers.rb +0 -53
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -212
- data/lib/swarm_sdk/snapshot.rb +0 -156
- data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
- data/lib/swarm_sdk/state_restorer.rb +0 -476
- data/lib/swarm_sdk/state_snapshot.rb +0 -334
- data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -195
- data/lib/swarm_sdk/swarm/builder.rb +0 -256
- data/lib/swarm_sdk/swarm/executor.rb +0 -290
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -151
- data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -360
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -270
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
- data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
- data/lib/swarm_sdk/swarm.rb +0 -843
- data/lib/swarm_sdk/swarm_loader.rb +0 -145
- data/lib/swarm_sdk/swarm_registry.rb +0 -136
- data/lib/swarm_sdk/tools/base.rb +0 -63
- data/lib/swarm_sdk/tools/bash.rb +0 -280
- data/lib/swarm_sdk/tools/clock.rb +0 -46
- data/lib/swarm_sdk/tools/delegate.rb +0 -389
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
- data/lib/swarm_sdk/tools/edit.rb +0 -145
- data/lib/swarm_sdk/tools/glob.rb +0 -166
- data/lib/swarm_sdk/tools/grep.rb +0 -235
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
- data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
- data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
- data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
- data/lib/swarm_sdk/tools/read.rb +0 -261
- data/lib/swarm_sdk/tools/registry.rb +0 -205
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
- data/lib/swarm_sdk/tools/think.rb +0 -100
- data/lib/swarm_sdk/tools/todo_write.rb +0 -237
- data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
- data/lib/swarm_sdk/tools/write.rb +0 -112
- data/lib/swarm_sdk/transcript_builder.rb +0 -278
- data/lib/swarm_sdk/utils.rb +0 -68
- data/lib/swarm_sdk/validation_result.rb +0 -33
- data/lib/swarm_sdk/version.rb +0 -5
- data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
- data/lib/swarm_sdk/workflow/builder.rb +0 -227
- data/lib/swarm_sdk/workflow/executor.rb +0 -497
- data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
- data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
- data/lib/swarm_sdk/workflow.rb +0 -589
- data/lib/swarm_sdk.rb +0 -718
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
# Glob tool for fast file pattern matching
|
|
7
|
+
#
|
|
8
|
+
# Finds files and directories matching glob patterns, sorted by modification time.
|
|
9
|
+
class Glob < Base
|
|
10
|
+
class << self
|
|
11
|
+
# @return [Array<Symbol>] Constructor requirements
|
|
12
|
+
def creation_requirements
|
|
13
|
+
[:directory]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
description <<~DESC
|
|
18
|
+
Fast file pattern matching tool.
|
|
19
|
+
|
|
20
|
+
Supports glob patterns like "**/*.js" or "src/**/*.ts".
|
|
21
|
+
Returns matching file paths sorted by modification time (most recent first).
|
|
22
|
+
DESC
|
|
23
|
+
|
|
24
|
+
param :pattern,
|
|
25
|
+
type: "string",
|
|
26
|
+
desc: "The glob pattern to match files against",
|
|
27
|
+
required: true
|
|
28
|
+
|
|
29
|
+
param :path,
|
|
30
|
+
type: "string",
|
|
31
|
+
desc: "Directory to search in. Defaults to working directory.",
|
|
32
|
+
required: false
|
|
33
|
+
|
|
34
|
+
# @param directory [String] Working directory for pattern matching
|
|
35
|
+
def initialize(directory:)
|
|
36
|
+
super()
|
|
37
|
+
@directory = File.expand_path(directory)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Execute glob search
|
|
41
|
+
#
|
|
42
|
+
# @param pattern [String] Glob pattern
|
|
43
|
+
# @param path [String, nil] Search directory
|
|
44
|
+
# @return [String] Matching paths or error
|
|
45
|
+
def execute(pattern:, path: nil)
|
|
46
|
+
return validation_error("pattern is required") if pattern.nil? || pattern.to_s.strip.empty?
|
|
47
|
+
|
|
48
|
+
search_path = resolve_search_path(path)
|
|
49
|
+
return search_path if search_path.start_with?("<tool_use_error>")
|
|
50
|
+
|
|
51
|
+
full_pattern = pattern.start_with?("/") ? pattern : File.join(search_path, pattern)
|
|
52
|
+
matches = Dir.glob(full_pattern, File::FNM_DOTMATCH)
|
|
53
|
+
|
|
54
|
+
matches.reject! do |f|
|
|
55
|
+
basename = File.basename(f.chomp("/"))
|
|
56
|
+
basename == "." || basename == ".."
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return "No matches found for pattern: #{pattern}" if matches.empty?
|
|
60
|
+
|
|
61
|
+
matches.sort_by! { |f| -File.mtime(f).to_i }
|
|
62
|
+
|
|
63
|
+
max_results = Configuration.instance.glob_result_limit
|
|
64
|
+
truncated = matches.count > max_results
|
|
65
|
+
matches = matches.take(max_results) if truncated
|
|
66
|
+
|
|
67
|
+
output = matches.join("\n")
|
|
68
|
+
output += "\n\n<system-reminder>Results limited to first #{max_results} matches.</system-reminder>" if truncated
|
|
69
|
+
output
|
|
70
|
+
rescue Errno::EACCES => e
|
|
71
|
+
error("Permission denied: #{e.message}")
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
error("Failed to execute glob: #{e.class.name} - #{e.message}")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Resolve search path with validation
|
|
79
|
+
#
|
|
80
|
+
# @param path [String, nil] User-provided path
|
|
81
|
+
# @return [String] Resolved absolute path or validation error
|
|
82
|
+
def resolve_search_path(path)
|
|
83
|
+
if path && !path.to_s.strip.empty?
|
|
84
|
+
return validation_error("Invalid path value.") if ["undefined", "null"].include?(path.to_s.strip.downcase)
|
|
85
|
+
return validation_error("Path does not exist: #{path}") unless File.exist?(path)
|
|
86
|
+
return validation_error("Path is not a directory: #{path}") unless File.directory?(path)
|
|
87
|
+
|
|
88
|
+
resolve_path(path)
|
|
89
|
+
else
|
|
90
|
+
@directory
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
# Grep tool for searching file contents using ripgrep
|
|
7
|
+
#
|
|
8
|
+
# Powerful search with regex support, context lines, and file filtering.
|
|
9
|
+
class Grep < Base
|
|
10
|
+
class << self
|
|
11
|
+
# @return [Array<Symbol>] Constructor requirements
|
|
12
|
+
def creation_requirements
|
|
13
|
+
[:directory]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
description <<~DESC
|
|
18
|
+
Search file contents using ripgrep.
|
|
19
|
+
|
|
20
|
+
Supports regex patterns, file type filtering, and context lines.
|
|
21
|
+
Output modes: "content" (matching lines), "files_with_matches" (file paths, default), "count" (match counts).
|
|
22
|
+
DESC
|
|
23
|
+
|
|
24
|
+
param :pattern,
|
|
25
|
+
type: "string",
|
|
26
|
+
desc: "Regular expression pattern to search for",
|
|
27
|
+
required: true
|
|
28
|
+
|
|
29
|
+
param :path,
|
|
30
|
+
type: "string",
|
|
31
|
+
desc: "File or directory to search in. Defaults to working directory.",
|
|
32
|
+
required: false
|
|
33
|
+
|
|
34
|
+
param :glob,
|
|
35
|
+
type: "string",
|
|
36
|
+
desc: 'Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}")',
|
|
37
|
+
required: false
|
|
38
|
+
|
|
39
|
+
param :type,
|
|
40
|
+
type: "string",
|
|
41
|
+
desc: "File type to search (e.g. js, py, ruby, rust)",
|
|
42
|
+
required: false
|
|
43
|
+
|
|
44
|
+
param :output_mode,
|
|
45
|
+
type: "string",
|
|
46
|
+
desc: '"content", "files_with_matches" (default), or "count"',
|
|
47
|
+
required: false
|
|
48
|
+
|
|
49
|
+
param :case_insensitive,
|
|
50
|
+
type: "boolean",
|
|
51
|
+
desc: "Case insensitive search",
|
|
52
|
+
required: false
|
|
53
|
+
|
|
54
|
+
param :multiline,
|
|
55
|
+
type: "boolean",
|
|
56
|
+
desc: "Enable multiline matching",
|
|
57
|
+
required: false
|
|
58
|
+
|
|
59
|
+
param :context_before,
|
|
60
|
+
type: "integer",
|
|
61
|
+
desc: "Lines to show before each match (content mode only)",
|
|
62
|
+
required: false
|
|
63
|
+
|
|
64
|
+
param :context_after,
|
|
65
|
+
type: "integer",
|
|
66
|
+
desc: "Lines to show after each match (content mode only)",
|
|
67
|
+
required: false
|
|
68
|
+
|
|
69
|
+
param :context,
|
|
70
|
+
type: "integer",
|
|
71
|
+
desc: "Lines to show before and after each match (content mode only)",
|
|
72
|
+
required: false
|
|
73
|
+
|
|
74
|
+
param :show_line_numbers,
|
|
75
|
+
type: "boolean",
|
|
76
|
+
desc: "Show line numbers (content mode only)",
|
|
77
|
+
required: false
|
|
78
|
+
|
|
79
|
+
param :head_limit,
|
|
80
|
+
type: "integer",
|
|
81
|
+
desc: "Limit output to first N lines/entries",
|
|
82
|
+
required: false
|
|
83
|
+
|
|
84
|
+
# @param directory [String] Working directory for searches
|
|
85
|
+
def initialize(directory:)
|
|
86
|
+
super()
|
|
87
|
+
@directory = File.expand_path(directory)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Execute content search
|
|
91
|
+
#
|
|
92
|
+
# @return [String] Search results or error
|
|
93
|
+
def execute(
|
|
94
|
+
pattern:,
|
|
95
|
+
path: nil,
|
|
96
|
+
glob: nil,
|
|
97
|
+
type: nil,
|
|
98
|
+
output_mode: "files_with_matches",
|
|
99
|
+
case_insensitive: false,
|
|
100
|
+
multiline: false,
|
|
101
|
+
context_before: nil,
|
|
102
|
+
context_after: nil,
|
|
103
|
+
context: nil,
|
|
104
|
+
show_line_numbers: false,
|
|
105
|
+
head_limit: nil
|
|
106
|
+
)
|
|
107
|
+
return validation_error("pattern is required") if pattern.nil? || pattern.empty?
|
|
108
|
+
|
|
109
|
+
search_path = resolve_search_path(path)
|
|
110
|
+
|
|
111
|
+
valid_modes = ["content", "files_with_matches", "count"]
|
|
112
|
+
return validation_error("output_mode must be one of: #{valid_modes.join(", ")}") unless valid_modes.include?(output_mode)
|
|
113
|
+
|
|
114
|
+
cmd = build_command(
|
|
115
|
+
pattern: pattern,
|
|
116
|
+
path: search_path,
|
|
117
|
+
glob: glob,
|
|
118
|
+
type: type,
|
|
119
|
+
output_mode: output_mode,
|
|
120
|
+
case_insensitive: case_insensitive,
|
|
121
|
+
multiline: multiline,
|
|
122
|
+
context_before: context_before,
|
|
123
|
+
context_after: context_after,
|
|
124
|
+
context: context,
|
|
125
|
+
show_line_numbers: show_line_numbers,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
run_ripgrep(cmd, pattern, head_limit)
|
|
129
|
+
rescue StandardError => e
|
|
130
|
+
error("Unexpected error: #{e.class.name} - #{e.message}")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
# Resolve the search path
|
|
136
|
+
#
|
|
137
|
+
# @param path [String, nil] User-provided path
|
|
138
|
+
# @return [String] Resolved absolute path
|
|
139
|
+
def resolve_search_path(path)
|
|
140
|
+
if path.nil? || path.to_s.strip.empty?
|
|
141
|
+
@directory
|
|
142
|
+
else
|
|
143
|
+
resolve_path(path)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Build ripgrep command array
|
|
148
|
+
#
|
|
149
|
+
# @return [Array<String>] Command parts
|
|
150
|
+
def build_command(pattern:, path:, glob:, type:, output_mode:, case_insensitive:, multiline:, context_before:, context_after:, context:, show_line_numbers:)
|
|
151
|
+
cmd = ["rg"]
|
|
152
|
+
|
|
153
|
+
case output_mode
|
|
154
|
+
when "files_with_matches" then cmd << "-l"
|
|
155
|
+
when "count" then cmd << "-c"
|
|
156
|
+
when "content"
|
|
157
|
+
cmd << "-n" if show_line_numbers
|
|
158
|
+
cmd << "-B" << context_before.to_s if context_before
|
|
159
|
+
cmd << "-A" << context_after.to_s if context_after
|
|
160
|
+
cmd << "-C" << context.to_s if context
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
cmd << "-i" if case_insensitive
|
|
164
|
+
cmd.push("-U", "--multiline-dotall") if multiline
|
|
165
|
+
cmd.push("--type", type) if type && !type.to_s.strip.empty?
|
|
166
|
+
cmd.push("--glob", glob) if glob && !glob.to_s.strip.empty?
|
|
167
|
+
cmd.push("-e", pattern, path)
|
|
168
|
+
|
|
169
|
+
cmd
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Run ripgrep and format output
|
|
173
|
+
#
|
|
174
|
+
# @param cmd [Array<String>] Command parts
|
|
175
|
+
# @param pattern [String] Search pattern
|
|
176
|
+
# @param head_limit [Integer, nil] Output limit
|
|
177
|
+
# @return [String] Formatted results
|
|
178
|
+
def run_ripgrep(cmd, pattern, head_limit)
|
|
179
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
180
|
+
|
|
181
|
+
return "No matches found for pattern: #{pattern}" if status.exitstatus == 1 && stderr.empty?
|
|
182
|
+
return error("ripgrep error: #{stderr}") if status.exitstatus == 2 || !stderr.empty?
|
|
183
|
+
|
|
184
|
+
output = stdout
|
|
185
|
+
if head_limit && head_limit > 0
|
|
186
|
+
lines = output.lines
|
|
187
|
+
if lines.count > head_limit
|
|
188
|
+
output = lines.take(head_limit).join
|
|
189
|
+
output += "\n<system-reminder>Output limited to first #{head_limit} lines.</system-reminder>"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
output.empty? ? "No matches found for pattern: #{pattern}" : output
|
|
194
|
+
rescue Errno::ENOENT
|
|
195
|
+
error("ripgrep (rg) is not installed or not in PATH.")
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
# Placeholder for inter-agent messaging (future hub integration)
|
|
7
|
+
#
|
|
8
|
+
# This tool will be implemented when the MessageHub primitive is built.
|
|
9
|
+
# For now it exists as a namespace placeholder.
|
|
10
|
+
class MessageTeammate < Base
|
|
11
|
+
# Not yet implemented — placeholder for hub integration
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
# Placeholder for user-facing messaging (future hub integration)
|
|
7
|
+
#
|
|
8
|
+
# This tool will be implemented when the MessageHub primitive is built.
|
|
9
|
+
# For now it exists as a namespace placeholder.
|
|
10
|
+
class MessageUser < Base
|
|
11
|
+
# Not yet implemented — placeholder for hub integration
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
# Read tool for reading file contents from the filesystem
|
|
7
|
+
#
|
|
8
|
+
# Supports reading entire files or specific line ranges with line numbers.
|
|
9
|
+
# Tracks reads per agent for enforcing read-before-write/edit rules.
|
|
10
|
+
class Read < Base
|
|
11
|
+
class << self
|
|
12
|
+
# @return [Array<Symbol>] Constructor requirements
|
|
13
|
+
def creation_requirements
|
|
14
|
+
[:agent_name, :directory, :read_tracker]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
description <<~DESC
|
|
19
|
+
Reads a file from the local filesystem.
|
|
20
|
+
|
|
21
|
+
Supports text files with line numbers. Binary files (images) are returned as visual content.
|
|
22
|
+
|
|
23
|
+
Path handling:
|
|
24
|
+
- Relative paths resolve against your working directory
|
|
25
|
+
- Absolute paths (starting with /) are used as-is
|
|
26
|
+
DESC
|
|
27
|
+
|
|
28
|
+
param :file_path,
|
|
29
|
+
type: "string",
|
|
30
|
+
desc: "Path to the file to read",
|
|
31
|
+
required: true
|
|
32
|
+
|
|
33
|
+
param :offset,
|
|
34
|
+
type: "integer",
|
|
35
|
+
desc: "Line number to start reading from (1-indexed). Use for large files.",
|
|
36
|
+
required: false
|
|
37
|
+
|
|
38
|
+
param :limit,
|
|
39
|
+
type: "integer",
|
|
40
|
+
desc: "Number of lines to read. Use for large files.",
|
|
41
|
+
required: false
|
|
42
|
+
|
|
43
|
+
# @param agent_name [Symbol, String] Agent identifier for read tracking
|
|
44
|
+
# @param directory [String] Agent's working directory
|
|
45
|
+
# @param read_tracker [ReadTracker] Shared read tracker for cross-tool enforcement
|
|
46
|
+
def initialize(agent_name:, directory:, read_tracker:)
|
|
47
|
+
super()
|
|
48
|
+
@agent_name = agent_name.to_sym
|
|
49
|
+
@directory = File.expand_path(directory)
|
|
50
|
+
@read_tracker = read_tracker
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if a file has been read by this agent
|
|
54
|
+
#
|
|
55
|
+
# @param path [String] Resolved file path
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def file_read?(path)
|
|
58
|
+
@read_tracker.file_read?(@agent_name, path)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Execute file read
|
|
62
|
+
#
|
|
63
|
+
# @param file_path [String] Path to the file
|
|
64
|
+
# @param offset [Integer, nil] Starting line number (1-indexed)
|
|
65
|
+
# @param limit [Integer, nil] Number of lines to read
|
|
66
|
+
# @return [String, RubyLLM::Content] File contents or error message
|
|
67
|
+
def execute(file_path:, offset: nil, limit: nil)
|
|
68
|
+
return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
|
|
69
|
+
|
|
70
|
+
resolved_path = resolve_path(file_path)
|
|
71
|
+
|
|
72
|
+
return validation_error("File does not exist: #{file_path}") unless File.exist?(resolved_path)
|
|
73
|
+
return validation_error("Path is a directory. Use Bash with ls to list directories.") if File.directory?(resolved_path)
|
|
74
|
+
|
|
75
|
+
content = read_file_content(resolved_path)
|
|
76
|
+
|
|
77
|
+
# Binary file — return as-is
|
|
78
|
+
return content if content.is_a?(RubyLLM::Content)
|
|
79
|
+
return content if content.start_with?("Error:")
|
|
80
|
+
|
|
81
|
+
@read_tracker.register_read(@agent_name, resolved_path)
|
|
82
|
+
|
|
83
|
+
return format_empty_file if content.empty?
|
|
84
|
+
|
|
85
|
+
format_text_content(content, file_path, offset, limit)
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
error("Unexpected error reading file: #{e.class.name} - #{e.message}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# @return [String] Config accessor
|
|
93
|
+
def config
|
|
94
|
+
Configuration.instance
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Read file content, handling encoding
|
|
98
|
+
#
|
|
99
|
+
# @param file_path [String] Resolved absolute path
|
|
100
|
+
# @return [String, RubyLLM::Content] Text content or binary content object
|
|
101
|
+
def read_file_content(file_path)
|
|
102
|
+
content = File.read(file_path, encoding: "UTF-8")
|
|
103
|
+
|
|
104
|
+
unless content.valid_encoding?
|
|
105
|
+
return binary_or_unsupported(file_path)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
content
|
|
109
|
+
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
|
110
|
+
binary_or_unsupported(file_path)
|
|
111
|
+
rescue Errno::EACCES
|
|
112
|
+
error("Permission denied: Cannot read file '#{file_path}'")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Handle binary files
|
|
116
|
+
#
|
|
117
|
+
# @param file_path [String] Path to binary file
|
|
118
|
+
# @return [RubyLLM::Content, String] Content object for images, error for others
|
|
119
|
+
def binary_or_unsupported(file_path)
|
|
120
|
+
ext = File.extname(file_path).downcase
|
|
121
|
+
image_formats = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".tif", ".svg", ".ico"]
|
|
122
|
+
|
|
123
|
+
if image_formats.include?(ext)
|
|
124
|
+
RubyLLM::Content.new("File: #{File.basename(file_path)}", file_path)
|
|
125
|
+
else
|
|
126
|
+
"Error: File contains binary data and cannot be displayed as text."
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Format empty file output
|
|
131
|
+
#
|
|
132
|
+
# @return [String]
|
|
133
|
+
def format_empty_file
|
|
134
|
+
"<system-reminder>Warning: This file exists but has empty contents.</system-reminder>"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Format text content with line numbers
|
|
138
|
+
#
|
|
139
|
+
# @param content [String] Raw file content
|
|
140
|
+
# @param file_path [String] Original file path for display
|
|
141
|
+
# @param offset [Integer, nil] Starting line (1-indexed)
|
|
142
|
+
# @param limit [Integer, nil] Lines to read
|
|
143
|
+
# @return [String] Formatted output with line numbers
|
|
144
|
+
def format_text_content(content, file_path, offset, limit)
|
|
145
|
+
lines = content.lines
|
|
146
|
+
total_lines = lines.count
|
|
147
|
+
|
|
148
|
+
start_line = offset ? [offset - 1, 0].max : 0
|
|
149
|
+
return validation_error("Offset #{offset} exceeds file length (#{total_lines} lines)") if start_line >= total_lines
|
|
150
|
+
|
|
151
|
+
lines = lines.drop(start_line)
|
|
152
|
+
|
|
153
|
+
default_limit = config.read_line_limit
|
|
154
|
+
effective_limit = limit || default_limit
|
|
155
|
+
lines = lines.take(effective_limit)
|
|
156
|
+
truncated = limit.nil? && total_lines > default_limit
|
|
157
|
+
|
|
158
|
+
max_line_length = config.line_character_limit
|
|
159
|
+
output_lines = lines.each_with_index.map do |line, idx|
|
|
160
|
+
line_number = start_line + idx + 1
|
|
161
|
+
display_line = line.chomp
|
|
162
|
+
display_line = "#{display_line[0...max_line_length]}... (line truncated)" if display_line.length > max_line_length
|
|
163
|
+
"#{line_number.to_s.rjust(6)}\t#{display_line}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
output = output_lines.join("\n")
|
|
167
|
+
output += truncation_notice(total_lines, default_limit) if truncated
|
|
168
|
+
output
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# @param total_lines [Integer] Total lines in file
|
|
172
|
+
# @param limit [Integer] Applied limit
|
|
173
|
+
# @return [String] Truncation notice
|
|
174
|
+
def truncation_notice(total_lines, limit)
|
|
175
|
+
"\n\n<system-reminder>This file has #{total_lines} lines but only the first #{limit} are shown. " \
|
|
176
|
+
"Use offset and limit parameters to read more.</system-reminder>"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
# Tracks which files have been read by each agent
|
|
7
|
+
#
|
|
8
|
+
# Shared across Read, Write, and Edit tool instances for the same agent.
|
|
9
|
+
# Enforces read-before-write/edit rules to prevent accidental overwrites.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# tracker = ReadTracker.new
|
|
13
|
+
# tracker.register_read(:backend, "/path/to/file.rb")
|
|
14
|
+
# tracker.file_read?(:backend, "/path/to/file.rb") #=> true
|
|
15
|
+
class ReadTracker
|
|
16
|
+
def initialize
|
|
17
|
+
@reads = Hash.new { |h, k| h[k] = Set.new }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Register that an agent has read a file
|
|
21
|
+
#
|
|
22
|
+
# @param agent_name [Symbol] Agent identifier
|
|
23
|
+
# @param path [String] Absolute file path
|
|
24
|
+
# @return [void]
|
|
25
|
+
def register_read(agent_name, path)
|
|
26
|
+
@reads[agent_name].add(path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if an agent has read a file
|
|
30
|
+
#
|
|
31
|
+
# @param agent_name [Symbol] Agent identifier
|
|
32
|
+
# @param path [String] Absolute file path
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
def file_read?(agent_name, path)
|
|
35
|
+
@reads[agent_name].include?(path)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|